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
+16
View File
@@ -23,6 +23,7 @@
"vscode-jsonrpc": "^8.2.1", "vscode-jsonrpc": "^8.2.1",
"vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-protocol": "^3.17.5",
"web-tree-sitter": "^0.25.10", "web-tree-sitter": "^0.25.10",
"yaml": "^2.8.2",
"zod": "^4.1.13" "zod": "^4.1.13"
}, },
"bin": { "bin": {
@@ -3661,6 +3662,21 @@
"node": ">=8" "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": { "node_modules/yoctocolors-cjs": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
+1
View File
@@ -40,6 +40,7 @@
"vscode-jsonrpc": "^8.2.1", "vscode-jsonrpc": "^8.2.1",
"vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-protocol": "^3.17.5",
"web-tree-sitter": "^0.25.10", "web-tree-sitter": "^0.25.10",
"yaml": "^2.8.2",
"zod": "^4.1.13" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
+357
View File
@@ -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,
];
+29
View File
@@ -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';
+201
View File
@@ -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();
+358
View File
@@ -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;
}
+109
View File
@@ -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;
}
+8
View File
@@ -11,6 +11,9 @@ import { todoReadTool, todoWriteTool } from './todo/index.js';
// Task 工具(Agent 子任务) // Task 工具(Agent 子任务)
import { taskTool, agentOutputTool } from './task/index.js'; import { taskTool, agentOutputTool } from './task/index.js';
// Skill 工具
import { skillTool, skillSearchTool } from './skill/index.js';
// 文件系统工具 // 文件系统工具
import { import {
readFileTool, readFileTool,
@@ -53,6 +56,10 @@ const allToolsWithMetadata: ToolWithMetadata[] = [
taskTool, taskTool,
agentOutputTool, agentOutputTool,
// Skill 工具 (deferLoading: false)
skillTool,
skillSearchTool,
// 文件系统工具 (deferLoading: true) // 文件系统工具 (deferLoading: true)
readFileTool, readFileTool,
writeFileTool, writeFileTool,
@@ -91,6 +98,7 @@ export { toolRegistry } from './registry.js';
export { toolSearchTool } from './tool-search.js'; export { toolSearchTool } from './tool-search.js';
export { todoManager } from './todo/index.js'; export { todoManager } from './todo/index.js';
export { initTaskContext, updateTaskDescription } from './task/index.js'; export { initTaskContext, updateTaskDescription } from './task/index.js';
export { updateSkillDescription } from './skill/index.js';
export type { ToolWithMetadata, ToolMetadata, ToolCategory, ToolSearchResult } from './types.js'; export type { ToolWithMetadata, ToolMetadata, ToolCategory, ToolSearchResult } from './types.js';
// 兼容旧代码:导出所有工具数组(基础 Tool 类型) // 兼容旧代码:导出所有工具数组(基础 Tool 类型)
+2
View File
@@ -0,0 +1,2 @@
export { skillTool, updateSkillDescription } from './skill.js';
export { skillSearchTool } from './skill_search.js';
+147
View File
@@ -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();
}
+192
View File
@@ -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 },
};
},
};
+193
View File
@@ -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');
});
});
});
+407
View File
@@ -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);
});
});
+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('使用方法');
});
});
});