feat(server): 添加统一命令系统 API

- 新增 /api/commands 路由,支持列出、查询、执行和搜索命令
- Server 通过动态导入 Core 的 CommandRegistry 和 CommandExecutor
- CLI、Web、Desktop 客户端均可通过 REST API 访问斜杠命令
- 支持三层命令优先级: project > user > builtin
This commit is contained in:
2025-12-12 18:24:04 +08:00
parent ada9f890c6
commit 61735317a0
7 changed files with 508 additions and 1 deletions
+2 -1
View File
@@ -9,7 +9,7 @@ import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { createBunWebSocket } from 'hono/bun';
import { sessionsRouter, toolsRouter, configRouter, filesRouter } from './routes/index.js';
import { sessionsRouter, toolsRouter, configRouter, filesRouter, commandsRouter } from './routes/index.js';
import {
handleWebSocket,
handleWebSocketMessage,
@@ -82,6 +82,7 @@ api.route('/sessions', sessionsRouter);
api.route('/tools', toolsRouter);
api.route('/config', configRouter);
api.route('/files', filesRouter);
api.route('/commands', commandsRouter);
// SSE 事件流
api.get('/sessions/:id/events', handleSSE);
+323
View File
@@ -0,0 +1,323 @@
/**
* Commands API Routes
*
* 提供斜杠命令的 REST API,支持列出、查询、执行和重载命令
*/
import { Hono } from 'hono';
import { z } from 'zod';
import { getConfig } from './config.js';
// Zod schemas
const ExecuteCommandInputSchema = z.object({
arguments: z.string().default(''),
});
const SearchCommandInputSchema = z.object({
query: z.string().min(1),
limit: z.number().int().min(1).max(50).default(10),
});
export const commandsRouter = new Hono();
// Core 模块类型
interface CommandModule {
getCommandRegistry: () => CommandRegistry;
createCommandExecutor: (workdir: string) => CommandExecutor;
}
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;
}
// Core 模块缓存
let commandModule: CommandModule | null = null;
/**
* 初始化 Command 模块
*/
async function initCommandModule(): Promise<CommandModule | null> {
if (commandModule) return commandModule;
try {
const corePath = '@ai-assistant/core';
const core = (await import(corePath)) as Record<string, unknown>;
if (
typeof core.getCommandRegistry !== 'function' ||
typeof core.createCommandExecutor !== 'function'
) {
console.warn('[Commands] Core module missing command exports');
return null;
}
commandModule = {
getCommandRegistry: core.getCommandRegistry as () => CommandRegistry,
createCommandExecutor: core.createCommandExecutor as (workdir: string) => CommandExecutor,
};
// Initialize registry with server workdir
const config = getConfig();
const registry = commandModule.getCommandRegistry();
await registry.initialize(config.workdir);
console.log('[Commands] Command module initialized');
return commandModule;
} catch (error) {
console.warn('[Commands] Failed to load core module:', error);
return null;
}
}
/**
* GET /commands - 列出所有命令
*/
commandsRouter.get('/', async (c) => {
const module = await initCommandModule();
if (!module) {
return c.json(
{
success: false,
error: 'Command module not available',
},
503
);
}
const registry = module.getCommandRegistry();
return c.json({
success: true,
data: {
commands: registry.list(),
stats: registry.getStats(),
},
});
});
/**
* POST /commands/search - 搜索命令
* 注意:这个路由必须在 /:name 之前定义,否则会被匹配为命令名
*/
commandsRouter.post('/search', async (c) => {
const module = await initCommandModule();
if (!module) {
return c.json(
{
success: false,
error: 'Command module not available',
},
503
);
}
try {
const body = await c.req.json();
const input = SearchCommandInputSchema.parse(body);
const registry = module.getCommandRegistry();
const results = registry.search(input.query, input.limit);
return c.json({
success: true,
data: results.map(({ command, score }) => ({
name: command.name,
description: command.description,
source: command.source,
score,
})),
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Invalid input',
},
400
);
}
});
/**
* POST /commands/reload - 重新加载命令
*/
commandsRouter.post('/reload', async (c) => {
const module = await initCommandModule();
if (!module) {
return c.json(
{
success: false,
error: 'Command module not available',
},
503
);
}
try {
const config = getConfig();
const registry = module.getCommandRegistry();
await registry.reload(config.workdir);
return c.json({
success: true,
data: {
message: 'Commands reloaded',
stats: registry.getStats(),
},
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Reload failed',
},
500
);
}
});
/**
* GET /commands/:name - 获取命令详情
* 使用 {.+} 匹配包含 / 的命令名(如 deploy/staging
*/
commandsRouter.get('/:name{.+}', async (c) => {
const name = c.req.param('name');
const module = await initCommandModule();
if (!module) {
return c.json(
{
success: false,
error: 'Command module not available',
},
503
);
}
const registry = module.getCommandRegistry();
const command = registry.get(name);
if (!command) {
return c.json(
{
success: false,
error: `Command not found: ${name}`,
},
404
);
}
// Return sanitized command info (exclude template for security)
return c.json({
success: true,
data: {
name: command.name,
description: command.description,
agent: command.agent,
model: command.model,
subtask: command.subtask,
source: command.source,
hasTemplate: !!command.template,
},
});
});
/**
* POST /commands/:name/execute - 执行命令(渲染模板)
*/
commandsRouter.post('/:name{.+}/execute', async (c) => {
const name = c.req.param('name');
const module = await initCommandModule();
if (!module) {
return c.json(
{
success: false,
error: 'Command module not available',
},
503
);
}
try {
const body = await c.req.json().catch(() => ({}));
const input = ExecuteCommandInputSchema.parse(body);
const config = getConfig();
const executor = module.createCommandExecutor(config.workdir);
// Parse arguments
const args = input.arguments ? input.arguments.split(/\s+/).filter(Boolean) : [];
const result = await executor.execute({
command: name,
arguments: input.arguments,
args,
workdir: config.workdir,
});
return c.json(
{
success: result.success,
data: result.success
? {
prompt: result.prompt,
agent: result.agent,
model: result.model,
subtask: result.subtask,
}
: undefined,
error: result.error,
},
result.success ? 200 : 400
);
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Invalid input',
},
400
);
}
});
+1
View File
@@ -8,3 +8,4 @@ export { sessionsRouter } from './sessions.js';
export { toolsRouter, registerTool, getRegisteredTools } from './tools.js';
export { configRouter, getConfig, setConfig } from './config.js';
export { filesRouter, setWorkingDirectory, getWorkingDirectory } from './files.js';
export { commandsRouter } from './commands.js';
+38
View File
@@ -141,3 +141,41 @@ export interface PaginatedResponse<T> {
pageSize: number;
hasMore: boolean;
}
// ============ Command 相关 ============
export interface CommandInfo {
name: string;
description?: string;
agent?: string;
model?: string;
subtask?: boolean;
source: 'builtin' | 'user' | 'project';
hasTemplate: boolean;
}
export interface CommandSearchResult {
name: string;
description?: string;
source: string;
score: number;
}
export interface CommandExecuteResult {
prompt: string;
agent?: string;
model?: string;
subtask?: boolean;
}
export interface CommandListResponse {
commands: Array<{
name: string;
description?: string;
source: string;
}>;
stats: {
total: number;
bySource: Record<string, number>;
};
}