GPU_GUARD_MONOREPO/docs/superpowers/plans/2026-04-27-p2-standard-compat.md
2026-05-20 21:39:12 +08:00

14 KiB
Raw Permalink Blame History

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 定义之前添加:

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 后添加:

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.tsexecute 方法中,create 分支的 parseSkillMd 之后添加:

const nameErrors = validateSkillName(parsed.name, params.name);
if (nameErrors.length > 0) {
  return textResult(`名称不合规: ${nameErrors.join('; ')}`);
}

skill_installer.tsinstallFromGitHubinstallFromZip 中,parseSkillMd 之后添加类似校验。

  • Step 4: Commit
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 方法:

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
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 方法体:

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
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') 条件中,增加过滤:

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
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 参数

const ReadSkillParams = Type.Object({
  name: Type.String({ description: '要读取的 skill 名称' }),
  task: Type.Optional(Type.String({ description: '当前任务描述,用于自动匹配需要读取的文档' })),
});
  • Step 2: 在 execute 方法中实现 routes 匹配

在获取 requiredRefs 之后、构建返回结果之前,添加 routes 匹配逻辑:

// 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 二次调用):

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
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 尚未添加):

classification: this.skillConfig.classify(fs.name),
  • Step 2: 前端 skill 选择器显示分类标签

agent-edit.vue 的可选 Skill 列表项中(约 line 38在现有 skillType 标签旁新增分类标签:

<el-tag v-if="sk.classification" size="small"
  :type="sk.classification === 'compute-entry' ? 'success' : sk.classification === 'compute-toolkit' ? '' : 'info'">
  {{ sk.classification }}
</el-tag>

已选择列表中同样添加:

<span class="item-main">{{ getSkillLabel(name) }}</span>
<el-tag size="small" :type="getSkillClassification(name) === 'compute-entry' ? 'success' : 'info'">
  {{ getSkillClassification(name) }}
</el-tag>

新增辅助函数:

function getSkillClassification(name: string): string {
  const meta = skillMetas.value.find((s) => s.name === name);
  return meta?.classification || 'prompt';
}
  • Step 3: 前端选择 compute skill 时显示工具权限 warning

addSkill 函数中添加校验:

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.tsupdate 方法中,注入 SkillLoaderService,保存前校验:

@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
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 函数上方添加:

/**
 * @deprecated 使用 tool_resolver.ts 中的 skill 工具注入逻辑替代。
 * 此函数不再被主链路调用,保留仅为兼容可能的外部引用。
 */
  • Step 2: Commit
git add packages/backend/src/modules/netaclaw/service/skill_context.ts
git commit -m "chore(skill): mark buildSkillContext as deprecated"