状态:一期已实施(2026-11-03),决策见 ADR-0027,实施中的调整见 §12。修改自定义技能(12-custom-skills.md)的来源:去掉目录技能源与上传 zip,改为"项目推送、管理员审核",保留后台编写。
技能通常由其他项目生成或维护(业务平台仓库、工具项目等),这些项目在别的用户目录、别的机器或 CI 中。现有的两种导入方式都不合适:
ProtectHome,读不到 /home,只能手动拷贝到服务能看到的位置;不在本期:正式的客户端工具(先给 curl 示例)、公开目录、跨实例同步。
| 概念 | 说明 |
|---|---|
| 发布密钥 | sk_pub_…,由管理员在后台创建。名称就是来源标识,例如"fwh-app · GitHub Actions""张三 · 本机"。只能调用发布接口,不能创建会话或访问其他数据。 |
| 提交 | 一次推送:一个技能包(可以包含多个技能)、提交说明、来源信息(仓库、分支、提交号,自由字段)。内容不可修改。 |
| 提交项 | 提交中的一个技能,是审核的单位:每个技能单独批准或拒绝。 |
不设"发布者",也不限定技能名:推送不会直接改变任何技能,新建还是覆盖、用什么名字,都由管理员在审核时决定。
┌── 批准:新建技能 ──────────────┐
pending ────────┤ ├──▶ approved(记录生成的技能与版本)
├── 批准:覆盖已有技能(再次确认)┘
├── 拒绝(填写原因)────────────────▶ rejected
├── 撤回(推送方)──────────────────▶ withdrawn
└── 同一密钥又推送了同名技能 ────────▶ superseded
invalid(附原因);与同名技能的当前版本完全相同为 unchanged。superseded,管理员只审核最新的一份。version / metadata.version)只用于展示和对比。<名称>/SKILL.md 或插件格式 skills/<名称>/SKILL.md),可附提交说明、来源信息。dryRun=1):只校验、不生成提交,便于在 CI 中先检查。| 检查 | 说明 |
|---|---|
| 结构 | SKILL.md 的 name / description、文件数、大小(不通过即 invalid) |
| 可执行内容 | 列出脚本文件(.sh、.py、.js 等)及其变化,是审核重点 |
| 疑似密钥 | 按常见格式匹配(各类 API Key、私钥、令牌、连接串),命中时标红 |
| 外部地址 | 列出 SKILL.md 与脚本中出现的网址 / 主机名 |
| 二进制文件 | 列出(模版等),可以下载查看 |
| 声明版本号 | 与同名技能的当前版本对比,没变但内容变了时提示 |
待审核列表:技能名、密钥名称、提交说明、来源信息、提交时间、检查结果摘要;是否已有同名技能。
打开一个提交项,先查看内容(文件树、SKILL.md、检查结果),然后选择:
批准为新技能
覆盖已有技能
拒绝
推送方可以查询用自己的密钥提交的内容:状态、审核意见、生成的技能名与版本号;可以撤回仍在待审核的提交项。看不到其他密钥的提交,也不能下载平台上的技能。
推送(密钥名称与前缀、包大小与 sha256、来源信息)、批准(新建 / 覆盖、技能名、版本号)、拒绝(原因)、撤回、删除提交记录、密钥的增删改都记入审计日志。
发布密钥的权限很小:只能产生待审核的提交,不能直接改变任何技能。即使泄露,最坏的结果是出现一些需要拒绝的提交,在后台吊销即可。因此不需要复杂的密钥系统,按使用场景放在项目代码之外:
| 场景 | 保管方式 |
|---|---|
| CI(GitHub Actions、GitLab CI 等) | 存为 CI 的加密变量(secret),如 AGENT_SKILLS_TOKEN;一个项目的流水线一个密钥,名称写明项目与 CI |
| 开发者本机 | 存在用户目录的配置文件(如 ~/.config/agent-skills/credentials,权限 600)或环境变量;一人一机一个密钥,名称写明人与机器 |
| 项目仓库 | 不放密钥;只放推送地址等不敏感的配置(如 .agent-skills.json 中的 server、技能目录) |
推送工具(后续实现)按"环境变量 → 用户配置文件"的顺序读取密钥,项目之间无需各自保存。
二期可以增加浏览器登录,免去复制密钥:推送工具执行 login 时显示一个验证码并打开后台页面,管理员确认后,平台为这台机器签发一个密钥并写入用户配置文件(与 gh auth login 的设备码流程相同)。
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
/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(审核页看到的目标技能版本号):目标技能在此期间又有新版本时拒绝并提示刷新,保证覆盖的就是对比过的那个版本。
新增:
| 表 | 主要字段 |
|---|---|
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 不再使用(已有技能全部转为可编辑)。提交内容直接存入现有的文件库(按内容寻址,相同文件只存一份)。已删除的提交项引用的文件,若不再被任何版本或提交引用,定期清理。
"技能与插件 › 自定义技能"左侧:
审核视图(右侧):
| 阶段 | 内容 |
|---|---|
| 一期 | 发布密钥、推送接口(含试运行)、自动检查、审核(新建 / 覆盖 / 拒绝)、已处理记录与删除、推送方查询、去掉技能源与上传 zip、审计 |
| 二期 | 推送工具(命令行,读取环境变量或用户配置)、浏览器登录签发密钥、审核结果通知 |
代码: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,所有技能变为可编辑 |
一并移除技能源 |