refactor(server): 消除与 Core 的重复类型定义

- 删除 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: 添加类型导入
This commit is contained in:
2025-12-16 20:19:24 +08:00
parent 026429cb2f
commit 1b7d55848d
14 changed files with 283 additions and 1240 deletions
+16
View File
@@ -44,7 +44,9 @@ export type {
MessageInfo, MessageInfo,
Part, Part,
PartType, PartType,
ToolPart,
ToolStatus, ToolStatus,
ToolState,
TodoItem, TodoItem,
TodoList, TodoList,
} from './session/index.js'; } from './session/index.js';
@@ -193,6 +195,11 @@ export type {
AgentModelConfig, AgentModelConfig,
AgentToolConfig, AgentToolConfig,
AgentPermission, AgentPermission,
AgentBashPermission,
AgentFilePermission,
AgentGitPermission,
PermissionAction,
PermissionRule,
} from './agent/index.js'; } from './agent/index.js';
// Agent Events (for subagent progress tracking) // Agent Events (for subagent progress tracking)
@@ -213,6 +220,15 @@ export {
getMCPManager, getMCPManager,
loadMCPConfig, loadMCPConfig,
createMCPToolAdapter, createMCPToolAdapter,
MCPManager,
} from './mcp/index.js';
export type {
MCPConfig,
MCPServerConfig,
MCPServerStatus,
MCPServerStatusType,
MCPTool,
} from './mcp/index.js'; } from './mcp/index.js';
// Provider // Provider
+1 -18
View File
@@ -11,24 +11,7 @@ import type {
PermissionRequestContext, PermissionRequestContext,
ServerMessage, ServerMessage,
} from '../types.js'; } from '../types.js';
import type { PermissionDecision, PermissionContext } from '@ai-assistant/core';
/**
* 权限决策结果
*/
export interface PermissionDecision {
allow: boolean;
remember?: boolean;
}
/**
* 权限上下文(来自 core 模块)
*/
export interface PermissionContext {
command: string;
workdir: string;
patterns?: string[];
externalPaths?: string[];
}
// 等待中的权限请求 // 等待中的权限请求
interface PendingRequest { interface PendingRequest {
+43 -244
View File
@@ -6,83 +6,20 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import { getConfig } from './config.js'; import { getConfig } from './config.js';
import type {
// Agent 类型定义(与 Core 对应) AgentMode,
type AgentMode = 'primary' | 'subagent' | 'all'; AgentInfo,
AgentConfigFile,
interface AgentModelConfig { AgentModelConfig,
provider?: 'anthropic' | 'deepseek' | 'openai'; AgentPermission,
model?: string; } from '@ai-assistant/core';
temperature?: number; import {
topP?: number; agentRegistry,
maxTokens?: number; loadAgentConfig,
} saveAgentConfig,
presetAgents,
interface AgentToolConfig { isPresetAgent,
disabled?: string[]; } from '@ai-assistant/core';
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<string, Omit<AgentInfo, 'name'>>;
}
// Core Agent 模块类型
interface AgentModule {
agentRegistry: {
init: () => Promise<void>;
get: (name: string) => AgentInfo | undefined;
list: () => AgentInfo[];
};
loadAgentConfig: (workdir: string) => Promise<AgentConfigFile | null>;
saveAgentConfig: (workdir: string, config: AgentConfigFile, format?: 'json' | 'yaml') => Promise<void>;
presetAgents: Record<string, Omit<AgentInfo, 'name'>>;
isPresetAgent: (name: string) => boolean;
}
// API 响应类型 // API 响应类型
interface AgentListItem { interface AgentListItem {
@@ -103,72 +40,30 @@ interface AgentDefaults {
export const agentsRouter = new Hono(); export const agentsRouter = new Hono();
// Core 模块缓存 // 初始化状态
let agentModule: AgentModule | null = null;
let initialized = false; let initialized = false;
/**
* 初始化 Agent 模块
*/
async function initAgentModule(): Promise<AgentModule | null> {
if (agentModule) return agentModule;
try {
const corePath = '@ai-assistant/core';
const core = (await import(corePath)) as Record<string, unknown>;
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 已初始化 * 确保 AgentRegistry 已初始化
*/ */
async function ensureRegistryInitialized(): Promise<boolean> { async function ensureRegistryInitialized(): Promise<boolean> {
const module = await initAgentModule();
if (!module) return false;
if (!initialized) { if (!initialized) {
await module.agentRegistry.init(); await agentRegistry.init();
initialized = true; initialized = true;
} }
return true; return true;
} }
/** /**
* 将 AgentInfo 转换为 AgentListItem * 将 AgentInfo 转换为 AgentListItem
*/ */
function toListItem(agent: AgentInfo, module: AgentModule, customAgentNames: Set<string>): AgentListItem { function toListItem(agent: AgentInfo, customAgentNames: Set<string>): AgentListItem {
const isPreset = module.isPresetAgent(agent.name); const isPresetAgent_ = isPresetAgent(agent.name);
return { return {
name: agent.name, name: agent.name,
description: agent.description, description: agent.description,
mode: agent.mode, mode: agent.mode,
isPreset, isPreset: isPresetAgent_,
isCustomized: customAgentNames.has(agent.name), isCustomized: customAgentNames.has(agent.name),
model: agent.model?.model, model: agent.model?.model,
maxSteps: agent.maxSteps, maxSteps: agent.maxSteps,
@@ -179,18 +74,6 @@ function toListItem(agent: AgentInfo, module: AgentModule, customAgentNames: Set
* GET /agents - 获取所有 Agent 列表 * GET /agents - 获取所有 Agent 列表
*/ */
agentsRouter.get('/', async (c) => { agentsRouter.get('/', async (c) => {
const module = await initAgentModule();
if (!module) {
return c.json(
{
success: false,
error: 'Agent module not available',
},
503
);
}
if (!(await ensureRegistryInitialized())) { if (!(await ensureRegistryInitialized())) {
return c.json( return c.json(
{ {
@@ -202,11 +85,11 @@ agentsRouter.get('/', async (c) => {
} }
const config = getConfig(); 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 customAgentNames = new Set(Object.keys(userConfig?.agents || {}));
const agents = module.agentRegistry.list(); const agents = agentRegistry.list();
const items = agents.map((agent) => toListItem(agent, module, customAgentNames)); const items = agents.map((agent) => toListItem(agent, customAgentNames));
return c.json({ return c.json({
success: true, success: true,
@@ -218,19 +101,7 @@ agentsRouter.get('/', async (c) => {
* GET /agents/presets - 获取预设 Agent 列表 * GET /agents/presets - 获取预设 Agent 列表
*/ */
agentsRouter.get('/presets', async (c) => { agentsRouter.get('/presets', async (c) => {
const module = await initAgentModule(); const presets = Object.entries(presetAgents).map(([name, agent]) => ({
if (!module) {
return c.json(
{
success: false,
error: 'Agent module not available',
},
503
);
}
const presets = Object.entries(module.presetAgents).map(([name, agent]) => ({
name, name,
description: agent.description, description: agent.description,
mode: agent.mode, mode: agent.mode,
@@ -250,20 +121,8 @@ agentsRouter.get('/presets', async (c) => {
* GET /agents/defaults - 获取全局默认配置 * GET /agents/defaults - 获取全局默认配置
*/ */
agentsRouter.get('/defaults', async (c) => { 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 config = getConfig();
const userConfig = await module.loadAgentConfig(config.workdir); const userConfig = await loadAgentConfig(config.workdir);
return c.json({ return c.json({
success: true, success: true,
@@ -275,24 +134,12 @@ agentsRouter.get('/defaults', async (c) => {
* PUT /agents/defaults - 更新全局默认配置 * PUT /agents/defaults - 更新全局默认配置
*/ */
agentsRouter.put('/defaults', async (c) => { agentsRouter.put('/defaults', async (c) => {
const module = await initAgentModule();
if (!module) {
return c.json(
{
success: false,
error: 'Agent module not available',
},
503
);
}
try { try {
const newDefaults = await c.req.json<AgentDefaults>(); const newDefaults = await c.req.json<AgentDefaults>();
const config = getConfig(); const config = getConfig();
// 加载现有配置 // 加载现有配置
let userConfig = await module.loadAgentConfig(config.workdir); let userConfig = await loadAgentConfig(config.workdir);
if (!userConfig) { if (!userConfig) {
userConfig = {}; userConfig = {};
} }
@@ -301,7 +148,7 @@ agentsRouter.put('/defaults', async (c) => {
userConfig.defaults = newDefaults; userConfig.defaults = newDefaults;
// 保存配置 // 保存配置
await module.saveAgentConfig(config.workdir, userConfig); await saveAgentConfig(config.workdir, userConfig);
// 重新初始化 registry 以应用新配置 // 重新初始化 registry 以应用新配置
initialized = false; initialized = false;
@@ -326,18 +173,6 @@ agentsRouter.put('/defaults', async (c) => {
* GET /agents/:name - 获取单个 Agent 详情 * GET /agents/:name - 获取单个 Agent 详情
*/ */
agentsRouter.get('/:name', async (c) => { 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())) { if (!(await ensureRegistryInitialized())) {
return c.json( return c.json(
{ {
@@ -349,7 +184,7 @@ agentsRouter.get('/:name', async (c) => {
} }
const name = c.req.param('name'); const name = c.req.param('name');
const agent = module.agentRegistry.get(name); const agent = agentRegistry.get(name);
if (!agent) { if (!agent) {
return c.json( return c.json(
@@ -362,15 +197,15 @@ agentsRouter.get('/:name', async (c) => {
} }
const config = getConfig(); const config = getConfig();
const userConfig = await module.loadAgentConfig(config.workdir); const userConfig = await loadAgentConfig(config.workdir);
const isPreset = module.isPresetAgent(name); const isPresetAgent_ = isPresetAgent(name);
const isCustomized = !!(userConfig?.agents && name in userConfig.agents); const isCustomized = !!(userConfig?.agents && name in userConfig.agents);
return c.json({ return c.json({
success: true, success: true,
data: { data: {
...agent, ...agent,
isPreset, isPreset: isPresetAgent_,
isCustomized, isCustomized,
}, },
}); });
@@ -380,18 +215,6 @@ agentsRouter.get('/:name', async (c) => {
* POST /agents - 创建新 Agent * POST /agents - 创建新 Agent
*/ */
agentsRouter.post('/', async (c) => { agentsRouter.post('/', async (c) => {
const module = await initAgentModule();
if (!module) {
return c.json(
{
success: false,
error: 'Agent module not available',
},
503
);
}
try { try {
const body = await c.req.json<{ name: string } & Omit<AgentInfo, 'name'>>(); const body = await c.req.json<{ name: string } & Omit<AgentInfo, 'name'>>();
const { name, ...agentConfig } = body; const { name, ...agentConfig } = body;
@@ -407,7 +230,7 @@ agentsRouter.post('/', async (c) => {
} }
// 检查名称是否与预设冲突 // 检查名称是否与预设冲突
if (module.isPresetAgent(name)) { if (isPresetAgent(name)) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -418,7 +241,7 @@ agentsRouter.post('/', async (c) => {
} }
const config = getConfig(); const config = getConfig();
let userConfig = await module.loadAgentConfig(config.workdir); let userConfig = await loadAgentConfig(config.workdir);
if (!userConfig) { if (!userConfig) {
userConfig = {}; userConfig = {};
} }
@@ -441,13 +264,13 @@ agentsRouter.post('/', async (c) => {
userConfig.agents[name] = agentConfig; userConfig.agents[name] = agentConfig;
// 保存配置 // 保存配置
await module.saveAgentConfig(config.workdir, userConfig); await saveAgentConfig(config.workdir, userConfig);
// 重新初始化 registry // 重新初始化 registry
initialized = false; initialized = false;
await ensureRegistryInitialized(); await ensureRegistryInitialized();
const createdAgent = module.agentRegistry.get(name); const createdAgent = agentRegistry.get(name);
return c.json({ return c.json({
success: true, success: true,
@@ -474,24 +297,12 @@ agentsRouter.post('/', async (c) => {
* PUT /agents/:name - 更新 Agent * PUT /agents/:name - 更新 Agent
*/ */
agentsRouter.put('/:name', async (c) => { agentsRouter.put('/:name', async (c) => {
const module = await initAgentModule();
if (!module) {
return c.json(
{
success: false,
error: 'Agent module not available',
},
503
);
}
try { try {
const name = c.req.param('name'); const name = c.req.param('name');
const agentConfig = await c.req.json<Omit<AgentInfo, 'name'>>(); const agentConfig = await c.req.json<Omit<AgentInfo, 'name'>>();
const config = getConfig(); const config = getConfig();
let userConfig = await module.loadAgentConfig(config.workdir); let userConfig = await loadAgentConfig(config.workdir);
if (!userConfig) { if (!userConfig) {
userConfig = {}; userConfig = {};
} }
@@ -503,24 +314,24 @@ agentsRouter.put('/:name', async (c) => {
userConfig.agents[name] = agentConfig; userConfig.agents[name] = agentConfig;
// 保存配置 // 保存配置
await module.saveAgentConfig(config.workdir, userConfig); await saveAgentConfig(config.workdir, userConfig);
// 重新初始化 registry // 重新初始化 registry
initialized = false; initialized = false;
await ensureRegistryInitialized(); await ensureRegistryInitialized();
const updatedAgent = module.agentRegistry.get(name); const updatedAgent = agentRegistry.get(name);
const isPreset = module.isPresetAgent(name); const isPresetAgent_ = isPresetAgent(name);
return c.json({ return c.json({
success: true, success: true,
data: updatedAgent data: updatedAgent
? { ? {
...updatedAgent, ...updatedAgent,
isPreset, isPreset: isPresetAgent_,
isCustomized: true, isCustomized: true,
} }
: { name, ...agentConfig, isPreset, isCustomized: true }, : { name, ...agentConfig, isPreset: isPresetAgent_, isCustomized: true },
}); });
} catch (error) { } catch (error) {
return c.json( return c.json(
@@ -537,26 +348,14 @@ agentsRouter.put('/:name', async (c) => {
* DELETE /agents/:name - 删除 Agent * DELETE /agents/:name - 删除 Agent
*/ */
agentsRouter.delete('/:name', async (c) => { agentsRouter.delete('/:name', async (c) => {
const module = await initAgentModule();
if (!module) {
return c.json(
{
success: false,
error: 'Agent module not available',
},
503
);
}
try { try {
const name = c.req.param('name'); const name = c.req.param('name');
const config = getConfig(); const config = getConfig();
const userConfig = await module.loadAgentConfig(config.workdir); const userConfig = await loadAgentConfig(config.workdir);
if (!userConfig?.agents || !(name in userConfig.agents)) { if (!userConfig?.agents || !(name in userConfig.agents)) {
// 如果是预设 Agent,返回特定错误 // 如果是预设 Agent,返回特定错误
if (module.isPresetAgent(name)) { if (isPresetAgent(name)) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -579,7 +378,7 @@ agentsRouter.delete('/:name', async (c) => {
delete userConfig.agents[name]; delete userConfig.agents[name];
// 保存配置 // 保存配置
await module.saveAgentConfig(config.workdir, userConfig); await saveAgentConfig(config.workdir, userConfig);
// 重新初始化 registry // 重新初始化 registry
initialized = false; initialized = false;
+66 -236
View File
@@ -6,157 +6,26 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import { getConfig } from './config.js'; import { getConfig } from './config.js';
import type {
// Core Checkpoint 模块类型 CheckpointMetadata,
interface CheckpointModule { CheckpointConfig,
getCheckpointManager: () => CheckpointManager; CheckpointTrigger,
initCheckpointManager: ( FileChange,
workDir: string, FileChangeType,
config?: Partial<CheckpointConfig> DiffInfo,
) => Promise<CheckpointManager>; FileDiff,
RestoreMode: typeof RestoreMode; RollbackOptions,
} RollbackResult,
RollbackRecord,
interface CheckpointManager { SafetyCheckResult,
initialize(): Promise<void>; UnrevertResult,
isEnabled(): boolean; } from '@ai-assistant/core';
getConfig(): CheckpointConfig; import {
listCheckpoints(): Promise<CheckpointMetadata[]>; CheckpointManager,
getCheckpoint(idOrHash: string): Promise<CheckpointMetadata | null>; getCheckpointManager,
getLatestCheckpoint(): Promise<CheckpointMetadata | null>; initCheckpointManager,
createCheckpoint(options: { RestoreMode,
name?: string; } from '@ai-assistant/core';
description?: string;
trigger?: CheckpointTrigger;
}): Promise<CheckpointMetadata>;
deleteCheckpoint(id: string): Promise<boolean>;
getDiff(checkpointId: string): Promise<DiffInfo>;
getFileDiff(checkpointId: string, filePath: string): Promise<FileDiff>;
rollback(options: RollbackOptions): Promise<RollbackResult>;
checkSafety(checkpointId: string): Promise<SafetyCheckResult>;
unrevert(): Promise<UnrevertResult>;
canUnrevert(): boolean;
getLastRollback(): RollbackRecord | null;
cleanup(): Promise<number>;
getStats(): Promise<CheckpointStats>;
getSessionCheckpoints(sessionId: string): Promise<CheckpointMetadata[]>;
getMessageCheckpoints(messageId: string): Promise<CheckpointMetadata[]>;
}
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<string, unknown>;
};
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;
}
interface CheckpointStats { interface CheckpointStats {
count: number; count: number;
@@ -166,46 +35,25 @@ interface CheckpointStats {
export const checkpointsRouter = new Hono(); export const checkpointsRouter = new Hono();
// Core 模块缓存 // Manager 初始化状态
let checkpointModule: CheckpointModule | null = null;
let managerInitialized = false; let managerInitialized = false;
/** /**
* 初始化 Checkpoint 模块 * 初始化 Checkpoint 模块
*/ */
async function initCheckpointModule(): Promise<CheckpointModule | null> { async function ensureCheckpointManager(): Promise<CheckpointManager | null> {
if (checkpointModule && managerInitialized) return checkpointModule; if (managerInitialized) {
return getCheckpointManager();
try {
const corePath = '@ai-assistant/core';
const core = (await import(corePath)) as Record<string, unknown>;
if (
typeof core.getCheckpointManager !== 'function' ||
typeof core.initCheckpointManager !== 'function'
) {
console.warn('[Checkpoints] Core module missing Checkpoint exports');
return null;
} }
checkpointModule = { try {
getCheckpointManager: core.getCheckpointManager as () => CheckpointManager,
initCheckpointManager: core.initCheckpointManager as (
workDir: string,
config?: Partial<CheckpointConfig>
) => Promise<CheckpointManager>,
RestoreMode: core.RestoreMode as typeof RestoreMode,
};
// 初始化 Checkpoint Manager
const config = getConfig(); const config = getConfig();
await checkpointModule.initCheckpointManager(config.workdir); await initCheckpointManager(config.workdir);
managerInitialized = true; managerInitialized = true;
console.log('[Checkpoints] Checkpoint module initialized'); console.log('[Checkpoints] Checkpoint module initialized');
return checkpointModule; return getCheckpointManager();
} catch (error) { } catch (error) {
console.warn('[Checkpoints] Failed to load Checkpoint module:', error); console.warn('[Checkpoints] Failed to initialize Checkpoint module:', error);
return null; return null;
} }
} }
@@ -214,9 +62,9 @@ async function initCheckpointModule(): Promise<CheckpointModule | null> {
* GET /checkpoints - 获取所有检查点列表 * GET /checkpoints - 获取所有检查点列表
*/ */
checkpointsRouter.get('/', async (c) => { checkpointsRouter.get('/', async (c) => {
const module = await initCheckpointModule(); const manager = await ensureCheckpointManager();
if (!module) { if (!manager) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -227,7 +75,6 @@ checkpointsRouter.get('/', async (c) => {
} }
try { try {
const manager = module.getCheckpointManager();
const checkpoints = await manager.listCheckpoints(); const checkpoints = await manager.listCheckpoints();
return c.json({ return c.json({
@@ -259,9 +106,9 @@ checkpointsRouter.get('/', async (c) => {
* GET /checkpoints/stats - 获取检查点统计信息 * GET /checkpoints/stats - 获取检查点统计信息
*/ */
checkpointsRouter.get('/stats', async (c) => { checkpointsRouter.get('/stats', async (c) => {
const module = await initCheckpointModule(); const manager = await ensureCheckpointManager();
if (!module) { if (!manager) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -272,7 +119,6 @@ checkpointsRouter.get('/stats', async (c) => {
} }
try { try {
const manager = module.getCheckpointManager();
const stats = await manager.getStats(); const stats = await manager.getStats();
return c.json({ return c.json({
@@ -294,9 +140,9 @@ checkpointsRouter.get('/stats', async (c) => {
* GET /checkpoints/latest - 获取最新检查点 * GET /checkpoints/latest - 获取最新检查点
*/ */
checkpointsRouter.get('/latest', async (c) => { checkpointsRouter.get('/latest', async (c) => {
const module = await initCheckpointModule(); const manager = await ensureCheckpointManager();
if (!module) { if (!manager) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -307,7 +153,6 @@ checkpointsRouter.get('/latest', async (c) => {
} }
try { try {
const manager = module.getCheckpointManager();
const checkpoint = await manager.getLatestCheckpoint(); const checkpoint = await manager.getLatestCheckpoint();
return c.json({ return c.json({
@@ -329,9 +174,9 @@ checkpointsRouter.get('/latest', async (c) => {
* GET /checkpoints/unrevert/status - 检查是否可撤销回滚 * GET /checkpoints/unrevert/status - 检查是否可撤销回滚
*/ */
checkpointsRouter.get('/unrevert/status', async (c) => { checkpointsRouter.get('/unrevert/status', async (c) => {
const module = await initCheckpointModule(); const manager = await ensureCheckpointManager();
if (!module) { if (!manager) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -342,7 +187,6 @@ checkpointsRouter.get('/unrevert/status', async (c) => {
} }
try { try {
const manager = module.getCheckpointManager();
const canUnrevert = manager.canUnrevert(); const canUnrevert = manager.canUnrevert();
const lastRollback = manager.getLastRollback(); const lastRollback = manager.getLastRollback();
@@ -376,9 +220,9 @@ checkpointsRouter.get('/unrevert/status', async (c) => {
*/ */
checkpointsRouter.get('/:id', async (c) => { checkpointsRouter.get('/:id', async (c) => {
const id = c.req.param('id'); const id = c.req.param('id');
const module = await initCheckpointModule(); const manager = await ensureCheckpointManager();
if (!module) { if (!manager) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -389,7 +233,6 @@ checkpointsRouter.get('/:id', async (c) => {
} }
try { try {
const manager = module.getCheckpointManager();
const checkpoint = await manager.getCheckpoint(id); const checkpoint = await manager.getCheckpoint(id);
if (!checkpoint) { if (!checkpoint) {
@@ -421,9 +264,9 @@ checkpointsRouter.get('/:id', async (c) => {
* POST /checkpoints - 创建手动检查点 * POST /checkpoints - 创建手动检查点
*/ */
checkpointsRouter.post('/', async (c) => { checkpointsRouter.post('/', async (c) => {
const module = await initCheckpointModule(); const manager = await ensureCheckpointManager();
if (!module) { if (!manager) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -435,7 +278,6 @@ checkpointsRouter.post('/', async (c) => {
try { try {
const body = await c.req.json<{ name?: string; description?: string }>(); const body = await c.req.json<{ name?: string; description?: string }>();
const manager = module.getCheckpointManager();
const checkpoint = await manager.createCheckpoint({ const checkpoint = await manager.createCheckpoint({
name: body.name, name: body.name,
@@ -463,9 +305,9 @@ checkpointsRouter.post('/', async (c) => {
*/ */
checkpointsRouter.delete('/:id', async (c) => { checkpointsRouter.delete('/:id', async (c) => {
const id = c.req.param('id'); const id = c.req.param('id');
const module = await initCheckpointModule(); const manager = await ensureCheckpointManager();
if (!module) { if (!manager) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -476,7 +318,6 @@ checkpointsRouter.delete('/:id', async (c) => {
} }
try { try {
const manager = module.getCheckpointManager();
const deleted = await manager.deleteCheckpoint(id); const deleted = await manager.deleteCheckpoint(id);
if (!deleted) { if (!deleted) {
@@ -509,9 +350,9 @@ checkpointsRouter.delete('/:id', async (c) => {
*/ */
checkpointsRouter.get('/:id/diff', async (c) => { checkpointsRouter.get('/:id/diff', async (c) => {
const id = c.req.param('id'); const id = c.req.param('id');
const module = await initCheckpointModule(); const manager = await ensureCheckpointManager();
if (!module) { if (!manager) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -522,7 +363,6 @@ checkpointsRouter.get('/:id/diff', async (c) => {
} }
try { try {
const manager = module.getCheckpointManager();
const diff = await manager.getDiff(id); const diff = await manager.getDiff(id);
return c.json({ return c.json({
@@ -546,9 +386,9 @@ checkpointsRouter.get('/:id/diff', async (c) => {
checkpointsRouter.get('/:id/file-diff', async (c) => { checkpointsRouter.get('/:id/file-diff', async (c) => {
const id = c.req.param('id'); const id = c.req.param('id');
const filePath = c.req.query('path'); const filePath = c.req.query('path');
const module = await initCheckpointModule(); const manager = await ensureCheckpointManager();
if (!module) { if (!manager) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -569,7 +409,6 @@ checkpointsRouter.get('/:id/file-diff', async (c) => {
} }
try { try {
const manager = module.getCheckpointManager();
const fileDiff = await manager.getFileDiff(id, filePath); const fileDiff = await manager.getFileDiff(id, filePath);
return c.json({ return c.json({
@@ -592,9 +431,9 @@ checkpointsRouter.get('/:id/file-diff', async (c) => {
*/ */
checkpointsRouter.post('/:id/restore', async (c) => { checkpointsRouter.post('/:id/restore', async (c) => {
const id = c.req.param('id'); const id = c.req.param('id');
const module = await initCheckpointModule(); const manager = await ensureCheckpointManager();
if (!module) { if (!manager) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -611,20 +450,18 @@ checkpointsRouter.post('/:id/restore', async (c) => {
skipSafetyCheck?: boolean; skipSafetyCheck?: boolean;
}>(); }>();
const manager = module.getCheckpointManager();
// 转换 mode 字符串为枚举值 // 转换 mode 字符串为枚举值
let mode: RestoreMode | undefined; let mode: RestoreMode | undefined;
if (body.mode) { if (body.mode) {
switch (body.mode) { switch (body.mode) {
case 'ai_changes_only': case 'ai_changes_only':
mode = module.RestoreMode.AI_CHANGES_ONLY; mode = RestoreMode.AI_CHANGES_ONLY;
break; break;
case 'workspace_only': case 'workspace_only':
mode = module.RestoreMode.WORKSPACE_ONLY; mode = RestoreMode.WORKSPACE_ONLY;
break; break;
case 'full': case 'full':
mode = module.RestoreMode.FULL; mode = RestoreMode.FULL;
break; break;
} }
} }
@@ -658,9 +495,9 @@ checkpointsRouter.get('/:id/restore/preview', async (c) => {
const id = c.req.param('id'); const id = c.req.param('id');
const modeParam = c.req.query('mode'); const modeParam = c.req.query('mode');
const filesParam = c.req.query('files'); const filesParam = c.req.query('files');
const module = await initCheckpointModule(); const manager = await ensureCheckpointManager();
if (!module) { if (!manager) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -671,20 +508,18 @@ checkpointsRouter.get('/:id/restore/preview', async (c) => {
} }
try { try {
const manager = module.getCheckpointManager();
// 转换 mode 字符串为枚举值 // 转换 mode 字符串为枚举值
let mode: RestoreMode | undefined; let mode: RestoreMode | undefined;
if (modeParam) { if (modeParam) {
switch (modeParam) { switch (modeParam) {
case 'ai_changes_only': case 'ai_changes_only':
mode = module.RestoreMode.AI_CHANGES_ONLY; mode = RestoreMode.AI_CHANGES_ONLY;
break; break;
case 'workspace_only': case 'workspace_only':
mode = module.RestoreMode.WORKSPACE_ONLY; mode = RestoreMode.WORKSPACE_ONLY;
break; break;
case 'full': case 'full':
mode = module.RestoreMode.FULL; mode = RestoreMode.FULL;
break; break;
} }
} }
@@ -717,9 +552,9 @@ checkpointsRouter.get('/:id/restore/preview', async (c) => {
* POST /checkpoints/unrevert - 撤销最近一次回滚 * POST /checkpoints/unrevert - 撤销最近一次回滚
*/ */
checkpointsRouter.post('/unrevert', async (c) => { checkpointsRouter.post('/unrevert', async (c) => {
const module = await initCheckpointModule(); const manager = await ensureCheckpointManager();
if (!module) { if (!manager) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -730,7 +565,6 @@ checkpointsRouter.post('/unrevert', async (c) => {
} }
try { try {
const manager = module.getCheckpointManager();
const result = await manager.unrevert(); const result = await manager.unrevert();
if (!result.success) { if (!result.success) {
@@ -763,9 +597,9 @@ checkpointsRouter.post('/unrevert', async (c) => {
*/ */
checkpointsRouter.get('/:id/safety-check', async (c) => { checkpointsRouter.get('/:id/safety-check', async (c) => {
const id = c.req.param('id'); const id = c.req.param('id');
const module = await initCheckpointModule(); const manager = await ensureCheckpointManager();
if (!module) { if (!manager) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -776,7 +610,6 @@ checkpointsRouter.get('/:id/safety-check', async (c) => {
} }
try { try {
const manager = module.getCheckpointManager();
const result = await manager.checkSafety(id); const result = await manager.checkSafety(id);
return c.json({ return c.json({
@@ -798,9 +631,9 @@ checkpointsRouter.get('/:id/safety-check', async (c) => {
* POST /checkpoints/cleanup - 清理过期检查点 * POST /checkpoints/cleanup - 清理过期检查点
*/ */
checkpointsRouter.post('/cleanup', async (c) => { checkpointsRouter.post('/cleanup', async (c) => {
const module = await initCheckpointModule(); const manager = await ensureCheckpointManager();
if (!module) { if (!manager) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -811,7 +644,6 @@ checkpointsRouter.post('/cleanup', async (c) => {
} }
try { try {
const manager = module.getCheckpointManager();
const deleted = await manager.cleanup(); const deleted = await manager.cleanup();
return c.json({ return c.json({
@@ -834,9 +666,9 @@ checkpointsRouter.post('/cleanup', async (c) => {
*/ */
checkpointsRouter.get('/sessions/:sessionId', async (c) => { checkpointsRouter.get('/sessions/:sessionId', async (c) => {
const sessionId = c.req.param('sessionId'); const sessionId = c.req.param('sessionId');
const module = await initCheckpointModule(); const manager = await ensureCheckpointManager();
if (!module) { if (!manager) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -847,7 +679,6 @@ checkpointsRouter.get('/sessions/:sessionId', async (c) => {
} }
try { try {
const manager = module.getCheckpointManager();
const checkpoints = await manager.getSessionCheckpoints(sessionId); const checkpoints = await manager.getSessionCheckpoints(sessionId);
return c.json({ return c.json({
@@ -870,9 +701,9 @@ checkpointsRouter.get('/sessions/:sessionId', async (c) => {
*/ */
checkpointsRouter.get('/messages/:messageId', async (c) => { checkpointsRouter.get('/messages/:messageId', async (c) => {
const messageId = c.req.param('messageId'); const messageId = c.req.param('messageId');
const module = await initCheckpointModule(); const manager = await ensureCheckpointManager();
if (!module) { if (!manager) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -883,7 +714,6 @@ checkpointsRouter.get('/messages/:messageId', async (c) => {
} }
try { try {
const manager = module.getCheckpointManager();
const checkpoints = await manager.getMessageCheckpoints(messageId); const checkpoints = await manager.getMessageCheckpoints(messageId);
return c.json({ return c.json({
+39 -146
View File
@@ -7,6 +7,16 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import { z } from 'zod'; import { z } from 'zod';
import { getConfig } from './config.js'; import { getConfig } from './config.js';
import type {
Command,
CommandInput,
CommandExecutionResult,
} from '@ai-assistant/core';
import {
getCommandRegistry,
createCommandExecutor,
createCommandManager,
} from '@ai-assistant/core';
// Zod schemas // Zod schemas
const ExecuteCommandInputSchema = z.object({ const ExecuteCommandInputSchema = z.object({
@@ -39,129 +49,25 @@ const UpdateCommandInputSchema = z.object({
subtask: z.boolean().optional(), subtask: z.boolean().optional(),
}); });
// Core 模块类型 // Registry 初始化状态
interface CommandModule { let registryInitialized = false;
getCommandRegistry: () => CommandRegistry;
createCommandExecutor: (workdir: string) => CommandExecutor;
createCommandManager: (workdir: string) => CommandManager;
}
interface CommandRegistry {
initialize(workdir: string): Promise<void>;
reload(workdir: string): Promise<void>;
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<string, number> };
}
interface CommandExecutor {
execute(input: CommandInput): Promise<CommandExecutionResult>;
}
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;
/** /**
* 初始化 Command 模块 * 确保 CommandRegistry 已初始化
*/ */
async function initCommandModule(): Promise<CommandModule | null> { async function ensureRegistryInitialized(): Promise<boolean> {
if (commandModule) return commandModule; if (registryInitialized) return true;
try { try {
const corePath = '@ai-assistant/core';
const core = (await import(corePath)) as Record<string, unknown>;
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 config = getConfig();
const registry = commandModule.getCommandRegistry(); const registry = getCommandRegistry();
await registry.initialize(config.workdir); await registry.initialize(config.workdir);
registryInitialized = true;
console.log('[Commands] Command module initialized'); console.log('[Commands] Command registry initialized');
return commandModule; return true;
} catch (error) { } catch (error) {
console.warn('[Commands] Failed to load core module:', error); console.warn('[Commands] Failed to initialize command registry:', error);
return null; return false;
} }
} }
@@ -169,9 +75,7 @@ async function initCommandModule(): Promise<CommandModule | null> {
* GET /commands - 列出所有命令 * GET /commands - 列出所有命令
*/ */
commandsRouter.get('/', async (c) => { commandsRouter.get('/', async (c) => {
const module = await initCommandModule(); if (!(await ensureRegistryInitialized())) {
if (!module) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -181,7 +85,7 @@ commandsRouter.get('/', async (c) => {
); );
} }
const registry = module.getCommandRegistry(); const registry = getCommandRegistry();
return c.json({ return c.json({
success: true, success: true,
@@ -197,9 +101,7 @@ commandsRouter.get('/', async (c) => {
* 注意:这个路由必须在 /:name 之前定义,否则会被匹配为命令名 * 注意:这个路由必须在 /:name 之前定义,否则会被匹配为命令名
*/ */
commandsRouter.post('/search', async (c) => { commandsRouter.post('/search', async (c) => {
const module = await initCommandModule(); if (!(await ensureRegistryInitialized())) {
if (!module) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -213,7 +115,7 @@ commandsRouter.post('/search', async (c) => {
const body = await c.req.json(); const body = await c.req.json();
const input = SearchCommandInputSchema.parse(body); const input = SearchCommandInputSchema.parse(body);
const registry = module.getCommandRegistry(); const registry = getCommandRegistry();
const results = registry.search(input.query, input.limit); const results = registry.search(input.query, input.limit);
return c.json({ return c.json({
@@ -240,9 +142,7 @@ commandsRouter.post('/search', async (c) => {
* POST /commands/reload - 重新加载命令 * POST /commands/reload - 重新加载命令
*/ */
commandsRouter.post('/reload', async (c) => { commandsRouter.post('/reload', async (c) => {
const module = await initCommandModule(); if (!(await ensureRegistryInitialized())) {
if (!module) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -254,7 +154,7 @@ commandsRouter.post('/reload', async (c) => {
try { try {
const config = getConfig(); const config = getConfig();
const registry = module.getCommandRegistry(); const registry = getCommandRegistry();
await registry.reload(config.workdir); await registry.reload(config.workdir);
return c.json({ return c.json({
@@ -281,9 +181,8 @@ commandsRouter.post('/reload', async (c) => {
*/ */
commandsRouter.post('/:name{.+}/execute', async (c) => { commandsRouter.post('/:name{.+}/execute', async (c) => {
const name = c.req.param('name'); const name = c.req.param('name');
const module = await initCommandModule();
if (!module) { if (!(await ensureRegistryInitialized())) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -298,7 +197,7 @@ commandsRouter.post('/:name{.+}/execute', async (c) => {
const input = ExecuteCommandInputSchema.parse(body); const input = ExecuteCommandInputSchema.parse(body);
const config = getConfig(); const config = getConfig();
const executor = module.createCommandExecutor(config.workdir); const executor = createCommandExecutor(config.workdir);
// Parse arguments // Parse arguments
const args = input.arguments ? input.arguments.split(/\s+/).filter(Boolean) : []; 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) => { commandsRouter.get('/:name{.+}/content', async (c) => {
const name = c.req.param('name'); const name = c.req.param('name');
const module = await initCommandModule();
if (!module) { if (!(await ensureRegistryInitialized())) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -355,7 +253,7 @@ commandsRouter.get('/:name{.+}/content', async (c) => {
} }
const config = getConfig(); const config = getConfig();
const manager = module.createCommandManager(config.workdir); const manager = createCommandManager(config.workdir);
const result = await manager.getContent(name); const result = await manager.getContent(name);
if (result.success) { if (result.success) {
@@ -381,9 +279,8 @@ commandsRouter.get('/:name{.+}/content', async (c) => {
*/ */
commandsRouter.get('/:name{.+}', async (c) => { commandsRouter.get('/:name{.+}', async (c) => {
const name = c.req.param('name'); const name = c.req.param('name');
const module = await initCommandModule();
if (!module) { if (!(await ensureRegistryInitialized())) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -393,7 +290,7 @@ commandsRouter.get('/:name{.+}', async (c) => {
); );
} }
const registry = module.getCommandRegistry(); const registry = getCommandRegistry();
const command = registry.get(name); const command = registry.get(name);
if (!command) { if (!command) {
@@ -429,9 +326,7 @@ commandsRouter.get('/:name{.+}', async (c) => {
* POST /commands - 创建命令 * POST /commands - 创建命令
*/ */
commandsRouter.post('/', async (c) => { commandsRouter.post('/', async (c) => {
const module = await initCommandModule(); if (!(await ensureRegistryInitialized())) {
if (!module) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -446,7 +341,7 @@ commandsRouter.post('/', async (c) => {
const input = CreateCommandInputSchema.parse(body); const input = CreateCommandInputSchema.parse(body);
const config = getConfig(); const config = getConfig();
const manager = module.createCommandManager(config.workdir); const manager = createCommandManager(config.workdir);
const result = await manager.create(input); const result = await manager.create(input);
if (result.success) { if (result.success) {
@@ -482,9 +377,8 @@ commandsRouter.post('/', async (c) => {
*/ */
commandsRouter.put('/:name{.+}', async (c) => { commandsRouter.put('/:name{.+}', async (c) => {
const name = c.req.param('name'); const name = c.req.param('name');
const module = await initCommandModule();
if (!module) { if (!(await ensureRegistryInitialized())) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -499,7 +393,7 @@ commandsRouter.put('/:name{.+}', async (c) => {
const input = UpdateCommandInputSchema.parse(body); const input = UpdateCommandInputSchema.parse(body);
const config = getConfig(); const config = getConfig();
const manager = module.createCommandManager(config.workdir); const manager = createCommandManager(config.workdir);
const result = await manager.update(name, input); const result = await manager.update(name, input);
if (result.success) { if (result.success) {
@@ -535,9 +429,8 @@ commandsRouter.put('/:name{.+}', async (c) => {
*/ */
commandsRouter.delete('/:name{.+}', async (c) => { commandsRouter.delete('/:name{.+}', async (c) => {
const name = c.req.param('name'); const name = c.req.param('name');
const module = await initCommandModule();
if (!module) { if (!(await ensureRegistryInitialized())) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -548,7 +441,7 @@ commandsRouter.delete('/:name{.+}', async (c) => {
} }
const config = getConfig(); const config = getConfig();
const manager = module.createCommandManager(config.workdir); const manager = createCommandManager(config.workdir);
const result = await manager.delete(name); const result = await manager.delete(name);
if (result.success) { if (result.success) {
+18 -199
View File
@@ -9,38 +9,18 @@ import * as fs from 'fs/promises';
import * as path from 'path'; import * as path from 'path';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import { getConfig } from './config.js'; import { getConfig } from './config.js';
import type {
// Core Hooks 模块类型 HookConfig,
interface HooksModule { ProjectConfig,
loadProjectConfig: (directory: string) => Promise<ProjectConfig | null>; ShellCommandConfig,
loadHookConfig: (directory: string) => Promise<HookConfig | null>; FileHookConfig,
getConfigFilePath: (directory: string) => Promise<string | null>; } from '@ai-assistant/core';
createDefaultConfig: (directory: string) => Promise<void>; import {
} loadProjectConfig,
loadHookConfig,
interface ShellCommandConfig { getConfigFilePath,
command: string[]; createDefaultConfig,
environment?: Record<string, string>; } from '@ai-assistant/core';
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;
}
interface HookTestResult { interface HookTestResult {
success: boolean; success: boolean;
@@ -52,44 +32,6 @@ interface HookTestResult {
export const hooksRouter = new Hono(); export const hooksRouter = new Hono();
// Core 模块缓存
let hooksModule: HooksModule | null = null;
/**
* 初始化 Hooks 模块
*/
async function initHooksModule(): Promise<HooksModule | null> {
if (hooksModule) return hooksModule;
try {
const corePath = '@ai-assistant/core';
const core = (await import(corePath)) as Record<string, unknown>;
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 格式) * 移除 JSON 中的注释(支持 JSONC 格式)
*/ */
@@ -123,11 +65,8 @@ async function writeConfigFile(configPath: string, config: ProjectConfig): Promi
* 获取或创建配置文件路径 * 获取或创建配置文件路径
*/ */
async function getOrCreateConfigPath(workdir: string): Promise<string> { async function getOrCreateConfigPath(workdir: string): Promise<string> {
const module = await initHooksModule(); const existingPath = await getConfigFilePath(workdir);
if (module) {
const existingPath = await module.getConfigFilePath(workdir);
if (existingPath) return existingPath; if (existingPath) return existingPath;
}
return path.join(workdir, '.ai-assistant.json'); return path.join(workdir, '.ai-assistant.json');
} }
@@ -135,20 +74,8 @@ async function getOrCreateConfigPath(workdir: string): Promise<string> {
* GET /hooks/config - 获取完整钩子配置 * GET /hooks/config - 获取完整钩子配置
*/ */
hooksRouter.get('/config', async (c) => { 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 config = getConfig();
const hookConfig = await module.loadHookConfig(config.workdir); const hookConfig = await loadHookConfig(config.workdir);
return c.json({ return c.json({
success: true, success: true,
@@ -165,18 +92,6 @@ hooksRouter.get('/config', async (c) => {
* PUT /hooks/config - 更新完整钩子配置 * PUT /hooks/config - 更新完整钩子配置
*/ */
hooksRouter.put('/config', async (c) => { hooksRouter.put('/config', async (c) => {
const module = await initHooksModule();
if (!module) {
return c.json(
{
success: false,
error: 'Hooks module not available',
},
503
);
}
try { try {
const newHookConfig = await c.req.json<HookConfig>(); const newHookConfig = await c.req.json<HookConfig>();
const config = getConfig(); const config = getConfig();
@@ -213,20 +128,8 @@ hooksRouter.put('/config', async (c) => {
* GET /hooks/file-edited - 获取 file_edited 钩子 * GET /hooks/file-edited - 获取 file_edited 钩子
*/ */
hooksRouter.get('/file-edited', async (c) => { 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 config = getConfig();
const hookConfig = await module.loadHookConfig(config.workdir); const hookConfig = await loadHookConfig(config.workdir);
return c.json({ return c.json({
success: true, success: true,
@@ -238,18 +141,6 @@ hooksRouter.get('/file-edited', async (c) => {
* PUT /hooks/file-edited - 更新 file_edited 钩子 * PUT /hooks/file-edited - 更新 file_edited 钩子
*/ */
hooksRouter.put('/file-edited', async (c) => { hooksRouter.put('/file-edited', async (c) => {
const module = await initHooksModule();
if (!module) {
return c.json(
{
success: false,
error: 'Hooks module not available',
},
503
);
}
try { try {
const newFileEditedHooks = await c.req.json<FileHookConfig>(); const newFileEditedHooks = await c.req.json<FileHookConfig>();
const config = getConfig(); const config = getConfig();
@@ -285,20 +176,8 @@ hooksRouter.put('/file-edited', async (c) => {
* GET /hooks/file-created - 获取 file_created 钩子 * GET /hooks/file-created - 获取 file_created 钩子
*/ */
hooksRouter.get('/file-created', async (c) => { 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 config = getConfig();
const hookConfig = await module.loadHookConfig(config.workdir); const hookConfig = await loadHookConfig(config.workdir);
return c.json({ return c.json({
success: true, success: true,
@@ -310,18 +189,6 @@ hooksRouter.get('/file-created', async (c) => {
* PUT /hooks/file-created - 更新 file_created 钩子 * PUT /hooks/file-created - 更新 file_created 钩子
*/ */
hooksRouter.put('/file-created', async (c) => { hooksRouter.put('/file-created', async (c) => {
const module = await initHooksModule();
if (!module) {
return c.json(
{
success: false,
error: 'Hooks module not available',
},
503
);
}
try { try {
const newFileCreatedHooks = await c.req.json<FileHookConfig>(); const newFileCreatedHooks = await c.req.json<FileHookConfig>();
const config = getConfig(); const config = getConfig();
@@ -357,20 +224,8 @@ hooksRouter.put('/file-created', async (c) => {
* GET /hooks/file-deleted - 获取 file_deleted 钩子 * GET /hooks/file-deleted - 获取 file_deleted 钩子
*/ */
hooksRouter.get('/file-deleted', async (c) => { 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 config = getConfig();
const hookConfig = await module.loadHookConfig(config.workdir); const hookConfig = await loadHookConfig(config.workdir);
return c.json({ return c.json({
success: true, success: true,
@@ -382,18 +237,6 @@ hooksRouter.get('/file-deleted', async (c) => {
* PUT /hooks/file-deleted - 更新 file_deleted 钩子 * PUT /hooks/file-deleted - 更新 file_deleted 钩子
*/ */
hooksRouter.put('/file-deleted', async (c) => { hooksRouter.put('/file-deleted', async (c) => {
const module = await initHooksModule();
if (!module) {
return c.json(
{
success: false,
error: 'Hooks module not available',
},
503
);
}
try { try {
const newFileDeletedHooks = await c.req.json<FileHookConfig>(); const newFileDeletedHooks = await c.req.json<FileHookConfig>();
const config = getConfig(); const config = getConfig();
@@ -429,20 +272,8 @@ hooksRouter.put('/file-deleted', async (c) => {
* GET /hooks/session-completed - 获取 session_completed 钩子 * GET /hooks/session-completed - 获取 session_completed 钩子
*/ */
hooksRouter.get('/session-completed', async (c) => { 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 config = getConfig();
const hookConfig = await module.loadHookConfig(config.workdir); const hookConfig = await loadHookConfig(config.workdir);
return c.json({ return c.json({
success: true, success: true,
@@ -454,18 +285,6 @@ hooksRouter.get('/session-completed', async (c) => {
* PUT /hooks/session-completed - 更新 session_completed 钩子 * PUT /hooks/session-completed - 更新 session_completed 钩子
*/ */
hooksRouter.put('/session-completed', async (c) => { hooksRouter.put('/session-completed', async (c) => {
const module = await initHooksModule();
if (!module) {
return c.json(
{
success: false,
error: 'Hooks module not available',
},
503
);
}
try { try {
const newSessionCompletedHooks = await c.req.json<ShellCommandConfig[]>(); const newSessionCompletedHooks = await c.req.json<ShellCommandConfig[]>();
const config = getConfig(); const config = getConfig();
+49 -109
View File
@@ -6,100 +6,44 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import { getConfig } from './config.js'; import { getConfig } from './config.js';
import type {
// Core MCP 模块类型 MCPConfig,
interface MCPModule { MCPServerConfig,
getMCPManager: () => MCPManager; MCPServerStatus,
loadMCPConfig: (workdir: string) => Promise<MCPConfig>; MCPTool,
} } from '@ai-assistant/core';
import {
interface MCPManager { getMCPManager,
initialize(config: MCPConfig): Promise<void>; loadMCPConfig,
shutdown(): Promise<void>; } from '@ai-assistant/core';
reconnect(serverName: string): Promise<void>;
setServerEnabled(serverName: string, enabled: boolean): Promise<void>;
getServerStatuses(): MCPServerStatus[];
getServerStatus(name: string): MCPServerStatus | undefined;
getTools(): MCPTool[];
getTool(name: string): MCPTool | undefined;
isInitialized(): boolean;
}
interface MCPConfig {
mcp?: Record<string, MCPServerConfig>;
tools?: Record<string, boolean>;
}
interface MCPServerConfig {
type: 'local' | 'remote';
command?: string[];
url?: string;
env?: Record<string, string>;
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<string, unknown>;
}
export const mcpRouter = new Hono(); export const mcpRouter = new Hono();
// Core 模块缓存 // 配置缓存
let mcpModule: MCPModule | null = null;
let currentConfig: MCPConfig | null = null; let currentConfig: MCPConfig | null = null;
let managerInitialized = false;
/** /**
* 初始化 MCP 模块 * 初始化 MCP Manager
*/ */
async function initMCPModule(): Promise<MCPModule | null> { async function ensureMCPInitialized(): Promise<boolean> {
if (mcpModule) return mcpModule; if (managerInitialized) return true;
try { try {
const corePath = '@ai-assistant/core';
const core = (await import(corePath)) as Record<string, unknown>;
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<MCPConfig>,
};
// 初始化 MCP Manager
const config = getConfig(); const config = getConfig();
currentConfig = await mcpModule.loadMCPConfig(config.workdir); currentConfig = await loadMCPConfig(config.workdir);
const manager = mcpModule.getMCPManager(); const manager = getMCPManager();
if (!manager.isInitialized() && currentConfig.mcp) { if (!manager.isInitialized() && currentConfig.mcp) {
await manager.initialize(currentConfig); await manager.initialize(currentConfig);
} }
managerInitialized = true;
console.log('[MCP] MCP module initialized'); console.log('[MCP] MCP module initialized');
return mcpModule; return true;
} catch (error) { } catch (error) {
console.warn('[MCP] Failed to load MCP module:', error); console.warn('[MCP] Failed to initialize MCP module:', error);
return null; return false;
} }
} }
@@ -107,9 +51,7 @@ async function initMCPModule(): Promise<MCPModule | null> {
* GET /mcp/servers - 获取所有服务器状态 * GET /mcp/servers - 获取所有服务器状态
*/ */
mcpRouter.get('/servers', async (c) => { mcpRouter.get('/servers', async (c) => {
const module = await initMCPModule(); if (!(await ensureMCPInitialized())) {
if (!module) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -119,7 +61,7 @@ mcpRouter.get('/servers', async (c) => {
); );
} }
const manager = module.getMCPManager(); const manager = getMCPManager();
const statuses = manager.getServerStatuses(); const statuses = manager.getServerStatuses();
// 添加配置信息 // 添加配置信息
@@ -149,9 +91,8 @@ mcpRouter.get('/servers', async (c) => {
*/ */
mcpRouter.get('/servers/:name', async (c) => { mcpRouter.get('/servers/:name', async (c) => {
const name = c.req.param('name'); const name = c.req.param('name');
const module = await initMCPModule();
if (!module) { if (!(await ensureMCPInitialized())) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -161,7 +102,7 @@ mcpRouter.get('/servers/:name', async (c) => {
); );
} }
const manager = module.getMCPManager(); const manager = getMCPManager();
const status = manager.getServerStatus(name); const status = manager.getServerStatus(name);
if (!status) { if (!status) {
@@ -206,9 +147,8 @@ mcpRouter.get('/servers/:name', async (c) => {
*/ */
mcpRouter.post('/servers/:name/connect', async (c) => { mcpRouter.post('/servers/:name/connect', async (c) => {
const name = c.req.param('name'); const name = c.req.param('name');
const module = await initMCPModule();
if (!module) { if (!(await ensureMCPInitialized())) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -218,7 +158,7 @@ mcpRouter.post('/servers/:name/connect', async (c) => {
); );
} }
const manager = module.getMCPManager(); const manager = getMCPManager();
try { try {
await manager.reconnect(name); await manager.reconnect(name);
@@ -246,9 +186,8 @@ mcpRouter.post('/servers/:name/connect', async (c) => {
*/ */
mcpRouter.post('/servers/:name/disconnect', async (c) => { mcpRouter.post('/servers/:name/disconnect', async (c) => {
const name = c.req.param('name'); const name = c.req.param('name');
const module = await initMCPModule();
if (!module) { if (!(await ensureMCPInitialized())) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -258,7 +197,7 @@ mcpRouter.post('/servers/:name/disconnect', async (c) => {
); );
} }
const manager = module.getMCPManager(); const manager = getMCPManager();
try { try {
// 通过禁用来断开连接 // 通过禁用来断开连接
@@ -287,9 +226,8 @@ mcpRouter.post('/servers/:name/disconnect', async (c) => {
*/ */
mcpRouter.post('/servers/:name/enable', async (c) => { mcpRouter.post('/servers/:name/enable', async (c) => {
const name = c.req.param('name'); const name = c.req.param('name');
const module = await initMCPModule();
if (!module) { if (!(await ensureMCPInitialized())) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -299,7 +237,7 @@ mcpRouter.post('/servers/:name/enable', async (c) => {
); );
} }
const manager = module.getMCPManager(); const manager = getMCPManager();
try { try {
await manager.setServerEnabled(name, true); await manager.setServerEnabled(name, true);
@@ -327,9 +265,8 @@ mcpRouter.post('/servers/:name/enable', async (c) => {
*/ */
mcpRouter.post('/servers/:name/disable', async (c) => { mcpRouter.post('/servers/:name/disable', async (c) => {
const name = c.req.param('name'); const name = c.req.param('name');
const module = await initMCPModule();
if (!module) { if (!(await ensureMCPInitialized())) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -339,7 +276,7 @@ mcpRouter.post('/servers/:name/disable', async (c) => {
); );
} }
const manager = module.getMCPManager(); const manager = getMCPManager();
try { try {
await manager.setServerEnabled(name, false); await manager.setServerEnabled(name, false);
@@ -366,9 +303,7 @@ mcpRouter.post('/servers/:name/disable', async (c) => {
* GET /mcp/tools - 获取所有 MCP 工具 * GET /mcp/tools - 获取所有 MCP 工具
*/ */
mcpRouter.get('/tools', async (c) => { mcpRouter.get('/tools', async (c) => {
const module = await initMCPModule(); if (!(await ensureMCPInitialized())) {
if (!module) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -378,7 +313,7 @@ mcpRouter.get('/tools', async (c) => {
); );
} }
const manager = module.getMCPManager(); const manager = getMCPManager();
const tools = manager.getTools(); const tools = manager.getTools();
return c.json({ return c.json({
@@ -398,9 +333,8 @@ mcpRouter.get('/tools', async (c) => {
*/ */
mcpRouter.get('/tools/:name', async (c) => { mcpRouter.get('/tools/:name', async (c) => {
const name = c.req.param('name'); const name = c.req.param('name');
const module = await initMCPModule();
if (!module) { if (!(await ensureMCPInitialized())) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -410,7 +344,7 @@ mcpRouter.get('/tools/:name', async (c) => {
); );
} }
const manager = module.getMCPManager(); const manager = getMCPManager();
const tool = manager.getTool(name); const tool = manager.getTool(name);
if (!tool) { if (!tool) {
@@ -439,9 +373,7 @@ mcpRouter.get('/tools/:name', async (c) => {
* GET /mcp/config - 获取 MCP 配置 * GET /mcp/config - 获取 MCP 配置
*/ */
mcpRouter.get('/config', async (c) => { mcpRouter.get('/config', async (c) => {
const module = await initMCPModule(); if (!(await ensureMCPInitialized())) {
if (!module) {
return c.json( return c.json(
{ {
success: false, success: false,
@@ -459,13 +391,21 @@ mcpRouter.get('/config', async (c) => {
if (currentConfig?.mcp) { if (currentConfig?.mcp) {
for (const [name, config] of Object.entries(currentConfig.mcp)) { for (const [name, config] of Object.entries(currentConfig.mcp)) {
if (config.type === 'local') {
safeConfig.mcp![name] = { safeConfig.mcp![name] = {
type: config.type, type: 'local',
command: config.type === 'local' ? config.command : undefined, command: config.command,
url: config.type === 'remote' ? config.url : undefined,
enabled: config.enabled, enabled: config.enabled,
timeout: config.timeout, timeout: config.timeout,
}; };
} else {
safeConfig.mcp![name] = {
type: 'remote',
url: config.url,
enabled: config.enabled,
timeout: config.timeout,
};
}
} }
} }
+18 -137
View File
@@ -6,103 +6,24 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import { getConfig } from './config.js'; import { getConfig } from './config.js';
import type {
// Types from core - dynamically import to avoid build dependency ProviderListItem,
interface ProviderListItem { ModelInfo,
id: string; ProviderDetail,
name: string; CustomProviderDefinition,
description?: string; ProviderConfig,
builtin: boolean; ConnectionTestResult,
enabled: boolean; } from '@ai-assistant/core';
hasApiKey: boolean; import { getProviderRegistry } from '@ai-assistant/core';
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;
}
export const providersRouter = new Hono(); 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 * GET /providers - List all providers
*/ */
providersRouter.get('/', async (c) => { providersRouter.get('/', async (c) => {
const core = await getCoreModule();
if (!core) {
return c.json({ success: false, error: 'Core module not available' }, 503);
}
try { try {
const registry = core.getProviderRegistry(); const registry = getProviderRegistry();
const providers: ProviderListItem[] = registry.listForApi(); const providers: ProviderListItem[] = registry.listForApi();
return c.json({ return c.json({
@@ -124,15 +45,10 @@ providersRouter.get('/', async (c) => {
* GET /providers/:id - Get provider detail * GET /providers/:id - Get provider detail
*/ */
providersRouter.get('/:id', async (c) => { 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'); const id = c.req.param('id');
try { try {
const registry = core.getProviderRegistry(); const registry = getProviderRegistry();
const detail: ProviderDetail | undefined = registry.getDetail(id); const detail: ProviderDetail | undefined = registry.getDetail(id);
if (!detail) { if (!detail) {
@@ -158,15 +74,10 @@ providersRouter.get('/:id', async (c) => {
* GET /providers/:id/models - Get provider's model list * GET /providers/:id/models - Get provider's model list
*/ */
providersRouter.get('/:id/models', async (c) => { 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'); const id = c.req.param('id');
try { try {
const registry = core.getProviderRegistry(); const registry = getProviderRegistry();
if (!registry.has(id)) { if (!registry.has(id)) {
return c.json({ success: false, error: `Provider not found: ${id}` }, 404); 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 * POST /providers/:id/test - Test provider connection
*/ */
providersRouter.post('/:id/test', async (c) => { 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'); const id = c.req.param('id');
try { try {
const body = await c.req.json().catch(() => ({})); const body = await c.req.json().catch(() => ({}));
const apiKey = body.apiKey as string | undefined; const apiKey = body.apiKey as string | undefined;
const registry = core.getProviderRegistry(); const registry = getProviderRegistry();
const result: ConnectionTestResult = await registry.testConnection(id, apiKey); const result: ConnectionTestResult = await registry.testConnection(id, apiKey);
return c.json({ return c.json({
@@ -226,11 +132,6 @@ providersRouter.post('/:id/test', async (c) => {
* POST /providers - Register custom provider * POST /providers - Register custom provider
*/ */
providersRouter.post('/', async (c) => { providersRouter.post('/', async (c) => {
const core = await getCoreModule();
if (!core) {
return c.json({ success: false, error: 'Core module not available' }, 503);
}
try { try {
const body: CustomProviderDefinition = await c.req.json(); 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); registry.registerCustom(body);
await registry.saveConfig(); await registry.saveConfig();
@@ -268,17 +169,12 @@ providersRouter.post('/', async (c) => {
* PUT /providers/:id - Update provider config * PUT /providers/:id - Update provider config
*/ */
providersRouter.put('/:id', async (c) => { 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'); const id = c.req.param('id');
try { try {
const body: ProviderConfig = await c.req.json(); const body: ProviderConfig = await c.req.json();
const registry = core.getProviderRegistry(); const registry = getProviderRegistry();
if (!registry.has(id)) { if (!registry.has(id)) {
return c.json({ success: false, error: `Provider not found: ${id}` }, 404); 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 * DELETE /providers/:id - Delete custom provider
*/ */
providersRouter.delete('/:id', async (c) => { 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'); const id = c.req.param('id');
try { try {
const registry = core.getProviderRegistry(); const registry = getProviderRegistry();
const removed = registry.removeCustom(id); const removed = registry.removeCustom(id);
if (!removed) { if (!removed) {
@@ -342,11 +233,6 @@ providersRouter.delete('/:id', async (c) => {
* POST /providers/:id/models - Add custom model * POST /providers/:id/models - Add custom model
*/ */
providersRouter.post('/:id/models', async (c) => { 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'); const providerId = c.req.param('id');
try { try {
@@ -363,7 +249,7 @@ providersRouter.post('/:id/models', async (c) => {
); );
} }
const registry = core.getProviderRegistry(); const registry = getProviderRegistry();
registry.addCustomModel(providerId, body); registry.addCustomModel(providerId, body);
await registry.saveConfig(); await registry.saveConfig();
@@ -386,16 +272,11 @@ providersRouter.post('/:id/models', async (c) => {
* DELETE /providers/:id/models/:modelId - Delete custom model * DELETE /providers/:id/models/:modelId - Delete custom model
*/ */
providersRouter.delete('/:id/models/:modelId', async (c) => { 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 providerId = c.req.param('id');
const modelId = c.req.param('modelId'); const modelId = c.req.param('modelId');
try { try {
const registry = core.getProviderRegistry(); const registry = getProviderRegistry();
const removed = registry.removeCustomModel(providerId, modelId); const removed = registry.removeCustomModel(providerId, modelId);
if (!removed) { if (!removed) {
+15 -47
View File
@@ -12,6 +12,8 @@ import {
type MergedMessage, type MergedMessage,
type MessagePart, type MessagePart,
} from '../types.js'; } from '../types.js';
import type { MessageInfo, Part, ToolPart } from '@ai-assistant/core';
import { MessageStorage, PartStorage } from '@ai-assistant/core';
export const sessionsRouter = new Hono(); export const sessionsRouter = new Hono();
@@ -125,41 +127,6 @@ sessionsRouter.get('/:id/messages', async (c) => {
} }
try { 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<string, unknown>;
output?: unknown;
error?: string;
time?: { start: number; end?: number };
};
};
const { MessageStorage, PartStorage } = (await import(/* webpackIgnore: true */ corePath)) as {
MessageStorage: {
listBySession(sessionId: string): Promise<MessageInfo[]>;
};
PartStorage: {
getByIds(messageId: string, partIds: string[]): Promise<Part[]>;
};
};
// 获取消息列表(按创建时间排序) // 获取消息列表(按创建时间排序)
const messageInfos = await MessageStorage.listBySession(id); const messageInfos = await MessageStorage.listBySession(id);
@@ -178,17 +145,18 @@ sessionsRouter.get('/:id/messages', async (c) => {
if (p.type === 'reasoning') { if (p.type === 'reasoning') {
return { type: 'reasoning', id: p.id, text: p.text ?? '' }; return { type: 'reasoning', id: p.id, text: p.text ?? '' };
} }
// tool // tool - 使用类型断言
const state = p.state!; const toolPart = p as ToolPart;
const startTime = state.time?.start; const state = toolPart.state;
const endTime = state.time?.end; const startTime = state.status !== 'pending' ? state.time?.start : undefined;
const endTime = state.status === 'completed' || state.status === 'error' ? state.time?.end : undefined;
return { return {
type: 'tool', type: 'tool',
id: p.id, id: p.id,
toolCallId: p.toolCallId ?? '', toolCallId: toolPart.toolCallId ?? '',
toolName: p.toolName ?? '', toolName: toolPart.toolName ?? '',
status: state.status, status: state.status,
arguments: state.input ?? {}, arguments: state.status !== 'pending' ? (state.input as Record<string, unknown>) : {},
result: state.status === 'completed' ? state.output : undefined, result: state.status === 'completed' ? state.output : undefined,
error: state.status === 'error' ? state.error : undefined, error: state.status === 'error' ? state.error : undefined,
duration: startTime && endTime ? endTime - startTime : undefined, duration: startTime && endTime ? endTime - startTime : undefined,
@@ -203,15 +171,15 @@ sessionsRouter.get('/:id/messages', async (c) => {
// 兼容字段:提取工具调用 // 兼容字段:提取工具调用
const toolCalls: ToolCallInfo[] = parts const toolCalls: ToolCallInfo[] = parts
.filter((p) => p.type === 'tool' && p.state) .filter((p): p is ToolPart => p.type === 'tool')
.map((p) => { .map((p) => {
const state = p.state!; const state = p.state;
const startTime = state.time?.start; const startTime = state.status !== 'pending' ? state.time?.start : undefined;
const endTime = state.time?.end; const endTime = state.status === 'completed' || state.status === 'error' ? state.time?.end : undefined;
return { return {
id: p.toolCallId ?? '', id: p.toolCallId ?? '',
name: p.toolName ?? '', name: p.toolName ?? '',
arguments: state.input ?? {}, arguments: state.status !== 'pending' ? (state.input as Record<string, unknown>) : {},
status: state.status, status: state.status,
result: state.status === 'completed' ? state.output : undefined, result: state.status === 'completed' ? state.output : undefined,
error: state.status === 'error' ? state.error : undefined, error: state.status === 'error' ? state.error : undefined,
+9 -59
View File
@@ -6,68 +6,24 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import type { Session, CreateSessionInput, SessionStatus } from '../types.js'; import type { Session, CreateSessionInput, SessionStatus } from '../types.js';
import type {
// ============================================================================ SessionData,
// Core 模块接口定义(避免构建时依赖) SessionSummary,
// ============================================================================ ProjectMetadata,
} from '@ai-assistant/core';
interface SessionSummary { import { SessionManager as CoreSessionManager } from '@ai-assistant/core';
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<SessionData>;
getSession(): SessionData | null;
getProject(): ProjectMetadata | null;
listSessions(): Promise<SessionSummary[]>;
listAllSessions(): Promise<SessionSummary[]>;
deleteSession(sessionId: string): Promise<boolean>;
newSession(workdir?: string): Promise<SessionData>;
restoreSession(sessionId: string): Promise<SessionData | null>;
getSessionId(): string | undefined;
}
// ============================================================================
// SessionManager 类
// ============================================================================
export class SessionManager { export class SessionManager {
private sessions: Map<string, Session> = new Map(); private sessions: Map<string, Session> = new Map();
private sessionProjects: Map<string, string> = new Map(); // sessionId -> projectId private sessionProjects: Map<string, string> = new Map(); // sessionId -> projectId
private coreManager: SessionManagerInterface | null = null; private coreManager: CoreSessionManager | null = null;
private currentProject: ProjectMetadata | null = null; private currentProject: ProjectMetadata | null = null;
private initialized = false; private initialized = false;
/** /**
* 获取 Core SessionManager 实例(供外部使用) * 获取 Core SessionManager 实例(供外部使用)
*/ */
getCoreManager(): SessionManagerInterface | null { getCoreManager(): CoreSessionManager | null {
return this.coreManager; return this.coreManager;
} }
@@ -85,13 +41,7 @@ export class SessionManager {
if (this.initialized) return; if (this.initialized) return;
try { try {
// 动态导入 Core 模块,避免构建时依赖 this.coreManager = new CoreSessionManager();
const corePath = '@ai-assistant/core';
const core = (await import(/* webpackIgnore: true */ corePath)) as {
SessionManager: new (storageDir?: string) => SessionManagerInterface;
};
this.coreManager = new core.SessionManager();
await this.coreManager.init(process.cwd()); await this.coreManager.init(process.cwd());
this.currentProject = this.coreManager.getProject(); this.currentProject = this.coreManager.getProject();
@@ -34,15 +34,7 @@ const mockAgentModule = vi.hoisted(() => ({
isPresetAgent: vi.fn((name: string) => name in mockPresetAgents), isPresetAgent: vi.fn((name: string) => name in mockPresetAgents),
})); }));
// Track if module should be available vi.mock('@ai-assistant/core', () => mockAgentModule);
let moduleAvailable = true;
vi.mock('@ai-assistant/core', () => {
if (!moduleAvailable) {
throw new Error('Module not found');
}
return mockAgentModule;
});
vi.mock('../../../src/routes/config.js', () => ({ vi.mock('../../../src/routes/config.js', () => ({
getConfig: vi.fn(() => ({ workdir: '/test/workdir' })), getConfig: vi.fn(() => ({ workdir: '/test/workdir' })),
@@ -57,7 +49,6 @@ app.route('/agents', agentsRouter);
describe('Agents Route', () => { describe('Agents Route', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
moduleAvailable = true;
mockAgentModule.loadAgentConfig.mockResolvedValue(null); mockAgentModule.loadAgentConfig.mockResolvedValue(null);
mockAgentModule.saveAgentConfig.mockResolvedValue(undefined); mockAgentModule.saveAgentConfig.mockResolvedValue(undefined);
}); });
@@ -35,15 +35,7 @@ const mockCommandModule = vi.hoisted(() => ({
createCommandManager: vi.fn(() => mockCommandManager), createCommandManager: vi.fn(() => mockCommandManager),
})); }));
// Track if module should be available vi.mock('@ai-assistant/core', () => mockCommandModule);
let moduleAvailable = true;
vi.mock('@ai-assistant/core', () => {
if (!moduleAvailable) {
throw new Error('Module not found');
}
return mockCommandModule;
});
vi.mock('../../../src/routes/config.js', () => ({ vi.mock('../../../src/routes/config.js', () => ({
getConfig: vi.fn(() => ({ workdir: '/test/workdir' })), getConfig: vi.fn(() => ({ workdir: '/test/workdir' })),
@@ -58,7 +50,6 @@ app.route('/commands', commandsRouter);
describe('Commands Route', () => { describe('Commands Route', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
moduleAvailable = true;
mockCommandRegistry.list.mockReturnValue([]); mockCommandRegistry.list.mockReturnValue([]);
mockCommandRegistry.getStats.mockReturnValue({ total: 0, bySource: {} }); mockCommandRegistry.getStats.mockReturnValue({ total: 0, bySource: {} });
}); });
@@ -15,15 +15,7 @@ const mockHooksModule = vi.hoisted(() => ({
createDefaultConfig: vi.fn(), createDefaultConfig: vi.fn(),
})); }));
// Track if module should be available vi.mock('@ai-assistant/core', () => mockHooksModule);
let moduleAvailable = true;
vi.mock('@ai-assistant/core', () => {
if (!moduleAvailable) {
throw new Error('Module not found');
}
return mockHooksModule;
});
vi.mock('../../../src/routes/config.js', () => ({ vi.mock('../../../src/routes/config.js', () => ({
getConfig: vi.fn(() => ({ workdir: '/test/workdir' })), getConfig: vi.fn(() => ({ workdir: '/test/workdir' })),
@@ -44,7 +36,6 @@ app.route('/hooks', hooksRouter);
describe('Hooks Route', () => { describe('Hooks Route', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
moduleAvailable = true;
mockHooksModule.loadHookConfig.mockResolvedValue(null); mockHooksModule.loadHookConfig.mockResolvedValue(null);
mockHooksModule.getConfigFilePath.mockResolvedValue('/test/workdir/.ai-assistant.json'); mockHooksModule.getConfigFilePath.mockResolvedValue('/test/workdir/.ai-assistant.json');
}); });
@@ -26,15 +26,7 @@ const mockCoreModule = vi.hoisted(() => ({
getProviderRegistry: vi.fn(() => mockRegistry), getProviderRegistry: vi.fn(() => mockRegistry),
})); }));
// Track if core module should be available vi.mock('@ai-assistant/core', () => mockCoreModule);
let coreModuleAvailable = true;
vi.mock('@ai-assistant/core', () => {
if (!coreModuleAvailable) {
throw new Error('Module not found');
}
return mockCoreModule;
});
import { providersRouter } from '../../../src/routes/providers.js'; import { providersRouter } from '../../../src/routes/providers.js';
@@ -45,7 +37,6 @@ app.route('/providers', providersRouter);
describe('Providers Route', () => { describe('Providers Route', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
coreModuleAvailable = true;
}); });
describe('GET /providers - 列出所有提供商', () => { describe('GET /providers - 列出所有提供商', () => {