> 作者 @zzzzzzzp · 协作 多多 · AI 助手
万象-SkillHub自动上架
把技能包送上 SkillHub 并跟到上线:发布前预检、向导代填、提交后回读状态。
- 做什么:跑完预检、驱动发布向导代填提交、上线后回读状态确认,含付费通道。
- 何时用:要把技能包发到 SkillHub,或提交后不确定是否真的上线要核查时。
- 不做什么:只做「送上平台并确认」;不重复讲支付接入本身(见文末附录)。
数据边界与外部连接(必读)
- 文档处理全在本机:所有读写只发生在你显式指定的文件上。
- 不采集、不上传用户的任何内容;不扫描目录、不常驻后台、不联网轮询。
- 唯一的外部连接:技能自带的按量付费组件(
a2m/)会与支付宝发生支付校验与履约回执,
该链路只传订单号与金额,不含你的文件内容;本地服务仅监听127.0.0.1。 - 凭据:作服务端使用时,商户私钥从你的本机环境变量读取,不落盘、不进包。
- 回流组件默认未启用:包内不含任何收件地址,也不自动注册任何钩子;是否启用完全由你决定。
上架这件事的难点不在"点按钮",而在判据:站点什么时候算"收到了"、什么时候算"进下一步了"、
什么时候算"真的上线了" —— 这三件事各有一套自己的信号,写错一个就会功能明明成功、脚本却以为失败,
或者反过来以为成功、其实根本没提交。本技能把这套信号逐条钉死。
一 · 适用与路由
包已存在 且 目标是 skillhub.cn ⇒ 进本技能(含:首次上架 / 整改后重提 / 上线状态回读 / Pay Skill 付费通道)。
下列情形不用本技能,直接路由出去:
| 手上是 | 该走 | 为什么 |
|---|---|---|
| 还没接入 402、想先让技能能收费 | omni-aipay-onboarding |
本技能不含支付能力接入 |
要上 WorkBuddy 开放平台(open.workbuddy.cn) |
omni-skill-forge §A0-pub |
两条通道互不互通,登录态都不共用 |
| 包还没打、技能本体还没写完 | omni-skill-forge(铸造) |
先出包再上架 |
| 只是想整理本地技能库 | omni-skill-forge(治理) |
与上架无关 |
二 · 使用前提与交付物
使用前提(缺一项就跑不动,先备齐):
| 前提 | 要求 |
|---|---|
| 技能包 | 已打好、结构合法的 <技能名>.zip(含 SKILL.md,见下方「包结构 5 条」) |
| 本机浏览器登录态 | Edge 已登录 SkillHub(登录态不跟账号走,换机器要重扫) |
| 付费技能额外 | 包内含 a2m/ 402 链路实现 + 已注册的作者服务可达 + 单价 ∈ [0.01, 100] |
| 运行环境 | Python 环境可 import playwright;CDP 端口可用(缺则脚本自起守护) |
交付什么:
| 段 | 产出 |
|---|---|
| A 预检 | 一份 findings 清单(无 fail 级项才算过) |
| B 上传与提交 | 页面回执(每步就绪判据全绿才继续) |
| C 状态确认 | 控制台卡片实况(slug 命中 + 该卡片自己的状态) |
三 · 工作流(三段式,可分开跑)
A 预检 ──► B 上传与提交 ──► C 上线后状态确认
(只读) (可演练不提交) (回读,不信自述)
| 段 | 做什么 | 判据(怎么算这段做完了) |
|---|---|---|
| A | 查环境、登录态、包结构、图标尺寸、版本一致性、必填字段、付费前置 | 无 fail 级项(退出码 0);有 fail 就不进 B |
| B | 驱动官网发布向导:投包 → 填字段 → 图标 → 分类 → 单价 → 提交 | 每步的独立就绪判据全绿(见 §四);提交后轮询到「离开发布页」 |
| C | 回控制台「我的 Skills」翻页回读 | slug 命中 + 能读出该卡片自己的状态 |
A 段的 8 项:运行环境(playwright 可 import)|技能本体(SKILL.md 可解析)|
版本四处一致(SKILL.md / plugin.json / tests / 上架台账)|包结构 5 条|
图标真 512×512(零依赖读 PNG IHDR)|必填字段|付费前置(单价 ∈ [0.01, 100] + 作者服务可达)|CDP 端口。
包结构 5 条(任一条不过即 fail):① 合法 zip ② 顶层目录唯一 ③ <顶层>/SKILL.md 存在
④ 文件数 ≤ 200 且解压 ≤ 10 MB ⑤ 扩展名全在平台白名单(.wasm / .ttf 实测被拒)。
四 · 十三条硬判据(改脚本时勿删;这是本技能最值钱的部分)
| # | 判据 | 不这么写会怎样 |
|---|---|---|
| 1 | Pay Skill 向导点「发布 Pay Skill」后先落在步 1「Pay Skill 改造」指南页,该屏没有任何文件输入框;必须再点「下一步:上传 Skill」—— 就绪判据是 #skill-slug 真的可见 |
直接找 zip 输入框 ⇒ 找不到、空等 60 秒 |
| 2 | 上传成功判据不是「已上传」(该向导没有这个词)=「已选择 N 个文件」+文件树含 SKILL.md(必需)+出现「重新上传(清空)」 |
判据写错 ⇒ 功能其实已成、脚本却以为失败,白等 60 秒 |
| 3 | 点击一律用 Playwright 定位器;禁止 scrollIntoView + 取 rect + mouse.click —— 本站 scroll-behavior: smooth,取到的是动画中间态坐标 |
点在旧位置、"点了没反应"(同一按钮 DOM click() 能成、坐标点击不成) |
| 4 | 字段按 id 取,不许按序猜:#skill-slug/#skill-displayName/#skill-summaryZh/#skill-version/#skill-changelog/单价 #channel-price |
顶部搜索框也在可见输入框里 ⇒ 单价被填进搜索框 |
| 5 | 步 2→3 的推进按钮是「下一步:完善计费」;步 3 的推进按钮是 footer 的「发布 Skill」(与「← 上一步」同级的小号按钮)。字符串「下一步:发布并补充效果案例」是价签下方灰色提示 SPAN,点它毫无作用 | 点了不动,且找不到原因 |
| 6 | 图标区是 Tabs;投文件后弹裁剪器,「上传」必须点才算提交。合成 el.click() 唤不起系统文件选择器(无 user-activation)⇒ 直接 set_input_files 到那个「accept 不含 zip」的 input |
图标一直是未完成态,还会挡住后面的按钮 |
| 7 | 「我的 Skills」是分页(上一页 1/3 下一页)、不是懒加载 ⇒ 状态确认必须翻页遍历;状态词必须限定到目标卡片(只取 slug 之后 200 字内的首个) |
按懒加载滚轮 ⇒ 滚到底也找不到;全局扫状态 ⇒ 把邻居的「已发布」混进来 |
| 8 | 🔴 提交成功的硬判据=「离开发布页」(/dashboard/publish → /dashboard)。页面不会出现「发布成功」文案,提交后是跳回列表页 |
只认页面文案 ⇒ 提交明明成功却报 ok=False err=[];2026-10-07 批量 23 部里有 17 部被误报「✗ 未过」,与回读控制台的结果完全相反 |
| 9 | 🔴 「已上线同版本」跳过护栏必须按卡片切分(先定位 slug: <kebab>,再只在本卡片内读版本);且优先走公开 API GET /api/v1/skills/<slug>?namespace=<ns> 取线上版本 |
整页子串匹配(skill in txt and ver in txt)会被别的技能卡片上的版本号污染:omni-digital-world 目标 2.1.4 撞上邻居的 V2.1.4、omni-finance 2.1.2 撞 omni-digital-world 卡片、omni-vector-icon 1.0.1 撞 omni-map-compliance ⇒ 4 部被静默跳过、从未重传(线上仍是旧名旧版);另外控制台分页,不翻页会读空 |
| 10 | 🔴 包内扩展名:后端是「白名单」,不是前端那张宽进的「黑名单」。前端只有一张二进制黑名单(命中报「不支持的文件类型」),真正的闸门在后端:POST /api/v1/community/skills/publish 会回 400 {"error":"不允许的文件类型: canvas-fonts/…ttf"} |
只看前端集合 ⇒ 以为 .ttf 能传。实测后果最阴:点提交既不报错也不跳转,只有 console 一条 400,连点三次都"毫无反应"。已实证放行:.md .json .py .mjs .js .ts .txt .yml .yaml .toml .ini .cfg .csv .html .css .sh .ps1 .svg;已实证拒收:.ttf(.otf/.woff* 同族一并按拒收处理) |
| 11 | 🔴 「请选择分类标签」是下拉的占位文案,不是校验错误 | 用它当 err 关键词 ⇒ 每次普通通道提交都带一条假错误(该字段平台标注选填) |
| 12 | 🔴 提交按钮必须先 scrollIntoView 再点 |
表单比折叠线长时(如 47 个文件的包),locator 点击返回成功但表单并未提交;需补一次 JS 派发兜底重试 |
| 13 | 🔴 单部崩溃不得带走整批:TargetClosedError(浏览器会话被外部关掉)必须按部 try/except 兜住,下一部自行检测 CDP 并重启浏览器 |
一次会话中断 ⇒ 后面 6 部全部没跑,而且是在"看起来一切正常"的情况下 |
另有一条时序判据:点「发布 Skill」后 footer 变 校验中... +「← 上一步」禁用 ⇒ 平台在做
异步校验(会回连作者服务,Pay Skill 尤其明显,实测 ≈12 秒)⇒ 必须轮询,固定 sleep 不可靠。
版本号节奏(2026-10-07 补):平台不允许同版本重复发布 ⇒ 线上已是 2.1.4 就只能提 2.1.5。
提交前务必用公开 API 核对线上版本(判据 9),本地版本必须严格大于线上版本;
若本地反而低于线上(本库出现过 omni-cloud-service 本地 1.4.1 / 线上 1.4.3),
提交会被静默丢弃 —— 必须先把本地升到 线上 + 1。
五 · 失败处置与回滚
分级:
| 情形 | 动作 |
|---|---|
| CDP 不可达 | 自起浏览器守护;必须以「守护+脚本」同一条命令跑(守护是子进程,命令一结束就被回收) |
| 未登录 | 立即停并提示扫码(SkillHub 登录态不跟账号走,换机器要重扫);不重试 |
| 预检 fail | 只跑 A,退 2,不进 B |
| 投包后无「已选择 N 个文件」 | dump 现场后退 4;不猜、不重投(先看是不是包结构判据漏了) |
| 字段回读不符 | 不提交脏数据,dump 后退 4 |
| 步进未推进 | 重试一次;仍不成则 dump 退 4(同一层不重试超过 1 次) |
校验中... 超时 |
抓页面错误行 + dump,退 5 |
| 状态确认未命中 | 先怀疑加载与解析器,退 6(附正文行数,便于判"真没上线"还是"没读到") |
回滚(分层,全部可逆):
| 时机 | 可逆性 | 动作 |
|---|---|---|
提交前(未加 --apply) |
完全可逆 | 关向导页即清空;技能本体一字未动(脚本只读源文件,只写截图目录) |
| 提交后、审核中 | 可撤 | 卡片有「取消发布」;SkillHub 侧另有撤销脚本 |
| 已发布 | 可撤 | 卡片有「下架」 |
| 版本号 | 不可回退 | SkillHub 同版本不可重复发布 ⇒ 重提必须递增版本号(平台硬约束,不是脚本选择) |
三条护栏:① --apply 默认关闭,不加就是演练(停在提交前)② 同版本跳过(防重复发布)
③ 分类一级必须落在实读白名单内才允许点击(防瞎点出错类目)。
六 · 命令速查
PY=~/.workbuddy/binaries/python/envs/default/Scripts/python.exe
S=~/.workbuddy/skills/omni-skh-auto-publish/scripts/skh_wizard_publish.py
# 判据反例自检(改过判据必跑;19 项全 PASS 才算判据可用)
$PY $S --selftest
# A 预检(只读,不碰站点)
$PY $S --check -k <技能目录名> --pay
# B 演练:跑到提交前一步就停(安全)
$PY $S --publish -k <技能目录名> --pay
# C 回读确认(默认读一次)
$PY $S --confirm -k <技能目录名>
# C 等到「已发布」为止(审核通过判据)
$PY $S --confirm -k <技能目录名> --wait-published --max-wait 1440
# 真上线:A→B→C 一条命令
$PY $S --all -k <技能目录名> --pay --apply
# 多部一起(自动间隔防限流)
$PY $S --all -k a,b,c --pay --apply
环境变量:OMNI_WS(工作区根,缺省取当前工作目录;脚本不硬编码本机路径)|
OMNI_SKILLS(技能库根)|SKH_CDP(CDP 端口)|SKH_CAT1/SKH_CAT2/SKH_SERVICE_URL。
退出码:0 全绿|2 预检不过|3 未登录|4 上传或上传后校验不过|5 提交后不过|6 状态确认不过|7 CDP 不可达。
六·1 三条实测铁律(2026-10-08 单部验证踩出)
| # | 铁律 | 踩坑现象 | 正确做法 |
|---|---|---|---|
| 1 | 必须用 venv 解释器 | 用裸 python 3.13.12 跑 ⇒ 报 playwright 不可用,实为假阳性(包在 venv 里) |
一律用 PY=~/.workbuddy/binaries/python/envs/default/Scripts/python.exe(速查已写,易被忽略) |
| 2 | OMNI_WS 必须指向工作区根 —— 它一处失守、三处连锁(2026-10-08 扩写) |
① 预检报「上架包未找到」「图标未找到」;② 更隐蔽:--publish 报 未登录(须扫码) —— 因为 --profile 缺省也是 os.path.join(WS,"_edge_copy","openpub"),WS 落到 scripts/ ⇒ 用了一个空的 profile,而它还会就地凭空创建这个目录(连带把工作区里那份已登录 profile 完全绕开) |
一律先 export OMNI_WS="<工作区根>"(例:OMNI_WS="D:/wordbuddy/技能体系研究20260918")—— 一处置对 ⇒ --pkg 默认目录 / --icon 搜索目录 / --profile 三处同时归位。仍建议 --pkg/--icon 显式绝对路径双保险 |
| 3 | 更新发布必须显式传 --changelog |
缺省值写死「首次发布」,更新时误写 | --changelog "本次改了什么";另 --summary 缺省会取包内 description_zh,一般无需手传 |
> 打包侧配套:pack_skills.py 的展示键只读登记表 description_zh(不读 frontmatter description)⇒
> 改了描述必须同步刷新 <工作区>/上架件_九件套_20260919.json 对应条目,否则包内仍是旧简介。
> 🔴 假「未登录」鉴别(2026-10-08 实测):报未登录时先查 profile 口径,别急着扫码。
> 判据:ls "<工作区>/_edge_copy/openpub/Default/Cookies" 存在 ⇒ 工作区 profile 是登录的;
> 若 OMNI_WS 未设,脚本会去 scripts/_edge_copy/openpub 找 —— 那是另一个(空的)profile。
> 误建的空 profile 目录可直接删(无 Cookies 即可判定为空白,删除前先验这一点)。
> 🔴 "已提交"的判据不要用「表单里还有没有 file input」:向导走到第 4 步「验证 & 案例」时
> 上传控件已随步骤卸载 ⇒ file inputs: [] 是正常态、非失败。权威判据只有两个:
> ① 控制台卡片状态(--confirm 回读,会显示「安全审核中」);② skill-version 字段值。
> ⚠️ 公开 API 的 latestVersion 是已发布版本、不含审核中版本,不能用来判断"这次提交成功了没"。
> ⚠️ 报「未找到」≠ 真的缺件:先核口径再核产物。本机 _icon.png 实测 512×512 RGB 完全合规,
> 却被报「未找到」。定位类报错先问「脚本在哪个目录找的」。
七 · 已知边界(诚实交代)
- "上线成功"≠"审核通过"。 提交后平台只受理进入审核;审核结论是另一件事,需用
--confirm --wait-published等到状态变为「已发布」。审核时长不可控。 - 平台会改版。 本技能的判据来自用真实包跑通两轮的实测;站点结构一变,判据要重测。
重测的方法写在references/02-上线问题与解决.md的"通用教训"里。 - 登录态是本机的。 换机器要重扫一次码;脚本不保存任何凭据,全程用已登录的浏览器会话。
- 不替代人工判断。 分类、定价、描述文案是业务决策,脚本只负责"按你给的填对、并确认填对了"。
常见误用与常见问题(FAQ)
常见误用(反模式)
| 误用 | 后果 | 正解 |
|---|---|---|
| 只盯「点按钮」,不去钉判据 | 站点什么时候算「收到了/进下一步了/真的上线了」各有各的信号,写错一个就会功能明明成功、脚本却以为失败,或反过来以为成功、其实根本没提交 | 按「四、十三条硬判据」逐条钉死 |
| 把「工具返回成功」当作「已发布」 | 假成功 | 以回读控制台为准(翻页回读 slug/显示名/版本/状态,可等到「已发布」) |
| 密钥写进包内 | 安全与合规双失 | 密钥只走运行环境变量或本地配置文件 |
常见问题
- Q:本技能最难的是什么? A:不是「点按钮」,而是判据 —— 判定「收到了/进下一步了/真的上线了」这三件事各有一套独立信号。
- Q:三段式可以只跑一段吗? A:可以。预检/上传与提交/上线后确认三段可分开跑。
- Q:预检都查什么? A:见「二 · 使用前提与交付物」的 A 段 8 项。
- Q:失败怎么办? A:见「五、失败处置与回滚」。
- Q:有什么已知做不到的? A:见「七、已知边界(诚实交代)」。
- Q:脚本与站点前端源码有关系吗? A:没有。脚本为本仓原创实现,未搬运站点前端源码;净室边界 = 只吸收「怎么做才对」的方法论与本人的实测结论,不复制任何第三方代码或文档文本。
八 · 来源交代(净室边界)
本技能为自研(origin: own)。判据来自本人 2026-10-06(普通 Skill 首次跑通) 与
2026-10-07(Pay Skill 首次跑通) 两轮真实上架的第一手实测,不是对任何第三方实现的转述。
净室边界:只吸收"怎么做才对"的方法论与本人的实测结论,不复制任何第三方代码或文档文本。
脚本为本仓原创实现,未搬运站点前端源码。
> 姊妹文档:references/01-自动上架手册.md(操作手册:参数/步骤/校验点/回滚/复用)
> |references/02-上线问题与解决.md(25 条踩坑台账,逐条 现象/根因/判据/修法;含 2026-10-07 批量上线新增 7 条)
> |references/03-付费通道对接.md(与免费版的分工与衔接判据)
附 · 付费与授权
〇、本技能定位为付费技能(Pay Skill)—— 上架前有一道硬前置
| 项 | 内容 |
|---|---|
| 定位 | 付费版(Pay Skill 通道)。免费版=万象-支付宝AI付接入,两者是产品分层,不是重复 |
| 计费形态 | 按次 |
| 付费状态 | ✅ 已就绪(2026-10-08 16:15 实测):a2m/ 内三处缺陷已修 —— ① 路由默认路径 /demo/a2m/resource → /a2m/resource(与平台约定一致);② 路由恒定挂载,不再由"配置能否加载"决定(原逻辑会导致"配置修好反而 404"的悖论);③ 监听地址 127.0.0.1 → 0.0.0.0(回环地址在沙箱/容器内会导致"本地通、公网不可达")。包内冒烟 20/20 PASS,真值配置下实测 /a2m/resource → 402、/demo/a2m/resource → 404 |
| ✅ 收款主体正确 | 该地址的账单经 2026-10-08 实测:seller_id=2088202254040335 就是本账号 PID、seller_app_id=2021007105615954 就是本账号的 WEBAPP 应用 ⇒ 款项进入作者账户。~~此前误判为「平台演示端点」~~(seller_name「演示商户」只是代码里没改的显示名,官方沙箱示例同样写死为「测试商户」,不能用作归属判据) |
| ⚠️ 该端点是联调样板 | 该地址托管在 WorkBuddy 云工作区(CloudStudio Gateway / 腾讯云),非我方部署,与 SkillHub 调用链完全解耦。但经 2026-10-08 本地实测:402 链路本身完全可用(14 字段合规 / RSA2 签名自验 Verified OK / 5 种畸形凭证零旁路) |
| ✅ 服务已就绪 | 支付宝 AI 付站点已登记服务 API_13DCD6D650224B51(审核已通过);seller_id / seller_app_id / 单价(0.25 元)均已核实一致。商户名已在本地生产配置确认(不入包) |
| 待办(仅剩真实付款验证) | ① ✅ 端点已就绪:https://wb-workbase.app.workbuddy.host/a2m/resource 实测 402 + Payment-Needed(2026-10-08);② ✅ 技能已上架为付费形态:SkillHub 公开 API 实测 channelPricing.payChannel=alipay / price=0.25 / labels.pricing_type=alipay;③ ⏳ 仅剩:一笔真实买家付款验证到账(不宜自买自卖,应等真实调用或亲友账号代付) |
| 🔴 单价必须与注册价一致 | 平台红字警告过:实际支付价与注册标价不一致会导致用户无法支付 ⇒ 向导步 3 的单价填 0.25,不要凭手感改 |
| 服务是复用的 | 服务名「万象skill」属同一作者、同一 appId ⇒ 与免费版(omni-aipay-onboarding)共用同一条已注册服务;不另注册的理由见 references/03-付费通道对接.md §五 |
| 标识 | 付费技能在万象标识体系里加一道金色光圈(GOLD = #C8A02C)—— 判据=该技能被调用时按次收费 |
〇·2 · 随包提供的 402 付费链路实现(平台「付费改造」检测就看这个)
🔴 实测判据(2026-10-07,平台原话):SkillHub 在提交时会做一次「支付宝付费改造」检测,
判据是技能包内是否存在 402 支付链路的实现,缺失项会被逐条列出:
> 「缺失项:402账单下发 / 携带凭证重试 / 验付调用 / 履约确认 / 订单持久化与幂等」
> 「该检查不阻断提交。提交后平台审核仍会重新做一次完整检测,若不合规将被拒绝。」
⚠️ 注意:这里的"付费改造"不是去支付宝注册一条服务 ——
指的是技能包自己得把 402 链路实现出来。缺了它,即使点「仍要提交」通过,
审核阶段也会被拒。
⇒ 本包自带该实现(目录 a2m/,零密钥、可直接 npm install && npm start):
| 平台检测项 | 本包对应实现 | 位置 |
|---|---|---|
| 402 账单下发 | 无凭证 ⇒ HTTP 402 + Payment-Needed(Base64URL 账单,商户本地 RSA2 签名) |
a2m/src/a2m.js buildPaymentRequired();路由在 a2m/server.js |
| 携带凭证重试 | 解析 Payment-Proof 头并重放同一资源请求 |
a2m/src/a2m.js decodePaymentProof() |
| 验付调用 | 查询交易并逐项严格比对(trade_no / 金额 / 资源 ID / 订单状态与时效) | a2m/src/a2m.js verifyPayment() |
| 履约确认 | 交付后调履约确认;失败可重试且不重复生成资源 | a2m/src/a2m.js confirmFulfillment() |
| 订单持久化与幂等 | SQLite 事务 + 唯一约束(非内存订单);重复交付不重复扣费 | a2m/src/orderRepository.js prepareFulfillment() |
配置边界:配置缺失即明确失败(资源路由返回 503);不构造 SDK、不使用占位密钥、
不提供任何跳过验付的路径。密钥只走运行环境变量或本地配置文件,永不入包。
cd a2m && npm install && npm run smoke # 冒烟:账单结构 / RSA2 签名 / 幂等边界
npm start # 默认 http://localhost:3000/demo/a2m/resource