mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: improve api
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -63,7 +63,7 @@ export const validateUser = async (username: string, password: string) => {
|
||||
}
|
||||
|
||||
// 检查账户是否激活
|
||||
if (user.isActive !== 'true') {
|
||||
if (!user.isActive) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user