技能上架流水线(元技能)
〇 定位与价值
把"从零做一个能发布的 skill"和"迭代更新一个已发布 skill"两条链路,连同"无 Chrome 时用 Edge 自动上传 SkillHub"的能力,封装成一个可复用元技能。
- 新建场景:给个概念 → 产出达标 SKILL.md → 自动上传。
- 更新场景:给个已有技能 + 改点 → 诊断 → 改 → 升版本 → 自动上传(同 slug 走升级迭代,不建重复条目)。
- 上游依赖:质量体检复用「方案代码优化器」的 100 分 Rubric;上传复用本技能自带 scripts/(Edge CDP 发布三件套)。
一 三分钟上手(先跑通最小闭环,再进阶)
第 1 分钟 · 只做体检,不发布(零风险,随时可跑):
cd <本技能目录>/scripts
python preflight_check.py --selftest
看到 [selftest] 通过 16/16 即说明门禁本身工作正常。
第 2 分钟 · 扫一遍你要发的技能:
python preflight_check.py "C:/path/to/my-skill"
- 输出
✅ 未检测到…泄露风险→ 干净,可继续。 - 输出
⛔ 发布前合规检查未通过: 发现 N 处风险→ 按提示改词,或把确属误报的词逐行写进.publish-allow。
第 3 分钟 · 真发一次(需 Edge 已开 9223 端口,详见 §七):
python publish_community.py --check-slug my-skill --cdp-port 9223
返回 {"available":true,...} 即登录态正常,可进入 §七 Step 5 的完整发布命令。
进阶(按需再读):§五 构建新技能 / §六 迭代已有技能 / §七 Edge 上传全链路 / §八 100 分 Rubric。
> 路径约定:下文所有 <venv> 指你的托管 Python 解释器,<skills-root> 指技能目录根,
> ~ 指用户主目录。发布前按你的实际环境替换,不要照抄别人的用户名路径。
二 触发与不触发表
| 场景 | 是否触发 | 本技能做什么 |
|---|---|---|
| "做一个 skill" / "新建一个技能" / "把某流程固化成 skill" | ✅ 触发 | 走 §五 模块A 全链路 |
| "优化 / 迭代 / 升级某个 skill 到 100 分"并发布 | ✅ 触发 | 走 §六 模块B + §八 Rubric |
| "发布 / 上传 skill 到 skillhub" | ✅ 触发 | 走 §七 模块D |
| 本机 Chrome 缺失或 9223 无响应、但装有 Edge | ✅ 触发 | §七 Step 1 的 Edge 替代方案 |
| 只想本地改文件、明确不发布 | ❌ 不触发 | 只做体检,不碰上传 |
| 用户明确要手动上传 | ❌ 不触发 | 只给清单与命令,不代为执行 |
| 只想问"我的 skill 能打几分"、不打算发布 | ⚠️ 半触发 | 只跑 §八 Rubric,跳过上传 |
三 前置依赖(一次性)
- Edge 已安装:
C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe(64 位在C:\Program Files\...)。 - Python 环境:任意 Python 3.8+;推荐用托管 venv(下称
<venv>)。 - websocket-client(多数环境默认未带,发布前必装):
"<venv>/Scripts/pip.exe" install websocket-client
四 输出契约(你会拿到什么)
| 输入 | 产出 | 形态 |
|---|---|---|
| 一个概念 / 一段流程描述 | 达标 SKILL.md(含合法 frontmatter + 六段骨架) | 文件 |
| 任一份 SKILL.md | 100 分 Rubric 逐维评分表 + 缺陷清单 | 表格 |
| 一个待发布目录 | 合规预检报告(file:行号 + 命中词 + 原文片段) |
stdout |
| 一个待发布目录 + 9223 端口 | 线上 listing(版本 / 展示名 / 简介 / 变更说明) | HTTP 响应 |
| 任意输入 | 退出码:0 通过 / 1 命中风险 / 2 输入错误 / 3 运行异常 | 进程码 |
发布成功的判定:HTTP 2xx 且响应体 ok:true,并以响应里的 "tags":{"latest":"X.Y.Z"} 为准确认版本。
五 模块A:构建新技能(从零)
- 定名与 slug:展示名中文,slug 用小写连字符英文(如
my-nice-skill)。 - 建目录:
<skills-root>/<slug>/,放SKILL.md;如需脚本建scripts/。 - 写 frontmatter(发布脚本靠它解析,必须带):
```
name: <展示名>
slug: <slug>
version: 1.0.0
description: <一句话说清解决什么、给谁;平台强制校验,缺失直接 400>
summary: <简介,与 description 可同源>
tags: [标签1, 标签2]
``
⚠️ **description是平台硬性校验项,不是可选项。** 缺失时发布返回:{"code":"SKILL_MD_FRONTMATTER_MISSING","error":"SKILL.md 的 frontmatter 缺少 description 字段"}`
(HTTP 400,且发生在打包上传之后,白跑一轮。发布前先 grep 一下 frontmatter。)
🔴 tags 只能写行内数组 tags: [a, b, c],严禁写 YAML 块列表(2026-09-07 实测,
代价是 8 个技能线上标签全为 0、占 96.3% 下载量在标签检索里搜不到):
CLI 的 parse_skill_md_frontmatter() 只认 key: value 与 key: [a, b],
tags:\n - x 被解析成空字符串 → 发布载荷 "tags": ... if isinstance(list) else []
→ 发空数组。发布后必须逐个调 API 复核标签数(?namespace=<你的namespace>),
不要相信"我本地写了标签"。
- 正文骨架:定位/触发/流程/框架/边界/质量门。能用表格就用表格。
- 100 分质量体检(见 §八 Rubric),未过不出手。
六 模块B:更新已有技能(迭代)
- 定位:
<skills-root>/<slug>/SKILL.md;读现有 frontmatter 的version(线上以_skillhub_meta.json/ dashboard 为准)。 - 诊断:按 Rubric 逐维打分,列缺陷清单与改进点。
- 改:最小破坏原则改写;补缺失模块(如决策链、数据降级、反虚构、质量门)。
- 升版本:
version主版本或次版本 +1(必须 ≥ 线上版本,否则发布 409)。变更写入--changelog或正文## 变更块。 - 复用 slug 发布:同一 slug 自动走升级迭代(发新版本,不建重复条目)。
七 模块D:Edge 自动上传(无 Chrome 也能发)
> 核心洞察(已验证):发布脚本只连 127.0.0.1:9223 标准 CDP,浏览器无关,Microsoft Edge(Chromium)可完全替代 Chrome。
Step 1 判断 Edge 是否已被用户占用(tasklist | grep -cE "^msedge\.exe"):
- 0 个进程 → 用默认 profile 直接启动(保留登录态,无需重登):
"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --remote-debugging-port=9223 --remote-allow-origins=* https://skillhub.cn/dashboard
- 有进程(用户正在用) → 不要杀进程。原因:Edge 单实例会把新参数转发给已有实例导致端口起不来,而强杀又会打断用户浏览。改用独立 profile 启动,两者并存互不干扰:
msedge.exe --remote-debugging-port=9223 --remote-allow-origins=* --no-first-run --no-default-browser-check \
--user-data-dir="<skills-root>/../scratch/edge-skillhub-profile" \
https://skillhub.cn/dashboard
代价:该 profile 无登录态,需用户在新窗口登录一次。
Step 2 验证端口:curl -s http://localhost:9223/json/version 见 webSocketDebuggerUrl 即通。
Step 3 用 check-slug 探测登录态(比肉眼看页面可靠):
python publish_community.py --check-slug <slug> --cdp-port 9223
| 返回 | 含义 |
|---|---|
{"available":true,"slug":"..."} |
已登录 且 slug 可用,可直接发布 |
{"available":false,...} |
已登录,但 slug 被占用 → 换 slug,或走同 slug 升级迭代 |
{"error":"unauthorized"} |
未登录 → 让用户在 9223 那个 Edge 窗口登录 |
⚠️ 判读线上版本以发布返回体的 tags.latest 为准。
GET /api/v1/skills/{slug} 详情接口有缓存延迟,刚发布完仍可能返回旧版本号。
不要据此误判"发布失败"或"版本没上去"——以发布响应里的 "tags":{"latest":"X.Y.Z"} 为准。
Step 4 守候发布模式(推荐,避免来回等待):
独立 profile 需用户手动登录,与其反复询问,不如写守候脚本:轮询 check-slug(10s/次,上限 15min),
一旦返回不再是 unauthorized,立即 subprocess 调 publish_community.py 发布。
守候脚本必须放临时目录,不要放进待发布技能目录(会被打包上传)。
用后台任务启动,发布完成自动通知。实测:用户登录后全程 44 秒完成发布。
Step 5 发布(需 cd 进本技能 scripts/ 以解析兄弟模块):
cd "<本技能目录>/scripts"
"<venv>/Scripts/python.exe" publish_community.py \
--folder "<skills-root>/<slug>" \
--slug <slug> --version <新版本号> \
--display-name "<展示名>" --summary "<简介>" --changelog "<变更>" \
--allow "<skills-root>/<slug>/.publish-allow" \
--cdp-port 9223
八 模块C:100 分质量 Rubric(方案代码优化器)
逐维评估,加权合计 100;全过才算达标交付:
| 维 | 权重 | 达标要点 |
|---|---|---|
| 1 触发精准度 | 10 | 明确触发词 + 非触发边界(什么不干) |
| 2 输入解析歧义 | 10 | 多义/重名/集团vs子公司等歧义处理规则 |
| 3 数据降级 | 15 | 显式 L1-L4 降级阶梯(API→Web→推断→待补充) |
| 4 反虚构 | 15 | 反虚构铁律:不杜撰真实品牌/认证/数据;补全标 [推断]/[待补充] |
| 5 框架深度 | 15 | 结构清晰、有可复用框架,非流水账 |
| 6 场景映射 | 10 | 输入→打法映射完整,有兜底 |
| 7 交付可用 | 10 | 产出可直接用的模板/脚本 |
| 8 风险边界 | 8 | 版权/合规/能力边界/冲突输入处理 |
| 9 输出模板 | 5 | 给出具体输出格式 |
| 10 质量门 | 2 | 含交付前自检清单(10 项) |
九 常见错误与应对
| 错误做法 | 为什么错 | 正确做法 |
|---|---|---|
只在本地写了 tags,发布后不复核 |
CLI 只认行内数组;块列表会被解析成空串,线上标签变 0,等于在标签检索里消失 | tags: [a, b, c] 行内写;发布后调 GET /api/v1/skills/{slug}?namespace=<ns> 逐个复核标签数 |
| 用详情接口的版本号判断"发布成功没" | 详情接口有约 24h 缓存延迟,刚发完仍返回旧版本,容易误判失败而重复发布 | 以发布响应体 "tags":{"latest":"X.Y.Z"} 为准 |
| 用户正在用 Edge 时强杀进程来腾出 9223 | 打断用户浏览;且 Edge 单实例转发参数,杀了也未必起得来 | 用 --user-data-dir 独立 profile 并存启动 |
把真实公司名 / 客户名写进 .publish-allow 一发了之 |
allow 只抑制门禁、不改变"内容已泄露"的事实,真实泄露词必须改内容 | 先改内容;allow 只用于「门禁误报的元词汇」 |
| 版本号比线上小还硬发 | 返回 409,且容易让人误以为"发了只是没显示" | 先看线上版本,version 必须 ≥ 线上 |
| 把守候脚本 / 临时测试文件留在待发布目录 | 会被一起打包上传,暴露本地路径与内部结构 | 临时产物一律放技能目录之外 |
门禁命中后直接 --no-preflight 绕过 |
绕过的可能是真实泄露,不是误报 | 先看命中片段;确属误报才用 allow 或授权后 --no-preflight |
十 异常与兜底
| 现象 | 处理 | 退出码 |
|---|---|---|
preflight: 目标不存在: <path> |
路径写错或目录没建。核对路径后重跑 | 2 |
preflight: 无法识别目标(需为目录或 .zip/.plugin) |
传了单个文件。传目录或 zip | 2 |
[警告] --config 文件不存在, 该配置未生效 |
禁用词文件路径错了,门禁会比预期松。修正路径或去掉该参数 | 继续,门禁按默认规则 |
[警告] --allow 文件不存在, 抑制规则未生效 |
allow 路径错了,门禁会比预期严(旧误报会重新报出来) | 继续,按无 allow 处理 |
[警告] 无法解码, 该文件未被扫描: <file> |
文件编码异常。转成 UTF-8 后重扫,否则这份文件等于没检查 | 继续,其余文件照常 |
{"error":"unauthorized"} |
9223 那个浏览器窗口未登录。让用户在该窗口登录,再重跑 check-slug | — |
| 端口 9223 连不上 | Edge 未带 --remote-debugging-port=9223 启动,或被防火墙挡。按 §七 Step 1 重启 Edge 后 curl 验证 |
— |
| 发布返回 409 | 版本号 ≤ 线上版本。升版本后重发 | — |
websocket-client 缺失 |
<venv>/Scripts/pip.exe install websocket-client 后重试 |
— |
| 未预期的异常 | 脚本兜底打印 [错误] 未预期的异常: <类型>: <详情>,不抛裸 traceback |
3 |
批量 / 守候场景:单条失败不中断整批——守候脚本每轮独立 try,登录态未就绪就继续下一轮轮询,不会因一次网络抖动终止。
十一 合规预检与敏感词(重要)
发布脚本会扫描打包内的全部文件,包括门禁自己的源码——因此门禁的默认规则里不含任何真实公司名,
否则会出现"发个检查敏感词的技能,结果包里自带敏感词"的自击中。
规则来源(后者追加,不覆盖):
1. 默认通用词(包内,只含"司内 / 内部研发"这类泛内部标识)
2. 环境变量 SKILLHUB_PREFLIGHT_CONFIG 指向的 JSON
3. 用户级 ~/.workbuddy/skills/.preflight_blocklist.json(不随包上传,放你自己的品牌词)
4. --config 显式指定的 JSON
用户级配置文件格式:
{"company_tokens": ["你的品牌名", "某客户名"], "client_tokens": ["甲方", "乙方"]}
- 安全边界:
.publish-allow仅当显式--allow时生效,默认(不带--allow)门禁完整生效,不削弱。
它是点开头文件,不随包上传,因此需要在你本地自建(每行一个抑制串,#开头为注释)。 - 强发需授权:仅当用户确认某词确属误报时,才用
--no-preflight或加 allow;未授权不得擅自绕过。 - 先修内容再谈放行:真实泄露词改内容;allow 只抑制"元词汇"型误报(如文档里讲解"客户"这个词本身)。
十二 双质量门(交付前自检)
构建质量门(§八 10 维全过) + 发布质量门:
1. [ ] Edge 调试端口 9223 已验证通
2. [ ] 已装 websocket-client
3. [ ] SKILL.md 含合法 frontmatter(name/slug/version/description/summary)—— 缺 description 会 400
4. [ ] --version ≥ 线上版本(避免 409)
5. [ ] slug 经 --check-slug 确认可发布,且返回值已判读登录态(见 §七 Step 3)
6. [ ] 用户正在用 Edge 时已走独立 profile,未强杀进程
7. [ ] .publish-allow 已就位(若打包内含发布脚本等自击中词)
8. [ ] 展示名/简介/变更说明已填
9. [ ] 发布命令从 scripts 目录执行(兄弟模块可 import)
10. [ ] 发布返回 HTTP 2xx 且 ok:true
11. [ ] 安全扫描/reviewStatus 已知(queued/pending/approved 均属正常,如实回报不夸大)
十三 实战案例(可照抄跑通)
需求:把一个内部用的"周报整理流程"固化成 skill 并发布到 SkillHub。
选型:新建场景 → 走 §五 模块A(不是迭代,因为线上没有这个 slug)。
数据:
- slug:weekly-report-trimmer
- 目录:<skills-root>/weekly-report-trimmer/,含 SKILL.md + scripts/trim.py
命令:
cd <本技能目录>/scripts
python preflight_check.py --selftest
python preflight_check.py "<skills-root>/weekly-report-trimmer"
python publish_community.py --check-slug weekly-report-trimmer --cdp-port 9223
python publish_community.py --folder "<skills-root>/weekly-report-trimmer" \
--slug weekly-report-trimmer --version 1.0.0 \
--display-name "周报整理流水线" --summary "把原始周报整理成结构化纪要" \
--changelog "首版" --cdp-port 9223
产出:
- 预检:✅ 未检测到公司名/客户/会议/文件名泄露风险。(退出码 0)
- 探测:{"available":true,"slug":"weekly-report-trimmer"} → 已登录且 slug 可用
- 发布:HTTP 200、ok:true、响应体 "tags":{"latest":"1.0.0"}
若预检不通过:报告会给出 文件:行号 + 命中词 + 原文片段。先把那些词替换掉再重发;
只有确认是"文档在讲这个词本身"的误报,才写进 .publish-allow。
十四 反虚构与边界
- 绝不虚构:质量评分、发布状态均以脚本/工具真实返回为准;不确定用
[待补充]。 - 本元技能不替用户杜撰待发布 skill 的内容,只按 Rubric 体检 + 跑上传。
- 门禁放行(allow /
--no-preflight)必须用户明确授权,且只抑制误报,不掩盖真实泄露。
十五 已验证清单(v1.1.5 实跑)
| 项 | 命令 / 断言名 | 实测结果 |
|---|---|---|
| 门禁自检 | preflight_check.py --selftest |
通过 16/16,退出码 0 |
| 包内零品牌词 | 「默认企业名规则 ⊆ 通用白名单」 | OK(默认词仅"司内 / 内部研发") |
| 通用代词不误报 | 「本公司」「某某集团」「公司法」三项 | 均不报 COMPANY |
| 具体企业名仍报 | 「星辰科技有限公司」 | 命中 COMPANY |
| 否定式豁免 | 「无客户相关信息」 | 不报 CLIENT |
| 会议词需同线 | 「周会」单独 / 「甲方 + 评审会」 | 前者不报,后者命中 MEETING |
| 外置配置叠加 | 「--config 追加生效」 | ZZBRAND 仅在加 --config 后被报出 |
| GBK 文件不漏检 | 「GBK 文件可解码」「GBK 文件被真正扫描」 | 均 OK,不静默跳过 |
| 目标不存在 | preflight_check.py <不存在的路径> |
[错误] 目标不存在,退出码 2 |
| 非目录非 zip | preflight_check.py <某个 .py 文件> |
[错误] 无法识别目标,退出码 2 |
| 未知参数 | preflight_check.py --bogus-flag <dir> |
退出码 2,无裸 traceback |
| 本技能自扫(不加 allow) | preflight_check.py <本技能目录> |
命中 91 处,退出码 1(见下说明) |
| 本技能自扫(加 allow) | 同上 + --allow .publish-allow |
0 处风险,退出码 0 |
> 为什么自扫会命中 91 处:本技能的全部内容就是在讲「哪些词算敏感词」,
> 文档与注释里必然出现这些词本身,这是结构性自击中,不是真实泄露。
> 判据是:包内不含任何具体企业名(已由自检白名单断言锁死),
> 其余命中全部是"文档在讲这个词"的元说明,用 .publish-allow 逐行抑制。
> .publish-allow 是点开头文件,不随包上传,且仅当显式 --allow 时生效。
十六 引用
- 自带
scripts/:publish_community.py(主)、cdp_driver.py(CDP 连接)、preflight_check.py(合规预检 + 自检),三者需同目录。 - 质量体检标准:复用「方案代码优化器」100 分 Rubric(本技能 §八)。
- 远程调试协议:Chrome DevTools Protocol(CDP,浏览器无关)。