Skip to content

Commit af6f40f

Browse files
author
huzijie.sea
committed
feat(skills): 实现完整的技能管理系统
- 新增技能注册表,支持用户级、项目级和内置技能 - 添加技能创建工具 skill-creator - 支持通过斜杠命令调用用户可调用的技能 - 扩展技能元数据以兼容 Claude Code 规范 - 改进技能管理界面显示详细信息 - 优化斜杠命令建议和补全逻辑
1 parent f2588b6 commit af6f40f

15 files changed

Lines changed: 751 additions & 107 deletions

File tree

.blade/settings.local.json

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,26 @@
55
"Bash(git status)",
66
"Bash(git diff *)",
77
"Bash(ls -l)",
8-
"Write(**/*.md)"
8+
"Write(**/*.md)",
9+
"Bash(ls -la *)",
10+
"Bash(bun install *)",
11+
"Skill",
12+
"Write(**/*.py)",
13+
"Bash(python test_echovic_website.py)",
14+
"Bash(pip install *)",
15+
"Bash(playwright install *)",
16+
"Bash(python test_echovic_comprehensive.py)",
17+
"Bash(curl -s *)",
18+
"Bash(curl -I *)",
19+
"Edit(**/*.py)",
20+
"Bash(python -c *)",
21+
"Bash(python test_echovic_debug.py)",
22+
"Bash(python test_echovic_fixed.py)",
23+
"Bash(python test_echovic_wait.py)",
24+
"Bash(python test_echovic_basic.py)",
25+
"Bash(python test_echovic_individual.py)",
26+
"Bash(python test_echovic_analysis.py)",
27+
"Bash(python3 -c *)"
928
],
1029
"ask": [],
1130
"deny": []

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@
7474
"Bash(gtimeout 3 npm run start:*)",
7575
"WebFetch(domain:leehanchung.github.io)",
7676
"WebFetch(domain:simonwillison.net)",
77-
"WebFetch(domain:scottspence.com)"
77+
"WebFetch(domain:scottspence.com)",
78+
"WebFetch(domain:docs.anthropic.com)"
7879
],
7980
"deny": [],
8081
"ask": [],

src/skills/SkillLoader.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,23 @@ const MAX_DESCRIPTION_LENGTH = 1024;
2121

2222
/**
2323
* 解析 SKILL.md 的 YAML 前置数据
24+
* 完全对齐 Claude Code Skills 规范
2425
*/
2526
interface RawFrontmatter {
2627
name?: string;
2728
description?: string;
2829
'allowed-tools'?: string | string[];
2930
version?: string;
31+
/** 参数提示,如 '<file_path>' */
32+
'argument-hint'?: string;
33+
/** 是否支持 /skill-name 调用 */
34+
'user-invocable'?: boolean | string;
35+
/** 是否禁止 AI 自动调用 */
36+
'disable-model-invocation'?: boolean | string;
37+
/** 指定模型 */
38+
model?: string;
39+
/** 额外触发条件 */
40+
when_to_use?: string;
3041
}
3142

3243
/**
@@ -50,6 +61,20 @@ function parseAllowedTools(raw: string | string[] | undefined): string[] | undef
5061
return undefined;
5162
}
5263

64+
/**
65+
* 解析布尔值字段(支持 true/false 字符串)
66+
*/
67+
function parseBoolean(value: boolean | string | undefined): boolean | undefined {
68+
if (value === undefined) return undefined;
69+
if (typeof value === 'boolean') return value;
70+
if (typeof value === 'string') {
71+
const lower = value.toLowerCase().trim();
72+
if (lower === 'true' || lower === 'yes' || lower === '1') return true;
73+
if (lower === 'false' || lower === 'no' || lower === '0') return false;
74+
}
75+
return undefined;
76+
}
77+
5378
/**
5479
* 验证 Skill 元数据
5580
*/
@@ -79,13 +104,26 @@ function validateMetadata(
79104
};
80105
}
81106

107+
// 解析 model 字段
108+
let model: string | undefined;
109+
if (frontmatter.model) {
110+
// 'inherit' 表示继承当前模型,其他值为具体模型名
111+
model = frontmatter.model === 'inherit' ? 'inherit' : frontmatter.model;
112+
}
113+
82114
return {
83115
valid: true,
84116
metadata: {
85117
name: frontmatter.name,
86118
description: frontmatter.description.trim(),
87119
allowedTools: parseAllowedTools(frontmatter['allowed-tools']),
88120
version: frontmatter.version,
121+
// 新增字段
122+
argumentHint: frontmatter['argument-hint']?.trim(),
123+
userInvocable: parseBoolean(frontmatter['user-invocable']),
124+
disableModelInvocation: parseBoolean(frontmatter['disable-model-invocation']),
125+
model,
126+
whenToUse: frontmatter.when_to_use?.trim(),
89127
},
90128
};
91129
}
@@ -96,7 +134,7 @@ function validateMetadata(
96134
export function parseSkillContent(
97135
content: string,
98136
filePath: string,
99-
source: 'user' | 'project'
137+
source: 'user' | 'project' | 'builtin'
100138
): SkillParseResult {
101139
// 匹配 YAML 前置数据
102140
const match = content.match(FRONTMATTER_REGEX);
@@ -150,7 +188,7 @@ export function parseSkillContent(
150188
*/
151189
export async function loadSkillMetadata(
152190
filePath: string,
153-
source: 'user' | 'project'
191+
source: 'user' | 'project' | 'builtin'
154192
): Promise<SkillParseResult> {
155193
try {
156194
const content = await fs.readFile(filePath, 'utf-8');

src/skills/SkillRegistry.ts

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import * as fs from 'node:fs/promises';
99
import * as path from 'node:path';
1010
import { homedir } from 'node:os';
11+
import { getSkillCreatorContent, skillCreatorMetadata } from './builtin/skill-creator.js';
1112
import { hasSkillFile, loadSkillContent, loadSkillMetadata } from './SkillLoader.js';
1213
import type {
1314
SkillContent,
@@ -22,6 +23,9 @@ import type {
2223
const DEFAULT_CONFIG: Required<SkillRegistryConfig> = {
2324
userSkillsDir: path.join(homedir(), '.blade', 'skills'),
2425
projectSkillsDir: '.blade/skills',
26+
// Claude Code 兼容路径
27+
claudeUserSkillsDir: path.join(homedir(), '.claude', 'skills'),
28+
claudeProjectSkillsDir: '.claude/skills',
2529
cwd: process.cwd(),
2630
};
2731

@@ -61,6 +65,13 @@ export class SkillRegistry {
6165

6266
/**
6367
* 初始化注册表,扫描所有 skills 目录
68+
*
69+
* 优先级(后加载的覆盖先加载的):
70+
* 1. 内置 Skills(builtin)
71+
* 2. Claude Code 用户级 Skills(~/.claude/skills/)
72+
* 3. Blade 用户级 Skills(~/.blade/skills/)
73+
* 4. Claude Code 项目级 Skills(.claude/skills/)
74+
* 5. Blade 项目级 Skills(.blade/skills/)- 优先级最高
6475
*/
6576
async initialize(): Promise<SkillDiscoveryResult> {
6677
if (this.initialized) {
@@ -73,12 +84,28 @@ export class SkillRegistry {
7384
const errors: SkillDiscoveryResult['errors'] = [];
7485
const discoveredSkills: SkillMetadata[] = [];
7586

76-
// 扫描用户级 skills(优先级 1)
87+
// 1. 加载内置 Skills(优先级最低,可被覆盖)
88+
this.loadBuiltinSkills();
89+
90+
// 2. 扫描 Claude Code 用户级 skills(~/.claude/skills/)
91+
const claudeUserResult = await this.scanDirectory(this.config.claudeUserSkillsDir, 'user');
92+
discoveredSkills.push(...claudeUserResult.skills);
93+
errors.push(...claudeUserResult.errors);
94+
95+
// 3. 扫描 Blade 用户级 skills(~/.blade/skills/)
7796
const userResult = await this.scanDirectory(this.config.userSkillsDir, 'user');
7897
discoveredSkills.push(...userResult.skills);
7998
errors.push(...userResult.errors);
8099

81-
// 扫描项目级 skills(优先级 2,可覆盖用户级同名 skill)
100+
// 4. 扫描 Claude Code 项目级 skills(.claude/skills/)
101+
const claudeProjectDir = path.isAbsolute(this.config.claudeProjectSkillsDir)
102+
? this.config.claudeProjectSkillsDir
103+
: path.join(this.config.cwd, this.config.claudeProjectSkillsDir);
104+
const claudeProjectResult = await this.scanDirectory(claudeProjectDir, 'project');
105+
discoveredSkills.push(...claudeProjectResult.skills);
106+
errors.push(...claudeProjectResult.errors);
107+
108+
// 5. 扫描 Blade 项目级 skills(.blade/skills/)- 优先级最高
82109
const projectDir = path.isAbsolute(this.config.projectSkillsDir)
83110
? this.config.projectSkillsDir
84111
: path.join(this.config.cwd, this.config.projectSkillsDir);
@@ -99,6 +126,14 @@ export class SkillRegistry {
99126
};
100127
}
101128

129+
/**
130+
* 加载内置 Skills
131+
*/
132+
private loadBuiltinSkills(): void {
133+
// 注册 skill-creator
134+
this.skills.set(skillCreatorMetadata.name, skillCreatorMetadata);
135+
}
136+
102137
/**
103138
* 扫描指定目录下的所有 skills
104139
*/
@@ -177,24 +212,71 @@ export class SkillRegistry {
177212
async loadContent(name: string): Promise<SkillContent | null> {
178213
const metadata = this.skills.get(name);
179214
if (!metadata) return null;
215+
216+
// 内置 Skill 直接返回内容
217+
if (metadata.source === 'builtin') {
218+
return this.loadBuiltinContent(name);
219+
}
220+
221+
// 文件系统 Skill 从文件加载
180222
return loadSkillContent(metadata);
181223
}
182224

225+
/**
226+
* 加载内置 Skill 的完整内容
227+
*/
228+
private loadBuiltinContent(name: string): SkillContent | null {
229+
switch (name) {
230+
case 'skill-creator':
231+
return getSkillCreatorContent();
232+
default:
233+
return null;
234+
}
235+
}
236+
237+
/**
238+
* 获取可被 AI 自动调用的 Skills(Model-invoked)
239+
* 排除设置了 disable-model-invocation: true 的 Skills
240+
*/
241+
getModelInvocableSkills(): SkillMetadata[] {
242+
return Array.from(this.skills.values()).filter(
243+
(skill) => !skill.disableModelInvocation
244+
);
245+
}
246+
247+
/**
248+
* 获取可通过 /skill-name 命令调用的 Skills(User-invoked)
249+
* 仅包含设置了 user-invocable: true 的 Skills
250+
*/
251+
getUserInvocableSkills(): SkillMetadata[] {
252+
return Array.from(this.skills.values()).filter(
253+
(skill) => skill.userInvocable === true
254+
);
255+
}
256+
183257
/**
184258
* 生成 <available_skills> 列表内容
185-
* 格式:每个 skill 一行 `- name: description`
259+
* 格式:每个 skill 一行 `- name [argument-hint]: description`
260+
* 仅包含可被 AI 自动调用的 Skills
186261
*/
187262
generateAvailableSkillsList(): string {
188-
if (this.skills.size === 0) {
263+
const modelInvocableSkills = this.getModelInvocableSkills();
264+
if (modelInvocableSkills.length === 0) {
189265
return '';
190266
}
191267

192268
const lines: string[] = [];
193-
for (const skill of this.skills.values()) {
269+
for (const skill of modelInvocableSkills) {
194270
// 截断过长的描述,保持列表简洁
195271
const desc =
196272
skill.description.length > 100 ? `${skill.description.substring(0, 97)}...` : skill.description;
197-
lines.push(`- ${skill.name}: ${desc}`);
273+
274+
// 如果有 argument-hint,添加到名称后面
275+
const nameWithHint = skill.argumentHint
276+
? `${skill.name} ${skill.argumentHint}`
277+
: skill.name;
278+
279+
lines.push(`- ${nameWithHint}: ${desc}`);
198280
}
199281

200282
return lines.join('\n');

0 commit comments

Comments
 (0)