双色球一注生成(支付宝 A2M 付费技能)
0. 变更记录
| 版本 | 变更 |
|---|---|
| 1.2.3 | 修复两处真实缺陷(2026-09-25 生产实测驱动):<br>① 跨订单串号判据扩展——真实链路里「收银台订单号」与「402 出账订单号」是两套命名空间,原实现做单字段严格相等比对,导致用户已付款成功却被判 A2M_RESOURCE_INVALID、号码无法交付。现改为多标识集合相交(资源体 out_trade_no/trade_no/resource_id 与本轮持有的订单号/交易号任一匹配即认定同单),并优先采用信封顶层 tradeNo 这一两侧真正共享的锚点;口径不一致时留 warning 便于排查。防护强度未削弱(全部标识均不匹配仍判 error)。<br>② alipay-bot 后缀探测与平台解耦——原 alipayBotCandidates 只在 win32 下列出 .exe/.cmd/.bat,导致「文档记载的安装目录里放着 alipay-bot.exe」在 Linux/macOS 上永远找不到(自测 8.39/8.40 失败)。现改为无后缀优先、其余后缀统一兜底,与平台无关。<br>另新增 N7b/N7c 两条回归断言锁住上述行为,自测由 158/160 提升至 162/162 全通过。 |
| 1.2.2 | 前一版。 |
1. 能力、触发与边界
| 项目 | 内容 |
|---|---|
| 触发条件 | 用户消息含「双色球」,且表达生成 / 随机选号 / 来一注 / 买一注 / 帮我选一注的意图 |
| 输入 | 无必填参数。用户只需表达「来一注双色球」;不需要也不应该向用户索取订单号、会话 ID、密钥 |
| 单次价格 | 0.01 元(1 分),按次计费,由服务端 402 出账为准 |
| 输出 | 一注双色球号码:6 个红球(1–33,升序)+ 1 个蓝球(1–16)。对客形式固定为一行纯文本(可整行复制),见 §5 |
| 凭证来源 | 服务端 pick_token 优先;服务端未签发时按订单号派生本地兜底凭证;已交付订单以凭证台账粘住原凭证(§4)。来源记在结构化输出与台账里,对客文本默认单行不带(§5) |
| 付费通道 | 支付宝 A2M(HTTP 402 + Payment-Needed),动作全部委托官方 alipay-bot CLI |
| 自动等待 | 第 4 步出码后取代 STOP:默认每 30s 查询一次同一订单,命中即出号;上限 300s(可配,硬上限 540s),超时退回人工触发。只查询,不付款(§3.1) |
| 不做 | 不自己发支付请求、不实现签名、不伪造支付结果、不处理异步通知、不发起退款、不代用户付款(付款只能由用户在支付宝内完成) |
业务链路:用户说「来一注双色球」→ 技能取 402 出账 → 用户完成 0.01 元付款 →
技能查询订单状态 → 服务端回传资源体(含 pick_token)→ 技能端用固定算法派生号码并展示。
其中「用户完成付款」是全链路唯一由人完成的动作:技能把收银台交出去之后,用
有界自动等待(§3.1)盯住这一单,用户一付款就立刻出号;等满上限仍未到账则退回人工触发。
注意:等待期间不会重复发起支付,所以「超时」只意味着「本轮结束」,不意味着订单作废。
2. 依赖与环境
| 依赖 | 版本/位置 | 说明 |
|---|---|---|
alipay-bot |
>= 0.4.1(2026-09-19 实测 v0.4.3)。安装路径随安装方式而异,用前先探测:command -v alipay-bot;未命中再试候选 ~/.openclaw-autoclaw/alipay-bot-cli/bin/alipay-bot(当前生效)与 ~/.local/bin/alipay-bot(旧路径,已不存在,直接调会 command not found)。Windows 上实际文件名带 .exe:bash 会自动补后缀,Node 的 spawn 不会——scripts/wait-pay.js 会自行探测 .exe/.cmd/.bat |
402 六步的唯一执行体 |
alipay-payment-skill |
官方技能 | 402 流程、参数、错误码的唯一权威来源 |
| Node.js | >= 18(本机 managed;实际路径用 node --version 自查,通常位于 ~/.workbuddy/binaries/node/versions/<版本>/node.exe) |
跑 scripts/ 与 test/ |
| npm 依赖 | 无 | 全部脚本纯 Node 标准库,零依赖,可整个目录直接搬走 |
| 凭证台账目录 | ~/.ssq-create/ledger/(运行期自动创建,0700/0600) |
粘性凭证,防止服务端后补 token 导致老订单换号(§4.3)。可用 SSQ_LEDGER_DIR 覆盖 |
3. 调用方法(照官方 402 六步骨架,不另造)
下文 <SKILL_DIR> 指技能加载时返回的 base directory。
第 1 步 · 取 402 出账(原始请求,不重写业务语义)
node "<SKILL_DIR>/scripts/fetch-402.js" \
--method GET \
--expect-amount-fen 1
- 默认打生产资源地址。该地址不在本文档里写字面值,硬编码在
scripts/fetch-402.js的DEFAULT_RESOURCE_URL;GET,无 body、无自定义 header。 - 下文命令里的
<RESOURCE_URL>是一个占位符,取值方式(按优先级):
1. 第 1 步输出 JSON 里的resource_url字段——首选,直接原样取用;
2. 或读scripts/fetch-402.js的DEFAULT_RESOURCE_URL。
不要手抄、不要凭记忆拼写:文档里的字面值会随服务端迁移而漂移,输出字段才是权威。 - 固定走本脚本而不是
curl:生产站只开 TLS 1.3,Windows Schannel 不支持 TLS 1.3,
curl会报schannel: failed to receive handshake;Node 自带 TLS 栈可正常握手。 - 环境变量
SSQ_RESOURCE_URL可覆盖资源地址,但只在指向本机(127.0.0.1/localhost/::1)
时生效,其他主机一律忽略并回落生产地址 —— 防误配把请求打到别处。 Payment-Needed的值是支付凭证:脚本只落盘、绝不打印,输出里只有secret_file路径与
secret_fingerprint(长度 + 摘要前 16 位)。目录 0700、文件 0600。
第 2 步 · 校验 402
读脚本输出 JSON:
ok: true且channel: "payment-needed"→ 继续第 3 步(--file分支)。channel: "amt-payment-link"→ 继续第 3 步(--amt-payment-link分支,按协议不写文件)。ok: false或errors非空 → 原样告知用户并 STOP,尤其当errors含
「金额不一致」时:说明服务端出账金额与技能声明的 1 分没拉平,此时不得发起付款。
第 3 步 · 发起支付(官方 CLI,技能端不自己实现)
从运行时元数据解析 sessionId(优先级:sessionId → session_id → threadId/thread_id
→ conversationId/conversation_id → agentContextId → CODEBUDDY_SESSION_ID →
CLAUDE_SESSION_ID → CODEX_THREAD_ID → AIPAY_SESSION_ID)。
可先自查:node "<SKILL_DIR>/scripts/a2m.js" session --session-id '<值>'。
全部为空时 STOP 并说明「运行时未提供当前会话标识」——不生成、不猜测、不向用户索取。
> 注意(2026-09-18 实测):WorkBuddy 运行时只提供 CODEBUDDY_SESSION_ID(兼容别名
> CLAUDE_SESSION_ID),不提供 CODEX_THREAD_ID / AIPAY_SESSION_ID。解析链里若缺
> CODEBUDDY_SESSION_ID 这一项,自动解析必然返回 ok:false,技能会在第 3 步直接 STOP、
> 永远走不到付款。新增环境名时务必同步 test/selftest.js 的 S5–S7 断言。
# Payment-Needed 分支
alipay-bot 402-buyer-pay \
--session-id '<sessionId>' \
--file '<第 1 步的 secret_file>' \
--resource-url '<RESOURCE_URL>' \
--method GET \
--intent-summary '原始请求:为用户生成一注双色球号码'
# amtPaymentLink 分支(仅在 channel 为 amt-payment-link 时)
alipay-bot 402-buyer-pay \
--session-id '<sessionId>' \
--amt-payment-link '<第 1 步响应头原值>' \
--resource-url '<RESOURCE_URL>' \
--method GET \
--intent-summary '原始请求:为用户生成一注双色球号码'
--intent-summary保留原始业务目的(生成一注双色球),不写 URL 或「支付」二字。--resource-url永远是资源地址,不是支付链接。- 发起前不重新请求资源;不要复用历史 402 文件(服务端会轮换 challenge)。
- 联调/演示开关:当运行环境设置了
SSQ_DRY_RUN=1时,不要执行第 3 步,改为把将要执行的
完整命令原样打印一次并 STOP(这一步的自动化实现就是
node "<SKILL_DIR>/test/verify-402-stop-before-pay.js" --session-id '<id>',它跑完 402 处理
的全部动作后停在付款确认前)。未设置该变量时才真正调用 CLI。
第 4 步 · 输出收款信息,然后进入有界自动等待
CLI 命令待确认时,把对客结果原样输出一次(订单号、支付方式链接逐字符保留)。
二维码/收银台必须先展示给用户(present_files 传二维码图片,或输出 CLI 给的链接),
再启动下面的自动等待——顺序反了用户根本看不到码,等于白等一轮。
记录对客标签备用:
| 标签 | 用途 |
|---|---|
| 订单号 / 查询单号 | 32 位数字且第 11–14 位为 8282 时优先;查询走 --out-shake-no |
| 交易号 | 上述号码不存在或无效时使用;查询走 --trade-no |
| 支付方式链接 | 仅供用户点击付款,不是查询参数 |
3.1 有界自动等待(紧接第 4 步,默认开启)
# 必须显式给 tool 传 timeout ≥ max-wait + 30s,否则会被工具默认超时(120s)打断
node "<SKILL_DIR>/scripts/wait-pay.js" \
--out-shake-no '<订单号>' \
--interval 30 \
--max-wait 300
行为约定:
- 每
--interval秒查询同一订单(默认 30,最小 1——不许高频打生产查单接口)。 - 查到
status=success就立刻派生号码并交付;text与手动路径逐字同口径(§5),
默认就是那一行「双色球:…,祝你中大奖!!」,原样发给用户即可。 - 等满
--max-wait秒(默认 300,硬上限 540)仍未到账 → 输出超时说明并结束本轮,
文案里必须带「不要重新发起支付」。 - 只调用
402-query-payment-status:子命令与参数由wait-pay.js固定拼装,
不接受外部注入、不重取 402、不复用历史凭证。 - 命令固定带
--output json(CLI 的全局开关),走结构化通道取资源体;
若某些版本不吃这个开关,解析层会自动回退到「Markdown + 末尾 JSON 块」的文本通道。 - 「未付款」只能靠信封的
status字段判定,不能靠文本正则(2026-09-19 生产实测):
真实未付款信封是{ active:false, resourceDelivered:false, status:"payment_unconfirmed", success:true, tradeNo }—— 里面没有任何中文「未支付」标记,而且success仍是true。
判定顺序为:资源体 → 信封status→ 文本标记;已登记的 pending 取值还带正则兜底。
因此「success:true就当作已付款」是错误读法,会直接把未付款误判成成功。 - 退出码不能当成败判据:真实 CLI 在「已履约、不重复提供」时返回
success:false
且进程退出码为 1,但资源体依然完整可用(2026-09-19 生产实测)。因此判定一律以
输出内容为准,且只有 spawn 级失败才计入连续失败(§6)。
退出码与处理:
| 退出码 | kind |
处理 |
|---|---|---|
| 0 | ticket |
照抄 text 交付号码,本轮结束 |
| 3 | timeout |
输出超时说明(含「不要重新发起支付」),告知付款后说一声即可出号 |
| 4 | failed |
输出失败说明,不出号,劝止重复付款 |
| 2 | error |
见 §6:QUERY_TARGET_MISSING / ALIPAY_BOT_NOT_FOUND / QUERY_FAILED / QUERY_CALL_FAILED / A2M_RESOURCE_INVALID |
参数与运行约束:
| 项 | 默认 | 说明 |
|---|---|---|
| 查询间隔 | 30s | --interval,最小 1s |
| 等待上限 | 300s | --max-wait,硬上限 540s(平台单次调用跑到 600s 会被直接杀掉) |
| 首次查询 | 等一个间隔 | --immediate 可改为立刻先查一次;--trade-no '<交易号>' 用于没有合规订单号的场景 |
| 单次查询超时 | 60s,且受剩余预算约束 | 不会出现「一次查询卡住 60s 把 max-wait 承诺推翻」 |
| tool timeout | 必须 ≥ max-wait + 30s | 忘记设置就会被 120s 默认超时打断;等待期间 stdout 是静默的(结束才吐一行 JSON),不是卡死 |
用户随时可以插话:付款后直接说「已付款 / 继续 / 查询」即走第 5 步,不必等自动等待结束。
SSQ_DRY_RUN=1 时同样不执行自动等待(它属于付款后的动作)。
第 5 步 · 用户表示已支付 → 查询同一订单
用户说「已支付 / 付好了 / 完成 / 搞定 / 继续 / 查询」时:
alipay-bot 402-query-payment-status \
--out-shake-no '<订单号>' \
--resource-url '<RESOURCE_URL>' \
--method GET
# 只有交易号时:--trade-no '<交易号>'(并带上 --resource-url / --method)
状态不明时先查单,不要重新发起支付、不要重取 402。
若本轮自动等待(§3.1)已经交付过号码,用户再说「已付款」时不必重查——直接复述同一份
text 即可(凭证台账保证同一订单号码不变,重查也只是重复同一条结果)。
自动等待超时后用户才说「已付款」的,就用上面这条命令走正常路径,无需再次付款。
第 6 步 · 校验资源体并渲染号码
把查询输出的 JSON 存到会话目录,再交给渲染器:
node "<SKILL_DIR>/scripts/a2m.js" render \
--resource "$HOME/.ssq-create/session/resource.json" \
--order-no '<订单号>'
退出码即结论,直接照它分支,不要自己猜:
| 退出码 | kind |
处理 |
|---|---|---|
| 0 | ticket |
输出 text(已渲染好的一注号码) |
| 3 | processing |
输出 text,提示稍后重新查询同一订单,不要重新付款 |
| 4 | failed |
输出 text,不做二次扣减、不自动重试付款 |
| 2 | error |
资源体校验不通过 → 原样告知用户,STOP |
- 资源体的
content字段是字符串形式的 JSON,渲染器会二次解析;解析失败一律拒绝渲染。 - 取到非空资源后官方 CLI 已自动发送履约回执,成功路径不追加独立回执、不重复查询。
- 仅当同一次输出明确提示「履约回执发送失败」且用户在后续消息要求恢复时,才执行一次
alipay-bot 402-buyer-fulfillment-ack --trade-no '<同次输出的交易号>'。
可选开关(只在排查/联调时用,日常不加):
| 开关 | 作用 |
|---|---|
--require-server-token |
服务端未签发 pick_token 就报错退出 2。用于服务端联调验收「是否真的签发了凭证」,此时不出兜底号码 |
--no-ledger |
不读也不写凭证台账。只在怀疑台账本身有问题时用;会放弃老订单的 sticky 保护(见 §4.3) |
4. 号码由技能端确定性派生(幂等的关键)
A2M 要求「同一订单重试返回同一份资源」,而本产品把号码生成放在技能端(服务端只当收款闸门)。
若让模型"随机想"一组号码,同一订单两次查询就会得到不同结果,幂等性直接崩掉。因此:
- 服务端在履约时签发
pick_token(32 字节随机 hex),与订单一起持久化,同一订单只签发一次;
资源体里同时带pick_algorithm: "ssq-v1"。 - 技能端用
scripts/derive.js的固定算法展开 token,禁止模型口算随机数:
| 位 | 算法 |
|---|---|
| 红球 | SHA-256(token + ":red:i")(i=0,1,2…)拼成字节流,每轮取 4 字节大端整数,对 [1..33] 做 Fisher-Yates 洗牌,取前 6 个升序 |
| 蓝球 | SHA-256(token + ":blue:0") 取前 4 字节大端整数,模 16 再 +1 |
每轮用 4 字节而非 1 字节:1 字节取模时 256 % (i+1) ≠ 0(如 256 % 33 = 25)会让部分下标
出现概率高约 12%,实测 5000 次抽样红球最大偏移 9.25%;改 4 字节后偏差降到 1e-8 量级。
服务端若自实现洗牌,必须与此一致。
- 于是服务端不存号码也能保证幂等,技能端"生成"了号码,双方还能各自复算对账。
node "<SKILL_DIR>/scripts/derive.js" <pick_token>
# {"algorithm":"ssq-v1","red":[11,12,22,26,28,33],"blue":1,"token_fingerprint":"..."}
node "<SKILL_DIR>/scripts/derive.js" --selftest 20000 # 稳定性/合法性/覆盖度/分布自检
4.1 凭证来源裁决顺序(token_source)
渲染器按固定顺序裁决用哪张凭证,不允许模型自行挑选:
| 优先级 | 来源 | token_source |
何时命中 |
|---|---|---|---|
| 1 | 凭证台账(已交付订单) | 台账原值 | 该订单此前交付过 → 沿用原凭证,见 4.3 |
| 2 | 服务端签发 pick_token |
server |
资源体里带了 pick_token |
| 3 | 技能端本地兜底 | local-fallback |
服务端没签发,见 4.2 |
服务端优先对新订单始终成立;台账只对已经交付、号码已对客可见的订单生效。
4.2 本地兜底凭证(服务端未签发 pick_token 时)
背景:2026-09-19 实测生产站履约返回的是 demo 占位体,content 里既无 pick_token
也无 pick_algorithm,导致渲染器退出码 2、无法交付。为打通闭环,技能端提供兜底:
凭证 = sha256("ssq-local-v1:" + source + ":" + 订单标识) // 64 位 hex,32 字节
source ∈ { out_trade_no, trade_no },订单号优先,缺失才回退交易号
- 仍是纯函数:同一订单在任何机器、任何时刻都复算出同一组号码,无需服务端存号码。
- 不是"随便编一个号":凭证由订单标识确定性派生,可复算、可对账。
- 加
ssq-local-v1:前缀,使兜底派生与服务端随机 token 在算法上不可能相撞,两套来源始终可区分。 - 两个标识都没有 → 报错(
DERIVE_LOCAL_BASIS_MISSING),绝不凭空生成。 - 兜底来源写在结构化输出(
token_source/local_basis)与凭证台账里;对客文本默认只有一行号码(§5),排查时用--details取完整行。 - 每次兜底都会在
warnings里留下basis与凭证指纹,便于排查与对账。 - 服务端联调验收想确认「是否真的签发了凭证」时,加
--require-server-token让兜底失效。
4.3 凭证台账(粘性,防止老订单换号)
台账每完成一次交付就把本次实际使用的凭证按订单落盘:
~/.ssq-create/ledger/<订单标识>.json # 目录 0700 / 文件 0600,可用 SSQ_LEDGER_DIR 覆盖
{ out_trade_no, trade_no, token, token_source, token_fingerprint, algorithm, local_basis, recorded_at }
为什么必须有它:今天用兜底发的号码,等服务端补上 pick_token 后查同一订单,
凭证一变、号码就变 —— 已交付的客户会看到自己的号码被改掉,这在幂等语义下是明确缺陷。
台账命中即锁定原凭证,优先于服务端本次签发的值,于是:
- 新订单 → 走服务端
pick_token; - 已交付的老订单 → 沿用首次交付的凭证,号码永不改变。
边界:台账不存号码(只存凭证,号码仍靠 derive 复算);订单标识做白名单校验防路径穿越
(^[A-Za-z0-9_.:-]{1,64}$ 且不含 ..);读写失败/内容损坏一律降级为 warning + 按未命中处理,
绝不阻断对客交付,同时把原因写进 warnings 以免"号码莫名换了却无从排查"。
5. 展示格式
付费提示(第 1 步取到 402 后,用服务端返回的值展示,不要用本地常量覆盖):
双色球 · 生成一注
本次需支付:0.01 CNY
商品:AI 生成内容服务
订单号:ORDER_...
成功交付(kind: ticket 的 text)——固定为一行纯文本:
双色球:11 12 22 26 28 33 + 01,祝你中大奖!!
- 照抄这一行即可,不要加标题、解释、结语或前后缀;
+前是 6 个红球(升序、两位补零),
+后是蓝球(两位补零)。 - 必须是纯文本:不得改成图片、二维码、卡片或 Markdown 表格——用户要能直接选中整行复制。
- 服务端凭证与本地兜底共用这一个格式(不再区分对客呈现);来源信息改记在结构化输出
(token_source/local_basis)与凭证台账里。 - 排查、对账或联调需要订单号与来源标注时,在命令上加
--details(等价SSQ_TICKET_DETAILS=1):
双色球:13 14 15 23 25 33 + 10,祝你中大奖!!
订单号:ORDER_...
凭证来源:本地兜底(服务端未签发 pick_token)
号码由订单凭证按固定算法确定性派生,同一订单重复查询结果一致。
details 模式首行仍是同一号码行(便于整行复制),运维行只是追加;对客交付默认不加。
两个入口都支持:a2m.js render --details、wait-pay.js --details。
6. 异常处理
| 现象 | 处理 |
|---|---|
| 「支付不成功」类反馈 | 第一诊断动作是 alipay-bot check-wallet,它比查订单更早给出根因;code=500 未开通 = 买家钱包未开通,不是订单问题 |
查询报 BIZ_ORDER_NOT_FOUND |
该单生成于钱包未开通期间,从未在支付宝侧成立 → 不重试,重新取当前 402 后重新发起 |
查询报 PAYMENT_QUERY_CREDENTIAL_UNAVAILABLE |
钱包未开通;不要反复重试查询 |
curl 连不上资源站 |
TLS 1.3 与 Windows Schannel 不兼容 → 换本技能的 Node 脚本 |
| 命令超时或网络失败 | 可重试一次;业务失败不重试 |
| 签名/验付失败 | 网关与 serviceId 必须同时切换,只换一个必然失败 |
付款返回 TRADE_PROGRESS_FAILED「买家和卖家不能相同」 |
服务端 sellerId 与 alipay-bot 绑定的买家账号是同一个支付宝账号,支付宝禁止自买自卖 → 这是配置问题、不是链路问题(请求已到达支付宝并拿到业务错误码)→ 不重试:换一个真实商户号做卖家,或换另一个支付宝账号绑买家钱包。2026-09-19 复测已解除(CLI 输出的商户已非本机买家账号);若再遇到按本条处理 |
付款被拒后查单返回 QUERY_FAILED「查询支付凭证失败」 |
该单从未在支付宝侧成立(付款被拒),查不到凭证属预期,不是故障;也不要反复查 |
资源体 status=success 但没有 pick_token |
正常路径:渲染器自动走本地兜底(§4.2),照常交付(对客仍是同一行号码,来源记在结构化输出与台账)。warnings 里会有 basis 与凭证指纹 → 说明服务端还没接入签发,值得去催;但不要因此拒绝交付 |
渲染返回 exit 2 / A2M_RESOURCE_INVALID |
资源体校验不通过。先看 message 指哪个字段:缺 pick_token(且兜底也失败:订单号与交易号都缺)、pick_algorithm 不匹配、content 二次解析失败、跨订单串号。原样告知用户并 STOP,绝不猜号码 |
warnings 里出现「订单号口径不一致但已由其它标识确认同单」 |
正常现象,不算故障:收银台订单号与 402 出账订单号分属两套命名空间,靠 trade_no 交叉确认了同单,号码照常交付。留痕用于观察服务端回填策略,不要因此拒绝交付 |
| 报「跨订单串号:…无一匹配」 | 真串号:资源体的 out_trade_no/trade_no/resource_id 与本轮持有的订单号/交易号全都不匹配。绝不猜号码,原样告知并 STOP;同时核对是否用错了订单号(例如把 --order-no 填成了交易号) |
warnings 里出现「台账命中已交付订单」 |
正常现象:该订单此前交付过,正在沿用原凭证(§4.3)。不要为了"用上服务端新 token"而加 --no-ledger 重算,那会让老客户换号 |
warnings 里出现「台账内容不是合法 JSON / 台账写入失败」 |
已降级处理(按未命中/未落盘),本次仍可交付。但要提醒用户台账盘可能被清理或被占满,否则老订单的 sticky 保护失效 |
自动等待 exit 3 / timeout |
正常结局,不是故障:上限内没等到付款。按 text 输出即可,不要重新取 402、不要重发支付——订单在支付宝侧仍然成立 |
自动等待 exit 2 / QUERY_FAILED |
「查询支付凭证失败」= 该单在支付宝侧从未成立(付款被拒或号码抄错)。停止等待是对的:让用户核对第 4 步输出里的订单号/交易号 |
自动等待 exit 2 / QUERY_CALL_FAILED |
连续 3 次调用本身失败(spawn 失败/查询超时),与「钱有没有到」无关。修好环境后重跑即可,付款不受影响 |
自动等待 exit 2 / ALIPAY_BOT_NOT_FOUND |
先 command -v alipay-bot;Windows 上真实文件名是 alipay-bot.exe(bash 会自动补 .exe,Node 不会,故脚本自己探测后缀)。也可用 --alipay-bot <路径> 显式指定 |
自动等待 exit 2 / QUERY_TARGET_MISSING |
既没给订单号也没给交易号 → 回第 4 步输出里取对客标签,不要重新发起支付 |
自动等待 exit 4 / failed |
该单明确不成功(支付失败/已关闭/已退款 等)→ 立即停止等待,不出号,劝止重复付款 |
自动等待 exit 2 / A2M_RESOURCE_INVALID |
查到成功但资源体校验不过(坏 content、跨订单串号、pick_algorithm 不匹配)→ 停止等待并原样报错,绝不猜号码 |
| 自动等待期间有若干次「输出无法识别」 | 正常路径现在不该再出现:未付款信封走结构化 status 判定(payment_unconfirmed → pending)。若仍出现,说明服务端换了状态值或 CLI 换了字段名 → 按 §3.1 设计继续等(宁可等满,也不误判成功、不误判失败),事后把新信封补进 §9 第 7 项 |
CLI 报 success:false + errorCode=FULFILLMENT_FAILED「履约失败」 |
这是陷阱,不是支付失败:重复查询一张已履约的订单时,服务端回「订单已履约,不重复提供」(code=ALREADY_FULFILLED),CLI 却把它包成 success:false 且退出码 1,但资源体照旧带回(2026-09-19 生产实测)。判定顺序必须是「先取资源体,再看失败标记」→ 有资源体就照常出号并留 warning;failed 标记里故意不含 履约失败 |
自动等待报 ALREADY_DELIVERED |
服务端说该单已履约、且没有重新下发资源体 → 立即停止等待并如实说明「号码此前已发出、不是支付失败」。不要当失败重付,也不要反复重查 |
| 上游取不到数据 | 服务端不出账(宁可 503),不要出现「用户付了钱拿不到东西」 |
原样透传的边界:CLI 的任何对客结果在整轮只输出一次,不加标题、解释、结论或后续引导;
只执行本文件与官方 402 文档列出的子命令。
7. 安全边界(硬性)
Payment-Needed、Payment-Proof、Authorization、Set-Cookie一律不进日志、不进对话;
对外只允许出现长度 + 摘要指纹。- 凭证文件仅当前用户可读(目录 0700 / 文件 0600),用后不跨订单复用。
- 密钥、
.env、商户私钥、支付宝公钥不由本技能持有,全部在服务端;本技能包内不得出现。 - 不构造官方 CLI 未公开的命令(如退款),不把不可信值当命令或路径片段。
- 自动等待(
wait-pay.js)只允许402-query-payment-status:子命令与参数位由常量拼装,
订单标识只走--out-shake-no/--trade-no的值位,任何来自资源体或用户输入的字符串
都不得进入子命令位置;脚本不接收外部注入的命令。 - 自动等待期间不接触任何支付凭证(
Payment-Proof由官方 CLI 自行管理),
也因此不需要、也不会读session/*.needed。 - 凭证台账(
~/.ssq-create/ledger/)只存凭证与订单标识,不存号码、不含任何支付凭据;
目录 0700 / 文件 0600。台账键必须过白名单校验后才拼路径,不得把资源体里的字符串直接当路径。 - 本地兜底凭证由订单标识派生,属于可复算的确定性值而非机密;但仍按内部数据对待,
对外只出现摘要指纹,不把完整 token 打进对话。
7.1 文档脱敏规则(写文档时必守)
本技能包会整包分发/发布,SKILL.md 与 scripts/、test/ 里的注释与字面值都算公开内容。
写文档时以下内容一律不得出现,必须改写为占位符或按位置描述:
| 不得出现 | 改写方式 |
|---|---|
| 真实姓名、商户实名、账号名称 | 只描述状态(如「商户已非本机买家账号」),不写名字 |
| 生产资源地址、支付链接的字面值 | 写 <RESOURCE_URL> 占位符,取值指向 fetch-402.js 的 DEFAULT_RESOURCE_URL 或上一步输出字段 |
| 本机绝对路径、用户名 | 一律用 ~ 与 $HOME;版本号用 <版本> 占位 |
| 真实订单号 / 交易号 / 凭证指纹原文 | 用 ORDER_... 之类的示例形态;确需记录时只留末 4 位并与场景说明绑定 |
密钥、私钥、公钥、Authorization、Payment-Needed 原文 |
永不出现;对外只允许「长度 + 摘要指纹」 |
平台账号 ID、ownerId 等标识 |
不写入文档(_meta.json 由平台托管,且不在打包白名单内,不会随包分发) |
代码里的功能必需值(如 fetch-402.js 的默认资源地址)不属此列——删了技能不可用;
但也只在代码常量里出现一次,文档不得重复抄写,避免文档与代码漂移后互相矛盾。
8. 安装 / 运行 / 本地加载 / 打包
8.1 安装(装入 WorkBuddy 技能目录)
# 用户级技能目录,装完所有项目都能用
cp -r <本目录> ~/.workbuddy/skills/ssq-create
ls ~/.workbuddy/skills/ssq-create/SKILL.md
8.2 运行
cd ~/.workbuddy/skills/ssq-create
node scripts/derive.js --selftest 20000 # 派生算法自检
node test/selftest.js # 八场景离线自测(160 项断言)
node test/mock-a2m-server.js --port 0 # 单独拉起本地 mock 服务端
node test/verify-402-stop-before-pay.js --session-id '<id>' # 停在付款确认前
node scripts/fetch-402.js --method GET --expect-amount-fen 1 # 取真实 402(会打生产)
# 有界自动等待(第 4 步出码后调用;只查询,绝不发起支付)
node scripts/wait-pay.js --out-shake-no '<订单号>' --interval 30 --max-wait 300
node scripts/wait-pay.js --trade-no '<交易号>' --interval 30 --max-wait 300
node scripts/wait-pay.js --help
验证兜底路径(不需服务端配合,用 mock 的 pay-no-token 状态):
# 起 mock 并把状态切到「支付成功但未签发 pick_token」,然后渲染即可看到兜底输出
node -e "const {createMockServer}=require('./test/mock-a2m-server');\
const m=createMockServer({amountFen:1});m.start(0).then(b=>{\
m.setState('pay-no-token');\
console.log(b+m.RESOURCE_PATH+'?out_trade_no=ORDER_DEMO');})"
# 渲染真实资源体(凭证台账默认落在 ~/.ssq-create/ledger)
node scripts/a2m.js render --resource <资源体.json> --order-no ORDER_DEMO
离线验证自动等待(不用真钱包、不用真服务端):
# 1) 起 mock 当资源站;2) 用测试替身冒充 alipay-bot;
# 3) 替身在「查询两次之后」才回报已付款 —— 验证命中即出号
SSQ_ALIPAY_BOT=./test/fake-alipay-bot.js \
FAKE_DIR=/tmp/ssq-fake FAKE_MODE=unpaid-then-paid:2 \
SSQ_RESOURCE_URL='http://127.0.0.1:<mock端口>/demo/a2m/resource' \
node scripts/wait-pay.js --out-shake-no ORDER_DEMO --interval 1 --max-wait 10 --immediate
# FAKE_MODE 可选:unpaid / unpaid-then-paid:<n> / processing-then-paid:<n> /
# fail / invalid-order / weird / call-error / already-delivered-no-body
# <n> = 前 n 次查询回报未付款,第 n+1 次起回报已付款(n=0 即首次就付款)
也可以 npm run selftest / npm run verify / npm run mock / npm run wait -- --out-shake-no <订单号>
(package.json 里已配好)。
8.3 本地加载
把技能目录放进 ~/.workbuddy/skills/<name>/ 后,当轮会话内即可用 Skill 工具加载并触发
(返回的 base directory 即新装入的目录),无需重启会话。正式对外发布前再确认新会话可见性。
8.4 打包(Windows 上无 zip 命令,故用纯 Node 实现)
cd ~/.workbuddy/skills/ssq-create
node scripts/pack.js # 输出到 ../ssq-create-<package.json 的 version>.zip
node scripts/pack.js --out D:/tmp/ssq.zip # 指定输出
node scripts/pack.js --list <zip 路径> # 只列清单 + 扫描已有包
scripts/pack.js 用 Node 内置 zlib.deflateRawSync 直接写标准 ZIP(deflate + CRC32),
不依赖任何外部命令(本机 Git Bash 没有 zip;PowerShell 5.1 读无 BOM 的 UTF-8 脚本会
让中文乱码,从 Bash 里调 PowerShell 又被安全策略拦掉)。规则是白名单而非黑名单:
只收 SKILL.md、package.json、.gitignore、scripts/、test/,其余从源头就进不来。
之后还有三道关:
- 防御性剔除:任何路径含
node_modules/session/state/logs或文件名形如.env*、
*.pem、*.key、*.needed、*.state、*.log的条目一律拒绝,命中即报错退出; - 内容扫描:对包内文本文件匹配私钥头、支付宝公钥字段名、应用私钥字段名、
2088开头的
13 位商户号、JWT 前缀(扫描脚本自身的特征字面量已拆分拼接,避免自命中;全零占位商户号
已列入安全白名单); - 回读校验:重新解析归档的中央目录、逐条解压、再扫一遍,证明落盘产物同样干净。
绝不进包:node_modules/、.env 及其变体、任何 *.pem/*.key、支付宝配置文件、
状态文件(session/、ledger/、*.needed、*.state、*.log)。
真正的拦截机制是 pack.js 的白名单(只收 SKILL.md、package.json、.gitignore、
scripts/、test/)+ 三道关(防御性剔除 → 内容扫描 → 回读复扫),
不依赖 .gitignore(本目录当前也没有该文件;若日后补上,它只是额外一层,白名单仍是主护栏)。
运行时的 402 凭证写在 ~/.ssq-create/session/、凭证台账写在 ~/.ssq-create/ledger/,
都在技能目录之外,从位置上就进不了包。
注意区分:scripts/ledger.js 是代码,属于技能包;
它运行时读写的 ~/.ssq-create/ledger/ 数据目录不属于技能包。
9. 上线前必须补齐的真实配置
| # | 缺口 | 现状 | 需要谁提供 |
|---|---|---|---|
| 1 | 服务端出账金额 | ✅ 已对齐:技能声明 amount_fen: 1(0.01 元),示例响应出账 0.01。服务端仍应把出账金额放在配置项(如 SSQ_PRICE)里而非硬编码 |
无需补充(服务端保持 0.01) |
| 2 | 服务端签发 pick_token |
🟡 不再是阻塞项,但仍应补齐。2026-09-19 实测服务端返回 demo 占位体:content 内只有 这是 AI 生成的内容示例,请替换为你的真实付费内容,既无 pick_token 也无 pick_algorithm。技能端已于 v1.1.0 加入本地兜底(§4.2),交付不再中断、对客明示来源;--require-server-token 可随时验服务端是否真的签发 |
服务端:履约时签发 pick_token(32 字节随机 hex,与订单一起持久化、同一订单只签发一次)并写入资源体,同时带 pick_algorithm: "ssq-v1";另需替换 demo 占位文案 |
| 2b | 兜底期间的凭证迁移 | ⚠️ 必须知悉:兜底凭证由订单标识派生,服务端补上真 token 后,这些老订单的号码会变。技能端用凭证台账(§4.3)粘住已交付订单,要求运行环境保留 ~/.ssq-create/ledger/ 不被清理 |
运维:保留台账目录;若台账丢失,老订单会按服务端新 token 换号 |
| 3 | 支付宝商户配置(serviceId、网关、应用私钥、支付宝公钥、sellerId) |
✅ 阻塞已解除:2026-09-18 曾实测返回 TRADE_PROGRESS_FAILED「买家和卖家不能相同」(sellerId 与买家同一账号);2026-09-19 复测 402-buyer-pay 正常返回「✓ 支付待确认」,商户已不是本机买家账号(卖家已换为非本机账号),自买自卖阻塞消失。已走到「等待用户付款」环节 |
无需补充(待一次完整付款闭环验证) |
| 4 | 买家钱包开通状态 | ✅ 已核实:alipay-bot check-wallet → code:200 已开启支付宝支付功能 |
无需补充 |
| 5 | 生产资源站可用性 | ✅ 已恢复:2026-09-18 19:29 实测默认资源地址返回 402 + Payment-Needed(amount: "0.01"),命令 node scripts/fetch-402.js --method GET --expect-amount-fen 1 走通(地址字面值见 fetch-402.js,文档不重复记录) |
无需补充(保持后端常驻) |
| 6 | 生产域名 TLS 兼容性 | Node 请求可正常握手;curl 在 Windows Schannel 下不可用 |
服务端保持 TLS 1.3 即可,客户端沿用本技能的 Node 脚本 |
| 6b | 订单号口径:收银台号 vs 402 出账号 | 🟡 技能端已兜底,但服务端仍应拉平。2026-09-25 生产实测:402 出账体回 out_trade_no=ORDER_1790259436071_9e7aae3f,收银台给 out_shake_no=20260924008282221700000000301064,而服务端履约时把前者回填进资源体 → 原实现按单字段相等比对,用户已付款成功却被判 A2M_RESOURCE_INVALID,号码无法交付。技能端已于 v1.2.3 改为多标识集合相交(优先用两侧共享的 trade_no 做锚点)并被 N7b/N7c 断言锁住 |
服务端:履约时把收银台订单号也回填进资源体(建议增字段 out_shake_no),使三套标识可互相对照;或至少保证 trade_no 稳定回填(当前已满足)。技能端已不阻塞交付 |
| 7 | 未付款时 CLI 的真实输出文本 | ✅ 已采样(2026-09-19)。--output json 下的未付款信封是结构化的,文本里没有任何中文「未支付」标记:<br>{ "active": false, "resourceDelivered": false, "status": "payment_unconfirmed", "success": true, "tradeNo": "…" }<br>三个反直觉点:没有 errorCode、没有 resourceResponse、且 success 仍为 true(它只表示「查询本身成功」,不代表已付款)。classifyQueryOutput 已改为先读信封 status 字段、正则只作兜底,并已进自测 8.60–8.67 与子进程端到端 8.47b |
无需补充(processing 分支的状态值仍是推演,由正则兜底) |
| 8 | alipay-bot 在 Windows 下的文件名后缀 |
✅ 已处理:真实文件是 alipay-bot.exe(bash 会自动补 .exe,Node 的 spawn 不会)。wait-pay.js 会自行探测 .exe/.cmd/.bat 后缀并对 .js 形态改用当前 Node 执行。v1.2.3 起后缀探测不再按平台二选一,Linux/macOS 上同样能发现带 Windows 后缀的候选 |
无需补充 |
| 9 | 运行环境身份稳定性(容器/沙箱) | ⚠️ 2026-09-25 实测暴露:沙箱环境经休眠/重建后 runtimeEnv 被识别为 container,钱包绑定的 identityFingerprint 随之失配,支付宝侧对 402-buyer-pay 与 check-wallet/apply-wallet 一律返回脱敏话术(security_error / 「系统暂时繁忙」)——网络正常,纯风控判定。 |
运维:真实支付应部署在固定环境(常驻服务器),避免容器/沙箱重建导致身份漂移;若必须在容器内,需在每次环境变更后重新走 alipay-authenticate-wallet 授权 |
| 10 | Node.js 版本下限 | ⚠️ 官方安装器要求 ≥ 22.16.0(check-env 硬校验,minNodeVersion),技能自身仅需 >= 18。2026-09-25 实测:22.13.1 会被 install-cli 拒绝 |
运维:环境准备 Node ≥ 22.16.0 |
| 11 | 官方安装器 install-cli 的 tar/unzip 不匹配 |
⚠️ 官方工具缺陷:@alipay/agent-payment 把 ZIP 格式的包以 .tgz 命名,却用 tar -xzf 解压,必然报 This does not look like a tar archive。2026-09-25 实测可通过「手动下载 → unzip → 拷贝 + 建软链」绕过 |
建议向支付宝反馈;绕过脚本见本技能安装记录 |