feat: improve api

This commit is contained in:
Acbox
2026-01-10 22:18:50 +08:00
parent 661d742750
commit fee657ddd2
22 changed files with 655 additions and 228 deletions
+2 -31
View File
@@ -1,6 +1,5 @@
import Elysia from 'elysia'
import { bearer } from '@elysiajs/bearer'
import { jwt } from '@elysiajs/jwt'
import { authMiddleware } from '../../middlewares/auth'
import { AgentStreamModel } from './model'
import { createAgentStream } from './service'
import { getChatModel, getEmbeddingModel, getSummaryModel } from '../model/service'
@@ -10,35 +9,7 @@ import { ChatModel, EmbeddingModel } from '@memohome/shared'
export const agentModule = new Elysia({
prefix: '/agent',
})
.use(
jwt({
name: 'jwt',
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
exp: process.env.JWT_EXPIRES_IN || '7d',
})
)
.use(bearer())
.derive(async ({ bearer, jwt, set }) => {
if (!bearer) {
set.status = 401
throw new Error('No bearer token provided')
}
const payload = await jwt.verify(bearer)
if (!payload) {
set.status = 401
throw new Error('Invalid or expired token')
}
return {
user: {
userId: payload.userId as string,
username: payload.username as string,
role: payload.role as string,
},
}
})
.use(authMiddleware)
// Stream agent conversation
.post('/stream', async ({ user, body, set }) => {
try {
+2 -10
View File
@@ -1,20 +1,12 @@
import Elysia from 'elysia'
import { bearer } from '@elysiajs/bearer'
import { jwt } from '@elysiajs/jwt'
import { jwtPlugin } from '../../middlewares/auth'
import { LoginModel } from './model'
import { validateUser } from './service'
export const authModule = new Elysia({
prefix: '/auth',
})
.use(
jwt({
name: 'jwt',
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
exp: process.env.JWT_EXPIRES_IN || '7d',
})
)
.use(bearer())
.use(jwtPlugin)
// Login endpoint
.post('/login', async ({ body, jwt, set }) => {
try {
+1 -1
View File
@@ -63,7 +63,7 @@ export const validateUser = async (username: string, password: string) => {
}
// 检查账户是否激活
if (user.isActive !== 'true') {
if (!user.isActive) {
return null
}
+2 -31
View File
@@ -1,6 +1,5 @@
import Elysia from 'elysia'
import { bearer } from '@elysiajs/bearer'
import { jwt } from '@elysiajs/jwt'
import { authMiddleware } from '../../middlewares/auth'
import { messageModule } from './message'
import { AddMemoryModel, SearchMemoryModel } from './model'
import { addMemory, searchMemory } from './service'
@@ -9,35 +8,7 @@ import { MemoryUnit } from '@memohome/memory'
export const memoryModule = new Elysia({
prefix: '/memory',
})
.use(
jwt({
name: 'jwt',
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
exp: process.env.JWT_EXPIRES_IN || '7d',
})
)
.use(bearer())
.derive(async ({ bearer, jwt, set }) => {
if (!bearer) {
set.status = 401
throw new Error('No bearer token provided')
}
const payload = await jwt.verify(bearer)
if (!payload) {
set.status = 401
throw new Error('Invalid or expired token')
}
return {
user: {
userId: payload.userId as string,
username: payload.username as string,
role: payload.role as string,
},
}
})
.use(authMiddleware)
.use(messageModule)
// Add memory for current user
.post('/', async ({ user, body, set }) => {
@@ -1,20 +1,12 @@
import Elysia from 'elysia'
import { bearer } from '@elysiajs/bearer'
import { jwt } from '@elysiajs/jwt'
import { authMiddleware } from '../../../middlewares/auth'
import { GetMemoryMessageFilterModel, GetMemoryMessageModel } from './model'
import { getMemoryMessages, getMemoryMessagesFilter } from './service'
export const messageModule = new Elysia({
prefix: '/message',
})
.use(
jwt({
name: 'jwt',
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
exp: process.env.JWT_EXPIRES_IN || '7d',
})
)
.use(bearer())
.use(authMiddleware)
.derive(async ({ bearer, jwt, set }) => {
if (!bearer) {
set.status = 401
+85 -62
View File
@@ -1,4 +1,5 @@
import Elysia from 'elysia'
import { adminMiddleware, optionalAuthMiddleware } from '../../middlewares/auth'
import {
CreateModelModel,
UpdateModelModel,
@@ -21,13 +22,24 @@ import { Model } from '@memohome/shared'
export const modelModule = new Elysia({
prefix: '/model',
})
// 公开的读取接口
.use(optionalAuthMiddleware)
// Get all models
.get('/', async () => {
.get('/', async ({ query }) => {
try {
const models = await getModels()
const page = parseInt(query.page as string) || 1
const limit = parseInt(query.limit as string) || 10
const sortOrder = (query.sortOrder as string) || 'desc'
const result = await getModels({
page,
limit,
sortOrder: sortOrder as 'asc' | 'desc',
})
return {
success: true,
data: models,
...result,
}
} catch (error) {
return {
@@ -58,65 +70,6 @@ export const modelModule = new Elysia({
}
}
}, GetModelByIdModel)
// Create new model
.post('/', async ({ body }) => {
try {
const newModel = await createModel(body as Model)
return {
success: true,
data: newModel,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to create model',
}
}
}, CreateModelModel)
// Update model
.put('/:id', async ({ params, body }) => {
try {
const { id } = params
const updatedModel = await updateModel(id, body as Model)
if (!updatedModel) {
return {
success: false,
error: 'Model not found',
}
}
return {
success: true,
data: updatedModel,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to update model',
}
}
}, UpdateModelModel)
// Delete model
.delete('/:id', async ({ params }) => {
try {
const { id } = params
const deletedModel = await deleteModel(id)
if (!deletedModel) {
return {
success: false,
error: 'Model not found',
}
}
return {
success: true,
data: deletedModel,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to delete model',
}
}
}, DeleteModelModel)
// Get default chat model
.get('/chat/default', async ({ query }) => {
try {
@@ -183,3 +136,73 @@ export const modelModule = new Elysia({
}
}
}, GetDefaultModelModel)
// 管理员权限的写入接口
.guard(
{
beforeHandle: () => {
// This will be overridden by adminMiddleware
},
},
(app) =>
app
.use(adminMiddleware)
// Create new model
.post('/', async ({ body }) => {
try {
const newModel = await createModel(body as Model)
return {
success: true,
data: newModel,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to create model',
}
}
}, CreateModelModel)
// Update model
.put('/:id', async ({ params, body }) => {
try {
const { id } = params
const updatedModel = await updateModel(id, body as Model)
if (!updatedModel) {
return {
success: false,
error: 'Model not found',
}
}
return {
success: true,
data: updatedModel,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to update model',
}
}
}, UpdateModelModel)
// Delete model
.delete('/:id', async ({ params }) => {
try {
const { id } = params
const deletedModel = await deleteModel(id)
if (!deletedModel) {
return {
success: false,
error: 'Model not found',
}
}
return {
success: true,
data: deletedModel,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to delete model',
}
}
}, DeleteModelModel)
)
+35 -4
View File
@@ -1,12 +1,43 @@
import { db } from '@memohome/db'
import { model } from '@memohome/db/schema'
import { Model } from '@memohome/shared'
import { eq } from 'drizzle-orm'
import { eq, sql, desc, asc } from 'drizzle-orm'
import { getSettings } from '@/modules/settings/service'
import { calculateOffset, createPaginatedResult, type PaginatedResult } from '../../utils/pagination'
export const getModels = async () => {
const models = await db.select().from(model)
return models
/**
* 模型列表返回类型
*/
type ModelListItem = {
id: string
model: Model
}
export const getModels = async (params?: {
page?: number
limit?: number
sortOrder?: 'asc' | 'desc'
}): Promise<PaginatedResult<ModelListItem>> => {
const page = params?.page || 1
const limit = params?.limit || 10
const sortOrder = params?.sortOrder || 'desc'
const offset = calculateOffset(page, limit)
// 获取总数
const [{ count }] = await db
.select({ count: sql<number>`count(*)` })
.from(model)
// 获取分页数据(按 id 排序,因为 model 表没有 createdAt
const orderFn = sortOrder === 'desc' ? desc : asc
const models = await db
.select()
.from(model)
.orderBy(orderFn(model.id))
.limit(limit)
.offset(offset)
return createPaginatedResult(models, Number(count), page, limit)
}
export const getModelById = async (id: string) => {
+2 -31
View File
@@ -1,41 +1,12 @@
import Elysia from 'elysia'
import { bearer } from '@elysiajs/bearer'
import { jwt } from '@elysiajs/jwt'
import { authMiddleware } from '../../middlewares/auth'
import { UpdateSettingsModel } from './model'
import { getSettings, upsertSettings } from './service'
export const settingsModule = new Elysia({
prefix: '/settings',
})
.use(
jwt({
name: 'jwt',
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
exp: process.env.JWT_EXPIRES_IN || '7d',
})
)
.use(bearer())
.derive(async ({ bearer, jwt, set }) => {
if (!bearer) {
set.status = 401
throw new Error('No bearer token provided')
}
const payload = await jwt.verify(bearer)
if (!payload) {
set.status = 401
throw new Error('Invalid or expired token')
}
return {
user: {
userId: payload.userId as string,
username: payload.username as string,
role: payload.role as string,
},
}
})
.use(authMiddleware)
// Get current user's settings
.get('/', async ({ user, set }) => {
try {
+14 -3
View File
@@ -22,12 +22,23 @@ export const userModule = new Elysia({
// 使用管理员中间件保护所有路由
.use(adminMiddleware)
// Get all users
.get('/', async () => {
.get('/', async ({ query }) => {
try {
const userList = await getUsers()
const page = parseInt(query.page as string) || 1
const limit = parseInt(query.limit as string) || 10
const sortBy = query.sortBy as string || 'createdAt'
const sortOrder = (query.sortOrder as string) || 'desc'
const result = await getUsers({
page,
limit,
sortBy,
sortOrder: sortOrder as 'asc' | 'desc',
})
return {
success: true,
data: userList,
...result,
}
} catch (error) {
return {
+1 -1
View File
@@ -19,7 +19,7 @@ const UpdateUserSchema = z.object({
role: UserRoleSchema.optional(),
displayName: z.string().optional(),
avatarUrl: z.string().url('Invalid URL format').optional(),
isActive: z.enum(['true', 'false']).optional(),
isActive: z.boolean().optional(),
})
// 更新密码的 Schema
+50 -5
View File
@@ -1,12 +1,55 @@
import { db } from '@memohome/db'
import { users, settings } from '@memohome/db/schema'
import { eq } from 'drizzle-orm'
import { eq, sql, desc, asc } from 'drizzle-orm'
import type { CreateUserInput, UpdateUserInput } from './model'
import { calculateOffset, createPaginatedResult, type PaginatedResult } from '../../utils/pagination'
/**
*
*
*/
export const getUsers = async () => {
type UserListItem = {
id: string
username: string
email: string | null
role: 'admin' | 'member'
displayName: string | null
avatarUrl: string | null
isActive: boolean
createdAt: Date
updatedAt: Date
lastLoginAt: Date | null
}
/**
*
*/
export const getUsers = async (params?: {
page?: number
limit?: number
sortBy?: string
sortOrder?: 'asc' | 'desc'
}): Promise<PaginatedResult<UserListItem>> => {
const page = params?.page || 1
const limit = params?.limit || 10
const sortBy = params?.sortBy || 'createdAt'
const sortOrder = params?.sortOrder || 'desc'
const offset = calculateOffset(page, limit)
// 获取总数
const [{ count }] = await db
.select({ count: sql<number>`count(*)` })
.from(users)
// 动态排序
const orderColumn = sortBy === 'username' ? users.username :
sortBy === 'email' ? users.email :
sortBy === 'role' ? users.role :
sortBy === 'updatedAt' ? users.updatedAt :
users.createdAt
const orderFn = sortOrder === 'desc' ? desc : asc
// 获取分页数据
const userList = await db
.select({
id: users.id,
@@ -21,9 +64,11 @@ export const getUsers = async () => {
lastLoginAt: users.lastLoginAt,
})
.from(users)
.orderBy(users.createdAt)
.orderBy(orderFn(orderColumn))
.limit(limit)
.offset(offset)
return userList
return createPaginatedResult(userList, Number(count), page, limit)
}
/**