423 lines
14 KiB
Markdown
423 lines
14 KiB
Markdown
|
|
# P2: Agent Skills 标准兼容 + References 加载 实施计划
|
|||
|
|
|
|||
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|||
|
|
|
|||
|
|
**Goal:** 兼容 Agent Skills 社区标准(名称验证、hidden skill、字段映射),改造 prompt 索引区分三种 skill 类型,改造 read_skill 返回格式强制 Agent 读取 required references。
|
|||
|
|
|
|||
|
|
**Architecture:** 新增 `validateSkillName()` 验证函数。`buildSkillsPrompt()` 输出增加 type 属性和 compute-entry 的 interface 摘要。`read_skill` 工具返回区分 `<skill_required_references>` 和 `<skill_optional_references>`。基础设施文件从 collectFiles 中过滤。
|
|||
|
|
|
|||
|
|
**Tech Stack:** TypeBox, XML 模板, Midway.js
|
|||
|
|
|
|||
|
|
**Spec:** `docs/superpowers/specs/2026-04-27-skill-system-evolution-design.md` Section 5 + 10.5-10.7 + 10.10
|
|||
|
|
|
|||
|
|
**Depends on:** P1 (SkillConfigService, SkillClassification must be integrated into SkillLoaderService before P2 Task 2 can work. Execute P1 Tasks 1-2 first.)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 1: 名称验证函数(无依赖,可独立执行)
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `packages/backend/src/modules/netaclaw/service/skill_loader.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 在 skill_loader.ts 顶部新增 validateSkillName 函数**
|
|||
|
|
|
|||
|
|
在 class 定义之前添加:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const MAX_NAME_LENGTH = 64;
|
|||
|
|
|
|||
|
|
export function validateSkillName(name: string, parentDirName?: string): string[] {
|
|||
|
|
const errors: string[] = [];
|
|||
|
|
if (!name) { errors.push('name is required'); return errors; }
|
|||
|
|
if (name.length > MAX_NAME_LENGTH) errors.push(`name exceeds ${MAX_NAME_LENGTH} characters`);
|
|||
|
|
if (!/^[a-z0-9-]+$/.test(name)) errors.push('name must be lowercase a-z, 0-9, hyphens only');
|
|||
|
|
if (name.startsWith('-') || name.endsWith('-')) errors.push('name must not start or end with hyphen');
|
|||
|
|
if (name.includes('--')) errors.push('name must not contain consecutive hyphens');
|
|||
|
|
if (parentDirName && name !== parentDirName) errors.push(`name "${name}" does not match directory "${parentDirName}"`);
|
|||
|
|
return errors;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 在 parseSkillMd 中调用验证(warning 级别,不阻塞加载)**
|
|||
|
|
|
|||
|
|
在 `parseSkillMd` 方法中,解析出 name 后添加:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const nameErrors = validateSkillName(name);
|
|||
|
|
if (nameErrors.length > 0) {
|
|||
|
|
this.logger.warn('[SkillLoader] Skill "%s" name validation: %s', name, nameErrors.join(', '));
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 在 skill_manage.ts 和 skill_installer.ts 的创建/安装流程中调用验证(error 级别,阻塞)**
|
|||
|
|
|
|||
|
|
在 `skill_manage.ts` 的 `execute` 方法中,`create` 分支的 `parseSkillMd` 之后添加:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const nameErrors = validateSkillName(parsed.name, params.name);
|
|||
|
|
if (nameErrors.length > 0) {
|
|||
|
|
return textResult(`名称不合规: ${nameErrors.join('; ')}`);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
在 `skill_installer.ts` 的 `installFromGitHub` 和 `installFromZip` 中,`parseSkillMd` 之后添加类似校验。
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add packages/backend/src/modules/netaclaw/service/skill_loader.ts \
|
|||
|
|
packages/backend/src/modules/netaclaw/tools/builtin/skill_manage.ts \
|
|||
|
|
packages/backend/src/modules/netaclaw/service/skill_installer.ts
|
|||
|
|
git commit -m "feat(skill): add Agent Skills standard name validation"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 2: buildSkillsPrompt 输出改造
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `packages/backend/src/modules/netaclaw/service/skill_loader.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 改造 buildSkillsPrompt 方法**
|
|||
|
|
|
|||
|
|
替换现有 `buildSkillsPrompt` 方法:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
buildSkillsPrompt(skillNames: string[], availableTools: string[]): string {
|
|||
|
|
const filtered = this.filterByConditions(skillNames, availableTools);
|
|||
|
|
if (filtered.length === 0) return '';
|
|||
|
|
|
|||
|
|
const lines: string[] = [];
|
|||
|
|
let totalChars = 0;
|
|||
|
|
|
|||
|
|
for (const s of filtered) {
|
|||
|
|
// 跳过 hidden skill
|
|||
|
|
const fm = s.metadata as any;
|
|||
|
|
if (fm?.['disable-model-invocation'] === true) continue;
|
|||
|
|
|
|||
|
|
const classification = this.skillConfig.classify(s.name);
|
|||
|
|
const category = fm?.category || '通用';
|
|||
|
|
const tags = Array.isArray(fm?.tags) ? fm.tags.join(',') : '';
|
|||
|
|
|
|||
|
|
let body = ` ${s.description}`;
|
|||
|
|
|
|||
|
|
// compute-entry: 附带 interface.input 摘要
|
|||
|
|
if (classification === 'compute-entry') {
|
|||
|
|
const config = this.skillConfig.getConfig(s.name);
|
|||
|
|
if (config?.interface?.input) {
|
|||
|
|
const inputDesc = Object.entries(config.interface.input)
|
|||
|
|
.map(([k, v]) => `${k}(${v.type}${v.required ? ',必填' : ''}${v.default ? ',默认' + v.default : ''})`)
|
|||
|
|
.join(', ');
|
|||
|
|
body += `\n 输入: ${inputDesc}`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const line = ` <skill name="${s.name}" type="${classification}" category="${category}" tags="${tags}">\n${body}\n </skill>`;
|
|||
|
|
if (totalChars + line.length > SkillLoaderService.MAX_SKILLS_PROMPT_CHARS) break;
|
|||
|
|
lines.push(line);
|
|||
|
|
totalChars += line.length;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (lines.length === 0) return '';
|
|||
|
|
|
|||
|
|
return `\n\n## 技能(必须扫描)\n回复前,扫描以下技能列表。prompt 类型用 read_skill 加载指令,compute-entry 类型用 execute_skill 直接调用,compute-toolkit 类型用 read_skill 加载指令后通过 bash 执行。\n读取 skill 后,如果返回中包含 <skill_required_references>,你必须在执行任何操作前先用 read_skill_file 逐一读取列出的所有文档。这不是建议,是强制要求。\n\n<available_skills>\n${lines.join('\n')}\n</available_skills>`;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 验证编译通过**
|
|||
|
|
|
|||
|
|
Run: `cd packages/backend && npx tsc --noEmit`
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add packages/backend/src/modules/netaclaw/service/skill_loader.ts
|
|||
|
|
git commit -m "feat(skill): enhance buildSkillsPrompt with type classification and required references guidance"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 3: read_skill 返回格式改造
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `packages/backend/src/modules/netaclaw/tools/builtin/read_skill.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 改造 read_skill 的 execute 方法**
|
|||
|
|
|
|||
|
|
替换现有 `execute` 方法体:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
async execute(_id, params) {
|
|||
|
|
const skill = skillLoader.getSkill(params.name);
|
|||
|
|
if (!skill) {
|
|||
|
|
return textResult(`未找到名为 "${params.name}" 的 skill`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let result = skill.content;
|
|||
|
|
|
|||
|
|
// 获取 references 配置
|
|||
|
|
const config = skillLoader.getSkillConfig(params.name);
|
|||
|
|
const configRefs = config?.references;
|
|||
|
|
const fmRefs = (skill.metadata as any)?.references;
|
|||
|
|
const refs = configRefs || fmRefs || null;
|
|||
|
|
|
|||
|
|
const requiredRefs: string[] = refs?.required || [];
|
|||
|
|
const allFiles = skill.files || [];
|
|||
|
|
|
|||
|
|
// 基础设施文件过滤
|
|||
|
|
const INFRA_FILES = new Set([
|
|||
|
|
'skill.config.yaml', 'requirements.txt', 'package.json',
|
|||
|
|
'package-lock.json', 'tsconfig.json', '.env',
|
|||
|
|
]);
|
|||
|
|
const referenceFiles = allFiles.filter(f => !INFRA_FILES.has(f) && !requiredRefs.includes(f));
|
|||
|
|
|
|||
|
|
if (requiredRefs.length > 0) {
|
|||
|
|
result += `\n\n<skill_required_references>`;
|
|||
|
|
result += `\n⚠️ 执行此 skill 的任务前,你必须先用 read_skill_file 读取以下文档:`;
|
|||
|
|
for (const ref of requiredRefs) {
|
|||
|
|
result += `\n- ${ref}`;
|
|||
|
|
}
|
|||
|
|
result += `\n未读取这些文档就执行操作会导致错误。`;
|
|||
|
|
result += `\n</skill_required_references>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (referenceFiles.length > 0) {
|
|||
|
|
result += `\n\n<skill_optional_references>`;
|
|||
|
|
result += `\n以下文档可按需读取(用 read_skill_file 工具):`;
|
|||
|
|
for (const ref of referenceFiles) {
|
|||
|
|
result += `\n- ${ref}`;
|
|||
|
|
}
|
|||
|
|
result += `\n</skill_optional_references>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return textResult(result);
|
|||
|
|
},
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 验证编译通过**
|
|||
|
|
|
|||
|
|
Run: `cd packages/backend && npx tsc --noEmit`
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add packages/backend/src/modules/netaclaw/tools/builtin/read_skill.ts
|
|||
|
|
git commit -m "feat(skill): enhance read_skill with required/optional references separation"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 4: collectFiles 基础设施文件过滤
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `packages/backend/src/modules/netaclaw/service/skill_loader.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 在 collectFiles 方法中过滤基础设施文件**
|
|||
|
|
|
|||
|
|
在 `collectFiles` 方法的 `else if (entry.name !== 'SKILL.md')` 条件中,增加过滤:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const INFRA_FILES = new Set([
|
|||
|
|
'skill.config.yaml', 'requirements.txt', 'package.json',
|
|||
|
|
'package-lock.json', 'tsconfig.json', '.env',
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
// 在 else if 分支中
|
|||
|
|
} else if (entry.name !== 'SKILL.md' && !INFRA_FILES.has(entry.name)) {
|
|||
|
|
const rel = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|||
|
|
results.push(rel);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add packages/backend/src/modules/netaclaw/service/skill_loader.ts
|
|||
|
|
git commit -m "feat(skill): filter infrastructure files from skill file listing"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 5: read_skill task 参数与 routes 匹配(Spec 10.10)
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `packages/backend/src/modules/netaclaw/tools/builtin/read_skill.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 给 ReadSkillParams 新增可选 task 参数**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const ReadSkillParams = Type.Object({
|
|||
|
|
name: Type.String({ description: '要读取的 skill 名称' }),
|
|||
|
|
task: Type.Optional(Type.String({ description: '当前任务描述,用于自动匹配需要读取的文档' })),
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 在 execute 方法中实现 routes 匹配**
|
|||
|
|
|
|||
|
|
在获取 `requiredRefs` 之后、构建返回结果之前,添加 routes 匹配逻辑:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// routes 匹配:如果传入了 task 且 config 有 routes,尝试关键词匹配
|
|||
|
|
let routeMatchedRefs: string[] = [];
|
|||
|
|
if (params.task && refs?.routes) {
|
|||
|
|
const taskLower = params.task.toLowerCase();
|
|||
|
|
for (const route of refs.routes) {
|
|||
|
|
if (route.match.some(keyword => taskLower.includes(keyword.toLowerCase()))) {
|
|||
|
|
routeMatchedRefs.push(...route.required_refs);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
routeMatchedRefs = [...new Set(routeMatchedRefs)]; // 去重
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 合并 required + route matched
|
|||
|
|
const allRequired = [...new Set([...requiredRefs, ...routeMatchedRefs])];
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
如果 `routeMatchedRefs` 命中了文档,尝试直接读取并拼接到返回结果中(减少 Agent 二次调用):
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
if (routeMatchedRefs.length > 0) {
|
|||
|
|
result += `\n\n<skill_route_matched_references>`;
|
|||
|
|
for (const ref of routeMatchedRefs) {
|
|||
|
|
const filePath = skillLoader.getSkillFilePath(params.name, ref);
|
|||
|
|
if (filePath) {
|
|||
|
|
try {
|
|||
|
|
const content = await fs.readFile(filePath, 'utf-8');
|
|||
|
|
result += `\n\n--- ${ref} ---\n${content}`;
|
|||
|
|
} catch { /* skip unreadable */ }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
result += `\n</skill_route_matched_references>`;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add packages/backend/src/modules/netaclaw/tools/builtin/read_skill.ts
|
|||
|
|
git commit -m "feat(skill): add task parameter and routes matching to read_skill tool"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 6: Agent 分配 skill 时的类型校验(Spec 10.6)
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `packages/frontend/src/modules/agent/views/agent-edit.vue`
|
|||
|
|
- Modify: `packages/backend/src/modules/netaclaw/controller/agent.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: metas 端点返回 classification 字段**
|
|||
|
|
|
|||
|
|
在 `SkillLoaderService.getSkillMetas()` 中,返回对象添加(如果 P3 Task 5 尚未添加):
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
classification: this.skillConfig.classify(fs.name),
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 前端 skill 选择器显示分类标签**
|
|||
|
|
|
|||
|
|
在 `agent-edit.vue` 的可选 Skill 列表项中(约 line 38),在现有 `skillType` 标签旁新增分类标签:
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<el-tag v-if="sk.classification" size="small"
|
|||
|
|
:type="sk.classification === 'compute-entry' ? 'success' : sk.classification === 'compute-toolkit' ? '' : 'info'">
|
|||
|
|
{{ sk.classification }}
|
|||
|
|
</el-tag>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
已选择列表中同样添加:
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<span class="item-main">{{ getSkillLabel(name) }}</span>
|
|||
|
|
<el-tag size="small" :type="getSkillClassification(name) === 'compute-entry' ? 'success' : 'info'">
|
|||
|
|
{{ getSkillClassification(name) }}
|
|||
|
|
</el-tag>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
新增辅助函数:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
function getSkillClassification(name: string): string {
|
|||
|
|
const meta = skillMetas.value.find((s) => s.name === name);
|
|||
|
|
return meta?.classification || 'prompt';
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 前端选择 compute skill 时显示工具权限 warning**
|
|||
|
|
|
|||
|
|
在 `addSkill` 函数中添加校验:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
function addSkill(name: string) {
|
|||
|
|
if (!form.value.skills.includes(name)) {
|
|||
|
|
form.value.skills.push(name);
|
|||
|
|
// 校验工具权限
|
|||
|
|
const classification = getSkillClassification(name);
|
|||
|
|
if (classification === 'compute-entry') {
|
|||
|
|
ElMessage.warning(`"${name}" 是 compute-entry 类型,请确保该 Agent 已启用 execute_skill 工具`);
|
|||
|
|
} else if (classification === 'compute-toolkit') {
|
|||
|
|
ElMessage.warning(`"${name}" 是 compute-toolkit 类型,请确保该 Agent 已启用 bash 工具`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 后端保存时返回 warnings**
|
|||
|
|
|
|||
|
|
在 `controller/agent.ts` 的 `update` 方法中,注入 `SkillLoaderService`,保存前校验:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
@Inject()
|
|||
|
|
skillLoader: SkillLoaderService;
|
|||
|
|
|
|||
|
|
@Post('/update')
|
|||
|
|
async update(@Body() body: any) {
|
|||
|
|
const warnings: string[] = [];
|
|||
|
|
if (body.skills?.length) {
|
|||
|
|
for (const skillName of body.skills) {
|
|||
|
|
const classification = this.skillLoader.getSkillClassification(skillName);
|
|||
|
|
if (classification === 'compute-entry') {
|
|||
|
|
warnings.push(`Skill "${skillName}" 需要 execute_skill 工具`);
|
|||
|
|
} else if (classification === 'compute-toolkit') {
|
|||
|
|
warnings.push(`Skill "${skillName}" 需要 bash 工具`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
await this.agentService.update(body);
|
|||
|
|
return { code: 1000, message: 'success', warnings };
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add packages/frontend/src/modules/agent/views/agent-edit.vue \
|
|||
|
|
packages/backend/src/modules/netaclaw/controller/agent.ts
|
|||
|
|
git commit -m "feat(skill): add skill classification display and tool permission warnings in agent editor"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 7: skill_context.ts 废弃标记(Spec 10.12)
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `packages/backend/src/modules/netaclaw/service/skill_context.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 添加 @deprecated 注释**
|
|||
|
|
|
|||
|
|
在 `buildSkillContext` 函数上方添加:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
/**
|
|||
|
|
* @deprecated 使用 tool_resolver.ts 中的 skill 工具注入逻辑替代。
|
|||
|
|
* 此函数不再被主链路调用,保留仅为兼容可能的外部引用。
|
|||
|
|
*/
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add packages/backend/src/modules/netaclaw/service/skill_context.ts
|
|||
|
|
git commit -m "chore(skill): mark buildSkillContext as deprecated"
|
|||
|
|
```
|