SkillHub 付费发布闸门
把「本地 skill → SkillHub 商店」固化下来:先预检、再付一次钱、然后发布、最后跟审核。
计费模型(与官方 SkillPay 个人接入教程一致):闸门是唯一实际收费点——每次执行发布扣 ¥0.01,走支付宝 AI 付的 402 协议,付款成功才签发发布授权。SkillHub 计费页的「单次调用价格」是展示价,官方要求支付宝登记价、闸门账单价、SkillHub 展示价三处一致,所以发布时也要把 ¥0.01 提交上去。
> 展示价不会造成重复收费:平台不代扣,买家实际付的就是闸门那笔 402 账单。
> 价格随发布提交、没有发布后改价接口;填错只能 bump 版本重发。
先分清:免费版不需要适配 SkillPay(2026-10-28 用户明确)
SkillPay(402 闸门、per_call_price、Payment-Proof、三价一致)只属于付费版发布。用户没提收费、或明确说「免费版发布」时:
- 不做任何 SkillPay 适配:frontmatter 不带
pricing/per_call_price,包内不放付费/闸门/402 章节,发布命令不带--price,也不走paid.py probe/pay/complete。 - 直接
python3 scripts/publish.py publish <dir> --yes --changelog ...,线上无计费渠道,买家装了就能用。 - 包里已有付费章节的要整节删掉再发(参考 2026-10-28 旅行照片 20 连发:删「付费与交付」节后自检无「付费与交付 / aipay / Payment-Proof / 价格数字」残留)。
- 只有用户明确要收费的 skill 才进入本文件下面的付费流程。
批量免费发布的两个坑(实测):连续发布会 429,两次发布间隔 ≥70s;409 = 同 slug 同版本已存在,bump 版本重发。
什么时候用它
- 「发布到 skillhub」「上架这个 skill」「发到 skill 商店」「传到 skillhub」
- 「重新发布」「更新一下线上版本」「发个新版本」
- 「上次发布失败了帮我看看」「审核过没过」
单次价格与依赖
| 项 | 值 |
|---|---|
| 单次价格 | ¥0.01 / 次(闸门账单价 = 支付宝登记价 = SkillHub 展示价,三处一致) |
| 计费方式 | 按次,付款后签发一次授权,一次授权一次发布;SkillHub 计费页只展示不扣款 |
| 支付能力 | 支付宝 AI 付(HTTP 402 协议) |
| 买家依赖 | alipay-bot(支付宝官方包 @alipay/agent-payment,未装时提示 npx -y @alipay/agent-payment@1.0.28 install;版本钉死,需用户确认后再执行) |
| 卖家依赖 | skillhub CLI(用 scripts/install_cli.py 装,不跑远程脚本)+ skh_ API Key |
两份参考文档分工不同:references/skillpay.md 讲官方计费口径、402 链路与上架验收;references/ai-pay-hardening.md 讲 8 条改造硬指标与实测踩坑。
一步把 skill 改造成付费版(payify)
把「普通 skill → 付费版可发布」固化成 8 条硬指标,全部落在 scripts/payify.py 里。完整判定依据与踩坑记录见 references/ai-pay-hardening.md。
| # | 指标 | 不过会怎样 |
|---|---|---|
| 1 | frontmatter pricing: per_call + per_call_price > 0 |
线上没有计费渠道,等于免费发 |
| 2 | scripts/paid.py 存在 |
买家无从下单,无法收款 |
| 3 | paid.py 具备 ensure_wallet_ready / _complete_via_bot / _delivery_verdict / _extract_delivered |
钱包未开通时直接失败;验付/交付解析不完整 |
| 4 | 验付判据是严判(喂 payment_unconfirmed 必须拒绝) |
未付款也会白签授权(10-29 真实事故) |
| 5 | 扫码/下单后自带轮询(poll-payment-status --launch-url) |
用户付完要手动 complete,等于「自动检测失效」 |
| 6 | complete 支持 --trade-no |
moveToMobile 支付只回交易号,会误报「没有可用的支付凭证」 |
| 7 | SKILL.md 有付费章节且写了金额(## 计费 / ## 付费 / ## 定价 等) |
买家看不到价格与付款方式,等于没说清楚怎么付 |
| 8 | 付款交付后生成回执(scripts/receipt.py + 文档说明 + 真实落盘断言) |
用户付了钱却拿不到「买了什么/产物在哪」的凭据,事后无法对账 |
第 9 条是人工确认项:三价一致(frontmatter = 闸门账单 = SkillHub 展示价),脚本只能提醒。
python3 scripts/payify.py audit <skill-dir> # 只读体检,exit 2 = 有阻断
python3 scripts/payify.py apply <skill-dir> --resource-id <闸门资源号> [--price 0.01] [--intent "..."]
python3 scripts/payify.py verify <skill-dir> # 硬化要点 + 函数级回归(含反例)
apply 做五件幂等的事:注入硬化版 paid.py(资源号/用途按本 skill 改写)、补 frontmatter 定价、补 SKILL.md 付费说明(已有 ## 计费/## 付费/## 定价 等章节就只补「付费命令」块,不重复建节)、注入 scripts/receipt.py 交付回执工具并补「### 交付回执」说明、写 .payify.json 改造记录。
verify 不只是 grep 源码:它把目标 skill 的 paid.py 当模块导入,喂四份 payload 做行为断言——payment_unconfirmed 必须拒、只有 tradeNo 必须拒、isFulfillmentSuccess 必须放、resourceDelivered 必须放。这就是那次「未付款却签了授权」的回归测试。第 8 条同样不看「文件在不在」:它用探针产物真跑一次 write_receipt(),核对回执 JSON 的 trace.trade_no、artifacts[].sha256_12 与时间戳是否齐全。
改造通过后的标准动作(金额必须先给用户确认):
python3 scripts/paid.py probe # 出账单,展示金额/订单号/有效期
python3 scripts/paid.py pay --scan # 或 pay(钱包已绑定时自动扣款);两者都自动等待结果
python3 scripts/publish.py publish <skill-dir> --auth <authorization.json> --changelog "..." --yes
python3 scripts/receipt.py --auth <authorization.json> --artifact <产物> --slug <slug> --version <ver> # 交付回执
receipt.py 会把「付了什么」(交易号/金额/资源号)和「交付了什么」(产物路径/sha256 前 12 位)写成
receipts/<slug>-<时间戳>.json,并在终端打印 上传信息: {...}。这是付款之后必须返还给用户的凭据。
--yes 不能省:缺了它脚本会 input() 等交互,非交互环境直接被 SIGTERM(exit 137、零输出)。
主流程
脚本都在 scripts/ 下,纯标准库。
第 1 步 预检(免费,先跑)
python3 scripts/publish.py check <skill-dir>
一次查完:slug(kebab-case 2-128)/version(SemVer)/displayName 必填;包内图片、pdf、zip、字体等二进制会被服务端 400 拒绝;summary/description/tags/license 缺失只告警;顺便联网看 slug 是否已被他人使用。退出码 2 = 有阻断项。
第 2 步 dry-run(免费)
python3 scripts/publish.py dryrun <skill-dir>
只打包校验,不发请求。看到 ✓ Dry-run passed: <slug>@<version> 才算过。
第 3 步 生成账单并让用户确认(免费,只读写本地)
python3 scripts/paid.py probe
输出金额、商品名、订单号、账单有效期。必须先把金额和用途展示给用户,用户确认后再付款。
第 4 步 支付(两条路,同一张账单都能用)
python3 scripts/paid.py pay # AI 支付:钱包已绑定则由 AI 直接扣款
python3 scripts/paid.py pay --scan # 人工支付:终端画二维码 + 落盘 png,本人扫码确认
调官方买家能力 alipay-bot 完成付款(会弹出支付确认)。未付款、钱包未开通、账单过期都会停在原地,不会继续。
- AI 支付:钱包已绑定时自动扣款,不需要人工介入,适合智能体全自动跑。
- 人工支付:
--scan不自动扣款,由alipay-bot在终端画出二维码(并另存一张 png),本人用支付宝 App 扫码确认。金额、商品名与 402 账单完全一致。 - 为什么要单独一个开关:默认的
--json输出只回结构化结果、不画二维码,脚本还会捕获输出;--scan改成文本输出且不捕获,二维码才显示得出来。 - 钱包未开通或支付授权状态查不到时,
pay会直接提示改用pay --scan,账单沿用,不必重新probe。
第 5 步 验付并领授权
python3 scripts/paid.py complete
服务端验付 → 商家履约确认 → 签发 authorization.json(有效期 30 分钟,一次一用)。
第 6 步 发布
python3 scripts/publish.py publish <skill-dir> --auth <authorization.json> --changelog "本次改了什么"
顺序是:预检 → 校验授权 → 校验 token → 读展示价/头像 → 确认 → 真发 → 核销授权。成功后回报 slug、版本号、审核状态、商店 URL、展示价是否生效。价格与头像随发布提交,没有发布后改价接口;channelPricing.applied=false 说明价格没写进去,重发一次即可。
发布成功后脚本会自己落盘两份凭据:upload-results/<slug>-<version>.json(上传回执,由 publish.py 写,含 skillId/versionId/publicUrl)和终端那行 上传信息: {...}。再用 receipt.py --auth ... --artifact <skill-dir> 补一份交付回执,把付款交易号与发布产物指纹写在同一个文件里,一起交给用户。
第 7 步 跟审核
python3 scripts/publish.py status <slug>
命中搜索索引 = 已上线;查不到 = 审核中或未过审。输出里的「展示价」是商店当前展示的单次价格,应与闸门账单的 ¥0.01 一致。
删除条目(dashboard 没有按钮时)
dashboard 只在两种状态下渲染「删除」按钮,其余状态一律没有:
| skill 状态 | dashboard 给的操作 |
|---|---|
已封禁(banned=true) |
封禁详情 / 申述 / 删除 |
已发布且已下架(approved + unlisted) |
版本历史 / 评测 / 更新 / 上架 / 删除 |
平台审核拒绝(platform_rejected) |
只剩「查看原因 / 重新发布」——没有删除,也没有下架 |
安全审核不通过(rejected) |
查看原因 / 重新发布 / 撤回发布 |
审核中(pending / platform_review) |
版本历史 / 撤回发布 |
也就是说,被平台审核拒绝后放弃的旧条目会永久占着 slug,前端永远不给删除入口。接口层没有这个限制:
python3 scripts/publish.py delete <slug> # 先打印状态与 dashboard 现有操作,不动手
python3 scripts/publish.py delete <slug> -y # 确认后真删(不可恢复)
删除会清掉条目、版本与下载记录,slug 当场释放,可以重新用它发布(新条目从 v1.0.0 起算)。
判断「到底删没删干净」看三处:dashboard 列表、他人视角的 /api/v1/users/<handle>/skills、搜索索引——三处都不再出现即为已下架;按 slug 直接访问详情接口仍可能返回旧记录(软删除保留),不影响商店展示。
计费与头像(发布时设置,没有发布后改价接口)
官方 CLI 的 payload 是固定键名白名单,装不下 channelPricing / iconUrl——这就是「用 CLI 发出去的 skill 没价格、没头像」的原因。dashboard 的做法也一样:在发布 payload 里带这两个字段。
channelPricing: {price, payChannel: "alipay", currency: "CNY"},price就是计费页的单次调用价格(展示价):官方明确「SkillHub 价格用于展示,实际收费以服务端账单为准」,要求支付宝登记价、账单价、展示价三处一致iconUrl来自upload-icon上传(png/jpg/webp,≤2MB)- 服务端没有发布后改价接口:想改价只能 bump 版本重新发布,新版本走一遍审核
- 不带
channelPricing或加--no-price→ 发布为免费技能 - 发布后用
publish.py status <slug>核对「展示价」与「线上版本」是否如期
CLI 安装(不执行远程脚本)
官方安装指引是把刚下载的脚本直接交给 shell 运行(下载即执行),多数安全扫描器会判为高风险。这里换成等价但可审计的做法:
python3 scripts/install_cli.py --check # 先看现状,不联网
python3 scripts/install_cli.py # 安装 / 升级
python3 scripts/install_cli.py --expect-sha256 <hex> # 钉死安装包指纹
python3 scripts/install_cli.py --accept-new # 官方升级、指纹变了才用
它做的事只有三件:HTTPS 下载 → 标准库解包 → 拷贝 4 个文件。
| 环节 | 做法 |
|---|---|
| 下载 | 域名白名单(skillhub.cn、官方 COS 桶),强制 TLS,重定向后仍校验域名 |
| 解包 | 只取 skills_store_cli.py / skills_upgrade.py / version.json / metadata.json,拒绝绝对路径与 .. |
| 执行 | 一行远程代码都不跑,包内 install.sh 直接跳过;~/.local/bin/skillhub 是本地生成的三行启动器 |
| 完整性 | 三段式:--expect-sha256 钉死 > ~/.skillhub/kit.sha256 上次记录 > 首次记录;指纹变了默认中止,必须 --accept-new 显式放行 |
授权规则
- 没有
authorization.json时publish直接拒绝(退出码 6),不会偷偷发出去; - 一次授权只对应一次发布,发布成功后写
used_at核销; - 超时 30 分钟自动失效,需重新
probe→pay;
失败对照表
| 现象 | 含义 | 处理 |
|---|---|---|
402 + Payment-Needed |
还没付款 | 正常,按账单付;不要绕过授权 |
502 PRECREATE_FAILED |
服务端预下单失败 | 不是买家问题,反馈服务方 |
402 反复出现(付过款) |
凭证过期/金额不一致 | 核对账单金额,重新 probe |
履约确认失败 |
资源已给、确认未成 | 用同一凭证重试,别重复付款 |
| 钱包「未开通」 | 买家未开启支付宝支付 | 按提示开启;未开通则不产生任何扣款 |
channelPricing.applied=false |
展示价没写进这一版 | 版本已提交但展示价未生效,重新发一次(无改价接口) |
| 401 / 403 | token 失效 / 未实名 | 重新生成 key、完成实名 |
| 409 slug 冲突 | 同 slug 重复提交 | bump version 或换 slug |
| 429 | 发布频率限制 | 等 1 分钟,禁止循环重试 |
| 400 不允许的文件类型 | 包里有图片/二进制 | 移出 skill 目录 |
未找到 skillhub CLI |
环境缺失 | python3 scripts/install_cli.py(下载后本地解包,不执行远程脚本) |
硬规则
- 凭证只回显类型(
skh_***),不打印任何字符片段;key 只从SKILLHUB_KEY或~/.skillhub/credentials.json读。 - 安装与升级 CLI 只用
install_cli.py;任何「先下载脚本、再把下载内容直接交给 shell 或 eval 执行」的写法都不允许出现在本 skill 里。 - 外部依赖只给版本钉死的官方包命令,不代替用户执行安装。
- 不改写用户 skill 内容,除补 frontmatter 必填项(需用户同意)。
- 付款前必展示金额;未获确认不发支付。
- 发布前确认三价一致:闸门账单价 = 支付宝登记价 = SkillHub 展示价。
- 用户没明确说「发」时,只做预检,不进入付费。
- 审核 pending 不等于上架,别对用户说「已上架」。
- 发布是外部副作用,
publish默认二次确认;用户明确要求才加-y。 - 发布包里不得出现自用旁路(
<!-- self-use -->段):publish/dryrun会自动生成裁剪副本,别为此手改原目录。
参数速查
| 命令 | 用途 |
|---|---|
publish.py check <path> [--no-remote] [--json] |
预检(含查重) |
publish.py dryrun <path> |
预检 + CLI dry-run |
publish.py publish <path> [--version X] [--changelog T] [--auth FILE] [-y] |
发布(需授权) |
publish.py status <slug> |
查上线状态与展示价 |
publish.py delete <slug> [-y] |
删除条目(dashboard 无按钮的 platform_rejected 死角用它) |
publish.py login [--key K] |
保存 API Key |
install_cli.py [--check] [--force] [--expect-sha256 H] |
安装/升级 skillhub CLI(不执行远程脚本) |
publish.py upload-icon <图片> |
上传头像拿 iconUrl(png/jpg/webp,≤2MB) |
publish.py publish <path> --price 0.01 --icon-url <URL> |
直连发布:payload 带 channelPricing / iconUrl |
paid.py probe / pay / complete / status |
SkillPay 账单、支付、验付授权 |
payify.py audit / apply / verify <dir> |
付费版改造体检 / 一键补齐 / 硬化回归 |
receipt.py --auth <授权> --artifact <产物> |
交付回执:付款凭证 + 产物指纹,打印 上传信息 |
paid.py pay --scan |
人工支付:终端画二维码 + 落盘 png,本人扫码确认 |
paid.py pay --no-wait |
只下单不等待(自己再跑 complete) |
paid.py pay --no-wait-wallet |
钱包未开通时只发开通码,不自动等待绑定 |
环境变量:SKILLHUB_PUBLISH_RESOURCE、SKILLHUB_PUBLISH_GATE、SKILLHUB_PUBLISH_SHARECODE、SKILLHUB_PAY_STATE_DIR、PAYMENT_PROOF、SKILLHUB_KEY、SKILLHUB_CLI_SHA256、SKILLHUB_HOST、SKILLHUB_HOME、SKILLHUB_BIN_DIR。
变更记录
-
1.2.9 — 付款前后两段自动化。付前自检 AI 付钱包:
pay先跑alipay-bot check-wallet,未开通时只发开通码(apply-wallet的链接 + 二维码图片),并默认每 5 秒轮询、约 90 秒内检测到bound就继续出支付码;--no-wait-wallet可关掉等待。付后自动识别:下单拿到needPolling后自动poll-payment-status(16×5s),outcome=success即自动402-query-payment-status取回资源并签发授权,不用再手敲complete;--no-wait保留旧行为。complete新增--trade-no:moveToMobile支付只回交易号、不回查询号,原先会误报「没有可用的支付凭证」。交付解析改为整棵 JSON 递归,兼容 CLI 的resourceResponse.body.content;以fulfillmentResponse.isFulfillmentSuccess判履约。 -
1.2.10 — 三处硬化。①
pay --scan补齐自动等待:扫码分支改为 JSON 下单,拿到mediaPaths与payScheme后每 5 秒轮询(最多约 100 秒),outcome=success即自动验付并签发授权,不用再手敲complete;--no-wait可关。②验付判据收紧:以前只要返回里有tradeNo就放行,status=payment_unconfirmed(付款未确认)也会被误判成功、白签授权;现在必须有fulfillmentResponse.isFulfillmentSuccess=true或resourceDelivered=true或真实交付正文才放行,PAYMENT_UNCONFIRMED/PENDING/PROCESSING一律拒绝。③complete --trade-no与_extract_delivered深度递归保留。 -
1.2.11 — 发布成功后返还上传信息:新增
_save_upload_receipt(),把回执写到<付费状态目录>/upload-results/<slug>-<version>.json(含 slug/name/version/skillId/versionId/status/publicUrl/changelog/文件数/时间戳/服务端原始响应),同时在终端打印一行上传信息: {json},便于脚本或人工复核。直连发布与 CLI 发布两条路径都会落盘。 -
1.2.12 — 新增
scripts/payify.py(audit / apply / verify)与references/ai-pay-hardening.md:把「一个 skill 一次改造成付费版」固化成 6 条硬指标 + 1 条人工确认项。apply幂等注入硬化版paid.py(改写资源号与支付用途)、补 frontmatter 定价、补 SKILL.md 付费章节/命令块、写.payify.json记录;verify把目标paid.py当模块导入并喂 4 份 payload 做行为断言(payment_unconfirmed必拒、只有 tradeNo 必拒、isFulfillmentSuccess必放、resourceDelivered必放),即 10-29「未付款却白签授权」bug 的回归测试。同时publish.py check对带per_call_price的 skill 自动跑这 6 条,阻断项直接拦在付费前并给出修法命令。 -
1.2.13 — 把「付款后返还上传信息」纳入改造与检验。新增
scripts/receipt.py(stdlib 交付回执工具:write_receipt()+ CLI,从authorization.json取交易号/金额/资源号作trace,记录产物path/bytes/sha256_12,落盘receipts/<slug>-<时间戳>.json并打印上传信息: {...})。payify.py新增delivery_receipt阻断项:工具存在 + SKILL.md 有「### 交付回执」+ 真跑一次探针回执核对字段与产物指纹;apply相应多两件幂等操作(注入receipt.py、补回执章节),并修掉「已有## 计费章节被重复建节」的老问题(现在识别## 计费/## 付费/## 定价/## 单次价格/## 价格)。paid.py在授权签发后直接打印回执命令,避免用户付完不知道怎么对账。 -
1.2.15 — 发布回执不再污染下一次打包:
upload-results/与receipts/是运行时产物,1.2.14 发布后它们被算进包体(9 → 11 个文件),已加入EXCLUDE_DIRS。判断依据:这两个目录只会越攒越多,且记录了历次发布的 skillId/versionId,不该发给下一个买家。 -
1.2.14 — 全量自查后的一致性修复。①发布前自动裁掉自用段:新增
stage_clean_copy(),publish/dryrun检测到<!-- self-use -->或# >>> self-use标记时先复制到临时区整段删除再打包,原目录不动——此前 README 承诺的「构建脚本会裁掉」并不存在,免付开关会随包发到商店。②paid.py三处help被写成字面\uXXXX转义(功能正常但源码不可读)已还原为中文;提示文案里的python3 paid.py补回scripts/;docstring 补SKILLHUB_PUBLISH_RESOURCE。③硬指标从 7 条校正为 8 条(漏写了doc_section),ai-pay-hardening.md编号与判定对齐,payify.py里重复的# 7注释改为 7/8,两处章节名列表合并为PAID_SECTION_MARKS,apply内联的两份付费命令块抽成PAID_COMMANDS常量。④receipt.py文件名加毫秒,同秒多次调用不再互相覆盖。⑤references/skillpay.md过时的「有 tradeNo 就签发」判据改为严判口径并在 SKILL.md 建立引用(此前是无人引用的孤儿文档);apply不再往.payify.json写空的gate/sharecode。