@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/README.md +27 -21
- package/bin/itp +38 -32
- package/docs/agent/buyer/account-portal.json +22 -6
- package/docs/agent/buyer/cart-checkout.json +9 -9
- package/docs/agent/buyer/catalog-search.json +35 -8
- package/docs/agent/buyer/human-claim-ui.json +20 -6
- package/docs/agent/buyer/payment-qr.json +11 -9
- package/docs/agent/buyer/payment-wait.json +5 -4
- package/docs/agent/buyer/product-recommendation.json +1 -1
- package/docs/agent/buyer/qr-refresh.json +18 -5
- package/docs/agent/buyer/quickstart.json +13 -9
- package/docs/agent/buyer/recovery.json +25 -6
- package/docs/agent/buyer/safety-policy.json +17 -3
- package/docs/agent/buyer/secure-delivery.json +24 -6
- package/docs/agent/buyer/vault-agent-read.json +29 -10
- package/lib/buyer.js +322 -63
- package/lib/client-context.js +126 -0
- package/lib/render-human.js +10 -75
- package/package.json +1 -1
- package/skills/itpay-buyer/SKILL.md +55 -37
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(``);
|
|
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 ["
|
|
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
|
|
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("
|
|
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 };
|