@itpay/cli 0.2.10 → 0.2.12
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
|
@@ -239,9 +239,9 @@ 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
|
-
-
|
|
243
|
-
-
|
|
244
|
-
- If
|
|
242
|
+
- In OpenClaw Telegram, use root `agent_instruction.openclaw_message.command_args` with `openclaw message send`; do not simulate buttons in normal prose.
|
|
243
|
+
- Telegram native buttons require `presentation.blocks[].type="buttons"` with `text/url/callback_data`.
|
|
244
|
+
- If `human_output_required=true` appears for another client, send root `human_output` before any next ItPay command.
|
|
245
245
|
- If status is `payment_handoff_required`, `next` is the human reply step, not payment wait.
|
|
246
246
|
- Treat only `payment_intent.verified` as payment success.
|
|
247
247
|
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
{
|
|
27
27
|
"intent": "create payment intent through the high-level buy flow",
|
|
28
28
|
"command": "itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
|
|
29
|
-
"success_signal": "response.status is payment_handoff_required;
|
|
29
|
+
"success_signal": "response.status is payment_handoff_required; for OpenClaw Telegram, send response.agent_instruction.openclaw_message.command_args with openclaw message send before any status check"
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
"intent": "recover a scanner order-not-found display problem",
|
|
@@ -41,14 +41,12 @@
|
|
|
41
41
|
"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.",
|
|
42
42
|
"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.",
|
|
43
43
|
"Do not describe auth_qr as payment success or payment proof. Only payment_intent.verified proves payment.",
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"If
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"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.",
|
|
51
|
-
"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.",
|
|
44
|
+
"In OpenClaw Telegram, if response.agent_instruction is present, it is the mandatory execution path. Use response.agent_instruction.openclaw_message.command_args with openclaw message send before any next ItPay command.",
|
|
45
|
+
"Do not rewrite response.agent_instruction.openclaw_message into a custom table. Use its message, media, and presentation as provided.",
|
|
46
|
+
"Telegram native buttons require openclaw message send --presentation; presentation.blocks[].type=\"buttons\" uses native text/url/callback_data. Do not simulate buttons in normal prose.",
|
|
47
|
+
"If agent_instruction or human_output links/buttons include URLs, include the real URL. Do not only show the link label.",
|
|
48
|
+
"In Codex or Claude Code app clients, send response.human_output.markdown first. Do not start a hidden long wait before the human-visible message is sent.",
|
|
49
|
+
"For Telegram-style message clients, send photo media first, include links, and expose native buttons for refresh/status when the adapter supports them.",
|
|
52
50
|
"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.",
|
|
53
51
|
"When response.status is payment_handoff_required, response.next is the human reply step, not payment wait.",
|
|
54
52
|
"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.",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
}
|
|
36
36
|
],
|
|
37
37
|
"agent_rules": [
|
|
38
|
-
"If payment wait returns payment_handoff_required
|
|
38
|
+
"If payment wait returns payment_handoff_required with response.agent_instruction, send response.agent_instruction.openclaw_message.command_args before running any later status check.",
|
|
39
39
|
"Start payment status checks only after the QR image/link has already been sent to the human or the human asks to check status.",
|
|
40
|
-
"If you run payment wait without a short timeout while payment is still pending, CLI returns payment_handoff_required and
|
|
40
|
+
"If you run payment wait without a short timeout while payment is still pending, CLI returns payment_handoff_required and the human handoff instruction instead of long-polling.",
|
|
41
41
|
"Payment wait/status check is a recovery loop around the same payment_intent_id, not a one-shot command.",
|
|
42
42
|
"wait.timeout is a heartbeat, not a payment failure.",
|
|
43
43
|
"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.",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
{
|
|
46
46
|
"intent": "buy a selected variant with the high-level cart-first flow",
|
|
47
47
|
"command": "itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
|
|
48
|
-
"success_signal": "response.status becomes waiting_human_auth or payment_handoff_required;
|
|
48
|
+
"success_signal": "response.status becomes waiting_human_auth or payment_handoff_required; in OpenClaw Telegram, if agent_instruction is present, send response.agent_instruction.openclaw_message.command_args before any payment status check"
|
|
49
49
|
}
|
|
50
50
|
],
|
|
51
51
|
"agent_rules": [
|
|
@@ -62,13 +62,13 @@
|
|
|
62
62
|
"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.",
|
|
63
63
|
"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.",
|
|
64
64
|
"Vault grants are only for reading approved delivered artifacts. They are not buyer session credentials and do not authorize refunds or account/order management.",
|
|
65
|
-
"Use high-level itp buy when possible, but in agent app clients split payment into two visible steps: first return and
|
|
66
|
-
"
|
|
65
|
+
"Use high-level itp buy when possible, but in agent app clients split payment into two visible steps: first return and send the human handoff instruction, then only check status after the human responds or uses a platform button.",
|
|
66
|
+
"In OpenClaw Telegram, if response.agent_instruction is present, call openclaw message send with response.agent_instruction.openclaw_message.command_args. It includes the QR media, exact message, and native button presentation.",
|
|
67
67
|
"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.",
|
|
68
68
|
"Show the returned payment QR exactly as provided before starting payment wait.",
|
|
69
69
|
"Payment truth comes only from payment_intent.verified.",
|
|
70
70
|
"Secure delivery goes to the human first; report redacted delivery status only.",
|
|
71
|
-
"
|
|
71
|
+
"For OpenClaw Telegram, root agent_instruction is the execution path. Do not use handwritten prose when agent_instruction.openclaw_message.command_args is present."
|
|
72
72
|
],
|
|
73
73
|
"forbidden": [
|
|
74
74
|
"Do not call ops commands from the buyer path.",
|
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 { buildHumanActionRenderPlan, renderHumanAction, renderItPayPaymentAction, shouldReturnAfterAgentTextQR, writeWaitHeartbeat } from "./render-human.js";
|
|
7
|
+
import { buildHumanActionRenderPlan, humanOutputFromRenderPlan, renderHumanAction, renderItPayPaymentAction, shouldReturnAfterAgentTextQR, writeWaitHeartbeat } from "./render-human.js";
|
|
8
8
|
|
|
9
9
|
async function buyerBuy(flags) {
|
|
10
10
|
rejectBuyerSandboxFlag(flags);
|
|
@@ -319,7 +319,8 @@ async function buyer(command, rest, flags) {
|
|
|
319
319
|
})));
|
|
320
320
|
return;
|
|
321
321
|
}
|
|
322
|
-
|
|
322
|
+
let intent = await getBuyerPaymentIntent(checkout.payment_intent_id, flags);
|
|
323
|
+
intent = await ensureTelegramPaymentDisplay(intent, flags);
|
|
323
324
|
await renderItPayPaymentAction(intent, flags);
|
|
324
325
|
output(buyerRunOutput({
|
|
325
326
|
status: intent.status === "verified" ? "payment_verified" : "payment_handoff_required",
|
|
@@ -367,11 +368,14 @@ async function buyer(command, rest, flags) {
|
|
|
367
368
|
if (command === "payment" && subcommand === "wait") {
|
|
368
369
|
const paymentIntentID = flags.payment_intent || flags.payment_intent_id || positional(rest, 1) || readState().last_core_payment_intent_id;
|
|
369
370
|
if (!paymentIntentID) throw new Error("payment_intent_id is required");
|
|
370
|
-
|
|
371
|
+
let intent = await getBuyerPaymentIntent(paymentIntentID, flags);
|
|
371
372
|
if (intent.status !== "verified" && shouldReturnPaymentHandoffBeforeWait(flags)) {
|
|
373
|
+
intent = await ensureTelegramPaymentDisplay(intent, flags);
|
|
372
374
|
await renderItPayPaymentAction(intent, flags);
|
|
375
|
+
const context = await optionalBuyerPaymentContext(intent, flags);
|
|
373
376
|
output(buyerRunOutput({
|
|
374
377
|
status: "payment_handoff_required",
|
|
378
|
+
...context,
|
|
375
379
|
payment_intent: intent,
|
|
376
380
|
...paymentHandoffFields(intent, flags),
|
|
377
381
|
payment_guidance: paymentRecoveryGuidance(intent, { event_type: "payment_display_required" }),
|
|
@@ -400,8 +404,10 @@ async function buyer(command, rest, flags) {
|
|
|
400
404
|
if (!paymentIntentID) throw new Error("payment_intent_id is required");
|
|
401
405
|
const refreshed = await refreshBuyerPaymentQR(paymentIntentID, flags);
|
|
402
406
|
await renderItPayPaymentAction(refreshed, flags);
|
|
407
|
+
const context = refreshed.status === "verified" ? {} : await optionalBuyerPaymentContext(refreshed, flags);
|
|
403
408
|
output(buyerRunOutput({
|
|
404
409
|
status: refreshed.status === "verified" ? "payment_verified" : "payment_handoff_required",
|
|
410
|
+
...context,
|
|
405
411
|
payment_intent: refreshed,
|
|
406
412
|
...(refreshed.status === "verified" ? {} : paymentHandoffFields(refreshed, flags)),
|
|
407
413
|
payment_guidance: paymentRecoveryGuidance(refreshed, { event_type: refreshed.status === "verified" ? "payment_intent.verified" : "qr_refreshed" }),
|
|
@@ -702,8 +708,44 @@ async function createBuyerCartFromSelections(selections, flags = {}) {
|
|
|
702
708
|
idempotencyKey: flags.cart_idempotency_key || `idem_cli_cart_${cryptoRandom()}`,
|
|
703
709
|
body
|
|
704
710
|
}, flags);
|
|
711
|
+
rememberBuyerCartDisplayContext(cart, selections, lineItems);
|
|
705
712
|
writeState({ ...readState(), last_core_cart_id: cart.cart_id || cart.id });
|
|
706
|
-
return cart;
|
|
713
|
+
return mergeBuyerCartDisplayContext(cart);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function rememberBuyerCartDisplayContext(cart = {}, selections = [], lineItems = []) {
|
|
717
|
+
const cartID = cart.cart_id || cart.id;
|
|
718
|
+
if (!cartID) return;
|
|
719
|
+
const state = readState();
|
|
720
|
+
const contexts = state.cart_display_contexts && typeof state.cart_display_contexts === "object" ? state.cart_display_contexts : {};
|
|
721
|
+
const entries = Object.entries({
|
|
722
|
+
...contexts,
|
|
723
|
+
[cartID]: {
|
|
724
|
+
line_items: lineItems.map((line, index) => compactObject({
|
|
725
|
+
quantity: line.quantity,
|
|
726
|
+
input: line.input,
|
|
727
|
+
title: selections[index]?.title || selections[index]?.variant_title
|
|
728
|
+
}))
|
|
729
|
+
}
|
|
730
|
+
}).slice(-20);
|
|
731
|
+
writeState({ ...state, cart_display_contexts: Object.fromEntries(entries) });
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function mergeBuyerCartDisplayContext(cart = {}) {
|
|
735
|
+
if (!cart || typeof cart !== "object") return cart;
|
|
736
|
+
const context = readState().cart_display_contexts?.[cart.cart_id || cart.id];
|
|
737
|
+
if (!context?.line_items?.length || !Array.isArray(cart.line_items)) return cart;
|
|
738
|
+
return {
|
|
739
|
+
...cart,
|
|
740
|
+
line_items: cart.line_items.map((line, index) => ({
|
|
741
|
+
...line,
|
|
742
|
+
input: line.input || context.line_items[index]?.input,
|
|
743
|
+
item: {
|
|
744
|
+
...(line.item || {}),
|
|
745
|
+
title: line.item?.title || context.line_items[index]?.title
|
|
746
|
+
}
|
|
747
|
+
}))
|
|
748
|
+
};
|
|
707
749
|
}
|
|
708
750
|
|
|
709
751
|
function buyerCartSelectionIDs(rest, flags = {}) {
|
|
@@ -794,7 +836,7 @@ function splitInputParts(raw) {
|
|
|
794
836
|
|
|
795
837
|
async function getBuyerCart(cartID, flags = {}) {
|
|
796
838
|
if (!cartID) throw new Error("cart_id is required");
|
|
797
|
-
return await coreApi(`/v1/carts/${encodeURIComponent(cartID)}`, { method: "GET" }, flags);
|
|
839
|
+
return mergeBuyerCartDisplayContext(await coreApi(`/v1/carts/${encodeURIComponent(cartID)}`, { method: "GET" }, flags));
|
|
798
840
|
}
|
|
799
841
|
|
|
800
842
|
async function addBuyerCartLineItem(cartID, selection, flags = {}) {
|
|
@@ -1103,6 +1145,59 @@ function shouldReturnPaymentHandoffBeforeWait(flags = {}) {
|
|
|
1103
1145
|
return !Number.isFinite(timeout) || timeout > 5;
|
|
1104
1146
|
}
|
|
1105
1147
|
|
|
1148
|
+
async function ensureTelegramPaymentDisplay(intent = {}, flags = {}) {
|
|
1149
|
+
if (!isTelegramBuyerHost(flags) || paymentDisplayAvailable(intent) || !intent.qr_refresh_url) return intent;
|
|
1150
|
+
try {
|
|
1151
|
+
return await refreshBuyerPaymentQR(intent.payment_intent_id, flags);
|
|
1152
|
+
} catch {
|
|
1153
|
+
return intent;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function paymentDisplayAvailable(intent = {}) {
|
|
1158
|
+
const action = intent.human_action || {};
|
|
1159
|
+
return Boolean(
|
|
1160
|
+
action.url ||
|
|
1161
|
+
action.local_qr_path ||
|
|
1162
|
+
action.qr_png_url ||
|
|
1163
|
+
action.qr_image_url ||
|
|
1164
|
+
action.preferred_qr_url ||
|
|
1165
|
+
action.mobile_wallet_url ||
|
|
1166
|
+
intent.payment_url ||
|
|
1167
|
+
intent.payment_entry_url ||
|
|
1168
|
+
intent.qr_png_url ||
|
|
1169
|
+
intent.qr_image_url ||
|
|
1170
|
+
intent.mobile_wallet_url
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
async function optionalBuyerPaymentContext(intent = {}, flags = {}) {
|
|
1175
|
+
if (!isTelegramBuyerHost(flags) || !intent.checkout_id) return {};
|
|
1176
|
+
try {
|
|
1177
|
+
const checkout = await getBuyerCheckout(intent.checkout_id, flags);
|
|
1178
|
+
return compactObject({
|
|
1179
|
+
checkout,
|
|
1180
|
+
cart: checkout.cart_id ? await getBuyerCart(checkout.cart_id, flags) : undefined
|
|
1181
|
+
});
|
|
1182
|
+
} catch {
|
|
1183
|
+
return {};
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function isTelegramBuyerHost(flags = {}) {
|
|
1188
|
+
const raw = String(
|
|
1189
|
+
process.env.ITP_HOST ||
|
|
1190
|
+
flags.host ||
|
|
1191
|
+
flags.channel ||
|
|
1192
|
+
process.env.OPENCLAW_CURRENT_CHANNEL_PROVIDER ||
|
|
1193
|
+
process.env.OPENCLAW_CHANNEL ||
|
|
1194
|
+
process.env.CURRENT_CHANNEL_PROVIDER ||
|
|
1195
|
+
process.env.AGENT_CHANNEL ||
|
|
1196
|
+
""
|
|
1197
|
+
).toLowerCase().replaceAll("-", "_");
|
|
1198
|
+
return raw === "telegram" || raw === "tg";
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1106
1201
|
function paymentHandoffAgentNextActions() {
|
|
1107
1202
|
return ["show_human_visible_markdown"];
|
|
1108
1203
|
}
|
|
@@ -1506,12 +1601,20 @@ function buyerRunOutput(value = {}) {
|
|
|
1506
1601
|
}
|
|
1507
1602
|
|
|
1508
1603
|
function compactBuyerOutput(value = {}) {
|
|
1604
|
+
const telegramInstruction = telegramBuyerInstruction(value);
|
|
1605
|
+
if (telegramInstruction) return telegramInstruction;
|
|
1509
1606
|
const result = {};
|
|
1510
1607
|
if (value.status !== undefined) result.status = value.status;
|
|
1511
1608
|
if (value.payment_handoff?.markdown) {
|
|
1512
1609
|
result.must_reply_to_human_before_next_command = true;
|
|
1513
1610
|
result.human_visible_markdown = value.payment_handoff.markdown;
|
|
1514
1611
|
}
|
|
1612
|
+
const humanOutput = humanOutputFromRenderPlan(value.render_plan);
|
|
1613
|
+
if (humanOutput) {
|
|
1614
|
+
result.human_output_required = true;
|
|
1615
|
+
result.must_send_human_output_before_next_command = true;
|
|
1616
|
+
result.human_output = humanOutput;
|
|
1617
|
+
}
|
|
1515
1618
|
for (const [key, item] of Object.entries(value)) {
|
|
1516
1619
|
if (["docs", "status"].includes(key)) continue;
|
|
1517
1620
|
result[key] = compactBuyerField(key, item);
|
|
@@ -1519,6 +1622,142 @@ function compactBuyerOutput(value = {}) {
|
|
|
1519
1622
|
return result;
|
|
1520
1623
|
}
|
|
1521
1624
|
|
|
1625
|
+
function telegramBuyerInstruction(value = {}) {
|
|
1626
|
+
const plan = value.render_plan || {};
|
|
1627
|
+
const selected = plan.selected || {};
|
|
1628
|
+
if (selected.platform !== "telegram" || !selected.openclaw_message) return null;
|
|
1629
|
+
const summary = buyerOrderSummary(value, plan.kind);
|
|
1630
|
+
const rawMessage = selected.openclaw_message;
|
|
1631
|
+
const presentation = telegramBuyerPresentation(plan.kind, rawMessage);
|
|
1632
|
+
const media = rawMessage.command_args?.media || rawMessage.media || selected.media?.[0]?.local_path || selected.media?.[0]?.fallback_url || "";
|
|
1633
|
+
const hasHumanEntry = Boolean(media || presentation.blocks?.some((block) => block.type === "buttons" && block.buttons?.some((button) => button.url)));
|
|
1634
|
+
const text = telegramBuyerInstructionText(plan.kind, summary, hasHumanEntry);
|
|
1635
|
+
const callbacks = rawMessage.callbacks || {};
|
|
1636
|
+
return compactObject({
|
|
1637
|
+
status: value.status,
|
|
1638
|
+
must_send_to_human_before_next_command: true,
|
|
1639
|
+
order_summary: summary,
|
|
1640
|
+
agent_instruction: {
|
|
1641
|
+
type: plan.kind === "auth_qr" ? "send_telegram_auth_handoff" : "send_telegram_payment_handoff",
|
|
1642
|
+
platform: "telegram",
|
|
1643
|
+
proof_rule: plan.proof_rule,
|
|
1644
|
+
instruction: "必须使用 openclaw_message.command_args 发送给用户。不要用自然语言伪造按钮,不要先运行 wait/status。只有 payment_intent.verified 才算付款成功。",
|
|
1645
|
+
openclaw_message: {
|
|
1646
|
+
command: "openclaw message send",
|
|
1647
|
+
command_args: compactObject({
|
|
1648
|
+
channel: "telegram",
|
|
1649
|
+
message: text,
|
|
1650
|
+
media: media || undefined,
|
|
1651
|
+
presentation
|
|
1652
|
+
}),
|
|
1653
|
+
telegram_rule: "Telegram 原生按钮必须通过 openclaw message send --presentation 发送;presentation.blocks[].type=\"buttons\" 才会生成 Inline Keyboard。",
|
|
1654
|
+
callbacks
|
|
1655
|
+
}
|
|
1656
|
+
},
|
|
1657
|
+
after_human_response: telegramAfterHumanResponse(value, callbacks),
|
|
1658
|
+
agent_next_actions: ["send_agent_instruction_to_human"],
|
|
1659
|
+
next: {
|
|
1660
|
+
type: "reply_to_human",
|
|
1661
|
+
safe_for_agent: false,
|
|
1662
|
+
instruction: "先把 agent_instruction.openclaw_message.command_args 发送给用户;用户点击按钮或回复后,再运行 after_human_response 里的命令。"
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function buyerOrderSummary(value = {}, kind = "") {
|
|
1668
|
+
const cart = value.cart || {};
|
|
1669
|
+
const checkout = value.checkout || {};
|
|
1670
|
+
const intent = value.payment_intent || {};
|
|
1671
|
+
const context = checkout.cart_id ? readState().cart_display_contexts?.[checkout.cart_id] : null;
|
|
1672
|
+
const line = cart.line_items?.[0] || context?.line_items?.[0] || {};
|
|
1673
|
+
const title = value.selection?.title || line.item?.title || line.title || "ItPay 服务";
|
|
1674
|
+
const quantity = line.quantity || 1;
|
|
1675
|
+
const amount = intent.amount ?? checkout.amount ?? cart.amount ?? value.selection?.expected_amount ?? value.selection?.amount;
|
|
1676
|
+
const currency = intent.currency || checkout.currency || cart.currency || value.selection?.currency || "";
|
|
1677
|
+
return compactObject({
|
|
1678
|
+
kind,
|
|
1679
|
+
title,
|
|
1680
|
+
quantity,
|
|
1681
|
+
input_summary: buyerInputSummary(line.input),
|
|
1682
|
+
amount_display: buyerHumanAmount(amount, currency),
|
|
1683
|
+
order_id: checkout.order_id || intent.order_id,
|
|
1684
|
+
checkout_id: checkout.checkout_id || intent.checkout_id,
|
|
1685
|
+
payment_intent_id: intent.payment_intent_id,
|
|
1686
|
+
status: intent.status || checkout.status || value.status
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
function buyerInputSummary(input = {}) {
|
|
1691
|
+
if (!input || typeof input !== "object") return "";
|
|
1692
|
+
return input.company_name || input.company_name_or_credit_no || Object.values(input).find((value) => value && value !== "1" && value !== "0") || "";
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function buyerHumanAmount(amount, currency = "") {
|
|
1696
|
+
if (!Number.isFinite(Number(amount))) return "";
|
|
1697
|
+
if (String(currency).toUpperCase() === "CNY") return `¥${(Number(amount) / 100).toFixed(2)}`;
|
|
1698
|
+
return `${String(currency).toUpperCase()} ${(Number(amount) / 100).toFixed(2)}`.trim();
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
function telegramBuyerInstructionText(kind, summary = {}, hasHumanEntry = true) {
|
|
1702
|
+
const amount = summary.amount_display ? `,金额 ${summary.amount_display}` : "";
|
|
1703
|
+
if (!hasHumanEntry) {
|
|
1704
|
+
return [
|
|
1705
|
+
`购物车和结算已创建,但当前付款入口需要刷新${amount}。`,
|
|
1706
|
+
"请点“支付遇到问题 / 刷新”,或运行 after_human_response.refresh_qr_command 后重新发送付款入口。",
|
|
1707
|
+
"",
|
|
1708
|
+
`摘要:${telegramOrderSummaryText(summary)}`
|
|
1709
|
+
].join("\n");
|
|
1710
|
+
}
|
|
1711
|
+
const action = kind === "auth_qr" ? "完成首次授权/付款" : "完成付款";
|
|
1712
|
+
const open = kind === "auth_qr" ? "打开授权" : "打开付款页面";
|
|
1713
|
+
const done = kind === "auth_qr" ? "我已完成,查询状态" : "我已付款,查询状态";
|
|
1714
|
+
return [
|
|
1715
|
+
`购物车和结算已创建。现在需要你${action}${amount}。`,
|
|
1716
|
+
`可扫二维码,或点“${open}”进入页面。`,
|
|
1717
|
+
`完成后点“${done}”,我会继续查支付和交付状态。`,
|
|
1718
|
+
"",
|
|
1719
|
+
`摘要:${telegramOrderSummaryText(summary)}`
|
|
1720
|
+
].join("\n");
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
function telegramOrderSummaryText(summary = {}) {
|
|
1724
|
+
const input = summary.input_summary ? `,关键词「${summary.input_summary}」` : "";
|
|
1725
|
+
const amount = summary.amount_display ? `,${summary.amount_display}` : "";
|
|
1726
|
+
return `${summary.title} × ${summary.quantity || 1} 次${input}${amount}。`;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function telegramBuyerPresentation(kind, rawMessage = {}) {
|
|
1730
|
+
const rawBlocks = rawMessage.command_args?.presentation?.blocks || rawMessage.presentation?.blocks || [];
|
|
1731
|
+
const buttons = rawBlocks.find((block) => block.type === "buttons")?.buttons || [];
|
|
1732
|
+
return {
|
|
1733
|
+
blocks: [{
|
|
1734
|
+
type: "buttons",
|
|
1735
|
+
buttons: buttons.map((button) => telegramBuyerButton(kind, button)).filter(Boolean)
|
|
1736
|
+
}]
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function telegramBuyerButton(kind, button = {}) {
|
|
1741
|
+
if (button.url) {
|
|
1742
|
+
if (kind === "auth_qr") return { text: "🔓 打开授权", url: button.url };
|
|
1743
|
+
if (button.text === "手机钱包打开") return { text: "📱 手机钱包打开", url: button.url };
|
|
1744
|
+
return { text: "💳 打开付款页面", url: button.url };
|
|
1745
|
+
}
|
|
1746
|
+
if (button.callback_data?.includes("refresh_payment_qr")) return { text: "🔄 支付遇到问题 / 刷新", callback_data: button.callback_data };
|
|
1747
|
+
if (button.callback_data?.includes("check_payment_status")) return { text: "✅ 我已付款,查询状态", callback_data: button.callback_data };
|
|
1748
|
+
if (button.callback_data?.includes("check_checkout_status")) return { text: "✅ 我已完成,查询状态", callback_data: button.callback_data };
|
|
1749
|
+
return button.text && button.callback_data ? { text: button.text, callback_data: button.callback_data } : null;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
function telegramAfterHumanResponse(value = {}, callbacks = {}) {
|
|
1753
|
+
return compactObject({
|
|
1754
|
+
check_status_command: callbacks.check_payment_status || callbacks.check_checkout_status || value.after_human_response?.check_payment_command,
|
|
1755
|
+
refresh_qr_command: callbacks.refresh_payment_qr || value.after_human_response?.refresh_qr_command,
|
|
1756
|
+
help_command: cliCommand("docs", "show", "payment-qr", "--role", "buyer", "--json"),
|
|
1757
|
+
instruction: "用户点击查询按钮或回复已完成后,运行 check_status_command。用户口头说已付款不是证明。"
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1522
1761
|
function compactBuyerField(key, value) {
|
|
1523
1762
|
if (key === "selection") return compactSelection(value);
|
|
1524
1763
|
if (key === "selections") return Array.isArray(value) ? value.map(compactSelection) : value;
|
package/lib/render-human.js
CHANGED
|
@@ -286,8 +286,9 @@ function annotateHumanActionPresentation(action, qrImageURL) {
|
|
|
286
286
|
function buildHumanActionRenderPlan(action = {}, intent = {}, flags = {}) {
|
|
287
287
|
if (!action || typeof action !== "object") return null;
|
|
288
288
|
const kind = action.kind || (isPaymentIntentHandoff(intent) ? "payment_qr" : "human_action");
|
|
289
|
-
const planAction = action.kind === kind ? action : { ...action, kind };
|
|
290
289
|
const entryURL = intent.payment_entry_url || intent.payment_url || action.url || action.web_url || "";
|
|
290
|
+
const checkoutID = intent.checkout_id || action.checkout_id || checkoutIDFromURL(entryURL);
|
|
291
|
+
const planAction = compactObject({ ...(action.kind === kind ? action : { ...action, kind }), checkout_id: checkoutID || undefined });
|
|
291
292
|
const qrPNGURL = action.qr_png_url || intent.qr_png_url || intent.qr?.png_url || "";
|
|
292
293
|
const preferredQRURL = action.preferred_qr_url || qrPNGURL || action.qr_image_url || intent.qr_image_url || intent.qr?.image_url || "";
|
|
293
294
|
const localQRPath = action.local_qr_path || intent.local_qr_path || "";
|
|
@@ -371,7 +372,10 @@ function telegramRenderPlan(action = {}, { localQRPath = "", preferredQRURL = ""
|
|
|
371
372
|
{ text: "支付遇到问题 / 刷新", intent: "refresh_payment_qr" },
|
|
372
373
|
{ text: "我已付款,查询状态", intent: "check_payment_status" }
|
|
373
374
|
]
|
|
374
|
-
:
|
|
375
|
+
: [
|
|
376
|
+
...(entryURL ? [{ text: "打开授权页面", url: entryURL }] : []),
|
|
377
|
+
...(action.checkout_id ? [{ text: "我已完成,查询状态", intent: "check_checkout_status" }] : [])
|
|
378
|
+
];
|
|
375
379
|
return {
|
|
376
380
|
format: action.kind === "payment_qr" ? "photo_text_inline_buttons" : "text_inline_buttons",
|
|
377
381
|
media,
|
|
@@ -400,6 +404,9 @@ function openclawTelegramMessage({ action = {}, media = [], links = [], buttons
|
|
|
400
404
|
if (button.url && !presentationButtons.some((item) => item.url === button.url)) {
|
|
401
405
|
presentationButtons.push(openclawURLButton(button.text || "打开链接", button.url));
|
|
402
406
|
}
|
|
407
|
+
if (button.intent === "check_checkout_status") {
|
|
408
|
+
presentationButtons.push(openclawCallbackButton("我已完成,查询状态", "check_checkout_status", action));
|
|
409
|
+
}
|
|
403
410
|
}
|
|
404
411
|
}
|
|
405
412
|
const presentation = {
|
|
@@ -420,10 +427,56 @@ function openclawTelegramMessage({ action = {}, media = [], links = [], buttons
|
|
|
420
427
|
media: mediaURL || undefined,
|
|
421
428
|
presentation
|
|
422
429
|
}),
|
|
423
|
-
callbacks: action
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
430
|
+
callbacks: openclawCallbackCommands(action)
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function humanOutputFromRenderPlan(plan = {}) {
|
|
435
|
+
if (!plan || typeof plan !== "object" || !plan.kind || !plan.selected) return null;
|
|
436
|
+
const selected = plan.selected || {};
|
|
437
|
+
const platforms = plan.platforms || {};
|
|
438
|
+
const base = {
|
|
439
|
+
required: true,
|
|
440
|
+
kind: plan.kind,
|
|
441
|
+
proof_rule: plan.proof_rule,
|
|
442
|
+
instruction: "Send this human_output to the human before running the next ItPay command."
|
|
443
|
+
};
|
|
444
|
+
if (selected.platform === "telegram") return { ...base, ...openclawHumanOutput(selected.openclaw_message) };
|
|
445
|
+
if (selected.platform === "codex_app" || selected.platform === "claude_code") return { ...base, method: "markdown_reply", markdown: selected.markdown };
|
|
446
|
+
if (selected.platform === "terminal") return { ...base, method: "terminal_display", print_terminal_qr: true, print_links: true };
|
|
447
|
+
if (selected.platform && selected.platform !== "current_channel_required") return { ...base, ...plainChatHumanOutput(selected) };
|
|
448
|
+
return {
|
|
449
|
+
...base,
|
|
450
|
+
method: "current_channel_required",
|
|
451
|
+
instruction: "Choose the branch matching the current chat platform, send it to the human, then stop until the human responds or clicks a platform button.",
|
|
452
|
+
channels: compactObject({
|
|
453
|
+
openclaw_telegram: openclawHumanOutput(platforms.telegram?.openclaw_message),
|
|
454
|
+
codex_app: { method: "markdown_reply", markdown: platforms.codex_app?.markdown },
|
|
455
|
+
claude_code: { method: "markdown_reply", markdown: platforms.claude_code?.markdown },
|
|
456
|
+
plain_chat: plainChatHumanOutput(platforms.plain_chat),
|
|
457
|
+
terminal: { method: "terminal_display", print_terminal_qr: true, print_links: true }
|
|
458
|
+
})
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function openclawHumanOutput(message = {}) {
|
|
463
|
+
if (!message) return null;
|
|
464
|
+
return compactObject({
|
|
465
|
+
method: "openclaw_message_send",
|
|
466
|
+
channel: "telegram",
|
|
467
|
+
message: message.command_args?.message || message.message,
|
|
468
|
+
media: message.command_args?.media || message.media,
|
|
469
|
+
presentation: message.command_args?.presentation || message.presentation,
|
|
470
|
+
callbacks: message.callbacks
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function plainChatHumanOutput(plan = {}) {
|
|
475
|
+
if (!plan) return null;
|
|
476
|
+
return compactObject({
|
|
477
|
+
method: "plain_chat_reply",
|
|
478
|
+
text: plan.text,
|
|
479
|
+
links: plan.links
|
|
427
480
|
});
|
|
428
481
|
}
|
|
429
482
|
|
|
@@ -432,10 +485,33 @@ function openclawURLButton(text, url) {
|
|
|
432
485
|
}
|
|
433
486
|
|
|
434
487
|
function openclawCallbackButton(text, intent, action = {}) {
|
|
435
|
-
const id = action.payment_intent_id || action.id || "current";
|
|
488
|
+
const id = action.payment_intent_id || action.checkout_id || action.id || "current";
|
|
436
489
|
return { text, callback_data: `itp:${intent}:${id}` };
|
|
437
490
|
}
|
|
438
491
|
|
|
492
|
+
function openclawCallbackCommands(action = {}) {
|
|
493
|
+
if (action.kind === "payment_qr") {
|
|
494
|
+
const id = action.payment_intent_id || action.id || "";
|
|
495
|
+
return compactObject({
|
|
496
|
+
refresh_payment_qr: id ? `itp buyer payment refresh-qr ${id} --json` : undefined,
|
|
497
|
+
check_payment_status: id ? `itp buyer payment wait ${id} --timeout 1 --json` : undefined
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
if (action.checkout_id) {
|
|
501
|
+
return { check_checkout_status: `itp buyer checkout resume ${action.checkout_id} --json` };
|
|
502
|
+
}
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function checkoutIDFromURL(value = "") {
|
|
507
|
+
try {
|
|
508
|
+
const url = new URL(value);
|
|
509
|
+
return url.pathname.match(/\/checkouts\/([^/]+)/)?.[1] || "";
|
|
510
|
+
} catch {
|
|
511
|
+
return String(value || "").match(/\/checkouts\/([^/?#]+)/)?.[1] || "";
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
439
515
|
function selectedRenderPlatform(host = "") {
|
|
440
516
|
const normalized = String(host || "").toLowerCase().replaceAll("-", "_");
|
|
441
517
|
if (["codex", "codex_app", "codex_cli"].includes(normalized)) return "codex_app";
|
|
@@ -704,4 +780,4 @@ function openBrowser(targetURL) {
|
|
|
704
780
|
return false;
|
|
705
781
|
}
|
|
706
782
|
|
|
707
|
-
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 };
|
|
783
|
+
export { renderItPayPaymentAction, humanActionSummaryLines, writeHumanActionSummary, waitHeartbeatMs, writeWaitHeartbeat, renderHumanAction, buildHumanActionRenderPlan, humanOutputFromRenderPlan, 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
|
@@ -211,14 +211,14 @@ itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent
|
|
|
211
211
|
|
|
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
|
-
agent/chat clients, prefer `--no-wait-payment
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
`
|
|
221
|
-
|
|
214
|
+
agent/chat clients, prefer `--no-wait-payment`. In OpenClaw Telegram, if
|
|
215
|
+
`agent_instruction` is present, call `openclaw message send` with
|
|
216
|
+
`agent_instruction.openclaw_message.command_args`. The `media` field is the QR
|
|
217
|
+
image, `message` is the exact human text, and
|
|
218
|
+
`presentation.blocks[].type="buttons"` creates Telegram native inline buttons.
|
|
219
|
+
Do not rewrite it as a table or normal prose. In Codex or Claude Code app
|
|
220
|
+
clients, if `human_output_required=true`, send `human_output.markdown` exactly
|
|
221
|
+
before any next ItPay command.
|
|
222
222
|
|
|
223
223
|
If a response has `status=payment_handoff_required`, `next` is the user-visible
|
|
224
224
|
reply step, not payment wait. Do not run `buyer payment wait` until the human
|