feat: 添加 Skills 系统支持可复用提示模板

- 新增 Skill 类型定义和加载器,支持 YAML/JSON/Markdown 格式
- 实现 Skill 注册表,支持搜索、分类和优先级覆盖
- 添加 8 个内置 Skills: code-review, explain-code, generate-docs 等
- 创建 skill 和 skill_search 工具供 Agent 调用
- 支持从用户目录和项目目录加载自定义 Skills
- 添加完整的单元测试覆盖
This commit is contained in:
2025-12-11 15:56:19 +08:00
parent ad5d30b262
commit 723558ff22
15 changed files with 2289 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { skillTool } from '../../../../src/tools/skill/skill.js';
import { getSkillRegistry, resetSkillRegistry } from '../../../../src/skills/registry.js';
import type { Skill } from '../../../../src/skills/types.js';
// Mock loader to prevent file system access
vi.mock('../../../../src/skills/loader.js', () => ({
skillLoader: {
loadFromDirectory: vi.fn().mockResolvedValue([]),
getUserSkillsDir: vi.fn().mockReturnValue('/mock/user/skills'),
getProjectSkillsDir: vi.fn().mockReturnValue('/mock/project/skills'),
},
}));
// Mock builtin skills
vi.mock('../../../../src/skills/builtin/index.js', () => ({
builtinSkills: [
{
name: 'code-review',
description: '代码审查',
promptTemplate: '请审查以下代码:\n\n{{code}}\n\n重点:{{focus}}',
parameters: {
code: { type: 'string', required: true, description: '代码' },
focus: { type: 'string', required: false, default: '代码质量', description: '审查重点' },
},
category: 'development',
source: 'builtin',
enabled: true,
},
{
name: 'disabled-skill',
description: '禁用的 Skill',
promptTemplate: '不应该执行',
source: 'builtin',
enabled: false,
},
],
}));
describe('skillTool - Skill 工具', () => {
beforeEach(async () => {
vi.clearAllMocks();
resetSkillRegistry();
// 初始化注册表
const registry = getSkillRegistry();
await registry.initialize();
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(skillTool.name).toBe('skill');
});
it('有正确的元数据', () => {
expect(skillTool.metadata.category).toBe('agent');
expect(skillTool.metadata.keywords).toContain('skill');
});
it('skill_name 参数是必须的', () => {
expect(skillTool.parameters.skill_name.required).toBe(true);
});
it('params 参数是可选的', () => {
expect(skillTool.parameters.params.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功执行 Skill 并渲染模板', async () => {
const result = await skillTool.execute({
skill_name: 'code-review',
params: {
code: 'function add(a, b) { return a + b; }',
focus: '性能',
},
});
expect(result.success).toBe(true);
expect(result.output).toContain('function add');
expect(result.output).toContain('性能');
expect(result.metadata?.skill).toBe('code-review');
});
it('使用默认参数值', async () => {
const result = await skillTool.execute({
skill_name: 'code-review',
params: {
code: 'const x = 1;',
},
});
expect(result.success).toBe(true);
expect(result.output).toContain('代码质量'); // 默认值
});
it('Skill 不存在时返回错误', async () => {
const result = await skillTool.execute({
skill_name: 'non-existent',
params: {},
});
expect(result.success).toBe(false);
expect(result.error).toContain('不存在');
});
it('Skill 不存在时提供建议', async () => {
const result = await skillTool.execute({
skill_name: 'code-rev', // 类似 code-review
params: {},
});
expect(result.success).toBe(false);
expect(result.error).toContain('code-review'); // 建议
});
it('缺少必需参数时返回错误', async () => {
const result = await skillTool.execute({
skill_name: 'code-review',
params: {}, // 缺少 code 参数
});
expect(result.success).toBe(false);
expect(result.error).toContain('缺少必需参数');
});
it('禁用的 Skill 返回错误', async () => {
const result = await skillTool.execute({
skill_name: 'disabled-skill',
params: {},
});
expect(result.success).toBe(false);
expect(result.error).toContain('禁用');
});
it('返回正确的 metadata', async () => {
const result = await skillTool.execute({
skill_name: 'code-review',
params: { code: 'test' },
});
expect(result.metadata?.skill).toBe('code-review');
expect(result.metadata?.category).toBe('development');
expect(result.metadata?.source).toBe('builtin');
});
});
});
+122
View File
@@ -0,0 +1,122 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { skillSearchTool } from '../../../../src/tools/skill/skill_search.js';
import { getSkillRegistry, resetSkillRegistry } from '../../../../src/skills/registry.js';
// Mock loader to prevent file system access
vi.mock('../../../../src/skills/loader.js', () => ({
skillLoader: {
loadFromDirectory: vi.fn().mockResolvedValue([]),
getUserSkillsDir: vi.fn().mockReturnValue('/mock/user/skills'),
getProjectSkillsDir: vi.fn().mockReturnValue('/mock/project/skills'),
},
}));
// Mock builtin skills
vi.mock('../../../../src/skills/builtin/index.js', () => ({
builtinSkills: [
{
name: 'code-review',
description: '代码审查',
promptTemplate: '审查代码',
keywords: ['review', 'quality'],
category: 'development',
source: 'builtin',
enabled: true,
},
{
name: 'generate-tests',
description: '生成测试',
promptTemplate: '生成测试',
keywords: ['test', 'unit'],
category: 'testing',
source: 'builtin',
enabled: true,
},
{
name: 'explain-code',
description: '解释代码功能',
promptTemplate: '解释代码',
category: 'development',
source: 'builtin',
enabled: true,
},
],
}));
describe('skillSearchTool - Skill 搜索工具', () => {
beforeEach(async () => {
vi.clearAllMocks();
resetSkillRegistry();
const registry = getSkillRegistry();
await registry.initialize();
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(skillSearchTool.name).toBe('skill_search');
});
it('有正确的元数据', () => {
expect(skillSearchTool.metadata.category).toBe('agent');
expect(skillSearchTool.metadata.keywords).toContain('search');
});
it('所有参数都是可选的', () => {
expect(skillSearchTool.parameters.query.required).toBe(false);
expect(skillSearchTool.parameters.category.required).toBe(false);
expect(skillSearchTool.parameters.list_all.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('list_all 列出所有 Skills', async () => {
const result = await skillSearchTool.execute({ list_all: true });
expect(result.success).toBe(true);
expect(result.output).toContain('code-review');
expect(result.output).toContain('generate-tests');
expect(result.output).toContain('explain-code');
expect(result.output).toContain('development');
expect(result.output).toContain('testing');
});
it('按关键词搜索', async () => {
const result = await skillSearchTool.execute({ query: 'review' });
expect(result.success).toBe(true);
expect(result.output).toContain('code-review');
expect(result.metadata?.query).toBe('review');
});
it('按分类筛选', async () => {
const result = await skillSearchTool.execute({ category: 'testing' });
expect(result.success).toBe(true);
expect(result.output).toContain('generate-tests');
expect(result.output).not.toContain('code-review');
});
it('分类不存在时显示可用分类', async () => {
const result = await skillSearchTool.execute({ category: 'non-existent' });
expect(result.success).toBe(true);
expect(result.output).toContain('没有 Skills');
expect(result.output).toContain('可用分类');
});
it('搜索无结果时提示', async () => {
const result = await skillSearchTool.execute({ query: 'xxxxxxxxx' });
expect(result.success).toBe(true);
expect(result.output).toContain('没有找到');
});
it('无参数时显示概览', async () => {
const result = await skillSearchTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toContain('概览');
expect(result.output).toContain('使用方法');
});
});
});