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:
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 - 列出所有提供商', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user