feat(commands): 实现命令 CRUD 完整功能

Core:
- 新增 CommandManager 类,支持创建、更新、删除命令
- 验证命令名称防止路径遍历攻击
- 自动生成 Markdown 文件(含 YAML frontmatter)
- 内置命令保护(不可修改/删除)

Server:
- POST /api/commands - 创建命令
- GET /api/commands/:name/content - 获取命令完整内容
- PUT /api/commands/:name - 更新命令
- DELETE /api/commands/:name - 删除命令

UI:
- 新增 createCommand、updateCommand、deleteCommand、getCommandContent 函数
- 新增 CreateCommandInput、UpdateCommandInput、CommandContent 类型
This commit is contained in:
2025-12-12 18:51:38 +08:00
parent db711648e0
commit f0385ef221
7 changed files with 780 additions and 3 deletions
+246 -1
View File
@@ -20,10 +20,30 @@ const SearchCommandInputSchema = z.object({
export const commandsRouter = new Hono();
// Zod schemas for CRUD
const CreateCommandInputSchema = z.object({
name: z.string().min(1),
description: z.string().optional(),
template: z.string().min(1),
agent: z.string().optional(),
model: z.string().optional(),
subtask: z.boolean().optional(),
scope: z.enum(['user', 'project']),
});
const UpdateCommandInputSchema = z.object({
description: z.string().optional(),
template: z.string().optional(),
agent: z.string().optional(),
model: z.string().optional(),
subtask: z.boolean().optional(),
});
// Core 模块类型
interface CommandModule {
getCommandRegistry: () => CommandRegistry;
createCommandExecutor: (workdir: string) => CommandExecutor;
createCommandManager: (workdir: string) => CommandManager;
}
interface CommandRegistry {
@@ -67,6 +87,43 @@ interface CommandExecutionResult {
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;
@@ -82,7 +139,8 @@ async function initCommandModule(): Promise<CommandModule | null> {
if (
typeof core.getCommandRegistry !== 'function' ||
typeof core.createCommandExecutor !== 'function'
typeof core.createCommandExecutor !== 'function' ||
typeof core.createCommandManager !== 'function'
) {
console.warn('[Commands] Core module missing command exports');
return null;
@@ -91,6 +149,7 @@ async function initCommandModule(): Promise<CommandModule | 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
@@ -321,3 +380,189 @@ commandsRouter.post('/:name{.+}/execute', async (c) => {
);
}
});
// ============================================================================
// CRUD 操作
// ============================================================================
/**
* POST /commands - 创建命令
*/
commandsRouter.post('/', 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 = CreateCommandInputSchema.parse(body);
const config = getConfig();
const manager = module.createCommandManager(config.workdir);
const result = await manager.create(input);
if (result.success) {
return c.json({
success: true,
data: {
name: input.name,
path: result.path,
},
});
} else {
return c.json(
{
success: false,
error: result.error,
},
400
);
}
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Invalid input',
},
400
);
}
});
/**
* GET /commands/:name/content - 获取命令完整内容(包含 template)
*/
commandsRouter.get('/:name{.+}/content', 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 config = getConfig();
const manager = module.createCommandManager(config.workdir);
const result = await manager.getContent(name);
if (result.success) {
return c.json({
success: true,
data: result.data,
});
} else {
return c.json(
{
success: false,
error: result.error,
},
404
);
}
});
/**
* PUT /commands/:name - 更新命令
*/
commandsRouter.put('/: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
);
}
try {
const body = await c.req.json();
const input = UpdateCommandInputSchema.parse(body);
const config = getConfig();
const manager = module.createCommandManager(config.workdir);
const result = await manager.update(name, input);
if (result.success) {
return c.json({
success: true,
data: {
name,
path: result.path,
},
});
} else {
return c.json(
{
success: false,
error: result.error,
},
400
);
}
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Invalid input',
},
400
);
}
});
/**
* DELETE /commands/:name - 删除命令
*/
commandsRouter.delete('/: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 config = getConfig();
const manager = module.createCommandManager(config.workdir);
const result = await manager.delete(name);
if (result.success) {
return c.json({
success: true,
data: {
name,
path: result.path,
},
});
} else {
return c.json(
{
success: false,
error: result.error,
},
400
);
}
});