状态:已实施(2026-10-25),一、二、三期全部完成,实施中的调整见 §11。由 FranklinWH ToolBox 新 AI 助手的需要引出(业务平台需要自己的、非公开的技能),内容是通用功能,不含业务平台专有逻辑。基于
0af6089的现有设计(ADR-0013、0016、0017,providers/claude/skills.ts、providers/codex/skills.ts、services/community-skills.ts)。
| 来源 | 存放位置 | 如何进入 | 技能 ID | 引擎 | Profile 中 |
|---|---|---|---|---|---|
| 本地技能目录(local) | 设置项 paths.skillsDir,生产为 /srv/agent-server/skills |
AgentServer 仓库 skills/ 经 deploy.sh 的 rsync --delete 发布 |
agent-skills:<name> |
两个(Claude 按路径加载插件,Codex 经 skills/extraRoots/set) |
包含在“全部技能”中 |
| Claude 插件(plugin) | Claude Code 自己的插件目录 | 后台“技能与插件”调用 claude plugin 安装官方市场插件 |
document-skills:xlsx 等 |
仅 Claude(ADR-0013) | 包含在“全部” |
| Codex 官方(official) | CODEX_HOME/skills/<name> |
从 openai/skills 固定提交复制 | openai-skills:<name> |
仅 Codex(ADR-0016) | 包含在“全部” |
| 第三方(community) | <dataDir>/managed-skills/community |
skills.sh 搜索 → 预览 → 按 GitHub 固定提交安装,只收开源许可 | community-skills:<name> |
两个 | 需单独勾选(optIn) |
| 引擎自带(system) | 引擎内部 | — | — | — | — |
使用 AgentServer 的业务平台(例如 ToolBox)需要自己的、非公开的技能:业务背景、工具使用流程、内部模版。现有来源都不合适:
rsync --delete 覆盖;把业务技能放进去,会让通用平台夹带业务内容;把 paths.skillsDir 改到别处,又会丢掉自带的 report-conventions。新增一类技能来源 自定义技能(custom),满足:
非目标:不做技能市场与分享;不做多管理员协作编辑与权限;不改变现有四种来源的行为。
所有自定义技能都存放在 AgentServer 数据目录内,采用 Claude 插件格式(与第三方技能相同):
<dataDir>/managed-skills/custom/
.claude-plugin/plugin.json { "name": "custom-skills" }
skills/<name>/SKILL.md 生效版本(运行时只读这里)
skills/<name>/references/…
skills/<name>/templates/…
<dataDir>/data/custom-skill-store/ 按内容寻址的文件库(sha256),保存各版本的文件
custom-skills:<name>,两个引擎一致。authored:后台编写或从上传的 zip 导入,可在线编辑;imported:从目录技能源导入,记录来源;默认只读(避免与来源不一致),可以“转为可编辑”(之后与来源脱钩,不再同步)。custom-skills/skills/ 下的生效版本;编辑、导入都先写入版本库,再“发布”为生效版本。这样无论来源如何,加载路径和 ID 都一致。不采用“直接引用外部目录、实时加载”:外部目录没有插件清单时 Claude 会用目录名作前缀,ID 不稳定;目录内容随时变化,无法审阅和回滚;还要处理符号链接与越权路径。导入快照与第三方技能的做法一致(只复制普通文件、固定版本)。
| 字段 | 说明 |
|---|---|
id、name、description |
后台显示 |
kind |
directory(服务器路径)。上传的 zip 是一次性导入,不登记为技能源(见 FR-1) |
path |
directory 时的绝对路径;可以是插件格式目录(skills/*/SKILL.md)、技能集合目录(*/SKILL.md)或单个技能目录(含 SKILL.md) |
syncMode |
manual(默认)/ auto(定时检查,有变化时自动导入并发布) |
intervalMin |
自动同步间隔,默认 10 分钟 |
lastSyncAt、lastSyncDigest、status、lastError |
同步状态 |
登记时目录可以手动输入,也可以从下拉框中逐级选择:GET /server-dirs?path= 列出服务进程看到的子目录(只列目录名;登录凭证所在目录不列出),标出含 SKILL.md 的目录,并显示当前目录中按上述规则能找到的技能数。
路径校验与资料库一致(KnowledgeService 的检测逻辑可复用):存在、服务进程能看到(受 ProtectHome 影响,home 下的路径通常不可见,需要放在 home 以外或绑定挂载)、不命中全局禁止项、不在服务数据目录与会话工作区内。
auto 模式下跳过确认,直接导入并发布,审计记录变化摘要。authored),版本原因为 import。version / metadata.version、文件差异),由管理员选择"覆盖"(作为新版本发布,旧版本可以在版本历史中恢复)或"保留原版本"。同一个技能只保留一份,不会同时存在两个版本。该技能有尚未保存的编辑时提示覆盖后会丢失。name(缺省用目录名),必须匹配 ^[a-z0-9][a-z0-9-]{0,63}$;同名技能已存在且来源不同时拒绝导入。name(与技能名一致)、description(非空,建议 ≤ 1024 字符,过短给出提醒:描述决定技能是否被触发);正文 ≤ 200KB(与第三方技能的 MAX_SKILL_MD 一致)。custom-skills:<name>,归为“自定义”分组。custom-skills 目录作为本地插件按路径加载(PluginRef,optIn: true),由 resolveSkills 按 Profile 选择。每次对话都会启动新的 CLI 进程,发布后下一轮即生效。custom-skills/skills 加入 skills/extraRoots/set(目前只在 app-server 启动时设置一次,providers/codex/adapter.ts)。需要在自定义技能发布后重新设置 extraRoots;已加载的会话按 ADR-0020 的方式处理:把自定义技能的版本指纹放入会话配置,变化时分叉,使新版本生效。skillsToDisable 按 Profile 选择禁用未勾选的自定义技能。managed-skills/custom 目录在两个引擎中只读可见(与 communitySkillsDir() 同样加入 resolveAccess 的基线可读路径);custom-skill-store 不可见。记录:技能源的增删改、扫描与同步(含变化摘要)、技能创建 / 保存 / 发布 / 恢复 / 停用 / 删除(含版本号与改动文件列表)、zip 上传(文件名、大小、sha256)。
| 项 | 要求 |
|---|---|
| 限额 | 单个技能 ≤ 500 个文件、≤ 20MB(与第三方技能一致);单个文件 ≤ 10MB;zip ≤ 50MB;自定义技能总数 ≤ 200 |
| 文件安全 | 路径规范化,拒绝绝对路径、..、隐藏的控制字符;只写入技能自己的目录;zip 解压防路径穿越与压缩炸弹(按解压后大小计) |
| 脚本 | 技能中的脚本只在 Agent 沙箱内由 Agent 执行,AgentServer 不在导入或保存时执行任何文件 |
| 原子发布 | 先写入临时目录,再整体替换 skills/<name>,运行中的对话不会读到一半新、一半旧的文件 |
| 并发 | 同一技能同时只允许一个保存 / 导入操作;编辑时以版本号做乐观锁,冲突时提示刷新 |
| 可恢复 | 服务重启后由数据库与版本库重建生效目录(启动时校验 skills/ 与当前版本一致,不一致则重新发布) |
| 表 | 字段 |
|---|---|
custom_skills |
id、name(唯一)、description、origin(authored / imported)、sourceId(可空)、enabled、includeInAll、publishedVersion、draftVersion(可空)、createdAt、updatedAt |
custom_skill_versions |
skillId、version、createdAt、reason(edit / import / sync / restore)、files(JSON:路径 → sha256、大小)、changed(相对上一版本的增删改文件) |
skill_sources |
见 3.2 |
文件内容存放在 custom-skill-store/<sha256 前两位>/<sha256>,各版本共享相同文件;没有版本引用的文件在删除技能或定期任务中清理。
/admin/v1)| 方法 | 路径 | 说明 |
|---|---|---|
| GET / POST | /custom-skills |
列表 / 新建(authored) |
| GET / PATCH / DELETE | /custom-skills/:id |
详情(含文件树、版本列表、被哪些 Profile 使用)/ 改描述、启停、includeInAll / 删除 |
| GET / PUT / DELETE | /custom-skills/:id/files/*path |
读 / 写(文本或 multipart 二进制)/ 删除草稿中的文件 |
| POST | /custom-skills/:id/save |
{ baseVersion, publish } 保存为新版本,可同时发布 |
| GET | /custom-skills/:id/versions/:v、/custom-skills/:id/diff?from=&to= |
版本详情 / 对比 |
| POST | /custom-skills/:id/versions/:v/restore |
恢复为新版本并发布 |
| GET | /custom-skills/:id/export |
下载 zip |
| POST | /custom-skills/:id/detach |
导入的技能转为可编辑 |
| GET / POST / PATCH / DELETE | /skill-sources[/:id] |
技能源管理 |
| POST | /skill-sources/:id/scan |
扫描,返回技能与差异 |
| POST | /skill-sources/:id/import |
{ skills: [name], scanDigest } 按扫描结果导入(摘要不一致时拒绝,避免导入未审阅的内容) |
| POST | /skill-sources/upload |
上传 zip,返回 { uploadId, name, scan } |
| POST / DELETE | /skill-sources/uploads/:id/import、/skill-sources/uploads/:id |
导入上传的 zip 中选中的技能(同名技能即覆盖)/ 取消上传 |
| GET | /server-dirs?path= |
选择目录:列出子目录 |
GET /admin/v1/skills 的结果中加入 source: 'custom' 的技能(SkillInfo.source 增加 custom)。
在“技能与插件”页增加“自定义技能”标签(现有标签:Claude、Codex、Codex 插件、第三方):
| 期 | 内容 |
|---|---|
| 一期 | 存储与加载(两个引擎、Profile 勾选、沙箱只读);目录技能源的扫描 / 手动同步;zip 上传导入;审计 |
| 二期 | 在线编写(文件树、Monaco、二进制上传)、草稿与发布、版本历史与恢复、导出 |
| 三期 | 自动同步、版本对比视图、includeInAll 等细节 |
一期完成后,业务平台即可把自己仓库中的技能目录作为技能源导入(路径需对服务可见,见 3.2),或打包为 zip 上传。
| 编号 | 用例 |
|---|---|
| C-01 | 登记一个目录技能源,扫描出 2 个技能,导入后在 Profile 中可勾选;Claude 与 Codex 的对话都能触发该技能,时间线中技能 ID 为 custom-skills:<name> |
| C-02 | 修改来源目录中的一个参考文件,同步时只显示该文件的差异;确认后下一轮对话读到新内容 |
| C-03 | 来源目录中放一个指向 /etc/passwd 的符号链接和一个 ../ 路径的 zip 条目:都被跳过或拒绝,并写入审计 |
| C-04 | 后台新建技能、上传一个 pptx 模版、保存为草稿:运行时不生效;发布后生效;恢复到上一版本后又回到旧内容 |
| C-05 | 选择“全部技能”的 Profile 不包含自定义技能;单独勾选后才加载 |
| C-06 | Agent 尝试写入技能目录被拒绝;读取 custom-skill-store 被拒绝 |
| C-07 | 删除一个被两个 Profile 使用的技能:二次确认后两个 Profile 中都已移除 |
| C-08 | 服务重启后自定义技能仍可用;手动删除 custom-skills/skills/<name> 后重启,自动按当前版本恢复 |
代码:apps/server/src/custom-skills/(store.ts 文件库与文件工具、service.ts 技能与版本、sources.ts 技能源与 zip),后台"技能与插件 › 自定义技能"(CustomSkillsPanel.vue),测试 test/custom-skills.test.ts 覆盖 C-01 ~ C-08。
与草案不同的地方:
| 草案 | 实施 | 原因 |
|---|---|---|
| 文本文件用 Monaco 编辑("后台已在 Profile 提示词中使用") | 起初为等宽文本框,现已改用 Monaco | 首版时后台并没有使用 Monaco;随文件管理一起引入 Monaco(按需加载,只打包常用语言),技能编辑器与 Profile 提示词都已换成 Monaco |
发布后重新设置 Codex 的 extraRoots |
服务启动时就创建 custom-skills/skills,Codex 启动时把它加入 extraRoots;之后 Codex 每次列技能都会重新扫描该目录 |
已验证新增的技能无需重新设置即可出现;已加载会话按 ADR-0020 处理:自定义技能的发布状态指纹放进网关地址,变化时会话分叉 |
草稿 draftVersion |
编辑中的改动保存在 custom_skills.working_files;"保存草稿"生成未发布的新版本,"保存并发布"同时发布 |
与 Profile 的"内容无变化不产生版本"一致,也便于"放弃改动"与"查看改动" |
| 停用:运行时不加载 | 停用时整体移除生效目录,启用时按生效版本重新写入 | 两个引擎都只看生效目录,不需要各自过滤 |
| 删除技能源 | 从它导入的技能保留,转为可编辑 | 草案未规定;避免误删 |
| 上传的 zip 登记为技能源 | 改为一次性导入:不出现在技能源列表,同名技能选择覆盖或保留;从 zip 导入的技能可编辑 | zip 每次独立上传,没有可以再同步的来源;之前每次上传都留下一个技能源,再次上传同一技能会因"来源不同"无法导入。启动时把早期登记的上传记录删除,从中导入的技能转为可编辑 |
实施中发现并修复的问题(同样影响之前的第三方技能,见 ADR-0022):
{dataDir}/data,技能生效目录位于其中,Agent 读不到技能的参考文档、运行不了脚本(Claude 的 Read 工具检查与 Codex 的沙箱都会拒绝)。现在只读放开服务管理的两个技能目录。验收:
custom-skills:e2e-lookup),Bash 读取技能的参考文件成功,读取 data/agent.db 被拒绝。