fix(commands): 修复路由顺序导致 /content 端点被遮蔽的问题
- 将 /:name{.+}/execute 和 /:name{.+}/content 路由移到 /:name{.+} 之前
- Hono 的 {.+} 模式会贪婪匹配,导致 /test/content 被解析为 name='test/content'
- 更新测试用例以正确测试 /content 端点功能
This commit is contained in:
@@ -275,54 +275,9 @@ commandsRouter.post('/reload', async (c) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 - 执行命令(渲染模板)
|
* POST /commands/:name/execute - 执行命令(渲染模板)
|
||||||
|
* 注意:带后缀的路由必须在 /:name{.+} 之前定义,否则会被 {.+} 匹配
|
||||||
*/
|
*/
|
||||||
commandsRouter.post('/:name{.+}/execute', async (c) => {
|
commandsRouter.post('/:name{.+}/execute', async (c) => {
|
||||||
const name = c.req.param('name');
|
const name = c.req.param('name');
|
||||||
@@ -381,6 +336,91 @@ commandsRouter.post('/:name{.+}/execute', async (c) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /commands/:name/content - 获取命令完整内容(包含 template)
|
||||||
|
* 注意:带后缀的路由必须在 /:name{.+} 之前定义,否则会被 {.+} 匹配
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// CRUD 操作
|
// CRUD 操作
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -437,43 +477,6 @@ commandsRouter.post('/', async (c) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 - 更新命令
|
* PUT /commands/:name - 更新命令
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -305,18 +305,16 @@ describe('Commands Route', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /commands/:name/content - 获取命令完整内容', () => {
|
describe('GET /commands/:name/content - 获取命令完整内容', () => {
|
||||||
// Note: Due to Hono route matching, /:name{.+} matches before /:name{.+}/content
|
it('获取成功,返回包含 template 的完整内容', async () => {
|
||||||
// So /test/content is interpreted as getting command "test/content" via the detail route
|
mockCommandManager.getContent.mockResolvedValue({
|
||||||
// This is a known issue in the source code - the /content endpoint is shadowed
|
success: true,
|
||||||
|
data: {
|
||||||
it('路由被 /:name{.+} 遮蔽,实际请求 test/content 作为命令名', async () => {
|
name: 'test',
|
||||||
// The request goes to GET /:name{.+} with name="test/content"
|
description: 'Test command',
|
||||||
// Not to GET /:name{.+}/content
|
|
||||||
mockCommandRegistry.get.mockReturnValue({
|
|
||||||
name: 'test/content',
|
|
||||||
description: 'A command with /content in name',
|
|
||||||
template: 'echo test',
|
template: 'echo test',
|
||||||
source: 'project',
|
source: 'project',
|
||||||
|
sourcePath: '/test/.claude/commands/test.md',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await app.request('/commands/test/content');
|
const res = await app.request('/commands/test/content');
|
||||||
@@ -324,21 +322,40 @@ describe('Commands Route', () => {
|
|||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(json.success).toBe(true);
|
expect(json.success).toBe(true);
|
||||||
// The detail route returns hasTemplate, not template
|
expect(json.data.template).toBe('echo test');
|
||||||
expect(json.data.hasTemplate).toBe(true);
|
expect(json.data.name).toBe('test');
|
||||||
expect(json.data.template).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('当命令不存在时返回 404', async () => {
|
it('支持包含斜杠的命令名', async () => {
|
||||||
// This also goes through GET /:name{.+} with name="non-existent/content"
|
mockCommandManager.getContent.mockResolvedValue({
|
||||||
mockCommandRegistry.get.mockReturnValue(undefined);
|
success: true,
|
||||||
|
data: {
|
||||||
|
name: 'deploy/staging',
|
||||||
|
template: 'deploy to staging',
|
||||||
|
source: 'project',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await app.request('/commands/deploy/staging/content');
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(json.success).toBe(true);
|
||||||
|
expect(json.data.template).toBe('deploy to staging');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('命令不存在返回 404', async () => {
|
||||||
|
mockCommandManager.getContent.mockResolvedValue({
|
||||||
|
success: false,
|
||||||
|
error: 'Command not found: non-existent',
|
||||||
|
});
|
||||||
|
|
||||||
const res = await app.request('/commands/non-existent/content');
|
const res = await app.request('/commands/non-existent/content');
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|
||||||
expect(res.status).toBe(404);
|
expect(res.status).toBe(404);
|
||||||
expect(json.success).toBe(false);
|
expect(json.success).toBe(false);
|
||||||
expect(json.error).toContain('non-existent/content');
|
expect(json.error).toContain('not found');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user