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
+43 -244
View File
@@ -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<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;
}
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<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 已初始化
*/
async function ensureRegistryInitialized(): Promise<boolean> {
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<string>): AgentListItem {
const isPreset = module.isPresetAgent(agent.name);
function toListItem(agent: AgentInfo, customAgentNames: Set<string>): 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<AgentDefaults>();
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<AgentInfo, 'name'>>();
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<Omit<AgentInfo, 'name'>>();
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;
+66 -236
View File
@@ -6,157 +6,26 @@
import { Hono } from 'hono';
import { getConfig } from './config.js';
// Core Checkpoint 模块类型
interface CheckpointModule {
getCheckpointManager: () => CheckpointManager;
initCheckpointManager: (
workDir: string,
config?: Partial<CheckpointConfig>
) => Promise<CheckpointManager>;
RestoreMode: typeof RestoreMode;
}
interface CheckpointManager {
initialize(): Promise<void>;
isEnabled(): boolean;
getConfig(): CheckpointConfig;
listCheckpoints(): Promise<CheckpointMetadata[]>;
getCheckpoint(idOrHash: string): Promise<CheckpointMetadata | null>;
getLatestCheckpoint(): Promise<CheckpointMetadata | null>;
createCheckpoint(options: {
name?: string;
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;
}
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<CheckpointModule | null> {
if (checkpointModule && managerInitialized) return checkpointModule;
async function ensureCheckpointManager(): Promise<CheckpointManager | null> {
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 = {
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();
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<CheckpointModule | null> {
* 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({
+39 -146
View File
@@ -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<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;
// Registry 初始化状态
let registryInitialized = false;
/**
* 初始化 Command 模块
* 确保 CommandRegistry 已初始化
*/
async function initCommandModule(): Promise<CommandModule | null> {
if (commandModule) return commandModule;
async function ensureRegistryInitialized(): Promise<boolean> {
if (registryInitialized) return true;
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 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<CommandModule | null> {
* 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) {
+19 -200
View File
@@ -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<ProjectConfig | null>;
loadHookConfig: (directory: string) => Promise<HookConfig | null>;
getConfigFilePath: (directory: string) => Promise<string | null>;
createDefaultConfig: (directory: string) => Promise<void>;
}
interface ShellCommandConfig {
command: string[];
environment?: Record<string, string>;
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<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 格式)
*/
@@ -123,11 +65,8 @@ async function writeConfigFile(configPath: string, config: ProjectConfig): Promi
* 获取或创建配置文件路径
*/
async function getOrCreateConfigPath(workdir: string): Promise<string> {
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<string> {
* 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<HookConfig>();
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<FileHookConfig>();
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<FileHookConfig>();
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<FileHookConfig>();
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<ShellCommandConfig[]>();
const config = getConfig();
+53 -113
View File
@@ -6,100 +6,44 @@
import { Hono } from 'hono';
import { getConfig } from './config.js';
// Core MCP 模块类型
interface MCPModule {
getMCPManager: () => MCPManager;
loadMCPConfig: (workdir: string) => Promise<MCPConfig>;
}
interface MCPManager {
initialize(config: MCPConfig): Promise<void>;
shutdown(): Promise<void>;
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>;
}
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<MCPModule | null> {
if (mcpModule) return mcpModule;
async function ensureMCPInitialized(): Promise<boolean> {
if (managerInitialized) return true;
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();
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<MCPModule | null> {
* 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,
};
}
}
}
+18 -137
View File
@@ -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) {
+15 -47
View File
@@ -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<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);
@@ -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<string, unknown>) : {},
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<string, unknown>) : {},
status: state.status,
result: state.status === 'completed' ? state.output : undefined,
error: state.status === 'error' ? state.error : undefined,