18-skill-registry.md 16 KB

18 技能发布平台(推送 + 审核)

状态:一期已实施(2026-11-03),决策见 ADR-0027,实施中的调整见 §12。修改自定义技能(12-custom-skills.md)的来源:去掉目录技能源与上传 zip,改为"项目推送、管理员审核",保留后台编写。

1. 背景

技能通常由其他项目生成或维护(业务平台仓库、工具项目等),这些项目在别的用户目录、别的机器或 CI 中。现有的两种导入方式都不合适:

  • 目录技能源:服务以专用账号运行并启用了 ProtectHome,读不到 /home,只能手动拷贝到服务能看到的位置;
  • 上传 zip:每次都要手动打包、登录后台上传,无法接入项目的构建流程,也说不清技能从哪个项目、哪次提交来。

2. 目标

  1. 项目通过接口推送技能包,服务器不需要能访问项目所在的位置;
  2. 推送的内容审核通过后才能使用;审核时决定新建技能还是覆盖已有技能,覆盖前看清差异并再次确认;
  3. 密钥简单、好管理:不引入"发布者",用密钥名称表示来源;
  4. 自定义技能只有两种产生方式:后台编写、推送并审核。

不在本期:正式的客户端工具(先给 curl 示例)、公开目录、跨实例同步。

3. 概念

概念 说明
发布密钥 sk_pub_…,由管理员在后台创建。名称就是来源标识,例如"fwh-app · GitHub Actions""张三 · 本机"。只能调用发布接口,不能创建会话或访问其他数据。
提交 一次推送:一个技能包(可以包含多个技能)、提交说明、来源信息(仓库、分支、提交号,自由字段)。内容不可修改。
提交项 提交中的一个技能,是审核的单位:每个技能单独批准或拒绝。

不设"发布者",也不限定技能名:推送不会直接改变任何技能,新建还是覆盖、用什么名字,都由管理员在审核时决定。

3.1 提交项的状态

                ┌── 批准:新建技能 ──────────────┐
pending ────────┤                                ├──▶ approved(记录生成的技能与版本)
                ├── 批准:覆盖已有技能(再次确认)┘
                ├── 拒绝(填写原因)────────────────▶ rejected
                ├── 撤回(推送方)──────────────────▶ withdrawn
                └── 同一密钥又推送了同名技能 ────────▶ superseded
  • 提交时就能判定的不进入审核:校验不通过为 invalid(附原因);与同名技能的当前版本完全相同为 unchanged。
  • 同一密钥推送同名技能时,之前待审核的那一份变为 superseded,管理员只审核最新的一份。
  • 管理员可以删除已处理的提交项(approved、rejected、withdrawn、superseded、invalid、unchanged)。删除只删提交记录,不影响已生成的技能版本:版本历史中保留提交说明与密钥名称的副本。待审核的提交项需要先批准或拒绝。

3.2 与自定义技能的关系

  • 自定义技能只有一种来源类型:后台可编辑。推送来的技能审核通过后与后台编写的技能相同,可以直接在后台修改。
  • 下一次推送覆盖时,审核页的差异会显示管理员在后台改过的内容(这些改动会被覆盖),由管理员决定是否覆盖。
  • 版本号仍由平台分配(v1、v2…);SKILL.md 中声明的版本号(version / metadata.version)只用于展示和对比。
  • 版本历史中标出每个版本的来源:"编辑""恢复""来自提交 sub_…(fwh-app · GitHub Actions):提交说明"。

4. 功能需求

FR-1 推送

  • 推送方用发布密钥调用接口上传技能包(zip:单个技能、技能集合 <名称>/SKILL.md 或插件格式 skills/<名称>/SKILL.md),可附提交说明、来源信息。
  • 服务端解压、逐个技能校验(只取普通文件、名称规则、大小与数量限额、SKILL.md 校验),文件存入现有的文件库,生成提交记录,立即返回每个技能的状态。
  • 试运行(dryRun=1):只校验、不生成提交,便于在 CI 中先检查。
  • 幂等:同一密钥推送与待审核内容完全相同的技能,返回已有的提交项,不重复生成。
  • 限额:每个密钥每小时最多 30 次提交,待审核的提交项最多 50 个(可在设置中调整)。

FR-2 自动检查(辅助审核,不自动拒绝)

检查 说明
结构 SKILL.md 的 name / description、文件数、大小(不通过即 invalid)
可执行内容 列出脚本文件(.sh、.py、.js 等)及其变化,是审核重点
疑似密钥 按常见格式匹配(各类 API Key、私钥、令牌、连接串),命中时标红
外部地址 列出 SKILL.md 与脚本中出现的网址 / 主机名
二进制文件 列出(模版等),可以下载查看
声明版本号 与同名技能的当前版本对比,没变但内容变了时提示

FR-3 审核

待审核列表:技能名、密钥名称、提交说明、来源信息、提交时间、检查结果摘要;是否已有同名技能。

打开一个提交项,先查看内容(文件树、SKILL.md、检查结果),然后选择:

批准为新技能

  • 技能名默认取 SKILL.md 中的 name,管理员可以修改(同名技能已存在时必须改名,否则请选择"覆盖");
  • 改名时自动把 SKILL.md frontmatter 中的 name 改为新名称(审核页显示这一处改动);
  • 可选"批准并发布"(默认)或"批准但暂不发布"(先在 Playground 中试用,之后在技能详情中发布)。

覆盖已有技能

  • 目标技能默认是同名技能,也可以选择其他技能(名称不同时同样改写 SKILL.md 中的 name);
  • 显示新旧差异:逐文件对比(新增 / 修改 / 删除),以目标技能的最新版本为准,并单独提示:
    • 目标技能有尚未保存的编辑(覆盖后丢失);
    • 目标技能在推送之后有新版本(例如管理员刚修改过);
    • 声明版本号的变化(如 2.0.0 → 2.1.0,或没变);
  • 点击"覆盖"后弹出二次确认("将用这次提交覆盖 fwh-assistant(v3 → v4),以上改动会生效"),确认后生成新版本;旧版本可以在版本历史中恢复。
  • 同样可选立即发布或暂不发布。

拒绝

  • 必须填写原因;推送方可以通过接口看到状态与原因。
  • 已拒绝的提交项仍可在"已处理"中查看完整内容(文件、检查结果、原因),可以删除。

FR-4 推送方查询

推送方可以查询用自己的密钥提交的内容:状态、审核意见、生成的技能名与版本号;可以撤回仍在待审核的提交项。看不到其他密钥的提交,也不能下载平台上的技能。

FR-5 密钥管理

  • 后台新建密钥:名称(必填,表示来源)、说明、有效期(默认 1 年,可选永久)。明文只在创建时显示一次,之后只显示前缀。
  • 可以停用 / 删除;显示最近使用时间与来源 IP、提交数、待审核数。
  • 删除密钥不影响已经批准的技能;它的待审核提交项保留,由管理员处理。

FR-6 审计

推送(密钥名称与前缀、包大小与 sha256、来源信息)、批准(新建 / 覆盖、技能名、版本号)、拒绝(原因)、撤回、删除提交记录、密钥的增删改都记入审计日志。

5. 密钥的保管

发布密钥的权限很小:只能产生待审核的提交,不能直接改变任何技能。即使泄露,最坏的结果是出现一些需要拒绝的提交,在后台吊销即可。因此不需要复杂的密钥系统,按使用场景放在项目代码之外:

场景 保管方式
CI(GitHub Actions、GitLab CI 等) 存为 CI 的加密变量(secret),如 AGENT_SKILLS_TOKEN;一个项目的流水线一个密钥,名称写明项目与 CI
开发者本机 存在用户目录的配置文件(如 ~/.config/agent-skills/credentials,权限 600)或环境变量;一人一机一个密钥,名称写明人与机器
项目仓库 不放密钥;只放推送地址等不敏感的配置(如 .agent-skills.json 中的 server、技能目录)

推送工具(后续实现)按"环境变量 → 用户配置文件"的顺序读取密钥,项目之间无需各自保存。

二期可以增加浏览器登录,免去复制密钥:推送工具执行 login 时显示一个验证码并打开后台页面,管理员确认后,平台为这台机器签发一个密钥并写入用户配置文件(与 gh auth login 的设备码流程相同)。

6. 接口

6.1 发布接口(发布密钥,Authorization: Bearer sk_pub_…)

与服务同一端口,前缀 /skills/registry/v1。

方法 路径 说明
POST /submissions multipart:file(zip)、message、meta(JSON,如 {"repo","branch","commit"});?dryRun=1 只校验。返回提交与各技能的状态
GET /submissions?status=&limit= 用这个密钥提交的列表
GET /submissions/:id 详情:各技能的状态、检查结果、审核意见、生成的技能名与版本号
DELETE /submissions/:id 撤回其中仍在待审核的技能

提交的返回示例:

{
  "id": "sub_01k…",
  "status": "pending",
  "items": [
    { "id": "sbi_01k…", "name": "fwh-assistant", "status": "pending", "declaredVersion": "2.1.0",
      "existing": { "version": 3, "declaredVersion": "2.0.0" },
      "checks": [{ "level": "warning", "code": "script", "message": "包含脚本 scripts/build.py" }] },
    { "id": "sbi_01k…", "name": "fwh-report", "status": "unchanged" }
  ]
}

审核后:

{ "id": "sbi_01k…", "name": "fwh-assistant", "status": "approved",
  "result": { "action": "overwrite", "skill": "fwh-assistant", "version": 4, "published": true } }
{ "id": "sbi_01k…", "name": "fwh-draft", "status": "rejected", "reviewNote": "脚本中包含测试环境地址,请改为参数" }

CI 中推送示例(正式的推送工具后续再定):

cd skills && zip -qr /tmp/skills.zip . && \
curl -fsS -H "Authorization: Bearer $AGENT_SKILLS_TOKEN" \
  -F file=@/tmp/skills.zip -F message="$(git log -1 --pretty=%s)" \
  -F meta="{\"repo\":\"fwh-app\",\"commit\":\"$(git rev-parse HEAD)\"}" \
  https://agent.zenpower.cc/skills/registry/v1/submissions

6.2 后台接口(/admin/v1)

方法 路径 说明
GET / POST / PATCH / DELETE /skill-keys[/:id] 发布密钥(新建时返回一次明文)
GET /skill-submissions?status=pending\|done 提交项列表
GET /skill-submissions/:itemId 详情:文件清单、检查结果、同名技能信息
GET /skill-submissions/:itemId/file?path= 查看提交中的文件
GET /skill-submissions/:itemId/diff?target=<技能ID>&name= 与目标技能最新版本的差异(含改名对 SKILL.md 的改动)
POST /skill-submissions/:itemId/approve { action: 'create', name, publish } 或 { action: 'overwrite', target, expectedVersion, publish }
POST /skill-submissions/:itemId/reject { reason }
DELETE /skill-submissions/:itemId 删除已处理的提交项

覆盖时提交 expectedVersion(审核页看到的目标技能版本号):目标技能在此期间又有新版本时拒绝并提示刷新,保证覆盖的就是对比过的那个版本。

7. 数据模型

新增:

表 主要字段
skill_publish_keys id、name、description、keyHash、keyPrefix、enabled、expiresAt、createdAt、lastUsedAt、lastUsedIp
skill_submissions id、keyId、keyName(副本,密钥删除后仍可显示)、message、meta(JSON)、packageSha、packageSize、createdAt
skill_submission_items id、submissionId、name、files(文件清单,内容在文件库中)、description、declaredVersion、checks(JSON)、status、reviewNote、reviewedAt、result(JSON:新建 / 覆盖、技能 ID、版本号、是否发布)

修改:

  • custom_skill_versions 增加 note(JSON:提交 ID、密钥名称、提交说明),reason 增加 submission。
  • 去掉技能源:删除 skill_sources 表;custom_skills 的 origin、sourceId 不再使用(已有技能全部转为可编辑)。

提交内容直接存入现有的文件库(按内容寻址,相同文件只存一份)。已删除的提交项引用的文件,若不再被任何版本或提交引用,定期清理。

8. 去掉目录技能源与上传 zip

  • 删除后台"技能源"区域、"上传 zip 导入"按钮,以及对应的接口、自动同步定时任务、上传暂存目录。
  • 已有的从技能源导入的技能转为可编辑,版本历史保留。
  • 保留:后台新建 / 编辑技能、技能内单个文件的上传与替换、导出 zip、版本历史与恢复。
  • 目录选择框继续用于资料库登记。

9. 后台界面

"技能与插件 › 自定义技能"左侧:

  • 技能列表(新建技能);
  • "待审核 (N)":提交项列表,数字同时显示在导航"技能与插件"旁;
  • "已处理":已批准、已拒绝等提交项,可以查看与删除;
  • "发布密钥":名称、前缀、有效期、最近使用、提交数;新建 / 停用 / 删除。

审核视图(右侧):

  • 顶部:技能名、密钥名称、提交说明、来源信息、提交时间;
  • 检查结果;文件树与文件内容;
  • 底部操作:"批准为新技能"(技能名输入框)/ "覆盖已有技能"(选择目标,显示差异,点击后二次确认)/ "拒绝"(填写原因);"立即发布"勾选框默认勾选。

10. 安全

  • 发布密钥只哈希存储,有有效期;只能访问发布接口,只能看到自己的提交。
  • 未经批准的内容不会写入生效目录,引擎看不到;批准时按提交项保存的文件清单生成版本,提交后内容不可修改,因此批准的就是审核时看到的内容。
  • zip 解压沿用现有的防护(路径穿越、压缩炸弹、符号链接);包大小与技能限额与原上传 zip 相同(zip ≤ 50MB,单个技能 ≤ 500 个文件、≤ 20MB)。
  • 推送接口有频率与待审核数量限制。

11. 分期

阶段 内容
一期 发布密钥、推送接口(含试运行)、自动检查、审核(新建 / 覆盖 / 拒绝)、已处理记录与删除、推送方查询、去掉技能源与上传 zip、审计
二期 推送工具(命令行,读取环境变量或用户配置)、浏览器登录签发密钥、审核结果通知

12. 实施记录(2026-11-03)

代码:apps/server/src/custom-skills/registry.ts(密钥、推送、检查、审核)、package.ts(解压与识别技能目录)、http/routes/registry.ts(发布接口);协议文档 api-reference/registry-reference.ts(后台"API 协议 › 技能发布 API",可填入发布密钥直接调试,也包含在下载的 PDF 中;测试核对文档与实际路由一致);后台 SkillReviewPane.vue、SkillPublishKeys.vue;测试 test/custom-skills.test.ts(C-01 ~ C-05)。迁移 0019_skill_registry、0020_drop_skill_sources。

与上文的差异:

设计 实施 原因
幂等:重复推送相同内容返回已有的提交项 每次推送都生成提交;同一密钥的同名技能替代之前待审核的那一份 结果相同(只审核一份),不需要额外的去重逻辑
推送限额可在设置中调整 固定为每个密钥每小时 30 次、待审核 50 个(REGISTRY_LIMITS) 一期先固定,需要时再放进设置
待审核数量显示在后台导航 显示在"技能与插件 › 自定义技能"标签上,左侧"待审核 (N)" 导航只在分组层级,标签上更直接
被拒绝、撤回的提交项保留 90 天后清理 由管理员在"已处理"中删除;删除后不再被引用的文件随之清理 数量不大,避免自动删除审核记录
旧的技能源数据 迁移删除 skill_sources 表和 custom_skills.origin / source_id,所有技能变为可编辑 一并移除技能源