资产上架流水线
三类资产,一条流水线。技能、专家、连接器(含专家团)从本地目录走到平台待审核,都走同样三段:
本地目录 ──校验──▶ 可上传 zip + 上架信息表 ──上传──▶ 平台草稿 ──确认──▶ 待审核
三类之间只有包规范和表单字段不同,流程本身没有差别。差异全部写在三份规格文件里,正文只讲怎么做。
除首次一次性配置外,日常上架是一句话加一两次确认。
做什么,不做什么
做:认准资产类型、按该类型的官方规范校验、打包成平台能吃的 zip、生成上架信息表、通过浏览器驱动完成上传和提交。
不做,这六条不能越界:
- 不改你的包。校验出问题只列整改项,不擅自改内容、不替你补字段。
- 不代登录、不代签约、不代开收款账户。涉身份与资金的一律人工。
- 不替你定价。草稿结构里就没有价格字段,定价和收费方式由你在页面选。
- 不未经确认就提交。
submit不带--yes直接拒绝执行。 - 不覆盖 Buddy 应用。那是第 4 类资产(
/buddy-app/publish,表单式配置、不走 zip),本技能不管。 - 不催审核、不写申诉。审核进度在
/{类型}/all自己看,驳回原因按平台提示改包重传。
何时用,何时不用
用:刚做完一个技能 / 专家 / 连接器要上架;想知道手里的包能不能过;上传被驳回要看原因;要给已上架资产发新版本。
不用:
- 只想打包备份、不打算上架 → 普通压缩就够。
- 想改技能内容或结构 → 那是制作环节的事,本技能只负责送上平台。
- 想查平台规则原文 → 直接看官方文档,本技能的 references 只是摘要。
- 要发到 GitHub、npm 或别的市场 → 与本技能无关。
容易误触发的几种说法,看到先问一句再动手:
- 「帮我打包」/「package 一下」——用户多半只想压成 zip 存档,不打算上架。确认一句「要不要顺便送上平台」,没有就只做压缩。
- 「传个 zip 给我」——是要上传到开放平台,还是只是把文件发过去?前者走本技能,后者不是。
- 「这个技能能不能发布」问的是平台审核门槛,不是要你现在提交。答完再问要不要继续走上架流程。
边界判断不清时,先复述一遍用户要什么,确认「送上平台」这步确实在意图里再动。
先判型
三类包结构互不通用,不要拿技能流程硬套专家或连接器。看目标目录里有哪个标志文件:
| 标志文件 | 类型 | --type |
规范文件 | 体积 | 能否收费 |
|---|---|---|---|---|---|
SKILL.md |
技能 | skill |
references/skill_pack_spec.md |
3 MB | 能 |
.codebuddy-plugin/plugin.json 或根目录 plugin.json |
专家 / 专家团 | expert |
references/expert_pack_spec.md |
20 MB | 能 |
connector-meta.json |
连接器 | connector |
references/connector_pack_spec.md |
20 MB | 不能 |
专家团没有独立类型:仍然 --type expert,靠 plugin.json 里的 expertType: "team" 走团队校验(setting.json、members[]、成员头像、唯一 lead)。
官方模板不能照抄。design-experts.zip 的 categoryId 用了非法值 01-Design、包里没有声明过的 avatars 目录;trading-team.zip 的配置文件名是 setting.json(文档写复数)、categoryId 用了 15 个枚举之外的 00-ExpertTeam。照抄模板就是被连环拒的典型原因,逐项对照见 references/expert_pack_spec.md。
三个发布入口:
| 类型 | 创建页 |
|---|---|
| 技能 | /skill/publish?action=create&step=1 |
| 专家 / 专家团 | /expert/publish?action=create&step=1(/expert-team/publish 实测 404) |
| 连接器 | /connector/publish?action=create&step=1 |
流程都是「上传 zip → 平台开包解析 → 三步表单(配置 → 确认信息 → 提交审核)」,但第 2 步字段完全不同:技能是市场分类 + 服务类目 + 头像,专家是 profession / quickPrompts 那套,连接器没有收费项。
上架四道闸口
替用户操作的是别人的代码和别人的公开主页,错一步就是替人乱发布。四道门按顺序过,缺一不做:
- 意图门:只有用户明确说"上架 / 发布 / 挂平台 / 变现"才进入上传环节。用户说的是"检查一下能不能上架 / 看看有没有问题"时,只做本地校验与生成信息表,一个字节都不传出本机,并告诉他要继续随时说一声。
- 目标门:脚本自动探测的是"按修改时间最近的技能目录",不能默认就是他要的那个。先回显目录名 + 展示名 + 作者请他确认,点头才继续。
- 上传门:把 zip 传到平台等于把代码传出本机,是独立对外动作,上传前单独确认一次,不与提交审核合并。
- 提交门:点提交前,列明资产名、展示名、分类、服务类目、价格、zip 路径,取得明确确认后才点。定价与收费方式永远由用户定,收费方式发布后不可更改。
任一门没过 → 停在该门,说明缺什么,继续做其它不依赖这道门的步骤。
环境检查
动手前先跑只读检查,别凭猜。本节及下文的 scripts/... 都相对于本技能根目录;在别处调用时先 cd 到该目录,或写展开后的绝对路径(Windows 的写法见「主流程」第 1 步):
python scripts/check_environment.py # 快检
python scripts/check_environment.py --deep # 连平台登录态一起验
四态路由,已就绪的配置不重复讲:
| 状态 | 含义 | 处理 |
|---|---|---|
ready |
可探测项全通,登录态未核验 | 直接进主流程;上传前再核一次登录:加 --deep 或跑 browser_session.mjs status |
partial |
核心可用,自动上传不可用 | 照常校验/打包/信息表,交付时标注「需人工上传」并给替代路径 |
needs_setup |
Python 低于 3.8 | 只展示 Python 缺失项,装完重跑 |
unavailable |
依赖在但登录失效 | 给重新登录步骤;不代登录、不代授权 |
输出里 capabilities 逐项标明 validate / package / listing / upload / submit / icon-generation 哪些还能用,checks 每条缺失都带 fix。快检不探登录态,所以那一次 upload / submit 显示 unchecked 而不是 yes,pending 里会点名 platform_login——unchecked 不等于可用。
五项依赖各自影响什么:
- Python 3.8+(必需):全部脚本能力。缺失 → 停脚本流程,人工核对字段后手打 zip,交付时标注未走校验。
- Node.js 22+(仅自动上传需要):驱动用全局
WebSocket(22 起才默认可用)。缺失或低于 22 → 校验、打包、信息表、分类推断照常,上传改人工。 - 浏览器调试实例(仅自动上传需要):缺失 → 同上降级,先跑
browser_session.mjs launch。 - 平台登录态(仅自动上传需要):
open.workbuddy.cn的会话,存在专用 profile 里。缺失或过期 → 校验、打包、信息表照常,上传改人工;不代登录、不代扫码。 - Pillow(可选):只影响
make_icon.py生成头像。缺失 → 让用户自备 512×512 图形头像,校验脚本直接读文件头核对尺寸。
完整安装、验证、轮换、撤销步骤见 references/setup-guide.md;依赖与能力的机器可读映射见 skill-dependencies.json。
主流程
1. 判型并确认目标
按上面的判型表认类型。用户给了目录就用他给的;没给就自动探测最近修改的技能目录:
python scripts/package_for_open_platform.py --type skill # 技能,自动探测
python scripts/package_for_open_platform.py --type expert --skill-dir <目录>
python scripts/package_for_open_platform.py --type connector --skill-dir <目录>
Windows PowerShell 要用展开后的绝对路径(~ 不展开、python3 常缺):
python "$env:USERPROFILE\.workbuddy\skills\publish-skill-helper\scripts\package_for_open_platform.py" --type skill
项目不在默认目录时加 --search-root <项目路径>(可重复)。产物默认落到 ~/Downloads——zip 与上架信息表同放一处,要换位置加 --out <目录>。
探测结果必须回显给用户确认,这就是目标门。
2. 本地校验与打包
同一个命令完成校验和打包。它会解析配置、按该类型的官方规范逐项检查、推断市场分类并给置信度、打成带顶层目录的 zip、写出上架信息表草稿,并给出下一步路径。
产物固定三个,都写在 --out 指向的目录里:
| 产物 | 文件名 | 何时出现 |
|---|---|---|
| 可上传包 | <name>.zip |
校验通过 |
| 上架信息表草稿 | <name>_上架信息表.md |
每次运行都写,含校验状态与待填项 |
| 结构化草稿 | 标准输出里的 JSON,不落盘 | 每次运行都打;要存档自行重定向到文件 |
校验不通过时产物命名为 <name>.INVALID.zip——不会长得像可上传包,不要上传它。
分类推断由 scripts/category_suggest.py 负责,按关键词给出市场分类与服务大类候选、得分与置信度:
python scripts/category_suggest.py --skill-dir <技能目录>
四种情况判为无法唯一确定类目,置 needs_human_review 并阻断打包:首选得分不足 2.0(命中词太少)、与次选得分打平、置信度低于 0.4、正文不足 300 字。打平时输出会写明并列的是哪两个类目,直接请用户裁决;其余情况先请用户补描述或调整用词,让信号更明确再重跑。
各类型的具体检查项、error 与 warning 的分档,写在对应规格文件的「校验强度」一节。
3. 补上架信息
这一步是流水线里唯一必须人工决策的环节,不是脚本能力不足,而是这几项本质是业务判断:
- 市场展示分类 + 服务类目:决定用户在哪个类目下搜到你,也影响后续被驳回能不能改。选错了不是技术问题,是卖东西的方向问题。
- 收费方式与定价:发布后不可改,定价直接定这个资产的商业模式。
- 头像与原创声明:对外身份与法律承诺。
所以脚本只做它能做的——从配置里提取候选命名与简介、从平台接口拉出可选值和适用范围——决策留给你。脚本不会替你定,也不该替你定。
下面五项必须问用户,不能自己定:
- 市场展示分类 + 服务类目:决定用户在哪个类目下搜到你。市场展示分类最多选 3 个;「双十一」是运营专用类目,平台置灰不可选。服务类目是最多三级的级联树(「生活服务 → 丽人服务 → 美甲」这样三层,「工具」下只有两级),子类必须在页面上实时读取,不要离线猜。每个类目都写着「适用范围」和「不包含」,那是这个类目的业务边界,不是资质要求。真正需要行业资质的只有「律师」一项——选中它必须上传《律师执业资格证》,否则平台不放行。
- 收费方式(发布后不可改):一次性买断 / 按月订阅。付费能力内测中,免费资产不能原地转付费——想收费要先开通收入变现,再新发一个付费版本。连接器不可收费。
- 定价:官方文档没给定价示例,以后台为准。初期小额测需求是常见做法,但具体数额由用户定,草稿不代填。
- 头像:512×512、≤500KB、有图形、不能和自己的其他资产复用。
- 原创声明:确认原创后开启。
确认结果写回上架信息表,用户可以照着填网页表单。文案怎么写才有转化,见对应规格文件。
4. 上传与提交
确认过上传门后,用浏览器驱动完成。命令与机制见下一节。
交付时给用户三样:zip 路径、上架信息表草稿、开放平台直达步骤。若用户还想变现,补一句:审核通过后到「收入变现」走完资料审核与微信签约,即可发布付费版本。
一次上架算完成,要同时满足四条,缺一条都不许报「已上架」:fill / prepare 输出的 warnings 为空;确认页上的展示名、分类、类目与信息表逐项一致;submit 退出码为 0(它自己会从接口读到稳定的 under_review,退出码 4 表示点了但没核实,必须人工确认,不许当成功);信息表里记的版本号与包内 version 相同。
变现这块别把社区说法当规则承诺:官方流程是「收入变现 → 填资料 → 资料审核 → 微信签约」,最迟 24 小时返回。至于"1% 服务费""新账号只能发 1 个付费资产""需已有 ≥1 个审核通过资产"这类流传说法,官方文档一条都没写,以后台当天显示为准。
更新已有资产
平台把资产 name 当唯一键。已有资产再走「新建」入口上传同名包会被拒:
解析失败:技能 "xxx" 已存在,请携带其 assetId 发布新版本
这句只渲染在页面上、不写进草稿,不主动读页面就只会看到超时——看起来像「上传卡住 / 平台故障」,实际是入口错了。这是最容易踩的坑。
正确做法是带 --asset-id:
- 拿 assetId:打开
https://open.workbuddy.cn/{skill|expert|connector}/all,卡片上有(os_技能 /oe_专家 /oc_连接器)。也可以点该资产的「编辑」进页面,地址里的那串就是。 - 版本号递增,与包内
version一致。 - 带 assetId 上传:
prepare <zip> --type skill --asset-id os_xxx。
驱动把路径拼成/skill/publish/os_xxx?action=create&entry=2——平台靠路径里有没有 assetId 区分新建和发版本,action参数始终是create,别被这个名字误导。 - 不要删草稿。草稿就是这一版的编辑态,删了得重选分类与类目。
--reset只在草稿确实损坏时用,且必须配--yes-discard-draft。 - 提交后在
/{类型}/all看状态。不通过就按原因改包重传,不要反复提交未修改的包。
同 assetId 更新不触发重名校验,上传链路与新建完全一致(init-upload → COS → complete-upload,均 200)。
> 只有首次上架才走不带 --asset-id 的新建流程。
驱动速查
上架全程用 scripts/publish_driver.mjs 直连浏览器调试端口,一次调用完成一个阶段,只回一屏以内的紧凑 JSON。
环境就绪(一次性)
只需要两样:一个带调试端口的浏览器实例,和这个实例里的平台登录态。不需要 MCP,也不需要浏览器扩展。
- 起实例:
node scripts/browser_session.mjs launch
以独立 profile(默认~/.workbuddy/edge-automation-profile)和调试端口 9233 打开平台首页。Windows 下由计划任务拉起(任务名WBEdgeAuto),这样实例挂在任务计划服务下、不随调用方进程树被回收。 - 登录一次:在这个窗口里扫码或手机号登录。登录态落在专用 profile 里,实例重启后一般仍然保留——但服务端会话过期时会被踢回首页,需要重新扫码。所以每次开始前先跑一次
status确认当前状态,别靠记忆判断。 - 核验:
node scripts/browser_session.mjs status返回LOGGED_IN即就绪。
要退出登录态(换账号、清掉残留会话)用 node scripts/browser_session.mjs clear:它清的是 open.workbuddy.cn 这个站点的 cookie、localStorage、IndexedDB 与缓存,只动这一个站点,不影响系统里其他浏览器数据,也不导出明文凭据。清完 status 会变 LOGGED_OUT,要重新扫码。
收工或换账号前用 node scripts/browser_session.mjs stop 关掉自动化实例:它只杀命令行里带本技能 profile 路径的进程,不碰你自己日常用的浏览器。launch 在 Windows 下会留下两样持久物——计划任务 WBEdgeAuto 和启动脚本 ~/.workbuddy/edge-automation-launch.cmd,stop 不会删它们,要彻底撤销用 schtasks /Delete /TN WBEdgeAuto /F 并删掉那个 .cmd。四步撤销与保留哪些数据的说明见 references/setup-guide.md。
三层探测,缺哪层修哪层:
- 端口层:
curl -s http://127.0.0.1:9233/json/version有响应。没有 → 跑launch。 - 标签层:驱动
state能读到page.url。报no_platform_tab→ 实例里没开平台页,先在窗口打开https://open.workbuddy.cn/。 - 登录层:
status为LOGGED_IN。显示已登录但被踢回首页,说明服务端会话过期,重新扫码。
浏览器不限 Edge / Chrome,能带 --remote-debugging-port 起就行。
命令
十二个命令。run 把整条流水线收成一次调用(推荐),其余各完成一个阶段,只回一屏以内的紧凑 JSON:
DRV=~/.workbuddy/skills/publish-skill-helper/scripts/publish_driver.mjs
node $DRV state # 读当前步骤与草稿摘要(只读)
node $DRV route <1|2|3> # 跳到指定步骤
node $DRV upload <zip> # 只挂包上传并等解析,不写表单
node $DRV waitparse [秒] # 等平台开包解析,默认 45 秒
node $DRV fill <上架信息.json> # 写表单并跳到第 3 步确认页
node $DRV run --zip <zip> --form <信息.json> --cover <头像.png> \
--asset-id os_xxx --through confirm # 一条会话跑完(推荐)
node $DRV prepare <zip> [信息.json] # 上传 + 等解析 + 写表单(+ --cover 时顺带传头像)
node $DRV cover <头像.png> # 只传头像并回第 3 步,包已传完时用
node $DRV shot <out.jpg> # 截图给用户核对
node $DRV check # 只读核对服务端状态,审核期间随时可跑
node $DRV cats [--find 办公] # 列为平台类目树:显示名 ↔ id,并校验草稿/表单里的取值
node $DRV submit --yes # 提交审核并核对落地;不带 --yes 直接拒绝
通用开关:
| 开关 | 作用 |
|---|---|
--type skill\|expert\|connector |
资产类型,默认 skill。决定发布页路径、标签页归属与体积上限 |
--asset-id os_xxx |
给已存在的资产发新版本,完整流程与坑见「更新已有资产」;不带会走新建入口 |
--reset --yes-discard-draft |
步骤 1 没有文件控件时清掉旧草稿重来。两个必须一起给,且要先确认那份草稿确实可以丢 |
--retry |
prepare 遇 complete-upload 挂死时,刷新发布页后自动重试一次。它内部会清草稿并重跳步骤 1,等于第二次上传,别与 --reset 一起当成"先丢再传"两件事 |
--cover <png> |
配 prepare 用:传完包顺带把头像挂上去,省一次单独调用。头像控件只在步骤 2 出现,所以它排在写表单之前 |
--through confirm\|submit |
配 run:confirm 停在确认页等人看(默认),submit 一路送审。选 submit 必须同时给 --yes |
--zip / --form / --shot |
配 run:包、上架表单、确认页截图路径(默认 publish-confirm.jpg)。不给 --form 就只传包、停在第 2 步 |
--replace-pending |
该资产已有待审版本时,允许本次提交把它顶掉。不给就直接拒绝——平台顶掉待审版本不留提示,审核时钟静默归零 |
--verify N |
submit / run --through submit 点完之后等多少秒核对服务端,默认 45(5-300) |
--no-verify |
跳过核对,只看页面反应。仅在接口读不到时用,用了就不保证真交上去了 |
--port N |
调试端口,默认 9233(也可用环境变量 WB_CDP_PORT) |
--log <路径> |
追踪日志落盘路径,默认系统临时目录 publish-skill-helper.trace.log |
参数写错会直接退出码 2 并列出可用项,不会静默忽略——拼错 --asset-id 会被当成没给,从而走新建入口撞上「已存在」。
提交后不许把页面切走。 点完「提交」、页面还在转圈时就跳去 /{类型}/all 看列表,会把提交链路里尚未发出的后续请求掐掉:前端照样回「已提交审核,预计 7 个工作日内出结果」,接口上也曾短暂读到 under_review,但四分钟后状态退回 draft、submitCountToday 归零——内容是白交的。submit 现在会在原地连读三次、跨度超过 6 秒都是 under_review 才报成功,期间别动那个标签页。
别拿 pendingVersion.version 判版本。 详情接口里这个字段是平台自己的记录版本(可能是 1.8.0 这种与包无关的号),草稿 fields.common.version 又常常是空的。包版本只认草稿里 upload.parsed.version 和上架信息表。
退出码
| 码 | 含义 |
|---|---|
| 0 | 成功 |
| 1 | 执行失败,看输出里的错误码 |
| 2 | 用法错误:参数不对、命令不认识、类型不在枚举内 |
| 3 | 环境未就绪:CDP 不通、标签页不在、登录失效 |
| 4 | 已点提交但服务端没确认。别当成功,也别直接重跑——先去 /{类型}/all 看真实状态 |
两个 Python 脚本各用一套,别混:
| 脚本 | 码 | 含义 |
|---|---|---|
check_environment.py |
0 | ready 或 partial |
check_environment.py |
2 | needs_setup(Python 低于 3.8) |
check_environment.py |
3 | unavailable(依赖在但登录失效) |
package_for_open_platform.py |
0 | 校验通过,产物是可上传的 <name>.zip |
package_for_open_platform.py |
2 | 用法错误:参数不对、找不到包目录 |
package_for_open_platform.py |
3 | 目标目录里没有 SKILL.md |
package_for_open_platform.py |
5 | 校验未通过,产物是 <name>.INVALID.zip,不可上传 |
> 退出码 5 不是失败退出——脚本照常写出了信息表草稿,只是这个包不能传。看到 5 就去改包里的 ERROR 项,别去修脚本。
带 assetId 的两种调用:
node $DRV prepare <zip> <上架信息.json> --asset-id os_xxxxxxxx
node $DRV state --type expert --asset-id oe_xxxxxxxx
三类各用各的标签页,靠 --type 指定(默认 skill):
node $DRV state --type expert
node $DRV prepare <zip> <上架信息.json> --type connector
node $DRV waitparse 45 --type expert
--type 同时决定发布页路径、标签页匹配规则和体积上限;连上后还会核对草稿里的 capabilityType,类型不符直接退出,不会误填到别的表单上。
run:一条会话跑完整条流水线
node $DRV run --zip <zip> --form <上架表单.json> --cover <头像.png> \
--asset-id os_xxx --through confirm --shot confirm.jpg
等价于 prepare(含头像)+ 状态快照 + 确认页截图;--through submit 再往下走提交与核对。为什么要有它:一轮上架原本是 9 条命令,每条都要重付 350–390 ms 的进程启动与建连(Node 启动 ≈138 ms、/json/list + WS 握手 + Network.enable + 草稿归属核对 ≈113–250 ms),而且上传链路的 Network 计数只落在单条命令的窗口里,跨步骤判断只能靠人拼。
一次会话内 connect() 是幂等的,只建一次 WS。
送审前的自检(都发生在 --through submit 点按钮之前,confirm 模式下只作为 selfcheck.blockers 返回):
| 判据 | 失败后果 |
|---|---|
表单 version == 包内 upload.parsed.version |
version_mismatch,硬失败——传的包与表单不是同一版 |
草稿 descZhLen > 0 |
desc_missing:确认页「介绍」会渲染成 -(1.19.9 就这么上过架) |
warnings 为空(分类、服务类目、头像都已就位) |
blocked_before_submit,列出缺哪几项 |
三道闸口不合并:--reset --yes-discard-draft(丢旧草稿)、--through submit --yes(送审)、--replace-pending(顶掉待审版本)。少给一个就停在那一步,不会顺手带走。
prepare 干五件事
- 打开步骤 1(标签页已停在这步且文件控件还在时直接复用,省一次整页加载),把 zip 挂到文件控件,上传由页面自己发起
- 轮询草稿里的解析结果等平台开包完成,上限 45 秒(正常解析是秒级)
- 给了
--cover就把头像挂到步骤 2 的图片控件(按accept找 image 那个,不按"第一个"找),等草稿回写coverUrl - 把整份表单写进发布页草稿
sessionStorage['wb.open.publication.draft'] - 给了上架信息 JSON 才跳到步骤 3。没给就停在第 2 步——解析成功后平台自己会跳第 2 步,驱动再抢着跳第 3 步会和它撞车:旧执行上下文已销毁、新的没建好,
Runtime.evaluate一直挂到超时,把「解析成功」变成一条cdp_timeout
脚本侧合计 3–7 秒:标签页已在发布页时约 3.5 秒,从别处进来约 6.5 秒。时间基本花在平台自己的页面加载上;唯一不可压缩的是平台开包解析。
为什么写草稿而不是点控件:整份表单状态存在 sessionStorage 草稿里,写回后必须靠页面内的路由跳转让表单重挂载才会生效——fill 跳到第 3 步正是干这个的。一次 JSON 写入替代逐个点下拉框,也绕开了自定义下拉的坐标漂移和渲染时机问题。
> 刷新不会丢表单,但会让「类目」这一行时好时坏。实测:把一个标记值写进 displayName,整页刷新、来回跳步骤之后它都还在——草稿在 sessionStorage 里,刷新只是让页面用草稿重建一次表单(prefillRevision 每次加载 +1,是填充计数,不是状态指纹)。真正会丢的是页面上改了但还没落进草稿的编辑态(改完立刻关标签会丢,改完失焦才会写回)。
>
> 会坏的是渲染:页面偶尔拿不到类目树,于是把合法 id 显示成「存在已失效的类目,请重新选择」或直接显示英文 id,而它的提交校验读的正是这份渲染状态。snapshot 现在遇到这种可疑渲染会去平台类目树对账(cats 用的是同一份接口),id 有效就只记一条 category_render_transient notice,不当失败;submit 在点按钮前也会因此刷新一次自愈。
上架信息 JSON
一个技能类资产的完整样例(其余类型字段集不同,见对应规格文件)。正常流程不必手写这份文件:package_for_open_platform.py 打包时已按同样的字段名与形状产出 <name>_上架表单.json,直接喂给 prepare 即可。手写只发生在包不是本包打的情况下。
{
"displayName": { "zh": "技能·专家·连接器上架", "en": "Skill, Expert & Connector Publisher" },
"description": { "zh": "把做好的技能、专家或连接器一键上架到开放平台。", "en": "Publish a finished skill, expert, or connector to the Open Platform." },
"version": "1.21.2",
"usageExamples": [
{ "title": { "zh": "上架技能", "en": "Publish skill" }, "prompt": { "zh": "帮我把这个技能上架到开放平台", "en": "Publish this skill to the Open Platform" } }
],
"marketCategories": ["development-tools"],
"categories": ["tools:office"],
"coverUrl": "https://...",
"coverObjectKey": "..."
}
字段说明:
| 字段 | 必填 | 说明 |
|---|---|---|
displayName |
是 | 双语展示名对象 {zh, en},同类型资产内唯一 |
description |
是 | 双语对象 {zh, en}(en 可省)。写成裸字符串时确认页「介绍」渲染成 -、上架后详情页介绍也是空的——这是实测出来的,别照包内那套写。直接写中文字符串也收,驱动会补成 {zh}。注意:包内专家 plugin.json / 连接器 connector-meta.json 的 description 反过来必须是字符串,写成对象整包解析失败 |
version |
是 | 与包内 version 一致 |
usageExamples |
建议 | 一两条真实用户话术,给审核者看 |
marketCategories |
建议 | 市场展示分类,最多 3 个,取值是市场分类 id(如 development-tools),不是服务类目 id |
categories |
建议 | 服务类目,对象数组 [{ "categoryId": "tools:office", "qualifications": [] }]。写字符串数组 ["tools:office"] 平台不报错,确认页却把「类目」整行删掉——草稿有值、页面没值,这是实测的静默失败。驱动会把字符串自动包成对象,但草稿是被别的东西写的就救不回来 |
coverUrl / coverObjectKey |
否 | 头像来源,留空则沿用草稿里已有的。一般不用手填——用 --cover <png> 让驱动挂文件上传,比往 JSON 里贴 COS 地址可靠 |
> 字段名必须与驱动一致。驱动 fill / prepare 只认上表这些名字:displayName、description、version、usageExamples、marketCategories、categories、coverUrl、coverObjectKey、expertiseTags、price。写成 serviceCategory、market_category 这类同义名,驱动会当场报错退出,错误码 form_unknown_fields,并列出可用字段——不会再像以前那样静默忽略、让你到提交时才发现类目没填进去。
驱动同时检查两处形状:description 必须是双语对象或非空字符串(数组、空值、缺 zh 的对象报 form_invalid),marketCategories 与 categories 必须是数组(写成单个值同样报 form_invalid)。这两种错误都在填草稿之前就抛出来,不会把坏数据写进平台。
两个字段的内部形状并不一样,这点踩过:
| 字段 | 草稿里的形状 | 平台校验 | 写错的后果 |
|---|---|---|---|
marketCategories |
字符串 id 数组,多选用数组长度表达(实测最多 3 个) | 不校验 | 错 id 会在确认页原样渲染,并直接上架(实测写入 __bogus__,页面照显示不误) |
categories |
对象数组 [{categoryId, qualifications}] |
校验,只在确认页给一句话 | 写成字符串数组 → 「类目」整行消失;写成不存在的 id → 「存在已失效的类目,请重新选择」 |
分类与类目不要猜 id:平台用 development-tools、tools:office 这类内部 id,但两套 id 不是一回事——development-tools 是市场展示分类,tools:office 是服务类目,分别填进 marketCategories 和 categories。权威来源是平台自己的接口:市场分类取 /api/v2/open-platform/capabilities/categories?capabilityType=skill,服务类目取 /api/v2/open-platform/capabilities/business-categories;两者都要带 x-party-id 请求头,它的值就是草稿里的 party.entityId。不用自己拼这几条请求:node $DRV cats 会把两份树打出来,--find 办公 直接反查 id,--validate-json <表单.json> / --validate-draft 对着树校验现有取值。注意市场展示分类这一侧平台不校验,所以宁可用 cats 核一遍,也别往表单里塞记不准的 id。省略这两项时驱动不猜,沿用草稿里已有的值(输出里的 inherited 标明哪几项是沿用的)。
coverUrl / coverObjectKey 来自页面已有的头像;要换头像在确认页点「重新上传」。
价格不在驱动范围内:草稿结构里没有价格字段,免费资产不填即默认免费;收费方式与定价仍需在页面选。
每次 fill / prepare 的输出都带 warnings,列出还缺的必填项(分类、类目、头像)——为空才说明可以进提交。
上传、提交与丢弃草稿的授权规则见「上架四道闸口」;--yes、--reset --yes-discard-draft 为什么必须成对给,见上方开关表。
故障索引
按症状查,别凭感觉猜。排查一律先看追踪日志:默认在系统临时目录 publish-skill-helper.trace.log,可用 --log <路径> 指定;每次运行第一条写入会截断旧文件,只保留当前这一轮,同步写盘所以进程被强杀也留得下最后一步。
| 症状 | 判据 | 动作 |
|---|---|---|
报 cdp_unreachable(退出码 3) |
三个回环地址都不通 | 驱动已按 127.0.0.1→localhost→[::1] 逐个试(新版 Edge 常只监听 IPv6);都不通就跑 launch |
报 no_platform_tab(退出码 3) |
实例里没有平台页 | 在窗口打开 https://open.workbuddy.cn/ |
报 no_file_input |
步骤 1 还留着上次的包 | 确认可丢弃旧草稿后加 --reset --yes-discard-draft |
| 页面提示「该名称已被使用」 | 展示名在同类型资产内撞名 | 不要改名硬凑。先去 /{类型}/all 看是不是自己传过:有就带 --asset-id 更新那条,没有才是别人占了名 |
| 页面提示「已存在,请携带 assetId」 | 走了新建入口 | 带 --asset-id 重发,见「更新已有资产」 |
页面提示 cannot unmarshal object ... of type string |
包内 plugin.json / connector-meta.json 的 description 写成了 {zh,en} 对象(这一步在读包,不是在读表单) |
包内改回单个字符串。专家与连接器的双语展示描述放 displayDescription / description_zh+description_en;表单里那个 description 反而是对象,见「上架信息 JSON」 |
确认页「介绍」显示 -,草稿 descZhLen 为 0 |
表单 description 写成了裸字符串,平台按 .zh 取,取不到就是空 |
改成 {zh, en} 对象后 route 3 重挂载;state 的 descZhLen 应为正数 |
| 页面提示别的「解析失败」 | 平台明确拒绝了这个包 | 说明请求是通的、不是平台故障,按提示改包重传 |
wait_timeout: parse |
45 秒没拿到解析结果 | 看驱动带的诊断:init-upload / complete-upload 各发几次、回几次、页面停在哪句。驱动会按计数给「卡在文件上传 / 卡在服务端解析 / 根本没发出」三种判定 |
| 「上传中…」卡住不动 | 页面显示上传中,但草稿里没有 upload、日志里没有 init-upload |
这是死状态,没复位再挂包不会触发任何请求。先整页重跳步骤 1(驱动已自动做),仍如此就换个新标签页重传 |
| 同一标签页反复上传失败 | init-upload 发得出、回不来 |
旧标签页连接池留了挂起请求。同一时刻同一个包,旧页 45 秒超时、新页 2 秒全通。关掉它让驱动新开 |
报 no_cover_input |
步骤 2 没有图片控件 | 包还没传上去。先 prepare/upload 把包挂上,控件才会渲染 |
报 cover_timeout |
20 秒内草稿没有 coverUrl |
presign 请求发出但无响应,旧标签页连接池残留——换新标签页重跑 cover |
报 route_retry_ok notice |
跳步骤首次 evaluate 超时,重试后已到位 | 不是失败,数据没受影响。别重跑,重跑会再清一次草稿 |
报 pending_exists |
该资产已有待审版本,submit 在点按钮之前就拒了 |
平台顶掉待审版本不留提示、审核时钟静默归零。确认要顶掉再加 --replace-pending;否则等这次审核出结果 |
报 version_mismatch |
表单 version 与包内解析出的版本不一致 |
传错包或表单没跟着升版。已经停在确认页、未提交,改名重打包再来 |
报 need_env: ... 没有全局 WebSocket |
Node 低于 22 | 全局 WebSocket 自 v22 起才默认开启。升级 Node,别在这个报错上查端口和登录态 |
| 头像上传卡在「上传中…」且控件锁死 | 日志里有 presign 请求发出、但没有响应 |
同一个连接池问题,这次坏的是取上传凭证这一步,不是 init-upload。换一个新标签页,同一个接口通常 2 秒就通 |
| 刷新页面后分类、类目、头像全没了 | 平台侧另有一份服务端字段,刷新时会把空项补齐 | 实测:驱动写进草稿的值刷新不会丢(标记值经历整页刷新 + 两次跳步骤仍在)。会丢的是页面上改了却没失焦、没落进草稿的编辑态。真遇到值回退,先用 cats --validate-draft 看草稿里到底有没有值,再决定是重选还是只重渲染 |
| 确认页写「存在已失效的类目,请重新选择」,但类目是刚选的 | 页面这次没取到类目树,合法 id 也会这么显示(实测复现,刷新一次即恢复) | 别急着重选。cats --validate-draft 对着平台树核一遍:说有效就是渲染问题。snapshot 会自动做这件事并记 category_render_transient;submit 点按钮前会刷新自愈 |
| 确认页「类目」整行不见了 | 草稿里 categories 是字符串数组而不是对象数组 |
平台不报错也不提示,就是渲染不出来。用 cats --validate-draft 能看到 id 是有效的,但形状不对——重写一次表单(驱动的 fill 会自动包成对象) |
「市场展示分类」显示成英文 id,如 development-tools |
可能是渲染没取到树,也可能是 id 根本不存在 | 平台不校验这个字段,错 id 会原样上架。先 cats --find <中文名> 反查,确认 id 在清单里;在清单里就是渲染问题 |
有 init-upload 响应但 PUT / complete-upload 不发 |
页面收到两次 change | DOM.setFileInputFiles 已由浏览器原生触发 change,不要再手动补。驱动只在 1.5 秒内确实没请求时才补发 |
blocked_incomplete |
页面提示还有必填项 | 补齐再提交 |
submit 报 submit_unverified(退出码 4) |
点了提交,但接口读不到稳定的 under_review |
先去 /{类型}/all 看真实状态。还是 draft 就重新走一遍,这次提交期间别切走标签页 |
提交时页面说成功、过几分钟状态退回 draft |
submitCountToday 归零 |
提交链路被中途掐断。重新提交并留在原页;submit 默认已核对,看到退出码 0 才算交上去了 |
详情里 pendingVersion.version 是很旧的号 |
与包内版本对不上 | 正常现象,别改包。那个字段是平台自己的记录版本,不是包版本;判成功看 status |
| 想确认资产现在到底什么状态 | 审核期间心里没底 | node $DRV check --asset-id os_xxx,只读,不写任何东西 |
| 页面结构变了导致整链失效 | 报错位置与上述都对不上 | 停止自动流程,回退到「信息表 + 人工提交」,不要反复重试 |
确认页底部红字「上传令牌无效」,或报 blocked_upload_token |
草稿 upload.expiresAt 已过(令牌寿命实测 2 小时)。平台只在提交接口上拒,页面上除这行字没有任何提示 |
令牌不可续期,只能重传同一个包:步骤 1 此时不再渲染文件控件,加 --reset --yes-discard-draft 清本地草稿后重传(清的是 sessionStorage,平台侧不删数据)。state 已把 upload.expiresAt / expired / minutesLeft 一并报出来,submit 会在点按钮之前拒绝 |
几条容易被误判成故障的机制:
complete-upload是同步解析接口:服务端拿到凭证后去读刚传的 COS 对象、解压、校验,把结果放在 HTTP 响应里返回,前端await完才写草稿。没有轮询接口可旁路,客户端既不能加速也不能绕过。HTTP 200 不等于解析成功——失败原因只写在页面上、不写进草稿。- 步骤切换只能整页跳转:页面自带的路由跳转对步骤不生效(组件不重挂、步骤不变),要么改地址栏,要么用驱动的
route。 - 没传包时跳步骤 3 会被弹回:新会话草稿是空的,平台不认这个步骤,访问后自动退回步骤 1。确认页能停留,说明包里已经传上去了。
- 实例可能被调用方环境回收:直接
spawn的浏览器进程(detached + unref也不行)会在命令结束后 1–3 分钟内被杀,表现为 CDP 突然不可达。launch在 Windows 下已改走计划任务绕开这个问题。 - 别用 MCP 浏览器工具做上架:
browser_observe/browser_act要经扩展中转,实测单次 10–26 秒且经常收不到回执,同页面直连只要 7 毫秒。扩展装着不影响,但上架不走它。
规则层面的红线在对应规格文件和 references/open_platform_publish_guide.md 里。解析失败求助:openworkbuddy@tencent.com,或开放平台首页扫码进开发者社群。
参考
references/skill_pack_spec.md— 技能包结构、必填字段、15 个市场分类、服务类目、命名与头像公约、文案标准、校验强度references/expert_pack_spec.md— 专家包完整字段表、16 个 categoryId、官方模板的反例与平台真实拦截项references/connector_pack_spec.md— 连接器 MCP / CLI / skill-only 三方案、认证约束、${VAR}占位规则references/open_platform_publish_guide.md— 平台规则、入驻认证、变现与定价、审核要点、实测耗时references/platform_background.md— 浏览器扩展配置、登录态会话管理、开发者资料规范、运营数据口径、官方 Open API 能力(上架主流程用不到,需要时再查)references/setup-guide.md— 五项依赖的安装、验证、轮换与撤销skill-dependencies.json— 依赖与能力的机器可读映射、降级矩阵、停止条件
脚本入口:python scripts/package_for_open_platform.py --type skill|expert|connector --skill-dir <目录> --out <输出目录>
改了本技能任何一个脚本之后跑 python scripts/selftest.py:覆盖四态环境路由、驱动 CLI 参数一致性、类目门禁、三类包校验、打包内容与平台规则文档同步。它只用标准库、在系统临时目录建夹具、跑完自清,不联网、不碰登录态。全部断言通过才算可交付。