@itpay/cli 0.2.11 → 0.2.13

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/lib/buyer.js CHANGED
@@ -3,8 +3,9 @@ import {
3
3
  readSessionToken, readState, safeErrorMessage, sleep, splitCSV, stripInternalBuyerFields, updateRun, writeConfig,
4
4
  writeCredentials, writeSessionCredentials, writeState
5
5
  } from "./env.js";
6
+ import { clientCommandArgs, clientHost, clientTarget } from "./client-context.js";
6
7
  import { coreApi, coreApiBase } from "./http.js";
7
- import { buildHumanActionRenderPlan, humanOutputFromRenderPlan, renderHumanAction, renderItPayPaymentAction, shouldReturnAfterAgentTextQR, writeWaitHeartbeat } from "./render-human.js";
8
+ import { buildHumanActionRenderPlan, renderHumanAction, renderItPayPaymentAction, shouldReturnAfterAgentTextQR, writeWaitHeartbeat } from "./render-human.js";
8
9
 
9
10
  async function buyerBuy(flags) {
10
11
  rejectBuyerSandboxFlag(flags);
@@ -319,7 +320,8 @@ async function buyer(command, rest, flags) {
319
320
  })));
320
321
  return;
321
322
  }
322
- const intent = await getBuyerPaymentIntent(checkout.payment_intent_id, flags);
323
+ let intent = await getBuyerPaymentIntent(checkout.payment_intent_id, flags);
324
+ intent = await ensureTelegramPaymentDisplay(intent, flags);
323
325
  await renderItPayPaymentAction(intent, flags);
324
326
  output(buyerRunOutput({
325
327
  status: intent.status === "verified" ? "payment_verified" : "payment_handoff_required",
@@ -367,11 +369,14 @@ async function buyer(command, rest, flags) {
367
369
  if (command === "payment" && subcommand === "wait") {
368
370
  const paymentIntentID = flags.payment_intent || flags.payment_intent_id || positional(rest, 1) || readState().last_core_payment_intent_id;
369
371
  if (!paymentIntentID) throw new Error("payment_intent_id is required");
370
- const intent = await getBuyerPaymentIntent(paymentIntentID, flags);
372
+ let intent = await getBuyerPaymentIntent(paymentIntentID, flags);
371
373
  if (intent.status !== "verified" && shouldReturnPaymentHandoffBeforeWait(flags)) {
374
+ intent = await ensureTelegramPaymentDisplay(intent, flags);
372
375
  await renderItPayPaymentAction(intent, flags);
376
+ const context = await optionalBuyerPaymentContext(intent, flags);
373
377
  output(buyerRunOutput({
374
378
  status: "payment_handoff_required",
379
+ ...context,
375
380
  payment_intent: intent,
376
381
  ...paymentHandoffFields(intent, flags),
377
382
  payment_guidance: paymentRecoveryGuidance(intent, { event_type: "payment_display_required" }),
@@ -391,7 +396,7 @@ async function buyer(command, rest, flags) {
391
396
  agent_next_actions: event.agent_next_actions || intent.agent_next_actions || paymentAgentNextActions(intent, event),
392
397
  next: event.event_type === "payment_intent.verified"
393
398
  ? { command: intent.checkout_id ? cliCommand("buyer", "checkout", "status", intent.checkout_id, "--json") : undefined, safe_for_agent: true }
394
- : { command: paymentStatusCheckCommand(paymentIntentID), safe_for_agent: true }
399
+ : { command: paymentStatusCheckCommand(paymentIntentID, flags), safe_for_agent: true }
395
400
  }));
396
401
  return;
397
402
  }
@@ -400,8 +405,10 @@ async function buyer(command, rest, flags) {
400
405
  if (!paymentIntentID) throw new Error("payment_intent_id is required");
401
406
  const refreshed = await refreshBuyerPaymentQR(paymentIntentID, flags);
402
407
  await renderItPayPaymentAction(refreshed, flags);
408
+ const context = refreshed.status === "verified" ? {} : await optionalBuyerPaymentContext(refreshed, flags);
403
409
  output(buyerRunOutput({
404
410
  status: refreshed.status === "verified" ? "payment_verified" : "payment_handoff_required",
411
+ ...context,
405
412
  payment_intent: refreshed,
406
413
  ...(refreshed.status === "verified" ? {} : paymentHandoffFields(refreshed, flags)),
407
414
  payment_guidance: paymentRecoveryGuidance(refreshed, { event_type: refreshed.status === "verified" ? "payment_intent.verified" : "qr_refreshed" }),
@@ -702,8 +709,44 @@ async function createBuyerCartFromSelections(selections, flags = {}) {
702
709
  idempotencyKey: flags.cart_idempotency_key || `idem_cli_cart_${cryptoRandom()}`,
703
710
  body
704
711
  }, flags);
712
+ rememberBuyerCartDisplayContext(cart, selections, lineItems);
705
713
  writeState({ ...readState(), last_core_cart_id: cart.cart_id || cart.id });
706
- return cart;
714
+ return mergeBuyerCartDisplayContext(cart);
715
+ }
716
+
717
+ function rememberBuyerCartDisplayContext(cart = {}, selections = [], lineItems = []) {
718
+ const cartID = cart.cart_id || cart.id;
719
+ if (!cartID) return;
720
+ const state = readState();
721
+ const contexts = state.cart_display_contexts && typeof state.cart_display_contexts === "object" ? state.cart_display_contexts : {};
722
+ const entries = Object.entries({
723
+ ...contexts,
724
+ [cartID]: {
725
+ line_items: lineItems.map((line, index) => compactObject({
726
+ quantity: line.quantity,
727
+ input: line.input,
728
+ title: selections[index]?.title || selections[index]?.variant_title
729
+ }))
730
+ }
731
+ }).slice(-20);
732
+ writeState({ ...state, cart_display_contexts: Object.fromEntries(entries) });
733
+ }
734
+
735
+ function mergeBuyerCartDisplayContext(cart = {}) {
736
+ if (!cart || typeof cart !== "object") return cart;
737
+ const context = readState().cart_display_contexts?.[cart.cart_id || cart.id];
738
+ if (!context?.line_items?.length || !Array.isArray(cart.line_items)) return cart;
739
+ return {
740
+ ...cart,
741
+ line_items: cart.line_items.map((line, index) => ({
742
+ ...line,
743
+ input: line.input || context.line_items[index]?.input,
744
+ item: {
745
+ ...(line.item || {}),
746
+ title: line.item?.title || context.line_items[index]?.title
747
+ }
748
+ }))
749
+ };
707
750
  }
708
751
 
709
752
  function buyerCartSelectionIDs(rest, flags = {}) {
@@ -794,7 +837,7 @@ function splitInputParts(raw) {
794
837
 
795
838
  async function getBuyerCart(cartID, flags = {}) {
796
839
  if (!cartID) throw new Error("cart_id is required");
797
- return await coreApi(`/v1/carts/${encodeURIComponent(cartID)}`, { method: "GET" }, flags);
840
+ return mergeBuyerCartDisplayContext(await coreApi(`/v1/carts/${encodeURIComponent(cartID)}`, { method: "GET" }, flags));
798
841
  }
799
842
 
800
843
  async function addBuyerCartLineItem(cartID, selection, flags = {}) {
@@ -1051,49 +1094,14 @@ function paymentAgentNextActions(intent = {}, event = {}) {
1051
1094
 
1052
1095
  function paymentHandoffFields(intent = {}, flags = {}) {
1053
1096
  return {
1054
- payment_handoff: paymentHandoff(intent),
1055
1097
  render_plan: buildHumanActionRenderPlan(intent.human_action || {}, intent, flags),
1056
- after_human_response: paymentHandoffAfterHumanResponse(intent)
1098
+ after_human_response: paymentHandoffAfterHumanResponse(intent, flags)
1057
1099
  };
1058
1100
  }
1059
1101
 
1060
- function paymentHandoff(intent = {}) {
1061
- const action = intent.human_action || {};
1062
- const paymentIntentID = intent.payment_intent_id || action.id || "";
1063
- const entryURL = intent.payment_entry_url || intent.payment_url || action.url || "";
1064
- const qrPNGURL = action.qr_png_url || intent.qr_png_url || intent.qr?.png_url || "";
1065
- const preferredQRURL = action.preferred_qr_url || qrPNGURL || action.qr_image_url || intent.qr_image_url || intent.qr?.image_url || "";
1066
- const localQRPath = action.local_qr_path || intent.local_qr_path || "";
1067
- const mobileWalletURL = action.mobile_wallet_url || intent.mobile_wallet_url || "";
1068
- return {
1069
- type: "payment_qr_handoff",
1070
- payment_intent_id: paymentIntentID || null,
1071
- checkout_id: intent.checkout_id || null,
1072
- primary: localQRPath ? "local_qr_path" : (qrPNGURL ? "qr_png_url" : (preferredQRURL ? "preferred_qr_url" : "payment_entry_url")),
1073
- local_qr_path: localQRPath || null,
1074
- qr_png_url: qrPNGURL || null,
1075
- preferred_qr_url: preferredQRURL || null,
1076
- payment_entry_url: entryURL || null,
1077
- mobile_wallet_url: mobileWalletURL || null,
1078
- markdown: paymentHandoffMarkdown({ localQRPath, qrPNGURL, preferredQRURL, entryURL, mobileWalletURL }),
1079
- safe_for_agent: true,
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."
1081
- };
1082
- }
1083
-
1084
- function paymentHandoffMarkdown({ localQRPath = "", qrPNGURL = "", preferredQRURL = "", entryURL = "", mobileWalletURL = "" } = {}) {
1085
- const imageURL = localQRPath || qrPNGURL || preferredQRURL;
1086
- const lines = ["请扫码付款:"];
1087
- if (imageURL) lines.push(`![ItPay payment QR](${imageURL})`);
1088
- if (entryURL) lines.push(`[打开付款页面](${entryURL})`);
1089
- if (mobileWalletURL) lines.push(`[手机钱包打开](${mobileWalletURL})`);
1090
- lines.push("付款后回复“我已付款”,我会查询真实支付状态。");
1091
- return lines.join("\n\n");
1092
- }
1093
-
1094
- function paymentStatusCheckCommand(paymentIntentID) {
1102
+ function paymentStatusCheckCommand(paymentIntentID, flags = {}) {
1095
1103
  if (!paymentIntentID) return "";
1096
- return cliCommand("buyer", "payment", "wait", paymentIntentID, "--timeout", "1", "--json");
1104
+ return cliCommand("buyer", "payment", "wait", paymentIntentID, "--timeout", "1", ...clientCommandArgs(flags), "--json");
1097
1105
  }
1098
1106
 
1099
1107
  function shouldReturnPaymentHandoffBeforeWait(flags = {}) {
@@ -1103,23 +1111,66 @@ function shouldReturnPaymentHandoffBeforeWait(flags = {}) {
1103
1111
  return !Number.isFinite(timeout) || timeout > 5;
1104
1112
  }
1105
1113
 
1114
+ async function ensureTelegramPaymentDisplay(intent = {}, flags = {}) {
1115
+ if (!isTelegramBuyerHost(flags) || paymentDisplayAvailable(intent) || !intent.qr_refresh_url) return intent;
1116
+ try {
1117
+ return await refreshBuyerPaymentQR(intent.payment_intent_id, flags);
1118
+ } catch {
1119
+ return intent;
1120
+ }
1121
+ }
1122
+
1123
+ function paymentDisplayAvailable(intent = {}) {
1124
+ const action = intent.human_action || {};
1125
+ return Boolean(
1126
+ action.url ||
1127
+ action.local_qr_path ||
1128
+ action.qr_png_url ||
1129
+ action.qr_image_url ||
1130
+ action.preferred_qr_url ||
1131
+ action.mobile_wallet_url ||
1132
+ intent.payment_url ||
1133
+ intent.payment_entry_url ||
1134
+ intent.qr_png_url ||
1135
+ intent.qr_image_url ||
1136
+ intent.mobile_wallet_url
1137
+ );
1138
+ }
1139
+
1140
+ async function optionalBuyerPaymentContext(intent = {}, flags = {}) {
1141
+ if (!isTelegramBuyerHost(flags) || !intent.checkout_id) return {};
1142
+ try {
1143
+ const checkout = await getBuyerCheckout(intent.checkout_id, flags);
1144
+ return compactObject({
1145
+ checkout,
1146
+ cart: checkout.cart_id ? await getBuyerCart(checkout.cart_id, flags) : undefined
1147
+ });
1148
+ } catch {
1149
+ return {};
1150
+ }
1151
+ }
1152
+
1153
+ function isTelegramBuyerHost(flags = {}) {
1154
+ return clientHost(flags) === "telegram";
1155
+ }
1156
+
1106
1157
  function paymentHandoffAgentNextActions() {
1107
- return ["show_human_visible_markdown"];
1158
+ return ["send_agent_instruction_to_human"];
1108
1159
  }
1109
1160
 
1110
1161
  function paymentHandoffNext() {
1111
1162
  return {
1112
1163
  type: "reply_to_human",
1113
1164
  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."
1165
+ instruction: "Send agent_instruction to the human now. Do not run payment wait until this message is visible to the human."
1115
1166
  };
1116
1167
  }
1117
1168
 
1118
- function paymentHandoffAfterHumanResponse(intent = {}) {
1169
+ function paymentHandoffAfterHumanResponse(intent = {}, flags = {}) {
1119
1170
  const paymentIntentID = intent.payment_intent_id || intent.human_action?.id || "";
1120
1171
  return {
1121
- check_payment_command: paymentStatusCheckCommand(paymentIntentID),
1122
- refresh_qr_command: paymentIntentID ? cliCommand("buyer", "payment", "refresh-qr", paymentIntentID, "--reason", "order-not-found", "--json") : "",
1172
+ check_payment_command: paymentStatusCheckCommand(paymentIntentID, flags),
1173
+ refresh_qr_command: paymentIntentID ? cliCommand("buyer", "payment", "refresh-qr", paymentIntentID, "--reason", "order-not-found", ...clientCommandArgs(flags), "--json") : "",
1123
1174
  safe_for_agent: true,
1124
1175
  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
1176
  };
@@ -1491,8 +1542,8 @@ function isBuyerDeliveryComplete(result) {
1491
1542
  checkout.agent_next_actions?.includes("stop_check_email");
1492
1543
  }
1493
1544
 
1494
- function buyerRunOutput(value = {}) {
1495
- const body = compactBuyerOutput(value);
1545
+ function buyerRunOutput(value = {}, flags = {}) {
1546
+ const body = compactBuyerOutput(value, flags);
1496
1547
  return normalizeBuyerMoneyFields(stripInternalBuyerFields({
1497
1548
  schema_version: "itp.buyer.v1",
1498
1549
  ...body,
@@ -1505,19 +1556,11 @@ function buyerRunOutput(value = {}) {
1505
1556
  }));
1506
1557
  }
1507
1558
 
1508
- function compactBuyerOutput(value = {}) {
1559
+ function compactBuyerOutput(value = {}, flags = {}) {
1560
+ const clientInstruction = clientBuyerInstruction(value, flags);
1561
+ if (clientInstruction) return clientInstruction;
1509
1562
  const result = {};
1510
1563
  if (value.status !== undefined) result.status = value.status;
1511
- if (value.payment_handoff?.markdown) {
1512
- result.must_reply_to_human_before_next_command = true;
1513
- result.human_visible_markdown = value.payment_handoff.markdown;
1514
- }
1515
- const humanOutput = humanOutputFromRenderPlan(value.render_plan);
1516
- if (humanOutput) {
1517
- result.human_output_required = true;
1518
- result.must_send_human_output_before_next_command = true;
1519
- result.human_output = humanOutput;
1520
- }
1521
1564
  for (const [key, item] of Object.entries(value)) {
1522
1565
  if (["docs", "status"].includes(key)) continue;
1523
1566
  result[key] = compactBuyerField(key, item);
@@ -1525,6 +1568,222 @@ function compactBuyerOutput(value = {}) {
1525
1568
  return result;
1526
1569
  }
1527
1570
 
1571
+ function clientBuyerInstruction(value = {}, flags = {}) {
1572
+ const plan = value.render_plan || {};
1573
+ const selected = plan.selected || {};
1574
+ if (!plan.kind || !selected.platform) return null;
1575
+ if (selected.platform === "telegram" && selected.openclaw_message) return telegramBuyerInstruction(value, flags);
1576
+ if ((selected.platform === "codex_app" || selected.platform === "claude_code") && selected.markdown) {
1577
+ return markdownBuyerInstruction(value, flags, selected.platform, selected.markdown);
1578
+ }
1579
+ if (selected.platform === "plain_chat") return plainChatBuyerInstruction(value, flags, selected);
1580
+ if (selected.platform === "terminal") return terminalBuyerInstruction(value, flags);
1581
+ return null;
1582
+ }
1583
+
1584
+ function telegramBuyerInstruction(value = {}, flags = {}) {
1585
+ const plan = value.render_plan || {};
1586
+ const selected = plan.selected || {};
1587
+ if (selected.platform !== "telegram" || !selected.openclaw_message) return null;
1588
+ const summary = buyerOrderSummary(value, plan.kind);
1589
+ const rawMessage = selected.openclaw_message;
1590
+ const presentation = telegramBuyerPresentation(plan.kind, rawMessage);
1591
+ const media = rawMessage.command_args?.media || rawMessage.media || selected.media?.[0]?.local_path || selected.media?.[0]?.fallback_url || "";
1592
+ const hasHumanEntry = Boolean(media || presentation.blocks?.some((block) => block.type === "buttons" && block.buttons?.some((button) => button.url)));
1593
+ const text = telegramBuyerInstructionText(plan.kind, summary, hasHumanEntry);
1594
+ const callbacks = rawMessage.callbacks || {};
1595
+ return compactObject({
1596
+ status: value.status,
1597
+ must_send_to_human_before_next_command: true,
1598
+ order_summary: summary,
1599
+ agent_instruction: {
1600
+ type: plan.kind === "auth_qr" ? "send_telegram_auth_handoff" : "send_telegram_payment_handoff",
1601
+ platform: "telegram",
1602
+ proof_rule: plan.proof_rule,
1603
+ instruction: "必须使用 openclaw_message.command_args 发送给用户。不要用自然语言伪造按钮,不要先运行 wait/status。只有 payment_intent.verified 才算付款成功。",
1604
+ openclaw_message: {
1605
+ command: "openclaw message send",
1606
+ command_args: compactObject({
1607
+ channel: "telegram",
1608
+ target: clientTarget(flags) || undefined,
1609
+ message: text,
1610
+ media: media || undefined,
1611
+ presentation
1612
+ }),
1613
+ telegram_rule: "Telegram 原生按钮必须通过 openclaw message send --presentation 发送;presentation.blocks[].type=\"buttons\" 才会生成 Inline Keyboard。",
1614
+ callbacks
1615
+ }
1616
+ },
1617
+ after_human_response: telegramAfterHumanResponse(value, callbacks),
1618
+ agent_next_actions: ["send_agent_instruction_to_human"],
1619
+ next: {
1620
+ type: "reply_to_human",
1621
+ safe_for_agent: false,
1622
+ instruction: "先把 agent_instruction.openclaw_message.command_args 发送给用户;用户点击按钮或回复后,再运行 after_human_response 里的命令。"
1623
+ }
1624
+ });
1625
+ }
1626
+
1627
+ function markdownBuyerInstruction(value = {}, flags = {}, platform = "codex_app", markdown = "") {
1628
+ const plan = value.render_plan || {};
1629
+ const summary = buyerOrderSummary(value, plan.kind);
1630
+ return handoffInstructionOutput(value, flags, {
1631
+ type: plan.kind === "auth_qr" ? "send_markdown_auth_handoff" : "send_markdown_payment_handoff",
1632
+ platform,
1633
+ proof_rule: plan.proof_rule,
1634
+ instruction: "必须先把 markdown 原样发给用户;不要先运行 wait/status。只有 payment_intent.verified 才算付款成功。",
1635
+ markdown
1636
+ }, summary);
1637
+ }
1638
+
1639
+ function plainChatBuyerInstruction(value = {}, flags = {}, selected = {}) {
1640
+ const plan = value.render_plan || {};
1641
+ const summary = buyerOrderSummary(value, plan.kind);
1642
+ return handoffInstructionOutput(value, flags, {
1643
+ type: plan.kind === "auth_qr" ? "send_plain_auth_handoff" : "send_plain_payment_handoff",
1644
+ platform: "plain_chat",
1645
+ proof_rule: plan.proof_rule,
1646
+ instruction: "把 message 和 links 发给用户后停下,等用户回复再查状态。",
1647
+ message: selected.text,
1648
+ links: selected.links || []
1649
+ }, summary);
1650
+ }
1651
+
1652
+ function terminalBuyerInstruction(value = {}, flags = {}) {
1653
+ const plan = value.render_plan || {};
1654
+ const summary = buyerOrderSummary(value, plan.kind);
1655
+ return handoffInstructionOutput(value, flags, {
1656
+ type: plan.kind === "auth_qr" ? "terminal_auth_handoff" : "terminal_payment_handoff",
1657
+ platform: "terminal",
1658
+ proof_rule: plan.proof_rule,
1659
+ instruction: "CLI 已负责终端展示;用户回复后再运行 after_human_response 里的命令。",
1660
+ print_terminal_qr: true
1661
+ }, summary);
1662
+ }
1663
+
1664
+ function handoffInstructionOutput(value = {}, flags = {}, agentInstruction = {}, summary = {}) {
1665
+ return compactObject({
1666
+ status: value.status,
1667
+ must_send_to_human_before_next_command: true,
1668
+ order_summary: summary,
1669
+ agent_instruction: agentInstruction,
1670
+ after_human_response: genericAfterHumanResponse(value, flags, summary),
1671
+ agent_next_actions: ["send_agent_instruction_to_human"],
1672
+ next: {
1673
+ type: "reply_to_human",
1674
+ safe_for_agent: false,
1675
+ instruction: "先把 agent_instruction 发送给用户;用户点击按钮或回复后,再运行 after_human_response 里的命令。"
1676
+ }
1677
+ });
1678
+ }
1679
+
1680
+ function buyerOrderSummary(value = {}, kind = "") {
1681
+ const cart = value.cart || {};
1682
+ const checkout = value.checkout || {};
1683
+ const intent = value.payment_intent || {};
1684
+ const context = checkout.cart_id ? readState().cart_display_contexts?.[checkout.cart_id] : null;
1685
+ const line = cart.line_items?.[0] || context?.line_items?.[0] || {};
1686
+ const title = value.selection?.title || line.item?.title || line.title || "ItPay 服务";
1687
+ const quantity = line.quantity || 1;
1688
+ const amount = intent.amount ?? checkout.amount ?? cart.amount ?? value.selection?.expected_amount ?? value.selection?.amount;
1689
+ const currency = intent.currency || checkout.currency || cart.currency || value.selection?.currency || "";
1690
+ return compactObject({
1691
+ kind,
1692
+ title,
1693
+ quantity,
1694
+ input_summary: buyerInputSummary(line.input),
1695
+ amount_display: buyerHumanAmount(amount, currency),
1696
+ order_id: checkout.order_id || intent.order_id,
1697
+ checkout_id: checkout.checkout_id || intent.checkout_id,
1698
+ payment_intent_id: intent.payment_intent_id,
1699
+ status: intent.status || checkout.status || value.status
1700
+ });
1701
+ }
1702
+
1703
+ function buyerInputSummary(input = {}) {
1704
+ if (!input || typeof input !== "object") return "";
1705
+ return input.company_name || input.company_name_or_credit_no || Object.values(input).find((value) => value && value !== "1" && value !== "0") || "";
1706
+ }
1707
+
1708
+ function buyerHumanAmount(amount, currency = "") {
1709
+ if (!Number.isFinite(Number(amount))) return "";
1710
+ if (String(currency).toUpperCase() === "CNY") return `¥${(Number(amount) / 100).toFixed(2)}`;
1711
+ return `${String(currency).toUpperCase()} ${(Number(amount) / 100).toFixed(2)}`.trim();
1712
+ }
1713
+
1714
+ function telegramBuyerInstructionText(kind, summary = {}, hasHumanEntry = true) {
1715
+ const amount = summary.amount_display ? `,金额 ${summary.amount_display}` : "";
1716
+ if (!hasHumanEntry) {
1717
+ return [
1718
+ `购物车和结算已创建,但当前付款入口需要刷新${amount}。`,
1719
+ "请点“支付遇到问题 / 刷新”,或运行 after_human_response.refresh_qr_command 后重新发送付款入口。",
1720
+ "",
1721
+ `摘要:${telegramOrderSummaryText(summary)}`
1722
+ ].join("\n");
1723
+ }
1724
+ const action = kind === "auth_qr" ? "完成首次授权/付款" : "完成付款";
1725
+ const open = kind === "auth_qr" ? "打开授权" : "打开付款页面";
1726
+ const done = kind === "auth_qr" ? "我已完成,查询状态" : "我已付款,查询状态";
1727
+ return [
1728
+ `购物车和结算已创建。现在需要你${action}${amount}。`,
1729
+ `可扫二维码,或点“${open}”进入页面。`,
1730
+ `完成后点“${done}”,我会继续查支付和交付状态。`,
1731
+ "",
1732
+ `摘要:${telegramOrderSummaryText(summary)}`
1733
+ ].join("\n");
1734
+ }
1735
+
1736
+ function telegramOrderSummaryText(summary = {}) {
1737
+ const input = summary.input_summary ? `,关键词「${summary.input_summary}」` : "";
1738
+ const amount = summary.amount_display ? `,${summary.amount_display}` : "";
1739
+ return `${summary.title} × ${summary.quantity || 1} 次${input}${amount}。`;
1740
+ }
1741
+
1742
+ function telegramBuyerPresentation(kind, rawMessage = {}) {
1743
+ const rawBlocks = rawMessage.command_args?.presentation?.blocks || rawMessage.presentation?.blocks || [];
1744
+ const buttons = rawBlocks.find((block) => block.type === "buttons")?.buttons || [];
1745
+ return {
1746
+ blocks: [{
1747
+ type: "buttons",
1748
+ buttons: buttons.map((button) => telegramBuyerButton(kind, button)).filter(Boolean)
1749
+ }]
1750
+ };
1751
+ }
1752
+
1753
+ function telegramBuyerButton(kind, button = {}) {
1754
+ if (button.url) {
1755
+ if (kind === "auth_qr") return { text: "🔓 打开授权", url: button.url };
1756
+ if (button.text === "手机钱包打开") return { text: "📱 手机钱包打开", url: button.url };
1757
+ return { text: "💳 打开付款页面", url: button.url };
1758
+ }
1759
+ if (button.callback_data?.includes("refresh_payment_qr")) return { text: "🔄 支付遇到问题 / 刷新", callback_data: button.callback_data };
1760
+ if (button.callback_data?.includes("check_payment_status")) return { text: "✅ 我已付款,查询状态", callback_data: button.callback_data };
1761
+ if (button.callback_data?.includes("check_checkout_status")) return { text: "✅ 我已完成,查询状态", callback_data: button.callback_data };
1762
+ return button.text && button.callback_data ? { text: button.text, callback_data: button.callback_data } : null;
1763
+ }
1764
+
1765
+ function telegramAfterHumanResponse(value = {}, callbacks = {}) {
1766
+ return compactObject({
1767
+ check_status_command: callbacks.check_payment_status || callbacks.check_checkout_status || value.after_human_response?.check_payment_command,
1768
+ refresh_qr_command: callbacks.refresh_payment_qr || value.after_human_response?.refresh_qr_command,
1769
+ help_command: cliCommand("docs", "show", "payment-qr", "--role", "buyer", "--json"),
1770
+ instruction: "用户点击查询按钮或回复已完成后,运行 check_status_command。用户口头说已付款不是证明。"
1771
+ });
1772
+ }
1773
+
1774
+ function genericAfterHumanResponse(value = {}, flags = {}, summary = {}) {
1775
+ const paymentIntentID = summary.payment_intent_id;
1776
+ const checkoutID = summary.checkout_id;
1777
+ return compactObject({
1778
+ check_status_command: value.after_human_response?.check_payment_command ||
1779
+ (paymentIntentID ? paymentStatusCheckCommand(paymentIntentID, flags) : undefined) ||
1780
+ (checkoutID ? cliCommand("buyer", "checkout", "resume", checkoutID, ...clientCommandArgs(flags), "--json") : undefined),
1781
+ refresh_qr_command: value.after_human_response?.refresh_qr_command,
1782
+ help_command: cliCommand("docs", "show", "payment-qr", "--role", "buyer", "--json"),
1783
+ instruction: "用户点击查询按钮或回复已完成后,运行 check_status_command。用户口头说已付款不是证明。"
1784
+ });
1785
+ }
1786
+
1528
1787
  function compactBuyerField(key, value) {
1529
1788
  if (key === "selection") return compactSelection(value);
1530
1789
  if (key === "selections") return Array.isArray(value) ? value.map(compactSelection) : value;
@@ -1698,7 +1957,7 @@ function buyerDocsFor(value = {}) {
1698
1957
  topics.add("cart-checkout");
1699
1958
  topics.add("payment-qr");
1700
1959
  }
1701
- 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) {
1960
+ if (status.includes("payment_handoff") || status.includes("waiting_user_payment") || actions.includes("send_agent_instruction_to_human") || actions.includes("wait_payment") || value.payment_intent?.human_action || value.payment_intent?.qr_image_url) {
1702
1961
  topics.add("payment-qr");
1703
1962
  topics.add("payment-wait");
1704
1963
  }
@@ -0,0 +1,126 @@
1
+ const SUPPORTED_HOSTS = new Set(["codex", "claude-code", "telegram", "discord", "whatsapp", "terminal", "plain-chat"]);
2
+ const RUNTIME_TARGETS = new Set(["codex", "claude-code", "openclaw", "generic"]);
3
+ const EXEMPT_GROUPS = new Set(["", "--help", "-h", "help", "version", "docs", "skill", "doctor", "install"]);
4
+
5
+ function clientContextGate(args = []) {
6
+ const group = args[0] || "";
7
+ if (EXEMPT_GROUPS.has(group)) return null;
8
+ const flags = parseArgFlags(args.slice(1));
9
+ const host = clientHost(flags);
10
+ if (!host) return clientContextRequired();
11
+ if (!SUPPORTED_HOSTS.has(host)) return unsupportedClientContext(host);
12
+ if (requiresTarget(host) && !clientTarget(flags)) return clientTargetRequired(host);
13
+ return null;
14
+ }
15
+
16
+ function clientHost(flags = {}) {
17
+ const raw = firstValue(
18
+ flags.host,
19
+ flags.client,
20
+ flags.channel,
21
+ argFlag("host"),
22
+ argFlag("client"),
23
+ argFlag("channel")
24
+ );
25
+ const host = normalizeHost(raw);
26
+ if (host === "openclaw" && flags.channel) return normalizeHost(flags.channel);
27
+ return host;
28
+ }
29
+
30
+ function clientTarget(flags = {}) {
31
+ const target = firstValue(flags.chat_target, flags.reply_target, argFlag("chat-target"), argFlag("reply-target"));
32
+ if (target) return target;
33
+ const flagTarget = flags.target || argFlag("target");
34
+ return flagTarget && !RUNTIME_TARGETS.has(normalizeHost(flagTarget)) ? String(flagTarget) : "";
35
+ }
36
+
37
+ function clientCommandArgs(flags = {}) {
38
+ const host = clientHost(flags);
39
+ const target = clientTarget(flags);
40
+ return [
41
+ ...(host ? ["--host", host] : []),
42
+ ...(target ? ["--target", target] : [])
43
+ ];
44
+ }
45
+
46
+ function requiresTarget(host) {
47
+ return host === "telegram" || host === "discord" || host === "whatsapp";
48
+ }
49
+
50
+ function clientContextRequired() {
51
+ return {
52
+ schema_version: "itp.client_context.v1",
53
+ status: "client_context_required",
54
+ must_rerun: true,
55
+ instruction: "Rerun the same itp command with --host <client>. ItPay will not guess the current chat/app client.",
56
+ allowed_hosts: Array.from(SUPPORTED_HOSTS),
57
+ examples: {
58
+ codex: "itp ... --host codex --json",
59
+ claude_code: "itp ... --host claude-code --json",
60
+ telegram: "itp ... --host telegram --target telegram:<chat_id> --json",
61
+ terminal: "itp ... --host terminal --json"
62
+ }
63
+ };
64
+ }
65
+
66
+ function clientTargetRequired(host) {
67
+ return {
68
+ schema_version: "itp.client_context.v1",
69
+ status: "client_target_required",
70
+ must_rerun: true,
71
+ host,
72
+ instruction: "Rerun the same itp command with the current chat target. For OpenClaw Telegram group/private chat, pass inbound_meta.chat_id as --target.",
73
+ examples: {
74
+ telegram_private: "itp ... --host telegram --target telegram:5559456744 --json",
75
+ telegram_group: "itp ... --host telegram --target telegram:-1001234567890 --json",
76
+ discord: "itp ... --host discord --target discord:<channel_id> --json"
77
+ }
78
+ };
79
+ }
80
+
81
+ function unsupportedClientContext(host) {
82
+ return {
83
+ schema_version: "itp.client_context.v1",
84
+ status: "unsupported_client_context",
85
+ must_rerun: true,
86
+ host,
87
+ instruction: "Use one supported --host value so ItPay can return a single executable human-output instruction.",
88
+ allowed_hosts: Array.from(SUPPORTED_HOSTS)
89
+ };
90
+ }
91
+
92
+ function normalizeHost(value = "") {
93
+ const normalized = String(value || "").trim().toLowerCase().replaceAll("_", "-");
94
+ if (["tg", "openclaw-telegram"].includes(normalized)) return "telegram";
95
+ if (["codex-app", "codex-cli"].includes(normalized)) return "codex";
96
+ if (["claude", "claudecode", "claude-code-app"].includes(normalized)) return "claude-code";
97
+ if (["plain", "plain-chat", "chat"].includes(normalized)) return "plain-chat";
98
+ return normalized;
99
+ }
100
+
101
+ function parseArgFlags(args = []) {
102
+ const flags = {};
103
+ for (let i = 0; i < args.length; i += 1) {
104
+ const arg = args[i];
105
+ if (!String(arg).startsWith("--")) continue;
106
+ const key = String(arg).slice(2).replaceAll("-", "_");
107
+ const next = args[i + 1];
108
+ if (!next || String(next).startsWith("--")) {
109
+ flags[key] = true;
110
+ } else {
111
+ flags[key] = next;
112
+ i += 1;
113
+ }
114
+ }
115
+ return flags;
116
+ }
117
+
118
+ function argFlag(name) {
119
+ return parseArgFlags(process.argv.slice(2))[String(name).replaceAll("-", "_")] || "";
120
+ }
121
+
122
+ function firstValue(...values) {
123
+ return values.find((value) => value !== undefined && value !== null && value !== false && String(value).trim() !== "") || "";
124
+ }
125
+
126
+ export { clientCommandArgs, clientContextGate, clientHost, clientTarget };