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:
Generated
+16
@@ -23,6 +23,7 @@
|
||||
"vscode-jsonrpc": "^8.2.1",
|
||||
"vscode-languageserver-protocol": "^3.17.5",
|
||||
"web-tree-sitter": "^0.25.10",
|
||||
"yaml": "^2.8.2",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
"bin": {
|
||||
@@ -3661,6 +3662,21 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/yoctocolors-cjs": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"vscode-jsonrpc": "^8.2.1",
|
||||
"vscode-languageserver-protocol": "^3.17.5",
|
||||
"web-tree-sitter": "^0.25.10",
|
||||
"yaml": "^2.8.2",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
/**
|
||||
* 内置 Skills
|
||||
*
|
||||
* 提供一些常用的预定义 Skills
|
||||
*/
|
||||
|
||||
import type { Skill } from '../types.js';
|
||||
|
||||
/**
|
||||
* 代码审查 Skill
|
||||
*/
|
||||
export const codeReviewSkill: Skill = {
|
||||
name: 'code-review',
|
||||
displayName: '代码审查',
|
||||
description: '对代码进行全面审查,检查潜在问题、最佳实践和改进建议',
|
||||
category: 'development',
|
||||
promptTemplate: `请对以下代码进行全面审查:
|
||||
|
||||
{{code}}
|
||||
|
||||
审查重点:
|
||||
{{#if focus}}
|
||||
- {{focus}}
|
||||
{{else}}
|
||||
- 代码质量和可读性
|
||||
- 潜在的 bug 和错误
|
||||
- 性能问题
|
||||
- 安全隐患
|
||||
- 最佳实践遵循情况
|
||||
{{/if}}
|
||||
|
||||
请提供:
|
||||
1. 发现的问题列表(按严重程度排序)
|
||||
2. 具体的改进建议
|
||||
3. 代码中做得好的地方`,
|
||||
parameters: {
|
||||
code: {
|
||||
type: 'string',
|
||||
description: '要审查的代码',
|
||||
required: true,
|
||||
},
|
||||
focus: {
|
||||
type: 'string',
|
||||
description: '审查重点(可选)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
keywords: ['review', 'code', 'quality', '审查', '代码', '质量'],
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 代码解释 Skill
|
||||
*/
|
||||
export const explainCodeSkill: Skill = {
|
||||
name: 'explain-code',
|
||||
displayName: '代码解释',
|
||||
description: '详细解释代码的功能、逻辑和工作原理',
|
||||
category: 'development',
|
||||
promptTemplate: `请详细解释以下代码:
|
||||
|
||||
{{code}}
|
||||
|
||||
{{#if level}}
|
||||
解释级别:{{level}}
|
||||
{{/if}}
|
||||
|
||||
请包含:
|
||||
1. 代码的整体功能
|
||||
2. 主要逻辑流程
|
||||
3. 关键部分的详细解释
|
||||
4. 使用的设计模式或技术(如果有)`,
|
||||
parameters: {
|
||||
code: {
|
||||
type: 'string',
|
||||
description: '要解释的代码',
|
||||
required: true,
|
||||
},
|
||||
level: {
|
||||
type: 'string',
|
||||
description: '解释级别(beginner/intermediate/advanced)',
|
||||
required: false,
|
||||
enum: ['beginner', 'intermediate', 'advanced'],
|
||||
default: 'intermediate',
|
||||
},
|
||||
},
|
||||
keywords: ['explain', 'code', 'understand', '解释', '理解', '说明'],
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 文档生成 Skill
|
||||
*/
|
||||
export const generateDocsSkill: Skill = {
|
||||
name: 'generate-docs',
|
||||
displayName: '文档生成',
|
||||
description: '为代码生成文档注释或 README',
|
||||
category: 'documentation',
|
||||
promptTemplate: `请为以下代码生成{{type}}:
|
||||
|
||||
{{code}}
|
||||
|
||||
{{#if style}}
|
||||
文档风格:{{style}}
|
||||
{{/if}}
|
||||
|
||||
要求:
|
||||
- 清晰描述功能和用途
|
||||
- 说明参数和返回值(如果适用)
|
||||
- 提供使用示例
|
||||
- 使用规范的格式`,
|
||||
parameters: {
|
||||
code: {
|
||||
type: 'string',
|
||||
description: '要生成文档的代码',
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: '文档类型',
|
||||
required: false,
|
||||
enum: ['JSDoc', 'TSDoc', 'README', '注释', 'API文档'],
|
||||
default: '文档注释',
|
||||
},
|
||||
style: {
|
||||
type: 'string',
|
||||
description: '文档风格(简洁/详细)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
keywords: ['docs', 'documentation', 'jsdoc', '文档', '注释', '说明'],
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 单元测试生成 Skill
|
||||
*/
|
||||
export const generateTestsSkill: Skill = {
|
||||
name: 'generate-tests',
|
||||
displayName: '测试生成',
|
||||
description: '为代码生成单元测试',
|
||||
category: 'testing',
|
||||
promptTemplate: `请为以下代码生成单元测试:
|
||||
|
||||
{{code}}
|
||||
|
||||
测试框架:{{framework}}
|
||||
|
||||
要求:
|
||||
- 覆盖主要功能路径
|
||||
- 包含边界条件测试
|
||||
- 包含错误处理测试
|
||||
- 使用清晰的测试描述
|
||||
- 遵循 AAA 模式(Arrange-Act-Assert)`,
|
||||
parameters: {
|
||||
code: {
|
||||
type: 'string',
|
||||
description: '要测试的代码',
|
||||
required: true,
|
||||
},
|
||||
framework: {
|
||||
type: 'string',
|
||||
description: '测试框架',
|
||||
required: false,
|
||||
enum: ['vitest', 'jest', 'mocha', 'pytest', 'unittest'],
|
||||
default: 'vitest',
|
||||
},
|
||||
},
|
||||
keywords: ['test', 'unit', 'testing', '测试', '单元测试', 'vitest', 'jest'],
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 重构建议 Skill
|
||||
*/
|
||||
export const refactorSuggestSkill: Skill = {
|
||||
name: 'refactor-suggest',
|
||||
displayName: '重构建议',
|
||||
description: '分析代码并提供重构建议',
|
||||
category: 'development',
|
||||
promptTemplate: `请分析以下代码并提供重构建议:
|
||||
|
||||
{{code}}
|
||||
|
||||
{{#if goal}}
|
||||
重构目标:{{goal}}
|
||||
{{/if}}
|
||||
|
||||
请提供:
|
||||
1. 当前代码的问题分析
|
||||
2. 具体的重构建议
|
||||
3. 重构后的代码示例
|
||||
4. 重构的好处说明`,
|
||||
parameters: {
|
||||
code: {
|
||||
type: 'string',
|
||||
description: '要重构的代码',
|
||||
required: true,
|
||||
},
|
||||
goal: {
|
||||
type: 'string',
|
||||
description: '重构目标(如:提高可读性、优化性能、减少重复)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
keywords: ['refactor', 'improve', 'optimize', '重构', '优化', '改进'],
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Bug 修复 Skill
|
||||
*/
|
||||
export const fixBugSkill: Skill = {
|
||||
name: 'fix-bug',
|
||||
displayName: 'Bug 修复',
|
||||
description: '分析代码问题并提供修复方案',
|
||||
category: 'debugging',
|
||||
promptTemplate: `请分析以下代码中的问题并提供修复方案:
|
||||
|
||||
代码:
|
||||
{{code}}
|
||||
|
||||
{{#if error}}
|
||||
错误信息:
|
||||
{{error}}
|
||||
{{/if}}
|
||||
|
||||
{{#if context}}
|
||||
上下文:
|
||||
{{context}}
|
||||
{{/if}}
|
||||
|
||||
请提供:
|
||||
1. 问题的根本原因分析
|
||||
2. 修复方案
|
||||
3. 修复后的代码
|
||||
4. 如何避免类似问题的建议`,
|
||||
parameters: {
|
||||
code: {
|
||||
type: 'string',
|
||||
description: '有问题的代码',
|
||||
required: true,
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
description: '错误信息',
|
||||
required: false,
|
||||
},
|
||||
context: {
|
||||
type: 'string',
|
||||
description: '额外的上下文信息',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
keywords: ['bug', 'fix', 'debug', 'error', '修复', '错误', '调试'],
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Git Commit 消息生成 Skill
|
||||
*/
|
||||
export const gitCommitSkill: Skill = {
|
||||
name: 'git-commit',
|
||||
displayName: 'Git Commit',
|
||||
description: '根据代码变更生成规范的 Git commit 消息',
|
||||
category: 'git',
|
||||
promptTemplate: `请根据以下代码变更生成规范的 Git commit 消息:
|
||||
|
||||
变更内容:
|
||||
{{diff}}
|
||||
|
||||
{{#if type}}
|
||||
Commit 类型:{{type}}
|
||||
{{/if}}
|
||||
|
||||
要求:
|
||||
- 遵循 Conventional Commits 规范
|
||||
- 第一行不超过 50 个字符
|
||||
- 清晰描述变更的目的
|
||||
- 如果有 breaking changes,请说明`,
|
||||
parameters: {
|
||||
diff: {
|
||||
type: 'string',
|
||||
description: 'Git diff 内容或变更描述',
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Commit 类型',
|
||||
required: false,
|
||||
enum: ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore'],
|
||||
},
|
||||
},
|
||||
keywords: ['git', 'commit', 'message', '提交', '消息'],
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* API 设计 Skill
|
||||
*/
|
||||
export const apiDesignSkill: Skill = {
|
||||
name: 'api-design',
|
||||
displayName: 'API 设计',
|
||||
description: '设计 RESTful API 接口',
|
||||
category: 'architecture',
|
||||
promptTemplate: `请为以下需求设计 RESTful API:
|
||||
|
||||
需求描述:
|
||||
{{requirement}}
|
||||
|
||||
{{#if constraints}}
|
||||
约束条件:
|
||||
{{constraints}}
|
||||
{{/if}}
|
||||
|
||||
请提供:
|
||||
1. API 端点设计(路径、方法、参数)
|
||||
2. 请求/响应格式(JSON 示例)
|
||||
3. 错误处理方案
|
||||
4. 认证/授权建议(如果需要)`,
|
||||
parameters: {
|
||||
requirement: {
|
||||
type: 'string',
|
||||
description: 'API 需求描述',
|
||||
required: true,
|
||||
},
|
||||
constraints: {
|
||||
type: 'string',
|
||||
description: '设计约束(如:现有系统兼容性、性能要求等)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
keywords: ['api', 'rest', 'design', 'endpoint', 'API', '接口', '设计'],
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 所有内置 Skills
|
||||
*/
|
||||
export const builtinSkills: Skill[] = [
|
||||
codeReviewSkill,
|
||||
explainCodeSkill,
|
||||
generateDocsSkill,
|
||||
generateTestsSkill,
|
||||
refactorSuggestSkill,
|
||||
fixBugSkill,
|
||||
gitCommitSkill,
|
||||
apiDesignSkill,
|
||||
];
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Skills 模块
|
||||
*
|
||||
* 提供 Skill 系统的所有功能导出
|
||||
*/
|
||||
|
||||
// 类型
|
||||
export type {
|
||||
Skill,
|
||||
SkillParameter,
|
||||
SkillContext,
|
||||
SkillExecutionResult,
|
||||
SkillFile,
|
||||
SkillSearchResult,
|
||||
SkillRegistryConfig,
|
||||
} from './types.js';
|
||||
|
||||
// 加载器
|
||||
export { SkillLoader, skillLoader } from './loader.js';
|
||||
|
||||
// 注册表
|
||||
export {
|
||||
SkillRegistry,
|
||||
getSkillRegistry,
|
||||
resetSkillRegistry,
|
||||
} from './registry.js';
|
||||
|
||||
// 内置 Skills
|
||||
export { builtinSkills } from './builtin/index.js';
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Skill 加载器
|
||||
*
|
||||
* 负责从文件系统加载 Skill 定义。
|
||||
* 支持从以下位置加载:
|
||||
* 1. 内置 Skills(代码中定义)
|
||||
* 2. 用户 Skills(~/.config/ai-terminal/skills/)
|
||||
* 3. 项目 Skills(./.ai-terminal/skills/)
|
||||
*/
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import * as yaml from 'yaml';
|
||||
import type { Skill, SkillFile } from './types.js';
|
||||
|
||||
/**
|
||||
* Skill 加载器
|
||||
*/
|
||||
export class SkillLoader {
|
||||
/**
|
||||
* 从目录加载所有 Skills
|
||||
*/
|
||||
async loadFromDirectory(
|
||||
dir: string,
|
||||
source: 'user' | 'project'
|
||||
): Promise<Skill[]> {
|
||||
const skills: Skill[] = [];
|
||||
|
||||
try {
|
||||
const exists = await fs
|
||||
.access(dir)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (!exists) {
|
||||
return skills;
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile()) {
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (['.yaml', '.yml', '.json', '.md'].includes(ext)) {
|
||||
const filePath = path.join(dir, entry.name);
|
||||
try {
|
||||
const skill = await this.loadFromFile(filePath, source);
|
||||
if (skill) {
|
||||
skills.push(skill);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`加载 Skill 文件失败: ${filePath}`, error);
|
||||
}
|
||||
}
|
||||
} else if (entry.isDirectory()) {
|
||||
// 递归加载子目录
|
||||
const subDir = path.join(dir, entry.name);
|
||||
const subSkills = await this.loadFromDirectory(subDir, source);
|
||||
skills.push(...subSkills);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`读取 Skills 目录失败: ${dir}`, error);
|
||||
}
|
||||
|
||||
return skills;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从单个文件加载 Skill
|
||||
*/
|
||||
async loadFromFile(
|
||||
filePath: string,
|
||||
source: 'user' | 'project'
|
||||
): Promise<Skill | null> {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
|
||||
let skillData: SkillFile | null = null;
|
||||
|
||||
if (ext === '.md') {
|
||||
// Markdown 格式:从 frontmatter 和内容中解析
|
||||
skillData = this.parseMarkdownSkill(content, filePath);
|
||||
} else if (ext === '.yaml' || ext === '.yml') {
|
||||
// YAML 格式
|
||||
skillData = yaml.parse(content) as SkillFile;
|
||||
} else if (ext === '.json') {
|
||||
// JSON 格式
|
||||
skillData = JSON.parse(content) as SkillFile;
|
||||
}
|
||||
|
||||
if (!skillData?.skill) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证必需字段
|
||||
const { skill } = skillData;
|
||||
if (!skill.name || !skill.promptTemplate) {
|
||||
console.warn(`Skill 文件缺少必需字段: ${filePath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...skill,
|
||||
source,
|
||||
sourcePath: filePath,
|
||||
enabled: skill.enabled ?? true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Markdown 格式的 Skill
|
||||
*
|
||||
* 格式示例:
|
||||
* ```markdown
|
||||
* ---
|
||||
* name: code-review
|
||||
* description: 代码审查
|
||||
* parameters:
|
||||
* focus:
|
||||
* type: string
|
||||
* description: 审查重点
|
||||
* ---
|
||||
*
|
||||
* # 代码审查
|
||||
*
|
||||
* 请审查以下代码,重点关注 {{focus}}:
|
||||
*
|
||||
* {{code}}
|
||||
* ```
|
||||
*/
|
||||
private parseMarkdownSkill(
|
||||
content: string,
|
||||
filePath: string
|
||||
): SkillFile | null {
|
||||
// 解析 frontmatter
|
||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
||||
|
||||
if (!frontmatterMatch) {
|
||||
// 没有 frontmatter,使用文件名作为 name,整个内容作为 promptTemplate
|
||||
const name = path.basename(filePath, path.extname(filePath));
|
||||
return {
|
||||
skill: {
|
||||
name,
|
||||
description: `Skill: ${name}`,
|
||||
promptTemplate: content.trim(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const [, frontmatterStr, bodyContent] = frontmatterMatch;
|
||||
|
||||
try {
|
||||
const frontmatter = yaml.parse(frontmatterStr) as Partial<Skill>;
|
||||
|
||||
// 如果 frontmatter 中没有 promptTemplate,使用 body 内容
|
||||
const promptTemplate = frontmatter.promptTemplate || bodyContent.trim();
|
||||
|
||||
// 从文件名获取默认 name
|
||||
const defaultName = path.basename(filePath, path.extname(filePath));
|
||||
|
||||
return {
|
||||
skill: {
|
||||
name: frontmatter.name || defaultName,
|
||||
displayName: frontmatter.displayName,
|
||||
description: frontmatter.description || `Skill: ${defaultName}`,
|
||||
category: frontmatter.category,
|
||||
promptTemplate,
|
||||
parameters: frontmatter.parameters,
|
||||
keywords: frontmatter.keywords,
|
||||
version: frontmatter.version,
|
||||
author: frontmatter.author,
|
||||
enabled: frontmatter.enabled,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(`解析 Skill frontmatter 失败: ${filePath}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户 Skills 目录
|
||||
*/
|
||||
getUserSkillsDir(): string {
|
||||
const home = process.env.HOME || process.env.USERPROFILE || '';
|
||||
return path.join(home, '.config', 'ai-terminal', 'skills');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目 Skills 目录
|
||||
*/
|
||||
getProjectSkillsDir(workdir: string = process.cwd()): string {
|
||||
return path.join(workdir, '.ai-terminal', 'skills');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局 Skill 加载器实例
|
||||
*/
|
||||
export const skillLoader = new SkillLoader();
|
||||
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* Skill 注册表
|
||||
*
|
||||
* 管理所有可用的 Skills,支持:
|
||||
* - 注册/注销 Skills
|
||||
* - 按名称/分类/关键词查询
|
||||
* - 渲染 Skill 提示模板
|
||||
*/
|
||||
|
||||
import type {
|
||||
Skill,
|
||||
SkillContext,
|
||||
SkillExecutionResult,
|
||||
SkillSearchResult,
|
||||
SkillRegistryConfig,
|
||||
} from './types.js';
|
||||
import { skillLoader } from './loader.js';
|
||||
import { builtinSkills } from './builtin/index.js';
|
||||
|
||||
/**
|
||||
* Skill 注册表
|
||||
*/
|
||||
export class SkillRegistry {
|
||||
private skills = new Map<string, Skill>();
|
||||
private config: SkillRegistryConfig;
|
||||
private initialized = false;
|
||||
|
||||
constructor(config: SkillRegistryConfig = {}) {
|
||||
this.config = {
|
||||
autoLoad: true,
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化注册表
|
||||
*/
|
||||
async initialize(workdir: string = process.cwd()): Promise<void> {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 注册内置 Skills
|
||||
for (const skill of builtinSkills) {
|
||||
this.register(skill);
|
||||
}
|
||||
|
||||
// 2. 加载用户 Skills
|
||||
if (this.config.autoLoad) {
|
||||
const userDir =
|
||||
this.config.userSkillsDir || skillLoader.getUserSkillsDir();
|
||||
const userSkills = await skillLoader.loadFromDirectory(userDir, 'user');
|
||||
for (const skill of userSkills) {
|
||||
this.register(skill);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 加载项目 Skills
|
||||
if (this.config.autoLoad) {
|
||||
const projectDir =
|
||||
this.config.projectSkillsDir || skillLoader.getProjectSkillsDir(workdir);
|
||||
const projectSkills = await skillLoader.loadFromDirectory(
|
||||
projectDir,
|
||||
'project'
|
||||
);
|
||||
for (const skill of projectSkills) {
|
||||
this.register(skill);
|
||||
}
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 Skill
|
||||
*/
|
||||
register(skill: Skill): void {
|
||||
// 项目 Skills 优先级最高,可以覆盖同名的内置/用户 Skills
|
||||
const existing = this.skills.get(skill.name);
|
||||
if (existing) {
|
||||
// 优先级: project > user > builtin
|
||||
const priority = { project: 3, user: 2, builtin: 1 };
|
||||
if (priority[skill.source] < priority[existing.source]) {
|
||||
return; // 不覆盖更高优先级的 Skill
|
||||
}
|
||||
}
|
||||
this.skills.set(skill.name, skill);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销 Skill
|
||||
*/
|
||||
unregister(name: string): boolean {
|
||||
return this.skills.delete(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Skill
|
||||
*/
|
||||
get(name: string): Skill | undefined {
|
||||
return this.skills.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 Skill 是否存在
|
||||
*/
|
||||
has(name: string): boolean {
|
||||
return this.skills.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有 Skills
|
||||
*/
|
||||
getAll(): Skill[] {
|
||||
return Array.from(this.skills.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用的 Skills
|
||||
*/
|
||||
getEnabled(): Skill[] {
|
||||
return this.getAll().filter((s) => s.enabled !== false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按分类获取 Skills
|
||||
*/
|
||||
getByCategory(category: string): Skill[] {
|
||||
return this.getEnabled().filter((s) => s.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有分类
|
||||
*/
|
||||
getCategories(): string[] {
|
||||
const categories = new Set<string>();
|
||||
for (const skill of this.getEnabled()) {
|
||||
if (skill.category) {
|
||||
categories.add(skill.category);
|
||||
}
|
||||
}
|
||||
return Array.from(categories).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索 Skills
|
||||
*/
|
||||
search(query: string, limit: number = 10): SkillSearchResult[] {
|
||||
const queryLower = query.toLowerCase();
|
||||
const results: SkillSearchResult[] = [];
|
||||
|
||||
for (const skill of this.getEnabled()) {
|
||||
let score = 0;
|
||||
let matchReason = '';
|
||||
|
||||
// 精确名称匹配
|
||||
if (skill.name.toLowerCase() === queryLower) {
|
||||
score = 100;
|
||||
matchReason = '名称精确匹配';
|
||||
}
|
||||
// 名称前缀匹配
|
||||
else if (skill.name.toLowerCase().startsWith(queryLower)) {
|
||||
score = 80;
|
||||
matchReason = '名称前缀匹配';
|
||||
}
|
||||
// 名称包含匹配
|
||||
else if (skill.name.toLowerCase().includes(queryLower)) {
|
||||
score = 60;
|
||||
matchReason = '名称包含匹配';
|
||||
}
|
||||
// 描述匹配
|
||||
else if (skill.description.toLowerCase().includes(queryLower)) {
|
||||
score = 40;
|
||||
matchReason = '描述匹配';
|
||||
}
|
||||
// 关键词匹配
|
||||
else if (
|
||||
skill.keywords?.some((k) => k.toLowerCase().includes(queryLower))
|
||||
) {
|
||||
score = 30;
|
||||
matchReason = '关键词匹配';
|
||||
}
|
||||
// 分类匹配
|
||||
else if (skill.category?.toLowerCase().includes(queryLower)) {
|
||||
score = 20;
|
||||
matchReason = '分类匹配';
|
||||
}
|
||||
|
||||
if (score > 0) {
|
||||
results.push({ skill, score, matchReason });
|
||||
}
|
||||
}
|
||||
|
||||
// 按分数降序排序
|
||||
results.sort((a, b) => b.score - a.score);
|
||||
|
||||
return results.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 Skill 提示模板
|
||||
*/
|
||||
renderPrompt(
|
||||
skill: Skill,
|
||||
params: Record<string, unknown>,
|
||||
context?: SkillContext
|
||||
): SkillExecutionResult {
|
||||
try {
|
||||
// 验证必需参数
|
||||
if (skill.parameters) {
|
||||
for (const [name, param] of Object.entries(skill.parameters)) {
|
||||
if (param.required && !(name in params)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `缺少必需参数: ${name}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建变量映射
|
||||
const variables: Record<string, string> = {};
|
||||
|
||||
// 添加参数值
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
variables[key] = String(value);
|
||||
}
|
||||
|
||||
// 添加上下文变量
|
||||
if (context?.variables) {
|
||||
for (const [key, value] of Object.entries(context.variables)) {
|
||||
if (!(key in variables)) {
|
||||
variables[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加默认值
|
||||
if (skill.parameters) {
|
||||
for (const [name, param] of Object.entries(skill.parameters)) {
|
||||
if (!(name in variables) && param.default !== undefined) {
|
||||
variables[name] = String(param.default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染模板
|
||||
let prompt = skill.promptTemplate;
|
||||
|
||||
// 替换 {{variable}} 格式的变量
|
||||
prompt = prompt.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
|
||||
if (varName in variables) {
|
||||
return variables[varName];
|
||||
}
|
||||
// 保留未匹配的变量(可能是用户意图保留)
|
||||
return match;
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
prompt,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `渲染 Skill 提示失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 Skill(渲染模板并返回提示)
|
||||
*/
|
||||
execute(
|
||||
name: string,
|
||||
params: Record<string, unknown>,
|
||||
context?: SkillContext
|
||||
): SkillExecutionResult {
|
||||
const skill = this.get(name);
|
||||
|
||||
if (!skill) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Skill 不存在: ${name}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (skill.enabled === false) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Skill 已禁用: ${name}`,
|
||||
};
|
||||
}
|
||||
|
||||
return this.renderPrompt(skill, params, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载 Skills
|
||||
*/
|
||||
async reload(workdir: string = process.cwd()): Promise<void> {
|
||||
this.skills.clear();
|
||||
this.initialized = false;
|
||||
await this.initialize(workdir);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Skill 统计信息
|
||||
*/
|
||||
getStats(): {
|
||||
total: number;
|
||||
enabled: number;
|
||||
bySource: Record<string, number>;
|
||||
byCategory: Record<string, number>;
|
||||
} {
|
||||
const skills = this.getAll();
|
||||
const enabled = this.getEnabled();
|
||||
|
||||
const bySource: Record<string, number> = {};
|
||||
const byCategory: Record<string, number> = {};
|
||||
|
||||
for (const skill of skills) {
|
||||
bySource[skill.source] = (bySource[skill.source] || 0) + 1;
|
||||
if (skill.category) {
|
||||
byCategory[skill.category] = (byCategory[skill.category] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
total: skills.length,
|
||||
enabled: enabled.length,
|
||||
bySource,
|
||||
byCategory,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局 Skill 注册表实例
|
||||
*/
|
||||
let skillRegistryInstance: SkillRegistry | null = null;
|
||||
|
||||
/**
|
||||
* 获取全局 Skill 注册表
|
||||
*/
|
||||
export function getSkillRegistry(): SkillRegistry {
|
||||
if (!skillRegistryInstance) {
|
||||
skillRegistryInstance = new SkillRegistry();
|
||||
}
|
||||
return skillRegistryInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置全局 Skill 注册表(用于测试)
|
||||
*/
|
||||
export function resetSkillRegistry(): void {
|
||||
skillRegistryInstance = null;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Skill 系统类型定义
|
||||
*
|
||||
* Skill 是可复用的提示模板,类似于 Claude Code 的 Skills 功能。
|
||||
* 与 Agent 不同,Skill 不是独立的执行单元,而是预定义的提示模板,
|
||||
* 可以被 Agent 调用来执行特定任务。
|
||||
*/
|
||||
|
||||
/**
|
||||
* Skill 参数定义
|
||||
*/
|
||||
export interface SkillParameter {
|
||||
/** 参数类型 */
|
||||
type: 'string' | 'number' | 'boolean' | 'array' | 'object';
|
||||
/** 参数描述 */
|
||||
description: string;
|
||||
/** 是否必需 */
|
||||
required?: boolean;
|
||||
/** 默认值 */
|
||||
default?: unknown;
|
||||
/** 枚举值(仅 string 类型) */
|
||||
enum?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill 定义
|
||||
*/
|
||||
export interface Skill {
|
||||
/** Skill 唯一标识 */
|
||||
name: string;
|
||||
/** Skill 显示名称 */
|
||||
displayName?: string;
|
||||
/** Skill 描述 */
|
||||
description: string;
|
||||
/** Skill 分类 */
|
||||
category?: string;
|
||||
/** 提示模板(支持变量插值 {{variable}}) */
|
||||
promptTemplate: string;
|
||||
/** Skill 参数定义 */
|
||||
parameters?: Record<string, SkillParameter>;
|
||||
/** 关键词(用于搜索) */
|
||||
keywords?: string[];
|
||||
/** 来源(内置/用户定义/项目) */
|
||||
source: 'builtin' | 'user' | 'project';
|
||||
/** 来源路径(用户定义或项目 Skill 的文件路径) */
|
||||
sourcePath?: string;
|
||||
/** 是否启用 */
|
||||
enabled?: boolean;
|
||||
/** 版本 */
|
||||
version?: string;
|
||||
/** 作者 */
|
||||
author?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill 执行上下文
|
||||
*/
|
||||
export interface SkillContext {
|
||||
/** 当前工作目录 */
|
||||
workdir: string;
|
||||
/** 额外的上下文变量 */
|
||||
variables?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill 执行结果
|
||||
*/
|
||||
export interface SkillExecutionResult {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
/** 渲染后的提示 */
|
||||
prompt?: string;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill 文件格式(YAML/JSON)
|
||||
*/
|
||||
export interface SkillFile {
|
||||
/** 文件版本 */
|
||||
version?: string;
|
||||
/** Skill 定义 */
|
||||
skill: Omit<Skill, 'source' | 'sourcePath'>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill 搜索结果
|
||||
*/
|
||||
export interface SkillSearchResult {
|
||||
/** Skill */
|
||||
skill: Skill;
|
||||
/** 匹配分数 */
|
||||
score: number;
|
||||
/** 匹配原因 */
|
||||
matchReason: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill 注册表配置
|
||||
*/
|
||||
export interface SkillRegistryConfig {
|
||||
/** 用户 Skills 目录 */
|
||||
userSkillsDir?: string;
|
||||
/** 项目 Skills 目录 */
|
||||
projectSkillsDir?: string;
|
||||
/** 是否自动加载 */
|
||||
autoLoad?: boolean;
|
||||
}
|
||||
@@ -11,6 +11,9 @@ import { todoReadTool, todoWriteTool } from './todo/index.js';
|
||||
// Task 工具(Agent 子任务)
|
||||
import { taskTool, agentOutputTool } from './task/index.js';
|
||||
|
||||
// Skill 工具
|
||||
import { skillTool, skillSearchTool } from './skill/index.js';
|
||||
|
||||
// 文件系统工具
|
||||
import {
|
||||
readFileTool,
|
||||
@@ -53,6 +56,10 @@ const allToolsWithMetadata: ToolWithMetadata[] = [
|
||||
taskTool,
|
||||
agentOutputTool,
|
||||
|
||||
// Skill 工具 (deferLoading: false)
|
||||
skillTool,
|
||||
skillSearchTool,
|
||||
|
||||
// 文件系统工具 (deferLoading: true)
|
||||
readFileTool,
|
||||
writeFileTool,
|
||||
@@ -91,6 +98,7 @@ export { toolRegistry } from './registry.js';
|
||||
export { toolSearchTool } from './tool-search.js';
|
||||
export { todoManager } from './todo/index.js';
|
||||
export { initTaskContext, updateTaskDescription } from './task/index.js';
|
||||
export { updateSkillDescription } from './skill/index.js';
|
||||
export type { ToolWithMetadata, ToolMetadata, ToolCategory, ToolSearchResult } from './types.js';
|
||||
|
||||
// 兼容旧代码:导出所有工具数组(基础 Tool 类型)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { skillTool, updateSkillDescription } from './skill.js';
|
||||
export { skillSearchTool } from './skill_search.js';
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Skill 工具
|
||||
*
|
||||
* 允许 Agent 调用预定义的 Skill 来执行特定任务。
|
||||
* Skill 是可复用的提示模板,不同于 Agent(独立执行单元)。
|
||||
*/
|
||||
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { getSkillRegistry } from '../../skills/registry.js';
|
||||
|
||||
/**
|
||||
* 获取 Skill 工具动态描述
|
||||
*/
|
||||
function getSkillDescription(): string {
|
||||
const registry = getSkillRegistry();
|
||||
const skills = registry.getEnabled();
|
||||
|
||||
if (skills.length === 0) {
|
||||
return '执行 Skill(当前没有可用的 Skill)';
|
||||
}
|
||||
|
||||
// 按分类分组
|
||||
const byCategory = new Map<string, typeof skills>();
|
||||
const uncategorized: typeof skills = [];
|
||||
|
||||
for (const skill of skills) {
|
||||
if (skill.category) {
|
||||
if (!byCategory.has(skill.category)) {
|
||||
byCategory.set(skill.category, []);
|
||||
}
|
||||
byCategory.get(skill.category)!.push(skill);
|
||||
} else {
|
||||
uncategorized.push(skill);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建描述
|
||||
let description = '执行预定义的 Skill 来完成特定任务。\n\n可用的 Skills:\n';
|
||||
|
||||
for (const [category, categorySkills] of byCategory) {
|
||||
description += `\n[${category}]\n`;
|
||||
for (const skill of categorySkills) {
|
||||
description += `- ${skill.name}: ${skill.description}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (uncategorized.length > 0) {
|
||||
description += '\n[其他]\n';
|
||||
for (const skill of uncategorized) {
|
||||
description += `- ${skill.name}: ${skill.description}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
description += `
|
||||
使用示例:
|
||||
- skill({ skill_name: "code-review", params: { code: "..." } })
|
||||
- skill({ skill_name: "generate-tests", params: { code: "...", framework: "vitest" } })`;
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill 工具
|
||||
*/
|
||||
export const skillTool: ToolWithMetadata = {
|
||||
name: 'skill',
|
||||
description: getSkillDescription(),
|
||||
parameters: {
|
||||
skill_name: {
|
||||
type: 'string',
|
||||
description: 'Skill 名称',
|
||||
required: true,
|
||||
},
|
||||
params: {
|
||||
type: 'object',
|
||||
description: 'Skill 参数(根据具体 Skill 定义)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
name: 'skill',
|
||||
category: 'agent',
|
||||
description: '执行预定义的 Skill',
|
||||
keywords: ['skill', 'template', 'prompt', '技能', '模板', '提示'],
|
||||
deferLoading: false,
|
||||
},
|
||||
async execute(params) {
|
||||
const { skill_name, params: skillParams = {} } = params as {
|
||||
skill_name: string;
|
||||
params?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const registry = getSkillRegistry();
|
||||
|
||||
// 检查 Skill 是否存在
|
||||
const skill = registry.get(skill_name);
|
||||
if (!skill) {
|
||||
// 尝试搜索相似的 Skill
|
||||
const suggestions = registry.search(skill_name, 3);
|
||||
let errorMsg = `Skill 不存在: ${skill_name}`;
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
errorMsg += '\n\n你可能想要的 Skill:\n';
|
||||
for (const { skill: s, matchReason } of suggestions) {
|
||||
errorMsg += `- ${s.name}: ${s.description} (${matchReason})\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: errorMsg,
|
||||
};
|
||||
}
|
||||
|
||||
// 执行 Skill(渲染模板)
|
||||
const result = registry.execute(skill_name, skillParams, {
|
||||
workdir: process.cwd(),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: result.error || 'Skill 执行失败',
|
||||
};
|
||||
}
|
||||
|
||||
// 返回渲染后的提示
|
||||
return {
|
||||
success: true,
|
||||
output: result.prompt || '',
|
||||
metadata: {
|
||||
skill: skill_name,
|
||||
category: skill.category,
|
||||
source: skill.source,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新 Skill 工具描述
|
||||
*/
|
||||
export function updateSkillDescription(): void {
|
||||
(skillTool as { description: string }).description = getSkillDescription();
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Skill 搜索工具
|
||||
*
|
||||
* 搜索可用的 Skills,帮助用户发现适合的 Skill。
|
||||
*/
|
||||
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { getSkillRegistry } from '../../skills/registry.js';
|
||||
|
||||
/**
|
||||
* Skill 搜索工具
|
||||
*/
|
||||
export const skillSearchTool: ToolWithMetadata = {
|
||||
name: 'skill_search',
|
||||
description: '搜索可用的 Skills,根据关键词或分类查找合适的 Skill',
|
||||
parameters: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: '搜索关键词',
|
||||
required: false,
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
description: '按分类筛选',
|
||||
required: false,
|
||||
},
|
||||
list_all: {
|
||||
type: 'boolean',
|
||||
description: '列出所有可用的 Skills',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
name: 'skill_search',
|
||||
category: 'agent',
|
||||
description: '搜索可用的 Skills',
|
||||
keywords: ['skill', 'search', 'find', '技能', '搜索', '查找'],
|
||||
deferLoading: false,
|
||||
},
|
||||
async execute(params) {
|
||||
const { query, category, list_all } = params as {
|
||||
query?: string;
|
||||
category?: string;
|
||||
list_all?: boolean;
|
||||
};
|
||||
|
||||
const registry = getSkillRegistry();
|
||||
|
||||
// 列出所有 Skills
|
||||
if (list_all) {
|
||||
const skills = registry.getEnabled();
|
||||
const stats = registry.getStats();
|
||||
|
||||
if (skills.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
output: '当前没有可用的 Skills。',
|
||||
};
|
||||
}
|
||||
|
||||
// 按分类分组
|
||||
const byCategory = new Map<string, typeof skills>();
|
||||
const uncategorized: typeof skills = [];
|
||||
|
||||
for (const skill of skills) {
|
||||
if (skill.category) {
|
||||
if (!byCategory.has(skill.category)) {
|
||||
byCategory.set(skill.category, []);
|
||||
}
|
||||
byCategory.get(skill.category)!.push(skill);
|
||||
} else {
|
||||
uncategorized.push(skill);
|
||||
}
|
||||
}
|
||||
|
||||
let output = `📚 可用的 Skills (共 ${stats.total} 个)\n\n`;
|
||||
|
||||
for (const [cat, catSkills] of byCategory) {
|
||||
output += `## ${cat}\n`;
|
||||
for (const skill of catSkills) {
|
||||
output += `- **${skill.name}**: ${skill.description}`;
|
||||
if (skill.source !== 'builtin') {
|
||||
output += ` [${skill.source}]`;
|
||||
}
|
||||
output += '\n';
|
||||
}
|
||||
output += '\n';
|
||||
}
|
||||
|
||||
if (uncategorized.length > 0) {
|
||||
output += '## 其他\n';
|
||||
for (const skill of uncategorized) {
|
||||
output += `- **${skill.name}**: ${skill.description}`;
|
||||
if (skill.source !== 'builtin') {
|
||||
output += ` [${skill.source}]`;
|
||||
}
|
||||
output += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
output += `\n来源统计: 内置 ${stats.bySource.builtin || 0}, 用户 ${stats.bySource.user || 0}, 项目 ${stats.bySource.project || 0}`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
metadata: { stats },
|
||||
};
|
||||
}
|
||||
|
||||
// 按分类筛选
|
||||
if (category) {
|
||||
const skills = registry.getByCategory(category);
|
||||
|
||||
if (skills.length === 0) {
|
||||
const categories = registry.getCategories();
|
||||
return {
|
||||
success: true,
|
||||
output: `分类 "${category}" 下没有 Skills。\n\n可用分类: ${categories.join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
let output = `📁 分类 "${category}" 下的 Skills:\n\n`;
|
||||
for (const skill of skills) {
|
||||
output += `- **${skill.name}**: ${skill.description}\n`;
|
||||
if (skill.parameters) {
|
||||
const paramNames = Object.keys(skill.parameters);
|
||||
output += ` 参数: ${paramNames.join(', ')}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
metadata: { category, count: skills.length },
|
||||
};
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (query) {
|
||||
const results = registry.search(query, 10);
|
||||
|
||||
if (results.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
output: `没有找到匹配 "${query}" 的 Skills。\n\n使用 skill_search({ list_all: true }) 查看所有可用的 Skills。`,
|
||||
};
|
||||
}
|
||||
|
||||
let output = `🔍 搜索 "${query}" 的结果:\n\n`;
|
||||
for (const { skill, score, matchReason } of results) {
|
||||
output += `- **${skill.name}** (${score}分): ${skill.description}\n`;
|
||||
output += ` 匹配: ${matchReason}`;
|
||||
if (skill.category) {
|
||||
output += ` | 分类: ${skill.category}`;
|
||||
}
|
||||
output += '\n';
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
metadata: { query, resultCount: results.length },
|
||||
};
|
||||
}
|
||||
|
||||
// 默认显示统计和分类
|
||||
const stats = registry.getStats();
|
||||
const categories = registry.getCategories();
|
||||
|
||||
let output = `📊 Skills 概览\n\n`;
|
||||
output += `总数: ${stats.total} (启用: ${stats.enabled})\n`;
|
||||
output += `来源: 内置 ${stats.bySource.builtin || 0}, 用户 ${stats.bySource.user || 0}, 项目 ${stats.bySource.project || 0}\n\n`;
|
||||
|
||||
if (categories.length > 0) {
|
||||
output += `可用分类:\n`;
|
||||
for (const cat of categories) {
|
||||
output += `- ${cat} (${stats.byCategory[cat]} 个)\n`;
|
||||
}
|
||||
}
|
||||
|
||||
output += `\n使用方法:\n`;
|
||||
output += `- skill_search({ list_all: true }) - 列出所有 Skills\n`;
|
||||
output += `- skill_search({ query: "关键词" }) - 搜索 Skills\n`;
|
||||
output += `- skill_search({ category: "分类名" }) - 按分类筛选`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
metadata: { stats },
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,193 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { SkillLoader } from '../../../src/skills/loader.js';
|
||||
|
||||
// Mock fs/promises
|
||||
vi.mock('fs/promises', () => ({
|
||||
access: vi.fn(),
|
||||
readdir: vi.fn(),
|
||||
readFile: vi.fn(),
|
||||
}));
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
|
||||
describe('SkillLoader - Skill 加载器', () => {
|
||||
let loader: SkillLoader;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
loader = new SkillLoader();
|
||||
});
|
||||
|
||||
describe('loadFromFile - 从文件加载', () => {
|
||||
it('加载 YAML 格式的 Skill', async () => {
|
||||
const yamlContent = `
|
||||
skill:
|
||||
name: test-skill
|
||||
description: 测试 Skill
|
||||
promptTemplate: "提示模板 {{param}}"
|
||||
parameters:
|
||||
param:
|
||||
type: string
|
||||
required: true
|
||||
description: 参数描述
|
||||
`;
|
||||
|
||||
vi.mocked(fs.readFile).mockResolvedValue(yamlContent);
|
||||
|
||||
const skill = await loader.loadFromFile('/test/skill.yaml', 'user');
|
||||
|
||||
expect(skill).not.toBeNull();
|
||||
expect(skill?.name).toBe('test-skill');
|
||||
expect(skill?.description).toBe('测试 Skill');
|
||||
expect(skill?.source).toBe('user');
|
||||
expect(skill?.parameters?.param.required).toBe(true);
|
||||
});
|
||||
|
||||
it('加载 JSON 格式的 Skill', async () => {
|
||||
const jsonContent = JSON.stringify({
|
||||
skill: {
|
||||
name: 'json-skill',
|
||||
description: 'JSON Skill',
|
||||
promptTemplate: '提示',
|
||||
},
|
||||
});
|
||||
|
||||
vi.mocked(fs.readFile).mockResolvedValue(jsonContent);
|
||||
|
||||
const skill = await loader.loadFromFile('/test/skill.json', 'project');
|
||||
|
||||
expect(skill).not.toBeNull();
|
||||
expect(skill?.name).toBe('json-skill');
|
||||
expect(skill?.source).toBe('project');
|
||||
});
|
||||
|
||||
it('加载 Markdown 格式的 Skill(带 frontmatter)', async () => {
|
||||
const mdContent = `---
|
||||
name: md-skill
|
||||
description: Markdown Skill
|
||||
parameters:
|
||||
code:
|
||||
type: string
|
||||
required: true
|
||||
description: 代码
|
||||
---
|
||||
|
||||
# 代码审查
|
||||
|
||||
请审查以下代码:
|
||||
|
||||
{{code}}
|
||||
`;
|
||||
|
||||
vi.mocked(fs.readFile).mockResolvedValue(mdContent);
|
||||
|
||||
const skill = await loader.loadFromFile('/test/skill.md', 'user');
|
||||
|
||||
expect(skill).not.toBeNull();
|
||||
expect(skill?.name).toBe('md-skill');
|
||||
expect(skill?.description).toBe('Markdown Skill');
|
||||
expect(skill?.promptTemplate).toContain('{{code}}');
|
||||
expect(skill?.promptTemplate).toContain('代码审查');
|
||||
});
|
||||
|
||||
it('加载 Markdown 格式的 Skill(无 frontmatter)', async () => {
|
||||
const mdContent = `# 简单提示
|
||||
|
||||
这是一个简单的提示模板。
|
||||
|
||||
{{input}}
|
||||
`;
|
||||
|
||||
vi.mocked(fs.readFile).mockResolvedValue(mdContent);
|
||||
|
||||
const skill = await loader.loadFromFile('/test/simple.md', 'user');
|
||||
|
||||
expect(skill).not.toBeNull();
|
||||
expect(skill?.name).toBe('simple'); // 从文件名获取
|
||||
expect(skill?.promptTemplate).toContain('简单提示');
|
||||
});
|
||||
|
||||
it('缺少必需字段时返回 null', async () => {
|
||||
const yamlContent = `
|
||||
skill:
|
||||
description: 没有 name 和 promptTemplate
|
||||
`;
|
||||
|
||||
vi.mocked(fs.readFile).mockResolvedValue(yamlContent);
|
||||
|
||||
const skill = await loader.loadFromFile('/test/invalid.yaml', 'user');
|
||||
|
||||
expect(skill).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadFromDirectory - 从目录加载', () => {
|
||||
it('目录不存在时返回空数组', async () => {
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error('Not found'));
|
||||
|
||||
const skills = await loader.loadFromDirectory('/non-existent', 'user');
|
||||
|
||||
expect(skills).toEqual([]);
|
||||
});
|
||||
|
||||
it('加载目录中的所有 Skill 文件', async () => {
|
||||
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.readdir).mockResolvedValue([
|
||||
{ name: 'skill1.yaml', isFile: () => true, isDirectory: () => false },
|
||||
{ name: 'skill2.json', isFile: () => true, isDirectory: () => false },
|
||||
{ name: 'readme.txt', isFile: () => true, isDirectory: () => false }, // 不支持的格式
|
||||
] as any);
|
||||
|
||||
vi.mocked(fs.readFile)
|
||||
.mockResolvedValueOnce(`skill:\n name: skill1\n description: 1\n promptTemplate: t1`)
|
||||
.mockResolvedValueOnce(JSON.stringify({ skill: { name: 'skill2', description: '2', promptTemplate: 't2' } }));
|
||||
|
||||
const skills = await loader.loadFromDirectory('/test', 'user');
|
||||
|
||||
expect(skills.length).toBe(2);
|
||||
expect(skills.map((s) => s.name)).toContain('skill1');
|
||||
expect(skills.map((s) => s.name)).toContain('skill2');
|
||||
});
|
||||
|
||||
it('递归加载子目录', async () => {
|
||||
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.readdir)
|
||||
.mockResolvedValueOnce([
|
||||
{ name: 'subdir', isFile: () => false, isDirectory: () => true },
|
||||
{ name: 'root.yaml', isFile: () => true, isDirectory: () => false },
|
||||
] as any)
|
||||
.mockResolvedValueOnce([
|
||||
{ name: 'sub.yaml', isFile: () => true, isDirectory: () => false },
|
||||
] as any);
|
||||
|
||||
vi.mocked(fs.readFile)
|
||||
.mockResolvedValueOnce(`skill:\n name: root\n description: root\n promptTemplate: root`)
|
||||
.mockResolvedValueOnce(`skill:\n name: sub\n description: sub\n promptTemplate: sub`);
|
||||
|
||||
const skills = await loader.loadFromDirectory('/test', 'user');
|
||||
|
||||
expect(skills.length).toBe(2);
|
||||
expect(skills.map((s) => s.name)).toContain('root');
|
||||
expect(skills.map((s) => s.name)).toContain('sub');
|
||||
});
|
||||
});
|
||||
|
||||
describe('目录路径', () => {
|
||||
it('getUserSkillsDir 返回用户 Skills 目录', () => {
|
||||
const originalHome = process.env.HOME;
|
||||
process.env.HOME = '/home/testuser';
|
||||
|
||||
const dir = loader.getUserSkillsDir();
|
||||
|
||||
expect(dir).toBe('/home/testuser/.config/ai-terminal/skills');
|
||||
|
||||
process.env.HOME = originalHome;
|
||||
});
|
||||
|
||||
it('getProjectSkillsDir 返回项目 Skills 目录', () => {
|
||||
const dir = loader.getProjectSkillsDir('/workspace');
|
||||
|
||||
expect(dir).toBe('/workspace/.ai-terminal/skills');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,407 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import {
|
||||
SkillRegistry,
|
||||
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: 'test-builtin',
|
||||
description: '内置测试 Skill',
|
||||
promptTemplate: '测试提示: {{param1}}',
|
||||
parameters: {
|
||||
param1: { type: 'string', required: true, description: '参数1' },
|
||||
},
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
describe('SkillRegistry - Skill 注册表', () => {
|
||||
let registry: SkillRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
resetSkillRegistry();
|
||||
registry = new SkillRegistry({ autoLoad: false });
|
||||
});
|
||||
|
||||
describe('register - 注册', () => {
|
||||
it('成功注册 Skill', () => {
|
||||
const skill: Skill = {
|
||||
name: 'test-skill',
|
||||
description: '测试 Skill',
|
||||
promptTemplate: '测试提示',
|
||||
source: 'user',
|
||||
};
|
||||
|
||||
registry.register(skill);
|
||||
expect(registry.has('test-skill')).toBe(true);
|
||||
});
|
||||
|
||||
it('高优先级 Skill 可以覆盖低优先级', () => {
|
||||
const builtinSkill: Skill = {
|
||||
name: 'same-name',
|
||||
description: '内置版本',
|
||||
promptTemplate: '内置提示',
|
||||
source: 'builtin',
|
||||
};
|
||||
|
||||
const projectSkill: Skill = {
|
||||
name: 'same-name',
|
||||
description: '项目版本',
|
||||
promptTemplate: '项目提示',
|
||||
source: 'project',
|
||||
};
|
||||
|
||||
registry.register(builtinSkill);
|
||||
registry.register(projectSkill);
|
||||
|
||||
const result = registry.get('same-name');
|
||||
expect(result?.source).toBe('project');
|
||||
});
|
||||
|
||||
it('低优先级 Skill 不能覆盖高优先级', () => {
|
||||
const projectSkill: Skill = {
|
||||
name: 'same-name',
|
||||
description: '项目版本',
|
||||
promptTemplate: '项目提示',
|
||||
source: 'project',
|
||||
};
|
||||
|
||||
const builtinSkill: Skill = {
|
||||
name: 'same-name',
|
||||
description: '内置版本',
|
||||
promptTemplate: '内置提示',
|
||||
source: 'builtin',
|
||||
};
|
||||
|
||||
registry.register(projectSkill);
|
||||
registry.register(builtinSkill);
|
||||
|
||||
const result = registry.get('same-name');
|
||||
expect(result?.source).toBe('project');
|
||||
});
|
||||
});
|
||||
|
||||
describe('get - 获取', () => {
|
||||
it('获取存在的 Skill', () => {
|
||||
const skill: Skill = {
|
||||
name: 'test-skill',
|
||||
description: '测试',
|
||||
promptTemplate: '提示',
|
||||
source: 'user',
|
||||
};
|
||||
|
||||
registry.register(skill);
|
||||
const result = registry.get('test-skill');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.name).toBe('test-skill');
|
||||
});
|
||||
|
||||
it('获取不存在的 Skill 返回 undefined', () => {
|
||||
const result = registry.get('non-existent');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('search - 搜索', () => {
|
||||
beforeEach(() => {
|
||||
const skills: Skill[] = [
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
for (const skill of skills) {
|
||||
registry.register(skill);
|
||||
}
|
||||
});
|
||||
|
||||
it('按名称精确匹配', () => {
|
||||
const results = registry.search('code-review');
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].skill.name).toBe('code-review');
|
||||
expect(results[0].score).toBe(100);
|
||||
});
|
||||
|
||||
it('按名称前缀匹配', () => {
|
||||
const results = registry.search('code');
|
||||
expect(results.length).toBe(2); // code-review, explain-code
|
||||
});
|
||||
|
||||
it('按描述匹配', () => {
|
||||
const results = registry.search('审查');
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].skill.name).toBe('code-review');
|
||||
});
|
||||
|
||||
it('按关键词匹配', () => {
|
||||
const results = registry.search('quality');
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].skill.name).toBe('code-review');
|
||||
});
|
||||
|
||||
it('按分类匹配', () => {
|
||||
const results = registry.search('testing');
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].skill.name).toBe('generate-tests');
|
||||
});
|
||||
|
||||
it('限制结果数量', () => {
|
||||
const results = registry.search('code', 1);
|
||||
expect(results.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderPrompt - 渲染模板', () => {
|
||||
it('成功渲染模板', () => {
|
||||
const skill: Skill = {
|
||||
name: 'test',
|
||||
description: '测试',
|
||||
promptTemplate: '你好 {{name}},请{{action}}',
|
||||
source: 'user',
|
||||
};
|
||||
|
||||
const result = registry.renderPrompt(skill, {
|
||||
name: '世界',
|
||||
action: '测试',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.prompt).toBe('你好 世界,请测试');
|
||||
});
|
||||
|
||||
it('缺少必需参数时返回错误', () => {
|
||||
const skill: Skill = {
|
||||
name: 'test',
|
||||
description: '测试',
|
||||
promptTemplate: '{{param}}',
|
||||
parameters: {
|
||||
param: { type: 'string', required: true, description: '必需参数' },
|
||||
},
|
||||
source: 'user',
|
||||
};
|
||||
|
||||
const result = registry.renderPrompt(skill, {});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('缺少必需参数');
|
||||
});
|
||||
|
||||
it('使用默认值', () => {
|
||||
const skill: Skill = {
|
||||
name: 'test',
|
||||
description: '测试',
|
||||
promptTemplate: '值: {{param}}',
|
||||
parameters: {
|
||||
param: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
default: '默认值',
|
||||
description: '可选参数',
|
||||
},
|
||||
},
|
||||
source: 'user',
|
||||
};
|
||||
|
||||
const result = registry.renderPrompt(skill, {});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.prompt).toBe('值: 默认值');
|
||||
});
|
||||
|
||||
it('保留未匹配的变量', () => {
|
||||
const skill: Skill = {
|
||||
name: 'test',
|
||||
description: '测试',
|
||||
promptTemplate: '{{known}} 和 {{unknown}}',
|
||||
source: 'user',
|
||||
};
|
||||
|
||||
const result = registry.renderPrompt(skill, { known: '已知' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.prompt).toBe('已知 和 {{unknown}}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute - 执行', () => {
|
||||
it('成功执行 Skill', () => {
|
||||
const skill: Skill = {
|
||||
name: 'test',
|
||||
description: '测试',
|
||||
promptTemplate: '提示: {{param}}',
|
||||
source: 'user',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
registry.register(skill);
|
||||
const result = registry.execute('test', { param: '值' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.prompt).toBe('提示: 值');
|
||||
});
|
||||
|
||||
it('Skill 不存在时返回错误', () => {
|
||||
const result = registry.execute('non-existent', {});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('不存在');
|
||||
});
|
||||
|
||||
it('Skill 禁用时返回错误', () => {
|
||||
const skill: Skill = {
|
||||
name: 'disabled',
|
||||
description: '禁用的 Skill',
|
||||
promptTemplate: '提示',
|
||||
source: 'user',
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
registry.register(skill);
|
||||
const result = registry.execute('disabled', {});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('禁用');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getByCategory - 按分类获取', () => {
|
||||
it('返回指定分类的 Skills', () => {
|
||||
const skills: Skill[] = [
|
||||
{
|
||||
name: 'skill1',
|
||||
description: '1',
|
||||
promptTemplate: '1',
|
||||
category: 'dev',
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'skill2',
|
||||
description: '2',
|
||||
promptTemplate: '2',
|
||||
category: 'dev',
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'skill3',
|
||||
description: '3',
|
||||
promptTemplate: '3',
|
||||
category: 'test',
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
for (const skill of skills) {
|
||||
registry.register(skill);
|
||||
}
|
||||
|
||||
const devSkills = registry.getByCategory('dev');
|
||||
expect(devSkills.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats - 统计信息', () => {
|
||||
it('返回正确的统计信息', () => {
|
||||
const skills: Skill[] = [
|
||||
{
|
||||
name: 'builtin1',
|
||||
description: '1',
|
||||
promptTemplate: '1',
|
||||
category: 'dev',
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'user1',
|
||||
description: '2',
|
||||
promptTemplate: '2',
|
||||
category: 'dev',
|
||||
source: 'user',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'disabled1',
|
||||
description: '3',
|
||||
promptTemplate: '3',
|
||||
source: 'builtin',
|
||||
enabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
for (const skill of skills) {
|
||||
registry.register(skill);
|
||||
}
|
||||
|
||||
const stats = registry.getStats();
|
||||
|
||||
expect(stats.total).toBe(3);
|
||||
expect(stats.enabled).toBe(2);
|
||||
expect(stats.bySource.builtin).toBe(2);
|
||||
expect(stats.bySource.user).toBe(1);
|
||||
expect(stats.byCategory.dev).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSkillRegistry - 全局注册表', () => {
|
||||
beforeEach(() => {
|
||||
resetSkillRegistry();
|
||||
});
|
||||
|
||||
it('返回单例实例', () => {
|
||||
const registry1 = getSkillRegistry();
|
||||
const registry2 = getSkillRegistry();
|
||||
|
||||
expect(registry1).toBe(registry2);
|
||||
});
|
||||
|
||||
it('resetSkillRegistry 重置实例', () => {
|
||||
const registry1 = getSkillRegistry();
|
||||
resetSkillRegistry();
|
||||
const registry2 = getSkillRegistry();
|
||||
|
||||
expect(registry1).not.toBe(registry2);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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('使用方法');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user