# 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-main**(MiniMax Skills):18 个生产级 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 ```yaml # --- 运行时声明 --- 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. P0:Skill-Scoped 密钥管理 ### 3.1 数据层 `netaclaw_skill` 表新增两个字段: ```typescript // 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 ```typescript // 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; decrypt(cipherText: string): Record; // 合并 DB secrets + envSchema defaults,返回完整 env map async resolveEnv(skillName: string): Promise>; // 保存 secrets(加密后写入 DB) async saveSecrets(skillName: string, secrets: Record): Promise; // 获取已配置的 key 列表(不返回明文) async getConfiguredKeys(skillName: string): Promise>; } ``` ### 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 注入到子进程。判断逻辑: ```typescript // 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. P1:Skill Runtime Executor ### 4.1 新增服务:SkillExecutorService ```typescript // service/skill_executor.ts interface SkillExecuteParams { skillName: string; input: Record; } interface SkillExecuteResult { success: boolean; output?: Record; error?: string; duration: number; // ms } @Provide() @Scope(ScopeEnum.Singleton) export class SkillExecutorService { async execute(params: SkillExecuteParams): Promise; } ``` ### 4.2 执行流程 ``` Agent 调用 execute_skill({ name: "ocr-reader", input: { image_path: "/tmp/a.png" } }) → SkillExecutorService.execute() → 1. 从 SkillLoaderService 获取 skillMeta,确认是 compute-entry → 2. 读取 skill.config.yaml:runtime / 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(期望 JSON),stderr 作为日志 → 8. 解析 stdout JSON → output,返回 SkillExecuteResult ``` ### 4.3 新增 Agent 工具:execute_skill ```typescript // 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: ```typescript 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 输入 - 环境变量中获取 secrets(如 `os.environ["OCR_API_KEY"]`) - 将 JSON 结果写入 stdout - 非零退出码 = 失败,stderr 内容作为错误信息 Python 示例: ```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.posix` 或 `setup.win32` 脚本 | ### 4.6 环境变量白名单 spawn 子进程时不继承完整 `process.env`,只传递白名单: ```typescript 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. P2:Agent 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 类型: ```xml 知识库管理(用 read_skill 加载完整指令) OCR 图片识别(用 execute_skill 调用) 输入: image_path(string,必填), language(string,默认auto) PDF 生成/填表/重排版(用 read_skill 加载指令后通过 bash 执行脚本) ``` compute-entry 的索引包含 `interface.input` 摘要,Agent 无需 read_skill 即可直接调用 execute_skill。 hidden skill(`disable-model-invocation: true`)不出现在索引中,但仍可通过 `read_skill` 显式加载。 ### 5.4 `read_skill` 返回格式改造 ```typescript // 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`; result += `\n⚠️ 执行此 skill 的任务前,你必须先用 read_skill_file 读取以下文档:`; for (const ref of requiredRefs) { result += `\n- ${ref}`; } result += `\n未读取这些文档就执行操作会导致错误。`; result += `\n`; } if (optionalRefs.length > 0) { result += `\n\n`; result += `\n以下文档可按需读取:`; for (const ref of optionalRefs) { result += `\n- ${ref}`; } result += `\n`; } return textResult(result); } ``` ### 5.5 references 声明优先级 references 可以在两个位置声明: 1. `skill.config.yaml` 的 `references` 字段(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 后,如果返回中包含 ,你必须在执行任何操作前 先用 read_skill_file 逐一读取列出的所有文档。这不是建议,是强制要求。 ``` ## 6. P3:碰撞检测与诊断系统 ### 6.1 诊断数据结构 ```typescript // service/skill_diagnostic.ts interface SkillDiagnostic { level: 'error' | 'warning' | 'info'; code: string; // 机器可读的诊断码 skillName: string; message: string; path?: string; detail?: Record; } ``` ### 6.2 诊断码清单 | code | level | 触发条件 | |------|-------|---------| | `NAME_COLLISION` | warning | 两个 skill 目录解析出相同 name | | `NAME_INVALID` | warning | 名称不符合 `[a-z0-9-]` 规范 | | `NAME_MISMATCH` | warning | frontmatter name 与目录名不一致 | | `DESC_MISSING` | error | 缺少 description,skill 不加载 | | `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` 和 `Set` - 同名 skill:保留先发现的(winner),后发现的记录 `NAME_COLLISION` 诊断 - symlink 去重:通过 `fs.realpath` 追踪,同一物理文件不重复加载 ### 6.4 诊断收集与暴露 `SkillLoaderService` 新增: ```typescript private diagnostics: SkillDiagnostic[] = []; getDiagnostics(): SkillDiagnostic[] { return this.diagnostics; } // scanSkills() 执行时清空并重新收集 async scanSkills(): Promise { 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) - 详情抽屉新增"配置"tab(env secrets 管理) - 详情抽屉新增"诊断"tab - 安装对话框不变 ### 7.3 Agent 运行时 - `buildSkillsPrompt()` 输出格式微调(增加 type 属性),向后兼容 - `buildSkillContext()` 返回的 `skillTools` 条件性新增 `execute_skill` - hidden skill 不进入 prompt 索引 - bash 工具执行 skill 目录下脚本时自动注入 skill-scoped env ### 7.4 现有 skill 零迁移 - `playwright-cli` 和 `llm-wiki` 没有 `skill.config.yaml`,自动识别为 prompt skill,行为不变 - skills-main 的 skill(minimax-pdf 等)安装后,如果没有 skill.config.yaml 也按 prompt skill 处理;后续可逐步补充 config 文件升级为 compute-toolkit ## 8. 新增文件清单 | 文件 | 说明 | |------|------| | `service/skill_secret.ts` | P0:密钥加密存储与解析 | | `service/skill_executor.ts` | P1:compute-entry 执行器 | | `service/skill_config.ts` | skill.config.yaml 解析器 | | `tools/builtin/execute_skill.ts` | P1:Agent 工具 | | `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.ts` 的 `buildSkillContext()` 函数标记为 deprecated,后续迁移到 tool_resolver 内部。 ### 10.2 bash 工具 env 注入重构 `bash.ts` 的 `createLocalBashOperations()` 需要重构: ```typescript // 当前签名 function createLocalBashOperations(shellConfig: ShellConfig): BashOperations // 改造后 interface BashEnvProvider { getAdditionalEnv(cwd: string): Promise>; } 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)` ```typescript class SkillSecretService { private readonly algorithm = 'aes-256-gcm'; private readonly ivLength = 16; encrypt(plainObj: Record): 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` 中添加新字段(`secrets`、`envSchema`、`skillTypeV2`) 2. 开发环境重启后 TypeORM 自动同步表结构 3. 生产环境通过 MCP `execute` 工具执行 ALTER TABLE(如需要) 现有行新字段为 NULL,不影响现有功能。`skillTypeV2` 在 scanSkills 时根据有无 skill.config.yaml 自动填充。 ### 10.5 类型同步 `shared/types/skill.types.ts` 更新: ```typescript 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`)中新增校验逻辑: ```typescript // 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()` 新增排除列表: ```typescript const INFRA_FILES = new Set([ 'skill.config.yaml', 'requirements.txt', 'package.json', 'package-lock.json', 'tsconfig.json', '.env', ]); ``` 这些文件不出现在 `read_skill` 返回的 `` 列表中,Agent 不会误读。 ### 10.8 Skill 执行审计日志 `SkillExecutorService.execute()` 执行完成后写入结构化日志: ```typescript 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 脚本路径不包含 `..`(路径穿越防护) ```typescript // 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 { // 执行 setup,timeout 120s } } ``` **前端(skills.vue 安装对话框):** - 安装完成后,如果 skill 包含 setup 脚本,弹出确认对话框: "此 Skill 包含安装脚本({scriptName}),是否执行?" - 用户确认后才调用 `/installDeps` 端点 ### 10.10 references.routes 的实现策略 routes 匹配在 server 端执行。`read_skill` 工具新增可选参数 `task`: ```typescript const ReadSkillParams = Type.Object({ name: Type.String(), task: Type.Optional(Type.String({ description: '当前任务描述,用于自动匹配需要读取的文档' })), }); ``` server 端对 `task` 做关键词匹配(`match` 数组中的词是否出现在 task 中),命中则将对应 `required_refs` 的内容直接拼接到返回结果中,Agent 无需二次调用 `read_skill_file`。未命中则 fallback 到 `` 列表提示。 ### 10.11 Symlink 去重 `scanSkills()` 中维护 `Set` 存储已加载 skill 的 realpath。对每个 skill 目录调用 `fs.realpath()` 获取真实路径,如果已在 set 中则跳过(不产生碰撞诊断,因为是同一物理目录)。 ```typescript const realPathSet = new Set(); // 在加载每个 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.ts` 的 `buildSkillContext()` 函数标记为 `@deprecated`,添加注释指向 `tool_resolver.ts` 中的新实现。不删除文件,避免破坏可能存在的外部引用。后续版本清理。 ```typescript /** * @deprecated 使用 tool_resolver.ts 中的 skill 工具注入逻辑替代。 * 此函数不再被主链路调用。 */ export function buildSkillContext(...) { ... } ```