付费 Skill 路由上云部署(统一后端 pay-all)
付费访问协议(Pay Skill · A2M / HTTP 402 按量计费)
本技能的完整版运维手册为付费资源(¥0.10/次),由后端服务 backend/(Node.js + Express,支付宝 AI 按量付费)对外提供。Agent 按以下固定流程执行,顺序不可颠倒:
① 付费前置检查(probe):GET https://pay.stockscore.online/deploy-route/analysis(不带凭证)
② 服务端返回 HTTP 402 + Payment-Needed Header(Base64URL 账单:订单号/金额/有效期/商家签名)
③ 解码账单 → 向用户呈现商品名称与金额 → 唤起支付(pay:收银台链接/二维码)
④ 支付完成后,携带 Payment-Proof Header 重试同一请求
⑤ 服务端验付(alipay.aipay.agent.payment.verify)→ 幂等履约 → 履约确认(fulfillment confirm)→ 返回 200 + 完整运维手册(Payment-Validation Header 标记验付通过)
Agent 执行规则:
- 402 是"需付费"的正常状态,不得当错误处理;同一请求 + Payment-Proof 重试即为 complete 流程。
- 履约幂等:重复携带同一 Payment-Proof 返回同一份手册,不重复计费。
- 若支付中断,可用订单号 查单对账 恢复,再重试 delivery。
- 必须等用户 ack(确认已支付/已拿到内容)后再结束;本机无法用真金白银跑完最后一公里。
- 本地 SKILL.md 的"免费层"已包含流程骨架;完整可复制命令脚本在付费接口后端,不在本文件。
- 支付宝应用私钥严禁打进任何发布包;仅在云 pay.stockscore.online 后端环境变量中。
何时用
新增一个方法论 Skill 的付费后端路由(如 /mungers/analysis),且 SkillHub 已发布(商店层 channelPricing 已设),但云上 3004 后端还没有这条路由 → 买家付费后 404、拿不到工具箱。此时需要把本地源推上云、让 Caddy 转发、后端重载。
典型征兆:curl https://pay.stockscore.online/xxx/analysis 返回 HTTP 404 Cannot GET /xxx/analysis(X-Powered-By: Express,说明已到 3004 但无此路由)。
关键事实(本机 / 云)
- 本地源 truth:
D:\wbtmp\skillhub-pay-all(server.js + cards.js + routes.json + lib/ + config/ + data/) - 云主机:
43.129.234.236,部署目录/opt/skillhub-pay-all,PORT=3004 - systemd:
skillhub-pay-all.service(enabled,开机自启) - 域名/反代:Caddy
pay.stockscore.online,各 path → 内部端口;pay-all 路由统一到127.0.0.1:3004;/graham/*例外走 3000(独立订单库,勿动) - SSH 密钥:
D:\wbtmp\id_ed25519 - SSH 必带参数:
$common=@('-o','StrictHostKeyChecking=no','-o','UserKnownHostsFile=D:/wbtmp/skh/kh_tmp','-o','ConnectTimeout=15','-F','D:/wbtmp/ssh_config_empty') - 统一 serviceId:
API_C7A7C2EF7B474517
路由三处改动(推上云之前,先在本地源 D:\wbtmp\skillhub-pay-all 完成)
cards.js:写评分卡const xxx = { key, title, evaluate(input){...} },底部 CARDS 映射注册'/xxx/analysis': xxxserver.js:DEFAULT_ROUTES加一行(serviceId 先用REPLACE_XXX占位;含 path / orderPrefix / serviceType / goodsName / reportTitle / summary / priceAmount)routes.json:加{ "path": "/xxx/analysis", "serviceId": "API_C7A7C2EF7B474517", "priceAmount": "0.10" }(最终售价与真实 serviceId 在此覆盖)
- 价格只由
routes.json的priceAmount决定,跟环境变量无关。 - 本地校验:
node --check cards.js && node --check server.js,并实跑evaluateCard('/xxx/analysis', {...真实数据})对照该 skill 自带示例结论。 - 评分卡阈值必须来自该 skill 的
references/*.md,不许编数字。
上云四步(PowerShell,本机执行)
$k='D:\wbtmp\id_ed25519'
$common=@('-o','StrictHostKeyChecking=no','-o','UserKnownHostsFile=D:/wbtmp/skh/kh_tmp','-o','ConnectTimeout=15','-F','D:/wbtmp/ssh_config_empty')
# 1) 推源(源 dir 无 node_modules 时很快;有则整体覆盖)
& scp.exe @common -i $k -r 'D:\wbtmp\skillhub-pay-all' root@43.129.234.236:/opt/
# 2) 装依赖(可能 >120s → 调用时 timeout 调到 420000)
& ssh.exe @common -i $k root@43.129.234.236 'cd /opt/skillhub-pay-all && npm install --registry=https://registry.npmmirror.com --no-audit --no-fund'
# 3) Caddy:本地重写整份 Caddyfile 加 handle(见下「Caddy 加路由」),scp 覆盖,validate+reload
# 4) 重启后端(Node 不热重载,必须重启)
& ssh.exe @common -i $k root@43.129.234.236 'systemctl restart skillhub-pay-all.service; sleep 2; systemctl status --no-pager skillhub-pay-all.service'
Caddy 加路由(重要坑)
- 推荐整份重写 Caddyfile 再 scp 覆盖(比 sed 插入稳)。在
pay.stockscore.online {块内、兜底handle { ... }之前加:
handle /xxx/* { reverse_proxy 127.0.0.1:3004 } - 若用 sed 插入:必须用
i\(插到目标行之前当兄弟节点),绝不能用a\——a\会把新块插进上一行handle {…}内部变成嵌套,Caddyvalidate仍报 Valid 但外部永远 404(极难一眼看出)。 - 先备份:
cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak - 校验:
caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile→ 必须输出Valid configuration - 热加载:
caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile --force /graham/*保持走 3000,别动(它有独立订单库,切走会让在途订单验付失败)。
验收(一条命令看穿)
& curl.exe -s -D 'D:\wbtmp\xxx_h.txt' -o NUL 'https://pay.stockscore.online/xxx/analysis'
期望 HTTP 402 + Payment-Needed 头。用 node 解码 base64url 账单,核对:
- amount = routes.json 的 priceAmount
- resource_id = /xxx/analysis
- service_id = API_C7A7C2EF7B474517
- out_trade_no 前缀 = server.js 里设的 orderPrefix
同时看 systemd status 日志里是否出现 /xxx/analysis | ¥0.10 | serviceId=... 一行,确认路由已加载。
解码账单的 node 片段(ASCII 路径脚本)
const fs=require('fs');
const t=fs.readFileSync('D:\\wbtmp\\xxx_h.txt','utf8').match(/Payment-Needed:\s*([^\r\n]+)/)[1].trim().replace(/\r$/,'');
const j=JSON.parse(Buffer.from(t.replace(/-/g,'+').replace(/_/g,'/'),'base64').toString('utf8'));
console.log('amount='+j.protocol.amount,'resource_id='+j.protocol.resource_id,'service_id='+j.method.service_id);
本机 PowerShell 沙箱铁律(不做就整条被拒 / 卡死)
- Bash 工具损坏(exit 125) → 全程 PowerShell。
- PowerShell stdout 不回流 → 所有结果
[System.IO.File]::WriteAllText(...)写文件 → 用 Read 回读。 - 远程命令里的
$(...)、%VAR%会被安全扫描拦截(曾因$(date +%Y%m%d%H%M%S)整条被拒)→ 用固定文件名(如Caddyfile.bak)或把动态值放到本地变量。 - scp/ssh 原生 exe 可直呼(2026-09-18 起恢复);但远程 shell 变量
$?也尽量避开,改用把输出写文件再 scp 回本地读。 npm install可能 >120s → PowerShell 调用时把timeout调大到 420000ms,否则默认超时返回 -1(脚本会被杀,后续行不执行)。- 路径含撇号
zhang'lei→ 用$env:USERPROFILE+Join-Path拼接,别写死字面量(如 node 路径:Join-Path $env:USERPROFILE '.workbuddy\binaries\node\versions\22.22.2\node.exe')。
一句话流程
本地三改(cards/server/routes.json)→ scp 推云 → npm install → Caddy 加 handle + validate + reload → systemd restart → curl 探针 402 + 解账单验收。