@itpay/cli 0.2.4 → 0.2.6

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.
package/README.md CHANGED
@@ -214,7 +214,7 @@ redacted secure delivery status:
214
214
  itp buyer catalog search --query 企业工商 --json
215
215
  itp buyer cart create --variant var_itpay_enterprise_fuzzy_search_cny01 --input company_name=阿里 --json
216
216
  itp buyer checkout create --cart <cart_id> --email <buyer_email> --json
217
- itp buyer payment wait <payment_intent_id> --json
217
+ itp buyer payment wait <payment_intent_id> --timeout 1 --json
218
218
  itp buyer checkout status <checkout_id> --json
219
219
  ```
220
220
 
@@ -239,7 +239,8 @@ Payment QR rules:
239
239
  - Otherwise render the ItPay-hosted `qr_png_url` / `preferred_qr_url`.
240
240
  - Use `mobile_wallet_url` only as a human mobile fallback.
241
241
  - Do not generate your own QR from payment URLs.
242
- - In agent app clients, send `human_visible_markdown` to the human first, then run `next.command`.
242
+ - In agent app clients, send `human_visible_markdown` or the relevant `render_plan` output to the human first.
243
+ - If status is `payment_handoff_required`, `next` is the human reply step, not payment wait.
243
244
  - Treat only `payment_intent.verified` as payment success.
244
245
 
245
246
  If the human wants the agent to analyze delivered content, the human must reveal
package/bin/itp CHANGED
@@ -109,7 +109,7 @@ async function main() {
109
109
  "buyer checkout status <checkout_id> --json",
110
110
  "buyer checkout resume <checkout_id> --json",
111
111
  "buyer auth status --json",
112
- "buyer payment wait <payment_intent_id> --json",
112
+ "buyer payment wait <payment_intent_id> --timeout 1 --json",
113
113
  "buyer payment refresh-qr <payment_intent_id> --reason order-not-found --json",
114
114
  "buyer deliveries list --checkout <checkout_id> --json",
115
115
  "buyer deliveries show <delivery_id> --checkout <checkout_id> --json",
@@ -69,7 +69,7 @@
69
69
  {
70
70
  "intent": "do both steps through the high-level command",
71
71
  "command": "itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
72
- "success_signal": "response.status is waiting_human_auth, waiting_user_payment, payment_verified, or delivery_claimable"
72
+ "success_signal": "response.status is waiting_human_auth, payment_handoff_required, payment_verified, or delivery_claimable"
73
73
  }
74
74
  ],
75
75
  "agent_rules": [
@@ -18,7 +18,7 @@
18
18
  {
19
19
  "intent": "create payment intent through the high-level buy flow",
20
20
  "command": "itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
21
- "success_signal": "response.human_visible_markdown and response.next.command are present"
21
+ "success_signal": "response.status is payment_handoff_required; response.human_visible_markdown and response.render_plan are present; render the QR/link to the human before any status check"
22
22
  },
23
23
  {
24
24
  "intent": "recover a scanner order-not-found display problem",
@@ -33,8 +33,10 @@
33
33
  "Payment QR display requires both a valid/saved buyer session and payment_intent_id. If payment_intent_id appears before buyer_session is saved, run buyer checkout resume/status instead of showing payment QR.",
34
34
  "Do not stop after showing auth_qr unless the human explicitly asks you to pause. Keep waiting/resuming the same checkout so the post-auth payment handoff can continue.",
35
35
  "Do not describe auth_qr as payment success or payment proof. Only payment_intent.verified proves payment.",
36
- "In Codex or Claude Code app clients, the default UX is show-then-wait: send response.human_visible_markdown to the human first, then run response.next.command.",
37
- "For Telegram-style message clients, show response.human_visible_markdown and stop unless the human asks you to keep waiting or returns with a payment question.",
36
+ "In Codex or Claude Code app clients, send response.human_visible_markdown or response.render_plan.platforms.codex_app.markdown to the human first. Do not start a hidden long wait before the human-visible message is sent.",
37
+ "For Telegram-style message clients, use response.render_plan.platforms.telegram: send photo media first, include links, and expose native buttons for refresh/status when the adapter supports them.",
38
+ "For plain chat clients, show response.human_visible_markdown and stop unless the human asks you to check status or returns with a payment question.",
39
+ "When response.status is payment_handoff_required, response.next is the human reply step, not payment wait.",
38
40
  "MUST show local_qr_path first when present. Many agent clients do not reliably render remote QR images; local_qr_path is the most reliable desktop/chat display artifact.",
39
41
  "If local_qr_path is not present, show qr_png_url or preferred_qr_url as the primary scannable QR. This is an ItPay-hosted human QR image; it may render the native provider payment code for scanner reliability, but the agent must not request or decode the raw provider payload.",
40
42
  "Use qr_image_url/SVG only as a fallback when PNG/local rendering is unavailable.",
@@ -42,8 +44,8 @@
42
44
  "payment_entry_url is the stable ItPay status/payment page. qr_png_url/local_qr_path is usually the better desktop scanner artifact.",
43
45
  "The provider can briefly show order not found even after precreate succeeded. Tell the human to wait 30-60 seconds and retry the same QR/page. Do not create a new checkout, do not create a new payment intent, and do not repeatedly refresh unless the ItPay page or CLI explicitly asks for recovery.",
44
46
  "Opening the payment page must not be treated as paid.",
45
- "Do not run payment wait before the QR/link has been sent to the human.",
46
- "After showing QR, run next.command or buyer payment wait for the same payment_intent_id."
47
+ "Do not run payment wait before the QR/link has been sent to the human. Without an explicit short timeout, CLI returns payment_handoff_required instead of blocking.",
48
+ "After the human says they paid or clicks a platform status button, run response.after_human_response.check_payment_command for the same payment_intent_id."
47
49
  ],
48
50
  "forbidden": [
49
51
  "Do not encode payment_entry_url into a new QR yourself when qr_png_url/local_qr_path is available.",
@@ -17,8 +17,8 @@
17
17
  "commands": [
18
18
  {
19
19
  "intent": "wait for payment verified event",
20
- "command": "itp buyer payment wait <payment_intent_id> --json",
21
- "success_signal": "response.payment_event.event_type == payment_intent.verified"
20
+ "command": "itp buyer payment wait <payment_intent_id> --timeout 1 --json",
21
+ "success_signal": "response.payment_event.event_type is payment_intent.verified or wait.timeout"
22
22
  },
23
23
  {
24
24
  "intent": "resume checkout after verified event",
@@ -27,11 +27,12 @@
27
27
  }
28
28
  ],
29
29
  "agent_rules": [
30
- "Start payment wait only after the QR image/link has already been sent to the human.",
31
- "Payment wait is a recovery loop around the same payment_intent_id, not a one-shot command.",
30
+ "Start payment status checks only after the QR image/link has already been sent to the human or the human asks to check status.",
31
+ "If you run payment wait without a short timeout while payment is still pending, CLI returns payment_handoff_required and human_visible_markdown instead of long-polling.",
32
+ "Payment wait/status check is a recovery loop around the same payment_intent_id, not a one-shot command.",
32
33
  "wait.timeout is a heartbeat, not a payment failure.",
33
- "In Codex or Claude Code app clients, after showing QR, wait online for a short visible cycle first. If the human still has not paid, explain that the same QR/link remains valid and pause for their reply.",
34
- "Continue waiting with the same payment_intent_id unless the user explicitly cancels or asks you to pause.",
34
+ "In Codex or Claude Code app clients, after showing QR, use a short status check first. If the human still has not paid, explain that the same QR/link remains valid and pause for their reply.",
35
+ "Continue checking the same payment_intent_id unless the user explicitly cancels or asks you to pause.",
35
36
  "If the network drops, the process is interrupted, or there is no response, rerun payment wait with the same payment_intent_id or run checkout status/resume with the same checkout_id.",
36
37
  "If status is unclear, tell the human it is unclear and continue querying the same payment_intent_id/checkout_id. Do not create a new checkout.",
37
38
  "Payment success is payment_intent.verified only.",
@@ -21,7 +21,7 @@
21
21
  },
22
22
  {
23
23
  "intent": "continue waiting after refresh",
24
- "command": "itp buyer payment wait <payment_intent_id> --json",
24
+ "command": "itp buyer payment wait <payment_intent_id> --timeout 1 --json",
25
25
  "success_signal": "payment_event.event_type == payment_intent.verified"
26
26
  }
27
27
  ],
@@ -37,7 +37,7 @@
37
37
  {
38
38
  "intent": "buy a selected variant with the high-level cart-first flow",
39
39
  "command": "itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
40
- "success_signal": "response.status becomes waiting_human_auth or waiting_user_payment; if waiting_user_payment, response.human_visible_markdown and response.next.command are present"
40
+ "success_signal": "response.status becomes waiting_human_auth or payment_handoff_required; if payment_handoff_required, render response.human_visible_markdown or response.render_plan to the human before any payment status check"
41
41
  }
42
42
  ],
43
43
  "agent_rules": [
@@ -54,8 +54,9 @@
54
54
  "After showing auth_qr, keep running/resuming the same checkout unless the human explicitly asks you to pause. Do not stop merely because a QR was displayed.",
55
55
  "When a buyer command returns buyer_session.status=buyer_session_saved, the CLI has stored the buyer account session for this agent device. You may continue with checkout/payment or `buyer vault` commands without asking the human for a token.",
56
56
  "Vault grants are only for reading approved delivered artifacts. They are not buyer session credentials and do not authorize refunds or account/order management.",
57
- "Use high-level itp buy when possible, but in agent app clients split payment into two visible steps: first return and show human_visible_markdown, then run next.command.",
57
+ "Use high-level itp buy when possible, but in agent app clients split payment into two visible steps: first return and show human_visible_markdown/render_plan, then only check status after the human responds or uses a platform button.",
58
58
  "If response.human_visible_markdown is present, send it to the human immediately in the same user-visible message. It includes the QR image and payment link.",
59
+ "If response.status is payment_handoff_required, do not run payment wait from next.command; next is the human-visible reply. Only after the human asks to check payment should you run after_human_response.check_payment_command.",
59
60
  "Show the returned payment QR exactly as provided before starting payment wait.",
60
61
  "Payment truth comes only from payment_intent.verified.",
61
62
  "Secure delivery goes to the human first; report redacted delivery status only."
@@ -22,7 +22,7 @@
22
22
  },
23
23
  {
24
24
  "intent": "wait again on the same payment intent",
25
- "command": "itp buyer payment wait <payment_intent_id> --json",
25
+ "command": "itp buyer payment wait <payment_intent_id> --timeout 1 --json",
26
26
  "success_signal": "response.payment_event.event_type is payment_intent.verified or still waiting"
27
27
  },
28
28
  {
package/lib/buyer.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  writeCredentials, writeSessionCredentials, writeState
5
5
  } from "./env.js";
6
6
  import { coreApi, coreApiBase } from "./http.js";
7
- import { renderHumanAction, renderItPayPaymentAction, shouldReturnAfterAgentTextQR, writeWaitHeartbeat } from "./render-human.js";
7
+ import { buildHumanActionRenderPlan, renderHumanAction, renderItPayPaymentAction, shouldReturnAfterAgentTextQR, writeWaitHeartbeat } from "./render-human.js";
8
8
 
9
9
  async function buyerBuy(flags) {
10
10
  rejectBuyerSandboxFlag(flags);
@@ -23,6 +23,7 @@ async function buyerBuy(flags) {
23
23
  cart,
24
24
  checkout,
25
25
  human_action: checkout.human_action,
26
+ render_plan: buildHumanActionRenderPlan(checkout.human_action, {}, flags),
26
27
  agent_next_actions: checkout.agent_next_actions || ["wait_human_auth", "poll_checkout"],
27
28
  next: {
28
29
  command: cliCommand("buyer", "checkout", "resume", checkout.checkout_id, "--json"),
@@ -45,18 +46,14 @@ async function buyerBuy(flags) {
45
46
 
46
47
  if (flags.no_wait || flags.no_wait_payment || showThenWait) {
47
48
  output(buyerRunOutput({
48
- status: "waiting_user_payment",
49
+ status: "payment_handoff_required",
49
50
  selection,
50
51
  cart,
51
52
  checkout,
52
53
  payment_intent: intent,
53
- payment_handoff: paymentHandoff(intent),
54
- agent_next_actions: intent.agent_next_actions || ["wait_payment"],
55
- next: {
56
- command: paymentWaitCommand(intent.payment_intent_id, { short: true }),
57
- safe_for_agent: true,
58
- instruction: "Send human_visible_markdown to the human first, then run this wait command unless the human explicitly asked you to pause."
59
- }
54
+ ...paymentHandoffFields(intent, flags),
55
+ agent_next_actions: paymentHandoffAgentNextActions(),
56
+ next: paymentHandoffNext()
60
57
  }));
61
58
  return;
62
59
  }
@@ -218,6 +215,9 @@ async function buyer(command, rest, flags) {
218
215
  checkout,
219
216
  buyer_session: buyerSessionClaimStatus(),
220
217
  checkout_guidance: checkoutAuthorizationGuidance(checkout),
218
+ render_plan: checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth"
219
+ ? buildHumanActionRenderPlan(checkout.human_action, {}, flags)
220
+ : undefined,
221
221
  agent_next_actions: checkout.agent_next_actions || checkoutAgentNextActions(checkout),
222
222
  next: checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth"
223
223
  ? {
@@ -243,6 +243,9 @@ async function buyer(command, rest, flags) {
243
243
  checkout,
244
244
  buyer_session: buyerSessionClaimStatus(),
245
245
  checkout_guidance: checkoutAuthorizationGuidance(checkout),
246
+ render_plan: checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth"
247
+ ? buildHumanActionRenderPlan(checkout.human_action, {}, flags)
248
+ : undefined,
246
249
  agent_next_actions: checkout.agent_next_actions || checkoutAgentNextActions(checkout),
247
250
  next: checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth"
248
251
  ? {
@@ -281,6 +284,7 @@ async function buyer(command, rest, flags) {
281
284
  status: "waiting_human_auth",
282
285
  checkout,
283
286
  human_action: checkout.human_action,
287
+ render_plan: buildHumanActionRenderPlan(checkout.human_action, {}, flags),
284
288
  agent_next_actions: checkout.agent_next_actions || ["wait_human_auth", "poll_checkout"],
285
289
  next: {
286
290
  command: cliCommand("buyer", "checkout", "resume", checkoutID, "--json"),
@@ -296,6 +300,7 @@ async function buyer(command, rest, flags) {
296
300
  status: "waiting_human_auth",
297
301
  checkout,
298
302
  human_action: checkout.human_action,
303
+ render_plan: buildHumanActionRenderPlan(checkout.human_action, {}, flags),
299
304
  agent_next_actions: checkout.agent_next_actions || ["wait_human_auth", "poll_checkout"],
300
305
  next: {
301
306
  command: cliCommand("buyer", "checkout", "resume", checkoutID, "--json"),
@@ -317,19 +322,15 @@ async function buyer(command, rest, flags) {
317
322
  const intent = await getBuyerPaymentIntent(checkout.payment_intent_id, flags);
318
323
  await renderItPayPaymentAction(intent, flags);
319
324
  output(buyerRunOutput({
320
- status: intent.status === "verified" ? "payment_verified" : "waiting_user_payment",
325
+ status: intent.status === "verified" ? "payment_verified" : "payment_handoff_required",
321
326
  checkout,
322
327
  payment_intent: intent,
323
- payment_handoff: intent.status === "verified" ? undefined : paymentHandoff(intent),
328
+ ...(intent.status === "verified" ? {} : paymentHandoffFields(intent, flags)),
324
329
  buyer_session: buyerSessionClaimStatus(claimedSession),
325
- agent_next_actions: intent.agent_next_actions || checkout.agent_next_actions || ["wait_payment"],
330
+ agent_next_actions: intent.status === "verified" ? (intent.agent_next_actions || checkout.agent_next_actions || ["poll_checkout"]) : paymentHandoffAgentNextActions(),
326
331
  next: intent.status === "verified"
327
332
  ? { command: cliCommand("buyer", "checkout", "status", checkout.checkout_id, "--json"), safe_for_agent: true }
328
- : {
329
- command: paymentWaitCommand(intent.payment_intent_id, { short: true }),
330
- safe_for_agent: true,
331
- instruction: "Send human_visible_markdown to the human first, then run this wait command unless the human explicitly asked you to pause."
332
- }
333
+ : paymentHandoffNext()
333
334
  }));
334
335
  return;
335
336
  }
@@ -343,16 +344,12 @@ async function buyer(command, rest, flags) {
343
344
  const intent = await createBuyerPaymentIntent(checkout.checkout_id, flags);
344
345
  await renderItPayPaymentAction(intent, flags);
345
346
  output(buyerRunOutput({
346
- status: "waiting_user_payment",
347
+ status: "payment_handoff_required",
347
348
  checkout,
348
349
  payment_intent: intent,
349
- payment_handoff: paymentHandoff(intent),
350
- agent_next_actions: intent.agent_next_actions || ["wait_payment"],
351
- next: {
352
- command: paymentWaitCommand(intent.payment_intent_id, { short: true }),
353
- safe_for_agent: true,
354
- instruction: "Send human_visible_markdown to the human first, then run this wait command unless the human explicitly asked you to pause."
355
- }
350
+ ...paymentHandoffFields(intent, flags),
351
+ agent_next_actions: paymentHandoffAgentNextActions(),
352
+ next: paymentHandoffNext()
356
353
  }));
357
354
  return;
358
355
  }
@@ -371,6 +368,18 @@ async function buyer(command, rest, flags) {
371
368
  const paymentIntentID = flags.payment_intent || flags.payment_intent_id || positional(rest, 1) || readState().last_core_payment_intent_id;
372
369
  if (!paymentIntentID) throw new Error("payment_intent_id is required");
373
370
  const intent = await getBuyerPaymentIntent(paymentIntentID, flags);
371
+ if (intent.status !== "verified" && shouldReturnPaymentHandoffBeforeWait(flags)) {
372
+ await renderItPayPaymentAction(intent, flags);
373
+ output(buyerRunOutput({
374
+ status: "payment_handoff_required",
375
+ payment_intent: intent,
376
+ ...paymentHandoffFields(intent, flags),
377
+ payment_guidance: paymentRecoveryGuidance(intent, { event_type: "payment_display_required" }),
378
+ agent_next_actions: paymentHandoffAgentNextActions(),
379
+ next: paymentHandoffNext()
380
+ }));
381
+ return;
382
+ }
374
383
  const event = intent.status === "verified"
375
384
  ? { event_type: "payment_intent.verified", payment_intent_id: paymentIntentID, agent_next_actions: intent.agent_next_actions || ["poll_checkout"] }
376
385
  : await waitBuyerPayment(intent, flags);
@@ -382,7 +391,7 @@ async function buyer(command, rest, flags) {
382
391
  agent_next_actions: event.agent_next_actions || intent.agent_next_actions || paymentAgentNextActions(intent, event),
383
392
  next: event.event_type === "payment_intent.verified"
384
393
  ? { command: intent.checkout_id ? cliCommand("buyer", "checkout", "status", intent.checkout_id, "--json") : undefined, safe_for_agent: true }
385
- : { command: cliCommand("buyer", "payment", "wait", paymentIntentID, "--json"), safe_for_agent: true }
394
+ : { command: paymentStatusCheckCommand(paymentIntentID), safe_for_agent: true }
386
395
  }));
387
396
  return;
388
397
  }
@@ -392,18 +401,14 @@ async function buyer(command, rest, flags) {
392
401
  const refreshed = await refreshBuyerPaymentQR(paymentIntentID, flags);
393
402
  await renderItPayPaymentAction(refreshed, flags);
394
403
  output(buyerRunOutput({
395
- status: refreshed.status === "verified" ? "payment_verified" : "waiting_user_payment",
404
+ status: refreshed.status === "verified" ? "payment_verified" : "payment_handoff_required",
396
405
  payment_intent: refreshed,
397
- payment_handoff: refreshed.status === "verified" ? undefined : paymentHandoff(refreshed),
406
+ ...(refreshed.status === "verified" ? {} : paymentHandoffFields(refreshed, flags)),
398
407
  payment_guidance: paymentRecoveryGuidance(refreshed, { event_type: refreshed.status === "verified" ? "payment_intent.verified" : "qr_refreshed" }),
399
- agent_next_actions: refreshed.agent_next_actions || paymentAgentNextActions(refreshed, { event_type: refreshed.status === "verified" ? "payment_intent.verified" : "qr_refreshed" }),
408
+ agent_next_actions: refreshed.status === "verified" ? (refreshed.agent_next_actions || paymentAgentNextActions(refreshed, { event_type: "payment_intent.verified" })) : paymentHandoffAgentNextActions(),
400
409
  next: refreshed.status === "verified"
401
410
  ? { command: cliCommand("buyer", "checkout", "status", refreshed.checkout_id, "--json"), safe_for_agent: true }
402
- : {
403
- command: paymentWaitCommand(refreshed.payment_intent_id, { short: true }),
404
- safe_for_agent: true,
405
- instruction: "Send human_visible_markdown to the human first, then run this wait command unless the human explicitly asked you to pause."
406
- }
411
+ : paymentHandoffNext()
407
412
  }));
408
413
  return;
409
414
  }
@@ -899,7 +904,7 @@ async function waitBuyerCheckoutAuth(checkout, flags = {}) {
899
904
  if (!checkoutID) throw new Error("checkout_id is required");
900
905
  const timeoutMs = Number(flags.auth_timeout || flags.timeout || 900) * 1000;
901
906
  const started = Date.now();
902
- let lastHeartbeatAt = 0;
907
+ let lastHeartbeatAt = Date.now();
903
908
  let current = typeof checkout === "string" ? await getBuyerCheckout(checkoutID, flags) : checkout;
904
909
  const authAction = current?.human_action || readCoreAuthAction(checkoutID) || null;
905
910
  rememberCoreAuthAction(checkoutID, authAction);
@@ -1036,12 +1041,20 @@ function paymentAgentNextActions(intent = {}, event = {}) {
1036
1041
  return ["poll_checkout_delivery"];
1037
1042
  }
1038
1043
  if (event.event_type === "wait.timeout") {
1039
- return ["wait_same_payment_intent", "resume_checkout_status_if_needed"];
1044
+ return ["check_same_payment_intent", "resume_checkout_status_if_needed"];
1040
1045
  }
1041
1046
  if (event.event_type === "qr_refreshed") {
1042
- return ["show_returned_qr", "wait_same_payment_intent"];
1047
+ return ["show_returned_qr", "check_same_payment_intent"];
1043
1048
  }
1044
- return ["show_payment_qr_if_needed", "wait_same_payment_intent"];
1049
+ return ["show_payment_qr_if_needed", "check_same_payment_intent"];
1050
+ }
1051
+
1052
+ function paymentHandoffFields(intent = {}, flags = {}) {
1053
+ return {
1054
+ payment_handoff: paymentHandoff(intent),
1055
+ render_plan: buildHumanActionRenderPlan(intent.human_action || {}, intent, flags),
1056
+ after_human_response: paymentHandoffAfterHumanResponse(intent)
1057
+ };
1045
1058
  }
1046
1059
 
1047
1060
  function paymentHandoff(intent = {}) {
@@ -1052,7 +1065,6 @@ function paymentHandoff(intent = {}) {
1052
1065
  const preferredQRURL = action.preferred_qr_url || qrPNGURL || action.qr_image_url || intent.qr_image_url || intent.qr?.image_url || "";
1053
1066
  const localQRPath = action.local_qr_path || intent.local_qr_path || "";
1054
1067
  const mobileWalletURL = action.mobile_wallet_url || intent.mobile_wallet_url || "";
1055
- const waitCommand = paymentWaitCommand(paymentIntentID, { short: true });
1056
1068
  return {
1057
1069
  type: "payment_qr_handoff",
1058
1070
  payment_intent_id: paymentIntentID || null,
@@ -1064,9 +1076,8 @@ function paymentHandoff(intent = {}) {
1064
1076
  payment_entry_url: entryURL || null,
1065
1077
  mobile_wallet_url: mobileWalletURL || null,
1066
1078
  markdown: paymentHandoffMarkdown({ localQRPath, qrPNGURL, preferredQRURL, entryURL, mobileWalletURL }),
1067
- wait_command: waitCommand || null,
1068
1079
  safe_for_agent: true,
1069
- instruction: "Show markdown to the human first. Then run wait_command and wait for payment_intent.verified unless the human asked you to pause."
1080
+ instruction: "Render the QR/link to the human first. After the human responds or uses a platform button, query the same payment_intent_id. Only payment_intent.verified proves payment."
1070
1081
  };
1071
1082
  }
1072
1083
 
@@ -1076,15 +1087,42 @@ function paymentHandoffMarkdown({ localQRPath = "", qrPNGURL = "", preferredQRUR
1076
1087
  if (imageURL) lines.push(`![ItPay payment QR](${imageURL})`);
1077
1088
  if (entryURL) lines.push(`[打开付款页面](${entryURL})`);
1078
1089
  if (mobileWalletURL) lines.push(`[手机钱包打开](${mobileWalletURL})`);
1079
- lines.push("我会继续等待 ItPay 返回 payment_intent.verified。");
1090
+ lines.push("付款后回复“我已付款”,我会查询真实支付状态。");
1080
1091
  return lines.join("\n\n");
1081
1092
  }
1082
1093
 
1083
- function paymentWaitCommand(paymentIntentID, { short = false } = {}) {
1094
+ function paymentStatusCheckCommand(paymentIntentID) {
1084
1095
  if (!paymentIntentID) return "";
1085
- return short
1086
- ? cliCommand("buyer", "payment", "wait", paymentIntentID, "--timeout", "60", "--json")
1087
- : cliCommand("buyer", "payment", "wait", paymentIntentID, "--json");
1096
+ return cliCommand("buyer", "payment", "wait", paymentIntentID, "--timeout", "1", "--json");
1097
+ }
1098
+
1099
+ function shouldReturnPaymentHandoffBeforeWait(flags = {}) {
1100
+ if (booleanFlag(flags.wait || flags.long_wait || false)) return false;
1101
+ if (flags.timeout === undefined) return true;
1102
+ const timeout = Number(flags.timeout);
1103
+ return !Number.isFinite(timeout) || timeout > 5;
1104
+ }
1105
+
1106
+ function paymentHandoffAgentNextActions() {
1107
+ return ["show_human_visible_markdown"];
1108
+ }
1109
+
1110
+ function paymentHandoffNext() {
1111
+ return {
1112
+ type: "reply_to_human",
1113
+ safe_for_agent: false,
1114
+ instruction: "Send human_visible_markdown to the human now. Do not run payment wait until this message is visible to the human."
1115
+ };
1116
+ }
1117
+
1118
+ function paymentHandoffAfterHumanResponse(intent = {}) {
1119
+ const paymentIntentID = intent.payment_intent_id || intent.human_action?.id || "";
1120
+ return {
1121
+ check_payment_command: paymentStatusCheckCommand(paymentIntentID),
1122
+ refresh_qr_command: paymentIntentID ? cliCommand("buyer", "payment", "refresh-qr", paymentIntentID, "--reason", "order-not-found", "--json") : "",
1123
+ safe_for_agent: true,
1124
+ instruction: "Use check_payment_command after the human says they paid or a platform button requests a status check. User text is not proof; only payment_intent.verified is."
1125
+ };
1088
1126
  }
1089
1127
 
1090
1128
  function paymentRecoveryGuidance(intent = {}, event = {}) {
@@ -1099,10 +1137,10 @@ function paymentRecoveryGuidance(intent = {}, event = {}) {
1099
1137
  next_step: event.event_type === "payment_intent.verified" || intent.status === "verified"
1100
1138
  ? "付款已验证;进入 checkout status / 交付模块。"
1101
1139
  : event.event_type === "wait.timeout"
1102
- ? "wait.timeout 不是付款失败;继续等待同一个 payment_intent_id,或用 checkout status/resume 确认状态。"
1140
+ ? "wait.timeout 不是付款失败;继续查询同一个 payment_intent_id,或用 checkout status/resume 确认状态。"
1103
1141
  : event.event_type === "qr_refreshed"
1104
- ? "展示 ItPay 返回的 QR,然后继续等待同一个 payment_intent_id。"
1105
- : "展示 ItPay 返回的付款入口并等待 payment_intent.verified;用户口头说已付款不算证明。",
1142
+ ? "展示 ItPay 返回的 QR,然后继续查询同一个 payment_intent_id。"
1143
+ : "展示 ItPay 返回的付款入口;用户反馈后查询 payment_intent.verified,用户口头说已付款不算证明。",
1106
1144
  network_or_interrupt: "断网、无响应、进程中断时,重新运行 payment wait 或 checkout status/resume,使用同一个 payment_intent_id / checkout_id。",
1107
1145
  qr_problem: "QR 未显示时不要自造二维码;先用返回的 local_qr_path/qr_png_url/preferred_qr_url。扫码提示订单不存在或过期时,先等 30-60 秒重扫同一 QR,仍失败才 refresh-qr。",
1108
1146
  do_not: [
@@ -1407,12 +1445,13 @@ async function waitBuyerPayment(intent, flags = {}) {
1407
1445
  const waitURL = flags.wait_url || intent?.agent_wait?.wait_url || `/v1/payment-intents/${encodeURIComponent(paymentIntentID)}/events/wait`;
1408
1446
  const timeoutMs = Number(flags.timeout || 900) * 1000;
1409
1447
  const started = Date.now();
1410
- let lastHeartbeatAt = 0;
1448
+ let lastHeartbeatAt = Date.now();
1411
1449
  let lastEvent = null;
1412
1450
  while (Date.now() - started < timeoutMs) {
1451
+ const remainingMs = Math.max(1000, timeoutMs - (Date.now() - started));
1413
1452
  const params = new URLSearchParams();
1414
1453
  if (cursor) params.set("cursor", cursor);
1415
- params.set("timeout", String(flags.poll_timeout || "30s"));
1454
+ params.set("timeout", String(flags.poll_timeout || `${Math.ceil(Math.min(30000, remainingMs) / 1000)}s`));
1416
1455
  const event = await coreApi(appendURLQuery(waitURL, params), { method: "GET" }, flags);
1417
1456
  lastEvent = event;
1418
1457
  cursor = event.cursor || cursor;
@@ -1426,10 +1465,10 @@ async function waitBuyerPayment(intent, flags = {}) {
1426
1465
  action: intent?.human_action || null,
1427
1466
  lastHeartbeatAt,
1428
1467
  flags,
1429
- command: cliCommand("buyer", "payment", "wait", paymentIntentID, "--json")
1468
+ command: paymentStatusCheckCommand(paymentIntentID)
1430
1469
  });
1431
1470
  }
1432
- return lastEvent || { event_type: "wait.timeout", payment_intent_id: paymentIntentID, cursor, agent_next_actions: ["wait_payment"] };
1471
+ return lastEvent || { event_type: "wait.timeout", payment_intent_id: paymentIntentID, cursor, agent_next_actions: ["check_payment_status"] };
1433
1472
  }
1434
1473
 
1435
1474
  async function waitBuyerDelivery(checkoutID, flags = {}) {
@@ -1602,6 +1641,7 @@ function compactHumanAction(action = null) {
1602
1641
  if (!action || typeof action !== "object") return action;
1603
1642
  return compactObject({
1604
1643
  kind: action.kind,
1644
+ payment_intent_id: action.payment_intent_id,
1605
1645
  url: action.url,
1606
1646
  local_qr_path: action.local_qr_path,
1607
1647
  local_qr_mime: action.local_qr_mime,
@@ -1651,7 +1691,7 @@ function buyerDocsFor(value = {}) {
1651
1691
  topics.add("cart-checkout");
1652
1692
  topics.add("payment-qr");
1653
1693
  }
1654
- if (status.includes("waiting_user_payment") || actions.includes("wait_payment") || value.payment_intent?.human_action || value.payment_intent?.qr_image_url) {
1694
+ if (status.includes("payment_handoff") || status.includes("waiting_user_payment") || actions.includes("show_human_visible_markdown") || actions.includes("wait_payment") || value.payment_intent?.human_action || value.payment_intent?.qr_image_url) {
1655
1695
  topics.add("payment-qr");
1656
1696
  topics.add("payment-wait");
1657
1697
  }
package/lib/env.js CHANGED
@@ -98,10 +98,14 @@ function apiTimeoutMs(flags = {}) {
98
98
  }
99
99
 
100
100
  function parseFlags(args) {
101
+ const removedQRDisplayFlag = ["--qr", "shown"].join("-");
101
102
  const flags = {};
102
103
  for (let i = 0; i < args.length; i += 1) {
103
104
  const arg = args[i];
104
105
  if (!arg.startsWith("--")) continue;
106
+ if (arg === removedQRDisplayFlag) {
107
+ throw new Error("legacy QR display flag has been removed; show the payment QR to the human, then run buyer payment wait <payment_intent_id> --timeout 1 --json.");
108
+ }
105
109
  const key = arg.slice(2).replaceAll("-", "_");
106
110
  const next = args[i + 1];
107
111
  if (!next || next.startsWith("--")) {
@@ -7,12 +7,18 @@ import { apiTimeoutMs, cliCommand, commandExists, mergeRun, readRun, readState,
7
7
 
8
8
  async function renderItPayPaymentAction(intent, flags = {}) {
9
9
  const action = intent?.human_action ? { ...intent.human_action } : (intent?.payment_url ? {
10
+ kind: "payment_qr",
10
11
  id: intent.payment_intent_id,
12
+ payment_intent_id: intent.payment_intent_id,
11
13
  title: "Scan payment QR",
12
14
  url: intent.payment_url,
13
15
  expires_at: intent.qr?.expires_at
14
16
  } : null);
15
17
  if (action) {
18
+ if (!action.kind && isPaymentIntentHandoff(intent)) action.kind = "payment_qr";
19
+ if (!action.id && intent?.payment_intent_id) action.id = intent.payment_intent_id;
20
+ if (!action.payment_intent_id && intent?.payment_intent_id) action.payment_intent_id = intent.payment_intent_id;
21
+ if (!action.url && (intent?.payment_url || intent?.payment_entry_url)) action.url = intent.payment_url || intent.payment_entry_url;
16
22
  if (intent?.qr_png_url || intent?.qr?.png_url) {
17
23
  action.qr_png_url = intent.qr_png_url || intent.qr.png_url;
18
24
  }
@@ -35,6 +41,18 @@ async function renderItPayPaymentAction(intent, flags = {}) {
35
41
  return result;
36
42
  }
37
43
 
44
+ function isPaymentIntentHandoff(intent = {}) {
45
+ return Boolean(
46
+ intent?.payment_intent_id ||
47
+ intent?.payment_url ||
48
+ intent?.payment_entry_url ||
49
+ intent?.qr_png_url ||
50
+ intent?.qr_image_url ||
51
+ intent?.qr?.png_url ||
52
+ intent?.qr?.image_url
53
+ );
54
+ }
55
+
38
56
  function humanActionSummaryLines(action) {
39
57
  if (!action?.url) return [];
40
58
  const lines = [
@@ -265,6 +283,99 @@ function annotateHumanActionPresentation(action, qrImageURL) {
265
283
  return action;
266
284
  }
267
285
 
286
+ function buildHumanActionRenderPlan(action = {}, intent = {}, flags = {}) {
287
+ if (!action || typeof action !== "object") return null;
288
+ const kind = action.kind || (isPaymentIntentHandoff(intent) ? "payment_qr" : "human_action");
289
+ const planAction = action.kind === kind ? action : { ...action, kind };
290
+ const entryURL = intent.payment_entry_url || intent.payment_url || action.url || action.web_url || "";
291
+ const qrPNGURL = action.qr_png_url || intent.qr_png_url || intent.qr?.png_url || "";
292
+ const preferredQRURL = action.preferred_qr_url || qrPNGURL || action.qr_image_url || intent.qr_image_url || intent.qr?.image_url || "";
293
+ const localQRPath = action.local_qr_path || intent.local_qr_path || "";
294
+ const mobileWalletURL = action.mobile_wallet_url || intent.mobile_wallet_url || humanActionPresentationURL(action, "mobile_wallet_url") || "";
295
+ const imageSource = localQRPath || qrPNGURL || preferredQRURL;
296
+ const requiredOutputs = [];
297
+ if (imageSource) {
298
+ requiredOutputs.push(compactObject({
299
+ type: "image",
300
+ local_path: localQRPath || undefined,
301
+ fallback_url: preferredQRURL || qrPNGURL || undefined,
302
+ must_be_user_visible: true
303
+ }));
304
+ }
305
+ if (entryURL) requiredOutputs.push({ type: "link", label: kind === "auth_qr" ? "打开授权页面" : "打开付款页面", url: entryURL });
306
+ if (mobileWalletURL) requiredOutputs.push({ type: "link", label: "手机钱包打开", url: mobileWalletURL });
307
+ const markdown = humanActionMarkdown(planAction, { localQRPath, qrPNGURL, preferredQRURL, entryURL, mobileWalletURL });
308
+ const telegram = telegramRenderPlan(planAction, { localQRPath, preferredQRURL, entryURL, mobileWalletURL });
309
+ return compactObject({
310
+ kind,
311
+ proof_rule: kind === "payment_qr"
312
+ ? "Only payment_intent.verified proves payment."
313
+ : "This human action is not payment proof.",
314
+ host: agentHost(flags) || undefined,
315
+ required_outputs: requiredOutputs,
316
+ platforms: compactObject({
317
+ codex_app: { format: "markdown_image_and_links", markdown },
318
+ claude_code: { format: "markdown_image_and_links", markdown },
319
+ telegram,
320
+ plain_chat: {
321
+ format: "image_or_link_then_human_reply",
322
+ text: kind === "payment_qr"
323
+ ? "请扫码或点击链接完成支付。付完后回复“我已付款”,我再查询真实状态。"
324
+ : "请打开上面的授权入口。完成后告诉我,我再继续查询状态。"
325
+ },
326
+ terminal: {
327
+ format: "cli_prints_qr_then_wait",
328
+ print_terminal_qr: true,
329
+ print_links: true
330
+ }
331
+ }),
332
+ forbidden: [
333
+ "Do not say 'scan the QR above' unless an image or scannable URL is actually attached.",
334
+ "Do not read a local image file as model input and treat that as sent to the human.",
335
+ "Do not treat page open, QR display, button click, or human text as payment proof.",
336
+ "Do not create a new checkout because payment is still pending."
337
+ ]
338
+ });
339
+ }
340
+
341
+ function humanActionMarkdown(action = {}, { localQRPath = "", qrPNGURL = "", preferredQRURL = "", entryURL = "", mobileWalletURL = "" } = {}) {
342
+ const imageURL = localQRPath || qrPNGURL || preferredQRURL;
343
+ const lines = [action.kind === "auth_qr" ? "请打开 ItPay 授权入口:" : "请扫码付款:"];
344
+ if (imageURL) lines.push(`![ItPay ${action.kind === "auth_qr" ? "auth" : "payment"} QR](${imageURL})`);
345
+ if (entryURL) lines.push(`[${action.kind === "auth_qr" ? "打开授权页面" : "打开付款页面"}](${entryURL})`);
346
+ if (mobileWalletURL) lines.push(`[手机钱包打开](${mobileWalletURL})`);
347
+ lines.push(action.kind === "payment_qr" ? "付款后回复“我已付款”,我会查询真实支付状态。" : "完成授权后回复,我会继续同一个 checkout。");
348
+ return lines.join("\n\n");
349
+ }
350
+
351
+ function telegramRenderPlan(action = {}, { localQRPath = "", preferredQRURL = "", entryURL = "", mobileWalletURL = "" } = {}) {
352
+ const media = localQRPath || preferredQRURL
353
+ ? [compactObject({ type: "photo", local_path: localQRPath || undefined, fallback_url: preferredQRURL || undefined })]
354
+ : [];
355
+ const links = [];
356
+ if (entryURL) links.push({ label: action.kind === "auth_qr" ? "打开授权页面" : "打开付款页面", url: entryURL });
357
+ if (mobileWalletURL) links.push({ label: "手机钱包打开", url: mobileWalletURL });
358
+ const buttons = action.kind === "payment_qr"
359
+ ? [
360
+ { text: "支付遇到问题 / 刷新", intent: "refresh_payment_qr" },
361
+ { text: "我已付款,查询状态", intent: "check_payment_status" }
362
+ ]
363
+ : entryURL ? [{ text: "打开授权页面", url: entryURL }] : [];
364
+ return {
365
+ format: action.kind === "payment_qr" ? "photo_text_inline_buttons" : "text_inline_buttons",
366
+ media,
367
+ text: action.kind === "payment_qr"
368
+ ? "请扫码或点击链接完成支付。付款后点“我已付款,查询状态”。"
369
+ : "请打开 ItPay 授权入口,完成后回到当前对话。",
370
+ links,
371
+ buttons
372
+ };
373
+ }
374
+
375
+ function compactObject(value = {}) {
376
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null && item !== ""));
377
+ }
378
+
268
379
  function shouldPrepareLocalQRForJSON(mode, flags = {}, action = {}) {
269
380
  if (mode === "file" || flags.qr_file || process.env.ITP_QR_FILE) return true;
270
381
  if (action?.qr_png_url || action?.preferred_qr_url || action?.qr_image_url) return true;
@@ -496,4 +607,4 @@ function openBrowser(targetURL) {
496
607
  return false;
497
608
  }
498
609
 
499
- export { renderItPayPaymentAction, humanActionSummaryLines, writeHumanActionSummary, waitHeartbeatMs, writeWaitHeartbeat, renderHumanAction, preferredHumanActionQRURL, humanActionPresentationURL, annotateHumanActionPresentation, shouldPrepareLocalQRForJSON, shouldGenerateLocalQRFromActionURL, prepareLocalQRFile, prepareLocalQRFromActionURL, defaultQRFilePath, defaultGeneratedQRFilePath, qrFileExtension, qrMimeType, sanitizeFilename, formatActionTime, shouldUseAgentTextQR, shouldReturnAfterAgentTextQR, attachAgentQRImage, attachAgentLocalQR, downloadQRImage, persistHumanAction, shouldRenderTerminalQR, terminalQRType, shouldOpenBrowser, openBrowser };
610
+ export { renderItPayPaymentAction, humanActionSummaryLines, writeHumanActionSummary, waitHeartbeatMs, writeWaitHeartbeat, renderHumanAction, buildHumanActionRenderPlan, preferredHumanActionQRURL, humanActionPresentationURL, annotateHumanActionPresentation, shouldPrepareLocalQRForJSON, shouldGenerateLocalQRFromActionURL, prepareLocalQRFile, prepareLocalQRFromActionURL, defaultQRFilePath, defaultGeneratedQRFilePath, qrFileExtension, qrMimeType, sanitizeFilename, formatActionTime, shouldUseAgentTextQR, shouldReturnAfterAgentTextQR, attachAgentQRImage, attachAgentLocalQR, downloadQRImage, persistHumanAction, shouldRenderTerminalQR, terminalQRType, shouldOpenBrowser, openBrowser };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itpay/cli",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "ItPay CLI, buyer skill, and agent-readable docs for agent-native commerce.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -75,7 +75,7 @@ itp buyer cart add <cart_id> --variant <variant_id> --input key=value --quantity
75
75
  itp buyer cart remove <cart_id> --line <cart_line_item_id> --json
76
76
  itp buyer checkout create --cart <cart_id> --email <buyer_email> --phone <buyer_phone> --json
77
77
  itp buyer checkout resume <checkout_id> --json
78
- itp buyer payment wait <payment_intent_id> --json
78
+ itp buyer payment wait <payment_intent_id> --timeout 1 --json
79
79
  itp buyer checkout status <checkout_id> --json
80
80
  itp buyer refund create --order <order_id> --amount-minor <minor_units> --currency CNY --reason buyer_requested --json
81
81
  itp buyer refund list --order <order_id> --json
@@ -212,10 +212,14 @@ itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent
212
212
  This keeps JSON output machine-readable while allowing the CLI to prepare a
213
213
  local QR image path for clients that cannot render remote SVG reliably. In
214
214
  Codex or Claude Code app clients, prefer `--no-wait-payment`: send
215
- `human_visible_markdown` to the human first, then run
216
- `next.command`. If the human is on mobile, present
217
- `mobile_wallet_url` as a clickable human-only fallback; do not convert it into a
218
- QR.
215
+ `human_visible_markdown` or `render_plan.platforms.codex_app.markdown` to the
216
+ human first. If the human is on mobile, present `mobile_wallet_url` as a
217
+ clickable human-only fallback; do not convert it into a QR.
218
+
219
+ If a response has `status=payment_handoff_required`, `next` is the user-visible
220
+ reply step, not payment wait. Do not run `buyer payment wait` until the human
221
+ has seen the QR/link and asks to check status; then use
222
+ `after_human_response.check_payment_command`.
219
223
 
220
224
  For first-purchase auth, treat the returned ItPay authorization entry as a
221
225
  single human orchestration entry. It may open Alipay login/registration first