GPU_GUARD_MONOREPO/docs/superpowers/specs/2026-04-27-skill-system-evolution-design.md
2026-05-20 21:39:12 +08:00

27 KiB
Raw Permalink Blame History

Skill 系统演进设计

日期2026-04-27 状态Draft 范围P0 密钥管理 · P1 运行时执行器 · P2 标准兼容 · P3 碰撞检测与诊断

1. 背景与目标

当前 Neta 的 Skill 系统以 SKILL.md 为载体,支持 GitHub/ZIP/本地安装,具备条件激活和附属文件读取能力。但存在以下架构缺口:

  • Skill 的 API Key / 密钥无归属,混在主系统环境变量或 prompt 硬编码中
  • Skill 自有代码Python/Node/.NET 脚本)的执行完全依赖 Agent 通过 bash 间接调用,无标准化执行器
  • 与 Agent Skills 社区标准agentskills.io不完全兼容社区 skill 安装后可能缺少字段映射
  • 多来源安装缺少碰撞检测和诊断,同名冲突无预警
  • Agent 读完 SKILL.md 后经常不读 references/ 子文档,导致执行质量下降

参考项目

  • pi-mono-main:纯 prompt skill 系统,多层级发现、碰撞检测、诊断系统、名称规范验证
  • skills-mainMiniMax Skills18 个生产级 skill涵盖 PDF/DOCX/XLSX/PPTX 文档处理、多模态 API 调用混合运行时Python + Node + .NET + bash系统级依赖ffmpeg/LibreOffice

2. Skill 分类体系

2.1 三种 Skill 类型

类型 判断条件 Agent 交互方式 典型场景
prompt 只有 SKILL.md无 skill.config.yaml read_skill → 遵循指令 llm-wiki、社区标准 skill
compute-entry config 有 entrypoint execute_skill 工具 OCR 接口封装、单一 API 调用
compute-toolkit config 有 runtime 但无 entrypoint read_skill → bash 执行脚本 minimax-pdf、minimax-xlsx

2.2 目录结构

prompt skill标准兼容

skills/llm-wiki/
├── SKILL.md
└── references/

compute-entry skill

skills/ocr-reader/
├── SKILL.md
├── skill.config.yaml
├── src/ocr.py
├── requirements.txt
└── .venv/                  ← 安装时自动创建

compute-toolkit skill

skills/minimax-pdf/
├── SKILL.md
├── skill.config.yaml
├── scripts/
│   ├── palette.py
│   ├── render_cover.js
│   └── make.sh
├── design/
│   └── design.md
├── references/
│   └── *.md
├── requirements.txt
└── .venv/

2.3 skill.config.yaml 完整 Schema

# --- 运行时声明 ---
runtime: python | node | bash | dotnet    # 主运行时
entrypoint: src/ocr.py                    # 可选,有则为 compute-entry无则为 compute-toolkit
timeout: 30000                            # 执行超时 ms默认 30s

# --- 环境变量声明 ---
env:
  - name: OCR_API_KEY
    required: true
    description: "OCR 服务 API Key"
  - name: OCR_ENDPOINT
    required: false
    default: "https://api.ocr.com/v1"

# --- 依赖声明 ---
dependencies:
  system:                                 # 系统级依赖
    - name: ffmpeg
      check: "ffmpeg -version"            # 检测命令
    - name: libreoffice
      check: "soffice --version"
  python:
    source: requirements.txt              # 或 inline: ["httpx>=0.27", "pillow"]
  node:
    packages: ["pptxgenjs"]               # npm install 到 skill 目录
  dotnet:
    project: scripts/dotnet/MyProject.Cli # dotnet restore 路径

# --- 安装钩子 ---
setup:
  posix: scripts/setup.sh                 # macOS/Linux
  win32: scripts/setup.ps1                # Windows

# --- compute-entry 专用:接口声明 ---
interface:
  input:
    image_path: { type: string, required: true, description: "图片路径" }
    language: { type: string, default: "auto" }
  output:
    text: { type: string }
    confidence: { type: number }

# --- references 加载策略 ---
references:
  required:                               # 执行前必须读取
    - references/create.md
    - references/format.md
  optional:                               # 按需读取
    - references/tracing.md
  routes:                                 # 任务路由 → 文档映射
    - match: ["create", "generate", "new"]
      required_refs: ["references/create.md", "references/format.md"]
    - match: ["edit", "modify", "fill"]
      required_refs: ["references/edit.md"]

3. P0Skill-Scoped 密钥管理

3.1 数据层

netaclaw_skill 表新增两个字段:

// entity/skill.ts
@Column({ type: 'text', comment: 'AES-256-GCM 加密的 secrets JSON', nullable: true })
secrets: string;

@Column({ type: 'json', comment: 'env 声明 schema', nullable: true })
envSchema: Array<{ name: string; required: boolean; description?: string; default?: string }>;

3.2 新增服务SkillSecretService

// service/skill_secret.ts
@Provide()
@Scope(ScopeEnum.Singleton)
export class SkillSecretService {
  // AES-256-GCM 加密,密钥来自 process.env.SKILL_SECRET_KEY || process.env.APP_SECRET
  encrypt(plainObj: Record<string, string>): string;
  decrypt(cipherText: string): Record<string, string>;

  // 合并 DB secrets + envSchema defaults返回完整 env map
  async resolveEnv(skillName: string): Promise<Record<string, string>>;

  // 保存 secrets加密后写入 DB
  async saveSecrets(skillName: string, secrets: Record<string, string>): Promise<void>;

  // 获取已配置的 key 列表(不返回明文)
  async getConfiguredKeys(skillName: string): Promise<Array<{ name: string; hasValue: boolean }>>;
}

3.3 API 变更

Admin controller 新增:

GET  /admin/netaclaw/skill/envSchema?name=ocr-reader
  → 返回 envSchema 声明 + 每个 key 的 hasValue 状态

POST /admin/netaclaw/skill/secrets
  body: { name: "ocr-reader", secrets: { "OCR_API_KEY": "sk-xxx" } }
  → 加密后存入 DB不返回明文

3.4 前端变更

skill-detail.vue 抽屉新增"配置"区域:

  • 读取 envSchema 展示每个变量的 name、description、required 标记
  • 已配置的显示 [********] + 修改按钮
  • 未配置且 required 的显示红色提示
  • 保存时调用 /secrets 接口

3.5 运行时 env 注入

compute-entry 模式: SkillExecutorService.execute() 调用 resolveEnv(skillName) 注入到子进程 env。

compute-toolkit 模式: bash 工具执行时,检查脚本路径是否落在某个 skill 目录下。如果是,自动从 SkillSecretService.resolveEnv() 获取对应 env 注入到子进程。判断逻辑:

// tools/builtin/bash.ts execute 方法内
const skillName = skillLoader.resolveSkillByPath(cwd || scriptPath);
if (skillName) {
  const skillEnv = await skillSecretService.resolveEnv(skillName);
  Object.assign(processEnv, skillEnv);
}

SkillLoaderService 新增 resolveSkillByPath(absPath) 方法:检查路径是否以某个 skill 目录为前缀,返回 skill name 或 null。

4. P1Skill Runtime Executor

4.1 新增服务SkillExecutorService

// service/skill_executor.ts
interface SkillExecuteParams {
  skillName: string;
  input: Record<string, unknown>;
}

interface SkillExecuteResult {
  success: boolean;
  output?: Record<string, unknown>;
  error?: string;
  duration: number;  // ms
}

@Provide()
@Scope(ScopeEnum.Singleton)
export class SkillExecutorService {
  async execute(params: SkillExecuteParams): Promise<SkillExecuteResult>;
}

4.2 执行流程

Agent 调用 execute_skill({ name: "ocr-reader", input: { image_path: "/tmp/a.png" } })
  → SkillExecutorService.execute()
    → 1. 从 SkillLoaderService 获取 skillMeta确认是 compute-entry
    → 2. 读取 skill.config.yamlruntime / entrypoint / timeout / interface
    → 3. 验证 input 满足 interface.input宽松模式缺少 optional 字段不报错)
    → 4. SkillSecretService.resolveEnv(skillName) 获取 env map
    → 5. 根据 runtime 构建命令:
         python  → {skillDir}/.venv/bin/python {entrypoint}  (win: .venv\Scripts\python)
         node    → node {entrypoint}
         bash    → bash {entrypoint}
         dotnet  → dotnet run --project {entrypoint}
    → 6. spawn 子进程:
         cwd = skillDir
         env = { ...process.env白名单, ...skill-scoped env }
         stdin = JSON.stringify(input)
         timeout = config.timeout || 30000
    → 7. 收集 stdout期望 JSONstderr 作为日志
    → 8. 解析 stdout JSON → output返回 SkillExecuteResult

4.3 新增 Agent 工具execute_skill

// tools/builtin/execute_skill.ts
const ExecuteSkillParams = Type.Object({
  name: Type.String({ description: 'compute skill 名称' }),
  input: Type.Record(Type.String(), Type.Unknown(), { description: '输入参数 JSON' }),
});

注册到 catalog

registerSchema({
  name: 'execute_skill',
  toolset: 'skill',
  description: '执行 compute skill',
  capability: 'compute',
  visibility: 'skill',
});

工具注入条件:buildSkillContext() 检查 Agent 配置的 skills 中是否存在 compute-entry 类型,有则注入 execute_skill 工具。

4.4 Skill 端协议

entrypoint 脚本遵循 stdin/stdout JSON 协议:

  • 从 stdin 读取 JSON 输入
  • 环境变量中获取 secretsos.environ["OCR_API_KEY"]
  • 将 JSON 结果写入 stdout
  • 非零退出码 = 失败stderr 内容作为错误信息

Python 示例:

import sys, json, os

def main():
    data = json.loads(sys.stdin.read())
    api_key = os.environ["OCR_API_KEY"]
    # ... 业务逻辑 ...
    print(json.dumps({"text": "识别结果", "confidence": 0.95}))

if __name__ == "__main__":
    main()

4.5 依赖安装改造

SkillInstallerService.installDependencies() 重构:

依赖类型 当前行为 改造后
python uv tool install 全局 skill 目录下 uv venv .venv && uv pip install -r requirements.txt
node pnpm add 到主项目 skill 目录下 npm install
dotnet 不支持 skill 目录下 dotnet restore
system 不支持 执行 check 命令检测,未安装则报诊断 warning
setup 不支持 首次安装后执行 setup.posixsetup.win32 脚本

4.6 环境变量白名单

spawn 子进程时不继承完整 process.env,只传递白名单:

const ENV_WHITELIST = [
  'PATH', 'HOME', 'USER', 'LANG', 'LC_ALL', 'TZ',
  'TEMP', 'TMP', 'TMPDIR',
  'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY',
];

加上 skill-scoped env确保主系统的敏感变量DB 密码等)不泄露给 skill 代码。

5. P2Agent Skills 标准兼容

5.1 字段映射

Agent Skills 标准字段 Neta 映射 说明
name 直接使用 新增命名规范验证
description 直接使用 最大 1024 字符
disable-model-invocation 新增支持 hidden skill不进入 prompt 索引
allowed-tools metadata.conditions.requires_tools 语义等价
compatibility metadata.compatibility 环境要求声明
license metadata.license 直接存储

5.2 名称规范验证

新增 validateSkillName(name: string) 函数,对齐 Agent Skills 标准:

  • 只允许 [a-z0-9-]
  • 1-64 字符
  • 不允许首尾连字符、连续连字符
  • 名称必须匹配父目录名

验证时机安装GitHub/ZIP、创建skill_manage 工具 / REST API。不合规则拒绝并返回具体错误。已存在的不合规 skill 在 scanSkills 时产生 warning 诊断但仍加载。

5.3 Prompt 索引变更

buildSkillsPrompt() 输出区分 skill 类型:

<available_skills>
  <skill name="llm-wiki" type="prompt" category="知识库">
    知识库管理(用 read_skill 加载完整指令)
  </skill>
  <skill name="ocr-reader" type="compute-entry" category="多模态">
    OCR 图片识别(用 execute_skill 调用)
    输入: image_path(string,必填), language(string,默认auto)
  </skill>
  <skill name="minimax-pdf" type="compute-toolkit" category="文档生成">
    PDF 生成/填表/重排版(用 read_skill 加载指令后通过 bash 执行脚本)
  </skill>
</available_skills>

compute-entry 的索引包含 interface.input 摘要Agent 无需 read_skill 即可直接调用 execute_skill。

hidden skilldisable-model-invocation: true)不出现在索引中,但仍可通过 read_skill 显式加载。

5.4 read_skill 返回格式改造

// tools/builtin/read_skill.ts
async execute(_id, params) {
  const skill = skillLoader.getSkill(params.name);
  let result = skill.content;

  // 附属文件:区分必读和可选
  const config = skillLoader.getSkillConfig(params.name);
  const requiredRefs = config?.references?.required || [];
  const optionalRefs = (skill.files || []).filter(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 (optionalRefs.length > 0) {
    result += `\n\n<skill_optional_references>`;
    result += `\n以下文档可按需读取`;
    for (const ref of optionalRefs) {
      result += `\n- ${ref}`;
    }
    result += `\n</skill_optional_references>`;
  }

  return textResult(result);
}

5.5 references 声明优先级

references 可以在两个位置声明:

  1. skill.config.yamlreferences 字段compute skill
  2. SKILL.md frontmatter 的 metadata.references 字段prompt skill 兼容)

优先级:skill.config.yaml > SKILL.md frontmatter。如果两者都存在,以 config 为准。对于纯 prompt skill无 config从 frontmatter 读取。如果两者都没有fallback 到现有行为(列出所有附属文件为 optional

5.6 系统 prompt 层面的约束

buildSkillsPrompt() 的引导文本增加:

读取 skill 后,如果返回中包含 <skill_required_references>,你必须在执行任何操作前
先用 read_skill_file 逐一读取列出的所有文档。这不是建议,是强制要求。

6. P3碰撞检测与诊断系统

6.1 诊断数据结构

// service/skill_diagnostic.ts
interface SkillDiagnostic {
  level: 'error' | 'warning' | 'info';
  code: string;           // 机器可读的诊断码
  skillName: string;
  message: string;
  path?: string;
  detail?: Record<string, unknown>;
}

6.2 诊断码清单

code level 触发条件
NAME_COLLISION warning 两个 skill 目录解析出相同 name
NAME_INVALID warning 名称不符合 [a-z0-9-] 规范
NAME_MISMATCH warning frontmatter name 与目录名不一致
DESC_MISSING error 缺少 descriptionskill 不加载
DESC_TOO_LONG warning description 超过 1024 字符
FINGERPRINT_CHANGED info 文件内容变更但未通过正式更新流程
ENV_NOT_CONFIGURED warning compute skill 声明了 required env 但 DB 无对应 secret
RUNTIME_UNAVAILABLE error 声明 python/node/dotnet 但系统未安装
SYSTEM_DEP_MISSING warning 系统依赖ffmpeg 等check 命令失败
VENV_MISSING warning Python skill 缺少 .venv 目录
CONFIG_PARSE_ERROR error skill.config.yaml 解析失败

6.3 碰撞检测

scanSkills() 中实现,逻辑参考 pi-mono

  • 维护 Map<name, skillPath>Set<realPath>
  • 同名 skill保留先发现的winner后发现的记录 NAME_COLLISION 诊断
  • symlink 去重:通过 fs.realpath 追踪,同一物理文件不重复加载

6.4 诊断收集与暴露

SkillLoaderService 新增:

private diagnostics: SkillDiagnostic[] = [];

getDiagnostics(): SkillDiagnostic[] { return this.diagnostics; }

// scanSkills() 执行时清空并重新收集
async scanSkills(): Promise<void> {
  this.diagnostics = [];
  // ... 扫描过程中 push 诊断 ...
}

6.5 API

GET /admin/netaclaw/skill/diagnostics
  → 返回当前所有诊断信息列表
  → 支持 ?level=error 过滤

6.6 前端展示

skills.vue 页面顶部新增诊断横幅:

  • error 级别:红色警告条,显示数量和摘要
  • warning 级别:黄色提示条
  • 点击展开查看详细诊断列表skill 名称 + 诊断码 + 消息)

skill-detail.vue 抽屉新增"诊断"区域:只显示该 skill 相关的诊断。

7. 对现有系统的兼容性影响

7.1 skill_manage 工具

  • 创建 skill 时新增名称规范验证,不合规则拒绝
  • 如果 content 的 frontmatter 包含 metadata.env,自动在 DB 创建 envSchema 记录
  • 不影响现有 create/edit/delete 流程

7.2 skill 管理页面

  • 卡片新增 type 标签prompt / compute-entry / compute-toolkit
  • 详情抽屉新增"配置"tabenv secrets 管理)
  • 详情抽屉新增"诊断"tab
  • 安装对话框不变

7.3 Agent 运行时

  • buildSkillsPrompt() 输出格式微调(增加 type 属性),向后兼容
  • buildSkillContext() 返回的 skillTools 条件性新增 execute_skill
  • hidden skill 不进入 prompt 索引
  • bash 工具执行 skill 目录下脚本时自动注入 skill-scoped env

7.4 现有 skill 零迁移

  • playwright-clillm-wiki 没有 skill.config.yaml,自动识别为 prompt skill行为不变
  • skills-main 的 skillminimax-pdf 等)安装后,如果没有 skill.config.yaml 也按 prompt skill 处理;后续可逐步补充 config 文件升级为 compute-toolkit

8. 新增文件清单

文件 说明
service/skill_secret.ts P0密钥加密存储与解析
service/skill_executor.ts P1compute-entry 执行器
service/skill_config.ts skill.config.yaml 解析器
tools/builtin/execute_skill.ts P1Agent 工具
service/skill_diagnostic.ts P3诊断收集可合并到 skill_loader

9. 修改文件清单

文件 变更
entity/skill.ts 新增 secrets、envSchema 字段
service/skill_loader.ts 加载 skill.config.yaml、碰撞检测、诊断收集、resolveSkillByPath
service/skill_installer.ts 依赖安装改造skill 级 venv/node_modules、setup 脚本执行
service/tool_resolver.ts execute_skill 工具实例化与条件注入(替代 skill_context.ts
tools/builtin/read_skill.ts 返回格式改造required/optional references
tools/builtin/bash.ts 重构 createLocalBashOperations 增加 envOverride 参数skill 路径检测注入 env
controller/admin/skill.ts 新增 envSchema/secrets/diagnostics 端点
service/skill_loader.ts:buildSkillsPrompt 输出格式调整type 属性、required references 提示)
frontend: skills.vue 诊断横幅、type 标签
frontend: skill-detail.vue 重构为 tab 布局(基本信息 / 配置 / 诊断)
shared/types/skill.types.ts 新增 SkillConfig、SkillDiagnostic扩展 runtime 枚举

10. 架构评审补充

10.1 工具注入链路修正

execute_skill 的注入目标从 skill_context.ts 改为 tool_resolver.ts。在 resolve() 方法中,当检测到 Agent 配置的 skills 包含 compute-entry 类型时,实例化 execute_skill 工具并加入工具列表。逻辑位置在现有 read_skill 注入点(约 line 603之后。

skill_context.tsbuildSkillContext() 函数标记为 deprecated后续迁移到 tool_resolver 内部。

10.2 bash 工具 env 注入重构

bash.tscreateLocalBashOperations() 需要重构:

// 当前签名
function createLocalBashOperations(shellConfig: ShellConfig): BashOperations

// 改造后
interface BashEnvProvider {
  getAdditionalEnv(cwd: string): Promise<Record<string, string>>;
}

function createLocalBashOperations(
  shellConfig: ShellConfig,
  envProvider?: BashEnvProvider,
): BashOperations

SkillLoaderService 实现 BashEnvProvider 接口:根据 cwd 判断是否在 skill 目录下,是则返回 skill-scoped env。判断策略严格前缀匹配 skillsDir + sep + skillName + sep,不做模糊匹配。

10.3 加密方案细化

secrets 字段的线格式:base64(IV:16bytes || ciphertext || authTag:16bytes)

class SkillSecretService {
  private readonly algorithm = 'aes-256-gcm';
  private readonly ivLength = 16;

  encrypt(plainObj: Record<string, string>): string {
    const iv = crypto.randomBytes(this.ivLength);
    const cipher = crypto.createCipheriv(this.algorithm, this.deriveKey(), iv);
    const encrypted = Buffer.concat([cipher.update(JSON.stringify(plainObj), 'utf8'), cipher.final()]);
    const authTag = cipher.getAuthTag();
    return Buffer.concat([iv, encrypted, authTag]).toString('base64');
  }

  private deriveKey(): Buffer {
    const raw = process.env.SKILL_SECRET_KEY || process.env.APP_SECRET;
    if (!raw) throw new Error('SKILL_SECRET_KEY or APP_SECRET must be set');
    return crypto.createHash('sha256').update(raw).digest(); // 32 bytes
  }
}

密钥轮换:提供 POST /admin/netaclaw/skill/rotateSecrets 端点,用旧 key 解密所有 secrets 后用新 key 重新加密。密钥丢失时所有 secrets 不可恢复,需管理员重新配置。

10.4 DB 迁移

项目使用 TypeORM synchronize: true(开发环境)自动同步 entity 变更到数据库。生产环境通过数据库 MCP 工具直接操作,不需要编写 migration SQL 脚本。

实施时只需:

  1. entity/skill.ts 中添加新字段(secretsenvSchemaskillTypeV2
  2. 开发环境重启后 TypeORM 自动同步表结构
  3. 生产环境通过 MCP execute 工具执行 ALTER TABLE如需要

现有行新字段为 NULL不影响现有功能。skillTypeV2 在 scanSkills 时根据有无 skill.config.yaml 自动填充。

10.5 类型同步

shared/types/skill.types.ts 更新:

export type SkillRuntime = 'node' | 'python' | 'bash' | 'dotnet';
export type SkillClassification = 'prompt' | 'compute-entry' | 'compute-toolkit';

DB 的 skillType 字段compute/llm/multimodal保留不变它描述的是 skill 的能力类别。新增 skill_type_v2 字段存储三分类,两者正交。

10.6 Agent 分配 skill 时的类型校验

Agent 编辑页(agent-edit.vue)的 skill 选择器改造:

前端改造:

  • 可选 Skill 列表中每个 skill 显示分类标签prompt / compute-entry / compute-toolkit
  • 已选择列表中同样显示分类标签
  • 当选择 compute-entry 或 compute-toolkit skill 时,如果该 Agent 缺少对应工具权限,显示黄色 warning 提示

后端校验:

在 Agent 保存接口(POST /admin/netaclaw/agent/update)中新增校验逻辑:

// controller/agent.ts 的 update 方法中
if (body.skills?.length) {
  const warnings: string[] = [];
  for (const skillName of body.skills) {
    const classification = this.skillLoader.getSkillClassification(skillName);
    if (classification === 'compute-entry') {
      // 检查 Agent 的 tool governance 是否允许 execute_skill
      // 通过 tool_resolver 的 catalog 检查 execute_skill 是否在 Agent 可用工具列表中
      const toolNames = collectToolNames({ hasSkills: true });
      if (!toolNames.includes('execute_skill')) {
        warnings.push(`Skill "${skillName}" 是 compute-entry 类型,但 execute_skill 工具未启用`);
      }
    }
    if (classification === 'compute-toolkit') {
      const toolNames = collectToolNames({ hasSkills: true });
      if (!toolNames.includes('bash')) {
        warnings.push(`Skill "${skillName}" 是 compute-toolkit 类型,但 bash 工具未启用`);
      }
    }
  }
  // warnings 不阻塞保存,随响应返回
}

/admin/netaclaw/skill/metas 端点返回数据新增 classification 字段,前端据此渲染标签。

10.7 基础设施文件过滤

collectFiles() 新增排除列表:

const INFRA_FILES = new Set([
  'skill.config.yaml', 'requirements.txt', 'package.json',
  'package-lock.json', 'tsconfig.json', '.env',
]);

这些文件不出现在 read_skill 返回的 <skill_files> 列表中Agent 不会误读。

10.8 Skill 执行审计日志

SkillExecutorService.execute() 执行完成后写入结构化日志:

this.logger.info('[SkillExecutor] %s executed by agent=%s duration=%dms success=%s',
  params.skillName, agentId, result.duration, result.success);

失败时额外记录 stderr 摘要(截断到 500 字符)。

10.9 setup 脚本安全约束

安装时执行 setup 脚本增加限制:

后端skill_installer.ts

  • timeout: 120s
  • 只允许 source === 'github'source === 'zip'(管理员手动上传)的 skill 执行 setup
  • source === 'local'(通过 skill_manage 工具创建)的 skill 不允许 setup 脚本
  • 执行前检查 setup 脚本路径不包含 ..(路径穿越防护)
// skill_installer.ts installDependencies 中
const setupKey = process.platform === 'win32' ? 'win32' : 'posix';
const setupScript = config?.setup?.[setupKey];
if (setupScript) {
  const origin = await this.registry.readOrigin(skillName);
  const allowedSources = ['github', 'zip'];
  if (!origin || !allowedSources.includes(origin.source)) {
    logs.push(`[setup] 跳过: 仅 GitHub/ZIP 安装的 skill 允许执行 setup 脚本`);
  } else if (setupScript.includes('..')) {
    logs.push(`[setup] 跳过: setup 脚本路径包含路径穿越`);
  } else {
    // 执行 setuptimeout 120s
  }
}

前端skills.vue 安装对话框):

  • 安装完成后,如果 skill 包含 setup 脚本,弹出确认对话框: "此 Skill 包含安装脚本({scriptName}),是否执行?"
  • 用户确认后才调用 /installDeps 端点

10.10 references.routes 的实现策略

routes 匹配在 server 端执行。read_skill 工具新增可选参数 task

const ReadSkillParams = Type.Object({
  name: Type.String(),
  task: Type.Optional(Type.String({ description: '当前任务描述,用于自动匹配需要读取的文档' })),
});

server 端对 task 做关键词匹配(match 数组中的词是否出现在 task 中),命中则将对应 required_refs 的内容直接拼接到返回结果中Agent 无需二次调用 read_skill_file。未命中则 fallback 到 <skill_required_references> 列表提示。

scanSkills() 中维护 Set<string> 存储已加载 skill 的 realpath。对每个 skill 目录调用 fs.realpath() 获取真实路径,如果已在 set 中则跳过(不产生碰撞诊断,因为是同一物理目录)。

const realPathSet = new Set<string>();
// 在加载每个 skill 时
const realPath = await fs.realpath(path.join(this.skillsDir, entry.name)).catch(() => null);
if (realPath && realPathSet.has(realPath)) continue; // 静默跳过 symlink 重复
if (realPath) realPathSet.add(realPath);

10.12 skill_context.ts 废弃

skill_context.tsbuildSkillContext() 函数标记为 @deprecated,添加注释指向 tool_resolver.ts 中的新实现。不删除文件,避免破坏可能存在的外部引用。后续版本清理。

/**
 * @deprecated 使用 tool_resolver.ts 中的 skill 工具注入逻辑替代。
 * 此函数不再被主链路调用。
 */
export function buildSkillContext(...) { ... }