refactor: remove all ts packages will no longer be used

This commit is contained in:
Acbox
2026-01-29 01:38:28 +08:00
parent af9023c87b
commit 0e56c85233
110 changed files with 0 additions and 8583 deletions
-111
View File
@@ -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)
-14
View File
@@ -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']>
-129
View File
@@ -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
}
-127
View File
@@ -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',
}
}
})
-13
View File
@@ -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,
}
-117
View File
@@ -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,
}
}
-268
View File
@@ -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',
}
}
-11
View File
@@ -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'
-152
View File
@@ -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)
-84
View File
@@ -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,
}
-233
View File
@@ -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))
}
-48
View File
@@ -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
}
-18
View File
@@ -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
}
-209
View File
@@ -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)
)
-55
View File
@@ -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'),
}),
}
-95
View File
@@ -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)
}
-235
View File
@@ -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)
)
-100
View File
@@ -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)
}
-172
View File
@@ -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
}
-182
View File
@@ -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)
-63
View File
@@ -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,
}
-268
View File
@@ -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
}