SkillHub 批量发布与版本同步
把本地 skill 目录批量发布到腾讯 SkillHub,并按版本做三分支决策:
| 情况 | 动作 |
|---|---|
| 平台上查不到该 slug | PUBLISH(new) 新建发布 |
| 平台上 live 版本 ≠ 本地版本 | PUBLISH(update) 更新版本 |
| 平台上 live 版本 == 本地版本 | SKIP(same) 跳过,不重复发布 |
| 已上传但还在内容审核中 | PENDING(audit-skip) 跳过,切勿重发 |
快速使用
PY="E:/WorkBuddyConfig/binaries/python/versions/3.13.12/python.exe"
S="E:/WorkBuddyConfig/skills/skillhub-bulk-publish/scripts"
"$PY" "$S/publish_skillhub.py" # 预演:只打印计划,不发布(默认)
"$PY" "$S/publish_skillhub.py" --execute # 真正发布
"$PY" "$S/publish_skillhub.py" --execute --only <关键字> # 只处理匹配的 skill
"$PY" "$S/backfill_version.py" # 预演:列出本地缺 version 的 skill
"$PY" "$S/backfill_version.py" --write # 把平台版本回填进本地 SKILL.md
"$PY" "$S/publish_clean.py" --src <skill目录> --slug <平台slug> \
--version 1.0.0 --display "中文名" [--exclude 文件名] [--execute]
产物:执行日志重定向到文件;result_v2.json / backfill_result.json 存明细。
三个脚本的分工:
| 脚本 | 干什么 | 什么时候用 |
|---|---|---|
publish_skillhub.py |
批量同步全部本地 skill,三分支决策 | 日常主力,先跑它 |
backfill_version.py |
把平台 live 版本号写回本地 frontmatter | 批量同步后仍有 PUBLISH(update) 且本地版本显示 0.0.0 时 |
publish_clean.py |
按白名单过滤后发单个 skill | 某个 skill 因文件类型或版权内容被整包拒绝时 |
标准收尾动作:批量 --execute 跑完后,立刻执行一次
backfill_version.py --write,把版本号写回本地,消除版本棘轮(见坑 10)。
关键前提
- SkillHub CLI 必须在
C:\Users\<you>\.skillhub\skills_store_cli.py,且已login(tokenskh_...存于credentials.json::user)。 - 没有 pyyaml,脚本用自写的容错 frontmatter 解析器(只处理单行
key: value)。
踩坑清单(每条都是实测翻车点)
1. slug 必须是「连字符」,不是「下划线」★ 最高频 ★
平台 publicSlug 形如 user-7e6f1fc8-<name>(连字符),但 namespace handle 是 user_7e6f1fc8(下划线)。
> 我曾把 plat_slug 用 HANDLE(含下划线)拼出来,得到 user_7e6f1fc8-xxx,
> 结果所有 GET /api/v1/skills/{slug} 全部返回 404,表现为「所有 skill 都没发布过」。
PLAT_HANDLE = "user-7e6f1fc8" # 拼 slug 用这个(连字符)
HANDLE = "user_7e6f1fc8" # 查详情用这个当 namespace(下划线)
2. 别用 search 查 live 版本,用详情接口
skillhub search 返回的是全局热门结果,输入精确 slug 也常常返回无关的别人的 skill,
无法据此判断「是否已发布」。可靠方式是直接 GET:
GET {host}/api/v1/skills/{plat_slug}?namespace={handle}
Header: Authorization: Bearer <skh_token>
→ 200: body.latestVersion.version 即当前版本;404: 未发布
3. --skip-self-upgrade 必须放在子命令「之前」
它是全局参数。写成 publish --skip-self-upgrade ... 会报
error: unrecognized arguments,导致全部发布失败(rc=2)。
# ❌ ['publish', '--json', '--skip-self-upgrade', dst]
# ✅ ['--skip-self-upgrade', 'publish', '--json', dst]
4. 发布前要补平台强制要求的前端字段,且不能破坏分隔符
平台对 SKILL.md frontmatter 的校验很严,常见报错:
- SKILL.md front matter 缺少结束标记 ---
- SKILL.md 缺少 displayName
- version 不是合法 SemVer: 1.5(必须 x.y.z)
- slug 不合法(必须 kebab-case 3-128 char)(含中文即非法)
> 我第一版重写 frontmatter 时只输出了 ---\nkeys...\n,漏了结尾 ---,
> 于是 8 个本来正常的 skill 全被判「缺少结束标记」。务必输出完整的 ---\n...\n---。
安全做法:复制一份到临时目录再改,绝不改动用户原始 SKILL.md:
注入/修正 slug(规范化、非 ASCII 转拼音/丢弃)、version(补成 SemVer)、
displayName(缺失时用目录名兜底),其余键原样保留。
5. 429 限流必须退避重试,否则连锁失败
平台发布频率限制很紧,sleep(1.2) 会连续吃 429:
{"success": false, "status": 429, "error": "发布频率过高: 请求过于频繁,请稍后再试"}
做法:每次发布间隔 ≥15s;遇 429 按 20/40/60/80/100s 递增退避重试(最多 5-6 次)。
另外偶发 网络错误: [Errno 10054] 远程主机强迫关闭了一个现有的连接,多为限流伴随现象。
6. 409 VERSION_EXISTS = 已发布过此版本,应跳过而非升版本号
{"success": false, "status": 409, "error": "slug 冲突: 版本 1.0.0 已存在,请使用新的版本号发布"}
脚本以 rc == -409 表示这种情况,标记为 ALREADY(skip),符合「版本无变化则跳过」语义。
7. 版本号比较要语义化
本地 1.5 与平台 1.5.0 是同一版本,直接字符串比较会误判为「需要更新」。
统一 semver() 归一到 x.y.z 后再比。
8. 中文 slug 需转 ASCII
user-7e6f1fc8-谢文东-perspective 非法。建一张已知中文→拼音映射
(如 谢文东 → xie-wen-dong)后再 encode('ascii','ignore') 丢弃残余非 ASCII。
这块需要按实际字符补表,脚本里 CMAP 已收录「谢文东」。
9. 别用 shutil.rmtree 清临时目录(会触发沙箱钩子超时)
复用同一个临时目录路径时用 rmtree 清理,会命中本环境的
SAFE_DELETE_BULK_GUARD 并超时中断整轮发布。
> 解法:每次用唯一临时目录名(拼毫秒时间戳),完全不删除;
> 同理,跑脚本时也不要在命令行里用 rm -f xxx.log,会触发 SIGTERM。
10. 审核中的 skill 会被误判成「未发布」★ 极易重复上传 ★
发布成功返回为 "contentAuditStatus": "pending"。在此之前,详情接口的行为是:
GET /api/v1/skills/{slug} → 200 OK,但 body.latestVersion.version 为空串
> 我第一版把「live 为空」和「404」都当成未发布,
> 结果刚发成功的 skill 立刻被排进下一轮重发队列,撞 409 VERSION_EXISTS。
正确区分:HTTP 404 = 未发布;HTTP 200 + 版本为空 = 审核中。后者应标
PENDING(audit-skip) 并跳过,等审核通过(通常几十秒到几分钟)后自然变成 SKIP(same)。
elif not ok: action = 'PUBLISH(new)' # 404
elif live == '': action = 'PENDING(audit-skip)' # 200 但空版本 = 审核中
11. 本地缺 version 字段会造成「版本棘轮」,必须用回填根治 ★★★ ★
这是「版本无变化则跳过」能否成立的前提。若 frontmatter 没有 version,
脚本无从比对,只能按 live+1 兜底发布——每跑一次批量同步,版本号就 +1。
> 实测:某 skill 本地无 version,第一轮被推到 1.1.9,第二轮 1.1.10,
> 而内容其实一字未改。表面看「更新成功」,实际是无意义刷版本。
根治:批量发布后立刻跑 backfill_version.py --write,把平台已生效的版本号
写回本地 SKILL.md。回填后这类 skill 会正常命中 SKIP(same)。
(本次一次性回填了 9 个 skill,棘轮归零。)
12. 含「不允许的文件类型」或侵权内容的 skill 会被整包拒
平台错误:不允许的文件类型: xxx.docx。已实测会被拒的类型包含 .docx、
以及 ZIP 格式但扩展名非 zip 的打包产物(如 .skill,文件头为 PK\x03\x04)。
典型坏样本:skill 目录里塞了作为语料参考的整本有版权小说 txt。
这类目录不要整包公开发布(既是侵权风险,也必定被拒)。
处理方式:用 publish_clean.py 只打包白名单文件(默认 .md / .py),
其余一律剔除。实测一个 7.2 MB 的 skill 剔除后只剩 199 KB,仍完整保留了
SKILL.md + 10 个知识 md + 校验脚本,功能无损。
"$PY" "$S/publish_clean.py" --src .../webnovel-hit-style \
--slug user-7e6f1fc8-webnovel-hit-style --version 1.0.0 \
--display "网文爆款风格仿写与诊断" \
--exclude 坏蛋是怎样炼成的.txt --exclude webnovel-hit-style.skill --execute
注意:涉及版权/自愿与否的判断要先问用户再动手,不要擅自删他本地文件。
publish_clean.py 只操作临时副本,本地原目录保持不变。
13. 已知残留限制
latestVersion有延迟:刚发布的版本要过一会儿才进详情接口,见坑 10。dist之类衍生的二级 skill 入口:若目录里含SKILL.md会被一并发布,
没有则自动跳过(按目录扫描,天然排除)。- 未知/异常查询结果:保守跳过并标
UNKNOWN(skip-warn),绝不盲目发布。