SkillHub/ClawHub 技能发布

把本地 Skill 打包并发布到 SkillHub(腾讯 skillhub.cn)与 ClawHub,覆盖预检、发布、状态回读与竞品对标全链路,并在上传被拒时用二分/ddmin 脚本把「服务端 WAF 拦内容(566)」与「包结构/字段问题」精确区分开。当用户说「发布技能到市场」「上架 skill」「SkillHub 提交失败」「Failed to fetc…

robin

@oracis

Install

$ openclaw skills install @oracis/skillhub-publish-skill

SkillHub / ClawHub 技能发布

发布一个技能实际要同步三个地方:GitHub(源码)、SkillHub(腾讯主市场)、ClawHub(海外市场)。 本技能覆盖全链路,并在任何一步被拒时给出可定位根因的判定,而不是猜。

铁律:报错先分类,别乱猜

上传失败的报错只有四类,每类的对策完全不同,先看报错文案归位:

报错文案类别对策
不支持的文件类型: xxx包内容用 scripts/publish.py 发(它按白名单自动过滤)
字段格式校验 类包内容跑 scripts/preflight.py 查 frontmatter
slug 已被占用 / 409 / 500slug 冲突publish.py 会自动追加后缀重试并回写 SKILL.md
提交失败 / Failed to fetch / net::ERR_FAILED 566 / CORS 报错服务端 WAF 拦内容跑 scripts/waf_bisect.py 定位,改内容,别改结构

不要把第四类当成网络/代理/浏览器扩展问题。判据是客观的:用本机脚本向发布端点 POST 同一份 multipart, 401 就说明请求已到达应用层(正常,只差登录态),566 才是被 WAF 拦。

这一条是本技能与其他发布类技能的核心差别。其他工具只处理 400/409/429, 遇到 566 会误导你去「检查元数据与文件类型」——方向完全错了。

铁律二:查不到 ≠ 不存在

判断「某个技能有没有」「上架了没有」之前,先确认你用的接口能看到什么范围。 公开接口天然过滤掉未公开、审核中、已下架的条目 —— 拿它当「是否存在」的判据会得到假阴性。

想知道什么用哪个别用哪个
这个技能公开发布了吗status <slug>(公开搜索)—
我到底发过哪些(含测试残留)mine(我的后台)❌ 公开搜索查不到
某个 slug 归谁status / 看 namespace.handle—

之前踩过的具体坑:用公开搜索查自家测试探针,搜不到就判定「已被平台自动清理」, 实际 18 个条目里 12 个探针都还挂在「我的 Skills」里。清点自己发了什么,永远用 mine。

标准工作流

Step 1 预检(不联网,可做卡点)

python scripts/preflight.py <skill目录>            # 人读报告
python scripts/preflight.py <skill目录> --json      # Agent 解析

三级输出:ERROR(会被拒,必须修)/ WARN(能过审但影响质量)/ INFO(发布操作提醒)。 存在 ERROR 时退出码为 1,可直接接发布脚本卡点。

覆盖:frontmatter 缺字段与非法值、位图与独立 LICENSE 拒收、包结构与白名单、密钥泄漏、安全红线、 占位符残留、WAF 命中特征(本脚本独有)。

Step 2 确认元数据

python scripts/publish.py validate <skill目录>

缺 slug / version / displayName 会明确列出。补进 SKILL.md frontmatter——这些字段不影响 skill 本地运行。

Step 3 预演(不上传)

python scripts/publish.py publish <skill目录> --version 1.4.0 --dry-run

打印将要上传的文件清单、体积、图标识别结果、被排除的文件及原因, 以及与线上版本的比对结果(versionCheck: OK / SAME / TOO_OLD)。确认无误再真发。

Step 4 发布

python scripts/publish.py publish <skill目录> --version 1.3.0 --changelog "变更说明" \
       [--icon icon.png] [--category dev-programming] [--subcategory dev-script]

脚本自动完成:白名单过滤 → 图标两步上传 → 分类 key 校验 → 429 指数退避 → slug 冲突自愈。 成功返回 skillId / versionId / slugUsed / category / subCategories; 失败会打印分类结论 + 下一步动作。

分类建议写进 SKILL.md frontmatter(比分次传参可靠,下次发版自动带上):

category: dev-programming
subCategories: [dev-script, dev-code-gen]

不填分类的话平台会显示「未分类」——注意「选填」不等于可以不管, 市场里未分类的条目曝光会明显吃亏。可选值用 publish.py categories 查。

带图标时务必确认响应里 iconAuditStatus: "pending" —— 若是 null 说明图标没收到 (那通常意味着把图片错当成 multipart part 传了,见上方「图标」注意事项)。 分类则看响应里有没有回显 category 字段。

Step 5 验证发布结果(不用开浏览器)

python scripts/publish.py status <skill目录>     # 传目录会自动比对本地/线上版本
python scripts/publish.py status <slug>          # 只查线上

输出线上版本、下载/安装/收藏数、更新时间,并给出同步状态:

同步状态含义
✓ 已同步本地版本 == 线上版本,审核已通过
↑ 本地更新本地版本更高 —— 还没发,或已提交仍在审核中
✗ 版本倒挂本地版本 ≤ 线上版本 —— 发上去会被服务端拒

⚠️ search 索引有延迟:刚发布(还在审核)的版本不会立即出现在查询结果里, 可能仍显示上一个线上版本。这是正常的,不是发布失败。

⚠️ status 走的是公开搜索接口,看不到未公开条目。要清点「我到底发过什么」 (含审核中、已下架、测试残留),用 mine 命令 —— 那是另一个接口(/api/v1/dashboard/skills)。

Step 6 清点与清理自己的技能

python scripts/publish.py mine                 # 列出我名下全部技能(含线上搜索看不到的)
python scripts/publish.py mine --json          # 机器可读
python scripts/publish.py rm <slug>            # 只提示,不执行(安全护栏)
python scripts/publish.py rm <slug> --yes      # 确认删除:自动「先下架 → 再删除」

mine 与 status 的区别(踩过坑,别搞混):

命令数据源看得到什么
status <slug>公开搜索 /api/v1/search只有已公开条目;未过审/已下架的查不到
mine我的后台 /api/v1/dashboard/skills我发的全部:审核中、已下架、测试残留都在

删除是两步(服务端强制):直接 DELETE 会返回 409 只能删除已下架的 Skill,请先下架。 rm 已自动串好 /unlist → DELETE,并对「已下架再删」的重复调用做了容错。 删除不可恢复,所以默认不给 --yes 就只打印风险提示、不执行。

Step 7 ClawHub

clawhub skill publish <skill目录> \
  --slug <slug> --name "<显示名>" \
  --categories "<14 项之一>" --topics "a,b,c" \
  --changelog "..."

--source-repo / --source-commit 成对给或都不给,只给一个报 must be provided together。

两条容易白费功夫的约束(详见 references/clawhub-catalog.md):

  • ClawHub 没有图标能力 —— CLI 全量无 --icon/--logo 参数,SKILL.md 里放 <slug>.png 对它无效。别为 ClawHub 折腾图标链路。
  • ClawHub 的分类 slug 是另一套(只有 14 个),与 SkillHub 的 13 个一级 key 完全不通用。最容易踩的是 developer-tools ❌ → 正确是 development。 传错报 Unknown skill category slug;超过 3 个报 Categories are limited to 3。
  • 传了 --categories/--topics 就一定发新版本(即使文件没改)——这是给 已发布技能补分类的官方路径,CLI 会自动 bump patch。

Step 8 GitHub

见 references/github-repo.md。本机已有可用凭据,不需要向用户索要 PAT。

版本递增是硬性要求

服务端要求更新时 version 必须严格高于线上版本(相同或更低都会被拒)。

publish 命令会在发布前自动比对并拦下,避免白发一次:

{ "success": false, "blockedBy": "version_check",
  "localVersion": "1.2.9", "remoteVersion": "1.3.0",
  "error": "版本号 1.2.9 比线上 1.3.0 更低,服务端会拒绝更新",
  "fix": "把版本号改成高于 1.3.0 的值(如升到 1.3.1),或加 --force 跳过此检查" }
  • 首次发布(线上查不到该 slug)会自动跳过检查。
  • 网络异常时降级为提示,不阻断发布。
  • 确需绕过:--force。

竞品对标

python scripts/publish.py compare "<关键词>"

按下载量列出同类技能,用客观数据判断投入方向 —— 比凭感觉判断「我们有什么优势」可靠:

关键词「skill 发布」同类技能(按下载量排序)
版本            下载     安装  技能
1.2.1           998      26   ClawHub Skill 发布避坑指南  @clawhub_tudoubudou
1.0.1            83       0   开源发布(GitHub Skill 发布器)  @user_73cb5b59

下载量反映曝光面,安装量反映真实使用。

三平台版本必须一致

发布前对齐六个地方的 slug/name:本地目录名、SKILL.md name、SKILL.md slug、manifest name、 GitHub 仓库名、ClawHub --slug。

SkillHub 已发布的 slug 不可改(更新对话框里只读、无改名入口),所以它以 SkillHub 为准当「锚」, 其余平台向它对齐,不要新建重复 Skill。

资源路由

文件什么时候读
scripts/preflight.pyStep 1;发布前必跑
scripts/publish.pyStep 2-6;日常发布主入口(含 status / compare / mine / rm / categories)
scripts/waf_bisect.py出现 566 / Failed to fetch 时定位根因
references/waf-details.md需要理解 WAF 命中特征、判据原理与规避写法时
references/icon-upload.md要传图标/封面,或怀疑图标没生效(iconAuditStatus: null)时
references/categories.mdSkillHub 要填/补分类,或后台显示「未分类」时
references/clawhub-catalog.mdClawHub 要填分类/话题,或问 ClawHub 要不要图标时(14 个可用 slug)
references/path-traps.md目录路径没生效、slug 被自动改写,或同名仓库不是扁平技能仓库时
references/cli-install.md需要安装/修复官方 CLI,或走网页 CDP 兜底路径时
references/github-repo.md需要建 GitHub 仓库并推送时

手头没有现成图标? 用 zero-dep-icon-gen 技能本地生成 (零依赖手写 PNG,不装 Pillow,一条命令出图):

PY="C:/Users/DELL/.workbuddy/binaries/python/versions/3.13.12/python.exe"
$PY ~/.workbuddy/skills/zero-dep-icon-gen/scripts/gen_icons.py \
    --spec "my-skill:#4F46E5,#818CF8,browser" -o my-skill/
# 生成 my-skill/my-skill.png,文件名 = slug,find_icon() 会自动兜底找到

可用符号:browser cloud shield folder check download arrow-up gear search lock text(点阵文字)。--list-symbols 看全量。

打包规范(手工打包时必看)

  1. 白名单:只收 SKILL.md / manifest.yaml / scripts/*.py。 .gitignore / LICENSE / README 会被拒。别用 GitHub「Download ZIP」(会带上那几个文件)。
  2. 结构:所有文件放进一个与 skill 同名的顶层目录。
  3. 文件名:<slug>.zip(不带版本号)。≤10MB。
  4. 图标:走表单单独上传,绝不打进包(zip 内位图会被拒收)。且必须两步走: 先 POST /api/v1/community/skill-icons/upload(multipart 字段名 file)拿 iconUrl, 再放进 payload.iconUrl 提交。把图片直接当作 cover/icon part 会被服务端静默忽略 —— 发布仍返回 201,但 iconAuditStatus 恒为 null(pending 才是生效)。 publish.py --icon xxx.png 已封装这条链路,上传失败只告警不阻断发布。
  5. 打包用 zipfile 手打,键名必须用正斜杠(os.path.join 在 Windows 产生反斜杠会导致 KeyError):
import zipfile, os
TOP = "<slug>"
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
    for rel in ["SKILL.md", "manifest.yaml"] + ["scripts/" + f for f in os.listdir("scripts") if f.endswith(".py")]:
        z.write(os.path.join(repo, rel), TOP + "/" + rel)

安全铁律

  1. 绝不在回复中打印、展示或记录 token(skh_ / gho_ / ghp_ 开头一律脱敏)。
  2. 发布前必须跑预检、必须获得用户确认。
  3. 不改动目标 skill 的功能字段,只补平台专用元数据。
  4. 全程使用绝对路径。

边界与免责

  • 预检覆盖已知拒收原因,平台规则随时会变,最终判定以平台审核反馈为准。
  • WAF 是累计评分制黑盒,waf_bisect.py 给出的是定位方法而非稳定规则; 命中特征会变化,按「二分定位」的思路排查,别背特征表。
  • 预检不是安全审计,不能替代平台安全扫描。

元教训:别信文档,信真实请求

本技能里最贵的两个结论(WAF 566 的真身、图标的两步链路)都不是文档里写的, 而是打真实请求试出来的。踩过的坑值得记住:

  1. 「接口返回 2xx」不等于「这个字段生效了」。cover part 返回 201,但字段被静默忽略。 判断字段是否生效,要看响应体里对应的状态字段(如 iconAuditStatus), 而不是看 HTTP 状态码。
  2. 服务端对不认识的 part 常常静默丢弃,不报错。所以要靠「换个明显非法的值试试」来 反证:如果传一堆垃圾内容它也照收不误,说明它压根没读这个字段。
  3. 平台只有网页、没有 API 文档时,去前端 bundle 里搜端点名反查真实实现 (见 references/icon-upload.md 的「反查方法」),比猜字段名快得多。
  4. 官方 CLI 没做的功能,不代表平台没有这个 API —— 两边是独立的。

Top skills in this category