From 1b7d55848dcd1414c121c6f427ae10b68f27c354 Mon Sep 17 00:00:00 2001 From: kurihada Date: Tue, 16 Dec 2025 20:19:24 +0800 Subject: [PATCH] =?UTF-8?q?refactor(server):=20=E6=B6=88=E9=99=A4=E4=B8=8E?= =?UTF-8?q?=20Core=20=E7=9A=84=E9=87=8D=E5=A4=8D=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 Server 中 60+ 个与 Core 重复的类型定义 - 将动态导入 (await import) 改为静态类型导入 (import type) - 保留必要的运行时静态导入 - 修复测试文件中的 mock 初始化问题 - 净删除约 960 行重复代码 重构文件: - routes/checkpoints.ts: 删除 155 行重复类型 - routes/agents.ts: 删除 93 行重复类型 - routes/commands.ts: 删除 83 行重复类型 - routes/mcp.ts: 修复类型窄化 - routes/hooks.ts: 已使用静态导入 - routes/providers.ts: 删除 63 行重复类型 - session/manager.ts: 删除 41 行重复类型 - routes/sessions.ts: 添加类型导入 - permission/handler.ts: 添加类型导入 --- packages/core/src/index.ts | 16 + packages/server/src/permission/handler.ts | 19 +- packages/server/src/routes/agents.ts | 287 +++-------------- packages/server/src/routes/checkpoints.ts | 302 ++++-------------- packages/server/src/routes/commands.ts | 185 +++-------- packages/server/src/routes/hooks.ts | 219 ++----------- packages/server/src/routes/mcp.ts | 166 +++------- packages/server/src/routes/providers.ts | 155 ++------- packages/server/src/routes/sessions.ts | 62 +--- packages/server/src/session/manager.ts | 68 +--- .../server/tests/unit/routes/agents.test.ts | 11 +- .../server/tests/unit/routes/commands.test.ts | 11 +- .../server/tests/unit/routes/hooks.test.ts | 11 +- .../tests/unit/routes/providers.test.ts | 11 +- 14 files changed, 283 insertions(+), 1240 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b360d70..8bf86e2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -44,7 +44,9 @@ export type { MessageInfo, Part, PartType, + ToolPart, ToolStatus, + ToolState, TodoItem, TodoList, } from './session/index.js'; @@ -193,6 +195,11 @@ export type { AgentModelConfig, AgentToolConfig, AgentPermission, + AgentBashPermission, + AgentFilePermission, + AgentGitPermission, + PermissionAction, + PermissionRule, } from './agent/index.js'; // Agent Events (for subagent progress tracking) @@ -213,6 +220,15 @@ export { getMCPManager, loadMCPConfig, createMCPToolAdapter, + MCPManager, +} from './mcp/index.js'; + +export type { + MCPConfig, + MCPServerConfig, + MCPServerStatus, + MCPServerStatusType, + MCPTool, } from './mcp/index.js'; // Provider diff --git a/packages/server/src/permission/handler.ts b/packages/server/src/permission/handler.ts index f308ad3..256cd90 100644 --- a/packages/server/src/permission/handler.ts +++ b/packages/server/src/permission/handler.ts @@ -11,24 +11,7 @@ import type { PermissionRequestContext, ServerMessage, } from '../types.js'; - -/** - * 权限决策结果 - */ -export interface PermissionDecision { - allow: boolean; - remember?: boolean; -} - -/** - * 权限上下文(来自 core 模块) - */ -export interface PermissionContext { - command: string; - workdir: string; - patterns?: string[]; - externalPaths?: string[]; -} +import type { PermissionDecision, PermissionContext } from '@ai-assistant/core'; // 等待中的权限请求 interface PendingRequest { diff --git a/packages/server/src/routes/agents.ts b/packages/server/src/routes/agents.ts index b123355..9984294 100644 --- a/packages/server/src/routes/agents.ts +++ b/packages/server/src/routes/agents.ts @@ -6,83 +6,20 @@ import { Hono } from 'hono'; import { getConfig } from './config.js'; - -// Agent 类型定义(与 Core 对应) -type AgentMode = 'primary' | 'subagent' | 'all'; - -interface AgentModelConfig { - provider?: 'anthropic' | 'deepseek' | 'openai'; - model?: string; - temperature?: number; - topP?: number; - maxTokens?: number; -} - -interface AgentToolConfig { - disabled?: string[]; - enabled?: string[]; - noTask?: boolean; -} - -interface PermissionRule { - pattern: string; - action: 'allow' | 'deny'; -} - -interface AgentBashPermission { - rules?: PermissionRule[]; - defaultAction?: 'allow' | 'deny'; -} - -interface AgentFilePermission { - read?: { rules?: PermissionRule[]; defaultAction?: 'allow' | 'deny' }; - write?: { rules?: PermissionRule[]; defaultAction?: 'allow' | 'deny' }; -} - -interface AgentGitPermission { - commands?: string[]; - allowPush?: boolean; - allowForce?: boolean; -} - -interface AgentPermission { - bash?: AgentBashPermission; - file?: AgentFilePermission; - git?: AgentGitPermission; -} - -interface AgentInfo { - name: string; - description: string; - mode: AgentMode; - prompt?: string; - model?: AgentModelConfig; - tools?: AgentToolConfig; - permission?: AgentPermission; - maxSteps?: number; -} - -interface AgentConfigFile { - defaults?: { - maxSteps?: number; - model?: AgentModelConfig; - permission?: AgentPermission; - }; - agents?: Record>; -} - -// Core Agent 模块类型 -interface AgentModule { - agentRegistry: { - init: () => Promise; - get: (name: string) => AgentInfo | undefined; - list: () => AgentInfo[]; - }; - loadAgentConfig: (workdir: string) => Promise; - saveAgentConfig: (workdir: string, config: AgentConfigFile, format?: 'json' | 'yaml') => Promise; - presetAgents: Record>; - isPresetAgent: (name: string) => boolean; -} +import type { + AgentMode, + AgentInfo, + AgentConfigFile, + AgentModelConfig, + AgentPermission, +} from '@ai-assistant/core'; +import { + agentRegistry, + loadAgentConfig, + saveAgentConfig, + presetAgents, + isPresetAgent, +} from '@ai-assistant/core'; // API 响应类型 interface AgentListItem { @@ -103,72 +40,30 @@ interface AgentDefaults { export const agentsRouter = new Hono(); -// Core 模块缓存 -let agentModule: AgentModule | null = null; +// 初始化状态 let initialized = false; -/** - * 初始化 Agent 模块 - */ -async function initAgentModule(): Promise { - if (agentModule) return agentModule; - - try { - const corePath = '@ai-assistant/core'; - const core = (await import(corePath)) as Record; - - if ( - !core.agentRegistry || - typeof core.loadAgentConfig !== 'function' || - typeof core.saveAgentConfig !== 'function' || - !core.presetAgents || - typeof core.isPresetAgent !== 'function' - ) { - console.warn('[Agents] Core module missing Agent exports'); - return null; - } - - agentModule = { - agentRegistry: core.agentRegistry as AgentModule['agentRegistry'], - loadAgentConfig: core.loadAgentConfig as AgentModule['loadAgentConfig'], - saveAgentConfig: core.saveAgentConfig as AgentModule['saveAgentConfig'], - presetAgents: core.presetAgents as AgentModule['presetAgents'], - isPresetAgent: core.isPresetAgent as AgentModule['isPresetAgent'], - }; - - console.log('[Agents] Agent module initialized'); - return agentModule; - } catch (error) { - console.warn('[Agents] Failed to load Agent module:', error); - return null; - } -} - /** * 确保 AgentRegistry 已初始化 */ async function ensureRegistryInitialized(): Promise { - const module = await initAgentModule(); - if (!module) return false; - if (!initialized) { - await module.agentRegistry.init(); + await agentRegistry.init(); initialized = true; } - return true; } /** * 将 AgentInfo 转换为 AgentListItem */ -function toListItem(agent: AgentInfo, module: AgentModule, customAgentNames: Set): AgentListItem { - const isPreset = module.isPresetAgent(agent.name); +function toListItem(agent: AgentInfo, customAgentNames: Set): AgentListItem { + const isPresetAgent_ = isPresetAgent(agent.name); return { name: agent.name, description: agent.description, mode: agent.mode, - isPreset, + isPreset: isPresetAgent_, isCustomized: customAgentNames.has(agent.name), model: agent.model?.model, maxSteps: agent.maxSteps, @@ -179,18 +74,6 @@ function toListItem(agent: AgentInfo, module: AgentModule, customAgentNames: Set * GET /agents - 获取所有 Agent 列表 */ agentsRouter.get('/', async (c) => { - const module = await initAgentModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Agent module not available', - }, - 503 - ); - } - if (!(await ensureRegistryInitialized())) { return c.json( { @@ -202,11 +85,11 @@ agentsRouter.get('/', async (c) => { } const config = getConfig(); - const userConfig = await module.loadAgentConfig(config.workdir); + const userConfig = await loadAgentConfig(config.workdir); const customAgentNames = new Set(Object.keys(userConfig?.agents || {})); - const agents = module.agentRegistry.list(); - const items = agents.map((agent) => toListItem(agent, module, customAgentNames)); + const agents = agentRegistry.list(); + const items = agents.map((agent) => toListItem(agent, customAgentNames)); return c.json({ success: true, @@ -218,19 +101,7 @@ agentsRouter.get('/', async (c) => { * GET /agents/presets - 获取预设 Agent 列表 */ agentsRouter.get('/presets', async (c) => { - const module = await initAgentModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Agent module not available', - }, - 503 - ); - } - - const presets = Object.entries(module.presetAgents).map(([name, agent]) => ({ + const presets = Object.entries(presetAgents).map(([name, agent]) => ({ name, description: agent.description, mode: agent.mode, @@ -250,20 +121,8 @@ agentsRouter.get('/presets', async (c) => { * GET /agents/defaults - 获取全局默认配置 */ agentsRouter.get('/defaults', async (c) => { - const module = await initAgentModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Agent module not available', - }, - 503 - ); - } - const config = getConfig(); - const userConfig = await module.loadAgentConfig(config.workdir); + const userConfig = await loadAgentConfig(config.workdir); return c.json({ success: true, @@ -275,24 +134,12 @@ agentsRouter.get('/defaults', async (c) => { * PUT /agents/defaults - 更新全局默认配置 */ agentsRouter.put('/defaults', async (c) => { - const module = await initAgentModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Agent module not available', - }, - 503 - ); - } - try { const newDefaults = await c.req.json(); const config = getConfig(); // 加载现有配置 - let userConfig = await module.loadAgentConfig(config.workdir); + let userConfig = await loadAgentConfig(config.workdir); if (!userConfig) { userConfig = {}; } @@ -301,7 +148,7 @@ agentsRouter.put('/defaults', async (c) => { userConfig.defaults = newDefaults; // 保存配置 - await module.saveAgentConfig(config.workdir, userConfig); + await saveAgentConfig(config.workdir, userConfig); // 重新初始化 registry 以应用新配置 initialized = false; @@ -326,18 +173,6 @@ agentsRouter.put('/defaults', async (c) => { * GET /agents/:name - 获取单个 Agent 详情 */ agentsRouter.get('/:name', async (c) => { - const module = await initAgentModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Agent module not available', - }, - 503 - ); - } - if (!(await ensureRegistryInitialized())) { return c.json( { @@ -349,7 +184,7 @@ agentsRouter.get('/:name', async (c) => { } const name = c.req.param('name'); - const agent = module.agentRegistry.get(name); + const agent = agentRegistry.get(name); if (!agent) { return c.json( @@ -362,15 +197,15 @@ agentsRouter.get('/:name', async (c) => { } const config = getConfig(); - const userConfig = await module.loadAgentConfig(config.workdir); - const isPreset = module.isPresetAgent(name); + const userConfig = await loadAgentConfig(config.workdir); + const isPresetAgent_ = isPresetAgent(name); const isCustomized = !!(userConfig?.agents && name in userConfig.agents); return c.json({ success: true, data: { ...agent, - isPreset, + isPreset: isPresetAgent_, isCustomized, }, }); @@ -380,18 +215,6 @@ agentsRouter.get('/:name', async (c) => { * POST /agents - 创建新 Agent */ agentsRouter.post('/', async (c) => { - const module = await initAgentModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Agent module not available', - }, - 503 - ); - } - try { const body = await c.req.json<{ name: string } & Omit>(); const { name, ...agentConfig } = body; @@ -407,7 +230,7 @@ agentsRouter.post('/', async (c) => { } // 检查名称是否与预设冲突 - if (module.isPresetAgent(name)) { + if (isPresetAgent(name)) { return c.json( { success: false, @@ -418,7 +241,7 @@ agentsRouter.post('/', async (c) => { } const config = getConfig(); - let userConfig = await module.loadAgentConfig(config.workdir); + let userConfig = await loadAgentConfig(config.workdir); if (!userConfig) { userConfig = {}; } @@ -441,13 +264,13 @@ agentsRouter.post('/', async (c) => { userConfig.agents[name] = agentConfig; // 保存配置 - await module.saveAgentConfig(config.workdir, userConfig); + await saveAgentConfig(config.workdir, userConfig); // 重新初始化 registry initialized = false; await ensureRegistryInitialized(); - const createdAgent = module.agentRegistry.get(name); + const createdAgent = agentRegistry.get(name); return c.json({ success: true, @@ -474,24 +297,12 @@ agentsRouter.post('/', async (c) => { * PUT /agents/:name - 更新 Agent */ agentsRouter.put('/:name', async (c) => { - const module = await initAgentModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Agent module not available', - }, - 503 - ); - } - try { const name = c.req.param('name'); const agentConfig = await c.req.json>(); const config = getConfig(); - let userConfig = await module.loadAgentConfig(config.workdir); + let userConfig = await loadAgentConfig(config.workdir); if (!userConfig) { userConfig = {}; } @@ -503,24 +314,24 @@ agentsRouter.put('/:name', async (c) => { userConfig.agents[name] = agentConfig; // 保存配置 - await module.saveAgentConfig(config.workdir, userConfig); + await saveAgentConfig(config.workdir, userConfig); // 重新初始化 registry initialized = false; await ensureRegistryInitialized(); - const updatedAgent = module.agentRegistry.get(name); - const isPreset = module.isPresetAgent(name); + const updatedAgent = agentRegistry.get(name); + const isPresetAgent_ = isPresetAgent(name); return c.json({ success: true, data: updatedAgent ? { ...updatedAgent, - isPreset, + isPreset: isPresetAgent_, isCustomized: true, } - : { name, ...agentConfig, isPreset, isCustomized: true }, + : { name, ...agentConfig, isPreset: isPresetAgent_, isCustomized: true }, }); } catch (error) { return c.json( @@ -537,26 +348,14 @@ agentsRouter.put('/:name', async (c) => { * DELETE /agents/:name - 删除 Agent */ agentsRouter.delete('/:name', async (c) => { - const module = await initAgentModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Agent module not available', - }, - 503 - ); - } - try { const name = c.req.param('name'); const config = getConfig(); - const userConfig = await module.loadAgentConfig(config.workdir); + const userConfig = await loadAgentConfig(config.workdir); if (!userConfig?.agents || !(name in userConfig.agents)) { // 如果是预设 Agent,返回特定错误 - if (module.isPresetAgent(name)) { + if (isPresetAgent(name)) { return c.json( { success: false, @@ -579,7 +378,7 @@ agentsRouter.delete('/:name', async (c) => { delete userConfig.agents[name]; // 保存配置 - await module.saveAgentConfig(config.workdir, userConfig); + await saveAgentConfig(config.workdir, userConfig); // 重新初始化 registry initialized = false; diff --git a/packages/server/src/routes/checkpoints.ts b/packages/server/src/routes/checkpoints.ts index 9b84e3f..a4bc752 100644 --- a/packages/server/src/routes/checkpoints.ts +++ b/packages/server/src/routes/checkpoints.ts @@ -6,157 +6,26 @@ import { Hono } from 'hono'; import { getConfig } from './config.js'; - -// Core Checkpoint 模块类型 -interface CheckpointModule { - getCheckpointManager: () => CheckpointManager; - initCheckpointManager: ( - workDir: string, - config?: Partial - ) => Promise; - RestoreMode: typeof RestoreMode; -} - -interface CheckpointManager { - initialize(): Promise; - isEnabled(): boolean; - getConfig(): CheckpointConfig; - listCheckpoints(): Promise; - getCheckpoint(idOrHash: string): Promise; - getLatestCheckpoint(): Promise; - createCheckpoint(options: { - name?: string; - description?: string; - trigger?: CheckpointTrigger; - }): Promise; - deleteCheckpoint(id: string): Promise; - getDiff(checkpointId: string): Promise; - getFileDiff(checkpointId: string, filePath: string): Promise; - rollback(options: RollbackOptions): Promise; - checkSafety(checkpointId: string): Promise; - unrevert(): Promise; - canUnrevert(): boolean; - getLastRollback(): RollbackRecord | null; - cleanup(): Promise; - getStats(): Promise; - getSessionCheckpoints(sessionId: string): Promise; - getMessageCheckpoints(messageId: string): Promise; -} - -interface CheckpointConfig { - enabled: boolean; - autoCheckpoint: { - beforeWrite: boolean; - beforeEdit: boolean; - beforeDelete: boolean; - beforeMove: boolean; - beforeBash: boolean; - }; - maxCheckpoints: number; - maxAge: number; - storageDir: string; -} - -type CheckpointTrigger = - | 'auto' - | 'manual' - | 'tool:write_file' - | 'tool:edit_file' - | 'tool:delete_file' - | 'tool:move_file' - | 'tool:copy_file' - | 'tool:bash' - | 'task_start' - | 'task_complete' - | 'pre_rollback' - | 'session_start' - | 'session_end'; - -interface CheckpointMetadata { - id: string; - name?: string; - description?: string; - timestamp: number; - trigger: CheckpointTrigger; - toolCall?: { - tool: string; - params: Record; - }; - commitHash: string; - filesChanged: number; - messageId?: string; - sessionId?: string; - turnIndex?: number; -} - -type FileChangeType = 'added' | 'modified' | 'deleted' | 'renamed'; - -interface FileChange { - path: string; - type: FileChangeType; - oldPath?: string; - insertions?: number; - deletions?: number; -} - -interface DiffInfo { - from: string; - to: string; - files: FileChange[]; - totalInsertions: number; - totalDeletions: number; -} - -interface FileDiff { - path: string; - type: FileChangeType; - oldContent?: string; - newContent?: string; - patch?: string; -} - -enum RestoreMode { - AI_CHANGES_ONLY = 'ai_changes_only', - WORKSPACE_ONLY = 'workspace_only', - FULL = 'full', -} - -interface RollbackOptions { - target: string; - files?: string[]; - dryRun?: boolean; - mode?: RestoreMode; - skipSafetyCheck?: boolean; -} - -interface RollbackResult { - success: boolean; - restoredFiles: string[]; - errors: Array<{ file: string; error: string }>; - previousCommit?: string; -} - -interface SafetyCheckResult { - safe: boolean; - warnings: string[]; - errors: string[]; -} - -interface RollbackRecord { - id: string; - timestamp: number; - targetCheckpoint: string; - previousCommit: string; - restoredFiles: string[]; - canUnrevert: boolean; -} - -interface UnrevertResult { - success: boolean; - restoredCommit: string; - filesRestored: number; - error?: string; -} +import type { + CheckpointMetadata, + CheckpointConfig, + CheckpointTrigger, + FileChange, + FileChangeType, + DiffInfo, + FileDiff, + RollbackOptions, + RollbackResult, + RollbackRecord, + SafetyCheckResult, + UnrevertResult, +} from '@ai-assistant/core'; +import { + CheckpointManager, + getCheckpointManager, + initCheckpointManager, + RestoreMode, +} from '@ai-assistant/core'; interface CheckpointStats { count: number; @@ -166,46 +35,25 @@ interface CheckpointStats { export const checkpointsRouter = new Hono(); -// Core 模块缓存 -let checkpointModule: CheckpointModule | null = null; +// Manager 初始化状态 let managerInitialized = false; /** * 初始化 Checkpoint 模块 */ -async function initCheckpointModule(): Promise { - if (checkpointModule && managerInitialized) return checkpointModule; +async function ensureCheckpointManager(): Promise { + if (managerInitialized) { + return getCheckpointManager(); + } try { - const corePath = '@ai-assistant/core'; - const core = (await import(corePath)) as Record; - - if ( - typeof core.getCheckpointManager !== 'function' || - typeof core.initCheckpointManager !== 'function' - ) { - console.warn('[Checkpoints] Core module missing Checkpoint exports'); - return null; - } - - checkpointModule = { - getCheckpointManager: core.getCheckpointManager as () => CheckpointManager, - initCheckpointManager: core.initCheckpointManager as ( - workDir: string, - config?: Partial - ) => Promise, - RestoreMode: core.RestoreMode as typeof RestoreMode, - }; - - // 初始化 Checkpoint Manager const config = getConfig(); - await checkpointModule.initCheckpointManager(config.workdir); + await initCheckpointManager(config.workdir); managerInitialized = true; - console.log('[Checkpoints] Checkpoint module initialized'); - return checkpointModule; + return getCheckpointManager(); } catch (error) { - console.warn('[Checkpoints] Failed to load Checkpoint module:', error); + console.warn('[Checkpoints] Failed to initialize Checkpoint module:', error); return null; } } @@ -214,9 +62,9 @@ async function initCheckpointModule(): Promise { * GET /checkpoints - 获取所有检查点列表 */ checkpointsRouter.get('/', async (c) => { - const module = await initCheckpointModule(); + const manager = await ensureCheckpointManager(); - if (!module) { + if (!manager) { return c.json( { success: false, @@ -227,7 +75,6 @@ checkpointsRouter.get('/', async (c) => { } try { - const manager = module.getCheckpointManager(); const checkpoints = await manager.listCheckpoints(); return c.json({ @@ -259,9 +106,9 @@ checkpointsRouter.get('/', async (c) => { * GET /checkpoints/stats - 获取检查点统计信息 */ checkpointsRouter.get('/stats', async (c) => { - const module = await initCheckpointModule(); + const manager = await ensureCheckpointManager(); - if (!module) { + if (!manager) { return c.json( { success: false, @@ -272,7 +119,6 @@ checkpointsRouter.get('/stats', async (c) => { } try { - const manager = module.getCheckpointManager(); const stats = await manager.getStats(); return c.json({ @@ -294,9 +140,9 @@ checkpointsRouter.get('/stats', async (c) => { * GET /checkpoints/latest - 获取最新检查点 */ checkpointsRouter.get('/latest', async (c) => { - const module = await initCheckpointModule(); + const manager = await ensureCheckpointManager(); - if (!module) { + if (!manager) { return c.json( { success: false, @@ -307,7 +153,6 @@ checkpointsRouter.get('/latest', async (c) => { } try { - const manager = module.getCheckpointManager(); const checkpoint = await manager.getLatestCheckpoint(); return c.json({ @@ -329,9 +174,9 @@ checkpointsRouter.get('/latest', async (c) => { * GET /checkpoints/unrevert/status - 检查是否可撤销回滚 */ checkpointsRouter.get('/unrevert/status', async (c) => { - const module = await initCheckpointModule(); + const manager = await ensureCheckpointManager(); - if (!module) { + if (!manager) { return c.json( { success: false, @@ -342,7 +187,6 @@ checkpointsRouter.get('/unrevert/status', async (c) => { } try { - const manager = module.getCheckpointManager(); const canUnrevert = manager.canUnrevert(); const lastRollback = manager.getLastRollback(); @@ -376,9 +220,9 @@ checkpointsRouter.get('/unrevert/status', async (c) => { */ checkpointsRouter.get('/:id', async (c) => { const id = c.req.param('id'); - const module = await initCheckpointModule(); + const manager = await ensureCheckpointManager(); - if (!module) { + if (!manager) { return c.json( { success: false, @@ -389,7 +233,6 @@ checkpointsRouter.get('/:id', async (c) => { } try { - const manager = module.getCheckpointManager(); const checkpoint = await manager.getCheckpoint(id); if (!checkpoint) { @@ -421,9 +264,9 @@ checkpointsRouter.get('/:id', async (c) => { * POST /checkpoints - 创建手动检查点 */ checkpointsRouter.post('/', async (c) => { - const module = await initCheckpointModule(); + const manager = await ensureCheckpointManager(); - if (!module) { + if (!manager) { return c.json( { success: false, @@ -435,7 +278,6 @@ checkpointsRouter.post('/', async (c) => { try { const body = await c.req.json<{ name?: string; description?: string }>(); - const manager = module.getCheckpointManager(); const checkpoint = await manager.createCheckpoint({ name: body.name, @@ -463,9 +305,9 @@ checkpointsRouter.post('/', async (c) => { */ checkpointsRouter.delete('/:id', async (c) => { const id = c.req.param('id'); - const module = await initCheckpointModule(); + const manager = await ensureCheckpointManager(); - if (!module) { + if (!manager) { return c.json( { success: false, @@ -476,7 +318,6 @@ checkpointsRouter.delete('/:id', async (c) => { } try { - const manager = module.getCheckpointManager(); const deleted = await manager.deleteCheckpoint(id); if (!deleted) { @@ -509,9 +350,9 @@ checkpointsRouter.delete('/:id', async (c) => { */ checkpointsRouter.get('/:id/diff', async (c) => { const id = c.req.param('id'); - const module = await initCheckpointModule(); + const manager = await ensureCheckpointManager(); - if (!module) { + if (!manager) { return c.json( { success: false, @@ -522,7 +363,6 @@ checkpointsRouter.get('/:id/diff', async (c) => { } try { - const manager = module.getCheckpointManager(); const diff = await manager.getDiff(id); return c.json({ @@ -546,9 +386,9 @@ checkpointsRouter.get('/:id/diff', async (c) => { checkpointsRouter.get('/:id/file-diff', async (c) => { const id = c.req.param('id'); const filePath = c.req.query('path'); - const module = await initCheckpointModule(); + const manager = await ensureCheckpointManager(); - if (!module) { + if (!manager) { return c.json( { success: false, @@ -569,7 +409,6 @@ checkpointsRouter.get('/:id/file-diff', async (c) => { } try { - const manager = module.getCheckpointManager(); const fileDiff = await manager.getFileDiff(id, filePath); return c.json({ @@ -592,9 +431,9 @@ checkpointsRouter.get('/:id/file-diff', async (c) => { */ checkpointsRouter.post('/:id/restore', async (c) => { const id = c.req.param('id'); - const module = await initCheckpointModule(); + const manager = await ensureCheckpointManager(); - if (!module) { + if (!manager) { return c.json( { success: false, @@ -611,20 +450,18 @@ checkpointsRouter.post('/:id/restore', async (c) => { skipSafetyCheck?: boolean; }>(); - const manager = module.getCheckpointManager(); - // 转换 mode 字符串为枚举值 let mode: RestoreMode | undefined; if (body.mode) { switch (body.mode) { case 'ai_changes_only': - mode = module.RestoreMode.AI_CHANGES_ONLY; + mode = RestoreMode.AI_CHANGES_ONLY; break; case 'workspace_only': - mode = module.RestoreMode.WORKSPACE_ONLY; + mode = RestoreMode.WORKSPACE_ONLY; break; case 'full': - mode = module.RestoreMode.FULL; + mode = RestoreMode.FULL; break; } } @@ -658,9 +495,9 @@ checkpointsRouter.get('/:id/restore/preview', async (c) => { const id = c.req.param('id'); const modeParam = c.req.query('mode'); const filesParam = c.req.query('files'); - const module = await initCheckpointModule(); + const manager = await ensureCheckpointManager(); - if (!module) { + if (!manager) { return c.json( { success: false, @@ -671,20 +508,18 @@ checkpointsRouter.get('/:id/restore/preview', async (c) => { } try { - const manager = module.getCheckpointManager(); - // 转换 mode 字符串为枚举值 let mode: RestoreMode | undefined; if (modeParam) { switch (modeParam) { case 'ai_changes_only': - mode = module.RestoreMode.AI_CHANGES_ONLY; + mode = RestoreMode.AI_CHANGES_ONLY; break; case 'workspace_only': - mode = module.RestoreMode.WORKSPACE_ONLY; + mode = RestoreMode.WORKSPACE_ONLY; break; case 'full': - mode = module.RestoreMode.FULL; + mode = RestoreMode.FULL; break; } } @@ -717,9 +552,9 @@ checkpointsRouter.get('/:id/restore/preview', async (c) => { * POST /checkpoints/unrevert - 撤销最近一次回滚 */ checkpointsRouter.post('/unrevert', async (c) => { - const module = await initCheckpointModule(); + const manager = await ensureCheckpointManager(); - if (!module) { + if (!manager) { return c.json( { success: false, @@ -730,7 +565,6 @@ checkpointsRouter.post('/unrevert', async (c) => { } try { - const manager = module.getCheckpointManager(); const result = await manager.unrevert(); if (!result.success) { @@ -763,9 +597,9 @@ checkpointsRouter.post('/unrevert', async (c) => { */ checkpointsRouter.get('/:id/safety-check', async (c) => { const id = c.req.param('id'); - const module = await initCheckpointModule(); + const manager = await ensureCheckpointManager(); - if (!module) { + if (!manager) { return c.json( { success: false, @@ -776,7 +610,6 @@ checkpointsRouter.get('/:id/safety-check', async (c) => { } try { - const manager = module.getCheckpointManager(); const result = await manager.checkSafety(id); return c.json({ @@ -798,9 +631,9 @@ checkpointsRouter.get('/:id/safety-check', async (c) => { * POST /checkpoints/cleanup - 清理过期检查点 */ checkpointsRouter.post('/cleanup', async (c) => { - const module = await initCheckpointModule(); + const manager = await ensureCheckpointManager(); - if (!module) { + if (!manager) { return c.json( { success: false, @@ -811,7 +644,6 @@ checkpointsRouter.post('/cleanup', async (c) => { } try { - const manager = module.getCheckpointManager(); const deleted = await manager.cleanup(); return c.json({ @@ -834,9 +666,9 @@ checkpointsRouter.post('/cleanup', async (c) => { */ checkpointsRouter.get('/sessions/:sessionId', async (c) => { const sessionId = c.req.param('sessionId'); - const module = await initCheckpointModule(); + const manager = await ensureCheckpointManager(); - if (!module) { + if (!manager) { return c.json( { success: false, @@ -847,7 +679,6 @@ checkpointsRouter.get('/sessions/:sessionId', async (c) => { } try { - const manager = module.getCheckpointManager(); const checkpoints = await manager.getSessionCheckpoints(sessionId); return c.json({ @@ -870,9 +701,9 @@ checkpointsRouter.get('/sessions/:sessionId', async (c) => { */ checkpointsRouter.get('/messages/:messageId', async (c) => { const messageId = c.req.param('messageId'); - const module = await initCheckpointModule(); + const manager = await ensureCheckpointManager(); - if (!module) { + if (!manager) { return c.json( { success: false, @@ -883,7 +714,6 @@ checkpointsRouter.get('/messages/:messageId', async (c) => { } try { - const manager = module.getCheckpointManager(); const checkpoints = await manager.getMessageCheckpoints(messageId); return c.json({ diff --git a/packages/server/src/routes/commands.ts b/packages/server/src/routes/commands.ts index 105874b..38e10c9 100644 --- a/packages/server/src/routes/commands.ts +++ b/packages/server/src/routes/commands.ts @@ -7,6 +7,16 @@ import { Hono } from 'hono'; import { z } from 'zod'; import { getConfig } from './config.js'; +import type { + Command, + CommandInput, + CommandExecutionResult, +} from '@ai-assistant/core'; +import { + getCommandRegistry, + createCommandExecutor, + createCommandManager, +} from '@ai-assistant/core'; // Zod schemas const ExecuteCommandInputSchema = z.object({ @@ -39,129 +49,25 @@ const UpdateCommandInputSchema = z.object({ subtask: z.boolean().optional(), }); -// Core 模块类型 -interface CommandModule { - getCommandRegistry: () => CommandRegistry; - createCommandExecutor: (workdir: string) => CommandExecutor; - createCommandManager: (workdir: string) => CommandManager; -} - -interface CommandRegistry { - initialize(workdir: string): Promise; - reload(workdir: string): Promise; - get(name: string): Command | undefined; - getAll(): Command[]; - list(): Array<{ name: string; description?: string; source: string }>; - search(query: string, limit?: number): Array<{ command: Command; score: number }>; - getStats(): { total: number; bySource: Record }; -} - -interface CommandExecutor { - execute(input: CommandInput): Promise; -} - -interface Command { - name: string; - description?: string; - template: string; - agent?: string; - model?: string; - subtask?: boolean; - source: 'builtin' | 'user' | 'project'; - sourcePath?: string; -} - -interface CommandInput { - command: string; - arguments: string; - args: string[]; - workdir: string; -} - -interface CommandExecutionResult { - success: boolean; - prompt?: string; - agent?: string; - model?: string; - subtask?: boolean; - error?: string; -} - -interface CommandManager { - create(input: { - name: string; - description?: string; - template: string; - agent?: string; - model?: string; - subtask?: boolean; - scope: 'user' | 'project'; - }): Promise<{ success: boolean; path?: string; error?: string }>; - update( - name: string, - input: { - description?: string; - template?: string; - agent?: string; - model?: string; - subtask?: boolean; - } - ): Promise<{ success: boolean; path?: string; error?: string }>; - delete(name: string): Promise<{ success: boolean; path?: string; error?: string }>; - getContent(name: string): Promise<{ - success: boolean; - data?: { - name: string; - description?: string; - template: string; - agent?: string; - model?: string; - subtask?: boolean; - source: string; - sourcePath?: string; - }; - error?: string; - }>; -} - -// Core 模块缓存 -let commandModule: CommandModule | null = null; +// Registry 初始化状态 +let registryInitialized = false; /** - * 初始化 Command 模块 + * 确保 CommandRegistry 已初始化 */ -async function initCommandModule(): Promise { - if (commandModule) return commandModule; +async function ensureRegistryInitialized(): Promise { + if (registryInitialized) return true; try { - const corePath = '@ai-assistant/core'; - const core = (await import(corePath)) as Record; - - if ( - typeof core.getCommandRegistry !== 'function' || - typeof core.createCommandExecutor !== 'function' || - typeof core.createCommandManager !== 'function' - ) { - console.warn('[Commands] Core module missing command exports'); - return null; - } - - commandModule = { - getCommandRegistry: core.getCommandRegistry as () => CommandRegistry, - createCommandExecutor: core.createCommandExecutor as (workdir: string) => CommandExecutor, - createCommandManager: core.createCommandManager as (workdir: string) => CommandManager, - }; - - // Initialize registry with server workdir const config = getConfig(); - const registry = commandModule.getCommandRegistry(); + const registry = getCommandRegistry(); await registry.initialize(config.workdir); - - console.log('[Commands] Command module initialized'); - return commandModule; + registryInitialized = true; + console.log('[Commands] Command registry initialized'); + return true; } catch (error) { - console.warn('[Commands] Failed to load core module:', error); - return null; + console.warn('[Commands] Failed to initialize command registry:', error); + return false; } } @@ -169,9 +75,7 @@ async function initCommandModule(): Promise { * GET /commands - 列出所有命令 */ commandsRouter.get('/', async (c) => { - const module = await initCommandModule(); - - if (!module) { + if (!(await ensureRegistryInitialized())) { return c.json( { success: false, @@ -181,7 +85,7 @@ commandsRouter.get('/', async (c) => { ); } - const registry = module.getCommandRegistry(); + const registry = getCommandRegistry(); return c.json({ success: true, @@ -197,9 +101,7 @@ commandsRouter.get('/', async (c) => { * 注意:这个路由必须在 /:name 之前定义,否则会被匹配为命令名 */ commandsRouter.post('/search', async (c) => { - const module = await initCommandModule(); - - if (!module) { + if (!(await ensureRegistryInitialized())) { return c.json( { success: false, @@ -213,7 +115,7 @@ commandsRouter.post('/search', async (c) => { const body = await c.req.json(); const input = SearchCommandInputSchema.parse(body); - const registry = module.getCommandRegistry(); + const registry = getCommandRegistry(); const results = registry.search(input.query, input.limit); return c.json({ @@ -240,9 +142,7 @@ commandsRouter.post('/search', async (c) => { * POST /commands/reload - 重新加载命令 */ commandsRouter.post('/reload', async (c) => { - const module = await initCommandModule(); - - if (!module) { + if (!(await ensureRegistryInitialized())) { return c.json( { success: false, @@ -254,7 +154,7 @@ commandsRouter.post('/reload', async (c) => { try { const config = getConfig(); - const registry = module.getCommandRegistry(); + const registry = getCommandRegistry(); await registry.reload(config.workdir); return c.json({ @@ -281,9 +181,8 @@ commandsRouter.post('/reload', async (c) => { */ commandsRouter.post('/:name{.+}/execute', async (c) => { const name = c.req.param('name'); - const module = await initCommandModule(); - if (!module) { + if (!(await ensureRegistryInitialized())) { return c.json( { success: false, @@ -298,7 +197,7 @@ commandsRouter.post('/:name{.+}/execute', async (c) => { const input = ExecuteCommandInputSchema.parse(body); const config = getConfig(); - const executor = module.createCommandExecutor(config.workdir); + const executor = createCommandExecutor(config.workdir); // Parse arguments const args = input.arguments ? input.arguments.split(/\s+/).filter(Boolean) : []; @@ -342,9 +241,8 @@ commandsRouter.post('/:name{.+}/execute', async (c) => { */ commandsRouter.get('/:name{.+}/content', async (c) => { const name = c.req.param('name'); - const module = await initCommandModule(); - if (!module) { + if (!(await ensureRegistryInitialized())) { return c.json( { success: false, @@ -355,7 +253,7 @@ commandsRouter.get('/:name{.+}/content', async (c) => { } const config = getConfig(); - const manager = module.createCommandManager(config.workdir); + const manager = createCommandManager(config.workdir); const result = await manager.getContent(name); if (result.success) { @@ -381,9 +279,8 @@ commandsRouter.get('/:name{.+}/content', async (c) => { */ commandsRouter.get('/:name{.+}', async (c) => { const name = c.req.param('name'); - const module = await initCommandModule(); - if (!module) { + if (!(await ensureRegistryInitialized())) { return c.json( { success: false, @@ -393,7 +290,7 @@ commandsRouter.get('/:name{.+}', async (c) => { ); } - const registry = module.getCommandRegistry(); + const registry = getCommandRegistry(); const command = registry.get(name); if (!command) { @@ -429,9 +326,7 @@ commandsRouter.get('/:name{.+}', async (c) => { * POST /commands - 创建命令 */ commandsRouter.post('/', async (c) => { - const module = await initCommandModule(); - - if (!module) { + if (!(await ensureRegistryInitialized())) { return c.json( { success: false, @@ -446,7 +341,7 @@ commandsRouter.post('/', async (c) => { const input = CreateCommandInputSchema.parse(body); const config = getConfig(); - const manager = module.createCommandManager(config.workdir); + const manager = createCommandManager(config.workdir); const result = await manager.create(input); if (result.success) { @@ -482,9 +377,8 @@ commandsRouter.post('/', async (c) => { */ commandsRouter.put('/:name{.+}', async (c) => { const name = c.req.param('name'); - const module = await initCommandModule(); - if (!module) { + if (!(await ensureRegistryInitialized())) { return c.json( { success: false, @@ -499,7 +393,7 @@ commandsRouter.put('/:name{.+}', async (c) => { const input = UpdateCommandInputSchema.parse(body); const config = getConfig(); - const manager = module.createCommandManager(config.workdir); + const manager = createCommandManager(config.workdir); const result = await manager.update(name, input); if (result.success) { @@ -535,9 +429,8 @@ commandsRouter.put('/:name{.+}', async (c) => { */ commandsRouter.delete('/:name{.+}', async (c) => { const name = c.req.param('name'); - const module = await initCommandModule(); - if (!module) { + if (!(await ensureRegistryInitialized())) { return c.json( { success: false, @@ -548,7 +441,7 @@ commandsRouter.delete('/:name{.+}', async (c) => { } const config = getConfig(); - const manager = module.createCommandManager(config.workdir); + const manager = createCommandManager(config.workdir); const result = await manager.delete(name); if (result.success) { diff --git a/packages/server/src/routes/hooks.ts b/packages/server/src/routes/hooks.ts index 18b9762..d0db77b 100644 --- a/packages/server/src/routes/hooks.ts +++ b/packages/server/src/routes/hooks.ts @@ -9,38 +9,18 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import { spawn } from 'child_process'; import { getConfig } from './config.js'; - -// Core Hooks 模块类型 -interface HooksModule { - loadProjectConfig: (directory: string) => Promise; - loadHookConfig: (directory: string) => Promise; - getConfigFilePath: (directory: string) => Promise; - createDefaultConfig: (directory: string) => Promise; -} - -interface ShellCommandConfig { - command: string[]; - environment?: Record; - timeout?: number; - cwd?: string; -} - -interface FileHookConfig { - [pattern: string]: ShellCommandConfig[]; -} - -interface HookConfig { - file_edited?: FileHookConfig; - file_created?: FileHookConfig; - file_deleted?: FileHookConfig; - session_completed?: ShellCommandConfig[]; -} - -interface ProjectConfig { - hooks?: HookConfig; - plugins?: string[]; - [key: string]: unknown; -} +import type { + HookConfig, + ProjectConfig, + ShellCommandConfig, + FileHookConfig, +} from '@ai-assistant/core'; +import { + loadProjectConfig, + loadHookConfig, + getConfigFilePath, + createDefaultConfig, +} from '@ai-assistant/core'; interface HookTestResult { success: boolean; @@ -52,44 +32,6 @@ interface HookTestResult { export const hooksRouter = new Hono(); -// Core 模块缓存 -let hooksModule: HooksModule | null = null; - -/** - * 初始化 Hooks 模块 - */ -async function initHooksModule(): Promise { - if (hooksModule) return hooksModule; - - try { - const corePath = '@ai-assistant/core'; - const core = (await import(corePath)) as Record; - - if ( - typeof core.loadProjectConfig !== 'function' || - typeof core.loadHookConfig !== 'function' || - typeof core.getConfigFilePath !== 'function' || - typeof core.createDefaultConfig !== 'function' - ) { - console.warn('[Hooks] Core module missing Hooks exports'); - return null; - } - - hooksModule = { - loadProjectConfig: core.loadProjectConfig as HooksModule['loadProjectConfig'], - loadHookConfig: core.loadHookConfig as HooksModule['loadHookConfig'], - getConfigFilePath: core.getConfigFilePath as HooksModule['getConfigFilePath'], - createDefaultConfig: core.createDefaultConfig as HooksModule['createDefaultConfig'], - }; - - console.log('[Hooks] Hooks module initialized'); - return hooksModule; - } catch (error) { - console.warn('[Hooks] Failed to load Hooks module:', error); - return null; - } -} - /** * 移除 JSON 中的注释(支持 JSONC 格式) */ @@ -123,11 +65,8 @@ async function writeConfigFile(configPath: string, config: ProjectConfig): Promi * 获取或创建配置文件路径 */ async function getOrCreateConfigPath(workdir: string): Promise { - const module = await initHooksModule(); - if (module) { - const existingPath = await module.getConfigFilePath(workdir); - if (existingPath) return existingPath; - } + const existingPath = await getConfigFilePath(workdir); + if (existingPath) return existingPath; return path.join(workdir, '.ai-assistant.json'); } @@ -135,20 +74,8 @@ async function getOrCreateConfigPath(workdir: string): Promise { * GET /hooks/config - 获取完整钩子配置 */ hooksRouter.get('/config', async (c) => { - const module = await initHooksModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Hooks module not available', - }, - 503 - ); - } - const config = getConfig(); - const hookConfig = await module.loadHookConfig(config.workdir); + const hookConfig = await loadHookConfig(config.workdir); return c.json({ success: true, @@ -165,18 +92,6 @@ hooksRouter.get('/config', async (c) => { * PUT /hooks/config - 更新完整钩子配置 */ hooksRouter.put('/config', async (c) => { - const module = await initHooksModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Hooks module not available', - }, - 503 - ); - } - try { const newHookConfig = await c.req.json(); const config = getConfig(); @@ -213,20 +128,8 @@ hooksRouter.put('/config', async (c) => { * GET /hooks/file-edited - 获取 file_edited 钩子 */ hooksRouter.get('/file-edited', async (c) => { - const module = await initHooksModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Hooks module not available', - }, - 503 - ); - } - const config = getConfig(); - const hookConfig = await module.loadHookConfig(config.workdir); + const hookConfig = await loadHookConfig(config.workdir); return c.json({ success: true, @@ -238,18 +141,6 @@ hooksRouter.get('/file-edited', async (c) => { * PUT /hooks/file-edited - 更新 file_edited 钩子 */ hooksRouter.put('/file-edited', async (c) => { - const module = await initHooksModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Hooks module not available', - }, - 503 - ); - } - try { const newFileEditedHooks = await c.req.json(); const config = getConfig(); @@ -285,20 +176,8 @@ hooksRouter.put('/file-edited', async (c) => { * GET /hooks/file-created - 获取 file_created 钩子 */ hooksRouter.get('/file-created', async (c) => { - const module = await initHooksModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Hooks module not available', - }, - 503 - ); - } - const config = getConfig(); - const hookConfig = await module.loadHookConfig(config.workdir); + const hookConfig = await loadHookConfig(config.workdir); return c.json({ success: true, @@ -310,18 +189,6 @@ hooksRouter.get('/file-created', async (c) => { * PUT /hooks/file-created - 更新 file_created 钩子 */ hooksRouter.put('/file-created', async (c) => { - const module = await initHooksModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Hooks module not available', - }, - 503 - ); - } - try { const newFileCreatedHooks = await c.req.json(); const config = getConfig(); @@ -357,20 +224,8 @@ hooksRouter.put('/file-created', async (c) => { * GET /hooks/file-deleted - 获取 file_deleted 钩子 */ hooksRouter.get('/file-deleted', async (c) => { - const module = await initHooksModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Hooks module not available', - }, - 503 - ); - } - const config = getConfig(); - const hookConfig = await module.loadHookConfig(config.workdir); + const hookConfig = await loadHookConfig(config.workdir); return c.json({ success: true, @@ -382,18 +237,6 @@ hooksRouter.get('/file-deleted', async (c) => { * PUT /hooks/file-deleted - 更新 file_deleted 钩子 */ hooksRouter.put('/file-deleted', async (c) => { - const module = await initHooksModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Hooks module not available', - }, - 503 - ); - } - try { const newFileDeletedHooks = await c.req.json(); const config = getConfig(); @@ -429,20 +272,8 @@ hooksRouter.put('/file-deleted', async (c) => { * GET /hooks/session-completed - 获取 session_completed 钩子 */ hooksRouter.get('/session-completed', async (c) => { - const module = await initHooksModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Hooks module not available', - }, - 503 - ); - } - const config = getConfig(); - const hookConfig = await module.loadHookConfig(config.workdir); + const hookConfig = await loadHookConfig(config.workdir); return c.json({ success: true, @@ -454,18 +285,6 @@ hooksRouter.get('/session-completed', async (c) => { * PUT /hooks/session-completed - 更新 session_completed 钩子 */ hooksRouter.put('/session-completed', async (c) => { - const module = await initHooksModule(); - - if (!module) { - return c.json( - { - success: false, - error: 'Hooks module not available', - }, - 503 - ); - } - try { const newSessionCompletedHooks = await c.req.json(); const config = getConfig(); diff --git a/packages/server/src/routes/mcp.ts b/packages/server/src/routes/mcp.ts index d4a39fe..0c83620 100644 --- a/packages/server/src/routes/mcp.ts +++ b/packages/server/src/routes/mcp.ts @@ -6,100 +6,44 @@ import { Hono } from 'hono'; import { getConfig } from './config.js'; - -// Core MCP 模块类型 -interface MCPModule { - getMCPManager: () => MCPManager; - loadMCPConfig: (workdir: string) => Promise; -} - -interface MCPManager { - initialize(config: MCPConfig): Promise; - shutdown(): Promise; - reconnect(serverName: string): Promise; - setServerEnabled(serverName: string, enabled: boolean): Promise; - getServerStatuses(): MCPServerStatus[]; - getServerStatus(name: string): MCPServerStatus | undefined; - getTools(): MCPTool[]; - getTool(name: string): MCPTool | undefined; - isInitialized(): boolean; -} - -interface MCPConfig { - mcp?: Record; - tools?: Record; -} - -interface MCPServerConfig { - type: 'local' | 'remote'; - command?: string[]; - url?: string; - env?: Record; - cwd?: string; - enabled?: boolean; - timeout?: number; -} - -interface MCPServerStatus { - name: string; - type: 'local' | 'remote'; - status: 'connected' | 'connecting' | 'disconnected' | 'disabled' | 'error'; - toolCount: number; - error?: string; - lastConnected?: Date; -} - -interface MCPTool { - server: string; - name: string; - originalName: string; - description: string; - inputSchema: Record; -} +import type { + MCPConfig, + MCPServerConfig, + MCPServerStatus, + MCPTool, +} from '@ai-assistant/core'; +import { + getMCPManager, + loadMCPConfig, +} from '@ai-assistant/core'; export const mcpRouter = new Hono(); -// Core 模块缓存 -let mcpModule: MCPModule | null = null; +// 配置缓存 let currentConfig: MCPConfig | null = null; +let managerInitialized = false; /** - * 初始化 MCP 模块 + * 初始化 MCP Manager */ -async function initMCPModule(): Promise { - if (mcpModule) return mcpModule; +async function ensureMCPInitialized(): Promise { + if (managerInitialized) return true; try { - const corePath = '@ai-assistant/core'; - const core = (await import(corePath)) as Record; - - if ( - typeof core.getMCPManager !== 'function' || - typeof core.loadMCPConfig !== 'function' - ) { - console.warn('[MCP] Core module missing MCP exports'); - return null; - } - - mcpModule = { - getMCPManager: core.getMCPManager as () => MCPManager, - loadMCPConfig: core.loadMCPConfig as (workdir: string) => Promise, - }; - - // 初始化 MCP Manager const config = getConfig(); - currentConfig = await mcpModule.loadMCPConfig(config.workdir); - const manager = mcpModule.getMCPManager(); + currentConfig = await loadMCPConfig(config.workdir); + const manager = getMCPManager(); if (!manager.isInitialized() && currentConfig.mcp) { await manager.initialize(currentConfig); } + managerInitialized = true; console.log('[MCP] MCP module initialized'); - return mcpModule; + return true; } catch (error) { - console.warn('[MCP] Failed to load MCP module:', error); - return null; + console.warn('[MCP] Failed to initialize MCP module:', error); + return false; } } @@ -107,9 +51,7 @@ async function initMCPModule(): Promise { * GET /mcp/servers - 获取所有服务器状态 */ mcpRouter.get('/servers', async (c) => { - const module = await initMCPModule(); - - if (!module) { + if (!(await ensureMCPInitialized())) { return c.json( { success: false, @@ -119,7 +61,7 @@ mcpRouter.get('/servers', async (c) => { ); } - const manager = module.getMCPManager(); + const manager = getMCPManager(); const statuses = manager.getServerStatuses(); // 添加配置信息 @@ -149,9 +91,8 @@ mcpRouter.get('/servers', async (c) => { */ mcpRouter.get('/servers/:name', async (c) => { const name = c.req.param('name'); - const module = await initMCPModule(); - if (!module) { + if (!(await ensureMCPInitialized())) { return c.json( { success: false, @@ -161,7 +102,7 @@ mcpRouter.get('/servers/:name', async (c) => { ); } - const manager = module.getMCPManager(); + const manager = getMCPManager(); const status = manager.getServerStatus(name); if (!status) { @@ -206,9 +147,8 @@ mcpRouter.get('/servers/:name', async (c) => { */ mcpRouter.post('/servers/:name/connect', async (c) => { const name = c.req.param('name'); - const module = await initMCPModule(); - if (!module) { + if (!(await ensureMCPInitialized())) { return c.json( { success: false, @@ -218,7 +158,7 @@ mcpRouter.post('/servers/:name/connect', async (c) => { ); } - const manager = module.getMCPManager(); + const manager = getMCPManager(); try { await manager.reconnect(name); @@ -246,9 +186,8 @@ mcpRouter.post('/servers/:name/connect', async (c) => { */ mcpRouter.post('/servers/:name/disconnect', async (c) => { const name = c.req.param('name'); - const module = await initMCPModule(); - if (!module) { + if (!(await ensureMCPInitialized())) { return c.json( { success: false, @@ -258,7 +197,7 @@ mcpRouter.post('/servers/:name/disconnect', async (c) => { ); } - const manager = module.getMCPManager(); + const manager = getMCPManager(); try { // 通过禁用来断开连接 @@ -287,9 +226,8 @@ mcpRouter.post('/servers/:name/disconnect', async (c) => { */ mcpRouter.post('/servers/:name/enable', async (c) => { const name = c.req.param('name'); - const module = await initMCPModule(); - if (!module) { + if (!(await ensureMCPInitialized())) { return c.json( { success: false, @@ -299,7 +237,7 @@ mcpRouter.post('/servers/:name/enable', async (c) => { ); } - const manager = module.getMCPManager(); + const manager = getMCPManager(); try { await manager.setServerEnabled(name, true); @@ -327,9 +265,8 @@ mcpRouter.post('/servers/:name/enable', async (c) => { */ mcpRouter.post('/servers/:name/disable', async (c) => { const name = c.req.param('name'); - const module = await initMCPModule(); - if (!module) { + if (!(await ensureMCPInitialized())) { return c.json( { success: false, @@ -339,7 +276,7 @@ mcpRouter.post('/servers/:name/disable', async (c) => { ); } - const manager = module.getMCPManager(); + const manager = getMCPManager(); try { await manager.setServerEnabled(name, false); @@ -366,9 +303,7 @@ mcpRouter.post('/servers/:name/disable', async (c) => { * GET /mcp/tools - 获取所有 MCP 工具 */ mcpRouter.get('/tools', async (c) => { - const module = await initMCPModule(); - - if (!module) { + if (!(await ensureMCPInitialized())) { return c.json( { success: false, @@ -378,7 +313,7 @@ mcpRouter.get('/tools', async (c) => { ); } - const manager = module.getMCPManager(); + const manager = getMCPManager(); const tools = manager.getTools(); return c.json({ @@ -398,9 +333,8 @@ mcpRouter.get('/tools', async (c) => { */ mcpRouter.get('/tools/:name', async (c) => { const name = c.req.param('name'); - const module = await initMCPModule(); - if (!module) { + if (!(await ensureMCPInitialized())) { return c.json( { success: false, @@ -410,7 +344,7 @@ mcpRouter.get('/tools/:name', async (c) => { ); } - const manager = module.getMCPManager(); + const manager = getMCPManager(); const tool = manager.getTool(name); if (!tool) { @@ -439,9 +373,7 @@ mcpRouter.get('/tools/:name', async (c) => { * GET /mcp/config - 获取 MCP 配置 */ mcpRouter.get('/config', async (c) => { - const module = await initMCPModule(); - - if (!module) { + if (!(await ensureMCPInitialized())) { return c.json( { success: false, @@ -459,13 +391,21 @@ mcpRouter.get('/config', async (c) => { if (currentConfig?.mcp) { for (const [name, config] of Object.entries(currentConfig.mcp)) { - safeConfig.mcp![name] = { - type: config.type, - command: config.type === 'local' ? config.command : undefined, - url: config.type === 'remote' ? config.url : undefined, - enabled: config.enabled, - timeout: config.timeout, - }; + if (config.type === 'local') { + safeConfig.mcp![name] = { + type: 'local', + command: config.command, + enabled: config.enabled, + timeout: config.timeout, + }; + } else { + safeConfig.mcp![name] = { + type: 'remote', + url: config.url, + enabled: config.enabled, + timeout: config.timeout, + }; + } } } diff --git a/packages/server/src/routes/providers.ts b/packages/server/src/routes/providers.ts index 0196273..149e0a1 100644 --- a/packages/server/src/routes/providers.ts +++ b/packages/server/src/routes/providers.ts @@ -6,103 +6,24 @@ import { Hono } from 'hono'; import { getConfig } from './config.js'; - -// Types from core - dynamically import to avoid build dependency -interface ProviderListItem { - id: string; - name: string; - description?: string; - builtin: boolean; - enabled: boolean; - hasApiKey: boolean; - modelCount: number; -} - -interface ModelInfo { - id: string; - name: string; - capabilities?: { - vision?: boolean; - functionCalling?: boolean; - streaming?: boolean; - }; - contextWindow?: number; - maxOutput?: number; -} - -interface ProviderDetail { - id: string; - name: string; - description?: string; - builtin: boolean; - baseUrl?: string; - apiKeyEnvVar?: string; - models: ModelInfo[]; - allowCustomModels: boolean; - config: { - enabled: boolean; - hasApiKey: boolean; - baseUrl?: string; - customModels: ModelInfo[]; - }; -} - -interface CustomProviderDefinition { - id: string; - name: string; - description?: string; - baseUrl: string; - apiKeyEnvVar?: string; - models?: ModelInfo[]; - allowCustomModels?: boolean; -} - -interface ProviderConfig { - id?: string; - apiKey?: string; - apiKeyEnvVar?: string; - baseUrl?: string; - enabled?: boolean; - customModels?: ModelInfo[]; -} - -interface ConnectionTestResult { - success: boolean; - latency?: number; - error?: string; -} +import type { + ProviderListItem, + ModelInfo, + ProviderDetail, + CustomProviderDefinition, + ProviderConfig, + ConnectionTestResult, +} from '@ai-assistant/core'; +import { getProviderRegistry } from '@ai-assistant/core'; export const providersRouter = new Hono(); -// Core module reference -let coreModule: any = null; - -/** - * Load core module dynamically - */ -async function getCoreModule() { - if (!coreModule) { - try { - const corePath = '@ai-assistant/core'; - coreModule = await import(corePath); - } catch { - return null; - } - } - return coreModule; -} - /** * GET /providers - List all providers */ providersRouter.get('/', async (c) => { - const core = await getCoreModule(); - if (!core) { - return c.json({ success: false, error: 'Core module not available' }, 503); - } - try { - const registry = core.getProviderRegistry(); + const registry = getProviderRegistry(); const providers: ProviderListItem[] = registry.listForApi(); return c.json({ @@ -124,15 +45,10 @@ providersRouter.get('/', async (c) => { * GET /providers/:id - Get provider detail */ providersRouter.get('/:id', async (c) => { - const core = await getCoreModule(); - if (!core) { - return c.json({ success: false, error: 'Core module not available' }, 503); - } - const id = c.req.param('id'); try { - const registry = core.getProviderRegistry(); + const registry = getProviderRegistry(); const detail: ProviderDetail | undefined = registry.getDetail(id); if (!detail) { @@ -158,15 +74,10 @@ providersRouter.get('/:id', async (c) => { * GET /providers/:id/models - Get provider's model list */ providersRouter.get('/:id/models', async (c) => { - const core = await getCoreModule(); - if (!core) { - return c.json({ success: false, error: 'Core module not available' }, 503); - } - const id = c.req.param('id'); try { - const registry = core.getProviderRegistry(); + const registry = getProviderRegistry(); if (!registry.has(id)) { return c.json({ success: false, error: `Provider not found: ${id}` }, 404); @@ -193,18 +104,13 @@ providersRouter.get('/:id/models', async (c) => { * POST /providers/:id/test - Test provider connection */ providersRouter.post('/:id/test', async (c) => { - const core = await getCoreModule(); - if (!core) { - return c.json({ success: false, error: 'Core module not available' }, 503); - } - const id = c.req.param('id'); try { const body = await c.req.json().catch(() => ({})); const apiKey = body.apiKey as string | undefined; - const registry = core.getProviderRegistry(); + const registry = getProviderRegistry(); const result: ConnectionTestResult = await registry.testConnection(id, apiKey); return c.json({ @@ -226,11 +132,6 @@ providersRouter.post('/:id/test', async (c) => { * POST /providers - Register custom provider */ providersRouter.post('/', async (c) => { - const core = await getCoreModule(); - if (!core) { - return c.json({ success: false, error: 'Core module not available' }, 503); - } - try { const body: CustomProviderDefinition = await c.req.json(); @@ -245,7 +146,7 @@ providersRouter.post('/', async (c) => { ); } - const registry = core.getProviderRegistry(); + const registry = getProviderRegistry(); registry.registerCustom(body); await registry.saveConfig(); @@ -268,17 +169,12 @@ providersRouter.post('/', async (c) => { * PUT /providers/:id - Update provider config */ providersRouter.put('/:id', async (c) => { - const core = await getCoreModule(); - if (!core) { - return c.json({ success: false, error: 'Core module not available' }, 503); - } - const id = c.req.param('id'); try { const body: ProviderConfig = await c.req.json(); - const registry = core.getProviderRegistry(); + const registry = getProviderRegistry(); if (!registry.has(id)) { return c.json({ success: false, error: `Provider not found: ${id}` }, 404); @@ -306,15 +202,10 @@ providersRouter.put('/:id', async (c) => { * DELETE /providers/:id - Delete custom provider */ providersRouter.delete('/:id', async (c) => { - const core = await getCoreModule(); - if (!core) { - return c.json({ success: false, error: 'Core module not available' }, 503); - } - const id = c.req.param('id'); try { - const registry = core.getProviderRegistry(); + const registry = getProviderRegistry(); const removed = registry.removeCustom(id); if (!removed) { @@ -342,11 +233,6 @@ providersRouter.delete('/:id', async (c) => { * POST /providers/:id/models - Add custom model */ providersRouter.post('/:id/models', async (c) => { - const core = await getCoreModule(); - if (!core) { - return c.json({ success: false, error: 'Core module not available' }, 503); - } - const providerId = c.req.param('id'); try { @@ -363,7 +249,7 @@ providersRouter.post('/:id/models', async (c) => { ); } - const registry = core.getProviderRegistry(); + const registry = getProviderRegistry(); registry.addCustomModel(providerId, body); await registry.saveConfig(); @@ -386,16 +272,11 @@ providersRouter.post('/:id/models', async (c) => { * DELETE /providers/:id/models/:modelId - Delete custom model */ providersRouter.delete('/:id/models/:modelId', async (c) => { - const core = await getCoreModule(); - if (!core) { - return c.json({ success: false, error: 'Core module not available' }, 503); - } - const providerId = c.req.param('id'); const modelId = c.req.param('modelId'); try { - const registry = core.getProviderRegistry(); + const registry = getProviderRegistry(); const removed = registry.removeCustomModel(providerId, modelId); if (!removed) { diff --git a/packages/server/src/routes/sessions.ts b/packages/server/src/routes/sessions.ts index 09b3d7d..c81a542 100644 --- a/packages/server/src/routes/sessions.ts +++ b/packages/server/src/routes/sessions.ts @@ -12,6 +12,8 @@ import { type MergedMessage, type MessagePart, } from '../types.js'; +import type { MessageInfo, Part, ToolPart } from '@ai-assistant/core'; +import { MessageStorage, PartStorage } from '@ai-assistant/core'; export const sessionsRouter = new Hono(); @@ -125,41 +127,6 @@ sessionsRouter.get('/:id/messages', async (c) => { } try { - // 动态导入 Core 存储 API - const corePath = '@ai-assistant/core'; - type MessageInfo = { - id: string; - sessionId: string; - role: 'user' | 'assistant'; - parentId?: string; - createdAt: number; - partIds: string[]; - }; - type Part = { - id: string; - createdAt: number; - type: string; - text?: string; - toolCallId?: string; - toolName?: string; - state?: { - status: 'pending' | 'running' | 'completed' | 'error'; - input?: Record; - output?: unknown; - error?: string; - time?: { start: number; end?: number }; - }; - }; - - const { MessageStorage, PartStorage } = (await import(/* webpackIgnore: true */ corePath)) as { - MessageStorage: { - listBySession(sessionId: string): Promise; - }; - PartStorage: { - getByIds(messageId: string, partIds: string[]): Promise; - }; - }; - // 获取消息列表(按创建时间排序) const messageInfos = await MessageStorage.listBySession(id); @@ -178,17 +145,18 @@ sessionsRouter.get('/:id/messages', async (c) => { if (p.type === 'reasoning') { return { type: 'reasoning', id: p.id, text: p.text ?? '' }; } - // tool - const state = p.state!; - const startTime = state.time?.start; - const endTime = state.time?.end; + // tool - 使用类型断言 + const toolPart = p as ToolPart; + const state = toolPart.state; + const startTime = state.status !== 'pending' ? state.time?.start : undefined; + const endTime = state.status === 'completed' || state.status === 'error' ? state.time?.end : undefined; return { type: 'tool', id: p.id, - toolCallId: p.toolCallId ?? '', - toolName: p.toolName ?? '', + toolCallId: toolPart.toolCallId ?? '', + toolName: toolPart.toolName ?? '', status: state.status, - arguments: state.input ?? {}, + arguments: state.status !== 'pending' ? (state.input as Record) : {}, result: state.status === 'completed' ? state.output : undefined, error: state.status === 'error' ? state.error : undefined, duration: startTime && endTime ? endTime - startTime : undefined, @@ -203,15 +171,15 @@ sessionsRouter.get('/:id/messages', async (c) => { // 兼容字段:提取工具调用 const toolCalls: ToolCallInfo[] = parts - .filter((p) => p.type === 'tool' && p.state) + .filter((p): p is ToolPart => p.type === 'tool') .map((p) => { - const state = p.state!; - const startTime = state.time?.start; - const endTime = state.time?.end; + const state = p.state; + const startTime = state.status !== 'pending' ? state.time?.start : undefined; + const endTime = state.status === 'completed' || state.status === 'error' ? state.time?.end : undefined; return { id: p.toolCallId ?? '', name: p.toolName ?? '', - arguments: state.input ?? {}, + arguments: state.status !== 'pending' ? (state.input as Record) : {}, status: state.status, result: state.status === 'completed' ? state.output : undefined, error: state.status === 'error' ? state.error : undefined, diff --git a/packages/server/src/session/manager.ts b/packages/server/src/session/manager.ts index 9e35bc7..ab73e9a 100644 --- a/packages/server/src/session/manager.ts +++ b/packages/server/src/session/manager.ts @@ -6,68 +6,24 @@ import { v4 as uuidv4 } from 'uuid'; import type { Session, CreateSessionInput, SessionStatus } from '../types.js'; - -// ============================================================================ -// Core 模块接口定义(避免构建时依赖) -// ============================================================================ - -interface SessionSummary { - id: string; - title: string; - workdir: string; - messageCount: number; - createdAt: string; - updatedAt: string; -} - -interface ProjectMetadata { - id: string; - workdir: string; - createdAt: string; - isGitRepo: boolean; -} - -interface SessionData { - id: string; - projectId: string; - parentId?: string; - agentName?: string; - createdAt: string; - updatedAt: string; - workdir: string; - title?: string; - messages: Array<{ role: string; content: unknown }>; - discoveredTools: string[]; - todos: unknown[]; -} - -interface SessionManagerInterface { - init(workdir: string): Promise; - getSession(): SessionData | null; - getProject(): ProjectMetadata | null; - listSessions(): Promise; - listAllSessions(): Promise; - deleteSession(sessionId: string): Promise; - newSession(workdir?: string): Promise; - restoreSession(sessionId: string): Promise; - getSessionId(): string | undefined; -} - -// ============================================================================ -// SessionManager 类 -// ============================================================================ +import type { + SessionData, + SessionSummary, + ProjectMetadata, +} from '@ai-assistant/core'; +import { SessionManager as CoreSessionManager } from '@ai-assistant/core'; export class SessionManager { private sessions: Map = new Map(); private sessionProjects: Map = new Map(); // sessionId -> projectId - private coreManager: SessionManagerInterface | null = null; + private coreManager: CoreSessionManager | null = null; private currentProject: ProjectMetadata | null = null; private initialized = false; /** * 获取 Core SessionManager 实例(供外部使用) */ - getCoreManager(): SessionManagerInterface | null { + getCoreManager(): CoreSessionManager | null { return this.coreManager; } @@ -85,13 +41,7 @@ export class SessionManager { if (this.initialized) return; try { - // 动态导入 Core 模块,避免构建时依赖 - const corePath = '@ai-assistant/core'; - const core = (await import(/* webpackIgnore: true */ corePath)) as { - SessionManager: new (storageDir?: string) => SessionManagerInterface; - }; - - this.coreManager = new core.SessionManager(); + this.coreManager = new CoreSessionManager(); await this.coreManager.init(process.cwd()); this.currentProject = this.coreManager.getProject(); diff --git a/packages/server/tests/unit/routes/agents.test.ts b/packages/server/tests/unit/routes/agents.test.ts index 16f03b2..37cd78b 100644 --- a/packages/server/tests/unit/routes/agents.test.ts +++ b/packages/server/tests/unit/routes/agents.test.ts @@ -34,15 +34,7 @@ const mockAgentModule = vi.hoisted(() => ({ isPresetAgent: vi.fn((name: string) => name in mockPresetAgents), })); -// Track if module should be available -let moduleAvailable = true; - -vi.mock('@ai-assistant/core', () => { - if (!moduleAvailable) { - throw new Error('Module not found'); - } - return mockAgentModule; -}); +vi.mock('@ai-assistant/core', () => mockAgentModule); vi.mock('../../../src/routes/config.js', () => ({ getConfig: vi.fn(() => ({ workdir: '/test/workdir' })), @@ -57,7 +49,6 @@ app.route('/agents', agentsRouter); describe('Agents Route', () => { beforeEach(() => { vi.clearAllMocks(); - moduleAvailable = true; mockAgentModule.loadAgentConfig.mockResolvedValue(null); mockAgentModule.saveAgentConfig.mockResolvedValue(undefined); }); diff --git a/packages/server/tests/unit/routes/commands.test.ts b/packages/server/tests/unit/routes/commands.test.ts index 33a69e1..075e352 100644 --- a/packages/server/tests/unit/routes/commands.test.ts +++ b/packages/server/tests/unit/routes/commands.test.ts @@ -35,15 +35,7 @@ const mockCommandModule = vi.hoisted(() => ({ createCommandManager: vi.fn(() => mockCommandManager), })); -// Track if module should be available -let moduleAvailable = true; - -vi.mock('@ai-assistant/core', () => { - if (!moduleAvailable) { - throw new Error('Module not found'); - } - return mockCommandModule; -}); +vi.mock('@ai-assistant/core', () => mockCommandModule); vi.mock('../../../src/routes/config.js', () => ({ getConfig: vi.fn(() => ({ workdir: '/test/workdir' })), @@ -58,7 +50,6 @@ app.route('/commands', commandsRouter); describe('Commands Route', () => { beforeEach(() => { vi.clearAllMocks(); - moduleAvailable = true; mockCommandRegistry.list.mockReturnValue([]); mockCommandRegistry.getStats.mockReturnValue({ total: 0, bySource: {} }); }); diff --git a/packages/server/tests/unit/routes/hooks.test.ts b/packages/server/tests/unit/routes/hooks.test.ts index d3e63d2..458c11b 100644 --- a/packages/server/tests/unit/routes/hooks.test.ts +++ b/packages/server/tests/unit/routes/hooks.test.ts @@ -15,15 +15,7 @@ const mockHooksModule = vi.hoisted(() => ({ createDefaultConfig: vi.fn(), })); -// Track if module should be available -let moduleAvailable = true; - -vi.mock('@ai-assistant/core', () => { - if (!moduleAvailable) { - throw new Error('Module not found'); - } - return mockHooksModule; -}); +vi.mock('@ai-assistant/core', () => mockHooksModule); vi.mock('../../../src/routes/config.js', () => ({ getConfig: vi.fn(() => ({ workdir: '/test/workdir' })), @@ -44,7 +36,6 @@ app.route('/hooks', hooksRouter); describe('Hooks Route', () => { beforeEach(() => { vi.clearAllMocks(); - moduleAvailable = true; mockHooksModule.loadHookConfig.mockResolvedValue(null); mockHooksModule.getConfigFilePath.mockResolvedValue('/test/workdir/.ai-assistant.json'); }); diff --git a/packages/server/tests/unit/routes/providers.test.ts b/packages/server/tests/unit/routes/providers.test.ts index a051ae6..7ef6e20 100644 --- a/packages/server/tests/unit/routes/providers.test.ts +++ b/packages/server/tests/unit/routes/providers.test.ts @@ -26,15 +26,7 @@ const mockCoreModule = vi.hoisted(() => ({ getProviderRegistry: vi.fn(() => mockRegistry), })); -// Track if core module should be available -let coreModuleAvailable = true; - -vi.mock('@ai-assistant/core', () => { - if (!coreModuleAvailable) { - throw new Error('Module not found'); - } - return mockCoreModule; -}); +vi.mock('@ai-assistant/core', () => mockCoreModule); import { providersRouter } from '../../../src/routes/providers.js'; @@ -45,7 +37,6 @@ app.route('/providers', providersRouter); describe('Providers Route', () => { beforeEach(() => { vi.clearAllMocks(); - coreModuleAvailable = true; }); describe('GET /providers - 列出所有提供商', () => {