mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
refactor: remove all ts packages will no longer be used
This commit is contained in:
@@ -1,111 +0,0 @@
|
||||
import Elysia from 'elysia'
|
||||
import { authMiddleware } from '../../middlewares/auth'
|
||||
import { AgentStreamModel } from './model'
|
||||
import { createAgent } from './service'
|
||||
import { getChatModel, getEmbeddingModel, getSummaryModel } from '../model/service'
|
||||
import { getSettings } from '../settings/service'
|
||||
import { ChatModel, EmbeddingModel } from '@memoh/shared'
|
||||
|
||||
export const agentModule = new Elysia({
|
||||
prefix: '/agent',
|
||||
})
|
||||
.use(authMiddleware)
|
||||
// Stream agent conversation
|
||||
.post('/stream', async ({ user, body, set }) => {
|
||||
try {
|
||||
// Get user's model configurations and settings
|
||||
const [chatModel, embeddingModel, summaryModel, userSettings] = await Promise.all([
|
||||
getChatModel(user.userId),
|
||||
getEmbeddingModel(user.userId),
|
||||
getSummaryModel(user.userId),
|
||||
getSettings(user.userId),
|
||||
])
|
||||
|
||||
if (!chatModel || !embeddingModel || !summaryModel) {
|
||||
set.status = 400
|
||||
return {
|
||||
success: false,
|
||||
error: 'Model configuration not found. Please configure your models in settings.',
|
||||
}
|
||||
}
|
||||
|
||||
// Use body params if provided, otherwise use settings, otherwise use defaults
|
||||
const maxContextLoadTime = body.maxContextLoadTime
|
||||
?? userSettings?.maxContextLoadTime
|
||||
?? 60
|
||||
const language = body.language
|
||||
?? userSettings?.language
|
||||
?? 'Same as user input'
|
||||
|
||||
// Create agent
|
||||
const agent = await createAgent({
|
||||
userId: user.userId,
|
||||
chatModel: chatModel.model as ChatModel,
|
||||
embeddingModel: embeddingModel.model as EmbeddingModel,
|
||||
summaryModel: summaryModel.model as ChatModel,
|
||||
maxContextLoadTime,
|
||||
language,
|
||||
})
|
||||
|
||||
// Set headers for Server-Sent Events
|
||||
set.headers['Content-Type'] = 'text/event-stream'
|
||||
set.headers['Cache-Control'] = 'no-cache'
|
||||
set.headers['Connection'] = 'keep-alive'
|
||||
|
||||
// Create a stream
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
try {
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
console.log('📨 [API] Starting agent stream for message:', body.message.substring(0, 50))
|
||||
console.log('🔗 [API] Starting event loop...')
|
||||
|
||||
let eventCount = 0
|
||||
// Send events as they come
|
||||
for await (const event of agent.ask(body.message)) {
|
||||
eventCount++
|
||||
console.log(`📤 [API] Received event #${eventCount}, type:`, event.type)
|
||||
const data = JSON.stringify(event)
|
||||
console.log(`📤 [API] Enqueueing event #${eventCount}...`)
|
||||
controller.enqueue(encoder.encode(`data: ${data}\n\n`))
|
||||
console.log(`✅ [API] Event #${eventCount} enqueued successfully`)
|
||||
}
|
||||
|
||||
console.log(`✅ [API] Agent stream completed successfully (${eventCount} events)`)
|
||||
|
||||
// Send done event
|
||||
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
|
||||
controller.close()
|
||||
} catch (error) {
|
||||
console.error('❌ Error in agent stream:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
const errorStack = error instanceof Error ? error.stack : undefined
|
||||
console.error('Error stack:', errorStack)
|
||||
|
||||
const errorData = JSON.stringify({
|
||||
type: 'error',
|
||||
error: errorMessage
|
||||
})
|
||||
controller.enqueue(new TextEncoder().encode(`data: ${errorData}\n\n`))
|
||||
controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n'))
|
||||
controller.close()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to process request',
|
||||
}
|
||||
}
|
||||
}, AgentStreamModel)
|
||||
@@ -1,14 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const AgentStreamModel = {
|
||||
body: z.object({
|
||||
message: z.string().min(1, 'Message is required'),
|
||||
// Optional overrides - if not provided, will use settings
|
||||
maxContextLoadTime: z.number().int().min(1).max(1440).optional(),
|
||||
language: z.string().optional(),
|
||||
platform: z.string().optional(),
|
||||
}),
|
||||
}
|
||||
|
||||
export type AgentStreamInput = z.infer<typeof AgentStreamModel['body']>
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import { createAgent as createAgentService } from '@memoh/agent'
|
||||
import { createMemory, filterByTimestamp, MemoryUnit } from '@memoh/memory'
|
||||
import { ChatModel, EmbeddingModel, Platform, Schedule } from '@memoh/shared'
|
||||
import { useContainer } from '@memoh/container'
|
||||
import { createSchedule, deleteSchedule, getActiveSchedules } from '../schedule/service'
|
||||
import { getActivePlatforms, sendMessageToPlatform } from '../platform/service'
|
||||
import { getActiveMCPConnections } from '../mcp/service'
|
||||
import { getUserContainerInfo } from '../container/service'
|
||||
|
||||
// Type for messages passed to onFinish callback
|
||||
type MessageType = Record<string, unknown>
|
||||
|
||||
export interface CreateAgentStreamParams {
|
||||
userId: string
|
||||
chatModel: ChatModel
|
||||
embeddingModel: EmbeddingModel
|
||||
summaryModel: ChatModel
|
||||
maxContextLoadTime?: number
|
||||
language?: string
|
||||
platform?: string
|
||||
onFinish?: (messages: MessageType[]) => Promise<void>
|
||||
}
|
||||
|
||||
export async function createAgent(params: CreateAgentStreamParams) {
|
||||
const {
|
||||
userId,
|
||||
chatModel,
|
||||
embeddingModel,
|
||||
summaryModel,
|
||||
maxContextLoadTime,
|
||||
language,
|
||||
platform,
|
||||
onFinish,
|
||||
} = params
|
||||
|
||||
// Create memory instance
|
||||
const memoryInstance = createMemory({
|
||||
summaryModel,
|
||||
embeddingModel,
|
||||
})
|
||||
|
||||
const platforms = await getActivePlatforms()
|
||||
const mcpConnections = await getActiveMCPConnections(userId)
|
||||
const containerInfo = await getUserContainerInfo(userId)
|
||||
if (!containerInfo) {
|
||||
throw new Error('Container not found')
|
||||
}
|
||||
|
||||
// Ensure container is running before creating agent
|
||||
const container = useContainer(containerInfo.containerName, {
|
||||
namespace: containerInfo.namespace,
|
||||
socket: process.env.CONTAINERD_SOCKET,
|
||||
})
|
||||
|
||||
// Check and start container if not running
|
||||
const info = await container.info()
|
||||
if (info.status !== 'running') {
|
||||
console.log(`🚀 Starting container ${containerInfo.containerName} for agent...`)
|
||||
await container.start()
|
||||
// Wait a bit for container to be fully ready
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
}
|
||||
// Create agent
|
||||
const agent = createAgentService({
|
||||
model: chatModel,
|
||||
maxContextLoadTime,
|
||||
language: language || 'Same as user input',
|
||||
platforms: platforms as Platform[],
|
||||
currentPlatform: platform,
|
||||
mcpConnections,
|
||||
onSendMessage: async (platform: string, options) => {
|
||||
await sendMessageToPlatform(platform, {
|
||||
message: options.message,
|
||||
userId,
|
||||
})
|
||||
},
|
||||
onReadMemory: async (from: Date, to: Date) => {
|
||||
return await filterByTimestamp(from, to, userId)
|
||||
},
|
||||
onSearchMemory: async (query: string) => {
|
||||
const results = await memoryInstance.searchMemory(query, userId)
|
||||
return results
|
||||
},
|
||||
onFinish: async (messages: MessageType[]) => {
|
||||
// Save conversation to memory
|
||||
const memoryUnit: MemoryUnit = {
|
||||
messages: messages as unknown as MemoryUnit['messages'],
|
||||
timestamp: new Date(),
|
||||
user: userId,
|
||||
}
|
||||
await memoryInstance.addMemory(memoryUnit)
|
||||
|
||||
// Call custom onFinish handler if provided
|
||||
await onFinish?.(messages)
|
||||
},
|
||||
onGetSchedules: async () => {
|
||||
const schedules = await getActiveSchedules(userId)
|
||||
return schedules.map(schedule => ({
|
||||
id: schedule.id!,
|
||||
pattern: schedule.pattern,
|
||||
name: schedule.name,
|
||||
description: schedule.description,
|
||||
command: schedule.command,
|
||||
maxCalls: schedule.maxCalls || undefined,
|
||||
}))
|
||||
},
|
||||
onRemoveSchedule: async (id: string) => {
|
||||
await deleteSchedule(id, userId)
|
||||
},
|
||||
onSchedule: async (schedule: Schedule) => {
|
||||
await createSchedule(userId, {
|
||||
name: schedule.name,
|
||||
description: schedule.description,
|
||||
command: schedule.command,
|
||||
pattern: schedule.pattern,
|
||||
maxCalls: schedule.maxCalls || undefined,
|
||||
})
|
||||
},
|
||||
onBuildExecCommand(command) {
|
||||
return container.buildExecCommand(command)
|
||||
},
|
||||
async onExecCommand(command) {
|
||||
return await container.exec(command)
|
||||
},
|
||||
})
|
||||
|
||||
return agent
|
||||
}
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import Elysia from 'elysia'
|
||||
import { jwtPlugin } from '../../middlewares/auth'
|
||||
import { LoginModel } from './model'
|
||||
import { validateUser } from './service'
|
||||
|
||||
export const authModule = new Elysia({
|
||||
prefix: '/auth',
|
||||
})
|
||||
.use(jwtPlugin)
|
||||
// Login endpoint
|
||||
.post('/login', async ({ body, jwt, set }) => {
|
||||
try {
|
||||
const user = await validateUser(body.username, body.password)
|
||||
|
||||
if (!user) {
|
||||
set.status = 401
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid username or password',
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 JWT 插件生成 token
|
||||
const token = await jwt.sign({
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
displayName: user.displayName,
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to login',
|
||||
}
|
||||
}
|
||||
}, LoginModel)
|
||||
// Verify token endpoint
|
||||
.get('/verify', async ({ bearer, jwt, set }) => {
|
||||
try {
|
||||
if (!bearer) {
|
||||
set.status = 401
|
||||
return {
|
||||
success: false,
|
||||
error: 'No bearer token provided',
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 JWT 插件验证 token
|
||||
const payload = await jwt.verify(bearer)
|
||||
|
||||
if (!payload) {
|
||||
set.status = 401
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid or expired token',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
userId: payload.userId,
|
||||
username: payload.username,
|
||||
role: payload.role,
|
||||
},
|
||||
}
|
||||
} catch {
|
||||
set.status = 401
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid or expired token',
|
||||
}
|
||||
}
|
||||
})
|
||||
// Get current user info
|
||||
.get('/me', async ({ bearer, jwt, set }) => {
|
||||
try {
|
||||
if (!bearer) {
|
||||
set.status = 401
|
||||
return {
|
||||
success: false,
|
||||
error: 'No bearer token provided',
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 JWT 插件验证 token
|
||||
const payload = await jwt.verify(bearer)
|
||||
|
||||
if (!payload) {
|
||||
set.status = 401
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid or expired token',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
userId: payload.userId,
|
||||
username: payload.username,
|
||||
role: payload.role,
|
||||
},
|
||||
}
|
||||
} catch {
|
||||
set.status = 401
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid or expired token',
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const LoginSchema = z.object({
|
||||
username: z.string().min(1, 'Username is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
})
|
||||
|
||||
export type LoginInput = z.infer<typeof LoginSchema>
|
||||
|
||||
export const LoginModel = {
|
||||
body: LoginSchema,
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import { db } from '@memoh/db'
|
||||
import { users, settings } from '@memoh/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
|
||||
/**
|
||||
* 验证用户凭据
|
||||
* 优先检查是否为 ROOT 用户,否则查询数据库
|
||||
*/
|
||||
export const validateUser = async (username: string, password: string) => {
|
||||
// 检查是否为 ROOT 用户
|
||||
const rootUser = process.env.ROOT_USER
|
||||
const rootPassword = process.env.ROOT_USER_PASSWORD
|
||||
|
||||
let userId: string | null = null
|
||||
|
||||
if (rootUser && rootPassword && username === rootUser) {
|
||||
if (password === rootPassword) {
|
||||
// 检查 root 用户是否存在于数据库中
|
||||
const [existingUser] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, rootUser))
|
||||
|
||||
userId = existingUser?.id
|
||||
if (!existingUser) {
|
||||
// 为 root 用户创建数据库记录
|
||||
// 使用占位符密码哈希,因为实际密码在环境变量中
|
||||
const [newUser] = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
username: rootUser,
|
||||
passwordHash: 'ENV_BASED_AUTH', // 占位符,实际使用环境变量验证
|
||||
role: 'admin',
|
||||
displayName: 'Root User',
|
||||
email: null,
|
||||
avatarUrl: null,
|
||||
isActive: true,
|
||||
})
|
||||
.onConflictDoNothing() // 避免并发创建导致的冲突
|
||||
.returning({
|
||||
id: users.id,
|
||||
})
|
||||
|
||||
userId = newUser.id
|
||||
}
|
||||
|
||||
// 检查 root 用户的 settings 是否存在,不存在则创建
|
||||
const [existingSettings] = await db
|
||||
.select()
|
||||
.from(settings)
|
||||
.where(eq(settings.userId, userId))
|
||||
|
||||
if (!existingSettings) {
|
||||
// 为 root 用户创建默认 settings
|
||||
await db
|
||||
.insert(settings)
|
||||
.values({
|
||||
userId: userId,
|
||||
defaultChatModel: null,
|
||||
defaultEmbeddingModel: null,
|
||||
defaultSummaryModel: null,
|
||||
maxContextLoadTime: 60,
|
||||
language: 'Same as user input',
|
||||
})
|
||||
.onConflictDoNothing() // 避免并发创建导致的冲突
|
||||
}
|
||||
|
||||
// 返回 ROOT 用户信息
|
||||
return {
|
||||
id: userId,
|
||||
username: rootUser,
|
||||
role: 'admin' as const,
|
||||
displayName: 'Root User',
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 查询数据库中的用户(使用 username 而不是 id)
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, username))
|
||||
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 验证密码 (这里使用简单的 Bun.password.verify)
|
||||
const isValid = await Bun.password.verify(password, user.passwordHash)
|
||||
|
||||
if (!isValid) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 检查账户是否激活
|
||||
if (!user.isActive) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
lastLoginAt: new Date(),
|
||||
})
|
||||
.where(eq(users.id, user.id))
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
displayName: user.displayName || user.username,
|
||||
email: user.email,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
import { Elysia } from 'elysia'
|
||||
import { adminMiddleware } from '../../middlewares'
|
||||
import {
|
||||
createUserContainer,
|
||||
startUserContainer,
|
||||
stopUserContainer,
|
||||
restartUserContainer,
|
||||
pauseUserContainer,
|
||||
resumeUserContainer,
|
||||
deleteUserContainer,
|
||||
getUserContainerInfo,
|
||||
ensureUserContainer,
|
||||
syncAllContainerStatus,
|
||||
getAllContainers,
|
||||
startAllAutoStartContainers,
|
||||
pauseAllContainers,
|
||||
} from './service'
|
||||
import {
|
||||
CreateContainerSchema,
|
||||
ContainerActionSchema,
|
||||
EnsureContainerSchema,
|
||||
} from './model'
|
||||
import { getUsers } from '../user/service'
|
||||
|
||||
/**
|
||||
* Container Management Module
|
||||
* All routes require admin privileges
|
||||
*/
|
||||
export const containerModule = new Elysia({ prefix: '/containers' })
|
||||
// Protect all routes with admin middleware
|
||||
.use(adminMiddleware)
|
||||
.onStart(async () => {
|
||||
console.log('\n📦 Initializing containers...')
|
||||
|
||||
try {
|
||||
// 0. 初始化容器基础目录
|
||||
const { initializeContainerBaseDirectory } = await import('./utils')
|
||||
initializeContainerBaseDirectory()
|
||||
|
||||
// 1. 同步所有容器状态
|
||||
await syncAllContainerStatus()
|
||||
|
||||
// 2. 检查所有用户是否有容器,没有则创建
|
||||
const usersResult = await getUsers({ page: 1, limit: 1000 })
|
||||
console.log(`👥 Found ${usersResult.items.length} users`)
|
||||
|
||||
for (const user of usersResult.items) {
|
||||
try {
|
||||
await ensureUserContainer(user.id)
|
||||
console.log(`✅ Container ensured for user: ${user.username}`)
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to ensure container for user ${user.username}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 启动所有自动启动的容器
|
||||
await startAllAutoStartContainers()
|
||||
|
||||
console.log('✨ Container initialization complete\n')
|
||||
} catch (error) {
|
||||
console.error('❌ Container initialization failed:', error)
|
||||
}
|
||||
})
|
||||
.onStop(async () => {
|
||||
console.log('\n⏸️ Pausing all containers...')
|
||||
|
||||
try {
|
||||
await pauseAllContainers()
|
||||
console.log('✨ All containers paused\n')
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to pause containers:', error)
|
||||
}
|
||||
})
|
||||
.get(
|
||||
'/',
|
||||
async () => {
|
||||
const containers = await getAllContainers()
|
||||
return {
|
||||
success: true,
|
||||
data: containers,
|
||||
}
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
tags: ['Container'],
|
||||
summary: 'Get all containers',
|
||||
description: 'Retrieve information about all containers in the system',
|
||||
},
|
||||
}
|
||||
)
|
||||
.get(
|
||||
'/user/:userId',
|
||||
async ({ params: { userId } }) => {
|
||||
const container = await getUserContainerInfo(userId)
|
||||
if (!container) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Container not found for user',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: container,
|
||||
}
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
tags: ['Container'],
|
||||
summary: 'Get user container',
|
||||
description: 'Get container information for a specific user',
|
||||
},
|
||||
}
|
||||
)
|
||||
.post(
|
||||
'/create',
|
||||
async ({ body }) => {
|
||||
try {
|
||||
const container = await createUserContainer(
|
||||
body.userId,
|
||||
body.image,
|
||||
body.namespace
|
||||
)
|
||||
return {
|
||||
success: true,
|
||||
data: container,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create container',
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
body: CreateContainerSchema,
|
||||
detail: {
|
||||
tags: ['Container'],
|
||||
summary: 'Create user container',
|
||||
description: 'Create a new container for a specified user',
|
||||
},
|
||||
}
|
||||
)
|
||||
.post(
|
||||
'/user/:userId/ensure',
|
||||
async ({ params: { userId }, body }) => {
|
||||
try {
|
||||
const container = await ensureUserContainer(
|
||||
userId,
|
||||
body?.image,
|
||||
body?.namespace
|
||||
)
|
||||
return {
|
||||
success: true,
|
||||
data: container,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to ensure container',
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
body: EnsureContainerSchema,
|
||||
detail: {
|
||||
tags: ['Container'],
|
||||
summary: 'Ensure user has container',
|
||||
description: 'Check if user has a container, create one if not exists',
|
||||
},
|
||||
}
|
||||
)
|
||||
.post(
|
||||
'/user/:userId/action',
|
||||
async ({ params: { userId }, body }) => {
|
||||
try {
|
||||
switch (body.action) {
|
||||
case 'start':
|
||||
await startUserContainer(userId)
|
||||
break
|
||||
case 'stop':
|
||||
await stopUserContainer(userId)
|
||||
break
|
||||
case 'restart':
|
||||
await restartUserContainer(userId)
|
||||
break
|
||||
case 'pause':
|
||||
await pauseUserContainer(userId)
|
||||
break
|
||||
case 'resume':
|
||||
await resumeUserContainer(userId)
|
||||
break
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid action',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Container ${body.action} successful`,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : `Failed to ${body.action} container`,
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
body: ContainerActionSchema,
|
||||
detail: {
|
||||
tags: ['Container'],
|
||||
summary: 'Execute container action',
|
||||
description: 'Perform start, stop, restart, pause, or resume actions on a user container',
|
||||
},
|
||||
}
|
||||
)
|
||||
.delete(
|
||||
'/user/:userId',
|
||||
async ({ params: { userId }, query }) => {
|
||||
try {
|
||||
const force = query.force === 'true'
|
||||
await deleteUserContainer(userId, force)
|
||||
return {
|
||||
success: true,
|
||||
message: 'Container deleted successfully',
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete container',
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
tags: ['Container'],
|
||||
summary: 'Delete user container',
|
||||
description: 'Delete the container for a specified user',
|
||||
},
|
||||
}
|
||||
)
|
||||
.post(
|
||||
'/sync',
|
||||
async () => {
|
||||
try {
|
||||
await syncAllContainerStatus()
|
||||
return {
|
||||
success: true,
|
||||
message: 'Container status synced successfully',
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to sync container status',
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
tags: ['Container'],
|
||||
summary: 'Sync all container statuses',
|
||||
description: 'Synchronize all container statuses from containerd to the database',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* 创建容器请求模型
|
||||
*/
|
||||
export const CreateContainerSchema = z.object({
|
||||
userId: z.string(),
|
||||
image: z.string().optional().default('docker.io/library/alpine:latest'),
|
||||
namespace: z.string().optional().default('default'),
|
||||
autoStart: z.boolean().optional().default(true),
|
||||
})
|
||||
|
||||
export type CreateContainerInput = z.infer<typeof CreateContainerSchema>
|
||||
|
||||
/**
|
||||
* 更新容器请求模型
|
||||
*/
|
||||
export const UpdateContainerSchema = z.object({
|
||||
autoStart: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type UpdateContainerInput = z.infer<typeof UpdateContainerSchema>
|
||||
|
||||
/**
|
||||
* 容器操作请求模型
|
||||
*/
|
||||
export const ContainerActionSchema = z.object({
|
||||
action: z.enum(['start', 'stop', 'restart', 'pause', 'resume']),
|
||||
})
|
||||
|
||||
export type ContainerActionInput = z.infer<typeof ContainerActionSchema>
|
||||
|
||||
/**
|
||||
* 确保容器请求模型
|
||||
*/
|
||||
export const EnsureContainerSchema = z.object({
|
||||
image: z.string().optional(),
|
||||
namespace: z.string().optional(),
|
||||
})
|
||||
|
||||
export type EnsureContainerInput = z.infer<typeof EnsureContainerSchema>
|
||||
|
||||
/**
|
||||
* 容器响应模型
|
||||
*/
|
||||
export const ContainerResponseSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
containerId: z.string(),
|
||||
containerName: z.string(),
|
||||
image: z.string(),
|
||||
status: z.string(),
|
||||
namespace: z.string(),
|
||||
autoStart: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
lastStartedAt: z.date().nullable().optional(),
|
||||
lastStoppedAt: z.date().nullable().optional(),
|
||||
})
|
||||
|
||||
export type ContainerResponse = z.infer<typeof ContainerResponseSchema>
|
||||
|
||||
@@ -1,375 +0,0 @@
|
||||
import {
|
||||
getAllContainers as dbGetAllContainers,
|
||||
getAutoStartContainers,
|
||||
getContainerByUserId,
|
||||
createContainerRecord,
|
||||
updateContainerStatus,
|
||||
deleteContainerRecord,
|
||||
type ContainerInfo,
|
||||
} from '@memoh/db'
|
||||
import { createContainer, useContainer, containerExists, type ContainerConfig } from '@memoh/container'
|
||||
import { getContainerPaths, ensureDirectoryExists } from './utils'
|
||||
|
||||
/**
|
||||
* 获取所有容器
|
||||
*/
|
||||
export const getAllContainers = async (): Promise<ContainerInfo[]> => {
|
||||
return await dbGetAllContainers()
|
||||
}
|
||||
|
||||
/**
|
||||
* 为用户创建容器
|
||||
*/
|
||||
export const createUserContainer = async (
|
||||
userId: string,
|
||||
image: string = 'docker.io/library/node:20-alpine',
|
||||
namespace: string = 'default'
|
||||
): Promise<ContainerInfo> => {
|
||||
// 检查用户是否已有容器
|
||||
const existing = await getContainerByUserId(userId)
|
||||
if (existing) {
|
||||
throw new Error('User already has a container')
|
||||
}
|
||||
|
||||
const containerName = `user-${userId.slice(0, 8)}-container`
|
||||
|
||||
// 检查 containerd 中是否已存在同名容器
|
||||
try {
|
||||
const exists = await containerExists(containerName, { namespace })
|
||||
if (exists) {
|
||||
console.log(`⚠️ Container ${containerName} already exists in containerd, syncing to database...` )
|
||||
|
||||
// 获取容器信息并同步到数据库
|
||||
const ops = useContainer(containerName, { namespace })
|
||||
const info = await ops.info()
|
||||
|
||||
const paths = getContainerPaths(userId)
|
||||
const dbRecord = await createContainerRecord({
|
||||
userId,
|
||||
containerId: info.id,
|
||||
containerName: info.name,
|
||||
image: info.image,
|
||||
namespace,
|
||||
autoStart: true,
|
||||
hostPath: paths.hostPath,
|
||||
containerPath: paths.containerPath,
|
||||
})
|
||||
|
||||
return dbRecord
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking container existence:', error)
|
||||
}
|
||||
|
||||
// 获取挂载路径
|
||||
const paths = getContainerPaths(userId)
|
||||
|
||||
// 确保宿主机目录存在
|
||||
ensureDirectoryExists(paths.hostPath)
|
||||
|
||||
// 创建容器配置
|
||||
const config: ContainerConfig = {
|
||||
name: containerName,
|
||||
image,
|
||||
command: ['sh', '-c', 'while true; do sleep 3600; done'], // 保持容器运行
|
||||
namespace,
|
||||
labels: {
|
||||
userId,
|
||||
managedBy: 'memoh-api',
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
type: 'bind',
|
||||
source: paths.hostPath,
|
||||
target: paths.containerPath,
|
||||
readonly: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// 在 containerd 中创建容器
|
||||
const containerInfo = await createContainer(config, {
|
||||
namespace,
|
||||
nerdctlCommand: process.env.NERDCTL_COMMAND || 'nerdctl',
|
||||
})
|
||||
|
||||
// 在数据库中记录
|
||||
const dbRecord = await createContainerRecord({
|
||||
userId,
|
||||
containerId: containerInfo.id,
|
||||
containerName: containerInfo.name,
|
||||
image: containerInfo.image,
|
||||
namespace,
|
||||
autoStart: true,
|
||||
hostPath: paths.hostPath,
|
||||
containerPath: paths.containerPath,
|
||||
})
|
||||
|
||||
console.log(`✅ Created container with mount: ${paths.hostPath} -> ${paths.containerPath}`)
|
||||
|
||||
return dbRecord
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动用户容器
|
||||
*/
|
||||
export const startUserContainer = async (userId: string): Promise<void> => {
|
||||
const container = await getContainerByUserId(userId)
|
||||
if (!container) {
|
||||
throw new Error('Container not found for user')
|
||||
}
|
||||
|
||||
const ops = useContainer(container.containerName, { namespace: container.namespace })
|
||||
await ops.start()
|
||||
|
||||
// 更新数据库状态
|
||||
await updateContainerStatus(container.containerId, 'running')
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止用户容器
|
||||
*/
|
||||
export const stopUserContainer = async (userId: string, timeout: number = 10): Promise<void> => {
|
||||
const container = await getContainerByUserId(userId)
|
||||
if (!container) {
|
||||
throw new Error('Container not found for user')
|
||||
}
|
||||
|
||||
const ops = useContainer(container.containerName, { namespace: container.namespace })
|
||||
await ops.stop(timeout)
|
||||
|
||||
// 更新数据库状态
|
||||
await updateContainerStatus(container.containerId, 'stopped')
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启用户容器
|
||||
*/
|
||||
export const restartUserContainer = async (userId: string): Promise<void> => {
|
||||
const container = await getContainerByUserId(userId)
|
||||
if (!container) {
|
||||
throw new Error('Container not found for user')
|
||||
}
|
||||
|
||||
const ops = useContainer(container.containerName, { namespace: container.namespace })
|
||||
await ops.restart()
|
||||
|
||||
// 更新数据库状态
|
||||
await updateContainerStatus(container.containerId, 'running')
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停用户容器
|
||||
*/
|
||||
export const pauseUserContainer = async (userId: string): Promise<void> => {
|
||||
const container = await getContainerByUserId(userId)
|
||||
if (!container) {
|
||||
throw new Error('Container not found for user')
|
||||
}
|
||||
|
||||
const ops = useContainer(container.containerName, { namespace: container.namespace })
|
||||
await ops.pause()
|
||||
|
||||
// 更新数据库状态
|
||||
await updateContainerStatus(container.containerId, 'paused')
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复用户容器
|
||||
*/
|
||||
export const resumeUserContainer = async (userId: string): Promise<void> => {
|
||||
const container = await getContainerByUserId(userId)
|
||||
if (!container) {
|
||||
throw new Error('Container not found for user')
|
||||
}
|
||||
|
||||
const ops = useContainer(container.containerName, { namespace: container.namespace })
|
||||
await ops.resume()
|
||||
|
||||
// 更新数据库状态
|
||||
await updateContainerStatus(container.containerId, 'running')
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户容器
|
||||
*/
|
||||
export const deleteUserContainer = async (userId: string, force: boolean = false): Promise<void> => {
|
||||
const container = await getContainerByUserId(userId)
|
||||
if (!container) {
|
||||
throw new Error('Container not found for user')
|
||||
}
|
||||
|
||||
const ops = useContainer(container.containerName, { namespace: container.namespace })
|
||||
await ops.remove(force)
|
||||
|
||||
// 从数据库删除记录
|
||||
await deleteContainerRecord(container.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户容器信息
|
||||
*/
|
||||
export const getUserContainerInfo = async (userId: string): Promise<ContainerInfo | undefined> => {
|
||||
return await getContainerByUserId(userId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动所有自动启动的容器
|
||||
*/
|
||||
export const startAllAutoStartContainers = async (): Promise<{ success: number; failed: number }> => {
|
||||
const containers = await getAutoStartContainers()
|
||||
let success = 0
|
||||
let failed = 0
|
||||
|
||||
console.log(`🚀 Starting ${containers.length} auto-start containers...`)
|
||||
|
||||
for (const container of containers) {
|
||||
try {
|
||||
const ops = useContainer(container.containerName, { namespace: container.namespace })
|
||||
|
||||
// 获取当前状态
|
||||
const info = await ops.info()
|
||||
|
||||
// 只有非运行状态才启动
|
||||
if (info.status !== 'running') {
|
||||
await ops.start()
|
||||
await updateContainerStatus(container.containerId, 'running')
|
||||
console.log(`✅ Started container: ${container.containerName}`)
|
||||
success++
|
||||
} else {
|
||||
console.log(`⏭️ Container already running: ${container.containerName}`)
|
||||
success++
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to start container ${container.containerName}:`, error)
|
||||
failed++
|
||||
// 更新状态为 unknown
|
||||
await updateContainerStatus(container.containerId, 'unknown')
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✨ Container startup complete: ${success} succeeded, ${failed} failed`)
|
||||
|
||||
return { success, failed }
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停所有运行中的容器
|
||||
*/
|
||||
export const pauseAllContainers = async (): Promise<{ success: number; failed: number }> => {
|
||||
const containers = await dbGetAllContainers()
|
||||
let success = 0
|
||||
let failed = 0
|
||||
|
||||
console.log(`⏸️ Pausing ${containers.length} containers...`)
|
||||
|
||||
for (const container of containers) {
|
||||
try {
|
||||
const ops = useContainer(container.containerName, { namespace: container.namespace })
|
||||
|
||||
// 获取当前状态
|
||||
const info = await ops.info()
|
||||
|
||||
// 只暂停运行中的容器
|
||||
if (info.status === 'running') {
|
||||
await ops.pause()
|
||||
await updateContainerStatus(container.containerId, 'paused')
|
||||
console.log(`✅ Paused container: ${container.containerName}`)
|
||||
success++
|
||||
} else {
|
||||
console.log(`⏭️ Container not running, skipped: ${container.containerName}`)
|
||||
success++
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to pause container ${container.containerName}:`, error)
|
||||
failed++
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✨ Container pause complete: ${success} succeeded, ${failed} failed`)
|
||||
|
||||
return { success, failed }
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止所有运行中的容器
|
||||
*/
|
||||
export const stopAllContainers = async (timeout: number = 10): Promise<{ success: number; failed: number }> => {
|
||||
const containers = await dbGetAllContainers()
|
||||
let success = 0
|
||||
let failed = 0
|
||||
|
||||
console.log(`⏹️ Stopping ${containers.length} containers...`)
|
||||
|
||||
for (const container of containers) {
|
||||
try {
|
||||
const ops = useContainer(container.containerName, { namespace: container.namespace })
|
||||
|
||||
// 获取当前状态
|
||||
const info = await ops.info()
|
||||
|
||||
// 只停止运行中的容器
|
||||
if (info.status === 'running') {
|
||||
await ops.stop(timeout)
|
||||
await updateContainerStatus(container.containerId, 'stopped')
|
||||
console.log(`✅ Stopped container: ${container.containerName}`)
|
||||
success++
|
||||
} else {
|
||||
console.log(`⏭️ Container not running, skipped: ${container.containerName}`)
|
||||
success++
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to stop container ${container.containerName}:`, error)
|
||||
failed++
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✨ Container stop complete: ${success} succeeded, ${failed} failed`)
|
||||
|
||||
return { success, failed }
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保用户有容器(没有则创建)
|
||||
*/
|
||||
export const ensureUserContainer = async (
|
||||
userId: string,
|
||||
image?: string,
|
||||
namespace?: string
|
||||
): Promise<ContainerInfo> => {
|
||||
const existing = await getContainerByUserId(userId)
|
||||
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
// 创建新容器
|
||||
return await createUserContainer(userId, image, namespace)
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步所有容器状态
|
||||
*/
|
||||
export const syncAllContainerStatus = async (): Promise<void> => {
|
||||
const containers = await dbGetAllContainers()
|
||||
|
||||
console.log(`🔄 Syncing ${containers.length} container statuses...`)
|
||||
|
||||
for (const container of containers) {
|
||||
try {
|
||||
const ops = useContainer(container.containerName, { namespace: container.namespace })
|
||||
const info = await ops.info()
|
||||
|
||||
if (info.status !== container.status) {
|
||||
await updateContainerStatus(container.containerId, info.status)
|
||||
console.log(`✅ Updated container ${container.containerName}: ${container.status} -> ${info.status}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to sync container ${container.containerName}:`, error)
|
||||
await updateContainerStatus(container.containerId, 'unknown')
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✨ Container status sync complete')
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { existsSync, mkdirSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
/**
|
||||
* Container data directory configuration
|
||||
*/
|
||||
const CONTAINER_BASE_DIR = process.env.CONTAINER_DATA_DIR || '/var/lib/memoh/containers'
|
||||
|
||||
/**
|
||||
* Get host path for user container
|
||||
* @param userId - User ID
|
||||
* @returns Host path for the container
|
||||
*/
|
||||
export function getUserContainerHostPath(userId: string): string {
|
||||
return join(CONTAINER_BASE_DIR, userId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure directory exists, create if not
|
||||
* @param path - Directory path
|
||||
*/
|
||||
export function ensureDirectoryExists(path: string): void {
|
||||
if (!existsSync(path)) {
|
||||
mkdirSync(path, { recursive: true, mode: 0o755 })
|
||||
console.log(`📁 Created directory: ${path}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize container base directory
|
||||
*/
|
||||
export function initializeContainerBaseDirectory(): void {
|
||||
ensureDirectoryExists(CONTAINER_BASE_DIR)
|
||||
console.log(`✅ Container base directory initialized: ${CONTAINER_BASE_DIR}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get container paths for a user
|
||||
* @param userId - User ID
|
||||
* @returns Object with host and container paths
|
||||
*/
|
||||
export function getContainerPaths(userId: string) {
|
||||
return {
|
||||
hostPath: getUserContainerHostPath(userId),
|
||||
containerPath: '/data',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export * from './agent'
|
||||
export * from './auth'
|
||||
export * from './model'
|
||||
export * from './schedule'
|
||||
export * from './settings'
|
||||
export * from './user'
|
||||
export * from './mcp'
|
||||
export * from './platform'
|
||||
export * from './schedule'
|
||||
export * from './memory'
|
||||
export * from './container'
|
||||
@@ -1,152 +0,0 @@
|
||||
import Elysia from 'elysia'
|
||||
import { authMiddleware } from '../../middlewares/auth'
|
||||
import {
|
||||
CreateMCPConnectionModel,
|
||||
UpdateMCPConnectionModel,
|
||||
GetMCPConnectionByIdModel,
|
||||
DeleteMCPConnectionModel,
|
||||
GetMCPConnectionsModel,
|
||||
} from './model'
|
||||
import {
|
||||
getMCPConnections,
|
||||
getMCPConnection,
|
||||
createMCPConnection,
|
||||
updateMCPConnection,
|
||||
deleteMCPConnection,
|
||||
} from './service'
|
||||
|
||||
export const mcpModule = new Elysia({ prefix: '/mcp' })
|
||||
.use(authMiddleware)
|
||||
// Get all MCP connections for current user
|
||||
.get('/', async ({ user, query }) => {
|
||||
try {
|
||||
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 getMCPConnections(user.userId, {
|
||||
page,
|
||||
limit,
|
||||
sortOrder: sortOrder as 'asc' | 'desc',
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
...result,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch MCP connections',
|
||||
}
|
||||
}
|
||||
}, GetMCPConnectionsModel)
|
||||
// Get MCP connection by ID
|
||||
.get('/:id', async ({ user, params, set }) => {
|
||||
try {
|
||||
const connection = await getMCPConnection(params.id)
|
||||
|
||||
if (!connection) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'MCP connection not found',
|
||||
}
|
||||
}
|
||||
|
||||
if (connection.user !== user.userId) {
|
||||
set.status = 403
|
||||
return {
|
||||
success: false,
|
||||
error: 'Forbidden: You do not have permission to access this MCP connection',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: connection,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch MCP connection',
|
||||
}
|
||||
}
|
||||
}, GetMCPConnectionByIdModel)
|
||||
// Create new MCP connection
|
||||
.post('/', async ({ user, body, set }) => {
|
||||
try {
|
||||
const newConnection = await createMCPConnection(user.userId, body)
|
||||
|
||||
set.status = 201
|
||||
return {
|
||||
success: true,
|
||||
data: newConnection,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create MCP connection',
|
||||
}
|
||||
}
|
||||
}, CreateMCPConnectionModel)
|
||||
// Update MCP connection
|
||||
.put('/:id', async ({ user, params, body, set }) => {
|
||||
try {
|
||||
const updatedConnection = await updateMCPConnection(params.id, user.userId, body)
|
||||
|
||||
if (!updatedConnection) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'MCP connection not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedConnection,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('Forbidden')) {
|
||||
set.status = 403
|
||||
} else {
|
||||
set.status = 500
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to update MCP connection',
|
||||
}
|
||||
}
|
||||
}, UpdateMCPConnectionModel)
|
||||
// Delete MCP connection
|
||||
.delete('/:id', async ({ user, params, set }) => {
|
||||
try {
|
||||
const deletedConnection = await deleteMCPConnection(params.id, user.userId)
|
||||
|
||||
if (!deletedConnection) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'MCP connection not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: deletedConnection,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('Forbidden')) {
|
||||
set.status = 403
|
||||
} else {
|
||||
set.status = 500
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete MCP connection',
|
||||
}
|
||||
}
|
||||
}, DeleteMCPConnectionModel)
|
||||
@@ -1,84 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
// Stdio MCP 连接配置
|
||||
const StdioMCPConnectionSchema = z.object({
|
||||
type: z.literal('stdio'),
|
||||
command: z.string().min(1, 'Command is required'),
|
||||
args: z.array(z.string()),
|
||||
env: z.record(z.string(), z.string()),
|
||||
cwd: z.string(),
|
||||
})
|
||||
|
||||
// HTTP MCP 连接配置
|
||||
const HTTPMCPConnectionSchema = z.object({
|
||||
type: z.literal('http'),
|
||||
url: z.string().url('Invalid URL'),
|
||||
headers: z.record(z.string(), z.string()),
|
||||
})
|
||||
|
||||
// SSE MCP 连接配置
|
||||
const SSEMCPConnectionSchema = z.object({
|
||||
type: z.literal('sse'),
|
||||
url: z.string().url('Invalid URL'),
|
||||
headers: z.record(z.string(), z.string()),
|
||||
})
|
||||
|
||||
// 联合类型
|
||||
const MCPConnectionConfigSchema = z.union([
|
||||
StdioMCPConnectionSchema,
|
||||
HTTPMCPConnectionSchema,
|
||||
SSEMCPConnectionSchema,
|
||||
])
|
||||
|
||||
// 创建 MCP 连接的 Schema
|
||||
const CreateMCPConnectionSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').max(100),
|
||||
config: MCPConnectionConfigSchema,
|
||||
active: z.boolean().default(true),
|
||||
})
|
||||
|
||||
// 更新 MCP 连接的 Schema
|
||||
const UpdateMCPConnectionSchema = z.object({
|
||||
name: z.string().min(1).max(100).optional(),
|
||||
config: MCPConnectionConfigSchema.optional(),
|
||||
active: z.boolean().optional(),
|
||||
})
|
||||
|
||||
// 查询参数 Schema
|
||||
const GetMCPConnectionsQuerySchema = z.object({
|
||||
page: z.string().optional(),
|
||||
limit: z.string().optional(),
|
||||
sortOrder: z.enum(['asc', 'desc']).optional(),
|
||||
})
|
||||
|
||||
export type CreateMCPConnectionInput = z.infer<typeof CreateMCPConnectionSchema>
|
||||
export type UpdateMCPConnectionInput = z.infer<typeof UpdateMCPConnectionSchema>
|
||||
export type GetMCPConnectionsQuery = z.infer<typeof GetMCPConnectionsQuerySchema>
|
||||
|
||||
export const CreateMCPConnectionModel = {
|
||||
body: CreateMCPConnectionSchema,
|
||||
}
|
||||
|
||||
export const UpdateMCPConnectionModel = {
|
||||
params: z.object({
|
||||
id: z.string().uuid('Invalid MCP connection ID format'),
|
||||
}),
|
||||
body: UpdateMCPConnectionSchema,
|
||||
}
|
||||
|
||||
export const GetMCPConnectionByIdModel = {
|
||||
params: z.object({
|
||||
id: z.string().uuid('Invalid MCP connection ID format'),
|
||||
}),
|
||||
}
|
||||
|
||||
export const DeleteMCPConnectionModel = {
|
||||
params: z.object({
|
||||
id: z.string().uuid('Invalid MCP connection ID format'),
|
||||
}),
|
||||
}
|
||||
|
||||
export const GetMCPConnectionsModel = {
|
||||
query: GetMCPConnectionsQuerySchema,
|
||||
}
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
import { db } from '@memoh/db'
|
||||
import { mcpConnection } from '@memoh/db/schema'
|
||||
import { eq, desc, asc, sql } from 'drizzle-orm'
|
||||
import { calculateOffset, createPaginatedResult, type PaginatedResult } from '../../utils/pagination'
|
||||
import type { CreateMCPConnectionInput, UpdateMCPConnectionInput } from './model'
|
||||
import { MCPConnection } from '@memoh/shared'
|
||||
|
||||
/**
|
||||
* MCP Connection 列表返回类型
|
||||
*/
|
||||
type MCPConnectionListItem = {
|
||||
id: string
|
||||
type: string
|
||||
name: string
|
||||
config: unknown
|
||||
active: boolean
|
||||
user: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的所有 MCP 连接(支持分页)
|
||||
*/
|
||||
export const getMCPConnections = async (
|
||||
userId: string,
|
||||
params?: {
|
||||
limit?: number
|
||||
page?: number
|
||||
sortOrder?: 'asc' | 'desc'
|
||||
}
|
||||
): Promise<PaginatedResult<MCPConnectionListItem>> => {
|
||||
const limit = params?.limit || 10
|
||||
const page = params?.page || 1
|
||||
const sortOrder = params?.sortOrder || 'desc'
|
||||
const offset = calculateOffset(page, limit)
|
||||
|
||||
// 获取总数
|
||||
const [{ count }] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(mcpConnection)
|
||||
.where(eq(mcpConnection.user, userId))
|
||||
|
||||
// 获取分页数据
|
||||
const orderFn = sortOrder === 'desc' ? desc : asc
|
||||
const connections = await db
|
||||
.select()
|
||||
.from(mcpConnection)
|
||||
.where(eq(mcpConnection.user, userId))
|
||||
.orderBy(orderFn(mcpConnection.id))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
|
||||
// 类型转换
|
||||
const formattedConnections = connections.map(conn => ({
|
||||
id: conn.id,
|
||||
type: conn.type,
|
||||
name: conn.name,
|
||||
config: conn.config,
|
||||
active: conn.active,
|
||||
user: conn.user,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}))
|
||||
|
||||
return createPaginatedResult(formattedConnections, Number(count), page, limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的所有活跃 MCP 连接
|
||||
*/
|
||||
export const getActiveMCPConnections = async (
|
||||
userId: string
|
||||
) => {
|
||||
const connections = await db
|
||||
.select()
|
||||
.from(mcpConnection)
|
||||
.where(eq(mcpConnection.user, userId))
|
||||
.orderBy(desc(mcpConnection.id))
|
||||
|
||||
return connections.filter(conn => conn.active).map(conn => conn.config) as MCPConnection[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 获取单个 MCP 连接
|
||||
*/
|
||||
export const getMCPConnection = async (
|
||||
connectionId: string
|
||||
) => {
|
||||
const [result] = await db
|
||||
.select()
|
||||
.from(mcpConnection)
|
||||
.where(eq(mcpConnection.id, connectionId))
|
||||
|
||||
if (!result) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: result.id,
|
||||
type: result.type,
|
||||
name: result.name,
|
||||
config: result.config,
|
||||
active: result.active,
|
||||
user: result.user,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的 MCP 连接
|
||||
*/
|
||||
export const createMCPConnection = async (
|
||||
userId: string,
|
||||
data: CreateMCPConnectionInput
|
||||
) => {
|
||||
const [newConnection] = await db
|
||||
.insert(mcpConnection)
|
||||
.values({
|
||||
user: userId,
|
||||
type: data.config.type,
|
||||
name: data.name,
|
||||
config: data.config,
|
||||
active: data.active,
|
||||
})
|
||||
.returning()
|
||||
|
||||
return {
|
||||
id: newConnection.id,
|
||||
type: newConnection.type,
|
||||
name: newConnection.name,
|
||||
config: newConnection.config,
|
||||
active: newConnection.active,
|
||||
user: newConnection.user,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 MCP 连接
|
||||
*/
|
||||
export const updateMCPConnection = async (
|
||||
connectionId: string,
|
||||
userId: string,
|
||||
data: UpdateMCPConnectionInput
|
||||
) => {
|
||||
// 检查 MCP 连接是否存在且属于该用户
|
||||
const existingConnection = await getMCPConnection(connectionId)
|
||||
if (!existingConnection) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (existingConnection.user !== userId) {
|
||||
throw new Error('Forbidden: You do not have permission to update this MCP connection')
|
||||
}
|
||||
|
||||
const updateData: {
|
||||
name?: string
|
||||
config?: unknown
|
||||
type?: string
|
||||
active?: boolean
|
||||
} = {}
|
||||
|
||||
if (data.name !== undefined) {
|
||||
updateData.name = data.name
|
||||
}
|
||||
if (data.config !== undefined) {
|
||||
updateData.config = data.config
|
||||
updateData.type = data.config.type
|
||||
}
|
||||
if (data.active !== undefined) {
|
||||
updateData.active = data.active
|
||||
}
|
||||
|
||||
const [updatedConnection] = await db
|
||||
.update(mcpConnection)
|
||||
.set(updateData)
|
||||
.where(eq(mcpConnection.id, connectionId))
|
||||
.returning()
|
||||
|
||||
return {
|
||||
id: updatedConnection.id,
|
||||
type: updatedConnection.type,
|
||||
name: updatedConnection.name,
|
||||
config: updatedConnection.config,
|
||||
active: updatedConnection.active,
|
||||
user: updatedConnection.user,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 MCP 连接
|
||||
*/
|
||||
export const deleteMCPConnection = async (
|
||||
connectionId: string,
|
||||
userId: string
|
||||
) => {
|
||||
// 检查 MCP 连接是否存在且属于该用户
|
||||
const existingConnection = await getMCPConnection(connectionId)
|
||||
if (!existingConnection) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (existingConnection.user !== userId) {
|
||||
throw new Error('Forbidden: You do not have permission to delete this MCP connection')
|
||||
}
|
||||
|
||||
const [deletedConnection] = await db
|
||||
.delete(mcpConnection)
|
||||
.where(eq(mcpConnection.id, connectionId))
|
||||
.returning()
|
||||
|
||||
return {
|
||||
id: deletedConnection.id,
|
||||
type: deletedConnection.type,
|
||||
name: deletedConnection.name,
|
||||
config: deletedConnection.config,
|
||||
active: deletedConnection.active,
|
||||
user: deletedConnection.user,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 MCP 连接的活跃状态
|
||||
*/
|
||||
export const setMCPConnectionActive = async (
|
||||
connectionId: string,
|
||||
active: boolean
|
||||
) => {
|
||||
await db
|
||||
.update(mcpConnection)
|
||||
.set({ active })
|
||||
.where(eq(mcpConnection.id, connectionId))
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import Elysia from 'elysia'
|
||||
import { authMiddleware } from '../../middlewares/auth'
|
||||
import { messageModule } from './message'
|
||||
import { AddMemoryModel, SearchMemoryModel } from './model'
|
||||
import { addMemory, searchMemory } from './service'
|
||||
import { MemoryUnit } from '@memoh/memory'
|
||||
|
||||
export const memoryModule = new Elysia({
|
||||
prefix: '/memory',
|
||||
})
|
||||
.use(authMiddleware)
|
||||
.use(messageModule)
|
||||
// Add memory for current user
|
||||
.post('/', async ({ user, body, set }) => {
|
||||
try {
|
||||
const memoryUnit: MemoryUnit = {
|
||||
...body,
|
||||
user: user.userId,
|
||||
}
|
||||
const result = await addMemory(memoryUnit)
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to add memory',
|
||||
}
|
||||
}
|
||||
}, AddMemoryModel)
|
||||
// Search memory for current user
|
||||
.get('/search', async ({ user, query, set }) => {
|
||||
try {
|
||||
const results = await searchMemory(query.query, user.userId)
|
||||
return {
|
||||
success: true,
|
||||
data: results,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to search memory',
|
||||
}
|
||||
}
|
||||
}, SearchMemoryModel)
|
||||
@@ -1,62 +0,0 @@
|
||||
import Elysia from 'elysia'
|
||||
import { authMiddleware } from '../../../middlewares/auth'
|
||||
import { GetMemoryMessageFilterModel, GetMemoryMessageModel } from './model'
|
||||
import { getMemoryMessages, getMemoryMessagesFilter } from './service'
|
||||
|
||||
export const messageModule = new Elysia({
|
||||
prefix: '/message',
|
||||
})
|
||||
.use(authMiddleware)
|
||||
.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,
|
||||
},
|
||||
}
|
||||
})
|
||||
// Get messages for current user (paginated)
|
||||
.get('/', async ({ user, query, set }) => {
|
||||
try {
|
||||
const units = await getMemoryMessages(user.userId, query)
|
||||
return {
|
||||
success: true,
|
||||
data: units,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch messages',
|
||||
}
|
||||
}
|
||||
}, GetMemoryMessageModel)
|
||||
// Get messages by date range for current user
|
||||
.get('/filter', async ({ user, query, set }) => {
|
||||
try {
|
||||
const units = await getMemoryMessagesFilter(user.userId, query)
|
||||
return {
|
||||
success: true,
|
||||
data: units,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to filter messages',
|
||||
}
|
||||
}
|
||||
}, GetMemoryMessageFilterModel)
|
||||
@@ -1,15 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const GetMemoryMessageModel = {
|
||||
query: z.object({
|
||||
limit: z.coerce.number().default(10),
|
||||
page: z.coerce.number().default(1),
|
||||
}),
|
||||
}
|
||||
|
||||
export const GetMemoryMessageFilterModel = {
|
||||
query: z.object({
|
||||
from: z.coerce.date(),
|
||||
to: z.coerce.date(),
|
||||
}),
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { db } from '@memoh/db'
|
||||
import { history } from '@memoh/db/schema'
|
||||
import { eq, desc, and, gte, lte, asc } from 'drizzle-orm'
|
||||
|
||||
export const getMemoryMessages = async (
|
||||
userId: string,
|
||||
query: {
|
||||
limit: number
|
||||
page: number
|
||||
}
|
||||
) => {
|
||||
const { limit, page } = query
|
||||
const results = await db
|
||||
.select()
|
||||
.from(history)
|
||||
.where(eq(history.user, userId))
|
||||
.orderBy(desc(history.timestamp))
|
||||
.limit(limit)
|
||||
.offset((page - 1) * limit)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
export const getMemoryMessagesFilter = async (
|
||||
userId: string,
|
||||
query: {
|
||||
from: Date
|
||||
to: Date
|
||||
}
|
||||
) => {
|
||||
const { from, to } = query
|
||||
const results = await db
|
||||
.select()
|
||||
.from(history)
|
||||
.where(and(
|
||||
gte(history.timestamp, from),
|
||||
lte(history.timestamp, to),
|
||||
eq(history.user, userId),
|
||||
))
|
||||
.orderBy(asc(history.timestamp))
|
||||
|
||||
return results
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
// MemoryUnit schema (without user field, will be added from auth)
|
||||
const MemoryUnitBodySchema = z.object({
|
||||
messages: z.array(z.any()),
|
||||
timestamp: z.coerce.date(),
|
||||
})
|
||||
|
||||
export const AddMemoryModel = {
|
||||
body: MemoryUnitBodySchema,
|
||||
}
|
||||
|
||||
export const SearchMemoryModel = {
|
||||
query: z.object({
|
||||
query: z.string().min(1, 'Search query is required'),
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { createMemory, MemoryUnit } from '@memoh/memory'
|
||||
import { getEmbeddingModel, getSummaryModel } from '@/modules/model/service'
|
||||
import { ChatModel, EmbeddingModel } from '@memoh/shared'
|
||||
|
||||
export const addMemory = async (memoryUnit: MemoryUnit) => {
|
||||
const [embeddingModel, summaryModel] = await Promise.all([
|
||||
getEmbeddingModel(memoryUnit.user),
|
||||
getSummaryModel(memoryUnit.user),
|
||||
])
|
||||
if (!embeddingModel || !summaryModel) {
|
||||
throw new Error('Embedding or summary model not found')
|
||||
}
|
||||
const { addMemory } = createMemory({
|
||||
summaryModel: summaryModel.model as ChatModel,
|
||||
embeddingModel: embeddingModel.model as EmbeddingModel,
|
||||
})
|
||||
await addMemory(memoryUnit)
|
||||
return memoryUnit
|
||||
}
|
||||
|
||||
export const searchMemory = async (query: string, userId: string) => {
|
||||
const [embeddingModel, summaryModel] = await Promise.all([
|
||||
getEmbeddingModel(userId),
|
||||
getSummaryModel(userId),
|
||||
])
|
||||
if (!embeddingModel || !summaryModel) {
|
||||
throw new Error('Embedding or summary model not found')
|
||||
}
|
||||
const { searchMemory } = createMemory({
|
||||
summaryModel: summaryModel.model as ChatModel,
|
||||
embeddingModel: embeddingModel.model as EmbeddingModel,
|
||||
})
|
||||
const results = await searchMemory(query, userId)
|
||||
return results
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
import Elysia from 'elysia'
|
||||
import { adminMiddleware, optionalAuthMiddleware } from '../../middlewares/auth'
|
||||
import {
|
||||
CreateModelModel,
|
||||
UpdateModelModel,
|
||||
GetModelByIdModel,
|
||||
DeleteModelModel,
|
||||
GetDefaultModelModel,
|
||||
} from './model'
|
||||
import {
|
||||
getModels,
|
||||
getModelById,
|
||||
createModel,
|
||||
updateModel,
|
||||
deleteModel,
|
||||
getChatModel,
|
||||
getSummaryModel,
|
||||
getEmbeddingModel,
|
||||
} from './service'
|
||||
import { Model } from '@memoh/shared'
|
||||
|
||||
export const modelModule = new Elysia({
|
||||
prefix: '/model',
|
||||
})
|
||||
// 公开的读取接口
|
||||
.use(optionalAuthMiddleware)
|
||||
// Get all models
|
||||
.get('/', async ({ query }) => {
|
||||
try {
|
||||
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,
|
||||
...result,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch models',
|
||||
}
|
||||
}
|
||||
})
|
||||
// Get model by ID
|
||||
.get('/:id', async ({ params }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const model = await getModelById(id)
|
||||
if (!model) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Model not found',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: model,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch model',
|
||||
}
|
||||
}
|
||||
}, GetModelByIdModel)
|
||||
// Get default chat model
|
||||
.get('/chat/default', async ({ query }) => {
|
||||
try {
|
||||
const { userId } = query
|
||||
const chatModel = await getChatModel(userId)
|
||||
if (!chatModel) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Default chat model not found or not set',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: chatModel,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch default chat model',
|
||||
}
|
||||
}
|
||||
}, GetDefaultModelModel)
|
||||
// Get default summary model
|
||||
.get('/summary/default', async ({ query }) => {
|
||||
try {
|
||||
const { userId } = query
|
||||
const summaryModel = await getSummaryModel(userId)
|
||||
if (!summaryModel) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Default summary model not found or not set',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: summaryModel,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch default summary model',
|
||||
}
|
||||
}
|
||||
}, GetDefaultModelModel)
|
||||
// Get default embedding model
|
||||
.get('/embedding/default', async ({ query }) => {
|
||||
try {
|
||||
const { userId } = query
|
||||
const embeddingModel = await getEmbeddingModel(userId)
|
||||
if (!embeddingModel) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Default embedding model not found or not set',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: embeddingModel,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch default embedding model',
|
||||
}
|
||||
}
|
||||
}, GetDefaultModelModel)
|
||||
// 管理员权限的写入接口
|
||||
.guard(
|
||||
{
|
||||
beforeHandle: () => {
|
||||
// This will be overridden by adminMiddleware
|
||||
},
|
||||
},
|
||||
(app) =>
|
||||
app
|
||||
.use(adminMiddleware)
|
||||
// Create new model
|
||||
.post('/', async ({ body }) => {
|
||||
console.log('body', 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,55 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const BaseModelSchema = z.object({
|
||||
modelId: z.string().min(1, 'Model ID is required'),
|
||||
baseUrl: z.string(),
|
||||
apiKey: z.string().min(1, 'API key is required'),
|
||||
clientType: z.string(),
|
||||
name: z.string().optional(),
|
||||
})
|
||||
|
||||
// Chat model schema (type is optional and defaults to 'chat')
|
||||
const ChatModelSchema = BaseModelSchema.extend({
|
||||
type: z.enum(['chat']).optional().default('chat'),
|
||||
})
|
||||
|
||||
// Embedding model schema (type must be 'embedding' and dimensions is required)
|
||||
const EmbeddingModelSchema = BaseModelSchema.extend({
|
||||
type: z.literal('embedding'),
|
||||
dimensions: z.number().int().positive('Dimensions must be a positive integer'),
|
||||
})
|
||||
|
||||
// Union of both model types
|
||||
const ModelSchema = z.union([ChatModelSchema, EmbeddingModelSchema])
|
||||
|
||||
// Export the inferred type from the schema
|
||||
export type ModelInput = z.infer<typeof ModelSchema>
|
||||
|
||||
export const CreateModelModel = {
|
||||
body: ModelSchema,
|
||||
}
|
||||
|
||||
export const UpdateModelModel = {
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
body: ModelSchema,
|
||||
}
|
||||
|
||||
export const GetModelByIdModel = {
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
}
|
||||
|
||||
export const DeleteModelModel = {
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
}
|
||||
|
||||
export const GetDefaultModelModel = {
|
||||
query: z.object({
|
||||
userId: z.string().min(1, 'User ID is required'),
|
||||
}),
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import { db } from '@memoh/db'
|
||||
import { model } from '@memoh/db/schema'
|
||||
import { Model } from '@memoh/shared'
|
||||
import { eq, sql, desc, asc } from 'drizzle-orm'
|
||||
import { getSettings } from '@/modules/settings/service'
|
||||
import { calculateOffset, createPaginatedResult, type PaginatedResult } from '../../utils/pagination'
|
||||
|
||||
/**
|
||||
* 模型列表返回类型
|
||||
*/
|
||||
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) => {
|
||||
const [result] = await db.select().from(model).where(eq(model.id, id))
|
||||
return result
|
||||
}
|
||||
|
||||
export const createModel = async (data: Model) => {
|
||||
const [newModel] = await db
|
||||
.insert(model)
|
||||
.values({ model: data })
|
||||
.returning()
|
||||
return newModel
|
||||
}
|
||||
|
||||
export const updateModel = async (id: string, data: Model) => {
|
||||
const [updatedModel] = await db
|
||||
.update(model)
|
||||
.set({ model: data })
|
||||
.where(eq(model.id, id))
|
||||
.returning()
|
||||
return updatedModel
|
||||
}
|
||||
|
||||
export const deleteModel = async (id: string) => {
|
||||
const [deletedModel] = await db
|
||||
.delete(model)
|
||||
.where(eq(model.id, id))
|
||||
.returning()
|
||||
return deletedModel
|
||||
}
|
||||
|
||||
export const getChatModel = async (userId: string) => {
|
||||
const userSettings = await getSettings(userId)
|
||||
if (!userSettings?.defaultChatModel) {
|
||||
return null
|
||||
}
|
||||
return await getModelById(userSettings.defaultChatModel)
|
||||
}
|
||||
|
||||
export const getSummaryModel = async (userId: string) => {
|
||||
const userSettings = await getSettings(userId)
|
||||
if (!userSettings?.defaultSummaryModel) {
|
||||
return null
|
||||
}
|
||||
return await getModelById(userSettings.defaultSummaryModel)
|
||||
}
|
||||
|
||||
export const getEmbeddingModel = async (userId: string) => {
|
||||
const userSettings = await getSettings(userId)
|
||||
if (!userSettings?.defaultEmbeddingModel) {
|
||||
return null
|
||||
}
|
||||
return await getModelById(userSettings.defaultEmbeddingModel)
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
import Elysia from 'elysia'
|
||||
import { adminMiddleware, optionalAuthMiddleware } from '../../middlewares/auth'
|
||||
import {
|
||||
CreatePlatformModel,
|
||||
UpdatePlatformModel,
|
||||
GetPlatformByIdModel,
|
||||
DeletePlatformModel,
|
||||
UpdatePlatformConfigModel,
|
||||
SetPlatformActiveModel,
|
||||
getPlatformConfigSchema,
|
||||
} from './model'
|
||||
import {
|
||||
getPlatforms,
|
||||
getPlatformById,
|
||||
createPlatform,
|
||||
updatePlatform,
|
||||
deletePlatform,
|
||||
updatePlatformConfig,
|
||||
getActivePlatforms,
|
||||
activePlatform,
|
||||
setActivePlatform,
|
||||
} from './service'
|
||||
import { Platform } from '@memoh/shared'
|
||||
|
||||
export const platformModule = new Elysia({
|
||||
prefix: '/platform',
|
||||
})
|
||||
// 公开的读取接口 - 用户可读
|
||||
.use(optionalAuthMiddleware)
|
||||
// Get all platforms
|
||||
.onStart(async () => {
|
||||
try {
|
||||
const platforms = await getActivePlatforms()
|
||||
for (const platform of platforms) {
|
||||
await activePlatform({
|
||||
id: platform.id,
|
||||
name: platform.name,
|
||||
config: platform.config as Record<string, unknown>,
|
||||
active: platform.active,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to start platform', error)
|
||||
}
|
||||
})
|
||||
.get('/', async ({ query }) => {
|
||||
try {
|
||||
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 getPlatforms({
|
||||
page,
|
||||
limit,
|
||||
sortOrder: sortOrder as 'asc' | 'desc',
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
...result,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch platforms',
|
||||
}
|
||||
}
|
||||
})
|
||||
// Get platform by ID
|
||||
.get('/:id', async ({ params }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const platform = await getPlatformById(id)
|
||||
if (!platform) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Platform not found',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: platform,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch platform',
|
||||
}
|
||||
}
|
||||
}, GetPlatformByIdModel)
|
||||
// 管理员权限的写入接口 - 管理员可读写
|
||||
.guard(
|
||||
{
|
||||
beforeHandle: () => {
|
||||
// This will be overridden by adminMiddleware
|
||||
},
|
||||
},
|
||||
(app) =>
|
||||
app
|
||||
.use(adminMiddleware)
|
||||
// Create new platform
|
||||
.post('/', async ({ body }) => {
|
||||
try {
|
||||
const newPlatform = await createPlatform(body as Omit<Platform, 'id'>)
|
||||
return {
|
||||
success: true,
|
||||
data: newPlatform,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create platform',
|
||||
}
|
||||
}
|
||||
}, CreatePlatformModel)
|
||||
// Update platform
|
||||
.put('/:id', async ({ params, body }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const updatedPlatform = await updatePlatform(id, body as Partial<Omit<Platform, 'id'>>)
|
||||
if (!updatedPlatform) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Platform not found',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: updatedPlatform,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to update platform',
|
||||
}
|
||||
}
|
||||
}, UpdatePlatformModel)
|
||||
// Update platform config
|
||||
.put('/:id/config', async ({ params, body }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const { config } = body as { config: Record<string, unknown> }
|
||||
|
||||
// Get the platform to validate config against its schema
|
||||
const platform = await getPlatformById(id)
|
||||
if (!platform) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Platform not found',
|
||||
}
|
||||
}
|
||||
|
||||
// Validate config against platform-specific schema
|
||||
const configSchema = getPlatformConfigSchema(platform.name)
|
||||
const validatedConfig = configSchema.parse(config) as Record<string, unknown>
|
||||
|
||||
const updatedPlatform = await updatePlatformConfig(id, validatedConfig)
|
||||
return {
|
||||
success: true,
|
||||
data: updatedPlatform,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to update platform config',
|
||||
}
|
||||
}
|
||||
}, UpdatePlatformConfigModel)
|
||||
// Delete platform
|
||||
.delete('/:id', async ({ params }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const deletedPlatform = await deletePlatform(id)
|
||||
if (!deletedPlatform) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Platform not found',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: deletedPlatform,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete platform',
|
||||
}
|
||||
}
|
||||
}, DeletePlatformModel)
|
||||
// Active platform
|
||||
.post('/:id/active', async ({ params }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const activatedPlatform = await setActivePlatform(id, true)
|
||||
if (!activatedPlatform) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Platform not found',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: activatedPlatform,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to activate platform',
|
||||
}
|
||||
}
|
||||
}, SetPlatformActiveModel)
|
||||
// Inactive platform
|
||||
.post('/:id/inactive', async ({ params }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const inactivatedPlatform = await setActivePlatform(id, false)
|
||||
if (!inactivatedPlatform) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Platform not found',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: inactivatedPlatform,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to inactivate platform',
|
||||
}
|
||||
}
|
||||
}, SetPlatformActiveModel)
|
||||
)
|
||||
@@ -1,100 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
// Platform-specific config schemas
|
||||
export const TelegramConfigSchema = z.object({
|
||||
botToken: z.string().min(1, 'Bot token is required'),
|
||||
})
|
||||
|
||||
// Registry of platform config schemas
|
||||
// When adding a new platform, add its config schema here
|
||||
export const platformConfigSchemas: Record<string, z.ZodSchema> = {
|
||||
telegram: TelegramConfigSchema,
|
||||
// Add more platforms here as they are implemented
|
||||
// discord: DiscordConfigSchema,
|
||||
// slack: SlackConfigSchema,
|
||||
}
|
||||
|
||||
// Helper function to get config schema for a platform
|
||||
export const getPlatformConfigSchema = (platformName: string): z.ZodSchema => {
|
||||
const schema = platformConfigSchemas[platformName]
|
||||
if (!schema) {
|
||||
throw new Error(`Unknown platform: ${platformName}. Supported platforms: ${Object.keys(platformConfigSchemas).join(', ')}`)
|
||||
}
|
||||
return schema
|
||||
}
|
||||
|
||||
// Base platform schema with dynamic config validation
|
||||
const PlatformSchema = z.object({
|
||||
name: z.string().min(1, 'Platform name is required'),
|
||||
config: z.record(z.string(), z.unknown()),
|
||||
active: z.boolean().optional().default(true),
|
||||
}).superRefine((data, ctx) => {
|
||||
// Validate that the platform name is supported
|
||||
if (!platformConfigSchemas[data.name]) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Unknown platform: ${data.name}. Supported platforms: ${Object.keys(platformConfigSchemas).join(', ')}`,
|
||||
path: ['name'],
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the config against the platform-specific schema
|
||||
try {
|
||||
const configSchema = getPlatformConfigSchema(data.name)
|
||||
configSchema.parse(data.config)
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
error.issues.forEach((issue: z.ZodIssue) => {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: issue.message,
|
||||
path: ['config', ...issue.path],
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export type PlatformInput = z.infer<typeof PlatformSchema>
|
||||
|
||||
export const CreatePlatformModel = {
|
||||
body: PlatformSchema,
|
||||
}
|
||||
|
||||
export const UpdatePlatformModel = {
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
body: PlatformSchema,
|
||||
}
|
||||
|
||||
export const GetPlatformByIdModel = {
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
}
|
||||
|
||||
export const DeletePlatformModel = {
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
}
|
||||
|
||||
// For updating config, we need to know the platform name to validate
|
||||
// This will be used with additional validation in the route handler
|
||||
export const UpdatePlatformConfigModel = {
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
body: z.object({
|
||||
config: z.record(z.string(), z.unknown()),
|
||||
}),
|
||||
}
|
||||
|
||||
export const SetPlatformActiveModel = {
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
import { db } from '@memoh/db'
|
||||
import { platform } from '@memoh/db/schema'
|
||||
import { Platform } from '@memoh/shared'
|
||||
import { eq, sql, desc, asc } from 'drizzle-orm'
|
||||
import { calculateOffset, createPaginatedResult, type PaginatedResult } from '../../utils/pagination'
|
||||
import { BasePlatform } from '@memoh/platform'
|
||||
import { TelegramPlatform } from '@memoh/platform-telegram'
|
||||
|
||||
type PlatformListItem = {
|
||||
id: string
|
||||
name: string
|
||||
config: Record<string, unknown>
|
||||
active: boolean
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export const getPlatforms = async (params?: {
|
||||
page?: number
|
||||
limit?: number
|
||||
sortOrder?: 'asc' | 'desc'
|
||||
}): Promise<PaginatedResult<PlatformListItem>> => {
|
||||
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(platform)
|
||||
|
||||
// 获取分页数据
|
||||
const orderFn = sortOrder === 'desc' ? desc : asc
|
||||
const platforms = await db
|
||||
.select()
|
||||
.from(platform)
|
||||
.orderBy(orderFn(platform.createdAt))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
|
||||
// Cast config to Record<string, unknown> for type safety
|
||||
const typedPlatforms = platforms.map(p => ({
|
||||
...p,
|
||||
config: p.config as Record<string, unknown>,
|
||||
}))
|
||||
|
||||
return createPaginatedResult(typedPlatforms, Number(count), page, limit)
|
||||
}
|
||||
|
||||
export const getPlatformById = async (id: string) => {
|
||||
const [result] = await db.select().from(platform).where(eq(platform.id, id))
|
||||
return result
|
||||
}
|
||||
|
||||
export const getPlatformByName = async (name: string) => {
|
||||
const [result] = await db.select().from(platform).where(eq(platform.name, name))
|
||||
return result
|
||||
}
|
||||
|
||||
export const getActivePlatforms = async () => {
|
||||
return await db.select()
|
||||
.from(platform)
|
||||
.where(eq(platform.active, true))
|
||||
}
|
||||
|
||||
export const createPlatform = async (data: Omit<Platform, 'id'>) => {
|
||||
const [newPlatform] = await db
|
||||
.insert(platform)
|
||||
.values({
|
||||
name: data.name,
|
||||
config: data.config,
|
||||
active: data.active ?? true,
|
||||
})
|
||||
.returning()
|
||||
if (data.active ?? true) {
|
||||
await activePlatform({
|
||||
id: newPlatform.id,
|
||||
name: newPlatform.name,
|
||||
config: newPlatform.config as Record<string, unknown>,
|
||||
active: newPlatform.active,
|
||||
})
|
||||
}
|
||||
return newPlatform
|
||||
}
|
||||
|
||||
export const updatePlatform = async (id: string, data: Partial<Omit<Platform, 'id'>>) => {
|
||||
const updateData: {
|
||||
name?: string
|
||||
config?: Record<string, unknown>
|
||||
active?: boolean
|
||||
updatedAt: Date
|
||||
} = {
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
if (data.name !== undefined) updateData.name = data.name
|
||||
if (data.config !== undefined) updateData.config = data.config
|
||||
if (data.active !== undefined) updateData.active = data.active
|
||||
|
||||
const [updatedPlatform] = await db
|
||||
.update(platform)
|
||||
.set(updateData)
|
||||
.where(eq(platform.id, id))
|
||||
.returning()
|
||||
return updatedPlatform
|
||||
}
|
||||
|
||||
export const deletePlatform = async (id: string) => {
|
||||
const [deletedPlatform] = await db
|
||||
.delete(platform)
|
||||
.where(eq(platform.id, id))
|
||||
.returning()
|
||||
return deletedPlatform
|
||||
}
|
||||
|
||||
export const updatePlatformConfig = async (id: string, config: Record<string, unknown>) => {
|
||||
const [updatedPlatform] = await db
|
||||
.update(platform)
|
||||
.set({
|
||||
config,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(platform.id, id))
|
||||
.returning()
|
||||
return updatedPlatform
|
||||
}
|
||||
|
||||
// active
|
||||
|
||||
export const platformConstructors: Record<string, typeof BasePlatform> = {
|
||||
telegram: TelegramPlatform,
|
||||
}
|
||||
|
||||
export const platforms = new Map<string, BasePlatform>()
|
||||
|
||||
export const activePlatform = async (platform: Platform) => {
|
||||
const Constructor = platformConstructors[platform.name]
|
||||
if (!Constructor) {
|
||||
throw new Error('Platform constructor not found')
|
||||
}
|
||||
const platformInstance = new Constructor()
|
||||
await platformInstance.start(platform.config)
|
||||
platforms.set(platform.name, platformInstance)
|
||||
}
|
||||
|
||||
export const inactivePlatform = async (platform: Platform) => {
|
||||
const platformInstance = platforms.get(platform.name)
|
||||
if (!platformInstance) {
|
||||
throw new Error('Platform not found')
|
||||
}
|
||||
await platformInstance.stop()
|
||||
platforms.delete(platform.name)
|
||||
}
|
||||
|
||||
export const setActivePlatform = async (id: string, active: boolean) => {
|
||||
const currentPlatform = await getPlatformById(id)
|
||||
if (!currentPlatform) {
|
||||
throw new Error('Platform not found')
|
||||
}
|
||||
const platformData: Platform = {
|
||||
id: currentPlatform.id,
|
||||
name: currentPlatform.name,
|
||||
config: currentPlatform.config as Record<string, unknown>,
|
||||
active: active,
|
||||
}
|
||||
if (active) {
|
||||
await activePlatform(platformData)
|
||||
} else {
|
||||
await inactivePlatform(platformData)
|
||||
}
|
||||
const [updatedPlatform] = await db
|
||||
.update(platform)
|
||||
.set({ active })
|
||||
.where(eq(platform.id, id))
|
||||
.returning()
|
||||
return updatedPlatform
|
||||
}
|
||||
|
||||
export const sendMessageToPlatform = async (name: string, options: {
|
||||
message: string
|
||||
userId: string
|
||||
}) => {
|
||||
const currentPlatform = await getPlatformByName(name)
|
||||
if (!currentPlatform) {
|
||||
throw new Error('Platform not found')
|
||||
}
|
||||
const platformInstance = platforms.get(currentPlatform.name)
|
||||
if (!platformInstance) {
|
||||
throw new Error('Platform not found')
|
||||
}
|
||||
await platformInstance.send(options)
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import Elysia from 'elysia'
|
||||
import { authMiddleware } from '../../middlewares/auth'
|
||||
import {
|
||||
CreateScheduleModel,
|
||||
UpdateScheduleModel,
|
||||
GetScheduleByIdModel,
|
||||
DeleteScheduleModel,
|
||||
GetSchedulesModel,
|
||||
} from './model'
|
||||
import {
|
||||
getSchedules,
|
||||
getSchedule,
|
||||
createSchedule,
|
||||
updateSchedule,
|
||||
deleteSchedule,
|
||||
createScheduler,
|
||||
} from './service'
|
||||
|
||||
export const { scheduleTask, resume } = createScheduler()
|
||||
|
||||
export const scheduleModule = new Elysia({ prefix: '/schedule' })
|
||||
.use(authMiddleware)
|
||||
// Get all schedules for current user
|
||||
.onStart(async () => {
|
||||
try {
|
||||
await resume()
|
||||
} catch (error) {
|
||||
console.error('Failed to resume schedule', error)
|
||||
}
|
||||
})
|
||||
.get('/', async ({ user, query }) => {
|
||||
try {
|
||||
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 getSchedules(user.userId, {
|
||||
page,
|
||||
limit,
|
||||
sortOrder: sortOrder as 'asc' | 'desc',
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
...result,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch schedules',
|
||||
}
|
||||
}
|
||||
}, GetSchedulesModel)
|
||||
// Get schedule by ID
|
||||
.get('/:id', async ({ user, params, set }) => {
|
||||
try {
|
||||
const schedule = await getSchedule(params.id)
|
||||
|
||||
if (!schedule) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'Schedule not found',
|
||||
}
|
||||
}
|
||||
|
||||
if (schedule.user !== user.userId) {
|
||||
set.status = 403
|
||||
return {
|
||||
success: false,
|
||||
error: 'Forbidden: You do not have permission to access this schedule',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: schedule,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch schedule',
|
||||
}
|
||||
}
|
||||
}, GetScheduleByIdModel)
|
||||
// Create new schedule
|
||||
.post('/', async ({ user, body, set }) => {
|
||||
try {
|
||||
const newSchedule = await createSchedule(user.userId, body)
|
||||
|
||||
// 启动定时任务
|
||||
scheduleTask(user.userId, {
|
||||
id: newSchedule.id!,
|
||||
pattern: newSchedule.pattern,
|
||||
name: newSchedule.name,
|
||||
description: newSchedule.description,
|
||||
command: newSchedule.command,
|
||||
maxCalls: newSchedule.maxCalls || undefined,
|
||||
})
|
||||
|
||||
set.status = 201
|
||||
return {
|
||||
success: true,
|
||||
data: newSchedule,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create schedule',
|
||||
}
|
||||
}
|
||||
}, CreateScheduleModel)
|
||||
// Update schedule
|
||||
.put('/:id', async ({ user, params, body, set }) => {
|
||||
try {
|
||||
const updatedSchedule = await updateSchedule(params.id, user.userId, body)
|
||||
|
||||
if (!updatedSchedule) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'Schedule not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedSchedule,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('Forbidden')) {
|
||||
set.status = 403
|
||||
} else {
|
||||
set.status = 500
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to update schedule',
|
||||
}
|
||||
}
|
||||
}, UpdateScheduleModel)
|
||||
// Delete schedule
|
||||
.delete('/:id', async ({ user, params, set }) => {
|
||||
try {
|
||||
const deletedSchedule = await deleteSchedule(params.id, user.userId)
|
||||
|
||||
if (!deletedSchedule) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'Schedule not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: deletedSchedule,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('Forbidden')) {
|
||||
set.status = 403
|
||||
} else {
|
||||
set.status = 500
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete schedule',
|
||||
}
|
||||
}
|
||||
}, DeleteScheduleModel)
|
||||
@@ -1,59 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
// 创建 Schedule 的 Schema
|
||||
const CreateScheduleSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').max(100),
|
||||
description: z.string().min(1, 'Description is required'),
|
||||
command: z.string().min(1, 'Command is required'),
|
||||
pattern: z.string().min(1, 'Cron pattern is required'),
|
||||
maxCalls: z.number().int().positive().optional(),
|
||||
})
|
||||
|
||||
// 更新 Schedule 的 Schema
|
||||
const UpdateScheduleSchema = z.object({
|
||||
name: z.string().min(1).max(100).optional(),
|
||||
description: z.string().optional(),
|
||||
command: z.string().optional(),
|
||||
pattern: z.string().optional(),
|
||||
maxCalls: z.number().int().positive().optional(),
|
||||
active: z.boolean().optional(),
|
||||
})
|
||||
|
||||
// 查询参数 Schema
|
||||
const GetSchedulesQuerySchema = z.object({
|
||||
page: z.string().optional(),
|
||||
limit: z.string().optional(),
|
||||
sortOrder: z.enum(['asc', 'desc']).optional(),
|
||||
})
|
||||
|
||||
export type CreateScheduleInput = z.infer<typeof CreateScheduleSchema>
|
||||
export type UpdateScheduleInput = z.infer<typeof UpdateScheduleSchema>
|
||||
export type GetSchedulesQuery = z.infer<typeof GetSchedulesQuerySchema>
|
||||
|
||||
export const CreateScheduleModel = {
|
||||
body: CreateScheduleSchema,
|
||||
}
|
||||
|
||||
export const UpdateScheduleModel = {
|
||||
params: z.object({
|
||||
id: z.string().uuid('Invalid schedule ID format'),
|
||||
}),
|
||||
body: UpdateScheduleSchema,
|
||||
}
|
||||
|
||||
export const GetScheduleByIdModel = {
|
||||
params: z.object({
|
||||
id: z.string().uuid('Invalid schedule ID format'),
|
||||
}),
|
||||
}
|
||||
|
||||
export const DeleteScheduleModel = {
|
||||
params: z.object({
|
||||
id: z.string().uuid('Invalid schedule ID format'),
|
||||
}),
|
||||
}
|
||||
|
||||
export const GetSchedulesModel = {
|
||||
query: GetSchedulesQuerySchema,
|
||||
}
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
import { db } from '@memoh/db'
|
||||
import { schedule } from '@memoh/db/schema'
|
||||
import { ChatModel, EmbeddingModel, Schedule } from '@memoh/shared'
|
||||
import { eq, desc, asc, and, sql } from 'drizzle-orm'
|
||||
import cron from 'node-cron'
|
||||
import { createAgent } from '../agent/service'
|
||||
import { getChatModel, getEmbeddingModel, getSummaryModel } from '../model/service'
|
||||
import { getSettings } from '../settings/service'
|
||||
import { calculateOffset, createPaginatedResult, type PaginatedResult } from '../../utils/pagination'
|
||||
import type { CreateScheduleInput, UpdateScheduleInput } from './model'
|
||||
|
||||
/**
|
||||
* Schedule 列表返回类型
|
||||
*/
|
||||
type ScheduleListItem = {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
command: string
|
||||
pattern: string
|
||||
maxCalls: number | null
|
||||
user: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
active: boolean
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取用户的所有 schedules(支持分页)
|
||||
*/
|
||||
export const getSchedules = async (
|
||||
userId: string,
|
||||
params?: {
|
||||
limit?: number
|
||||
page?: number
|
||||
sortOrder?: 'asc' | 'desc'
|
||||
}
|
||||
): Promise<PaginatedResult<ScheduleListItem>> => {
|
||||
const limit = params?.limit || 10
|
||||
const page = params?.page || 1
|
||||
const sortOrder = params?.sortOrder || 'desc'
|
||||
const offset = calculateOffset(page, limit)
|
||||
|
||||
// 获取总数
|
||||
const [{ count }] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(schedule)
|
||||
.where(eq(schedule.user, userId))
|
||||
|
||||
// 获取分页数据
|
||||
const orderFn = sortOrder === 'desc' ? desc : asc
|
||||
const schedules = await db
|
||||
.select()
|
||||
.from(schedule)
|
||||
.where(eq(schedule.user, userId))
|
||||
.orderBy(orderFn(schedule.createdAt))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
|
||||
return createPaginatedResult(schedules, Number(count), page, limit)
|
||||
}
|
||||
|
||||
export const getActiveSchedules = async (
|
||||
userId?: string
|
||||
) => {
|
||||
const schedules = await db
|
||||
.select().from(schedule)
|
||||
.where(and(...[
|
||||
userId ? eq(schedule.user, userId) : undefined,
|
||||
eq(schedule.active, true),
|
||||
]))
|
||||
.orderBy(desc(schedule.createdAt))
|
||||
return schedules
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 获取单个 schedule
|
||||
*/
|
||||
export const getSchedule = async (
|
||||
scheduleId: string
|
||||
) => {
|
||||
const [result] = await db
|
||||
.select()
|
||||
.from(schedule)
|
||||
.where(eq(schedule.id, scheduleId))
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的 schedule
|
||||
*/
|
||||
export const createSchedule = async (
|
||||
userId: string,
|
||||
data: CreateScheduleInput
|
||||
) => {
|
||||
const { scheduleTask } = createScheduler()
|
||||
const [newSchedule] = await db
|
||||
.insert(schedule)
|
||||
.values({
|
||||
user: userId,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
command: data.command,
|
||||
pattern: data.pattern,
|
||||
maxCalls: data.maxCalls || null,
|
||||
active: true,
|
||||
})
|
||||
.returning()
|
||||
|
||||
scheduleTask(userId, {
|
||||
id: newSchedule.id!,
|
||||
pattern: newSchedule.pattern,
|
||||
name: newSchedule.name,
|
||||
description: newSchedule.description,
|
||||
command: newSchedule.command,
|
||||
maxCalls: newSchedule.maxCalls || undefined,
|
||||
})
|
||||
|
||||
return newSchedule
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 schedule
|
||||
*/
|
||||
export const updateSchedule = async (
|
||||
scheduleId: string,
|
||||
userId: string,
|
||||
data: UpdateScheduleInput
|
||||
) => {
|
||||
// 检查 schedule 是否存在且属于该用户
|
||||
const existingSchedule = await getSchedule(scheduleId)
|
||||
if (!existingSchedule) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (existingSchedule.user !== userId) {
|
||||
throw new Error('Forbidden: You do not have permission to update this schedule')
|
||||
}
|
||||
|
||||
const [updatedSchedule] = await db
|
||||
.update(schedule)
|
||||
.set({
|
||||
...data,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schedule.id, scheduleId))
|
||||
.returning()
|
||||
|
||||
return updatedSchedule
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 schedule
|
||||
*/
|
||||
export const deleteSchedule = async (
|
||||
scheduleId: string,
|
||||
userId: string
|
||||
) => {
|
||||
// 检查 schedule 是否存在且属于该用户
|
||||
const existingSchedule = await getSchedule(scheduleId)
|
||||
if (!existingSchedule) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (existingSchedule.user !== userId) {
|
||||
throw new Error('Forbidden: You do not have permission to delete this schedule')
|
||||
}
|
||||
|
||||
const [deletedSchedule] = await db
|
||||
.delete(schedule)
|
||||
.where(eq(schedule.id, scheduleId))
|
||||
.returning()
|
||||
|
||||
return deletedSchedule
|
||||
}
|
||||
|
||||
export const setMaxCalls = async (
|
||||
scheduleId: string,
|
||||
maxCalls: number
|
||||
) => {
|
||||
await db
|
||||
.update(schedule)
|
||||
.set({ maxCalls })
|
||||
.where(eq(schedule.id, scheduleId))
|
||||
}
|
||||
|
||||
export const setActive = async (
|
||||
scheduleId: string,
|
||||
active: boolean
|
||||
) => {
|
||||
await db
|
||||
.update(schedule)
|
||||
.set({ active })
|
||||
.where(eq(schedule.id, scheduleId))
|
||||
}
|
||||
|
||||
export const createScheduler = () => {
|
||||
const scheduleTask = (userId: string, schedule: Schedule) => {
|
||||
const task = cron.schedule(schedule.pattern, async () => {
|
||||
const [chatModel, embeddingModel, summaryModel, userSettings] = await Promise.all([
|
||||
getChatModel(userId),
|
||||
getEmbeddingModel(userId),
|
||||
getSummaryModel(userId),
|
||||
getSettings(userId),
|
||||
])
|
||||
if (!chatModel || !embeddingModel || !summaryModel) {
|
||||
throw new Error('Model configuration not found. Please configure your models in settings.')
|
||||
}
|
||||
const agent = await createAgent({
|
||||
userId,
|
||||
chatModel: chatModel.model as ChatModel,
|
||||
embeddingModel: embeddingModel.model as EmbeddingModel,
|
||||
summaryModel: summaryModel.model as ChatModel,
|
||||
maxContextLoadTime: userSettings?.maxContextLoadTime || undefined,
|
||||
language: userSettings?.language || undefined,
|
||||
})
|
||||
await agent.triggerSchedule(schedule)
|
||||
}, {
|
||||
maxExecutions: schedule.maxCalls || undefined,
|
||||
})
|
||||
task.on('execution:finished', async () => {
|
||||
const { maxCalls } = await getSchedule(schedule.id!)
|
||||
if (maxCalls) {
|
||||
setMaxCalls(schedule.id!, maxCalls - 1)
|
||||
if (maxCalls - 1 === 0) {
|
||||
await setActive(schedule.id!, false)
|
||||
}
|
||||
}
|
||||
})
|
||||
task.on('execution:maxReached', async () => {
|
||||
await setActive(schedule.id!, false)
|
||||
})
|
||||
}
|
||||
|
||||
const resume = async () => {
|
||||
const schedules = await getActiveSchedules()
|
||||
for (const schedule of schedules) {
|
||||
scheduleTask(schedule.user, {
|
||||
id: schedule.id!,
|
||||
pattern: schedule.pattern,
|
||||
name: schedule.name,
|
||||
description: schedule.description,
|
||||
command: schedule.command,
|
||||
maxCalls: schedule.maxCalls || undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
scheduleTask,
|
||||
resume,
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import Elysia from 'elysia'
|
||||
import { authMiddleware } from '../../middlewares/auth'
|
||||
import { UpdateSettingsModel } from './model'
|
||||
import { getSettings, upsertSettings } from './service'
|
||||
|
||||
export const settingsModule = new Elysia({
|
||||
prefix: '/settings',
|
||||
})
|
||||
.use(authMiddleware)
|
||||
// Get current user's settings
|
||||
.get('/', async ({ user, set }) => {
|
||||
try {
|
||||
const userSettings = await getSettings(user.userId)
|
||||
if (!userSettings) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'Settings not found',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: userSettings,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch settings',
|
||||
}
|
||||
}
|
||||
})
|
||||
// Update or create current user's settings
|
||||
.put('/', async ({ user, body, set }) => {
|
||||
try {
|
||||
const result = await upsertSettings(user.userId, body)
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to update settings',
|
||||
}
|
||||
}
|
||||
}, UpdateSettingsModel)
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const SettingsBodySchema = z.object({
|
||||
defaultChatModel: z.string().uuid().nullable().optional(),
|
||||
defaultEmbeddingModel: z.string().uuid().nullable().optional(),
|
||||
defaultSummaryModel: z.string().uuid().nullable().optional(),
|
||||
maxContextLoadTime: z.number().int().min(1).max(1440).optional(), // 1 minute to 24 hours
|
||||
language: z.string().optional(),
|
||||
})
|
||||
|
||||
export type SettingsInput = z.infer<typeof SettingsBodySchema>
|
||||
|
||||
export const UpdateSettingsModel = {
|
||||
body: SettingsBodySchema,
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { db } from '@memoh/db'
|
||||
import { settings } from '@memoh/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { SettingsInput } from './model'
|
||||
|
||||
export const getSettings = async (userId: string) => {
|
||||
const [result] = await db
|
||||
.select()
|
||||
.from(settings)
|
||||
.where(eq(settings.userId, userId))
|
||||
return result
|
||||
}
|
||||
|
||||
export const upsertSettings = async (userId: string, data: SettingsInput) => {
|
||||
const updateData: Record<string, unknown> = {}
|
||||
|
||||
if (data.defaultChatModel !== undefined) {
|
||||
updateData.defaultChatModel = data.defaultChatModel
|
||||
}
|
||||
if (data.defaultEmbeddingModel !== undefined) {
|
||||
updateData.defaultEmbeddingModel = data.defaultEmbeddingModel
|
||||
}
|
||||
if (data.defaultSummaryModel !== undefined) {
|
||||
updateData.defaultSummaryModel = data.defaultSummaryModel
|
||||
}
|
||||
if (data.maxContextLoadTime !== undefined) {
|
||||
updateData.maxContextLoadTime = data.maxContextLoadTime
|
||||
}
|
||||
if (data.language !== undefined) {
|
||||
updateData.language = data.language
|
||||
}
|
||||
|
||||
const [result] = await db
|
||||
.insert(settings)
|
||||
.values({
|
||||
userId: userId,
|
||||
defaultChatModel: data.defaultChatModel || null,
|
||||
defaultEmbeddingModel: data.defaultEmbeddingModel || null,
|
||||
defaultSummaryModel: data.defaultSummaryModel || null,
|
||||
maxContextLoadTime: data.maxContextLoadTime || 60,
|
||||
language: data.language || 'Same as user input',
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: settings.userId,
|
||||
set: updateData,
|
||||
})
|
||||
.returning()
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
import Elysia from 'elysia'
|
||||
import { adminMiddleware } from '../../middlewares'
|
||||
import {
|
||||
GetUserByIdModel,
|
||||
CreateUserModel,
|
||||
UpdateUserModel,
|
||||
DeleteUserModel,
|
||||
UpdatePasswordModel,
|
||||
} from './model'
|
||||
import {
|
||||
getUsers,
|
||||
getUserById,
|
||||
createUser,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
updateUserPassword,
|
||||
} from './service'
|
||||
|
||||
export const userModule = new Elysia({
|
||||
prefix: '/user',
|
||||
})
|
||||
// 使用管理员中间件保护所有路由
|
||||
.use(adminMiddleware)
|
||||
// Get all users
|
||||
.get('/', async ({ query }) => {
|
||||
try {
|
||||
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,
|
||||
...result,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch users',
|
||||
}
|
||||
}
|
||||
})
|
||||
// Get user by ID
|
||||
.get('/:id', async ({ params, set }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const user = await getUserById(id)
|
||||
|
||||
if (!user) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'User not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: user,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch user',
|
||||
}
|
||||
}
|
||||
}, GetUserByIdModel)
|
||||
// Create new user
|
||||
.post('/', async ({ body, set }) => {
|
||||
try {
|
||||
const newUser = await createUser(body)
|
||||
set.status = 201
|
||||
return {
|
||||
success: true,
|
||||
data: newUser,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && (
|
||||
error.message.includes('already exists')
|
||||
)) {
|
||||
set.status = 409
|
||||
} else {
|
||||
set.status = 500
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create user',
|
||||
}
|
||||
}
|
||||
}, CreateUserModel)
|
||||
// Update user
|
||||
.put('/:id', async ({ params, body, set }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const updatedUser = await updateUser(id, body)
|
||||
|
||||
if (!updatedUser) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'User not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedUser,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('already exists')) {
|
||||
set.status = 409
|
||||
} else {
|
||||
set.status = 500
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to update user',
|
||||
}
|
||||
}
|
||||
}, UpdateUserModel)
|
||||
// Delete user
|
||||
.delete('/:id', async ({ params, set }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const deletedUser = await deleteUser(id)
|
||||
|
||||
if (!deletedUser) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'User not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: deletedUser,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete user',
|
||||
}
|
||||
}
|
||||
}, DeleteUserModel)
|
||||
// Update user password
|
||||
.patch('/:id/password', async ({ params, body, set }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const updatedUser = await updateUserPassword(id, body.password)
|
||||
|
||||
if (!updatedUser) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'User not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedUser,
|
||||
message: 'Password updated successfully',
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to update password',
|
||||
}
|
||||
}
|
||||
}, UpdatePasswordModel)
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
// 用户角色枚举
|
||||
const UserRoleSchema = z.enum(['admin', 'member'])
|
||||
|
||||
// 创建用户的 Schema
|
||||
const CreateUserSchema = z.object({
|
||||
username: z.string().min(3, 'Username must be at least 3 characters').max(50),
|
||||
email: z.string().email('Invalid email format').optional(),
|
||||
password: z.string().min(6, 'Password must be at least 6 characters'),
|
||||
role: UserRoleSchema.default('member'),
|
||||
displayName: z.string().optional(),
|
||||
avatarUrl: z.string().url('Invalid URL format').optional(),
|
||||
})
|
||||
|
||||
// 更新用户的 Schema
|
||||
const UpdateUserSchema = z.object({
|
||||
email: z.string().email('Invalid email format').optional(),
|
||||
role: UserRoleSchema.optional(),
|
||||
displayName: z.string().optional(),
|
||||
avatarUrl: z.string().url('Invalid URL format').optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
})
|
||||
|
||||
// 更新密码的 Schema
|
||||
const UpdatePasswordSchema = z.object({
|
||||
password: z.string().min(6, 'Password must be at least 6 characters'),
|
||||
})
|
||||
|
||||
export type CreateUserInput = z.infer<typeof CreateUserSchema>
|
||||
export type UpdateUserInput = z.infer<typeof UpdateUserSchema>
|
||||
export type UpdatePasswordInput = z.infer<typeof UpdatePasswordSchema>
|
||||
|
||||
export const GetUserByIdModel = {
|
||||
params: z.object({
|
||||
id: z.string().uuid('Invalid user ID format'),
|
||||
}),
|
||||
}
|
||||
|
||||
export const CreateUserModel = {
|
||||
body: CreateUserSchema,
|
||||
}
|
||||
|
||||
export const UpdateUserModel = {
|
||||
params: z.object({
|
||||
id: z.string().uuid('Invalid user ID format'),
|
||||
}),
|
||||
body: UpdateUserSchema,
|
||||
}
|
||||
|
||||
export const DeleteUserModel = {
|
||||
params: z.object({
|
||||
id: z.string().uuid('Invalid user ID format'),
|
||||
}),
|
||||
}
|
||||
|
||||
export const UpdatePasswordModel = {
|
||||
params: z.object({
|
||||
id: z.string().uuid('Invalid user ID format'),
|
||||
}),
|
||||
body: UpdatePasswordSchema,
|
||||
}
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
import { db } from '@memoh/db'
|
||||
import { users, settings } from '@memoh/db/schema'
|
||||
import { eq, sql, desc, asc } from 'drizzle-orm'
|
||||
import type { CreateUserInput, UpdateUserInput } from './model'
|
||||
import { calculateOffset, createPaginatedResult, type PaginatedResult } from '../../utils/pagination'
|
||||
|
||||
/**
|
||||
* 用户列表返回类型
|
||||
*/
|
||||
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,
|
||||
username: users.username,
|
||||
email: users.email,
|
||||
role: users.role,
|
||||
displayName: users.displayName,
|
||||
avatarUrl: users.avatarUrl,
|
||||
isActive: users.isActive,
|
||||
createdAt: users.createdAt,
|
||||
updatedAt: users.updatedAt,
|
||||
lastLoginAt: users.lastLoginAt,
|
||||
})
|
||||
.from(users)
|
||||
.orderBy(orderFn(orderColumn))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
|
||||
return createPaginatedResult(userList, Number(count), page, limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 获取用户
|
||||
*/
|
||||
export const getUserById = async (id: string) => {
|
||||
const [user] = await db
|
||||
.select({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
email: users.email,
|
||||
role: users.role,
|
||||
displayName: users.displayName,
|
||||
avatarUrl: users.avatarUrl,
|
||||
isActive: users.isActive,
|
||||
createdAt: users.createdAt,
|
||||
updatedAt: users.updatedAt,
|
||||
lastLoginAt: users.lastLoginAt,
|
||||
})
|
||||
.from(users)
|
||||
.where(eq(users.id, id))
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新用户
|
||||
*/
|
||||
export const createUser = async (data: CreateUserInput) => {
|
||||
// 检查用户名是否已存在
|
||||
const [existingUser] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, data.username))
|
||||
|
||||
if (existingUser) {
|
||||
throw new Error('Username already exists')
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在(如果提供了邮箱)
|
||||
if (data.email) {
|
||||
const [existingEmail] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, data.email))
|
||||
|
||||
if (existingEmail) {
|
||||
throw new Error('Email already exists')
|
||||
}
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
const passwordHash = await Bun.password.hash(data.password)
|
||||
|
||||
// 创建用户
|
||||
const [newUser] = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
username: data.username,
|
||||
email: data.email || null,
|
||||
passwordHash,
|
||||
role: data.role || 'member',
|
||||
displayName: data.displayName || null,
|
||||
avatarUrl: data.avatarUrl || null,
|
||||
})
|
||||
.returning({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
email: users.email,
|
||||
role: users.role,
|
||||
displayName: users.displayName,
|
||||
avatarUrl: users.avatarUrl,
|
||||
isActive: users.isActive,
|
||||
createdAt: users.createdAt,
|
||||
})
|
||||
|
||||
// 自动创建用户的 settings 条目(使用默认值)
|
||||
await db
|
||||
.insert(settings)
|
||||
.values({
|
||||
userId: newUser.id,
|
||||
defaultChatModel: null,
|
||||
defaultEmbeddingModel: null,
|
||||
defaultSummaryModel: null,
|
||||
maxContextLoadTime: 60,
|
||||
language: 'Same as user input',
|
||||
})
|
||||
|
||||
// 自动创建用户的容器
|
||||
try {
|
||||
const { createUserContainer } = await import('../container/service')
|
||||
await createUserContainer(newUser.id)
|
||||
console.log(`✅ Container created for user: ${newUser.username}`)
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to create container for user ${newUser.username}:`, error)
|
||||
// 不阻塞用户创建,容器可以后续创建
|
||||
}
|
||||
|
||||
return newUser
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
export const updateUser = async (id: string, data: UpdateUserInput) => {
|
||||
// 检查用户是否存在
|
||||
const existingUser = await getUserById(id)
|
||||
if (!existingUser) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 如果更新邮箱,检查邮箱是否已被其他用户使用
|
||||
if (data.email) {
|
||||
const [emailUser] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, data.email))
|
||||
|
||||
if (emailUser && emailUser.id !== id) {
|
||||
throw new Error('Email already exists')
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
const [updatedUser] = await db
|
||||
.update(users)
|
||||
.set({
|
||||
...data,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(users.id, id))
|
||||
.returning({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
email: users.email,
|
||||
role: users.role,
|
||||
displayName: users.displayName,
|
||||
avatarUrl: users.avatarUrl,
|
||||
isActive: users.isActive,
|
||||
createdAt: users.createdAt,
|
||||
updatedAt: users.updatedAt,
|
||||
lastLoginAt: users.lastLoginAt,
|
||||
})
|
||||
|
||||
return updatedUser
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
export const deleteUser = async (id: string) => {
|
||||
// 检查用户是否存在
|
||||
const existingUser = await getUserById(id)
|
||||
if (!existingUser) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [deletedUser] = await db
|
||||
.delete(users)
|
||||
.where(eq(users.id, id))
|
||||
.returning({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
})
|
||||
|
||||
return deletedUser
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户密码
|
||||
*/
|
||||
export const updateUserPassword = async (id: string, password: string) => {
|
||||
// 检查用户是否存在
|
||||
const existingUser = await getUserById(id)
|
||||
if (!existingUser) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
const passwordHash = await Bun.password.hash(password)
|
||||
|
||||
// 更新密码
|
||||
const [updatedUser] = await db
|
||||
.update(users)
|
||||
.set({
|
||||
passwordHash,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(users.id, id))
|
||||
.returning({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
})
|
||||
|
||||
return updatedUser
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user