diff --git a/packages/agent/.gitignore b/packages/agent/.gitignore deleted file mode 100644 index 1ed3b73d..00000000 --- a/packages/agent/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -screenshot-*.png -memory.db \ No newline at end of file diff --git a/packages/agent/README.md b/packages/agent/README.md deleted file mode 100644 index 2da9b92d..00000000 --- a/packages/agent/README.md +++ /dev/null @@ -1 +0,0 @@ -# @memoh/agent diff --git a/packages/agent/client/README.md b/packages/agent/client/README.md deleted file mode 100644 index 32fd0b41..00000000 --- a/packages/agent/client/README.md +++ /dev/null @@ -1,107 +0,0 @@ -# Agent CLI - -A command-line interface for the personal housekeeper assistant agent. - -## Setup - -1. Create a `.env` file in the project root (not in this directory) with the following variables: - -```env -# Main chat model -MODEL=gpt-4o -BASE_URL=https://api.openai.com/v1 -API_KEY=your-api-key-here -MODEL_CLIENT_TYPE=openai - -# Embedding model for memory search (using mem0ai) -EMBEDDING_MODEL=text-embedding-3-small -EMBEDDING_BASE_URL=https://api.openai.com/v1 -EMBEDDING_API_KEY=your-api-key-here -EMBEDDING_CLIENT_TYPE=openai -EMBEDDING_DIMENSIONS=1536 - -# Summary model for memory generation (optional, defaults to main model) -SUMMARY_MODEL=gpt-4o-mini -SUMMARY_BASE_URL=https://api.openai.com/v1 -SUMMARY_API_KEY=your-api-key-here -SUMMARY_CLIENT_TYPE=openai - -# Database -DATABASE_URL=postgresql://user:password@localhost:5432/byte -``` - -2. Make sure the database is set up and running (required for memory storage). - -## Usage - -Run the CLI from the agent package: - -```bash -pnpm start -``` - -Or with Bun directly: - -```bash -bun run index.ts -``` - -## Features - -- **Interactive Chat**: Type your messages and get responses from the AI agent -- **Long-term Memory**: Conversations are automatically saved with LLM-generated summaries -- **Context Loading**: Automatically loads recent conversations (last 60 minutes) -- **Memory Search**: The agent can search through past conversations using natural language and embeddings -- **Tool Calling**: Supports automatic tool execution with multi-step reasoning -- **Multi-Provider Support**: Works with OpenAI, Anthropic, and Google AI (via Vercel AI SDK) - -## Commands - -- Type your message and press Enter to chat -- Type `exit` or `quit` to close the application - -## Environment Variables - -### Required - -- `MODEL`: The main LLM model ID (e.g., `gpt-4o`, `claude-3-5-sonnet-20241022`) -- `BASE_URL`: The API base URL for the main model -- `API_KEY`: Your API key for the main model -- `EMBEDDING_MODEL`: The embedding model for memory search (e.g., `text-embedding-3-small`) -- `DATABASE_URL`: PostgreSQL connection string with pgvector extension enabled - -### Optional - -- `MODEL_CLIENT_TYPE`: The model provider type (default: `openai`, options: `openai`, `anthropic`, `google`) -- `EMBEDDING_BASE_URL`: Base URL for embedding API (default: same as `BASE_URL`) -- `EMBEDDING_API_KEY`: API key for embedding (default: same as `API_KEY`) -- `EMBEDDING_CLIENT_TYPE`: Provider type for embedding (default: `openai`) -- `EMBEDDING_DIMENSIONS`: The dimensions of the embedding model (default: `1536`) -- `SUMMARY_MODEL`: The model used to summarize conversations for memory (default: same as `MODEL`) -- `SUMMARY_BASE_URL`: Base URL for summary model (default: same as `BASE_URL`) -- `SUMMARY_API_KEY`: API key for summary model (default: same as `API_KEY`) -- `SUMMARY_CLIENT_TYPE`: Provider type for summary model (default: same as `MODEL_CLIENT_TYPE`) - -## Memory System - -The agent uses [mem0ai](https://github.com/mem0ai/mem0) for sophisticated memory management: - -1. **Conversation Storage**: After each conversation, mem0ai uses an LLM to extract and store key information -2. **Embedding Generation**: The extracted information is converted to embedding vectors using your configured embedding model -3. **Vector Storage**: Embeddings are stored in PostgreSQL with pgvector extension -4. **Semantic Search**: The agent can search past memories using natural language queries via vector similarity -5. **Context Loading**: Recent conversations are automatically loaded from history into context - -### Model Recommendations - -- **Main Model**: Use a powerful model like `gpt-4o` or `claude-3-5-sonnet-20241022` for best conversation quality -- **Summary Model**: Use a cheaper/faster model like `gpt-4o-mini` for memory extraction to save costs -- **Embedding Model**: Use `text-embedding-3-small` (1536 dims) or `text-embedding-3-large` (3072 dims) for OpenAI - -### Database Requirements - -The memory system requires PostgreSQL with the `pgvector` extension installed. Make sure: -1. PostgreSQL is installed and running -2. pgvector extension is enabled: `CREATE EXTENSION IF NOT EXISTS vector;` -3. mem0ai will automatically create the required tables on first run - diff --git a/packages/agent/client/index.ts b/packages/agent/client/index.ts deleted file mode 100644 index d20c8af2..00000000 --- a/packages/agent/client/index.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { createInterface } from 'node:readline' -import { stdin as input, stdout as output } from 'node:process' -import { createAgent } from '../src/agent' -import { createMemory, filterByTimestamp, MemoryUnit } from '@memoh/memory' -import { ModelClientType, ChatModel, EmbeddingModel } from '@memoh/shared' - -// Load environment variables -const MODEL = process.env.MODEL -const BASE_URL = process.env.BASE_URL -const API_KEY = process.env.API_KEY -const MODEL_CLIENT_TYPE = process.env.MODEL_CLIENT_TYPE || 'openai' - -const EMBEDDING_MODEL = process.env.EMBEDDING_MODEL -const EMBEDDING_BASE_URL = process.env.EMBEDDING_BASE_URL || BASE_URL -const EMBEDDING_API_KEY = process.env.EMBEDDING_API_KEY || API_KEY -const EMBEDDING_CLIENT_TYPE = process.env.EMBEDDING_CLIENT_TYPE || 'openai' -const EMBEDDING_DIMENSIONS = parseInt(process.env.EMBEDDING_DIMENSIONS || '1536', 10) - -const SUMMARY_MODEL = process.env.SUMMARY_MODEL || MODEL -const SUMMARY_BASE_URL = process.env.SUMMARY_BASE_URL || BASE_URL -const SUMMARY_API_KEY = process.env.SUMMARY_API_KEY || API_KEY -const SUMMARY_CLIENT_TYPE = process.env.SUMMARY_CLIENT_TYPE || MODEL_CLIENT_TYPE - -if (!MODEL || !BASE_URL || !API_KEY || !EMBEDDING_MODEL) { - console.error('Error: Missing required environment variables') - console.error('Required: MODEL, BASE_URL, API_KEY, EMBEDDING_MODEL') - console.error('Optional: MODEL_CLIENT_TYPE (default: openai)') - console.error('Optional: SUMMARY_MODEL (default: same as MODEL)') - console.error('Optional: SUMMARY_BASE_URL (default: same as BASE_URL)') - console.error('Optional: SUMMARY_API_KEY (default: same as API_KEY)') - console.error('Optional: SUMMARY_CLIENT_TYPE (default: same as MODEL_CLIENT_TYPE)') - process.exit(1) -} - -const USER_ID = 'cli-user' - -// Create model configurations -const embeddingModel: EmbeddingModel = { - modelId: EMBEDDING_MODEL!, - baseUrl: EMBEDDING_BASE_URL!, - apiKey: EMBEDDING_API_KEY!, - clientType: EMBEDDING_CLIENT_TYPE as ModelClientType, - dimensions: EMBEDDING_DIMENSIONS, - name: `Embedding: ${EMBEDDING_MODEL}`, -} - -const summaryModel: ChatModel = { - modelId: SUMMARY_MODEL!, - baseUrl: SUMMARY_BASE_URL!, - apiKey: SUMMARY_API_KEY!, - clientType: SUMMARY_CLIENT_TYPE as ModelClientType, - name: `Summary: ${SUMMARY_MODEL}`, -} - -// Create memory instance -const memoryInstance = createMemory({ - summaryModel, - embeddingModel, -}) - -// Create agent -const agent = createAgent({ - model: { - modelId: MODEL, - baseUrl: BASE_URL, - apiKey: API_KEY, - clientType: MODEL_CLIENT_TYPE as ModelClientType, - name: MODEL, - }, - maxContextLoadTime: 60, // 60 minutes - language: 'Same as user input', - onReadMemory: async (from: Date, to: Date) => { - return await filterByTimestamp(from, to, USER_ID) - }, - onSearchMemory: async (query: string) => { - const results = await memoryInstance.searchMemory(query, USER_ID) - // Transform search results to MemoryUnit format - // Note: mem0ai returns semantic search results, not full conversation history - return results - }, - onFinish: async (messages) => { - // Save conversation to memory - const memoryUnit: MemoryUnit = { - messages: messages as unknown as MemoryUnit['messages'], - timestamp: new Date(), - user: USER_ID, - } - await memoryInstance.addMemory(memoryUnit) - }, -}) - -async function main() { - console.log('🤖 Agent CLI Started') - console.log('Type your message and press Enter. Type "exit" to quit.\n') - - // Load context - // await agent.loadContext() - - const rl = createInterface({ input, output }) - - rl.on('line', async (line) => { - const userInput = line.trim() - - if (userInput === 'exit' || userInput === 'quit') { - console.log('\n👋 Goodbye!') - rl.close() - process.exit(0) - } - - if (!userInput) { - rl.prompt() - return - } - - try { - process.stdout.write('\n🤖 ') - - let hasOutput = false - for await (const event of agent.ask(userInput)) { - if (event.type === 'text-delta' && 'text' in event && event.text) { - process.stdout.write(String(event.text)) - hasOutput = true - } else if (event.type === 'tool-call' && 'toolName' in event) { - process.stdout.write(`\n[Tool: ${event.toolName}]`) - hasOutput = true - } - } - - if (!hasOutput) { - process.stdout.write('(No response)') - } - console.log('\n') - } catch (error) { - console.error('\n❌ Error:', error instanceof Error ? error.message : String(error)) - console.log() - } - - rl.prompt() - }) - - rl.setPrompt('You: ') - rl.prompt() -} - -main().catch(console.error) - diff --git a/packages/agent/package.json b/packages/agent/package.json deleted file mode 100644 index a5501afb..00000000 --- a/packages/agent/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "@memoh/agent", - "version": "1.0.0", - "description": "Agent package for the phonetutor monorepo", - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "scripts": { - "test": "vitest", - "start": "bun run client/index.ts" - }, - "packageManager": "pnpm@10.27.0", - "dependencies": { - "@ai-sdk/anthropic": "^3.0.9", - "@ai-sdk/google": "^3.0.6", - "@ai-sdk/mcp": "^1.0.6", - "@ai-sdk/openai": "^3.0.7", - "@memoh/ai-gateway": "workspace:*", - "@memoh/memory": "workspace:*", - "@memoh/shared": "workspace:*", - "@modelcontextprotocol/sdk": "^1.25.2", - "ai": "^6.0.25", - "dotenv": "^17.2.3", - "sqlite3": "^5.1.7", - "xsai": "^0.4.1", - "zod": "^4.3.5" - }, - "pnpm": { - "onlyBuiltDependencies": [ - "sqlite3", - "mem0ai" - ] - } -} diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts deleted file mode 100644 index 1307ea5a..00000000 --- a/packages/agent/src/agent.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { streamText, generateText, ModelMessage, stepCountIs, UserModelMessage, Tool } from 'ai' -import { AgentParams } from './types' -import { system, schedule as schedulePrompt } from './prompts' -import { getMemoryTools, getScheduleTools, getMessageTools } from './tools' -import { createChatGateway } from '@memoh/ai-gateway' -import { MCPConnection, Schedule } from '@memoh/shared' -import { createMCPClient, MCPClient } from '@ai-sdk/mcp' -import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' - -export const createAgent = (params: AgentParams) => { - const messages: ModelMessage[] = [] - const mcpClients: MCPClient[] = [] - - const gateway = createChatGateway(params.model) - - const maxContextLoadTime = params.maxContextLoadTime ?? 24 * 60 // 24 hours - const language = params.language ?? 'Same as user input' - const platforms = params.platforms ?? [] - const currentPlatform = params.platforms - ? platforms.find(p => p.name === params.currentPlatform)?.name ?? 'Unknown Platform' - : 'client' - const mcpConnections = params.mcpConnections ?? [] - - const launchMCPConnections = async () => { - const launch = async (connection: MCPConnection) => { - if (connection.type === 'http' || connection.type === 'sse') { - return await createMCPClient({ - transport: { - url: connection.url, - headers: connection.headers, - type: connection.type, - } - }) - } else if (connection.type === 'stdio') { - // Build exec command for container execution - const commands = params.onBuildExecCommand?.([connection.command]) ?? [connection.command] - commands.push(...connection.args) - const [command, ...args] = commands - return await createMCPClient({ - transport: new StdioClientTransport({ - command, - args, - env: connection.env, - cwd: connection.cwd, - }), - }) - } - } - const connections = await Promise.all(mcpConnections.map(launch)) - return connections.filter(connection => connection !== undefined) - } - - const getTools = async () => { - const connections = await launchMCPConnections() - mcpClients.length = 0 - mcpClients.push(...connections) - const mcpTools = await Promise.all(connections.map(connection => connection.tools())) as Record[] - const tools = Object.assign({}, ...mcpTools) - return { - ...getMemoryTools({ - searchMemory: params.onSearchMemory ?? (() => Promise.resolve([])) - }), - ...getScheduleTools({ - onGetSchedules: params.onGetSchedules ?? (() => Promise.resolve([])), - onRemoveSchedule: params.onRemoveSchedule ?? (() => Promise.resolve()), - onSchedule: params.onSchedule ?? (() => Promise.resolve()), - }), - ...getMessageTools( - platforms, - params.onSendMessage ?? (() => Promise.resolve()) - ), - ...tools, - } - } - - const onComplete = async () => { - await Promise.all(mcpClients.map(client => client.close())) - } - - const loadContext = async () => { - const from = new Date(Date.now() - maxContextLoadTime * 60 * 1000) - const to = new Date() - const memory = await params.onReadMemory?.(from, to) ?? [] - const context = memory.flatMap(m => m.messages) - messages.unshift(...context) - } - - const getSystemPrompt = () => { - return system({ - date: new Date(), - language, - locale: params.locale, - maxContextLoadTime, - platforms, - currentPlatform, - }) - } - - const getSchedulePrompt = (schedule: Schedule) => { - return schedulePrompt({ - schedule, - locale: params.locale, - date: new Date(), - }) - } - - async function askDirectly(input: string) { - await loadContext() - const user = { - role: 'user', - content: input, - } as UserModelMessage - messages.push(user) - const { response } = await generateText({ - model: gateway, - system: getSystemPrompt(), - messages, - stopWhen: stepCountIs(50), - tools: await getTools(), - onFinish: async () => { - await onComplete() - }, - }) - await params.onFinish?.([ - user as ModelMessage, - ...response.messages, - ]) - } - - async function* ask(input: string) { - try { - await loadContext() - const user = { - role: 'user', - content: input, - } as UserModelMessage - messages.push(user) - const { fullStream, response } = streamText({ - model: gateway, - system: getSystemPrompt(), - prepareStep: async () => { - return { - system: getSystemPrompt(), - } - }, - stopWhen: stepCountIs(50), - messages, - tools: await getTools(), - onFinish: async () => { - await onComplete() - }, - }) - for await (const event of fullStream) { - yield event - } - - // Wait for response and save to memory - try { - const newMessages = (await response).messages - await params.onFinish?.([ - user as ModelMessage, - ...newMessages, - ]) - } catch (finishError) { - console.error('Error in onFinish callback:', finishError) - // Yield error event but don't throw - let the stream complete - yield { - type: 'error' as const, - error: finishError instanceof Error ? finishError.message : 'Failed to save conversation' - } - } - } catch (error) { - console.error('Error in agent.ask():', error) - yield { - type: 'error' as const, - error: error instanceof Error ? error.message : 'Unknown error occurred' - } - } - } - - const triggerSchedule = async (schedule: Schedule) => { - const prompt = getSchedulePrompt(schedule) - await askDirectly(prompt) - } - - return { - ask, - askDirectly, - loadContext, - getSystemPrompt, - getSchedulePrompt, - triggerSchedule, - onComplete, - launchMCPConnections, - } -} \ No newline at end of file diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts deleted file mode 100644 index e63e6783..00000000 --- a/packages/agent/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './agent' -export * from './types' - diff --git a/packages/agent/src/tools/index.ts b/packages/agent/src/tools/index.ts deleted file mode 100644 index b28ef4cb..00000000 --- a/packages/agent/src/tools/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './memory' -export * from './schedule' -export * from './message' \ No newline at end of file diff --git a/packages/agent/src/tools/memory.ts b/packages/agent/src/tools/memory.ts deleted file mode 100644 index 37313404..00000000 --- a/packages/agent/src/tools/memory.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { tool } from 'ai' -import { z } from 'zod' - -export interface GetMemoryToolParams { - searchMemory: (query: string) => Promise -} - -export const getMemoryTools = ({ searchMemory }: GetMemoryToolParams) => { - const searchMemoryTool = tool({ - description: 'Search chat history in the memory', - inputSchema: z.object({ - query: z.string().describe('The query to search the memory'), - }), - execute: async ({ query }) => { - const memory = await searchMemory(query) - console.log(memory) - return { - success: true, - memories: memory, - } - }, - }) - - return { - 'search-memory': searchMemoryTool, - } -} \ No newline at end of file diff --git a/packages/agent/src/tools/message.ts b/packages/agent/src/tools/message.ts deleted file mode 100644 index 7227219f..00000000 --- a/packages/agent/src/tools/message.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Platform } from '@memoh/shared' -import { SendMessageOptions } from '../types' -import { tool } from 'ai' -import z from 'zod' - -export const getMessageTools = ( - platforms: Platform[], - onSendMessage: (platform: string, options: SendMessageOptions) => Promise, -) => { - const sendMessageTool = tool({ - description: 'Send a message to a platform', - inputSchema: z.object({ - platform: z.enum(platforms.map(platform => platform.name)), - message: z.string(), - }), - execute: async ({ platform, message }) => { - await onSendMessage(platform, { message }) - }, - }) - - return { - 'send-message': sendMessageTool, - } -} \ No newline at end of file diff --git a/packages/agent/src/tools/schedule.ts b/packages/agent/src/tools/schedule.ts deleted file mode 100644 index 8915b0b5..00000000 --- a/packages/agent/src/tools/schedule.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Schedule } from '@memoh/shared' -import { tool } from 'ai' -import z from 'zod' - -export interface GetScheduleToolParams { - onGetSchedules: () => Promise - onRemoveSchedule: (id: string) => Promise - onSchedule: (schedule: Schedule) => Promise -} - -export const getScheduleTools = ({ onGetSchedules, onRemoveSchedule, onSchedule }: GetScheduleToolParams) => { - const getSchedulesTool = tool({ - description: 'Get the list of schedules', - inputSchema: z.object(), - execute: async () => { - const schedules = await onGetSchedules() - return { - success: true, - schedules, - } - }, - }) - - const removeScheduleTool = tool({ - description: 'Remove a schedule', - inputSchema: z.object({ - id: z.string().describe('The id of the schedule'), - }), - execute: async ({ id }) => { - await onRemoveSchedule(id) - }, - }) - - const scheduleTool = tool({ - description: 'Schedule a command', - inputSchema: z.object({ - pattern: z.string().describe('The pattern of the schedule with **Cron Syntax**'), - command: z.string().describe('The natural language command to execute, will send to you when the schedule is triggered'), - name: z.string().describe('The name of the schedule'), - description: z.string().describe('The description of the schedule'), - maxCalls: z.number().describe('The maximum number of calls to the schedule').optional(), - }), - execute: async ({ pattern, command, name, description, maxCalls }) => { - await onSchedule({ pattern, command, name, description, maxCalls }) - }, - }) - - return { - 'get-schedules': getSchedulesTool, - 'remove-schedule': removeScheduleTool, - 'schedule': scheduleTool, - } -} \ No newline at end of file diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts deleted file mode 100644 index fa6ed3cc..00000000 --- a/packages/agent/src/types.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { MemoryUnit } from '@memoh/memory' -import { ChatModel, MCPConnection, Platform, Schedule } from '@memoh/shared' -import { ModelMessage } from 'ai' - -export interface SendMessageOptions { - message: string -} - -export interface AgentParams { - model: ChatModel - - /** - * Unit: minutes - */ - maxContextLoadTime?: number - - locale?: Intl.LocalesArgument - - /** - * Preferred language of the assistant. - * @default 'Same as user input' - */ - language?: string - - platforms?: Platform[] - - currentPlatform?: string - - mcpConnections?: MCPConnection[] - - onBuildExecCommand?: (command: string[]) => string[] - - onExecCommand?: (command: string[]) => Promise<{ stdout: string, stderr: string, exitCode: number }> - - onSendMessage?: (platform: string, options: SendMessageOptions) => Promise - - onReadMemory?: (from: Date, to: Date) => Promise - - onSearchMemory?: (query: string) => Promise - - onSchedule?: (schedule: Schedule) => Promise - - onGetSchedules?: () => Promise - - onRemoveSchedule?: (id: string) => Promise - - onFinish?: (messages: ModelMessage[]) => Promise - - onError?: (error: Error) => Promise -} \ No newline at end of file diff --git a/packages/ai-gateway/README.md b/packages/ai-gateway/README.md deleted file mode 100644 index e50cda4f..00000000 --- a/packages/ai-gateway/README.md +++ /dev/null @@ -1 +0,0 @@ -# @memoh/ai-gateway diff --git a/packages/ai-gateway/package.json b/packages/ai-gateway/package.json deleted file mode 100644 index 4309f04e..00000000 --- a/packages/ai-gateway/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "@memoh/ai-gateway", - "version": "1.0.0", - "description": "AI Gateway for Memoh", - "exports": { - ".": "./src/index.ts" - }, - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { - "@ai-sdk/anthropic": "^3.0.9", - "@ai-sdk/google": "^3.0.6", - "@ai-sdk/openai": "^3.0.7", - "@memoh/shared": "workspace:*", - "ai": "^6.0.25" - }, - "packageManager": "pnpm@10.27.0" -} diff --git a/packages/ai-gateway/src/chat.ts b/packages/ai-gateway/src/chat.ts deleted file mode 100644 index 6cd3ce33..00000000 --- a/packages/ai-gateway/src/chat.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createGateway as createAiGateway } from 'ai' -import { createOpenAI } from '@ai-sdk/openai' -import { createAnthropic } from '@ai-sdk/anthropic' -import { createGoogleGenerativeAI } from '@ai-sdk/google' -import { ChatModel, ModelClientType } from '@memoh/shared' - -export const createChatGateway = (model: ChatModel) => { - const clients = { - [ModelClientType.OPENAI]: createOpenAI, - [ModelClientType.ANTHROPIC]: createAnthropic, - [ModelClientType.GOOGLE]: createGoogleGenerativeAI, - } - return (clients[model.clientType] ?? createAiGateway)({ - apiKey: model.apiKey, - baseURL: model.baseUrl, - })(model.modelId) -} \ No newline at end of file diff --git a/packages/ai-gateway/src/embedding.ts b/packages/ai-gateway/src/embedding.ts deleted file mode 100644 index b402b75e..00000000 --- a/packages/ai-gateway/src/embedding.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createOpenAI } from '@ai-sdk/openai' -import { EmbeddingModel } from '@memoh/shared' - -export const createEmbeddingGateway = (model: EmbeddingModel) => { - return createOpenAI({ - apiKey: model.apiKey, - baseURL: model.baseUrl, - }).embedding(model.modelId) -} \ No newline at end of file diff --git a/packages/ai-gateway/src/index.ts b/packages/ai-gateway/src/index.ts deleted file mode 100644 index c21d9941..00000000 --- a/packages/ai-gateway/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './chat' -export * from './embedding' \ No newline at end of file diff --git a/packages/api/.gitignore b/packages/api/.gitignore deleted file mode 100644 index 2466cdcc..00000000 --- a/packages/api/.gitignore +++ /dev/null @@ -1,44 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env.local -.env.development.local -.env.test.local -.env.production.local - -# vercel -.vercel - -**/*.trace -**/*.zip -**/*.tar.gz -**/*.tgz -**/*.log -package-lock.json -**/*.bun - -memory.db \ No newline at end of file diff --git a/packages/api/README.md b/packages/api/README.md deleted file mode 100644 index a82eabbc..00000000 --- a/packages/api/README.md +++ /dev/null @@ -1,192 +0,0 @@ -# @memoh/api - -API 服务器,基于 Elysia 构建。 - -## API 模块 - -### 认证模块 (`/auth`) - -用户认证和授权管理。 - -- `POST /auth/login` - 用户登录 -- `GET /auth/verify` - 验证 token -- `GET /auth/me` - 获取当前用户信息 - -详细文档:[AUTH_README.md](./AUTH_README.md) - -### 用户管理模块 (`/user`) 🔒 仅管理员 - -完整的用户 CRUD 操作接口。 - -- `GET /user` - 获取所有用户 -- `GET /user/:id` - 获取单个用户 -- `POST /user` - 创建用户 -- `PUT /user/:id` - 更新用户信息 -- `DELETE /user/:id` - 删除用户 -- `PATCH /user/:id/password` - 更新用户密码 - -详细文档:[USER_MANAGEMENT.md](./USER_MANAGEMENT.md) - -### 模型管理模块 (`/model`) - -AI 模型配置管理。 - -- `GET /model` - 获取所有模型 -- `GET /model/:id` - 获取单个模型 -- `POST /model` - 创建模型配置 -- `PUT /model/:id` - 更新模型配置 -- `DELETE /model/:id` - 删除模型配置 -- `GET /model/chat/default` - 获取默认聊天模型 -- `GET /model/summary/default` - 获取默认摘要模型 -- `GET /model/embedding/default` - 获取默认嵌入模型 - -### 设置模块 (`/settings`) 🔒 需要认证 - -用户偏好设置管理(自动使用当前登录用户)。 - -- `GET /settings` - 获取当前用户设置 -- `PUT /settings` - 更新当前用户设置(支持模型配置、Agent 参数等) - -详细文档:[SETTINGS_API.md](./SETTINGS_API.md) - -### 记忆模块 (`/memory`) 🔒 需要认证 - -用户记忆和对话历史管理(自动使用当前登录用户)。 - -- `POST /memory` - 添加记忆 -- `GET /memory/search` - 搜索记忆 -- `GET /memory/message` - 获取消息历史(分页) -- `GET /memory/message/filter` - 按日期范围过滤消息 - -详细文档:[API_CHANGES.md](./API_CHANGES.md) - -### Agent 模块 (`/agent`) 🔒 需要认证 - -AI Agent 智能对话接口,支持流式响应和记忆管理。 - -- `POST /agent/stream` - 流式对话(Server-Sent Events) - -详细文档:[AGENT_API.md](./AGENT_API.md) - -## 快速开始 - -### 安装依赖 - -```bash -pnpm install -``` - -### 配置环境变量 - -复制并编辑环境变量文件: - -```bash -cp ../../.env.example ../../.env -``` - -必需配置: -- `DATABASE_URL` - PostgreSQL 连接字符串 -- `ROOT_USER` - Root 超级管理员用户名 -- `ROOT_USER_PASSWORD` - Root 超级管理员密码 -- `JWT_SECRET` - JWT 签名密钥 - -### 启动开发服务器 - -```bash -pnpm run dev -``` - -服务器将在 `http://localhost:7002` 启动。 - -### 构建生产版本 - -```bash -pnpm run build -pnpm run start -``` - -## 认证和权限 - -### Bearer Token 认证 - -所有受保护的 API 端点需要在请求头中携带 JWT token: - -``` -Authorization: Bearer -``` - -### 权限级别 - -- **公开接口**:无需认证 -- **用户接口**:需要有效的 JWT token -- **管理员接口** 🔒:需要管理员角色的 JWT token - -## 中间件 - -### `authMiddleware` - -强制认证中间件,要求请求必须包含有效的 Bearer token。 - -```typescript -import { authMiddleware } from './middlewares' - -const protectedModule = new Elysia() - .use(authMiddleware) - .get('/protected', ({ user }) => { - return { message: `Hello ${user.username}!` } - }) -``` - -### `adminMiddleware` 🔒 - -管理员权限中间件,要求用户角色为 `admin`。 - -```typescript -import { adminMiddleware } from './middlewares' - -const adminModule = new Elysia() - .use(adminMiddleware) - .get('/admin-only', ({ user }) => { - return { message: 'Admin access granted' } - }) -``` - -### `optionalAuthMiddleware` - -可选认证中间件,如果有 token 则验证,没有则 `user` 为 `null`。 - -```typescript -import { optionalAuthMiddleware } from './middlewares' - -const publicModule = new Elysia() - .use(optionalAuthMiddleware) - .get('/public', ({ user }) => { - if (user) { - return { message: `Welcome back, ${user.username}!` } - } - return { message: 'Welcome, guest!' } - }) -``` - -## 运行测试 - -```bash -pnpm test -``` - -## 技术栈 - -- **Elysia** - 高性能 Web 框架 -- **@elysiajs/jwt** - JWT 认证插件 -- **@elysiajs/bearer** - Bearer token 提取插件 -- **@elysiajs/cors** - CORS 支持 -- **Drizzle ORM** - 数据库 ORM -- **Zod** - 数据验证 -- **Bun** - JavaScript 运行时 - -## 相关文档 - -- [认证系统](./AUTH_README.md) -- [用户管理](./USER_MANAGEMENT.md) -- [项目设置指南](../../SETUP.md) -- [数据库 Schema](../db/USERS_SCHEMA.md) \ No newline at end of file diff --git a/packages/api/bun.lock b/packages/api/bun.lock deleted file mode 100644 index 593fd77b..00000000 --- a/packages/api/bun.lock +++ /dev/null @@ -1,56 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "api", - "dependencies": { - "elysia": "latest", - }, - "devDependencies": { - "bun-types": "latest", - }, - }, - }, - "packages": { - "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "https://registry.npmmirror.com/@borewit/text-codec/-/text-codec-0.2.1.tgz", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], - - "@sinclair/typebox": ["@sinclair/typebox@0.34.47", "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.34.47.tgz", {}, "sha512-ZGIBQ+XDvO5JQku9wmwtabcVTHJsgSWAHYtVuM9pBNNR5E88v6Jcj/llpmsjivig5X8A8HHOb4/mbEKPS5EvAw=="], - - "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "https://registry.npmmirror.com/@tokenizer/inflate/-/inflate-0.4.1.tgz", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], - - "@tokenizer/token": ["@tokenizer/token@0.3.0", "https://registry.npmmirror.com/@tokenizer/token/-/token-0.3.0.tgz", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - - "@types/node": ["@types/node@25.0.3", "https://registry.npmmirror.com/@types/node/-/node-25.0.3.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], - - "bun-types": ["bun-types@1.3.5", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.5.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], - - "cookie": ["cookie@1.1.1", "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], - - "debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "elysia": ["elysia@1.4.21", "https://registry.npmmirror.com/elysia/-/elysia-1.4.21.tgz", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.6", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-bGSbPSGnkWbO0qUDKS5Q+6iEewBdMmIiJ8F0li4djZ6WjpixUQouOzePYscG1Lemdv6pZpFi1YPfI/kjeq2voA=="], - - "exact-mirror": ["exact-mirror@0.2.6", "https://registry.npmmirror.com/exact-mirror/-/exact-mirror-0.2.6.tgz", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-7s059UIx9/tnOKSySzUk5cPGkoILhTE4p6ncf6uIPaQ+9aRBQzQjc9+q85l51+oZ+P6aBxh084pD0CzBQPcFUA=="], - - "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "https://registry.npmmirror.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], - - "file-type": ["file-type@21.3.0", "https://registry.npmmirror.com/file-type/-/file-type-21.3.0.tgz", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="], - - "ieee754": ["ieee754@1.2.1", "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "memoirist": ["memoirist@0.4.0", "https://registry.npmmirror.com/memoirist/-/memoirist-0.4.0.tgz", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], - - "ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "openapi-types": ["openapi-types@12.1.3", "https://registry.npmmirror.com/openapi-types/-/openapi-types-12.1.3.tgz", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], - - "strtok3": ["strtok3@10.3.4", "https://registry.npmmirror.com/strtok3/-/strtok3-10.3.4.tgz", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], - - "token-types": ["token-types@6.1.2", "https://registry.npmmirror.com/token-types/-/token-types-6.1.2.tgz", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], - - "uint8array-extras": ["uint8array-extras@1.5.0", "https://registry.npmmirror.com/uint8array-extras/-/uint8array-extras-1.5.0.tgz", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], - - "undici-types": ["undici-types@7.16.0", "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - } -} diff --git a/packages/api/package.json b/packages/api/package.json deleted file mode 100644 index 7ad48e8a..00000000 --- a/packages/api/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "@memoh/api", - "version": "1.0.50", - "scripts": { - "dev": "bun run --env-file=../../.env --watch src/index.ts", - "build": "bun build src/index.ts --outfile dist/index.js --target bun --minify", - "start": "bun run dist/index.js", - "test": "vitest" - }, - "exports": { - "./client": "./src/client.ts" - }, - "dependencies": { - "@elysiajs/bearer": "^1.1.4", - "@elysiajs/cors": "^1.4.1", - "@elysiajs/cron": "^1.4.1", - "@elysiajs/eden": "^1.4.6", - "@elysiajs/jwt": "^1.2.0", - "@elysiajs/openapi": "^1.4.13", - "@memoh/agent": "workspace:*", - "@memoh/db": "workspace:*", - "@memoh/memory": "workspace:*", - "@memoh/shared": "workspace:*", - "@memoh/container": "workspace:*", - "@memoh/platform": "workspace:*", - "@memoh/platform-telegram": "workspace:*", - "drizzle-orm": "^0.45.1", - "elysia": "latest", - "node-cron": "^4.2.1", - "zod": "^4.3.5" - }, - "devDependencies": { - "bun-types": "latest" - }, - "module": "src/index.js" -} diff --git a/packages/api/src/client.ts b/packages/api/src/client.ts deleted file mode 100644 index 5822b084..00000000 --- a/packages/api/src/client.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { app } from './index' -import { treaty } from '@elysiajs/eden' - -export type ApiClient = typeof app - -export const createClient = ( - baseUrl: string = process.env.API_BASE_URL ?? 'http://localhost:7002', - token?: string, -) => { - return treaty(baseUrl, { - headers: token ? { - 'Authorization': `Bearer ${token}`, - } : undefined, - }) -} \ No newline at end of file diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts deleted file mode 100644 index f9059f24..00000000 --- a/packages/api/src/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Elysia } from 'elysia' -import { corsMiddleware, errorMiddleware } from './middlewares' -import { - agentModule, - authModule, - modelModule, - scheduleModule, - settingsModule, - userModule, - platformModule, - memoryModule, - mcpModule, - containerModule, -} from './modules' -import openapi from '@elysiajs/openapi' - -const port = process.env.API_SERVER_PORT || 7002 - -export const app = new Elysia() - .use(errorMiddleware) - .use(openapi()) - .use(corsMiddleware) - .use(authModule) - .use(agentModule) - .use(memoryModule) - .use(modelModule) - .use(scheduleModule) - .use(settingsModule) - .use(userModule) - .use(platformModule) - .use(mcpModule) - .use(containerModule) - .listen(port) - -console.log( - `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` -) diff --git a/packages/api/src/middlewares/auth.ts b/packages/api/src/middlewares/auth.ts deleted file mode 100644 index 914edd2f..00000000 --- a/packages/api/src/middlewares/auth.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { Elysia } from 'elysia' -import { bearer } from '@elysiajs/bearer' -import { jwt } from '@elysiajs/jwt' - -/** - * JWT 配置常量 - */ -const JWT_CONFIG = { - name: 'jwt', - secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production', - exp: process.env.JWT_EXPIRES_IN || '7d', -} - -/** - * 用户信息类型 - */ -export type AuthUser = { - userId: string - username: string - role: string -} - -/** - * 共享的基础认证插件 - * 提供 JWT 和 Bearer token 功能 - */ -export const jwtPlugin = new Elysia({ name: 'jwt-plugin' }) - .use(jwt(JWT_CONFIG)) - .use(bearer()) - -/** - * 认证中间件 - * 验证 Bearer token 并将用户信息注入到 context 中 - */ -export const authMiddleware = new Elysia({ name: 'auth' }) - .use(jwt(JWT_CONFIG)) - .use(bearer()) - .derive({ as: 'scoped' }, 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, - } as AuthUser, - } - }) - -/** - * 可选认证中间件 - * 如果有 token 则验证,没有 token 则继续(user 为 null) - */ -export const optionalAuthMiddleware = new Elysia({ name: 'optional-auth' }) - .use(jwt(JWT_CONFIG)) - .use(bearer()) - .derive({ as: 'scoped' }, async ({ bearer, jwt }) => { - if (!bearer) { - return { user: null as AuthUser | null } - } - - const payload = await jwt.verify(bearer) - - if (!payload) { - return { user: null as AuthUser | null } - } - - return { - user: { - userId: payload.userId as string, - username: payload.username as string, - role: payload.role as string, - } as AuthUser | null, - } - }) - -/** - * 管理员权限中间件 - * 验证 token 并检查用户是否为管理员 - */ -export const adminMiddleware = new Elysia({ name: 'admin' }) - .use(jwt(JWT_CONFIG)) - .use(bearer()) - .derive({ as: 'scoped' }, 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') - } - - const user: AuthUser = { - userId: payload.userId as string, - username: payload.username as string, - role: payload.role as string, - } - - // 检查是否为管理员 - if (user.role !== 'admin') { - set.status = 403 - throw new Error('Forbidden: Admin access required') - } - - return { user } - }) - diff --git a/packages/api/src/middlewares/cors.ts b/packages/api/src/middlewares/cors.ts deleted file mode 100644 index fdb7aa5a..00000000 --- a/packages/api/src/middlewares/cors.ts +++ /dev/null @@ -1,9 +0,0 @@ -import cors from '@elysiajs/cors' - -export const corsMiddleware = cors({ - origin: '*', - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'], - exposeHeaders: ['Content-Type', 'Authorization'], - credentials: true, -}) \ No newline at end of file diff --git a/packages/api/src/middlewares/error.ts b/packages/api/src/middlewares/error.ts deleted file mode 100644 index a83fb144..00000000 --- a/packages/api/src/middlewares/error.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Elysia } from 'elysia' - -/** - * 统一错误响应格式 - */ -export interface ErrorResponse { - success: false - error: string - code?: string - details?: unknown -} - -/** - * 统一成功响应格式 - */ -export interface SuccessResponse { - success: true - data: T - message?: string -} - -/** - * 统一错误处理中间件 - * 捕获所有未处理的错误并返回统一格式 - */ -export const errorMiddleware = new Elysia({ name: 'error' }) - .onError(({ code, error, set }) => { - console.error('[Error]', code, error) - - // 根据不同的错误类型设置不同的状态码和响应 - switch (code) { - case 'VALIDATION': - set.status = 400 - return { - success: false, - error: 'Validation failed', - code: 'VALIDATION_ERROR', - details: error.message, - } satisfies ErrorResponse - - case 'NOT_FOUND': - set.status = 404 - return { - success: false, - error: 'Resource not found', - code: 'NOT_FOUND', - } satisfies ErrorResponse - - case 'PARSE': - set.status = 400 - return { - success: false, - error: 'Invalid request format', - code: 'PARSE_ERROR', - details: error.message, - } satisfies ErrorResponse - - case 'INTERNAL_SERVER_ERROR': - set.status = 500 - return { - success: false, - error: 'Internal server error', - code: 'INTERNAL_SERVER_ERROR', - } satisfies ErrorResponse - - case 'UNKNOWN': - default: - // 处理自定义错误 - if (error instanceof Error) { - const message = error.message - - // 401 未授权错误 - if ( - message.includes('No bearer token') || - message.includes('Invalid or expired token') - ) { - set.status = 401 - return { - success: false, - error: message, - code: 'UNAUTHORIZED', - } satisfies ErrorResponse - } - - // 403 权限不足错误 - if (message.includes('Forbidden') || message.includes('Admin access required')) { - set.status = 403 - return { - success: false, - error: message, - code: 'FORBIDDEN', - } satisfies ErrorResponse - } - - // 409 冲突错误(如用户已存在) - if (message.includes('already exists')) { - set.status = 409 - return { - success: false, - error: message, - code: 'CONFLICT', - } satisfies ErrorResponse - } - - // 404 未找到错误 - if (message.includes('not found')) { - set.status = 404 - return { - success: false, - error: message, - code: 'NOT_FOUND', - } satisfies ErrorResponse - } - - // 默认 500 服务器错误 - set.status = 500 - return { - success: false, - error: message, - code: 'ERROR', - } satisfies ErrorResponse - } - - // 未知错误 - set.status = 500 - return { - success: false, - error: 'An unexpected error occurred', - code: 'UNKNOWN_ERROR', - } satisfies ErrorResponse - } - }) - diff --git a/packages/api/src/middlewares/index.ts b/packages/api/src/middlewares/index.ts deleted file mode 100644 index 2fa00005..00000000 --- a/packages/api/src/middlewares/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './auth' -export * from './cors' -export * from './error' \ No newline at end of file diff --git a/packages/api/src/modules/agent/index.ts b/packages/api/src/modules/agent/index.ts deleted file mode 100644 index bedd6a0f..00000000 --- a/packages/api/src/modules/agent/index.ts +++ /dev/null @@ -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) \ No newline at end of file diff --git a/packages/api/src/modules/agent/model.ts b/packages/api/src/modules/agent/model.ts deleted file mode 100644 index 0d5caee2..00000000 --- a/packages/api/src/modules/agent/model.ts +++ /dev/null @@ -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 - diff --git a/packages/api/src/modules/agent/service.ts b/packages/api/src/modules/agent/service.ts deleted file mode 100644 index 3aa1da05..00000000 --- a/packages/api/src/modules/agent/service.ts +++ /dev/null @@ -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 - -export interface CreateAgentStreamParams { - userId: string - chatModel: ChatModel - embeddingModel: EmbeddingModel - summaryModel: ChatModel - maxContextLoadTime?: number - language?: string - platform?: string - onFinish?: (messages: MessageType[]) => Promise -} - -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 -} - diff --git a/packages/api/src/modules/auth/index.ts b/packages/api/src/modules/auth/index.ts deleted file mode 100644 index 9c2c0f2a..00000000 --- a/packages/api/src/modules/auth/index.ts +++ /dev/null @@ -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', - } - } - }) - diff --git a/packages/api/src/modules/auth/model.ts b/packages/api/src/modules/auth/model.ts deleted file mode 100644 index 1f06e579..00000000 --- a/packages/api/src/modules/auth/model.ts +++ /dev/null @@ -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 - -export const LoginModel = { - body: LoginSchema, -} - diff --git a/packages/api/src/modules/auth/service.ts b/packages/api/src/modules/auth/service.ts deleted file mode 100644 index e7f5bf37..00000000 --- a/packages/api/src/modules/auth/service.ts +++ /dev/null @@ -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, - } -} - diff --git a/packages/api/src/modules/container/index.ts b/packages/api/src/modules/container/index.ts deleted file mode 100644 index f64e5c37..00000000 --- a/packages/api/src/modules/container/index.ts +++ /dev/null @@ -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', - }, - } - ) - diff --git a/packages/api/src/modules/container/model.ts b/packages/api/src/modules/container/model.ts deleted file mode 100644 index 6c8a72f2..00000000 --- a/packages/api/src/modules/container/model.ts +++ /dev/null @@ -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 - -/** - * 更新容器请求模型 - */ -export const UpdateContainerSchema = z.object({ - autoStart: z.boolean().optional(), -}) - -export type UpdateContainerInput = z.infer - -/** - * 容器操作请求模型 - */ -export const ContainerActionSchema = z.object({ - action: z.enum(['start', 'stop', 'restart', 'pause', 'resume']), -}) - -export type ContainerActionInput = z.infer - -/** - * 确保容器请求模型 - */ -export const EnsureContainerSchema = z.object({ - image: z.string().optional(), - namespace: z.string().optional(), -}) - -export type EnsureContainerInput = z.infer - -/** - * 容器响应模型 - */ -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 - diff --git a/packages/api/src/modules/container/service.ts b/packages/api/src/modules/container/service.ts deleted file mode 100644 index 1d75da4d..00000000 --- a/packages/api/src/modules/container/service.ts +++ /dev/null @@ -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 => { - return await dbGetAllContainers() -} - -/** - * 为用户创建容器 - */ -export const createUserContainer = async ( - userId: string, - image: string = 'docker.io/library/node:20-alpine', - namespace: string = 'default' -): Promise => { - // 检查用户是否已有容器 - 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - const existing = await getContainerByUserId(userId) - - if (existing) { - return existing - } - - // 创建新容器 - return await createUserContainer(userId, image, namespace) -} - -/** - * 同步所有容器状态 - */ -export const syncAllContainerStatus = async (): Promise => { - 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') -} - diff --git a/packages/api/src/modules/container/utils.ts b/packages/api/src/modules/container/utils.ts deleted file mode 100644 index 365446e8..00000000 --- a/packages/api/src/modules/container/utils.ts +++ /dev/null @@ -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', - } -} - diff --git a/packages/api/src/modules/index.ts b/packages/api/src/modules/index.ts deleted file mode 100644 index 404191ca..00000000 --- a/packages/api/src/modules/index.ts +++ /dev/null @@ -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' \ No newline at end of file diff --git a/packages/api/src/modules/mcp/index.ts b/packages/api/src/modules/mcp/index.ts deleted file mode 100644 index f0221bdf..00000000 --- a/packages/api/src/modules/mcp/index.ts +++ /dev/null @@ -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) diff --git a/packages/api/src/modules/mcp/model.ts b/packages/api/src/modules/mcp/model.ts deleted file mode 100644 index b966d92d..00000000 --- a/packages/api/src/modules/mcp/model.ts +++ /dev/null @@ -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 -export type UpdateMCPConnectionInput = z.infer -export type GetMCPConnectionsQuery = z.infer - -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, -} - diff --git a/packages/api/src/modules/mcp/service.ts b/packages/api/src/modules/mcp/service.ts deleted file mode 100644 index 942a0453..00000000 --- a/packages/api/src/modules/mcp/service.ts +++ /dev/null @@ -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> => { - 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`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)) -} - diff --git a/packages/api/src/modules/memory/index.ts b/packages/api/src/modules/memory/index.ts deleted file mode 100644 index 655552d9..00000000 --- a/packages/api/src/modules/memory/index.ts +++ /dev/null @@ -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) \ No newline at end of file diff --git a/packages/api/src/modules/memory/message/index.ts b/packages/api/src/modules/memory/message/index.ts deleted file mode 100644 index 98c0cb0f..00000000 --- a/packages/api/src/modules/memory/message/index.ts +++ /dev/null @@ -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) \ No newline at end of file diff --git a/packages/api/src/modules/memory/message/model.ts b/packages/api/src/modules/memory/message/model.ts deleted file mode 100644 index f8c6c263..00000000 --- a/packages/api/src/modules/memory/message/model.ts +++ /dev/null @@ -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(), - }), -} \ No newline at end of file diff --git a/packages/api/src/modules/memory/message/service.ts b/packages/api/src/modules/memory/message/service.ts deleted file mode 100644 index 73d5db45..00000000 --- a/packages/api/src/modules/memory/message/service.ts +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/packages/api/src/modules/memory/model.ts b/packages/api/src/modules/memory/model.ts deleted file mode 100644 index 93035b68..00000000 --- a/packages/api/src/modules/memory/model.ts +++ /dev/null @@ -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'), - }), -} - diff --git a/packages/api/src/modules/memory/service.ts b/packages/api/src/modules/memory/service.ts deleted file mode 100644 index c5ecdd6c..00000000 --- a/packages/api/src/modules/memory/service.ts +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/packages/api/src/modules/model/index.ts b/packages/api/src/modules/model/index.ts deleted file mode 100644 index 697fc682..00000000 --- a/packages/api/src/modules/model/index.ts +++ /dev/null @@ -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) - ) diff --git a/packages/api/src/modules/model/model.ts b/packages/api/src/modules/model/model.ts deleted file mode 100644 index 82222df2..00000000 --- a/packages/api/src/modules/model/model.ts +++ /dev/null @@ -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 - -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'), - }), -} diff --git a/packages/api/src/modules/model/service.ts b/packages/api/src/modules/model/service.ts deleted file mode 100644 index b65ac9ae..00000000 --- a/packages/api/src/modules/model/service.ts +++ /dev/null @@ -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> => { - 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`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) -} \ No newline at end of file diff --git a/packages/api/src/modules/platform/index.ts b/packages/api/src/modules/platform/index.ts deleted file mode 100644 index dbd1a044..00000000 --- a/packages/api/src/modules/platform/index.ts +++ /dev/null @@ -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, - 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) - 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>) - 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 } - - // 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 - - 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) - ) diff --git a/packages/api/src/modules/platform/model.ts b/packages/api/src/modules/platform/model.ts deleted file mode 100644 index 7ea52209..00000000 --- a/packages/api/src/modules/platform/model.ts +++ /dev/null @@ -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 = { - 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 - -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(), - }), -} - diff --git a/packages/api/src/modules/platform/service.ts b/packages/api/src/modules/platform/service.ts deleted file mode 100644 index eafbac6e..00000000 --- a/packages/api/src/modules/platform/service.ts +++ /dev/null @@ -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 - active: boolean - createdAt: Date - updatedAt: Date -} - -export const getPlatforms = async (params?: { - page?: number - limit?: number - sortOrder?: 'asc' | 'desc' -}): Promise> => { - 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`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 for type safety - const typedPlatforms = platforms.map(p => ({ - ...p, - config: p.config as Record, - })) - - 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) => { - 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, - active: newPlatform.active, - }) - } - return newPlatform -} - -export const updatePlatform = async (id: string, data: Partial>) => { - const updateData: { - name?: string - config?: Record - 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) => { - const [updatedPlatform] = await db - .update(platform) - .set({ - config, - updatedAt: new Date(), - }) - .where(eq(platform.id, id)) - .returning() - return updatedPlatform -} - -// active - -export const platformConstructors: Record = { - telegram: TelegramPlatform, -} - -export const platforms = new Map() - -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, - 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) -} diff --git a/packages/api/src/modules/schedule/index.ts b/packages/api/src/modules/schedule/index.ts deleted file mode 100644 index a7c60bc4..00000000 --- a/packages/api/src/modules/schedule/index.ts +++ /dev/null @@ -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) diff --git a/packages/api/src/modules/schedule/model.ts b/packages/api/src/modules/schedule/model.ts deleted file mode 100644 index a284da40..00000000 --- a/packages/api/src/modules/schedule/model.ts +++ /dev/null @@ -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 -export type UpdateScheduleInput = z.infer -export type GetSchedulesQuery = z.infer - -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, -} - diff --git a/packages/api/src/modules/schedule/service.ts b/packages/api/src/modules/schedule/service.ts deleted file mode 100644 index e83df146..00000000 --- a/packages/api/src/modules/schedule/service.ts +++ /dev/null @@ -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> => { - 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`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, - } -} diff --git a/packages/api/src/modules/settings/index.ts b/packages/api/src/modules/settings/index.ts deleted file mode 100644 index e1e27b06..00000000 --- a/packages/api/src/modules/settings/index.ts +++ /dev/null @@ -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) - diff --git a/packages/api/src/modules/settings/model.ts b/packages/api/src/modules/settings/model.ts deleted file mode 100644 index 6adaadc3..00000000 --- a/packages/api/src/modules/settings/model.ts +++ /dev/null @@ -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 - -export const UpdateSettingsModel = { - body: SettingsBodySchema, -} - diff --git a/packages/api/src/modules/settings/service.ts b/packages/api/src/modules/settings/service.ts deleted file mode 100644 index 6ebf8191..00000000 --- a/packages/api/src/modules/settings/service.ts +++ /dev/null @@ -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 = {} - - 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 -} - diff --git a/packages/api/src/modules/user/index.ts b/packages/api/src/modules/user/index.ts deleted file mode 100644 index 805d2633..00000000 --- a/packages/api/src/modules/user/index.ts +++ /dev/null @@ -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) - diff --git a/packages/api/src/modules/user/model.ts b/packages/api/src/modules/user/model.ts deleted file mode 100644 index 00e18d53..00000000 --- a/packages/api/src/modules/user/model.ts +++ /dev/null @@ -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 -export type UpdateUserInput = z.infer -export type UpdatePasswordInput = z.infer - -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, -} - diff --git a/packages/api/src/modules/user/service.ts b/packages/api/src/modules/user/service.ts deleted file mode 100644 index cb4fdf54..00000000 --- a/packages/api/src/modules/user/service.ts +++ /dev/null @@ -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> => { - 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`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 -} - diff --git a/packages/api/src/utils/pagination.ts b/packages/api/src/utils/pagination.ts deleted file mode 100644 index 5511246d..00000000 --- a/packages/api/src/utils/pagination.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * 分页参数接口 - */ -export interface PaginationParams { - page?: number - limit?: number - sortBy?: string - sortOrder?: 'asc' | 'desc' -} - -/** - * 分页结果接口 - */ -export interface PaginatedResult { - items: T[] - pagination: { - page: number - limit: number - total: number - totalPages: number - hasNext: boolean - hasPrev: boolean - } -} - -/** - * 解析分页参数 - */ -export function parsePaginationParams(query: Record): Required { - const page = Math.max(1, parseInt(query.page as string) || 1) - const limit = Math.min(100, Math.max(1, parseInt(query.limit as string) || 10)) - const sortBy = query.sortBy as string || 'createdAt' - const sortOrder = (query.sortOrder as string)?.toLowerCase() === 'desc' ? 'desc' : 'asc' - - return { - page, - limit, - sortBy, - sortOrder, - } -} - -/** - * 创建分页结果 - */ -export function createPaginatedResult( - items: T[], - total: number, - page: number, - limit: number -): PaginatedResult { - const totalPages = Math.ceil(total / limit) - - return { - items, - pagination: { - page, - limit, - total, - totalPages, - hasNext: page < totalPages, - hasPrev: page > 1, - }, - } -} - -/** - * 计算偏移量 - */ -export function calculateOffset(page: number, limit: number): number { - return (page - 1) * limit -} - diff --git a/packages/api/test/README.md b/packages/api/test/README.md deleted file mode 100644 index e1c86f0f..00000000 --- a/packages/api/test/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# API Tests - -这个目录包含了使用 Elysia Eden Client 编写的 API 测试。 - -## 测试文件 - -- `setup.ts` - 测试设置文件,配置测试服务器和 eden client -- `memory.test.ts` - Memory API 测试 -- `memory-message.test.ts` - Memory Message API 测试 -- `model.test.ts` - Model API 测试 -- `settings.test.ts` - Settings API 测试 - -## 运行测试 - -从项目根目录运行: - -```bash -pnpm test -``` - -或者只运行 API 包的测试: - -```bash -cd packages/api -pnpm test -``` - -## 测试说明 - -测试使用 vitest 作为测试框架,并使用 Elysia Eden Client (treaty) 来测试 API 端点。 - -测试服务器会在测试开始前启动(端口 7003),测试结束后自动关闭。 - -## 注意事项 - -- 确保数据库已配置并运行(某些测试可能需要数据库连接) -- 测试使用独立的测试端口(7003)以避免与开发服务器冲突 -- 某些测试可能需要先创建数据才能测试查询功能 - diff --git a/packages/api/test/memory-message.test.ts b/packages/api/test/memory-message.test.ts deleted file mode 100644 index 88c83113..00000000 --- a/packages/api/test/memory-message.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { getTestClient } from './setup' - -describe('Memory Message API', () => { - const client = getTestClient() - - describe('GET /memory/message', () => { - it('should get memory messages successfully', async () => { - const response = await client.memory.message.get({ - query: { - userId: 'test-user-123', - limit: 10, - page: 1, - }, - }) - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - expect(response.data?.success).toBe(true) - expect(response.data?.units).toBeDefined() - }) - - it('should use default limit and page when not provided', async () => { - const response = await client.memory.message.get({ - query: { - userId: 'test-user-123', - limit: 10, - page: 1, - }, - }) - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - expect(response.data?.success).toBe(true) - }) - - it('should return error for missing userId', async () => { - const response = await client.memory.message.get({ - // @ts-expect-error - Testing invalid input - query: { - limit: 10, - page: 1, - // missing userId - }, - }) - - expect([400, 422]).toContain(response.status) - }) - }) - - describe('GET /memory/message/filter', () => { - it('should filter memory messages successfully', async () => { - const response = await client.memory.message.filter.get({ - query: { - userId: 'test-user-123', - from: new Date('2024-01-01') as unknown as string, - to: new Date('2024-12-31') as unknown as string, - }, - }) - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - expect(response.data?.success).toBe(true) - expect(response.data?.units).toBeDefined() - }) - - it('should return error for missing required fields', async () => { - const response = await client.memory.message.filter.get({ - // @ts-expect-error - Testing invalid input - query: { - userId: 'test-user-123', - // missing from and to - }, - }) - - expect([400, 422]).toContain(response.status) - }) - }) -}) - diff --git a/packages/api/test/memory.test.ts b/packages/api/test/memory.test.ts deleted file mode 100644 index a8f07f3a..00000000 --- a/packages/api/test/memory.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { getTestClient } from './setup' - -describe('Memory API', () => { - const client = getTestClient() - - describe('POST /memory', () => { - it('should add memory successfully', async () => { - const memoryData = { - messages: [ - { role: 'user', content: 'Hello, this is a test message' }, - { role: 'assistant', content: 'Hello! How can I help you?' }, - ], - timestamp: new Date(), - user: 'test-user-123', - } - - const response = await client.memory.post(memoryData) - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - console.log(response.data?.error) - expect(response.data?.success).toBe(true) - expect(response.data?.data).toBeDefined() - }) - - it('should return error for invalid memory data', async () => { - const invalidData = { - messages: [], - // missing timestamp and user - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const response = await client.memory.post(invalidData as any) - - // Elysia 会返回 400 或 422 对于验证错误 - expect([400, 422]).toContain(response.status) - }) - }) - - describe('GET /memory/search', () => { - it('should search memory successfully', async () => { - const response = await client.memory.search.get({ - query: { - query: 'test search', - userId: 'test-user-123', - }, - }) - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - expect(response.data?.success).toBe(true) - expect(Array.isArray(response.data?.data)).toBe(true) - }) - - it('should return error for missing query', async () => { - const response = await client.memory.search.get({ - // @ts-expect-error - Testing invalid input - query: { - userId: 'test-user-123', - // missing query - }, - }) - - expect([400, 422]).toContain(response.status) - }) - - it('should return error for missing userId', async () => { - const response = await client.memory.search.get({ - // @ts-expect-error - Testing invalid input - query: { - query: 'test search', - // missing userId - }, - }) - - expect([400, 422]).toContain(response.status) - }) - }) -}) - diff --git a/packages/api/test/model.test.ts b/packages/api/test/model.test.ts deleted file mode 100644 index 38406622..00000000 --- a/packages/api/test/model.test.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest' -import { getTestClient } from './setup' - -describe('Model API', () => { - const client = getTestClient() - let createdModelId: string | null = null - - describe('GET /model', () => { - it('should get all models successfully', async () => { - const response = await client.model.get() - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - expect(response.data?.success).toBe(true) - expect(Array.isArray(response.data?.data)).toBe(true) - }) - }) - - describe('POST /model', () => { - it('should create a chat model successfully', async () => { - const modelData = { - modelId: 'test-chat-model', - baseUrl: 'https://api.openai.com/v1', - apiKey: 'test-api-key', - clientType: 'openai', - name: 'Test Chat Model', - type: 'chat' as const, - } - - const response = await client.model.post(modelData) - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - expect(response.data?.success).toBe(true) - expect(response.data?.data).toBeDefined() - - if (response.data?.data?.id) { - createdModelId = response.data.data.id - } - }) - - it('should create an embedding model successfully', async () => { - const modelData = { - modelId: 'test-embedding-model', - baseUrl: 'https://api.openai.com/v1', - apiKey: 'test-api-key', - clientType: 'openai', - name: 'Test Embedding Model', - type: 'embedding' as const, - dimensions: 1536, - } - - const response = await client.model.post(modelData) - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - expect(response.data?.success).toBe(true) - expect(response.data?.data).toBeDefined() - }) - - it('should return error for invalid model data', async () => { - const invalidData = { - // missing required fields - name: 'Invalid Model', - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const response = await client.model.post(invalidData as any) - - expect([400, 422]).toContain(response.status) - }) - - it('should return error for embedding model without dimensions', async () => { - const invalidData = { - modelId: 'test-embedding-model', - baseUrl: 'https://api.openai.com/v1', - apiKey: 'test-api-key', - clientType: 'openai', - type: 'embedding' as const, - // missing dimensions - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const response = await client.model.post(invalidData as any) - - expect([400, 422]).toContain(response.status) - }) - }) - - describe('GET /model/:id', () => { - it('should get model by id successfully', async () => { - if (!createdModelId) { - // 先创建一个模型 - const createResponse = await client.model.post({ - modelId: 'test-get-model', - baseUrl: 'https://api.openai.com/v1', - apiKey: 'test-api-key', - clientType: 'openai', - type: 'chat' as const, - }) - - if (createResponse.data?.data?.id) { - createdModelId = createResponse.data.data.id - } - } - - if (createdModelId) { - const response = await client.model({ - id: createdModelId, - }).get() - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - expect(response.data?.success).toBe(true) - expect(response.data?.data).toBeDefined() - } - }) - - it('should return error for non-existent model', async () => { - const response = await client.model({ - id: 'non-existent-id', - }).get() - - expect(response.status).toBe(200) // API 返回 200 但 success: false - expect(response.data?.success).toBe(false) - expect(response.data?.error).toBeDefined() - }) - }) - - describe('PUT /model/:id', () => { - it('should update model successfully', async () => { - if (!createdModelId) { - const createResponse = await client.model.post({ - modelId: 'test-update-model', - baseUrl: 'https://api.openai.com/v1', - apiKey: 'test-api-key', - clientType: 'openai', - type: 'chat' as const, - }) - - if (createResponse.data?.data?.id) { - createdModelId = createResponse.data.data.id - } - } - - if (createdModelId) { - const updateData = { - modelId: 'test-updated-model', - baseUrl: 'https://api.openai.com/v1', - apiKey: 'updated-api-key', - clientType: 'openai', - name: 'Updated Model', - type: 'chat' as const, - } - - const response = await client.model[createdModelId].put(updateData) - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - expect(response.data?.success).toBe(true) - expect(response.data?.data).toBeDefined() - } - }) - }) - - describe('DELETE /model/:id', () => { - it('should delete model successfully', async () => { - // 先创建一个模型用于删除 - const createResponse = await client.model.post({ - modelId: 'test-delete-model', - baseUrl: 'https://api.openai.com/v1', - apiKey: 'test-api-key', - clientType: 'openai', - type: 'chat' as const, - }) - - const modelId = createResponse.data?.data?.id - if (modelId) { - const response = await client.model[modelId].delete() - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - expect(response.data?.success).toBe(true) - } - }) - }) - - describe('GET /model/chat/default', () => { - it('should get default chat model successfully', async () => { - const response = await client.model.chat.default.get({ - query: { - userId: 'test-user-123', - }, - }) - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - // 可能返回 success: false 如果没有设置默认模型 - expect(response.data?.success !== undefined).toBe(true) - }) - - it('should return error for missing userId', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const response = await client.model.chat.default.get({ query: {} } as any) - - expect([400, 422]).toContain(response.status) - }) - }) - - describe('GET /model/summary/default', () => { - it('should get default summary model successfully', async () => { - const response = await client.model.summary.default.get({ - query: { - userId: 'test-user-123', - }, - }) - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - expect(response.data?.success !== undefined).toBe(true) - }) - }) - - describe('GET /model/embedding/default', () => { - it('should get default embedding model successfully', async () => { - const response = await client.model.embedding.default.get({ - query: { - userId: 'test-user-123', - }, - }) - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - expect(response.data?.success !== undefined).toBe(true) - }) - }) -}) - diff --git a/packages/api/test/settings.test.ts b/packages/api/test/settings.test.ts deleted file mode 100644 index f60aff20..00000000 --- a/packages/api/test/settings.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest' -import { getTestClient } from './setup' - -describe('Settings API', () => { - const client = getTestClient() - const testUserId = 'test-user-settings-123' - - describe('GET /settings/:userId', () => { - it('should get settings successfully', async () => { - const response = await client.settings({ - userId: testUserId, - }).get() - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - // 可能返回 success: false 如果设置不存在 - expect(response.data?.success !== undefined).toBe(true) - }) - }) - - describe('POST /settings', () => { - it('should create settings successfully', async () => { - const settingsData = { - userId: testUserId, - defaultChatModel: null, - defaultEmbeddingModel: null, - defaultSummaryModel: null, - } - - const response = await client.settings.post(settingsData) - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - expect(response.data?.success).toBe(true) - expect(response.data?.data).toBeDefined() - }) - - it('should create settings with model IDs', async () => { - const settingsData = { - userId: `${testUserId}-with-models`, - defaultChatModel: '00000000-0000-0000-0000-000000000001', - defaultEmbeddingModel: '00000000-0000-0000-0000-000000000002', - defaultSummaryModel: '00000000-0000-0000-0000-000000000003', - } - - const response = await client.settings.post(settingsData) - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - expect(response.data?.success).toBe(true) - }) - - it('should return error for invalid UUID format', async () => { - const invalidData = { - userId: testUserId, - defaultChatModel: 'invalid-uuid', - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const response = await client.settings.post(invalidData as any) - - expect([400, 422]).toContain(response.status) - }) - - it('should return error for missing userId', async () => { - const invalidData = { - defaultChatModel: null, - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const response = await client.settings.post(invalidData as any) - - expect([400, 422]).toContain(response.status) - }) - }) - - describe('PUT /settings/:userId', () => { - it('should update settings successfully', async () => { - // 先创建设置 - await client.settings.post({ - userId: `${testUserId}-update`, - defaultChatModel: null, - }) - - const updateData = { - defaultChatModel: '00000000-0000-0000-0000-000000000001', - defaultEmbeddingModel: '00000000-0000-0000-0000-000000000002', - defaultSummaryModel: null, - } - - const response = await client.settings({ - userId: `${testUserId}-update`, - }).put(updateData) - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - expect(response.data?.success).toBe(true) - expect(response.data?.data).toBeDefined() - }) - - it('should return error for non-existent settings', async () => { - const updateData = { - defaultChatModel: '00000000-0000-0000-0000-000000000001', - } - - const response = await client.settings({ - userId: 'non-existent-user', - }).put(updateData) - - expect(response.status).toBe(200) // API 返回 200 但 success: false - expect(response.data?.success).toBe(false) - expect(response.data?.error).toBeDefined() - }) - }) - - describe('PATCH /settings', () => { - it('should upsert settings successfully', async () => { - const settingsData = { - userId: `${testUserId}-upsert`, - defaultChatModel: '00000000-0000-0000-0000-000000000001', - defaultEmbeddingModel: null, - defaultSummaryModel: null, - } - - const response = await client.settings.patch(settingsData) - - expect(response.status).toBe(200) - expect(response.data).toBeDefined() - expect(response.data?.success).toBe(true) - expect(response.data?.data).toBeDefined() - }) - - it('should update existing settings on upsert', async () => { - const userId = `${testUserId}-upsert-update` - - // 先创建 - await client.settings.post({ - userId, - defaultChatModel: null, - }) - - // 然后 upsert - const upsertData = { - userId, - defaultChatModel: '00000000-0000-0000-0000-000000000001', - } - - const response = await client.settings.patch(upsertData) - - expect(response.status).toBe(200) - expect(response.data?.success).toBe(true) - }) - }) -}) - diff --git a/packages/api/test/setup.ts b/packages/api/test/setup.ts deleted file mode 100644 index 567fac7f..00000000 --- a/packages/api/test/setup.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createClient } from '../src/client' - -export const getTestClient = () => { - return createClient() -} - - diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json deleted file mode 100644 index 12a1a66a..00000000 --- a/packages/api/tsconfig.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "ES2022", /* Specify what module code is generated. */ - // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "Bundler", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true, /* Skip type checking all .d.ts files. */ - "paths": { - "@/*": ["./src/*"] - }, - }, - "include": ["src/**/*", "src/**/**/*"] -} diff --git a/packages/container/README.md b/packages/container/README.md deleted file mode 100644 index 2219baaa..00000000 --- a/packages/container/README.md +++ /dev/null @@ -1,370 +0,0 @@ -# @memoh/container - -基于 nerdctl (containerd) 的容器化工具包,提供简单易用的容器管理 API。 - -## 特性 - -- 🚀 基于 nerdctl 的现代容器管理(Docker 兼容) -- 📦 简洁的 API 设计 -- 🔧 完整的容器生命周期管理 -- 📝 TypeScript 支持 -- 🎯 命名空间隔离 - -## 安装 - -```bash -pnpm install @memoh/container -``` - -## 前置要求 - -### macOS (推荐使用 Lima) - -```bash -# 安装 Lima -brew install lima - -# 启动 Lima(已包含 nerdctl) -limactl start - -# 验证 -lima nerdctl version -``` - -### Linux - -```bash -# 安装 nerdctl -# 参考: https://github.com/containerd/nerdctl/releases - -# 或使用包管理器 -brew install nerdctl # Homebrew on Linux -``` - -**详细 macOS 配置请参考 [NERDCTL_SETUP.md](./NERDCTL_SETUP.md)** - -## 快速开始 - -### 创建容器 - -使用 `createContainer` 创建一个新容器: - -```typescript -import { createContainer } from '@memoh/container'; - -const container = await createContainer({ - name: 'my-nginx', - image: 'docker.io/library/nginx:latest', - env: { - PORT: '8080', - NODE_ENV: 'production', - }, -}); - -console.log('Container created:', container.id); -console.log('Status:', container.status); -``` - -### 操作容器 - -使用 `useContainer` 获取容器操作方法: - -```typescript -import { useContainer } from '@memoh/container'; - -const container = useContainer('my-nginx'); - -// 启动容器 -await container.start(); - -// 获取容器信息 -const info = await container.info(); -console.log('Container status:', info.status); - -// 执行命令 -const result = await container.exec(['nginx', '-v']); -console.log('Output:', result.stdout); - -// 查看日志 -const logs = await container.logs(); -console.log(logs); - -// 暂停容器 -await container.pause(); - -// 恢复容器 -await container.resume(); - -// 停止容器 -await container.stop(10); // 10秒超时 - -// 删除容器 -await container.remove(); -``` - -## API 文档 - -### createContainer - -创建并返回容器信息。 - -```typescript -function createContainer( - config: ContainerConfig, - options?: ContainerdOptions -): Promise -``` - -**参数:** - -- `config.name` - 容器名称(必需) -- `config.image` - 镜像引用(必需) -- `config.command` - 容器启动命令 -- `config.env` - 环境变量 -- `config.workingDir` - 工作目录 -- `config.namespace` - 命名空间(默认:default) -- `config.labels` - 容器标签 - -**返回:** `ContainerInfo` 对象 - -### useContainer - -获取容器操作方法。 - -```typescript -function useContainer( - containerIdOrName: string, - options?: ContainerdOptions -): ContainerOperations -``` - -**返回的操作方法:** - -- `start()` - 启动容器 -- `stop(timeout?)` - 停止容器 -- `restart(timeout?)` - 重启容器 -- `pause()` - 暂停容器 -- `resume()` - 恢复容器 -- `remove(force?)` - 删除容器 -- `exec(command)` - 执行命令 -- `info()` - 获取容器信息 -- `logs(follow?)` - 获取日志 -- `stats()` - 获取统计信息 - -### listContainers - -列出所有容器。 - -```typescript -function listContainers(options?: ContainerdOptions): Promise -``` - -**示例:** - -```typescript -import { listContainers } from '@memoh/container'; - -const containers = await listContainers(); -for (const container of containers) { - console.log(`${container.name}: ${container.status}`); -} -``` - -### containerExists - -检查容器是否存在。 - -```typescript -function containerExists( - containerIdOrName: string, - options?: ContainerdOptions -): Promise -``` - -### removeAllContainers - -删除所有容器。 - -```typescript -function removeAllContainers( - force?: boolean, - options?: ContainerdOptions -): Promise -``` - -## 高级用法 - -### 自定义命名空间 - -```typescript -import { createContainer, useContainer } from '@memoh/container'; - -// 在自定义命名空间中创建容器 -const container = await createContainer( - { - name: 'my-app', - image: 'docker.io/library/node:18', - }, - { - namespace: 'production', - } -); - -// 操作同一命名空间的容器 -const ops = useContainer('my-app', { namespace: 'production' }); -await ops.start(); -``` - -### 自定义 Socket 路径 - -```typescript -const container = useContainer('my-app', { - socket: '/custom/path/to/containerd.sock', -}); -``` - -### 完整示例:Web 服务部署 - -```typescript -import { createContainer, useContainer } from '@memoh/container'; - -async function deployWebService() { - // 创建容器 - const container = await createContainer({ - name: 'web-service', - image: 'docker.io/library/nginx:alpine', - env: { - NGINX_PORT: '8080', - }, - labels: { - app: 'web-service', - version: '1.0.0', - }, - }); - - console.log('Container created:', container.id); - - // 启动容器 - const ops = useContainer(container.name); - await ops.start(); - console.log('Container started'); - - // 等待服务就绪 - await new Promise(resolve => setTimeout(resolve, 2000)); - - // 检查状态 - const info = await ops.info(); - console.log('Status:', info.status); - - // 执行健康检查 - const health = await ops.exec(['curl', '-f', 'http://localhost:8080']); - if (health.exitCode === 0) { - console.log('Service is healthy'); - } - - return ops; -} - -// 使用 -const service = await deployWebService(); - -// 稍后停止服务 -await service.stop(); -await service.remove(); -``` - -## 类型定义 - -```typescript -interface ContainerConfig { - name: string; - image: string; - command?: string[]; - env?: Record; - workingDir?: string; - network?: string; - mounts?: Mount[]; - labels?: Record; - namespace?: string; -} - -interface ContainerInfo { - id: string; - name: string; - image: string; - status: ContainerStatus; - namespace: string; - createdAt: Date; - labels?: Record; -} - -type ContainerStatus = 'created' | 'running' | 'paused' | 'stopped' | 'unknown'; - -interface ExecResult { - exitCode: number; - stdout: string; - stderr: string; -} -``` - -## 注意事项 - -1. **权限要求**:操作容器通常需要 root 权限或将用户添加到适当的组 -2. **containerd 服务**:确保 containerd 服务正在运行 -3. **镜像拉取**:首次使用镜像时会自动拉取,可能需要一些时间 -4. **命名空间**:不同命名空间的容器相互隔离 -5. **清理资源**:使用完容器后记得清理(stop + remove) - -## 故障排查 - -### 命令未找到 - -如果遇到 `ctr command not found` 错误: - -```bash -# 检查 containerd 是否安装 -which ctr - -# 安装 containerd -brew install containerd # macOS -apt-get install containerd # Linux -``` - -### 权限被拒绝 - -如果遇到权限错误: - -```bash -# 将用户添加到 docker 组(如果存在) -sudo usermod -aG docker $USER - -# 或者使用 sudo 运行你的程序 -sudo node your-script.js -``` - -### 容器无法启动 - -检查容器日志: - -```typescript -const container = useContainer('my-container'); -const logs = await container.logs(); -console.log(logs); -``` - -## 开发 - -```bash -# 安装依赖 -pnpm install - -# 运行测试 -pnpm test - -# 构建 -pnpm build -``` - -## 许可证 - -MIT diff --git a/packages/container/examples/basic.ts b/packages/container/examples/basic.ts deleted file mode 100644 index b2aee775..00000000 --- a/packages/container/examples/basic.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Basic usage examples for @memoh/container - */ - -import { createContainer, useContainer, listContainers } from '../src' - -async function main() { - console.log('🚀 Container Management Examples\n') - - // Example 1: Create and start a container - console.log('📦 Example 1: Create and start a container') - try { - const container = await createContainer({ - name: 'example-nginx', - image: 'docker.io/library/nginx:alpine', - env: { - NGINX_HOST: 'localhost', - NGINX_PORT: '80', - }, - labels: { - example: 'basic', - version: '1.0', - }, - }) - - console.log('✅ Container created:', container.id) - console.log(' Status:', container.status) - console.log(' Image:', container.image) - console.log('') - - // Start the container - const ops = useContainer(container.name) - await ops.start() - console.log('✅ Container started\n') - - // Get container info - const info = await ops.info() - console.log('📊 Container info:') - console.log(' Name:', info.name) - console.log(' Status:', info.status) - console.log(' Created:', info.createdAt) - console.log('') - - // Stop and remove - await ops.stop(5) - console.log('⏹️ Container stopped') - await ops.remove() - console.log('🗑️ Container removed\n') - } catch (error) { - console.error('❌ Error:', error) - } - - // Example 2: List all containers - console.log('📋 Example 2: List all containers') - try { - const containers = await listContainers() - if (containers.length === 0) { - console.log(' No containers found\n') - } else { - for (const container of containers) { - console.log(` - ${container.name}: ${container.status}`) - } - console.log('') - } - } catch (error) { - console.error('❌ Error:', error) - } - - // Example 3: Execute commands in container - console.log('🔧 Example 3: Execute commands in container') - try { - const container = await createContainer({ - name: 'example-alpine', - image: 'docker.io/library/alpine:latest', - command: ['sh', '-c', 'while true; do sleep 1; done'], - }) - - const ops = useContainer(container.name) - await ops.start() - console.log('✅ Container started') - - // Execute command - const result = await ops.exec(['echo', 'Hello from container!']) - console.log('📤 Command output:', result.stdout) - console.log(' Exit code:', result.exitCode) - console.log('') - - // Cleanup - await ops.stop(2) - await ops.remove() - console.log('🧹 Cleaned up\n') - } catch (error) { - console.error('❌ Error:', error) - } - - console.log('✨ All examples completed!') -} - -// Run examples -main().catch(console.error) - diff --git a/packages/container/package.json b/packages/container/package.json deleted file mode 100644 index da4ae6f5..00000000 --- a/packages/container/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "@memoh/container", - "version": "1.0.0", - "description": "Containerd-based container management utilities", - "exports": { - ".": "./src/index.ts" - }, - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { - "execa": "^9.5.2" - }, - "devDependencies": { - "@types/node": "^22.10.5", - "typescript": "^5.7.3" - }, - "packageManager": "pnpm@10.27.0" -} diff --git a/packages/container/src/container.ts b/packages/container/src/container.ts deleted file mode 100644 index ec2619c8..00000000 --- a/packages/container/src/container.ts +++ /dev/null @@ -1,252 +0,0 @@ -/** - * High-level container management API - */ - -import { NerdctlClient } from './nerdctl' -import type { - ContainerConfig, - ContainerInfo, - ContainerOperations, - ExecResult, - ContainerStats, - ContainerdOptions, -} from './types' - -/** - * Create a new container - * - * @param config - Container configuration - * @param options - Containerd client options - * @returns Container information including ID and metadata - * - * @example - * ```typescript - * const container = await createContainer({ - * name: 'my-app', - * image: 'docker.io/library/nginx:latest', - * env: { PORT: '8080' }, - * }); - * - * console.log('Container created:', container.id); - * ``` - */ -export async function createContainer( - config: ContainerConfig, - options?: ContainerdOptions -): Promise { - const client = new NerdctlClient(options) - - // Ensure image is pulled - await client.pullImage(config.image) - - // Create container - const containerInfo = await client.createContainer(config) - - return containerInfo -} - -/** - * Get container operations for an existing container - * - * @param containerIdOrName - Container ID or name - * @param options - Containerd client options - * @returns Object with methods to operate on the container - * - * @example - * ```typescript - * const container = useContainer('my-app'); - * - * // Start the container - * await container.start(); - * - * // Get container info - * const info = await container.info(); - * console.log('Status:', info.status); - * - * // Execute command - * const result = await container.exec(['echo', 'hello']); - * console.log(result.stdout); - * - * // Stop and remove - * await container.stop(); - * await container.remove(); - * ``` - */ -export function useContainer( - containerIdOrName: string, - options?: ContainerdOptions -): ContainerOperations { - const client = new NerdctlClient(options) - const containerName = containerIdOrName - - return { - /** - * Start the container - */ - async start(): Promise { - await client.startContainer(containerName) - }, - - /** - * Stop the container - * @param timeout - Graceful shutdown timeout in seconds (default: 10) - */ - async stop(timeout: number = 10): Promise { - await client.stopContainer(containerName, timeout) - }, - - /** - * Restart the container - * @param timeout - Graceful shutdown timeout in seconds (default: 10) - */ - async restart(timeout: number = 10): Promise { - await client.stopContainer(containerName, timeout) - await client.startContainer(containerName) - }, - - /** - * Pause the container - */ - async pause(): Promise { - await client.pauseContainer(containerName) - }, - - /** - * Resume a paused container - */ - async resume(): Promise { - await client.resumeContainer(containerName) - }, - - /** - * Remove the container - * @param force - Force remove even if running (default: false) - */ - async remove(force: boolean = false): Promise { - await client.removeContainer(containerName, force) - }, - - /** - * Execute a command in the container - * @param command - Command and arguments to execute - * @returns Execution result with exit code and output - */ - async exec(command: string[]): Promise { - const result = await client.execInContainer(containerName, command) - return { - exitCode: result.exitCode, - stdout: result.stdout, - stderr: result.stderr, - } - }, - - /** - * Get container information - */ - async info(): Promise { - return await client.getContainerInfo(containerName) - }, - - /** - * Get container logs - * @param follow - Follow log output (not implemented yet) - */ - async logs(follow: boolean = false): Promise { - if (follow) { - throw new Error('Follow mode not implemented yet') - } - return await client.getContainerLogs(containerName) - }, - - /** - * Get container stats - * Note: This is a placeholder implementation - * Real implementation would require parsing nerdctl metrics - */ - async stats(): Promise { - // This is a simplified implementation - // Full implementation would require parsing nerdctl metrics output - return { - cpuUsage: 0, - memoryUsage: 0, - memoryLimit: 0, - networkIO: { - rxBytes: 0, - txBytes: 0, - }, - } - }, - - buildExecCommand(command: string[]): string[] { - // nerdctl exec with -i to keep STDIN open for MCP servers - const socket = options?.socket ? ['--address', options.socket] : [] - return [...client.nerdctlCommand, ...socket, 'exec', '-i', containerName, ...command] - } - } -} - -/** - * List all containers in the namespace - * - * @param options - Containerd client options - * @returns Array of container information - * - * @example - * ```typescript - * const containers = await listContainers(); - * for (const container of containers) { - * console.log(`${container.name}: ${container.status}`); - * } - * ``` - */ -export async function listContainers(options?: ContainerdOptions): Promise { - const client = new NerdctlClient(options) - return await client.listContainers() -} - -/** - * Check if a container exists - * - * @param containerIdOrName - Container ID or name - * @param options - Containerd client options - * @returns True if container exists, false otherwise - * - * @example - * ```typescript - * if (await containerExists('my-app')) { - * console.log('Container exists'); - * } - * ``` - */ -export async function containerExists( - containerIdOrName: string, - options?: ContainerdOptions -): Promise { - const client = new NerdctlClient(options) - return await client.containerExists(containerIdOrName) -} - -/** - * Remove all containers in the namespace - * - * @param force - Force remove even if running - * @param options - Containerd client options - * - * @example - * ```typescript - * await removeAllContainers(true); - * console.log('All containers removed'); - * ``` - */ -export async function removeAllContainers( - force: boolean = false, - options?: ContainerdOptions -): Promise { - const client = new NerdctlClient(options) - const containers = await client.listContainers() - - for (const container of containers) { - await client.removeContainer(container.name, force) - } -} - diff --git a/packages/container/src/index.ts b/packages/container/src/index.ts deleted file mode 100644 index 28582a29..00000000 --- a/packages/container/src/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @memoh/container - Containerd-based container management utilities - */ - -// Export main API -export { - createContainer, - useContainer, - listContainers, - containerExists, - removeAllContainers, -} from './container' - -// Export clients -export { NerdctlClient } from './nerdctl' - -// Export types -export type { - ContainerConfig, - ContainerInfo, - ContainerStatus, - ContainerOperations, - ContainerStats, - ExecResult, - Mount, - ContainerdOptions, -} from './types' - diff --git a/packages/container/src/nerdctl.ts b/packages/container/src/nerdctl.ts deleted file mode 100644 index b08ac9ed..00000000 --- a/packages/container/src/nerdctl.ts +++ /dev/null @@ -1,339 +0,0 @@ -/** - * Nerdctl client implementation (Docker-compatible CLI for containerd) - */ - -import { execa } from 'execa' -import type { ContainerConfig, ContainerInfo, ContainerStatus, ContainerdOptions } from './types' - -/** - * Nerdctl client for managing containers - * Provides a Docker-like interface for containerd - */ -export class NerdctlClient { - private namespace: string - private socket?: string - private timeout: number - nerdctlCommand: string[] - - constructor(options: ContainerdOptions = {}) { - this.namespace = options.namespace || 'default' - this.socket = options.socket || process.env.CONTAINERD_SOCKET - this.timeout = options.timeout || 30000 - // Support commands like "lima nerdctl" - const rawCommand = options.nerdctlCommand || process.env.NERDCTL_COMMAND || 'nerdctl' - this.nerdctlCommand = rawCommand.split(' ').filter(part => part.length > 0) - } - - /** - * Build nerdctl command with global options - */ - private buildCommand(args: string[]): string[] { - // Split command to support "lima nerdctl" - const cmd = [...this.nerdctlCommand] - - // Add global options before the subcommand - if (this.socket) { - cmd.push('--address', this.socket) - } - - cmd.push('--namespace', this.namespace) - cmd.push(...args) - - return cmd - } - - /** - * Execute nerdctl command - */ - private async exec(args: string[]): Promise<{ stdout: string; stderr: string }> { - const cmd = this.buildCommand(args) - const [program, ...programArgs] = cmd - - try { - const result = await execa(program, programArgs, { - timeout: this.timeout, - }) - - return { - stdout: result.stdout, - stderr: result.stderr, - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - throw new Error(`Nerdctl command failed: ${message}`) - } - } - - /** - * Pull container image - */ - async pullImage(image: string): Promise { - await this.exec(['pull', image]) - } - - /** - * Create a new container - */ - async createContainer(config: ContainerConfig): Promise { - const args = ['container', 'create'] - - // Add container name - args.push('--name', config.name) - - // Add environment variables - if (config.env) { - for (const [key, value] of Object.entries(config.env)) { - args.push('--env', `${key}=${value}`) - } - } - - // Add working directory - if (config.workingDir) { - args.push('--workdir', config.workingDir) - } - - // Add labels - if (config.labels) { - for (const [key, value] of Object.entries(config.labels)) { - args.push('--label', `${key}=${value}`) - } - } - - // Add mounts (nerdctl uses Docker-style mount syntax) - if (config.mounts && config.mounts.length > 0) { - for (const mount of config.mounts) { - let mountStr = `type=${mount.type},src=${mount.source},dst=${mount.target}` - if (mount.readonly) { - mountStr += ',readonly' - } - args.push('--mount', mountStr) - } - } - - // Add image - args.push(config.image) - - // Add command if specified - if (config.command && config.command.length > 0) { - args.push(...config.command) - } - - await this.exec(args) - - // Return container info - return this.getContainerInfo(config.name) - } - - /** - * Start a container - */ - async startContainer(name: string): Promise { - // Check container status and handle accordingly - try { - const status = await this.getContainerStatus(name) - - if (status === 'running') { - console.log(`Container ${name} is already running`) - return - } - - if (status === 'paused') { - console.log(`Container ${name} is paused, unpausing first...`) - await this.exec(['unpause', name]) - return - } - - // For 'created' or 'stopped' status, we can start - } catch { - // Container might not exist, let start command handle it - } - - await this.exec(['start', name]) - } - - /** - * Stop a container - */ - async stopContainer(name: string, timeout: number = 10): Promise { - // Check if container is running - try { - const status = await this.getContainerStatus(name) - if (status !== 'running') { - console.log(`Container ${name} is not running (status: ${status})`) - return - } - } catch { - // Container might not exist, let stop command handle it - } - - await this.exec(['stop', '--time', timeout.toString(), name]) - } - - /** - * Pause a container - */ - async pauseContainer(name: string): Promise { - // Check if container is running before pausing - const status = await this.getContainerStatus(name) - if (status !== 'running') { - console.log(`Container ${name} cannot be paused (status: ${status})`) - return - } - - await this.exec(['pause', name]) - } - - /** - * Resume a paused container - */ - async resumeContainer(name: string): Promise { - // Check if container is paused before resuming - const status = await this.getContainerStatus(name) - if (status !== 'paused') { - console.log(`Container ${name} is not paused (status: ${status})`) - return - } - - await this.exec(['unpause', name]) - } - - /** - * Remove a container - */ - async removeContainer(name: string, force: boolean = false): Promise { - const args = ['rm'] - if (force) { - args.push('--force') - } - args.push(name) - await this.exec(args) - } - - /** - * Execute command in container - */ - async execInContainer(name: string, command: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> { - const args = ['exec', name, ...command] - - try { - const result = await this.exec(args) - return { - stdout: result.stdout, - stderr: result.stderr, - exitCode: 0, - } - } catch (error: unknown) { - const err = error as { stdout?: string; stderr?: string; exitCode?: number; message?: string } - return { - stdout: err.stdout || '', - stderr: err.stderr || err.message || '', - exitCode: err.exitCode || 1, - } - } - } - - /** - * Get container information - */ - async getContainerInfo(name: string): Promise { - const result = await this.exec(['inspect', name]) - - try { - const data = JSON.parse(result.stdout) - const info = Array.isArray(data) ? data[0] : data - - // Parse nerdctl inspect output (similar to Docker) - return { - id: info.Id || name, - name: info.Name?.replace(/^\//, '') || name, - image: info.Config?.Image || info.Image || '', - status: this.parseStatus(info.State), - namespace: this.namespace, - createdAt: info.Created ? new Date(info.Created) : new Date(), - labels: info.Config?.Labels || {}, - } - } catch { - // Fallback if JSON parsing fails - return { - id: name, - name: name, - image: '', - status: 'unknown', - namespace: this.namespace, - createdAt: new Date(), - } - } - } - - /** - * Parse container status from inspect output - */ - private parseStatus(state: unknown): ContainerStatus { - const s = state as { Running?: boolean; Paused?: boolean; Status?: string; Dead?: boolean } - if (!s) return 'unknown' - - if (s.Running) return 'running' - if (s.Paused) return 'paused' - if (s.Status === 'created') return 'created' - if (s.Status === 'exited' || s.Dead) return 'stopped' - - return 'unknown' - } - - /** - * Get container status - */ - async getContainerStatus(name: string): Promise { - try { - const info = await this.getContainerInfo(name) - return info.status - } catch { - return 'unknown' - } - } - - /** - * Get container logs - */ - async getContainerLogs(name: string): Promise { - try { - const result = await this.exec(['logs', name]) - return result.stdout - } catch (error: unknown) { - return error instanceof Error ? error.message : '' - } - } - - /** - * List all containers - */ - async listContainers(): Promise { - const result = await this.exec(['ps', '--all', '--format', '{{.Names}}']) - const containerNames = result.stdout.split('\n').filter(name => name.trim()) - - const containers: ContainerInfo[] = [] - for (const name of containerNames) { - try { - const info = await this.getContainerInfo(name) - containers.push(info) - } catch { - // Skip containers that can't be accessed - } - } - - return containers - } - - /** - * Check if container exists - */ - async containerExists(name: string): Promise { - try { - await this.getContainerInfo(name) - return true - } catch { - return false - } - } -} - diff --git a/packages/container/src/types.ts b/packages/container/src/types.ts deleted file mode 100644 index 2e29b65f..00000000 --- a/packages/container/src/types.ts +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Container runtime types and interfaces - */ - -/** - * Container configuration options - */ -export interface ContainerConfig { - /** Container name/ID */ - name: string; - /** Container image reference */ - image: string; - /** Command to run in the container */ - command?: string[]; - /** Environment variables */ - env?: Record; - /** Working directory */ - workingDir?: string; - /** Network namespace */ - network?: string; - /** Mount points */ - mounts?: Mount[]; - /** Labels for the container */ - labels?: Record; - /** Container namespace (default: "default") */ - namespace?: string; -} - -/** - * Mount configuration - */ -export interface Mount { - /** Mount type: bind, volume, tmpfs */ - type: 'bind' | 'volume' | 'tmpfs'; - /** Source path (host) */ - source: string; - /** Target path (container) */ - target: string; - /** Read-only mount */ - readonly?: boolean; -} - -/** - * Container information - */ -export interface ContainerInfo { - /** Container ID */ - id: string; - /** Container name */ - name: string; - /** Container image */ - image: string; - /** Container status */ - status: ContainerStatus; - /** Container namespace */ - namespace: string; - /** Creation timestamp */ - createdAt: Date; - /** Labels */ - labels?: Record; -} - -/** - * Container status - */ -export type ContainerStatus = 'created' | 'running' | 'paused' | 'stopped' | 'unknown'; - -/** - * Container execution result - */ -export interface ExecResult { - /** Exit code */ - exitCode: number; - /** Standard output */ - stdout: string; - /** Standard error */ - stderr: string; -} - -/** - * Container stats - */ -export interface ContainerStats { - /** CPU usage percentage */ - cpuUsage: number; - /** Memory usage in bytes */ - memoryUsage: number; - /** Memory limit in bytes */ - memoryLimit: number; - /** Network I/O */ - networkIO?: { - rxBytes: number; - txBytes: number; - }; -} - -/** - * Container operations interface - */ -export interface ContainerOperations { - /** Build exec command */ - buildExecCommand(command: string[]): string[]; - /** Start the container */ - start(): Promise; - /** Stop the container */ - stop(timeout?: number): Promise; - /** Restart the container */ - restart(timeout?: number): Promise; - /** Pause the container */ - pause(): Promise; - /** Resume the container */ - resume(): Promise; - /** Remove the container */ - remove(force?: boolean): Promise; - /** Execute a command in the container */ - exec(command: string[]): Promise; - /** Get container info */ - info(): Promise; - /** Get container logs */ - logs(follow?: boolean): Promise; - /** Get container stats */ - stats(): Promise; -} - -/** - * Containerd client options - */ -export interface ContainerdOptions { - /** Containerd socket path */ - socket?: string; - /** Containerd namespace */ - namespace?: string; - /** Timeout for operations (ms) */ - timeout?: number; - /** nerdctl command */ - nerdctlCommand?: string; -} - diff --git a/packages/container/tsconfig.json b/packages/container/tsconfig.json deleted file mode 100644 index 4d3f7307..00000000 --- a/packages/container/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "lib": ["ES2020"], - "moduleResolution": "bundler", - "resolveJsonModule": true, - "esModuleInterop": true, - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "**/*.test.ts"] -} - diff --git a/packages/db/README.md b/packages/db/README.md deleted file mode 100644 index 07ea9374..00000000 --- a/packages/db/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# @memoh/db - -Database package for memoh project using Drizzle ORM and PostgreSQL. - -## Database Schema - -### Tables - -- **model**: AI model configurations -- **history**: Chat history records -- **settings**: User settings and preferences -- **users**: User accounts with authentication - -See [USERS_SCHEMA.md](./USERS_SCHEMA.md) for detailed user table documentation. - -## Quick Start - -### Initialize Database - -```bash -pnpm push -``` - -### Generate Schema - -```bash -pnpm generate -``` - -### Start Studio - -```bash -pnpm studio -``` \ No newline at end of file diff --git a/packages/db/drizzle.config.ts b/packages/db/drizzle.config.ts deleted file mode 100644 index 11c30e13..00000000 --- a/packages/db/drizzle.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { config } from 'dotenv' -import { defineConfig } from 'drizzle-kit' - -config({ path: '../../.env' }) - -export default defineConfig({ - out: './drizzle', - schema: './src/schema.ts', - dialect: 'postgresql', - dbCredentials: { - url: process.env.DATABASE_URL!, - }, -}) diff --git a/packages/db/package.json b/packages/db/package.json deleted file mode 100644 index 8535b8fc..00000000 --- a/packages/db/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@memoh/db", - "version": "1.0.0", - "type": "module", - "exports": { - ".": "./src/index.ts", - "./schema": "./src/schema.ts" - }, - "scripts": { - "push": "drizzle-kit push", - "generate": "drizzle-kit generate", - "migrate": "drizzle-kit migrate", - "studio": "drizzle-kit studio" - }, - "dependencies": { - "@memoh/shared": "workspace:*", - "dotenv": "^17.2.3", - "drizzle-orm": "^0.45.1", - "pg": "^8.16.3" - }, - "devDependencies": { - "@types/pg": "^8.16.0", - "drizzle-kit": "^0.31.8", - "tsx": "^4.21.0" - } -} diff --git a/packages/db/src/container-helpers.ts b/packages/db/src/container-helpers.ts deleted file mode 100644 index 4659d437..00000000 --- a/packages/db/src/container-helpers.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { db } from './index' -import { containers } from './container' -import { eq } from 'drizzle-orm' - -/** - * 容器信息类型 - */ -export type ContainerInfo = { - id: string - userId: string - containerId: string - containerName: string - image: string - status: string - namespace: string - autoStart: boolean - hostPath: string | null - containerPath: string - createdAt: Date - updatedAt: Date - lastStartedAt: Date | null - lastStoppedAt: Date | null -} - -/** - * 创建容器输入类型 - */ -export type CreateContainerInput = { - userId: string - containerId: string - containerName: string - image: string - namespace?: string - autoStart?: boolean - hostPath?: string - containerPath?: string -} - -/** - * 更新容器输入类型 - */ -export type UpdateContainerInput = { - status?: string - autoStart?: boolean - lastStartedAt?: Date - lastStoppedAt?: Date -} - -/** - * 获取所有容器 - */ -export const getAllContainers = async (): Promise => { - const containerList = await db.select().from(containers) - return containerList -} - -/** - * 获取所有自动启动的容器 - */ -export const getAutoStartContainers = async (): Promise => { - const containerList = await db - .select() - .from(containers) - .where(eq(containers.autoStart, true)) - return containerList -} - -/** - * 根据用户ID获取容器 - */ -export const getContainerByUserId = async (userId: string): Promise => { - const [container] = await db - .select() - .from(containers) - .where(eq(containers.userId, userId)) - - return container -} - -/** - * 根据容器名称获取容器 - */ -export const getContainerByName = async (containerName: string): Promise => { - const [container] = await db - .select() - .from(containers) - .where(eq(containers.containerName, containerName)) - - return container -} - -/** - * 根据容器ID获取容器 - */ -export const getContainerById = async (id: string): Promise => { - const [container] = await db - .select() - .from(containers) - .where(eq(containers.id, id)) - - return container -} - -/** - * 创建容器记录 - */ -export const createContainerRecord = async (data: CreateContainerInput): Promise => { - // 检查用户是否已有容器 - const existing = await getContainerByUserId(data.userId) - if (existing) { - throw new Error('User already has a container') - } - - const [newContainer] = await db - .insert(containers) - .values({ - userId: data.userId, - containerId: data.containerId, - containerName: data.containerName, - image: data.image, - namespace: data.namespace || 'default', - autoStart: data.autoStart ?? true, - hostPath: data.hostPath || null, - containerPath: data.containerPath || '/data', - status: 'created', - }) - .returning() - - return newContainer -} - -/** - * 更新容器状态 - */ -export const updateContainerStatus = async ( - containerId: string, - status: string -): Promise => { - const [updated] = await db - .update(containers) - .set({ - status, - updatedAt: new Date(), - ...(status === 'running' ? { lastStartedAt: new Date() } : {}), - ...(status === 'stopped' || status === 'paused' ? { lastStoppedAt: new Date() } : {}), - }) - .where(eq(containers.containerId, containerId)) - .returning() - - return updated || null -} - -/** - * 更新容器信息 - */ -export const updateContainer = async ( - id: string, - data: UpdateContainerInput -): Promise => { - const [updated] = await db - .update(containers) - .set({ - ...data, - updatedAt: new Date(), - }) - .where(eq(containers.id, id)) - .returning() - - return updated || null -} - -/** - * 删除容器记录 - */ -export const deleteContainerRecord = async (id: string): Promise => { - const [deleted] = await db - .delete(containers) - .where(eq(containers.id, id)) - .returning() - - return deleted || null -} - -/** - * 根据用户ID删除容器记录 - */ -export const deleteContainerByUserId = async (userId: string): Promise => { - const [deleted] = await db - .delete(containers) - .where(eq(containers.userId, userId)) - .returning() - - return deleted || null -} - -/** - * 检查用户是否有容器 - */ -export const userHasContainer = async (userId: string): Promise => { - const container = await getContainerByUserId(userId) - return !!container -} - diff --git a/packages/db/src/container.ts b/packages/db/src/container.ts deleted file mode 100644 index 2b19e463..00000000 --- a/packages/db/src/container.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { pgTable, text, timestamp, uuid, boolean } from 'drizzle-orm/pg-core' -import { users } from './users' - -/** - * 容器表 - 存储用户容器信息 - */ -export const containers = pgTable('containers', { - // 主键ID - id: uuid('id').primaryKey().defaultRandom(), - - // 关联用户ID - userId: uuid('user_id') - .notNull() - .references(() => users.id, { onDelete: 'cascade' }), - - // 容器ID(containerd 中的实际容器ID) - containerId: text('container_id').notNull().unique(), - - // 容器名称 - containerName: text('container_name').notNull().unique(), - - // 容器镜像 - image: text('image').notNull(), - - // 容器状态:created, running, paused, stopped, unknown - status: text('status').notNull().default('created'), - - // 容器命名空间 - namespace: text('namespace').notNull().default('default'), - - // 是否自动启动 - autoStart: boolean('auto_start').notNull().default(true), - - // 宿主机挂载目录 - hostPath: text('host_path'), - - // 容器内挂载目录 - containerPath: text('container_path').notNull().default('/data'), - - // 创建时间 - createdAt: timestamp('created_at').notNull().defaultNow(), - - // 更新时间 - updatedAt: timestamp('updated_at').notNull().defaultNow(), - - // 最后启动时间 - lastStartedAt: timestamp('last_started_at'), - - // 最后停止时间 - lastStoppedAt: timestamp('last_stopped_at'), -}) - diff --git a/packages/db/src/history.ts b/packages/db/src/history.ts deleted file mode 100644 index 03f899ce..00000000 --- a/packages/db/src/history.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { pgTable, timestamp, uuid, jsonb } from 'drizzle-orm/pg-core' -import { users } from './users' - -export const history = pgTable( - 'history', - { - id: uuid('id').primaryKey().defaultRandom(), - messages: jsonb('messages').notNull(), - timestamp: timestamp('timestamp').notNull(), - user: uuid('user').notNull().references(() => users.id), - } -) \ No newline at end of file diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts deleted file mode 100644 index 71559fea..00000000 --- a/packages/db/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { drizzle } from 'drizzle-orm/node-postgres' -import { config } from 'dotenv' - -config({ path: '../../' }) - -export const db = drizzle(process.env.DATABASE_URL!) - -// Export helpers -export * from './user-helpers' -export * from './container-helpers' diff --git a/packages/db/src/mcp-connection.ts b/packages/db/src/mcp-connection.ts deleted file mode 100644 index b864c720..00000000 --- a/packages/db/src/mcp-connection.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { boolean, jsonb, pgTable, text, uuid } from 'drizzle-orm/pg-core' -import { users } from './users' - -export const mcpConnection = pgTable('mcp_connection', { - id: uuid('id').primaryKey().defaultRandom(), - type: text('type').notNull(), - name: text('name').notNull(), - config: jsonb('config').notNull(), - active: boolean('active').notNull().default(true), - user: uuid('user').notNull().references(() => users.id), -}) \ No newline at end of file diff --git a/packages/db/src/model.ts b/packages/db/src/model.ts deleted file mode 100644 index 308d687d..00000000 --- a/packages/db/src/model.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Model } from '@memoh/shared' -import { jsonb, pgTable, uuid } from 'drizzle-orm/pg-core' - -export const model = pgTable('model', { - id: uuid('id').primaryKey().defaultRandom(), - model: jsonb('model').notNull().$type(), -}) \ No newline at end of file diff --git a/packages/db/src/platform.ts b/packages/db/src/platform.ts deleted file mode 100644 index d19bd652..00000000 --- a/packages/db/src/platform.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { boolean, jsonb, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' - -export const platform = pgTable('platform', { - id: uuid('id').primaryKey().defaultRandom(), - name: text('name').notNull(), - // endpoint: text('endpoint').notNull(), - config: jsonb('config').notNull(), - active: boolean('active').notNull().default(true), - createdAt: timestamp('created_at').notNull().defaultNow(), - updatedAt: timestamp('updated_at').notNull().defaultNow(), -}) \ No newline at end of file diff --git a/packages/db/src/schedule.ts b/packages/db/src/schedule.ts deleted file mode 100644 index 83776147..00000000 --- a/packages/db/src/schedule.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { boolean, integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' -import { users } from './users' - -export const schedule = pgTable('schedule', { - id: uuid('id').primaryKey().defaultRandom(), - name: text('name').notNull(), - description: text('description').notNull(), - command: text('command').notNull(), - pattern: text('pattern').notNull(), - maxCalls: integer('max_calls'), - user: uuid('user').notNull().references(() => users.id), - createdAt: timestamp('created_at').notNull().defaultNow(), - updatedAt: timestamp('updated_at').notNull().defaultNow(), - active: boolean('active').notNull().default(true), -}) \ No newline at end of file diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts deleted file mode 100644 index 87bd2e93..00000000 --- a/packages/db/src/schema.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './history' -export * from './model' -export * from './settings' -export * from './schedule' -export * from './users' -export * from './platform' -export * from './mcp-connection' -export * from './container' \ No newline at end of file diff --git a/packages/db/src/settings.ts b/packages/db/src/settings.ts deleted file mode 100644 index 421e5b4f..00000000 --- a/packages/db/src/settings.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { pgTable, text, uuid, integer } from 'drizzle-orm/pg-core' -import { model } from './model' -import { users } from './users' - -export const settings = pgTable('settings', { - userId: uuid('user_id').primaryKey().references(() => users.id), - defaultChatModel: uuid('default_chat_model').references(() => model.id), - defaultEmbeddingModel: uuid('default_embedding_model').references(() => model.id), - defaultSummaryModel: uuid('default_summary_model').references(() => model.id), - // Agent settings - maxContextLoadTime: integer('max_context_load_time').default(60), // minutes - language: text('language').default('Same as user input'), -}) \ No newline at end of file diff --git a/packages/db/src/user-helpers.ts b/packages/db/src/user-helpers.ts deleted file mode 100644 index b638f27a..00000000 --- a/packages/db/src/user-helpers.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { eq } from 'drizzle-orm' -import { db } from './index' -import { users } from './users' - -/** - * 用户操作辅助函数 - * 这些函数提供了常用的用户、用户组操作 - */ - -// ============= 用户操作 ============= - -/** - * 根据用户名查找用户 - */ -export async function findUserByUsername(username: string) { - const result = await db.select().from(users).where(eq(users.username, username)) - return result[0] || null -} - -/** - * 根据邮箱查找用户 - */ -export async function findUserByEmail(email: string) { - const result = await db.select().from(users).where(eq(users.email, email)) - return result[0] || null -} - -/** - * 根据ID查找用户 - */ -export async function findUserById(id: string) { - const result = await db.select().from(users).where(eq(users.id, id)) - return result[0] || null -} - -/** - * 创建新用户 - */ -export async function createUser(data: { - username: string - email?: string - passwordHash: string - role?: 'admin' | 'member' - displayName?: string - avatarUrl?: string -}) { - const result = await db.insert(users).values({ - username: data.username, - email: data.email, - passwordHash: data.passwordHash, - role: data.role || 'member', - displayName: data.displayName, - avatarUrl: data.avatarUrl, - }).returning() - - return result[0] -} - -/** - * 更新用户信息 - */ -export async function updateUser(id: string, data: { - displayName?: string - avatarUrl?: string - email?: string - role?: 'admin' | 'member' - isActive?: string -}) { - const result = await db - .update(users) - .set({ - ...data, - updatedAt: new Date(), - }) - .where(eq(users.id, id)) - .returning() - - return result[0] -} - -/** - * 更新用户密码 - */ -export async function updateUserPassword(id: string, passwordHash: string) { - await db - .update(users) - .set({ - passwordHash, - updatedAt: new Date(), - }) - .where(eq(users.id, id)) -} - -/** - * 更新最后登录时间 - */ -export async function updateLastLogin(id: string) { - await db - .update(users) - .set({ - lastLoginAt: new Date(), - }) - .where(eq(users.id, id)) -} - -/** - * 删除用户 - */ -export async function deleteUser(id: string) { - await db.delete(users).where(eq(users.id, id)) -} - diff --git a/packages/db/src/users.ts b/packages/db/src/users.ts deleted file mode 100644 index a117f177..00000000 --- a/packages/db/src/users.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { pgTable, pgEnum, text, timestamp, uuid, boolean } from 'drizzle-orm/pg-core' - -// 定义用户角色枚举 -export const userRoleEnum = pgEnum('user_role', ['admin', 'member']) - -// 用户表 -export const users = pgTable('users', { - // 主键ID - id: uuid('id').primaryKey().defaultRandom(), - - // 用户名(唯一) - username: text('username').notNull().unique(), - - // 邮箱(可选,唯一) - email: text('email').unique(), - - // 密码哈希值(使用 bcrypt 或其他加密方式) - passwordHash: text('password_hash').notNull(), - - // 用户角色 - role: userRoleEnum('role').notNull().default('member'), - - // 显示名称 - displayName: text('display_name'), - - // 头像 URL - avatarUrl: text('avatar_url'), - - // 账户状态(是否激活) - isActive: boolean('is_active').notNull().default(true), - - // 创建时间 - createdAt: timestamp('created_at').notNull().defaultNow(), - - // 更新时间 - updatedAt: timestamp('updated_at').notNull().defaultNow(), - - // 最后登录时间 - lastLoginAt: timestamp('last_login_at'), -}) diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json deleted file mode 100644 index 01bddbfc..00000000 --- a/packages/db/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../tsconfig.json", -} \ No newline at end of file diff --git a/packages/memory/README.md b/packages/memory/README.md deleted file mode 100644 index 6b0c38a1..00000000 --- a/packages/memory/README.md +++ /dev/null @@ -1 +0,0 @@ -# @memoh/memory diff --git a/packages/memory/package.json b/packages/memory/package.json deleted file mode 100644 index 36ee4293..00000000 --- a/packages/memory/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@memoh/memory", - "version": "1.0.0", - "description": "", - "exports": { - ".": "./src/index.ts" - }, - "packageManager": "pnpm@10.27.0", - "dependencies": { - "@ai-sdk/openai": "^3.0.7", - "@memoh/ai-gateway": "workspace:*", - "@memoh/db": "workspace:*", - "@memoh/shared": "workspace:*", - "ai": "^6.0.25", - "drizzle-orm": "^0.45.1", - "mem0ai": "^2.2.0", - "xsai": "^0.4.1" - } -} diff --git a/packages/memory/src/filter.ts b/packages/memory/src/filter.ts deleted file mode 100644 index cf47c936..00000000 --- a/packages/memory/src/filter.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { db } from '@memoh/db' -import { history } from '@memoh/db/schema' -import { and, gte, lte, asc, eq } from 'drizzle-orm' -import { MemoryUnit } from './memory-unit' - -export const filterByTimestamp = async ( - from: Date, - to: Date, - user: string, -) => { - const results = await db - .select() - .from(history) - .where(and( - gte(history.timestamp, from), - lte(history.timestamp, to), - eq(history.user, user), - )) - .orderBy(asc(history.timestamp)) - - return results.map((result) => ({ - messages: result.messages, - timestamp: new Date(result.timestamp), - user: result.user, - })) as MemoryUnit[] -} diff --git a/packages/memory/src/index.ts b/packages/memory/src/index.ts deleted file mode 100644 index 2c98f1f0..00000000 --- a/packages/memory/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './memory-unit' -export * from './filter' -export * from './memory' \ No newline at end of file diff --git a/packages/memory/src/memory-unit.ts b/packages/memory/src/memory-unit.ts deleted file mode 100644 index 228ca075..00000000 --- a/packages/memory/src/memory-unit.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ModelMessage } from 'ai' - -export interface MemoryUnit { - messages: ModelMessage[] - timestamp: Date - user: string -} \ No newline at end of file diff --git a/packages/memory/src/memory.ts b/packages/memory/src/memory.ts deleted file mode 100644 index 29ee4862..00000000 --- a/packages/memory/src/memory.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Memory, type Message } from 'mem0ai/oss' -import { ChatModel, EmbeddingModel } from '@memoh/shared' -import { MemoryUnit } from './memory-unit' -import { db } from '@memoh/db' -import { history } from '@memoh/db/schema' - -export interface CreateMemoryParams { - summaryModel: ChatModel - embeddingModel: EmbeddingModel -} - -export const createMemory = ({ summaryModel, embeddingModel }: CreateMemoryParams) => { - process.env.OPENAI_BASE_URL = embeddingModel.baseUrl - const memory = new Memory({ - version: 'v1.1', - embedder: { - provider: 'openai', - config: { - apiKey: embeddingModel.apiKey, - model: embeddingModel.modelId, - embeddingDims: embeddingModel.dimensions, - url: embeddingModel.baseUrl, - } - }, - llm: { - provider: summaryModel.clientType, - config: { - apiKey: summaryModel.apiKey, - model: summaryModel.modelId, - baseURL: summaryModel.baseUrl, - } - }, - vectorStore: { - provider: 'qdrant', - config: { - collectionName: 'memory', - embeddingModelDims: embeddingModel.dimensions, - url: process.env.QDRANT_URL!, - } - } - }) - - const addMemory = async (memoryUnit: MemoryUnit) => { - await memory.add(memoryUnit.messages as Message[], { - userId: memoryUnit.user, - }) - await db.insert(history) - .values({ - id: crypto.randomUUID(), - timestamp: memoryUnit.timestamp, - user: memoryUnit.user, - messages: memoryUnit.messages, - }) - .onConflictDoNothing() - } - - const searchMemory = async (query: string, userId: string) => { - console.log('Searching memory with query:', query, 'userId:', userId) - try { - const { results } = await memory.search(query, { - userId, - }) - return results.map((result) => ({ - content: result.memory, - metadata: result.metadata, - })) - } catch (error) { - console.error('Memory search error:', error) - // Log the full error details if available - if (error && typeof error === 'object') { - console.error('Error details:', JSON.stringify(error, null, 2)) - } - return [] - } - } - - return { - addMemory, - searchMemory, - } -} \ No newline at end of file diff --git a/packages/platform-telegram/.gitignore b/packages/platform-telegram/.gitignore deleted file mode 100644 index ca07d7a5..00000000 --- a/packages/platform-telegram/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -.env -node_modules/ -dist/ -*.log -.DS_Store - diff --git a/packages/platform-telegram/README.md b/packages/platform-telegram/README.md deleted file mode 100644 index 3e23e463..00000000 --- a/packages/platform-telegram/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @memoh/platform-telegram - -Telegram bot platform for Memoh. \ No newline at end of file diff --git a/packages/platform-telegram/package.json b/packages/platform-telegram/package.json deleted file mode 100644 index 34c84746..00000000 --- a/packages/platform-telegram/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@memoh/platform-telegram", - "version": "1.0.0", - "description": "Telegram platform for Memoh", - "exports": { - ".": "./src/index.ts" - }, - "main": "src/index.ts", - "bin": { - "memoh-tg-bot": "./src/bot.ts" - }, - "scripts": { - "dev": "bun run --watch src/bot.ts" - }, - "keywords": [], - "author": "", - "license": "ISC", - "packageManager": "pnpm@10.27.0", - "dependencies": { - "@memoh/client": "workspace:*", - "@memoh/platform": "workspace:*", - "@memoh/shared": "workspace:*", - "dotenv": "^16.4.7", - "ioredis": "^5.9.1", - "telegraf": "^4.16.3", - "zod": "^4.3.5" - }, - "devDependencies": { - "@types/node": "^22.10.5" - } -} diff --git a/packages/platform-telegram/src/auth.ts b/packages/platform-telegram/src/auth.ts deleted file mode 100644 index e61bc807..00000000 --- a/packages/platform-telegram/src/auth.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { Context } from 'telegraf' -import { login, logout, isLoggedIn, getCurrentUser } from '@memoh/client' -import { getTokenStorage } from './storage' - - -/** - * Login command handler for Telegram bot - * Usage: /login username password - */ -export async function handleLogin(ctx: Context) { - - // Parse command arguments - const args = ctx.message && 'text' in ctx.message - ? ctx.message.text.split(' ').slice(1) - : [] - - if (args.length !== 2) { - await ctx.reply( - '❌ Invalid format\n\n' + - 'Usage: /login \n' + - 'Example: /login admin password' - ) - return - } - - const [username, password] = args - - const storage = await getTokenStorage(ctx) - - // Attempt login - const result = await login({ username, password }, { storage }) - - if (result.success && result.user) { - storage.setUserId(result.user.id) - - await ctx.reply( - '✅ Login successful!\n\n' + - `👤 Username: ${result.user.username}\n` + - `🎭 Role: ${result.user.role}\n` + - `🔑 User ID: ${result.user.id}\n\n` + - 'You can now use the bot to interact with Memoh.' - ) - } else { - await ctx.reply('❌ Login failed: Invalid response from server') - } -} - -/** - * Logout command handler for Telegram bot - * Usage: /logout - */ -export async function handleLogout(ctx: Context) { - try { - const storage = await getTokenStorage(ctx) - - await logout({ storage }) - await ctx.reply('✅ Logged out successfully') - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - await ctx.reply(`❌ Logout failed: ${message}`) - } -} - -/** - * Whoami command handler - show current logged in user - * Usage: /whoami - */ -export async function handleWhoami(ctx: Context) { - try { - const storage = await getTokenStorage(ctx) - - const isLogged = await isLoggedIn({ storage }) - - if (!isLogged) { - await ctx.reply( - '❌ You are not logged in\n\n' + - 'Use /login to login' - ) - return - } - - const user = await getCurrentUser({ storage }) - - await ctx.reply( - '👤 Current User:\n\n' + - `Username: ${user.username}\n` + - `Role: ${user.role}\n` + - `User ID: ${user.id}\n` + - `Telegram ID: ${storage.getChatId()}` - ) - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - await ctx.reply(`❌ Error: ${message}`) - } -} - -/** - * Middleware to require authentication - * Add this middleware to commands that require login - */ -export function requireAuth() { - return async (ctx: Context, next: () => Promise) => { - const storage = await getTokenStorage(ctx) - - const isLogged = await isLoggedIn({ storage }) - - if (!isLogged) { - await ctx.reply( - '❌ You need to login first\n\n' + - 'Use /login to login' - ) - return - } - - // User is authenticated, continue to next handler - await next() - } -} - diff --git a/packages/platform-telegram/src/index.ts b/packages/platform-telegram/src/index.ts deleted file mode 100644 index c3962374..00000000 --- a/packages/platform-telegram/src/index.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { Telegraf, type Context } from 'telegraf' -import { BasePlatform, PlatformMessage } from '@memoh/platform' -import { handleLogin, handleLogout, handleWhoami, requireAuth } from './auth' -import { chatStreamAsync, type StreamEvent } from '@memoh/client' -import { getTokenStorage } from './storage' -import Redis from 'ioredis' -import { Platform } from '@memoh/shared' - -export interface TelegramPlatformConfig { - botToken: string -} - - -export class TelegramPlatform extends BasePlatform { - name = 'telegram' - description = 'Telegram Bot platform' - - private bot?: Telegraf - redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379') - - async start({ botToken }: TelegramPlatformConfig): Promise { - this.bot = new Telegraf(botToken) - - this.registerCommands() - - this.bot.launch() - } - - async stop(): Promise { - if (this.bot) { - this.bot.stop('SIGTERM') - } - } - - async send({ message, userId }: PlatformMessage): Promise { - const pattern = 'memoh:telegram:*:userId' - let cursor = '0' - let telegramUserId: string | null = null - - do { - const [nextCursor, keys] = await this.redis.scan( - cursor, - 'MATCH', - pattern, - 'COUNT', - 100 - ) - cursor = nextCursor - - for (const key of keys) { - const storedUserId = await this.redis.get(key) - if (storedUserId === userId) { - const match = key.match(/^memoh:telegram:(.+):userId$/) - if (match) { - telegramUserId = match[1] - break - } - } - } - } while (cursor !== '0') - if (telegramUserId) { - const chatId = await this.redis.get(`memoh:telegram:${telegramUserId}:chatId`) - if (chatId && this.bot) { - await this.bot.telegram.sendMessage(chatId, message) - } - } - } - - private registerCommands(): void { - if (!this.bot) { - throw new Error('Bot or storage not initialized') - } - - // Start command - this.bot.command('start', async (ctx) => { - await ctx.reply( - '👋 Welcome to Memoh Bot!\n\n' + - 'Available commands:\n' + - '/login - Login to your account\n' + - '/logout - Logout from your account\n' + - '/whoami - Show current user info\n' + - '/chat - Chat with AI agent\n' + - '/help - Show this help message' - ) - }) - - // Help command - this.bot.command('help', async (ctx) => { - await ctx.reply( - '📚 Memoh Bot Help\n\n' + - '🔐 Authentication:\n' + - '/login - Login\n' + - '/logout - Logout\n' + - '/whoami - Show current user\n\n' + - '💬 Chat:\n' + - '/chat - Talk to AI\n' + - 'Or just send a message directly\n\n' + - '❓ Help:\n' + - '/help - Show this message' - ) - }) - - // Auth commands - this.bot.command('login', (ctx) => handleLogin(ctx)) - this.bot.command('logout', (ctx) => handleLogout(ctx)) - this.bot.command('whoami', (ctx) => handleWhoami(ctx)) - - // Chat command (requires auth) - this.bot.command('chat', requireAuth(), async (ctx) => { - const args = ctx.message.text.split(' ').slice(1) - if (args.length === 0) { - await ctx.reply('❌ Please provide a message\n\nUsage: /chat ') - return - } - - const message = args.join(' ') - await this.handleChat(ctx, message) - }) - - // Handle direct messages (requires auth) - this.bot.on('text', requireAuth(), async (ctx) => { - // Skip if it's a command - if (ctx.message.text.startsWith('/')) { - return - } - - await this.handleChat(ctx, ctx.message.text) - }) - - // Error handling - this.bot.catch((err, ctx) => { - console.error('Bot error:', err) - ctx.reply('❌ An error occurred. Please try again.') - }) - } - - private async handleChat(ctx: Context, message: string): Promise { - try { - // Send typing indicator - await ctx.sendChatAction('typing') - const storage = await getTokenStorage(ctx) - - let responseText = '' - let lastUpdateTime = Date.now() - let messageId: number | undefined - - await chatStreamAsync( - { - message, - language: 'Chinese', - }, - async (event: StreamEvent) => { - if (event.type === 'text-delta' && event.text) { - responseText += event.text - - // Update message every 1 second or when response is complete - const now = Date.now() - if (now - lastUpdateTime > 1000) { - lastUpdateTime = now - - if (messageId && ctx.chat) { - // Edit existing message - try { - await ctx.telegram.editMessageText( - ctx.chat.id, - messageId, - undefined, - `🤖 ${responseText}` - ) - } catch { - // Ignore if message is not modified - } - } else { - // Send first message - const sent = await ctx.reply(`🤖 ${responseText}`) - messageId = sent.message_id - } - } - } else if (event.type === 'tool-call') { - // Show tool usage - if (messageId && ctx.chat) { - try { - await ctx.telegram.editMessageText( - ctx.chat.id, - messageId, - undefined, - `🤖 ${responseText}\n\n🔧 Using tool: ${event.toolName}...` - ) - } catch { - // Ignore - } - } - } else if (event.type === 'error') { - await ctx.reply(`❌ Error: ${event.error}`) - } else if (event.type === 'done') { - // Final update - if (messageId && responseText && ctx.chat) { - try { - await ctx.telegram.editMessageText( - ctx.chat.id, - messageId, - undefined, - `🤖 ${responseText}` - ) - } catch { - // Ignore - } - } else if (!messageId && responseText) { - await ctx.reply(`🤖 ${responseText}`) - } - } - }, - { storage } - ) - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error' - await ctx.reply(`❌ Error: ${errorMessage}`) - } - } -} - -// Export for easy use -export { handleLogin, handleLogout, handleWhoami, requireAuth } from './auth' diff --git a/packages/platform-telegram/src/storage.ts b/packages/platform-telegram/src/storage.ts deleted file mode 100644 index d527418d..00000000 --- a/packages/platform-telegram/src/storage.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { TokenStorage } from '@memoh/client' -import Redis from 'ioredis' -import { Context } from 'telegraf' - -export interface TelegramTokenStorage extends TokenStorage { - getUserId: () => string | null - setUserId: (userId: string) => void - getChatId: () => string | null - setChatId: (chatId: string) => void - getTelegramIdByUserId: (userId: string) => Promise -} - -export const getTokenStorage = async (ctx: Context): Promise => { - const telegramUserId = ctx.from?.id.toString() - if (!telegramUserId) { - throw new Error('Unable to identify user') - } - const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379') - const isTokenExists = await redis.exists(`memoh:telegram:${telegramUserId}:token`) - const token = isTokenExists ? await redis.get(`memoh:telegram:${telegramUserId}:token`) : null - const isUserIdExists = await redis.exists(`memoh:telegram:${telegramUserId}:userId`) - const userId = isUserIdExists ? await redis.get(`memoh:telegram:${telegramUserId}:userId`) : null - const chatId = ctx.chat?.id.toString() ?? null - if (chatId) await redis.set(`memoh:telegram:${telegramUserId}:chatId`, chatId) - return { - getChatId: () => chatId, - setChatId: (chatId: string) => { - redis.set(`memoh:telegram:${telegramUserId}:chatId`, chatId) - .then(() => { - redis.save() - }) - }, - getApiUrl: () => process.env.API_URL || 'http://localhost:7002', - setApiUrl: () => {}, - getToken: () => token, - setToken: (token: string) => { - redis.set(`memoh:telegram:${telegramUserId}:token`, token) - .then(() => { - redis.save() - }) - }, - clearToken: () => { - redis.del(`memoh:telegram:${telegramUserId}:token`) - .then(() => { - redis.save() - }) - }, - getUserId: () => userId, - setUserId: (userId: string) => { - redis.set(`memoh:telegram:${telegramUserId}:userId`, userId) - .then(() => { - redis.save() - }) - }, - getTelegramIdByUserId: async (userId: string) => { - // 扫描所有 memoh:telegram:*:userId 的 key - const pattern = 'memoh:telegram:*:userId' - let cursor = '0' - - do { - const [nextCursor, keys] = await redis.scan( - cursor, - 'MATCH', - pattern, - 'COUNT', - 100 - ) - cursor = nextCursor - - // 检查每个 key 的值是否匹配目标 userId - for (const key of keys) { - const storedUserId = await redis.get(key) - if (storedUserId === userId) { - // 从 key 中提取 telegramUserId: memoh:telegram:{telegramUserId}:userId - const match = key.match(/^memoh:telegram:(.+):userId$/) - if (match) { - return match[1] - } - } - } - } while (cursor !== '0') - - return null - }, - } -} - diff --git a/packages/platform-telegram/test/send.test.ts b/packages/platform-telegram/test/send.test.ts deleted file mode 100644 index cf8490f7..00000000 --- a/packages/platform-telegram/test/send.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { describe, expect, it } from "vitest" - -describe('Telegram Platform', () => { - it('should send a message to a user', async () => { - const response = await fetch('http://localhost:7003/send', { - method: 'POST', - body: JSON.stringify({ - userId: '66392e42-333a-4ee9-9276-1b216d3400b1', - message: 'Hello, world!', - }), - headers: { - 'Content-Type': 'application/json', - }, - }) - console.log(await response.json()) - expect(response.status).toBe(200) - }) -}) \ No newline at end of file diff --git a/packages/platform/README.md b/packages/platform/README.md deleted file mode 100644 index 66cfd5e2..00000000 --- a/packages/platform/README.md +++ /dev/null @@ -1 +0,0 @@ -# @memoh/platform diff --git a/packages/platform/package.json b/packages/platform/package.json deleted file mode 100644 index 8d0a4ba5..00000000 --- a/packages/platform/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "@memoh/platform", - "version": "1.0.0", - "description": "", - "exports": { - ".": "./src/index.ts" - }, - "packageManager": "pnpm@10.27.0", - "dependencies": { - "@elysiajs/cors": "^1.4.1", - "elysia": "^1.4.21", - "zod": "^4.3.5" - } -} diff --git a/packages/platform/src/index.ts b/packages/platform/src/index.ts deleted file mode 100644 index a29ed966..00000000 --- a/packages/platform/src/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface PlatformMessage { - message: string - userId: string -} - -export class BasePlatform { - name: string = 'base' - description: string = 'Base platform' - started: boolean = false - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async start(config: unknown): Promise {} - - async stop(): Promise {} - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async send(data: PlatformMessage): Promise {} -} \ No newline at end of file