@itpay/cli 2.0.20 → 2.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,8 +35,10 @@ export class BackendClient {
35
35
  createCheckout(input, idempotencyKey) {
36
36
  return this.http.post("/v1/checkouts", input, idempotencyKey ? { idempotencyKey } : undefined);
37
37
  }
38
- getCheckoutPresentation(checkoutID, displayToken) {
38
+ getCheckoutPresentation(checkoutID, displayToken, locale) {
39
39
  const qs = new URLSearchParams({ display_token: displayToken });
40
+ if (locale)
41
+ qs.set("locale", locale);
40
42
  return this.http.get(`/v1/checkouts/${encodeURIComponent(checkoutID)}/presentation?${qs}`);
41
43
  }
42
44
  // --- Payment intents ---
@@ -13,6 +13,7 @@ import { validateContext } from "../state/client_context.js";
13
13
  import { dispatchRender } from "../render/index.js";
14
14
  import { platformKeyForHost } from "../render/plan.js";
15
15
  import { ensureIdeImageAttach } from "../render/ide.js";
16
+ import { localizeCardURL, normalizeCardLocale } from "../render/locale.js";
16
17
  import { buildAgentChatHandoff } from "../render/markdown.js";
17
18
  import { formatMoney } from "../render/output.js";
18
19
  import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
@@ -68,8 +69,10 @@ export async function runBuy(backend, config, options) {
68
69
  });
69
70
  const checkoutID = checkout.checkout.checkout_id;
70
71
  const displayToken = checkout.display_token;
72
+ const locale = normalizeCardLocale(options.locale);
71
73
  const checkoutURL = tokenizedCheckoutURL(checkout.checkout_url, displayToken, checkout.qr_payload);
72
- const qrPNGURL = checkout.qr_png_url ? absolutePublicURL(config.baseURL, checkout.qr_png_url) : undefined;
74
+ const cardURL = localizeCardURL(absolutePublicURL(config.baseURL, checkout.card_url ?? fallbackCardURL(config.baseURL, checkoutID, displayToken)), locale);
75
+ const qrPNGURL = localizeCardURL(absolutePublicURL(config.baseURL, checkout.card_png_url ?? checkout.qr_png_url ?? fallbackCardPNGURL(config.baseURL, checkoutID, displayToken)), locale);
73
76
  const orderItems = cart.items.map((item) => ({
74
77
  title: item.title,
75
78
  quantity: item.quantity,
@@ -106,12 +109,14 @@ export async function runBuy(backend, config, options) {
106
109
  host: options.host,
107
110
  checkoutID,
108
111
  checkoutURL,
112
+ cardURL,
109
113
  displayToken,
110
114
  qrPayload: checkout.qr_payload,
111
115
  nextAction: checkout.checkout.next_action,
112
116
  orderItems,
113
117
  orderCurrency: checkout.checkout.currency,
114
118
  ...(options.agentType ? { agentType: options.agentType } : {}),
119
+ locale,
115
120
  };
116
121
  if (qrPNGURL)
117
122
  planInput.qrPNGURL = qrPNGURL;
@@ -138,7 +143,7 @@ export async function runBuy(backend, config, options) {
138
143
  displayToken,
139
144
  plan,
140
145
  waitStatus,
141
- ...(qrPNGURL ? { qrPNGURL } : {}),
146
+ qrPNGURL,
142
147
  ...(paymentIntent ? { paymentIntent } : {}),
143
148
  ...(options.agentType ? { agentType: options.agentType } : {}),
144
149
  ...(options.target ? { target: options.target } : {}),
@@ -197,7 +202,7 @@ function buildBuyEnvelope(input) {
197
202
  }
198
203
  const presentationHandoff = buildCheckoutHandoff({
199
204
  platform,
200
- url: input.checkoutURL,
205
+ url: input.plan.linkOnlyURL ?? input.checkoutURL,
201
206
  amount,
202
207
  plan: input.plan,
203
208
  ...(input.agentType ? { agentType: input.agentType } : {}),
@@ -213,7 +218,7 @@ function buildBuyEnvelope(input) {
213
218
  result,
214
219
  handoff: presentationHandoff.handoff,
215
220
  instruction: presentationHandoff.instruction,
216
- next: { command: `itpay checkout --id ${input.checkoutID} --token ${input.displayToken} --json`, reason: "稍后查询同一笔 Checkout 状态" },
221
+ next: { command: input.plan.afterActionCommand ?? `itpay checkout --id ${input.checkoutID} --token ${input.displayToken} --json`, reason: "稍后查询同一笔 Checkout 状态" },
217
222
  recovery: [],
218
223
  };
219
224
  }
@@ -242,7 +247,7 @@ async function waitForPaymentSSE(backend, checkoutID, displayToken, timeoutSec)
242
247
  export function buildCheckoutQRPlan(input) {
243
248
  const summary = `Scan the QR or open ${input.checkoutURL} to start the human checkout flow.`;
244
249
  const isPayment = input.paymentIntentID != null;
245
- const afterCommand = qualifyItPayCommand(`itpay checkout --id ${input.checkoutID} --token ${input.displayToken} --json`, input.agentType);
250
+ const afterCommand = qualifyItPayCommand(`itpay checkout --id ${input.checkoutID} --token ${input.displayToken}${input.locale === "en" ? " --locale en" : ""} --json`, input.agentType);
246
251
  const platform = {
247
252
  text: summary,
248
253
  links: [
@@ -262,6 +267,7 @@ export function buildCheckoutQRPlan(input) {
262
267
  host: input.host,
263
268
  summary,
264
269
  url: input.qrPayload,
270
+ ...(input.agentType?.trim().toLowerCase() === "workbuddy" && input.cardURL ? { linkOnlyURL: input.cardURL } : {}),
265
271
  preferredQRSources: [input.qrPNGURL ?? input.qrPayload],
266
272
  checkoutID: input.checkoutID,
267
273
  platform,
@@ -280,6 +286,13 @@ export function buildCheckoutQRPlan(input) {
280
286
  plan.paymentIntentID = input.paymentIntentID;
281
287
  return plan;
282
288
  }
289
+ function fallbackCardURL(baseURL, checkoutID, displayToken) {
290
+ const root = baseURL.replace(/\/$/, "");
291
+ return `${root}/v1/checkouts/${encodeURIComponent(checkoutID)}/card?display_token=${encodeURIComponent(displayToken)}`;
292
+ }
293
+ function fallbackCardPNGURL(baseURL, checkoutID, displayToken) {
294
+ return `${fallbackCardURL(baseURL, checkoutID, displayToken)}.png`.replace("/card?", "/card.png?");
295
+ }
283
296
  // --- contact field interaction ---
284
297
  function findMissingContactFields(contact, fields) {
285
298
  return fields.filter((field) => {
@@ -4,12 +4,14 @@ import { ensureIdeImageAttach } from "../render/ide.js";
4
4
  import { buildAgentChatHandoff } from "../render/markdown.js";
5
5
  import { platformKeyForHost } from "../render/plan.js";
6
6
  import { renderTerminalQR } from "../render/qr.js";
7
+ import { localizeCardURL, normalizeCardLocale } from "../render/locale.js";
7
8
  import { DEFAULT_BASE_URL } from "../state/config.js";
8
9
  import { buildCheckoutQRPlan } from "./buy.js";
9
10
  import { buildCheckoutHandoff, shouldPrepareLocalCheckoutImage } from "./checkout_handoff.js";
10
11
  import { writeCommandEnvelope } from "./guidance.js";
11
12
  export async function runCheckoutPresentation(backend, options) {
12
- const presentation = await backend.getCheckoutPresentation(options.checkoutID, options.displayToken);
13
+ const locale = normalizeCardLocale(options.locale);
14
+ const presentation = await backend.getCheckoutPresentation(options.checkoutID, options.displayToken, locale === "en" ? locale : undefined);
13
15
  const host = options.host ?? "terminal";
14
16
  if (!checkoutNeedsHumanHandoff(presentation.checkout.status)) {
15
17
  const envelope = terminalCheckoutEnvelope(presentation);
@@ -21,15 +23,18 @@ export async function runCheckoutPresentation(backend, options) {
21
23
  return;
22
24
  }
23
25
  const checkoutURL = checkoutPageURL(options.baseURL, options.checkoutID, options.displayToken);
24
- const qrPNGURL = absolutePublicURL(options.baseURL, presentation.qr_png_url ?? checkoutQRPNGURL(options.baseURL, options.checkoutID, options.displayToken));
25
- const nextCommand = `itpay checkout --id ${options.checkoutID} --token ${options.displayToken} --json`;
26
+ const cardURL = localizeCardURL(absolutePublicURL(options.baseURL, presentation.card_url ?? checkoutCardURL(options.baseURL, options.checkoutID, options.displayToken)), locale);
27
+ const qrPNGURL = absolutePublicURL(options.baseURL, presentation.card_png_url ?? presentation.qr_png_url ?? checkoutCardPNGURL(options.baseURL, options.checkoutID, options.displayToken));
28
+ const localizedPNGURL = localizeCardURL(qrPNGURL, locale);
29
+ const nextCommand = `itpay checkout --id ${options.checkoutID} --token ${options.displayToken}${locale === "en" ? " --locale en" : ""} --json`;
26
30
  const plan = buildCheckoutQRPlan({
27
31
  host,
28
32
  checkoutID: options.checkoutID,
29
33
  checkoutURL,
34
+ cardURL,
30
35
  displayToken: options.displayToken,
31
36
  qrPayload: checkoutURL,
32
- qrPNGURL,
37
+ qrPNGURL: localizedPNGURL,
33
38
  nextAction: presentation.checkout.next_action,
34
39
  orderItems: presentation.items.map((item) => ({
35
40
  title: item.title,
@@ -62,7 +67,7 @@ function pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand, a
62
67
  const amount = formatMoney(presentation.checkout.amount_minor, presentation.checkout.currency);
63
68
  const presentationHandoff = buildCheckoutHandoff({
64
69
  platform,
65
- url: checkoutURL,
70
+ url: plan.linkOnlyURL ?? checkoutURL,
66
71
  amount,
67
72
  plan,
68
73
  ...(agentType ? { agentType } : {}),
@@ -138,9 +143,13 @@ function checkoutPageURL(baseURL, checkoutID, displayToken) {
138
143
  const root = publicRoot(baseURL);
139
144
  return `${root}/checkout/${encodeURIComponent(checkoutID)}?display_token=${encodeURIComponent(displayToken)}`;
140
145
  }
141
- function checkoutQRPNGURL(baseURL, checkoutID, displayToken) {
146
+ function checkoutCardURL(baseURL, checkoutID, displayToken) {
142
147
  const root = publicRoot(baseURL);
143
- return `${root}/v1/checkouts/${encodeURIComponent(checkoutID)}/qr.png?display_token=${encodeURIComponent(displayToken)}`;
148
+ return `${root}/v1/checkouts/${encodeURIComponent(checkoutID)}/card?display_token=${encodeURIComponent(displayToken)}`;
149
+ }
150
+ function checkoutCardPNGURL(baseURL, checkoutID, displayToken) {
151
+ const root = publicRoot(baseURL);
152
+ return `${root}/v1/checkouts/${encodeURIComponent(checkoutID)}/card.png?display_token=${encodeURIComponent(displayToken)}`;
144
153
  }
145
154
  function publicRoot(baseURL) {
146
155
  return (baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
@@ -7,13 +7,14 @@ export function isWorkBuddyPlainChat(agentType, platform) {
7
7
  }
8
8
  export function buildCheckoutHandoff(input) {
9
9
  const handoff = { url: input.url };
10
+ const workBuddyLinkOnly = isWorkBuddyPlainChat(input.agentType, input.platform);
10
11
  if (input.platform === "markdown") {
11
12
  if (input.localPath)
12
13
  handoff.qr_local_path = input.localPath;
13
14
  if (input.markdown)
14
15
  handoff.markdown = input.markdown;
15
16
  }
16
- else if (input.platform === "plain_chat" && input.qrImageURL) {
17
+ else if (input.platform === "plain_chat" && input.qrImageURL && !workBuddyLinkOnly) {
17
18
  handoff.qr_image_url = input.qrImageURL;
18
19
  }
19
20
  else if (input.platform === "telegram" && input.qrImageURL) {
@@ -24,15 +25,12 @@ export function buildCheckoutHandoff(input) {
24
25
  }
25
26
  return {
26
27
  handoff,
27
- instruction: checkoutHandoffInstruction(input.agentType, input.platform, input.amount, Boolean(input.qrImageURL)),
28
+ instruction: checkoutHandoffInstruction(input.agentType, input.platform, input.amount),
28
29
  };
29
30
  }
30
- function checkoutHandoffInstruction(agentType, platform, amount, hasQRImage) {
31
+ function checkoutHandoffInstruction(agentType, platform, amount) {
31
32
  if (isWorkBuddyPlainChat(agentType, platform)) {
32
- if (!hasQRImage) {
33
- return `Backend 尚未确认付款,且本次没有返回可展示的二维码。向用户说明金额 ${amount} 并发送 handoff.url,然后停止等待。不要调用 present_files,不要检查本地文件,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
34
- }
35
- return `Backend 尚未确认付款。读取 handoff.qr_image_url 的完整字符串,原样作为 files 数组唯一元素调用 present_files({ files: ["<完整 qr_image_url>"] });确认右侧二维码预览已打开后,向用户说明金额 ${amount} 并发送 handoff.url,然后停止等待。如果 present_files 失败,只发送 handoff.url 并说明二维码预览未打开,然后停止。不要检查本地文件,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
33
+ return `Backend 尚未确认付款。向用户说明金额 ${amount},直接发送并打开 handoff.url,然后停止等待。不要调用 present_files,不要检查本地文件,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
36
34
  }
37
35
  if (platform === "markdown") {
38
36
  return `Backend 尚未确认付款。把 handoff.markdown 原样发送到当前桌面对话,确认二维码、付款链接和金额 ${amount} 均已实际对用户可见,然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
@@ -15,7 +15,7 @@ const INSTRUCTIONS = {
15
15
  "codex-cli": "在 Codex CLI 中始终传这个 Agent Type;付款交接只应显示在用户可见终端。",
16
16
  "claude-code-desktop": "在 Claude Code Desktop 中始终传这个 Agent Type;付款时把返回的二维码和链接实际展示到当前对话。",
17
17
  "claude-code-cli": "在 Claude Code CLI 中始终传这个 Agent Type;付款交接只应显示在用户可见终端。",
18
- workbuddy: "在 WorkBuddy 中始终传这个 Agent Type。运行会持久化 ~/.itpay-v3 的 ItPay Bash 命令前,把 Shell 工具参数 dangerouslyDisableSandbox 设为 true;用户拒绝权限时停止,不要切换 Node、删除 identity 或处理 lock。Checkout 返回 qr_image_url 时,按当次 instruction 调用 present_files 在右侧打开二维码,不要检查本地二维码文件。",
18
+ workbuddy: "在 WorkBuddy 中始终传这个 Agent Type。运行会持久化 ~/.itpay-v3 的 ItPay Bash 命令前,把 Shell 工具参数 dangerouslyDisableSandbox 设为 true;用户拒绝权限时停止,不要切换 Node、删除 identity 或处理 lock。Checkout 只返回已渲染的付款 Card Link;直接打开 handoff.url,不要调用 present_files,也不要检查本地二维码文件。",
19
19
  "kimi-code": "保持 kimi-code Agent Type,使用标准 CLI 命令和终端付款交接;不要切换到其他平台身份。",
20
20
  openclaw: "保持 openclaw Agent Type;每个展示命令都从当前可信会话上下文显式传 --host,IM 入口同时传 --target。Telegram 使用返回的原生 message action,其他入口展示标准二维码和付款链接。",
21
21
  };
@@ -20,10 +20,18 @@ function payEnvelope(intent, options) {
20
20
  const terminal = ["failed", "expired", "refunded"].includes(intent.status);
21
21
  const verified = intent.status === "verified" || intent.status === "partially_refunded";
22
22
  const handoff = {};
23
- if (!terminal && !verified && intent.action?.qr_image_url)
24
- handoff.qr_image_url = intent.action.qr_image_url;
25
- if (!terminal && !verified && intent.action?.mobile_wallet_url)
26
- handoff.mobile_wallet_url = intent.action.mobile_wallet_url;
23
+ const workBuddyLinkOnly = isWorkBuddyPlainChat(options.agentType, platformKeyForHost(options.host));
24
+ if (!terminal && !verified && workBuddyLinkOnly) {
25
+ const url = intent.action?.mobile_wallet_url ?? intent.action?.qr_image_url;
26
+ if (url)
27
+ handoff.url = url;
28
+ }
29
+ else {
30
+ if (!terminal && !verified && intent.action?.qr_image_url)
31
+ handoff.qr_image_url = intent.action.qr_image_url;
32
+ if (!terminal && !verified && intent.action?.mobile_wallet_url)
33
+ handoff.mobile_wallet_url = intent.action.mobile_wallet_url;
34
+ }
27
35
  const hasAction = Object.keys(handoff).length > 0;
28
36
  const amount = formatMoney(intent.amount_minor, intent.currency);
29
37
  return {
@@ -35,7 +43,7 @@ function payEnvelope(intent, options) {
35
43
  amount,
36
44
  },
37
45
  ...(hasAction ? { handoff } : {}),
38
- instruction: payInstruction(options, verified, terminal, hasAction, Boolean(handoff.qr_image_url), Boolean(handoff.mobile_wallet_url), amount),
46
+ instruction: payInstruction(options, verified, terminal, hasAction, amount),
39
47
  next: {
40
48
  command: `itpay checkout --id ${options.checkoutID} --token ${options.displayToken} --json`,
41
49
  reason: verified ? "读取权威订单和履约状态" : "读取同一 Checkout 的权威付款状态",
@@ -43,7 +51,7 @@ function payEnvelope(intent, options) {
43
51
  recovery: [],
44
52
  };
45
53
  }
46
- function payInstruction(options, verified, terminal, hasAction, hasQR, hasWallet, amount) {
54
+ function payInstruction(options, verified, terminal, hasAction, amount) {
47
55
  if (verified)
48
56
  return "付款已确认;不要再次展示付款动作,继续读取同一 Checkout。";
49
57
  if (terminal)
@@ -52,11 +60,7 @@ function payInstruction(options, verified, terminal, hasAction, hasQR, hasWallet
52
60
  return "Payment Intent 尚未返回可展示动作;不要猜测渠道链接,回到同一 Checkout 查询。";
53
61
  const platform = platformKeyForHost(options.host);
54
62
  if (isWorkBuddyPlainChat(options.agentType, platform)) {
55
- if (hasQR && hasWallet)
56
- return `这是受控逃生入口。读取 handoff.qr_image_url 的完整字符串,原样作为 files 数组唯一元素调用 present_files({ files: ["<完整 qr_image_url>"] });右侧预览打开后说明金额 ${amount}、发送 handoff.mobile_wallet_url 并停止等待。如果 present_files 失败,发送 handoff.qr_image_url 和 handoff.mobile_wallet_url 并说明二维码预览未打开,然后停止。不要立即查询、创建替代 Checkout 或 Payment Intent。`;
57
- if (hasQR)
58
- return `这是受控逃生入口。读取 handoff.qr_image_url 的完整字符串,原样作为 files 数组唯一元素调用 present_files({ files: ["<完整 qr_image_url>"] });右侧预览打开后说明金额 ${amount} 并停止等待。如果 present_files 失败,把 handoff.qr_image_url 作为可点击链接发送给用户并说明二维码预览未打开,然后停止。不要立即查询、创建替代 Checkout 或 Payment Intent。`;
59
- return `这是受控逃生入口。说明金额 ${amount},把 handoff.mobile_wallet_url 作为可点击链接发送给用户,然后停止等待;不要立即查询、创建替代 Checkout 或 Payment Intent。`;
63
+ return `这是受控逃生入口。说明金额 ${amount},把 handoff.url 作为可点击链接发送给用户,然后停止等待。不要调用 present_files,不要立即查询、创建替代 Checkout 或 Payment Intent。`;
60
64
  }
61
65
  if (options.host === "codex" || options.host === "claude-code")
62
66
  return "这是受控逃生入口;把 handoff 中的二维码或钱包链接实际发到当前桌面对话,然后停止等待。";
@@ -23,6 +23,18 @@ function refundStateEnvelope(refund, status) {
23
23
  instruction = "退款已成功;交付永久关闭。";
24
24
  if (refund.status === "cancelled" || refund.status === "rejected")
25
25
  instruction = "退款未执行,交付资格可恢复;旧 grant 不会复活,需要用户重新授权。";
26
+ if (refund.status === "failed") {
27
+ if (refund.failure_class === "known_no_effect")
28
+ instruction = "退款渠道请求确认未发送;不要自行重试。请用户联系平台管理员决定是否重新执行。";
29
+ else if (refund.failure_class === "retryable")
30
+ instruction = "退款渠道明确返回可重试失败;不要自行重试。请用户等待平台管理员处理。";
31
+ else if (refund.failure_class === "outcome_unknown")
32
+ instruction = "退款请求结果未知,交付继续锁定;必须先由平台对账,禁止重试或重复申请。";
33
+ else if (refund.failure_class === "permanent")
34
+ instruction = "退款渠道明确拒绝本次退款;不要重试。请用户联系平台支持。";
35
+ else
36
+ instruction = "退款执行失败;不要重试或重复申请,请用户联系平台支持。";
37
+ }
26
38
  return {
27
39
  status,
28
40
  result: {
@@ -3,6 +3,7 @@ import { validateContext } from "../state/client_context.js";
3
3
  import { dispatchRender } from "../render/index.js";
4
4
  import { ensureIdeImageAttach } from "../render/ide.js";
5
5
  import { buildCheckoutHandoff, shouldPrepareLocalCheckoutImage } from "./checkout_handoff.js";
6
+ import { localizeCardURL, normalizeCardLocale } from "../render/locale.js";
6
7
  import { buildAgentChatHandoff } from "../render/markdown.js";
7
8
  import { platformKeyForHost } from "../render/plan.js";
8
9
  import { renderTerminalQR } from "../render/qr.js";
@@ -413,14 +414,18 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
413
414
  const checkout = response.checkout;
414
415
  const checkoutID = checkout.checkout.checkout_id;
415
416
  const displayToken = checkout.display_token;
417
+ const locale = normalizeCardLocale(options.locale);
416
418
  const checkoutURL = tokenizedCheckoutURL(checkout.checkout_url, displayToken, checkout.qr_payload);
419
+ const cardURL = localizeCardURL(absolutePublicURL(config.baseURL, checkout.card_url ?? fallbackCardURL(config.baseURL, checkoutID, displayToken)), locale);
420
+ const cardPNGURL = localizeCardURL(absolutePublicURL(config.baseURL, checkout.card_png_url ?? checkout.qr_png_url ?? fallbackCardPNGURL(config.baseURL, checkoutID, displayToken)), locale);
417
421
  const plan = buildCheckoutQRPlan({
418
422
  host,
419
423
  checkoutID,
420
424
  checkoutURL,
425
+ cardURL,
421
426
  displayToken,
422
427
  qrPayload: checkout.qr_payload,
423
- ...(checkout.qr_png_url ? { qrPNGURL: absolutePublicURL(config.baseURL, checkout.qr_png_url) } : {}),
428
+ qrPNGURL: cardPNGURL,
424
429
  nextAction: checkout.checkout.next_action,
425
430
  orderItems: response.cart.items.map((item) => ({
426
431
  title: item.title,
@@ -430,6 +435,7 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
430
435
  })),
431
436
  orderCurrency: checkout.checkout.currency,
432
437
  ...(options.agentType ? { agentType: options.agentType } : {}),
438
+ locale,
433
439
  });
434
440
  options.persistHandoff?.({
435
441
  serviceExecutionID,
@@ -458,7 +464,7 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
458
464
  ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
459
465
  });
460
466
  }
461
- const envelope = buildServicesCheckoutEnvelope(response, checkoutURL, plan, config.baseURL, options.agentType, options.target);
467
+ const envelope = buildServicesCheckoutEnvelope(response, checkoutURL, plan, options.agentType, options.target);
462
468
  const plainResult = [
463
469
  `service_execution_id: ${response.binding.service_execution_id}`,
464
470
  `checkout_id: ${checkoutID}`,
@@ -939,18 +945,18 @@ function parseValue(value) {
939
945
  return Number(value);
940
946
  return value;
941
947
  }
942
- function buildServicesCheckoutEnvelope(response, checkoutURL, plan, baseURL, agentType, target) {
948
+ function buildServicesCheckoutEnvelope(response, checkoutURL, plan, agentType, target) {
943
949
  const checkout = response.checkout;
944
950
  const platform = platformKeyForHost(plan.host);
945
951
  const amount = formatMoney(checkout.checkout.amount_minor, checkout.checkout.currency);
946
952
  const presentationHandoff = buildCheckoutHandoff({
947
953
  platform,
948
- url: checkoutURL,
954
+ url: plan.linkOnlyURL ?? checkoutURL,
949
955
  amount,
950
956
  plan,
951
957
  ...(agentType ? { agentType } : {}),
952
958
  ...(target ? { target } : {}),
953
- ...(checkout.qr_png_url ? { qrImageURL: absolutePublicURL(baseURL, checkout.qr_png_url) } : {}),
959
+ ...(plan.preferredQRSources[0] ? { qrImageURL: plan.preferredQRSources[0] } : {}),
954
960
  ...(plan.ideImageAttach?.status === "downloaded" && plan.ideImageAttach.localPath
955
961
  ? { localPath: plan.ideImageAttach.localPath }
956
962
  : {}),
@@ -968,12 +974,19 @@ function buildServicesCheckoutEnvelope(response, checkoutURL, plan, baseURL, age
968
974
  handoff: presentationHandoff.handoff,
969
975
  instruction: presentationHandoff.instruction,
970
976
  next: {
971
- command: `itpay checkout --id ${checkout.checkout.checkout_id} --token ${checkout.display_token} --json`,
977
+ command: plan.afterActionCommand ?? `itpay checkout --id ${checkout.checkout.checkout_id} --token ${checkout.display_token} --json`,
972
978
  reason: "仅在用户完成付款操作或要求查询后,读取同一 Checkout 的权威状态",
973
979
  },
974
980
  recovery: [],
975
981
  };
976
982
  }
983
+ function fallbackCardURL(baseURL, checkoutID, displayToken) {
984
+ const root = baseURL.replace(/\/$/, "");
985
+ return `${root}/v1/checkouts/${encodeURIComponent(checkoutID)}/card?display_token=${encodeURIComponent(displayToken)}`;
986
+ }
987
+ function fallbackCardPNGURL(baseURL, checkoutID, displayToken) {
988
+ return `${fallbackCardURL(baseURL, checkoutID, displayToken)}.png`.replace("/card?", "/card.png?");
989
+ }
977
990
  function checkoutCapabilityID(response, fallback = "") {
978
991
  return response.capability_id || fallback;
979
992
  }
package/dist/src/main.js CHANGED
@@ -691,6 +691,7 @@ program
691
691
  .option("--require-contact <fields>", "comma-separated required contact fields: email,phone")
692
692
  .option("--qr-format <format>", "unicode|utf8|ansi|terminal")
693
693
  .option("--qr-file <path>", "explicit QR file path")
694
+ .option("--locale <locale>", "payment card language: zh-CN|en", "zh-CN")
694
695
  .option("--pay", "also create a payment intent and optionally wait for verification")
695
696
  .option("--method <alipay|wechatpay>", "payment method for --pay", "alipay")
696
697
  .option("--no-wait", "do not wait for payment verification after --pay")
@@ -752,6 +753,7 @@ program
752
753
  ...(requiredContactFields ? { requiredContactFields } : {}),
753
754
  ...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
754
755
  ...(options.qrFile ? { qrFilePath: options.qrFile } : {}),
756
+ locale: options.locale,
755
757
  ...(options.pay ? { pay: true, payMethod: options.method, noWait: options.wait === false, payTimeoutSec: timeout } : {}),
756
758
  ...(jsonOutput ? { jsonOutput: true } : {}),
757
759
  };
@@ -779,6 +781,7 @@ program
779
781
  .option("--target <target>")
780
782
  .option("--id <checkout_id>")
781
783
  .option("--token <display_token>")
784
+ .option("--locale <locale>", "payment card language: zh-CN|en", "zh-CN")
782
785
  .option("--json", "output compact JSON")
783
786
  .action(async (options) => {
784
787
  const config = loadConfig();
@@ -800,6 +803,7 @@ program
800
803
  ...(options.target ? { target: options.target } : {}),
801
804
  ...(config.agentType ? { agentType: config.agentType } : {}),
802
805
  baseURL: config.baseURL,
806
+ locale: options.locale,
803
807
  jsonOutput: Boolean(options.json),
804
808
  });
805
809
  }
@@ -1166,6 +1170,7 @@ services
1166
1170
  .option("--target <target>", "chat id / channel id / open id for IM hosts")
1167
1171
  .option("--qr-format <format>", "unicode|utf8|ansi|terminal")
1168
1172
  .option("--qr-file <path>", "explicit QR file path")
1173
+ .option("--locale <locale>", "payment card language: zh-CN|en", "zh-CN")
1169
1174
  .option("--json", "output JSON instead of terminal text")
1170
1175
  .action(async (serviceExecutionID, options) => {
1171
1176
  const config = loadConfig();
@@ -1182,6 +1187,7 @@ services
1182
1187
  ...(options.target ? { target: options.target } : {}),
1183
1188
  ...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
1184
1189
  ...(options.qrFile ? { qrFilePath: options.qrFile } : {}),
1190
+ locale: options.locale,
1185
1191
  jsonOutput: Boolean(options.json),
1186
1192
  persistHandoff: (handoff) => {
1187
1193
  session.rememberCheckout({
@@ -0,0 +1,27 @@
1
+ export function normalizeCardLocale(value) {
2
+ switch (value?.trim().toLowerCase()) {
3
+ case undefined:
4
+ case "":
5
+ case "zh":
6
+ case "zh-cn":
7
+ case "zh_cn":
8
+ case "cn":
9
+ return "zh-CN";
10
+ case "en":
11
+ case "en-us":
12
+ case "en_us":
13
+ return "en";
14
+ default:
15
+ throw new Error(`unsupported card locale: ${value}; expected zh-CN or en`);
16
+ }
17
+ }
18
+ export function localizeCardURL(value, locale) {
19
+ try {
20
+ const parsed = new URL(value);
21
+ parsed.searchParams.set("locale", locale);
22
+ return parsed.toString();
23
+ }
24
+ catch {
25
+ return value;
26
+ }
27
+ }
@@ -6,6 +6,10 @@
6
6
  import { ideImageAttachBlock } from "./ide.js";
7
7
  export function renderPlainChat(plan, options = {}) {
8
8
  const out = options.output ?? ((line) => process.stdout.write(line));
9
+ if (plan.linkOnlyURL) {
10
+ out(`${plan.summary}\nopen: ${plan.linkOnlyURL}\n`);
11
+ return;
12
+ }
9
13
  const brand = plan.preferredQRSources.find((src) => src.length > 0);
10
14
  const lines = [];
11
15
  lines.push(plan.summary);
@@ -12,8 +12,8 @@ import { DeviceAuthority } from "./device_authority.js";
12
12
  import { OperationJournal } from "./operation_journal.js";
13
13
  export const DEFAULT_BASE_URL = "https://app.itpay.ai";
14
14
  export const DEV_BASE_URL = "https://dev.itpay.ai";
15
- export const CLI_VERSION = "2.0.20";
16
- export const API_CONTRACT_REVISION = "sha256:ab3e76d7eee227c9192d41023c5bdf0e0e793bcad294287c75de6ac606f8caf7";
15
+ export const CLI_VERSION = "2.0.22";
16
+ export const API_CONTRACT_REVISION = "sha256:0c6470d9e3c1b4ec317d6c1b0b597acc13ec10d39312f994ece4cfd25cb0b65e";
17
17
  const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
18
18
  const CART_SESSION_FILENAME = "cart.json";
19
19
  const OPERATION_JOURNAL_FILENAME = "operations.json";
@@ -31,7 +31,7 @@
31
31
  "Use only handoff fields returned for the current Host and make them actually visible to the human.",
32
32
  "Before creating a single-Service Checkout, send the exact price confirmation requested by the current instruction, stop, and wait for explicit human agreement.",
33
33
  "After presenting a Checkout, stop. Run the returned next.command only after the human completes the payment action or asks to query the same Checkout.",
34
- "For workbuddy with plain-chat, present the returned HTTPS QR with present_files exactly as instructed; presentation failure falls back only to handoff.url and never creates another payment resource.",
34
+ "For workbuddy with plain-chat, send or open the fully rendered Card Link in handoff.url and never call present_files or create another payment resource.",
35
35
  "For openclaw with telegram, execute the returned native message action using the current trusted target. For any other explicit OpenClaw Host, show the returned HTTPS QR image and Checkout URL.",
36
36
  "Payment is verified only by Backend Checkout or Order state, never by QR rendering, redirect, or user claim.",
37
37
  "A terminal payment state must never display another payment handoff."
@@ -33,7 +33,7 @@
33
33
  {
34
34
  "agent_type": "workbuddy",
35
35
  "default_host": "plain-chat",
36
- "responsibility": "when handoff.qr_image_url exists, use its exact HTTPS value as the only files element in present_files; otherwise send the Checkout URL without calling present_files; show the amount and stop"
36
+ "responsibility": "show the amount and send or open handoff.url, which is the fully rendered HTML Card Link; never call present_files; then stop"
37
37
  },
38
38
  {
39
39
  "agent_type": "kimi-code",
@@ -63,7 +63,7 @@
63
63
  "agent_rules": [
64
64
  "Agent Type is stable identity; Host is presentation and may be explicitly overridden.",
65
65
  "A local QR path is not visible until a desktop Agent attaches or renders that file on the human-facing surface; WorkBuddy never receives a local QR path.",
66
- "For workbuddy with plain-chat, call present_files exactly as the returned instruction says. If it fails, send only handoff.url, report that the preview did not open, and stop.",
66
+ "For workbuddy with plain-chat, send or open only the rendered handoff.url and never call present_files, inspect files, or rebuild a QR.",
67
67
  "Do not claim the handoff was shown until both a usable payment image or QR and the Checkout URL are visible.",
68
68
  "IM Hosts that require a target must receive --target before Checkout creation. OpenClaw has no default Host; Kimi Code uses terminal.",
69
69
  "OpenClaw Telegram uses flat url/value buttons for deployed-version compatibility. A grant_confirmed callback carries only the Checkout ID, never a display token, and never proves payment or an active grant."
@@ -72,7 +72,7 @@
72
72
  "Do not expose display tokens separately from the tokenized Checkout handoff.",
73
73
  "Do not print base64 images, mirror lists, renderer debug state, or provider QR URLs.",
74
74
  "Do not claim a desktop attachment exists when only a filesystem path was printed.",
75
- "Do not inspect, download, or regenerate a local QR file for WorkBuddy."
75
+ "Do not call present_files or inspect, download, or regenerate a local QR file for WorkBuddy."
76
76
  ],
77
77
  "next_docs": [
78
78
  {
@@ -14,7 +14,7 @@
14
14
  | `codex-cli` | `terminal` | 在用户可见终端渲染二维码并输出付款链接;若用户不看该终端,要求改用正确 Host。 |
15
15
  | `claude-code-desktop` | `claude-code` | 返回桌面对话可展示的 Markdown 图片和付款链接,要求先展示再等待。 |
16
16
  | `claude-code-cli` | `terminal` | 在用户可见终端输出二维码和链接,不声称已在桌面对话展示。 |
17
- | `workbuddy` | `plain-chat` | 只返回 HTTPS 二维码和付款链接;要求 Agent `present_files` 在右侧预览打开二维码,不返回或检查本地图片路径。 |
17
+ | `workbuddy` | `plain-chat` | 只返回完整渲染的 HTML Card Link;直接打开链接,不调用 `present_files`,不返回或检查本地图片路径。 |
18
18
  | `kimi-code` | `terminal` | 使用标准 CLI 引导,在用户可见终端渲染二维码和付款链接。 |
19
19
  | `openclaw` | 无;必须显式传入 | `--host telegram` 使用 OpenClaw 原生 `message` action;其他入口返回标准 HTTPS 二维码和付款链接。 |
20
20
 
@@ -43,9 +43,9 @@
43
43
  "amount": "<amount> <currency>"
44
44
  },
45
45
  "handoff": {
46
- "url": "<checkout_url>",
46
+ "url": "<checkout_or_rendered_card_url>",
47
47
  "qr_local_path": "<desktop_optional_local_path>",
48
- "qr_image_url": "<workbuddy_optional_absolute_https_png>",
48
+ "qr_image_url": "<non_workbuddy_optional_absolute_https_png>",
49
49
  "markdown": "<desktop_optional_host_ready_markdown>"
50
50
  },
51
51
  "instruction": "<agent-type-specific instruction>",
@@ -66,11 +66,11 @@
66
66
  | `claude-code-desktop / claude-code` | `url, qr_local_path, markdown` |
67
67
  | `codex-cli / terminal` | `url`;非 JSON 输出另外渲染终端二维码 |
68
68
  | `claude-code-cli / terminal` | `url`;非 JSON 输出另外渲染终端二维码 |
69
- | `workbuddy / plain-chat` | `url, qr_image_url` |
69
+ | `workbuddy / plain-chat` | `url`(完整渲染的 HTML Card Link) |
70
70
  | `kimi-code / terminal` | `url`;非 JSON 输出另外渲染终端二维码 |
71
71
  | `openclaw / telegram` | `url, qr_image_url, agent_action` |
72
72
  | `openclaw / other` | `url, qr_image_url` |
73
73
 
74
- WorkBuddy instruction 只在 `handoff.qr_image_url` 存在时要求读取其完整字符串并作为 `files` 数组唯一元素调用 `present_files`。如果该可选字段不存在,必须直接发送 `handoff.url`,明确不要调用 `present_files`。两种情况都要停止等待;不能检查本地文件、下载或重建二维码、调用 `pay` 或创建替代付款资源。显式 `--host` 仍覆盖默认展示方式,因此只有 `workbuddy + plain-chat` 使用该规则。
74
+ WorkBuddy instruction 必须直接发送并打开 `handoff.url`,明确禁止 `present_files`。这个 URL 指向 Backend 已渲染的 HTML Card;不能检查本地文件、下载或重建二维码、调用 `pay` 或创建替代付款资源。显式 `--host` 仍覆盖默认展示方式。
75
75
 
76
76
  OpenClaw Telegram 的 `handoff.agent_action` 是可原样执行的原生 `message` tool action。`presentation` 只包含标准 `blocks.buttons`:`📱 手机点这儿支付` 使用扁平 `url`,`📋 已授权给我读` 使用扁平 `value=itp:grant_confirmed:<checkout_id>`;二维码单独使用 action 的 `media`。CLI `instruction` 必须要求 Agent 原样执行该 action,不得改写 Presentation、换用其他消息工具或声称普通文本回复等同于已发送按钮。收到授权 callback 后立即执行 `next.command` 查询同一 Checkout,再只跟随后端返回的同一 Execution grant 流程;callback 只携带 Checkout ID,不携带 display token,也不证明付款或 grant 已生效。OpenClaw `target` 使用原生 chat target(如 `5559456744` 或 `-1001234567890:topic:42`),不添加 `telegram:` 前缀。
@@ -165,7 +165,7 @@ itpay buy \
165
165
  | `codex-cli` | `terminal` | `url` | 非 JSON 模式在用户可见终端渲染二维码;始终保留付款链接。 |
166
166
  | `claude-code-desktop` | `claude-code` | `url`、可用时 `qr_local_path` 和 `markdown` | 把 Markdown handoff 发到当前桌面对话,不能只输出本地路径。 |
167
167
  | `claude-code-cli` | `terminal` | `url` | 在用户可见终端展示;不能声称桌面对话已收到图片。 |
168
- | `workbuddy` | `plain-chat` | `url, qr_image_url?` | `qr_image_url` 时调用 `present_files`;没有时只发送金额和 `url` 且不调用工具。两者都停止,不读取本地文件。 |
168
+ | `workbuddy` | `plain-chat` | `url` | `url` 是完整渲染的 HTML Card Link;直接发送/打开,不调用 `present_files`,然后停止。 |
169
169
  | `kimi-code` | `terminal` | `url` | 使用标准 CLI 非 JSON 终端二维码和链接。 |
170
170
  | `openclaw` | 必须显式 | Telegram 返回 `url,qr_image_url,agent_action`;其他入口返回 `url,qr_image_url` | Telegram 执行原生 `message` action;其他入口直接展示图片和链接。 |
171
171
 
@@ -71,7 +71,7 @@ token 缺失或不匹配时使用本机句柄恢复。只有请求的 Checkout
71
71
  | `codex-cli` | `url`;普通文本模式渲染终端二维码。 |
72
72
  | `claude-code-desktop` | `url, qr_local_path, markdown`;原样发送 Markdown。 |
73
73
  | `claude-code-cli` | `url`;普通文本模式渲染终端二维码。 |
74
- | `workbuddy` | `url, qr_image_url?`;有二维码 URL 时按 `services checkout` 相同规则调用 `present_files`,没有时只发送 Checkout URL,不生成本地文件。 |
74
+ | `workbuddy` | 只返回 `url`;它是完整渲染的 HTML Card Link。直接打开,不调用 `present_files`,不生成本地文件。 |
75
75
  | `kimi-code` | `url`;普通文本模式渲染标准终端二维码。 |
76
76
  | `openclaw` | Telegram 为 `url,qr_image_url,agent_action`;instruction 强制原样执行 action。`📋 已授权给我读` callback 触发同一 Checkout 查询,再由 Backend 决定是否进入 grant 读取;其他显式 Host 为 `url,qr_image_url`。 |
77
77
 
@@ -117,7 +117,7 @@ OpenClaw 额外明确没有默认入口:
117
117
  | `codex-cli` | `terminal` | 只在用户可见终端展示付款交接。 |
118
118
  | `claude-code-desktop` | `claude-code` | 桌面对话必须实际展示二维码和付款链接。 |
119
119
  | `claude-code-cli` | `terminal` | 只在用户可见终端展示付款交接。 |
120
- | `workbuddy` | `plain-chat` | 会持久化 `~/.itpay-v3` 的 Bash 调用先设 Shell 工具参数 `dangerouslyDisableSandbox=true`;拒绝权限时停止,不切换 Node、不删除 identity、不处理 lock。Checkout 返回 `qr_image_url` 时按 instruction 调用 `present_files`。 |
120
+ | `workbuddy` | `plain-chat` | 会持久化 `~/.itpay-v3` 的 Bash 调用先设 Shell 工具参数 `dangerouslyDisableSandbox=true`;拒绝权限时停止。Checkout 只发送/打开渲染后的 Card Link,不调用 `present_files`。 |
121
121
  | `kimi-code` | `terminal` | 使用标准 CLI 流程和终端展示,不增加 Kimi 专属命令。 |
122
122
  | `openclaw` | 无 | 必须显式提供当前入口;Telegram 使用原生 action,其他入口展示标准二维码和链接。 |
123
123
 
@@ -100,6 +100,6 @@ API 安全合同要求后端验证 display token 是该 Checkout 当前有效的
100
100
  | `codex-cli` | `terminal` | 只在用户可见终端展示渠道动作。 |
101
101
  | `claude-code-desktop` | `claude-code` | 把安全 handoff 发到当前桌面对话。 |
102
102
  | `claude-code-cli` | `terminal` | 只在用户可见终端展示渠道动作。 |
103
- | `workbuddy` | `plain-chat` | `qr_image_url` 时调用 `present_files`;若同时有钱包链接则发送 `mobile_wallet_url`。二维码预览失败时只发送实际存在的二维码/钱包链接;只有钱包链接时直接发送它。展示后停止,不立即查询或创建替代付款。 |
103
+ | `workbuddy` | `plain-chat` | 受控逃生入口只返回一个可点击的 `handoff.url`;不得调用 `present_files`。展示后停止,不立即查询或创建替代付款。 |
104
104
 
105
105
  Host 只改变 instruction;Payment Intent ID、金额、状态、重试语义和权限必须一致。
@@ -49,7 +49,12 @@ itpay refund get <refund_request_id> [--json]
49
49
 
50
50
  - `succeeded`:instruction 明确“退款已成功;交付永久关闭”,`next=null`。
51
51
  - `cancelled/rejected`:说明交付资格可恢复,但旧 grant 不复活,需要用户重新授权,`next=null`。
52
- - `failed`:是否继续锁定以 `access_locked` `failure_class` 的服务器裁定为准,`next=null`。
52
+ - `failed + known_no_effect`:渠道请求确认未发送;Agent 不重试,由平台管理员决定是否重新执行,`next=null`。
53
+ - `failed + retryable`:渠道明确返回可重试失败;Agent 不重试,等待平台管理员处理,`next=null`。
54
+ - `failed + outcome_unknown`:渠道可能已受理;交付继续锁定,必须先对账,禁止重试或重复申请,`next=null`。
55
+ - `failed + permanent`:渠道明确拒绝;停止并联系平台支持,`next=null`。
56
+
57
+ `decision_mode=manual` 只说明该退款采用人工裁定,不得覆盖上述失败终态的 instruction。CLI 永远不向 Agent 暴露 Provider 原始响应、签名、URL、支付标识或内部错误文本。
53
58
 
54
59
  CLI 不因为退款终态自行修改订单或 grant。
55
60
 
@@ -44,7 +44,7 @@ itpay refund watch <refund_request_id> [--interval <seconds>] [--timeout <second
44
44
  }
45
45
  ```
46
46
 
47
- 其他终态使用与 `refund get` 相同的锁、恢复资格和旧 grant 规则。
47
+ 其他终态使用与 `refund get` 相同的锁、恢复资格和旧 grant 规则。特别是 `failed` 必须按 `failure_class` 区分 `known_no_effect`、`retryable`、`outcome_unknown` 和 `permanent`;任何一种都不向 Agent 返回重试命令。
48
48
 
49
49
  ## Timeout 输出
50
50
 
@@ -81,16 +81,16 @@ itpay services checkout <service_execution_id> --resume
81
81
  | `codex-cli` | `handoff={url}`;普通文本模式在用户可见终端渲染二维码。 |
82
82
  | `claude-code-desktop` | `handoff={url,qr_local_path,markdown}`;把 `handoff.markdown` 原样发送到当前桌面对话。 |
83
83
  | `claude-code-cli` | `handoff={url}`;普通文本模式在用户可见终端渲染二维码。 |
84
- | `workbuddy` | `handoff={url,qr_image_url?}`;有 `qr_image_url` 时读取完整值并作为 `files` 数组唯一元素调用 `present_files`;没有时直接发送金额与 `url`,不得调用 `present_files`。两者随后都停止,不得检查或生成本地文件。 |
84
+ | `workbuddy` | `handoff={url}`;`url` 是完整渲染的 HTML Card Link。直接发送/打开并停止,不得调用 `present_files`、检查或生成本地文件。 |
85
85
  | `kimi-code` | `handoff={url}`;复用标准 CLI 终端展示。 |
86
86
  | `openclaw` | 必须显式传 Host;Telegram 还必须传 OpenClaw 原生 Target,并返回必须原样执行的 `message` action;其他入口返回标准 `url,qr_image_url`。 |
87
87
 
88
88
  WorkBuddy 的准确 instruction 语义必须完整包含:
89
89
 
90
90
  ```text
91
- Backend 尚未确认付款。读取 handoff.qr_image_url 的完整字符串,原样作为 files 数组唯一元素调用 present_files({ files: ["<完整 qr_image_url>"] });确认右侧二维码预览已打开后,向用户说明金额并发送 handoff.url,然后停止等待。如果 present_files 失败,只发送 handoff.url 并说明二维码预览未打开,然后停止。不要检查本地文件,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。
91
+ Backend 尚未确认付款。向用户说明金额,直接发送并打开 handoff.url,然后停止等待。不要调用 present_files,不要检查本地文件,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。
92
92
  ```
93
93
 
94
- `qr_image_url` 缺失,准确 instruction 必须改为:说明本次没有可展示二维码,发送金额与 `handoff.url`,明确不要调用 `present_files`,然后遵守相同停止和付款证明规则。
94
+ `--locale` 默认 `zh-CN`,可显式使用 `--locale en`。语言只影响 Card 渲染,不改变 Checkout、付款或恢复状态。
95
95
 
96
96
  OpenClaw 的 Host/Target 校验必须发生在读取 execution 后、创建 Checkout 前;校验失败不能调用 Checkout 创建接口。Telegram `agent_action.arguments.presentation` 只使用 `blocks.buttons`,其中 `📱 手机点这儿支付` 使用扁平 `url`,`📋 已授权给我读` 使用 `value=itp:grant_confirmed:<checkout_id>`。准确 instruction 必须要求 Agent 立即用原生 `message` tool 原样执行 `handoff.agent_action`,不得改写 Presentation、换工具或用普通回复冒充按钮已发送。收到该 callback 表示用户声明已在收银台授权读取;Agent 立即执行 `next.command` 查询同一 Checkout,再只按 Backend 返回的 `next.command` 进入同一 Execution grant 流程。callback 不包含 display token,也不证明付款或 grant active;Backend 未返回 `grant_active` 前不得读取结果。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itpay/cli",
3
- "version": "2.0.20",
3
+ "version": "2.0.22",
4
4
  "description": "The single ItPay CLI entry point for buy workflows and future sell workflows.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -85,8 +85,8 @@ When `status` is `human_checkout_required`, make the amount, ItPay Checkout QR,
85
85
 
86
86
  - Desktop Agents: send `handoff.markdown` unchanged; confirm QR, amount, and link are visible, then stop.
87
87
  - CLI Agents: show the terminal QR, amount, and link in the watched terminal, then stop; never claim a desktop image was shown.
88
- - WorkBuddy with `plain-chat`: when `handoff.qr_image_url` exists, use its complete value as the only `files` element in `present_files`; confirm the right-side QR preview opened, show amount and `handoff.url`, then stop. If it is absent, send amount and `handoff.url`, do not call `present_files`, then stop.
89
- - If WorkBuddy `present_files` fails, send only `handoff.url`, report the failure, and stop. Never inspect files, switch Node, rebuild a QR, call `pay`, or create another Checkout.
88
+ - WorkBuddy with `plain-chat`: `handoff.url` is the fully rendered ItPay Card Link. Show the amount, send/open that link, then stop. Never call `present_files`, inspect files, download or rebuild a QR, call `pay`, or create another Checkout.
89
+ - Desktop image hosts receive the PNG rendered from that same Card HTML. `--locale` defaults to `zh-CN`; use `--locale en` only when the human needs English.
90
90
  - An explicit `--host` overrides presentation only. It never changes Agent identity or payment state.
91
91
 
92
92
  Run `next.command` only after the human says they acted or asks for status. QR rendering, redirects, and human claims are not payment proof; only Backend Checkout or Order state is. Normal payment uses the Checkout page; `pay` and `buy --pay` are operator escape hatches, never recovery.