@itpay/cli 2.0.10 → 2.0.11
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 +1 -1
- package/dist/src/commands/buy.js +40 -30
- package/dist/src/commands/checkout.js +23 -24
- package/dist/src/commands/checkout_handoff.js +37 -0
- package/dist/src/commands/install.js +1 -1
- package/dist/src/commands/pay.js +19 -8
- package/dist/src/commands/services.js +24 -26
- package/dist/src/main.js +4 -0
- package/dist/src/render/index.js +6 -4
- package/dist/src/state/config.js +1 -1
- package/docs/agent/buyer/payment-flow.json +1 -0
- package/docs/agent/buyer/render-hosts.json +5 -3
- package/docs/cli-reference/agent-types.md +16 -3
- package/docs/cli-reference/commands/buy.md +6 -6
- package/docs/cli-reference/commands/checkout.md +5 -5
- package/docs/cli-reference/commands/install.md +1 -1
- package/docs/cli-reference/commands/pay.md +1 -1
- package/docs/cli-reference/commands/services/checkout.md +13 -5
- package/package.json +1 -1
- package/skills/itpay-buyer/SKILL.md +41 -48
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ itpay --agent-type codex-desktop readyz --json
|
|
|
11
11
|
# follow next.command: typed skill show, then catalog list
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
The default API is temporarily `https://dev.itpay.ai` during the 2.0.
|
|
14
|
+
The default API is temporarily `https://dev.itpay.ai` during the 2.0.11 beta. Set `ITPAY_BACKEND_URL` only for an intentional override.
|
|
15
15
|
|
|
16
16
|
## Output Contract
|
|
17
17
|
|
package/dist/src/commands/buy.js
CHANGED
|
@@ -16,6 +16,8 @@ import { ensureIdeImageAttach } from "../render/ide.js";
|
|
|
16
16
|
import { buildAgentChatHandoff } from "../render/markdown.js";
|
|
17
17
|
import { formatMoney } from "../render/output.js";
|
|
18
18
|
import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
|
|
19
|
+
import { buildCheckoutHandoff, shouldPrepareLocalCheckoutImage } from "./checkout_handoff.js";
|
|
20
|
+
import { qualifyItPayCommand } from "../state/agent_type.js";
|
|
19
21
|
export async function runBuy(backend, config, options) {
|
|
20
22
|
const err = validateContext(options.host, options.target);
|
|
21
23
|
if (err) {
|
|
@@ -67,6 +69,7 @@ export async function runBuy(backend, config, options) {
|
|
|
67
69
|
const checkoutID = checkout.checkout.checkout_id;
|
|
68
70
|
const displayToken = checkout.display_token;
|
|
69
71
|
const checkoutURL = tokenizedCheckoutURL(checkout.checkout_url, displayToken, checkout.qr_payload);
|
|
72
|
+
const qrPNGURL = checkout.qr_png_url ? absolutePublicURL(config.baseURL, checkout.qr_png_url) : undefined;
|
|
70
73
|
const orderItems = cart.items.map((item) => ({
|
|
71
74
|
title: item.title,
|
|
72
75
|
quantity: item.quantity,
|
|
@@ -108,21 +111,24 @@ export async function runBuy(backend, config, options) {
|
|
|
108
111
|
nextAction: checkout.checkout.next_action,
|
|
109
112
|
orderItems,
|
|
110
113
|
orderCurrency: checkout.checkout.currency,
|
|
114
|
+
...(options.agentType ? { agentType: options.agentType } : {}),
|
|
111
115
|
};
|
|
112
|
-
if (
|
|
113
|
-
planInput.qrPNGURL =
|
|
116
|
+
if (qrPNGURL)
|
|
117
|
+
planInput.qrPNGURL = qrPNGURL;
|
|
114
118
|
if (paymentIntent) {
|
|
115
119
|
planInput.paymentIntentID = paymentIntent.payment_intent_id;
|
|
116
120
|
planInput.paymentMethod = paymentIntent.payment_method_type;
|
|
117
121
|
planInput.paymentStatus = paymentIntent.status;
|
|
118
122
|
}
|
|
119
123
|
const plan = buildCheckoutQRPlan(planInput);
|
|
120
|
-
// --- Download
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
// --- Download a local QR only for desktop Markdown hosts ---
|
|
125
|
+
if (shouldPrepareLocalCheckoutImage(platformKeyForHost(options.host))) {
|
|
126
|
+
await ensureIdeImageAttach(plan, {
|
|
127
|
+
enabled: config.ideImageAttach,
|
|
128
|
+
...(config.baseURL ? { baseURL: config.baseURL } : {}),
|
|
129
|
+
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
126
132
|
// --- Output ---
|
|
127
133
|
if (options.jsonOutput) {
|
|
128
134
|
const envelope = buildBuyEnvelope({
|
|
@@ -132,8 +138,9 @@ export async function runBuy(backend, config, options) {
|
|
|
132
138
|
displayToken,
|
|
133
139
|
plan,
|
|
134
140
|
waitStatus,
|
|
135
|
-
...(
|
|
141
|
+
...(qrPNGURL ? { qrPNGURL } : {}),
|
|
136
142
|
...(paymentIntent ? { paymentIntent } : {}),
|
|
143
|
+
...(options.agentType ? { agentType: options.agentType } : {}),
|
|
137
144
|
});
|
|
138
145
|
writeCommandEnvelope(envelope, { jsonOutput: true, ...(options.output ? { output: options.output } : {}) });
|
|
139
146
|
return {
|
|
@@ -166,20 +173,11 @@ export async function runBuy(backend, config, options) {
|
|
|
166
173
|
function buildBuyEnvelope(input) {
|
|
167
174
|
const verified = input.waitStatus === "verified";
|
|
168
175
|
const platform = platformKeyForHost(input.plan.host);
|
|
169
|
-
const
|
|
170
|
-
if (input.plan.ideImageAttach?.status === "downloaded" && input.plan.ideImageAttach.localPath) {
|
|
171
|
-
handoff.qr_local_path = input.plan.ideImageAttach.localPath;
|
|
172
|
-
}
|
|
173
|
-
if (platform === "markdown") {
|
|
174
|
-
handoff.markdown = buildAgentChatHandoff(input.plan).markdown;
|
|
175
|
-
}
|
|
176
|
-
else if (platform === "plain_chat" && input.qrPNGURL) {
|
|
177
|
-
handoff.qr_image_url = input.qrPNGURL;
|
|
178
|
-
}
|
|
176
|
+
const amount = formatMoney(input.cart.amount_minor, input.cart.currency);
|
|
179
177
|
const result = {
|
|
180
178
|
checkout_id: input.checkoutID,
|
|
181
179
|
payment: verified ? "verified" : "pending",
|
|
182
|
-
amount
|
|
180
|
+
amount,
|
|
183
181
|
item_count: input.cart.items.length,
|
|
184
182
|
...(input.paymentIntent ? {
|
|
185
183
|
payment_intent_id: input.paymentIntent.payment_intent_id,
|
|
@@ -196,22 +194,26 @@ function buildBuyEnvelope(input) {
|
|
|
196
194
|
recovery: [],
|
|
197
195
|
};
|
|
198
196
|
}
|
|
197
|
+
const presentationHandoff = buildCheckoutHandoff({
|
|
198
|
+
platform,
|
|
199
|
+
url: input.checkoutURL,
|
|
200
|
+
amount,
|
|
201
|
+
...(input.agentType ? { agentType: input.agentType } : {}),
|
|
202
|
+
...(input.qrPNGURL ? { qrImageURL: input.qrPNGURL } : {}),
|
|
203
|
+
...(input.plan.ideImageAttach?.status === "downloaded" && input.plan.ideImageAttach.localPath
|
|
204
|
+
? { localPath: input.plan.ideImageAttach.localPath }
|
|
205
|
+
: {}),
|
|
206
|
+
...(platform === "markdown" ? { markdown: buildAgentChatHandoff(input.plan).markdown } : {}),
|
|
207
|
+
});
|
|
199
208
|
return {
|
|
200
209
|
status: "human_checkout_required",
|
|
201
210
|
result,
|
|
202
|
-
handoff,
|
|
203
|
-
instruction:
|
|
211
|
+
handoff: presentationHandoff.handoff,
|
|
212
|
+
instruction: presentationHandoff.instruction,
|
|
204
213
|
next: { command: `itpay checkout --id ${input.checkoutID} --token ${input.displayToken} --json`, reason: "稍后查询同一笔 Checkout 状态" },
|
|
205
214
|
recovery: [],
|
|
206
215
|
};
|
|
207
216
|
}
|
|
208
|
-
function buyHandoffInstruction(platform) {
|
|
209
|
-
if (platform === "markdown")
|
|
210
|
-
return "把 handoff.markdown 原样发送到当前桌面对话;二维码和链接可见后等待用户操作,不要创建新 Checkout。";
|
|
211
|
-
if (platform === "terminal")
|
|
212
|
-
return "在用户可见终端展示 handoff 中的付款入口,然后等待用户操作;不要创建新 Checkout。";
|
|
213
|
-
return "把 handoff.url 和可用二维码附件发送给用户,然后等待用户操作;不要创建新 Checkout。";
|
|
214
|
-
}
|
|
215
217
|
// --- SSE wait for payment verification ---
|
|
216
218
|
async function waitForPaymentSSE(backend, checkoutID, displayToken, timeoutSec) {
|
|
217
219
|
return new Promise((resolve) => {
|
|
@@ -237,7 +239,7 @@ async function waitForPaymentSSE(backend, checkoutID, displayToken, timeoutSec)
|
|
|
237
239
|
export function buildCheckoutQRPlan(input) {
|
|
238
240
|
const summary = `Scan the QR or open ${input.checkoutURL} to start the human checkout flow.`;
|
|
239
241
|
const isPayment = input.paymentIntentID != null;
|
|
240
|
-
const afterCommand = `itpay checkout --id ${input.checkoutID} --token ${input.displayToken} --json
|
|
242
|
+
const afterCommand = qualifyItPayCommand(`itpay checkout --id ${input.checkoutID} --token ${input.displayToken} --json`, input.agentType);
|
|
241
243
|
const platform = {
|
|
242
244
|
text: summary,
|
|
243
245
|
links: [
|
|
@@ -301,3 +303,11 @@ function tokenizedCheckoutURL(checkoutURL, displayToken, qrPayload) {
|
|
|
301
303
|
return `${checkoutURL}${separator}display_token=${encodeURIComponent(displayToken)}`;
|
|
302
304
|
}
|
|
303
305
|
}
|
|
306
|
+
function absolutePublicURL(baseURL, value) {
|
|
307
|
+
try {
|
|
308
|
+
return new URL(value, baseURL.endsWith("/") ? baseURL : `${baseURL}/`).toString();
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return value;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -6,6 +6,7 @@ import { platformKeyForHost } from "../render/plan.js";
|
|
|
6
6
|
import { renderTerminalQR } from "../render/qr.js";
|
|
7
7
|
import { DEFAULT_BASE_URL } from "../state/config.js";
|
|
8
8
|
import { buildCheckoutQRPlan } from "./buy.js";
|
|
9
|
+
import { buildCheckoutHandoff, shouldPrepareLocalCheckoutImage } from "./checkout_handoff.js";
|
|
9
10
|
import { writeCommandEnvelope } from "./guidance.js";
|
|
10
11
|
export async function runCheckoutPresentation(backend, options) {
|
|
11
12
|
const presentation = await backend.getCheckoutPresentation(options.checkoutID, options.displayToken);
|
|
@@ -37,11 +38,15 @@ export async function runCheckoutPresentation(backend, options) {
|
|
|
37
38
|
currency: item.currency,
|
|
38
39
|
})),
|
|
39
40
|
orderCurrency: presentation.checkout.currency,
|
|
41
|
+
...(options.agentType ? { agentType: options.agentType } : {}),
|
|
40
42
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const platform = platformKeyForHost(host);
|
|
44
|
+
if (shouldPrepareLocalCheckoutImage(platform)) {
|
|
45
|
+
await ensureIdeImageAttach(plan, {
|
|
46
|
+
...(options.baseURL ? { baseURL: options.baseURL } : {}),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const envelope = pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand, options.agentType);
|
|
45
50
|
const plainResult = checkoutPlainResult(envelope.result);
|
|
46
51
|
if (!options.jsonOutput && platformKeyForHost(host) === "terminal") {
|
|
47
52
|
plainResult.push("qr:", await renderTerminalQR(checkoutURL, "terminal"));
|
|
@@ -52,19 +57,20 @@ export async function runCheckoutPresentation(backend, options) {
|
|
|
52
57
|
plainResult,
|
|
53
58
|
});
|
|
54
59
|
}
|
|
55
|
-
function pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand) {
|
|
60
|
+
function pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand, agentType) {
|
|
56
61
|
const platform = platformKeyForHost(plan.host);
|
|
57
|
-
const handoff = { url: checkoutURL };
|
|
58
|
-
if (plan.ideImageAttach?.status === "downloaded" && plan.ideImageAttach.localPath) {
|
|
59
|
-
handoff.qr_local_path = plan.ideImageAttach.localPath;
|
|
60
|
-
}
|
|
61
|
-
if (platform === "markdown") {
|
|
62
|
-
handoff.markdown = buildAgentChatHandoff(plan).markdown;
|
|
63
|
-
}
|
|
64
|
-
else if (platform === "plain_chat" && plan.preferredQRSources[0]) {
|
|
65
|
-
handoff.qr_image_url = plan.preferredQRSources[0];
|
|
66
|
-
}
|
|
67
62
|
const amount = formatMoney(presentation.checkout.amount_minor, presentation.checkout.currency);
|
|
63
|
+
const presentationHandoff = buildCheckoutHandoff({
|
|
64
|
+
platform,
|
|
65
|
+
url: checkoutURL,
|
|
66
|
+
amount,
|
|
67
|
+
...(agentType ? { agentType } : {}),
|
|
68
|
+
...(plan.preferredQRSources[0] ? { qrImageURL: plan.preferredQRSources[0] } : {}),
|
|
69
|
+
...(plan.ideImageAttach?.status === "downloaded" && plan.ideImageAttach.localPath
|
|
70
|
+
? { localPath: plan.ideImageAttach.localPath }
|
|
71
|
+
: {}),
|
|
72
|
+
...(platform === "markdown" ? { markdown: buildAgentChatHandoff(plan).markdown } : {}),
|
|
73
|
+
});
|
|
68
74
|
return {
|
|
69
75
|
status: "human_checkout_required",
|
|
70
76
|
result: {
|
|
@@ -72,8 +78,8 @@ function pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand) {
|
|
|
72
78
|
payment: "pending",
|
|
73
79
|
amount,
|
|
74
80
|
},
|
|
75
|
-
handoff,
|
|
76
|
-
instruction:
|
|
81
|
+
handoff: presentationHandoff.handoff,
|
|
82
|
+
instruction: presentationHandoff.instruction,
|
|
77
83
|
next: { command: nextCommand, reason: "稍后只查询同一 Checkout" },
|
|
78
84
|
recovery: [],
|
|
79
85
|
};
|
|
@@ -123,13 +129,6 @@ function checkoutNeedsHumanHandoff(status) {
|
|
|
123
129
|
function checkoutPlainResult(result) {
|
|
124
130
|
return Object.entries(result).map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
|
|
125
131
|
}
|
|
126
|
-
function pendingInstruction(platform, amount) {
|
|
127
|
-
if (platform === "markdown")
|
|
128
|
-
return `Backend 尚未确认付款。把 handoff.markdown 原样发送到当前桌面对话,确认二维码、链接和金额 ${amount} 已对用户可见,然后停止等待。不要创建新 Checkout、Execution 或 Payment Intent;稍后仍然只执行 next.command 查询这一笔 Checkout。`;
|
|
129
|
-
if (platform === "terminal")
|
|
130
|
-
return `Backend 尚未确认付款。在用户可见终端展示当前同一 Checkout 的二维码、链接和金额 ${amount},然后停止等待。不要创建新 Checkout、Execution 或 Payment Intent;稍后仍然只执行 next.command 查询这一笔 Checkout。`;
|
|
131
|
-
return `Backend 尚未确认付款。把 handoff.url 作为可点击链接发送给用户,优先把 handoff.qr_local_path 作为图片附件发送,不能发送本地附件时使用 handoff.qr_image_url,并说明金额 ${amount},然后停止等待。不要声称付款成功,不要创建新 Checkout、Execution 或 Payment Intent。稍后仍然只执行 next.command 查询这一笔 Checkout。`;
|
|
132
|
-
}
|
|
133
132
|
function formatMoney(amountMinor, currency) {
|
|
134
133
|
return `${(amountMinor / 100).toFixed(2)} ${currency}`;
|
|
135
134
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function shouldPrepareLocalCheckoutImage(platform) {
|
|
2
|
+
return platform === "markdown";
|
|
3
|
+
}
|
|
4
|
+
export function isWorkBuddyPlainChat(agentType, platform) {
|
|
5
|
+
return agentType?.trim().toLowerCase() === "workbuddy" && platform === "plain_chat";
|
|
6
|
+
}
|
|
7
|
+
export function buildCheckoutHandoff(input) {
|
|
8
|
+
const handoff = { url: input.url };
|
|
9
|
+
if (input.platform === "markdown") {
|
|
10
|
+
if (input.localPath)
|
|
11
|
+
handoff.qr_local_path = input.localPath;
|
|
12
|
+
if (input.markdown)
|
|
13
|
+
handoff.markdown = input.markdown;
|
|
14
|
+
}
|
|
15
|
+
else if (input.platform === "plain_chat" && input.qrImageURL) {
|
|
16
|
+
handoff.qr_image_url = input.qrImageURL;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
handoff,
|
|
20
|
+
instruction: checkoutHandoffInstruction(input.agentType, input.platform, input.amount, Boolean(input.qrImageURL)),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function checkoutHandoffInstruction(agentType, platform, amount, hasQRImage) {
|
|
24
|
+
if (isWorkBuddyPlainChat(agentType, platform)) {
|
|
25
|
+
if (!hasQRImage) {
|
|
26
|
+
return `Backend 尚未确认付款,且本次没有返回可展示的二维码。向用户说明金额 ${amount} 并发送 handoff.url,然后停止等待。不要调用 present_files,不要检查本地文件,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
27
|
+
}
|
|
28
|
+
return `Backend 尚未确认付款。读取 handoff.qr_image_url 的完整字符串,原样作为 files 数组唯一元素调用 present_files({ files: ["<完整 qr_image_url>"] });确认右侧二维码预览已打开后,向用户说明金额 ${amount} 并发送 handoff.url,然后停止等待。如果 present_files 失败,只发送 handoff.url 并说明二维码预览未打开,然后停止。不要检查本地文件,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
29
|
+
}
|
|
30
|
+
if (platform === "markdown") {
|
|
31
|
+
return `Backend 尚未确认付款。把 handoff.markdown 原样发送到当前桌面对话,确认二维码、付款链接和金额 ${amount} 均已实际对用户可见,然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
32
|
+
}
|
|
33
|
+
if (platform === "terminal") {
|
|
34
|
+
return `Backend 尚未确认付款。在用户可见终端展示当前 Checkout 的二维码、handoff.url 和金额 ${amount},然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
35
|
+
}
|
|
36
|
+
return `Backend 尚未确认付款。把 handoff.url 和可用的 handoff.qr_image_url 实际发送到当前会话,说明金额 ${amount},然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
37
|
+
}
|
|
@@ -13,7 +13,7 @@ const INSTRUCTIONS = {
|
|
|
13
13
|
"codex-cli": "在 Codex CLI 中始终传这个 Agent Type;付款交接只应显示在用户可见终端。",
|
|
14
14
|
"claude-code-desktop": "在 Claude Code Desktop 中始终传这个 Agent Type;付款时把返回的二维码和链接实际展示到当前对话。",
|
|
15
15
|
"claude-code-cli": "在 Claude Code CLI 中始终传这个 Agent Type;付款交接只应显示在用户可见终端。",
|
|
16
|
-
workbuddy: "在 WorkBuddy 中始终传这个 Agent Type
|
|
16
|
+
workbuddy: "在 WorkBuddy 中始终传这个 Agent Type;Checkout 返回 qr_image_url 时,必须按当次 instruction 调用 present_files 在右侧打开二维码,不要检查本地二维码文件。",
|
|
17
17
|
};
|
|
18
18
|
export function runInstall(target, options = {}) {
|
|
19
19
|
if (!target || target === "list") {
|
package/dist/src/commands/pay.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Checkout page; this command exists for controlled integration recovery.
|
|
3
3
|
import { formatMoney } from "../render/output.js";
|
|
4
4
|
import { writeCommandEnvelope } from "./guidance.js";
|
|
5
|
+
import { isWorkBuddyPlainChat } from "./checkout_handoff.js";
|
|
6
|
+
import { platformKeyForHost } from "../render/plan.js";
|
|
5
7
|
export async function runPay(backend, options) {
|
|
6
8
|
const intent = await backend.createPaymentIntent(options.checkoutID, {
|
|
7
9
|
payment_method_type: options.method,
|
|
@@ -23,16 +25,17 @@ function payEnvelope(intent, options) {
|
|
|
23
25
|
if (!terminal && !verified && intent.action?.mobile_wallet_url)
|
|
24
26
|
handoff.mobile_wallet_url = intent.action.mobile_wallet_url;
|
|
25
27
|
const hasAction = Object.keys(handoff).length > 0;
|
|
28
|
+
const amount = formatMoney(intent.amount_minor, intent.currency);
|
|
26
29
|
return {
|
|
27
30
|
status: verified ? "payment_verified" : terminal ? "payment_unavailable" : hasAction ? "payment_action_ready" : "payment_action_pending",
|
|
28
31
|
result: {
|
|
29
32
|
checkout_id: options.checkoutID,
|
|
30
33
|
payment_intent_id: intent.payment_intent_id,
|
|
31
34
|
payment: verified ? "verified" : intent.status,
|
|
32
|
-
amount
|
|
35
|
+
amount,
|
|
33
36
|
},
|
|
34
37
|
...(hasAction ? { handoff } : {}),
|
|
35
|
-
instruction: payInstruction(options
|
|
38
|
+
instruction: payInstruction(options, verified, terminal, hasAction, Boolean(handoff.qr_image_url), Boolean(handoff.mobile_wallet_url), amount),
|
|
36
39
|
next: {
|
|
37
40
|
command: `itpay checkout --id ${options.checkoutID} --token ${options.displayToken} --json`,
|
|
38
41
|
reason: verified ? "读取权威订单和履约状态" : "读取同一 Checkout 的权威付款状态",
|
|
@@ -40,16 +43,24 @@ function payEnvelope(intent, options) {
|
|
|
40
43
|
recovery: [],
|
|
41
44
|
};
|
|
42
45
|
}
|
|
43
|
-
function payInstruction(
|
|
46
|
+
function payInstruction(options, verified, terminal, hasAction, hasQR, hasWallet, amount) {
|
|
44
47
|
if (verified)
|
|
45
48
|
return "付款已确认;不要再次展示付款动作,继续读取同一 Checkout。";
|
|
46
49
|
if (terminal)
|
|
47
50
|
return "Payment Intent 已终止;不要自行创建替代付款,回到同一 Checkout 读取恢复方向。";
|
|
48
51
|
if (!hasAction)
|
|
49
52
|
return "Payment Intent 尚未返回可展示动作;不要猜测渠道链接,回到同一 Checkout 查询。";
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const platform = platformKeyForHost(options.host);
|
|
54
|
+
if (isWorkBuddyPlainChat(options.agentType, platform)) {
|
|
55
|
+
if (hasQR && hasWallet)
|
|
56
|
+
return `这是受控逃生入口。读取 handoff.qr_image_url 的完整字符串,原样作为 files 数组唯一元素调用 present_files({ files: ["<完整 qr_image_url>"] });右侧预览打开后说明金额 ${amount}、发送 handoff.mobile_wallet_url 并停止等待。如果 present_files 失败,发送 handoff.qr_image_url 和 handoff.mobile_wallet_url 并说明二维码预览未打开,然后停止。不要立即查询、创建替代 Checkout 或 Payment Intent。`;
|
|
57
|
+
if (hasQR)
|
|
58
|
+
return `这是受控逃生入口。读取 handoff.qr_image_url 的完整字符串,原样作为 files 数组唯一元素调用 present_files({ files: ["<完整 qr_image_url>"] });右侧预览打开后说明金额 ${amount} 并停止等待。如果 present_files 失败,把 handoff.qr_image_url 作为可点击链接发送给用户并说明二维码预览未打开,然后停止。不要立即查询、创建替代 Checkout 或 Payment Intent。`;
|
|
59
|
+
return `这是受控逃生入口。说明金额 ${amount},把 handoff.mobile_wallet_url 作为可点击链接发送给用户,然后停止等待;不要立即查询、创建替代 Checkout 或 Payment Intent。`;
|
|
60
|
+
}
|
|
61
|
+
if (options.host === "codex" || options.host === "claude-code")
|
|
62
|
+
return "这是受控逃生入口;把 handoff 中的二维码或钱包链接实际发到当前桌面对话,然后停止等待。";
|
|
63
|
+
if (options.host === "terminal")
|
|
64
|
+
return "这是受控逃生入口;只在用户可见终端展示 handoff,然后停止等待。";
|
|
65
|
+
return "这是受控逃生入口;把 handoff 中的二维码或钱包链接发送到当前会话,然后停止等待。";
|
|
55
66
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { operationID } from "../state/config.js";
|
|
2
2
|
import { dispatchRender } from "../render/index.js";
|
|
3
3
|
import { ensureIdeImageAttach } from "../render/ide.js";
|
|
4
|
+
import { buildCheckoutHandoff, shouldPrepareLocalCheckoutImage } from "./checkout_handoff.js";
|
|
4
5
|
import { buildAgentChatHandoff } from "../render/markdown.js";
|
|
5
6
|
import { platformKeyForHost } from "../render/plan.js";
|
|
6
7
|
import { renderTerminalQR } from "../render/qr.js";
|
|
@@ -398,6 +399,7 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
398
399
|
currency: item.currency,
|
|
399
400
|
})),
|
|
400
401
|
orderCurrency: checkout.checkout.currency,
|
|
402
|
+
...(options.agentType ? { agentType: options.agentType } : {}),
|
|
401
403
|
});
|
|
402
404
|
options.persistHandoff?.({
|
|
403
405
|
serviceExecutionID,
|
|
@@ -419,12 +421,14 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
419
421
|
});
|
|
420
422
|
return;
|
|
421
423
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
424
|
+
if (shouldPrepareLocalCheckoutImage(platform)) {
|
|
425
|
+
await ensureIdeImageAttach(plan, {
|
|
426
|
+
enabled: config.ideImageAttach,
|
|
427
|
+
...(config.baseURL ? { baseURL: config.baseURL } : {}),
|
|
428
|
+
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
const envelope = buildServicesCheckoutEnvelope(response, checkoutURL, plan, config.baseURL, options.agentType);
|
|
428
432
|
const plainResult = [
|
|
429
433
|
`service_execution_id: ${response.binding.service_execution_id}`,
|
|
430
434
|
`checkout_id: ${checkoutID}`,
|
|
@@ -898,20 +902,21 @@ function parseValue(value) {
|
|
|
898
902
|
return Number(value);
|
|
899
903
|
return value;
|
|
900
904
|
}
|
|
901
|
-
function buildServicesCheckoutEnvelope(response, checkoutURL, plan, baseURL) {
|
|
905
|
+
function buildServicesCheckoutEnvelope(response, checkoutURL, plan, baseURL, agentType) {
|
|
902
906
|
const checkout = response.checkout;
|
|
903
907
|
const platform = platformKeyForHost(plan.host);
|
|
904
|
-
const handoff = { url: checkoutURL };
|
|
905
|
-
if (plan.ideImageAttach?.status === "downloaded" && plan.ideImageAttach.localPath) {
|
|
906
|
-
handoff.qr_local_path = plan.ideImageAttach.localPath;
|
|
907
|
-
}
|
|
908
|
-
if (platform === "markdown") {
|
|
909
|
-
handoff.markdown = buildAgentChatHandoff(plan).markdown;
|
|
910
|
-
}
|
|
911
|
-
else if (platform === "plain_chat" && checkout.qr_png_url) {
|
|
912
|
-
handoff.qr_image_url = absolutePublicURL(baseURL, checkout.qr_png_url);
|
|
913
|
-
}
|
|
914
908
|
const amount = formatMoney(checkout.checkout.amount_minor, checkout.checkout.currency);
|
|
909
|
+
const presentationHandoff = buildCheckoutHandoff({
|
|
910
|
+
platform,
|
|
911
|
+
url: checkoutURL,
|
|
912
|
+
amount,
|
|
913
|
+
...(agentType ? { agentType } : {}),
|
|
914
|
+
...(checkout.qr_png_url ? { qrImageURL: absolutePublicURL(baseURL, checkout.qr_png_url) } : {}),
|
|
915
|
+
...(plan.ideImageAttach?.status === "downloaded" && plan.ideImageAttach.localPath
|
|
916
|
+
? { localPath: plan.ideImageAttach.localPath }
|
|
917
|
+
: {}),
|
|
918
|
+
...(platform === "markdown" ? { markdown: buildAgentChatHandoff(plan).markdown } : {}),
|
|
919
|
+
});
|
|
915
920
|
return {
|
|
916
921
|
status: "human_checkout_required",
|
|
917
922
|
result: {
|
|
@@ -921,8 +926,8 @@ function buildServicesCheckoutEnvelope(response, checkoutURL, plan, baseURL) {
|
|
|
921
926
|
locked_input: response.locked_input,
|
|
922
927
|
amount,
|
|
923
928
|
},
|
|
924
|
-
handoff,
|
|
925
|
-
instruction:
|
|
929
|
+
handoff: presentationHandoff.handoff,
|
|
930
|
+
instruction: presentationHandoff.instruction,
|
|
926
931
|
next: {
|
|
927
932
|
command: `itpay checkout --id ${checkout.checkout.checkout_id} --token ${checkout.display_token} --json`,
|
|
928
933
|
reason: "仅在用户完成付款操作或要求查询后,读取同一 Checkout 的权威状态",
|
|
@@ -933,13 +938,6 @@ function buildServicesCheckoutEnvelope(response, checkoutURL, plan, baseURL) {
|
|
|
933
938
|
function checkoutCapabilityID(response, fallback = "") {
|
|
934
939
|
return response.capability_id || fallback;
|
|
935
940
|
}
|
|
936
|
-
function checkoutInstruction(platform, amount) {
|
|
937
|
-
if (platform === "markdown")
|
|
938
|
-
return `把 handoff.markdown 原样发送到当前桌面对话,确认二维码、付款链接和金额 ${amount} 都已实际对用户可见,然后停止等待。不要立即执行 next.command,不要创建第二个 Checkout、Execution 或调用 pay;用户完成付款操作或要求查询后,只执行 next.command。`;
|
|
939
|
-
if (platform === "terminal")
|
|
940
|
-
return `在用户可见终端展示二维码、付款链接和金额 ${amount},然后停止等待。不要立即执行 next.command,不要创建第二个 Checkout、Execution 或调用 pay;用户完成付款操作或要求查询后,只执行 next.command。`;
|
|
941
|
-
return `现在只做以下动作:1)把 handoff.url 作为可点击付款链接发送给用户;2)优先把 handoff.qr_local_path 作为图片附件发送,不能发送本地附件时使用 handoff.qr_image_url;3)明确告诉用户本次金额是 ${amount};4)发送完成后停止并等待用户操作。不要立即执行 next.command,不要创建第二个 Checkout,不要新建 Execution,不要调用 pay。用户表示已经完成付款或要求查询状态后,只执行 next.command;用户的话本身不是付款成功证明。`;
|
|
942
|
-
}
|
|
943
941
|
function absolutePublicURL(baseURL, value) {
|
|
944
942
|
try {
|
|
945
943
|
return new URL(value, baseURL.endsWith("/") ? baseURL : `${baseURL}/`).toString();
|
package/dist/src/main.js
CHANGED
|
@@ -668,6 +668,7 @@ program
|
|
|
668
668
|
const buyOptions = {
|
|
669
669
|
cartSession: session,
|
|
670
670
|
host,
|
|
671
|
+
...(config.agentType ? { agentType: config.agentType } : {}),
|
|
671
672
|
...(options.cart ? { cartID: options.cart } : {}),
|
|
672
673
|
...(options.target ? { target: options.target } : {}),
|
|
673
674
|
...(options.ref ? { clientReferenceID: options.ref } : {}),
|
|
@@ -720,6 +721,7 @@ program
|
|
|
720
721
|
checkoutID,
|
|
721
722
|
displayToken,
|
|
722
723
|
host,
|
|
724
|
+
...(config.agentType ? { agentType: config.agentType } : {}),
|
|
723
725
|
baseURL: config.baseURL,
|
|
724
726
|
jsonOutput: Boolean(options.json),
|
|
725
727
|
});
|
|
@@ -777,6 +779,7 @@ program
|
|
|
777
779
|
displayToken,
|
|
778
780
|
method: options.method,
|
|
779
781
|
host,
|
|
782
|
+
...(config.agentType ? { agentType: config.agentType } : {}),
|
|
780
783
|
...(options.refresh ? { refreshAction: true } : {}),
|
|
781
784
|
...(jsonOutput ? { jsonOutput: true } : {}),
|
|
782
785
|
});
|
|
@@ -1098,6 +1101,7 @@ services
|
|
|
1098
1101
|
lockedInput: parseKeyValueList(options.input),
|
|
1099
1102
|
resume: Boolean(options.resume),
|
|
1100
1103
|
host: withHost(options.host ?? defaultHostForAgentType(config.agentType)),
|
|
1104
|
+
...(config.agentType ? { agentType: config.agentType } : {}),
|
|
1101
1105
|
...(options.target ? { target: options.target } : {}),
|
|
1102
1106
|
...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
|
|
1103
1107
|
...(options.qrFile ? { qrFilePath: options.qrFile } : {}),
|
package/dist/src/render/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Render plan entry point. `dispatch` runs the right renderer based
|
|
2
|
-
// on the host.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// on the host. Desktop Markdown and terminal renderers prepare a local
|
|
3
|
+
// image; plain-chat and IM renderers keep the server URL and never create
|
|
4
|
+
// an unused local attachment.
|
|
5
5
|
import { renderTerminal } from "./terminal.js";
|
|
6
6
|
import { renderMarkdown } from "./markdown.js";
|
|
7
7
|
import { renderPlainChat } from "./plain_chat.js";
|
|
@@ -10,8 +10,10 @@ import { renderFeishu } from "./feishu.js";
|
|
|
10
10
|
import { platformKeyForHost } from "./plan.js";
|
|
11
11
|
import { ensureIdeImageAttach } from "./ide.js";
|
|
12
12
|
export async function dispatchRender(plan, options) {
|
|
13
|
-
await ensureIdeImageAttach(plan, options);
|
|
14
13
|
const key = platformKeyForHost(plan.host);
|
|
14
|
+
if (key === "markdown" || key === "terminal") {
|
|
15
|
+
await ensureIdeImageAttach(plan, options);
|
|
16
|
+
}
|
|
15
17
|
switch (key) {
|
|
16
18
|
case "terminal": {
|
|
17
19
|
const terminalOptions = {
|
package/dist/src/state/config.js
CHANGED
|
@@ -10,7 +10,7 @@ import { declaredAgentType } from "./agent_type.js";
|
|
|
10
10
|
import { DeviceAuthority } from "./device_authority.js";
|
|
11
11
|
import { OperationJournal } from "./operation_journal.js";
|
|
12
12
|
export const DEFAULT_BASE_URL = "https://dev.itpay.ai";
|
|
13
|
-
export const CLI_VERSION = "2.0.
|
|
13
|
+
export const CLI_VERSION = "2.0.11";
|
|
14
14
|
export const API_CONTRACT_REVISION = "sha256:3779f5468ea2109d4134c4ace66258c9eabe82461fa86e83c84df8c860276886";
|
|
15
15
|
const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
|
|
16
16
|
const CART_SESSION_FILENAME = "cart.json";
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"Use only handoff fields returned for the current Host and make them actually visible to the human.",
|
|
31
31
|
"Before creating a single-Service Checkout, send the exact price confirmation requested by the current instruction, stop, and wait for explicit human agreement.",
|
|
32
32
|
"After presenting a Checkout, stop. Run the returned next.command only after the human completes the payment action or asks to query the same Checkout.",
|
|
33
|
+
"For workbuddy with plain-chat, present the returned HTTPS QR with present_files exactly as instructed; presentation failure falls back only to handoff.url and never creates another payment resource.",
|
|
33
34
|
"Payment is verified only by Backend Checkout or Order state, never by QR rendering, redirect, or user claim.",
|
|
34
35
|
"A terminal payment state must never display another payment handoff."
|
|
35
36
|
],
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
{
|
|
33
33
|
"agent_type": "workbuddy",
|
|
34
34
|
"default_host": "plain-chat",
|
|
35
|
-
"responsibility": "send the
|
|
35
|
+
"responsibility": "when handoff.qr_image_url exists, use its exact HTTPS value as the only files element in present_files; otherwise send the Checkout URL without calling present_files; show the amount and stop"
|
|
36
36
|
}
|
|
37
37
|
],
|
|
38
38
|
"commands": [
|
|
@@ -49,14 +49,16 @@
|
|
|
49
49
|
],
|
|
50
50
|
"agent_rules": [
|
|
51
51
|
"Agent Type is stable identity; Host is presentation and may be explicitly overridden.",
|
|
52
|
-
"A local QR path is not visible until
|
|
52
|
+
"A local QR path is not visible until a desktop Agent attaches or renders that file on the human-facing surface; WorkBuddy never receives a local QR path.",
|
|
53
|
+
"For workbuddy with plain-chat, call present_files exactly as the returned instruction says. If it fails, send only handoff.url, report that the preview did not open, and stop.",
|
|
53
54
|
"Do not claim the handoff was shown until both a usable payment image or QR and the Checkout URL are visible.",
|
|
54
55
|
"IM Hosts that require a target must receive --target; the initial five Agent Types currently default to desktop, terminal, or plain-chat Hosts."
|
|
55
56
|
],
|
|
56
57
|
"forbidden": [
|
|
57
58
|
"Do not expose display tokens separately from the tokenized Checkout handoff.",
|
|
58
59
|
"Do not print base64 images, mirror lists, renderer debug state, or provider QR URLs.",
|
|
59
|
-
"Do not claim a desktop attachment exists when only a filesystem path was printed."
|
|
60
|
+
"Do not claim a desktop attachment exists when only a filesystem path was printed.",
|
|
61
|
+
"Do not inspect, download, or regenerate a local QR file for WorkBuddy."
|
|
60
62
|
],
|
|
61
63
|
"next_docs": [
|
|
62
64
|
{ "condition": "Need payment verification rules", "topic": "payment-flow" }
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
| `codex-cli` | `terminal` | 在用户可见终端渲染二维码并输出付款链接;若用户不看该终端,要求改用正确 Host。 |
|
|
13
13
|
| `claude-code-desktop` | `claude-code` | 返回桌面对话可展示的 Markdown 图片和付款链接,要求先展示再等待。 |
|
|
14
14
|
| `claude-code-cli` | `terminal` | 在用户可见终端输出二维码和链接,不声称已在桌面对话展示。 |
|
|
15
|
-
| `workbuddy` | `plain-chat` |
|
|
15
|
+
| `workbuddy` | `plain-chat` | 只返回 HTTPS 二维码和付款链接;要求 Agent 用 `present_files` 在右侧预览打开二维码,不返回或检查本地图片路径。 |
|
|
16
16
|
|
|
17
17
|
## 通用规则
|
|
18
18
|
|
|
@@ -37,8 +37,9 @@
|
|
|
37
37
|
},
|
|
38
38
|
"handoff": {
|
|
39
39
|
"url": "<checkout_url>",
|
|
40
|
-
"qr_local_path": "<
|
|
41
|
-
"
|
|
40
|
+
"qr_local_path": "<desktop_optional_local_path>",
|
|
41
|
+
"qr_image_url": "<workbuddy_optional_absolute_https_png>",
|
|
42
|
+
"markdown": "<desktop_optional_host_ready_markdown>"
|
|
42
43
|
},
|
|
43
44
|
"instruction": "<agent-type-specific instruction>",
|
|
44
45
|
"next": {
|
|
@@ -49,3 +50,15 @@
|
|
|
49
50
|
```
|
|
50
51
|
|
|
51
52
|
只返回当前 Host 能使用的 `handoff` 字段,不返回镜像路径列表、渲染器内部状态或重复的 action 描述。
|
|
53
|
+
|
|
54
|
+
准确字段集合:
|
|
55
|
+
|
|
56
|
+
| Agent Type / Host | `handoff` keys |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `codex-desktop / codex` | `url, qr_local_path, markdown` |
|
|
59
|
+
| `claude-code-desktop / claude-code` | `url, qr_local_path, markdown` |
|
|
60
|
+
| `codex-cli / terminal` | `url`;非 JSON 输出另外渲染终端二维码 |
|
|
61
|
+
| `claude-code-cli / terminal` | `url`;非 JSON 输出另外渲染终端二维码 |
|
|
62
|
+
| `workbuddy / plain-chat` | `url, qr_image_url` |
|
|
63
|
+
|
|
64
|
+
WorkBuddy instruction 只在 `handoff.qr_image_url` 存在时要求读取其完整字符串并作为 `files` 数组唯一元素调用 `present_files`。如果该可选字段不存在,必须直接发送 `handoff.url`,明确不要调用 `present_files`。两种情况都要停止等待;不能检查本地文件、下载或重建二维码、调用 `pay` 或创建替代付款资源。显式 `--host` 仍覆盖默认展示方式,因此只有 `workbuddy + plain-chat` 使用该规则。
|
|
@@ -87,9 +87,9 @@ itpay buy \
|
|
|
87
87
|
},
|
|
88
88
|
"handoff": {
|
|
89
89
|
"url": "<tokenized_checkout_url>",
|
|
90
|
-
"qr_local_path": "<
|
|
90
|
+
"qr_local_path": "<desktop_optional_host_ready_file>",
|
|
91
91
|
"markdown": "<desktop_only_optional_markdown>",
|
|
92
|
-
"qr_image_url": "<
|
|
92
|
+
"qr_image_url": "<workbuddy_only_absolute_https_png>"
|
|
93
93
|
},
|
|
94
94
|
"instruction": "<current_host_instruction>",
|
|
95
95
|
"next": {
|
|
@@ -102,7 +102,7 @@ itpay buy \
|
|
|
102
102
|
|
|
103
103
|
`handoff` 只保留当前 Host 可以使用的字段。不得返回二维码 base64、镜像路径数组、renderer 状态、原始后端 DTO 或重复的 `agent_action`。
|
|
104
104
|
|
|
105
|
-
**Instruction:**
|
|
105
|
+
**Instruction:** 必须使用下方 Agent Type 表定义的展示动作;展示完成或失败后停止。只有用户明确表示已付款或要求查询时才执行 `next.command`,并以后端状态为准。
|
|
106
106
|
|
|
107
107
|
### `--pay` 已观察到付款事件
|
|
108
108
|
|
|
@@ -159,9 +159,9 @@ itpay buy \
|
|
|
159
159
|
| Agent Type | 默认 Host | JSON handoff | Instruction |
|
|
160
160
|
|---|---|---|---|
|
|
161
161
|
| `codex-desktop` | `codex` | `url`、可用时 `qr_local_path` 和 `markdown` | 把 `handoff.markdown` 原样发到当前 Codex 对话,确认二维码和链接可见后等待。 |
|
|
162
|
-
| `codex-cli` | `terminal` | `url
|
|
162
|
+
| `codex-cli` | `terminal` | `url` | 非 JSON 模式在用户可见终端渲染二维码;始终保留付款链接。 |
|
|
163
163
|
| `claude-code-desktop` | `claude-code` | `url`、可用时 `qr_local_path` 和 `markdown` | 把 Markdown handoff 发到当前桌面对话,不能只输出本地路径。 |
|
|
164
|
-
| `claude-code-cli` | `terminal` | `url
|
|
165
|
-
| `workbuddy` | `plain-chat` | `url
|
|
164
|
+
| `claude-code-cli` | `terminal` | `url` | 在用户可见终端展示;不能声称桌面对话已收到图片。 |
|
|
165
|
+
| `workbuddy` | `plain-chat` | `url, qr_image_url?` | 有 `qr_image_url` 时调用 `present_files`;没有时只发送金额和 `url` 且不调用工具。两者都停止,不读取本地文件。 |
|
|
166
166
|
|
|
167
167
|
显式 `--host` 可以覆盖展示方式,但不会改变 Agent Type、设备身份、金额、权限或交易状态。
|
|
@@ -24,8 +24,8 @@ itpay checkout [--id <checkout_id>] [--token <display_token>]
|
|
|
24
24
|
{
|
|
25
25
|
"status": "human_checkout_required",
|
|
26
26
|
"result": { "checkout_id": "<checkout_id>", "payment": "pending", "amount": "<amount> <currency>" },
|
|
27
|
-
"handoff": { "url": "<checkout_url>", "qr_local_path": "<
|
|
28
|
-
"instruction": "
|
|
27
|
+
"handoff": { "url": "<checkout_url>", "qr_local_path": "<desktop_optional_path>", "qr_image_url": "<workbuddy_optional_absolute_https_png>", "markdown": "<desktop_optional_markdown>" },
|
|
28
|
+
"instruction": "<exact_agent_type_instruction>",
|
|
29
29
|
"next": { "command": "itpay checkout --id <checkout_id> --token <display_token> --json", "reason": "稍后只查询同一 Checkout" },
|
|
30
30
|
"recovery": []
|
|
31
31
|
}
|
|
@@ -66,9 +66,9 @@ token 缺失或不匹配时使用本机句柄恢复。只有请求的 Checkout
|
|
|
66
66
|
| Agent Type | Handoff |
|
|
67
67
|
|---|---|
|
|
68
68
|
| `codex-desktop` | `url, qr_local_path, markdown`;原样发送 Markdown。 |
|
|
69
|
-
| `codex-cli` | `url
|
|
69
|
+
| `codex-cli` | `url`;普通文本模式渲染终端二维码。 |
|
|
70
70
|
| `claude-code-desktop` | `url, qr_local_path, markdown`;原样发送 Markdown。 |
|
|
71
|
-
| `claude-code-cli` | `url
|
|
72
|
-
| `workbuddy` | `url,
|
|
71
|
+
| `claude-code-cli` | `url`;普通文本模式渲染终端二维码。 |
|
|
72
|
+
| `workbuddy` | `url, qr_image_url?`;有二维码 URL 时按 `services checkout` 相同规则调用 `present_files`,没有时只发送 Checkout URL,不生成本地文件。 |
|
|
73
73
|
|
|
74
74
|
完成、退款或失效状态下五种 Agent Type 都只返回同一状态和下一步,不渲染二维码。
|
|
@@ -83,7 +83,7 @@ itpay install [target] [--json]
|
|
|
83
83
|
| `codex-cli` | `terminal` | 只在用户可见终端展示付款交接。 |
|
|
84
84
|
| `claude-code-desktop` | `claude-code` | 桌面对话必须实际展示二维码和付款链接。 |
|
|
85
85
|
| `claude-code-cli` | `terminal` | 只在用户可见终端展示付款交接。 |
|
|
86
|
-
| `workbuddy` | `plain-chat` |
|
|
86
|
+
| `workbuddy` | `plain-chat` | Checkout 返回 `qr_image_url` 时,按当次 instruction 调用 `present_files` 在右侧打开二维码;不检查本地二维码文件。 |
|
|
87
87
|
|
|
88
88
|
显式 `--host` 可以在后续 commerce 命令覆盖默认 Host,但不会改变 Agent Type 或设备归属。
|
|
89
89
|
|
|
@@ -98,6 +98,6 @@ API 安全合同要求后端验证 display token 是该 Checkout 当前有效的
|
|
|
98
98
|
| `codex-cli` | `terminal` | 只在用户可见终端展示渠道动作。 |
|
|
99
99
|
| `claude-code-desktop` | `claude-code` | 把安全 handoff 发到当前桌面对话。 |
|
|
100
100
|
| `claude-code-cli` | `terminal` | 只在用户可见终端展示渠道动作。 |
|
|
101
|
-
| `workbuddy` | `plain-chat` |
|
|
101
|
+
| `workbuddy` | `plain-chat` | 有 `qr_image_url` 时调用 `present_files`;若同时有钱包链接则发送 `mobile_wallet_url`。二维码预览失败时只发送实际存在的二维码/钱包链接;只有钱包链接时直接发送它。展示后停止,不立即查询或创建替代付款。 |
|
|
102
102
|
|
|
103
103
|
Host 只改变 instruction;Payment Intent ID、金额、状态、重试语义和权限必须一致。
|
|
@@ -32,8 +32,8 @@ itpay services checkout <service_execution_id> --resume
|
|
|
32
32
|
"locked_input": { "<required_key>": "<value>" },
|
|
33
33
|
"amount": "<amount> <currency>"
|
|
34
34
|
},
|
|
35
|
-
"handoff": { "url": "<checkout_url>", "qr_local_path": "<
|
|
36
|
-
"instruction": "
|
|
35
|
+
"handoff": { "url": "<checkout_url>", "qr_local_path": "<desktop_optional_path>", "qr_image_url": "<workbuddy_optional_absolute_https_png>", "markdown": "<desktop_optional_markdown>" },
|
|
36
|
+
"instruction": "<exact_agent_type_instruction>",
|
|
37
37
|
"next": { "command": "itpay checkout --id <checkout_id> --token <display_token> --json", "reason": "仅在用户完成付款操作或要求查询后,读取同一 Checkout 的权威状态" },
|
|
38
38
|
"recovery": []
|
|
39
39
|
}
|
|
@@ -76,7 +76,15 @@ itpay services checkout <service_execution_id> --resume
|
|
|
76
76
|
| Agent Type | Instruction |
|
|
77
77
|
|---|---|
|
|
78
78
|
| `codex-desktop` | `handoff={url,qr_local_path,markdown}`;把 `handoff.markdown` 原样发送到当前桌面对话。 |
|
|
79
|
-
| `codex-cli` | `handoff={url
|
|
79
|
+
| `codex-cli` | `handoff={url}`;普通文本模式在用户可见终端渲染二维码。 |
|
|
80
80
|
| `claude-code-desktop` | `handoff={url,qr_local_path,markdown}`;把 `handoff.markdown` 原样发送到当前桌面对话。 |
|
|
81
|
-
| `claude-code-cli` | `handoff={url
|
|
82
|
-
| `workbuddy` | `handoff={url,
|
|
81
|
+
| `claude-code-cli` | `handoff={url}`;普通文本模式在用户可见终端渲染二维码。 |
|
|
82
|
+
| `workbuddy` | `handoff={url,qr_image_url?}`;有 `qr_image_url` 时读取完整值并作为 `files` 数组唯一元素调用 `present_files`;没有时直接发送金额与 `url`,不得调用 `present_files`。两者随后都停止,不得检查或生成本地文件。 |
|
|
83
|
+
|
|
84
|
+
WorkBuddy 的准确 instruction 语义必须完整包含:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
Backend 尚未确认付款。读取 handoff.qr_image_url 的完整字符串,原样作为 files 数组唯一元素调用 present_files({ files: ["<完整 qr_image_url>"] });确认右侧二维码预览已打开后,向用户说明金额并发送 handoff.url,然后停止等待。如果 present_files 失败,只发送 handoff.url 并说明二维码预览未打开,然后停止。不要检查本地文件,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
若 `qr_image_url` 缺失,准确 instruction 必须改为:说明本次没有可展示二维码,发送金额与 `handoff.url`,明确不要调用 `present_files`,然后遵守相同停止和付款证明规则。
|
package/package.json
CHANGED
|
@@ -8,14 +8,14 @@ description: >
|
|
|
8
8
|
|
|
9
9
|
# ItPay Buyer
|
|
10
10
|
|
|
11
|
-
Use the CLI as the only ItPay control surface.
|
|
11
|
+
Use the CLI as the only ItPay control surface. Never recreate API calls or hardcode a service-specific sequence.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Envelope Rule
|
|
14
14
|
|
|
15
|
-
- Keep one honest Agent Type,
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
15
|
+
- Keep one honest Agent Type, CLI/Node launcher, and Host permission context. Supported types: `codex-desktop`, `codex-cli`, `claude-code-desktop`, `claude-code-cli`, `workbuddy`.
|
|
16
|
+
- Windows, tasks, chats, processes, and model sessions are not new Agents. Never rotate Agent Type or identity to reset quota.
|
|
17
|
+
- Read `status` and `result` as facts, follow `instruction`, and execute at most the applicable `next.command`; use `recovery` only when it cannot continue.
|
|
18
|
+
- `next.command` is not unconditional. If the result satisfies the user's goal, present useful facts and stop; never dump the whole envelope.
|
|
19
19
|
|
|
20
20
|
## Bootstrap
|
|
21
21
|
|
|
@@ -28,31 +28,15 @@ itpay install <agent_type> --json
|
|
|
28
28
|
itpay --agent-type <agent_type> readyz --json
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Follow
|
|
32
|
-
|
|
33
|
-
Supported types are `codex-desktop`, `codex-cli`, `claude-code-desktop`, `claude-code-cli`, and `workbuddy`. State the real stable runtime type honestly. Do not identify a window, chat, task, process, or model session as a new Agent.
|
|
31
|
+
Follow the returned `next.command`. After typed `readyz`, read this complete Skill again, then continue to Catalog.
|
|
34
32
|
|
|
35
33
|
## Identity And Sessions
|
|
36
34
|
|
|
37
|
-
- One local Ed25519 private key represents this
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
- A revoked v2 device is not replaced automatically. It requires an explicit operator recovery path.
|
|
43
|
-
- If an operator confirms that one Backend registration database was reset, use `device recover --confirm-backend-reset` for that selected Backend only. This preserves the private key and every other Backend registration; never use it for ordinary session expiry or revocation.
|
|
44
|
-
- `--host` selects presentation. `--target` is only the destination chat/channel/open ID required by some Hosts. Neither is business input or identity.
|
|
45
|
-
|
|
46
|
-
## Envelope Rule
|
|
47
|
-
|
|
48
|
-
For every JSON response:
|
|
49
|
-
|
|
50
|
-
1. Read `status` and `result` as current facts.
|
|
51
|
-
2. Follow `instruction` when explaining or presenting those facts.
|
|
52
|
-
3. Execute at most the one `next.command`, filling only explicit placeholders or required user data.
|
|
53
|
-
4. Use `recovery` only when the normal next step cannot continue.
|
|
54
|
-
|
|
55
|
-
Do not print the whole envelope to the user. Return the useful result, a short explanation, and the next human action when needed.
|
|
35
|
+
- One local Ed25519 private key represents this installation. Never expose, copy, delete, or rotate it during normal recovery.
|
|
36
|
+
- `dev`, `test`, and `app` have separate Device registrations under the same key. Each registration has one Agent Instance per `agent_type`; same-type windows reuse it.
|
|
37
|
+
- Keep the returned `--agent-type` on every commerce command, or use one stable `ITPAY_AGENT_TYPE`. `--host` is presentation and `--target` is routing; neither is identity or business input.
|
|
38
|
+
- The CLI may renew a rejected/expired session and retry once. If it still fails or Device state is not writable, stop; do not loop, switch Node, edit locks, inspect credentials, or change identity.
|
|
39
|
+
- Use `device recover --confirm-backend-reset` only after an operator confirms that Backend was reset. It preserves the key and other Backend registrations.
|
|
56
40
|
|
|
57
41
|
## Golden Flow
|
|
58
42
|
|
|
@@ -61,29 +45,37 @@ itpay --agent-type <agent_type> catalog list --json
|
|
|
61
45
|
itpay --agent-type <agent_type> services start <service_id> --json
|
|
62
46
|
```
|
|
63
47
|
|
|
64
|
-
Then
|
|
48
|
+
Then follow each returned `next.command` on the same Service Execution.
|
|
65
49
|
|
|
66
|
-
|
|
50
|
+
- Put business input only in repeated `--input key=value` options. A keyword such as `美团` never belongs in `--target`.
|
|
51
|
+
- One independent service intent uses one Service Execution.
|
|
52
|
+
- Candidate lists belong to their source Execution. Ask the human to select a displayed rank, then submit it on that same Execution; never construct a candidate ID.
|
|
53
|
+
- Before a paid step, show the exact price, ask for required contact fields with their purpose, and wait for explicit human agreement. Never invent contact data.
|
|
54
|
+
- A normal single-Execution purchase uses the exact returned `services checkout` command.
|
|
55
|
+
- `services quote -> cart add --quote -> buy --cart` is only for a human who explicitly asks to combine Quotes from multiple independent Executions. It is not failure recovery.
|
|
67
56
|
|
|
68
|
-
|
|
57
|
+
## Checkout Handoff
|
|
69
58
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
-
|
|
59
|
+
When `status` is `human_checkout_required`, make the amount, ItPay Checkout QR, and `handoff.url` visible on the current human surface, then stop.
|
|
60
|
+
|
|
61
|
+
- Desktop Agents: send `handoff.markdown` unchanged; confirm QR, amount, and link are visible, then stop.
|
|
62
|
+
- CLI Agents: show the terminal QR, amount, and link in the watched terminal, then stop; never claim a desktop image was shown.
|
|
63
|
+
- WorkBuddy with `plain-chat`: when `handoff.qr_image_url` exists, use its complete value as the only `files` element in `present_files`; confirm the right-side QR preview opened, show amount and `handoff.url`, then stop. If it is absent, send amount and `handoff.url`, do not call `present_files`, then stop.
|
|
64
|
+
- If WorkBuddy `present_files` fails, send only `handoff.url`, report the failure, and stop. Never inspect files, switch Node, rebuild a QR, call `pay`, or create another Checkout.
|
|
65
|
+
- An explicit `--host` overrides presentation only. It never changes Agent identity or payment state.
|
|
66
|
+
|
|
67
|
+
Run `next.command` only after the human says they acted or asks for status. QR rendering, redirects, and human claims are not payment proof; only Backend Checkout or Order state is. Normal payment uses the Checkout page; `pay` and `buy --pay` are operator escape hatches, never recovery.
|
|
68
|
+
|
|
69
|
+
## Delivery And Refunds
|
|
70
|
+
|
|
71
|
+
- Agent-visible results come from `services next`; do not use `read-result` for them.
|
|
72
|
+
- Protected results require a current 15-minute human grant scoped to one delivery, approved fields, and frozen Agent audience.
|
|
73
|
+
- An Execution may have delivery history; follow `services next` for the Backend-selected current delivery.
|
|
74
|
+
- A pending refund locks delivery and revokes active grants. Follow the returned refund command and state.
|
|
83
75
|
|
|
84
76
|
## Recovery
|
|
85
77
|
|
|
86
|
-
Before creating anything again:
|
|
78
|
+
Before creating anything again, use only the applicable read/resume command:
|
|
87
79
|
|
|
88
80
|
```bash
|
|
89
81
|
itpay --agent-type <agent_type> next --json
|
|
@@ -92,16 +84,17 @@ itpay --agent-type <agent_type> services next <service_execution_id> --json
|
|
|
92
84
|
itpay --agent-type <agent_type> services checkout <service_execution_id> --resume --json
|
|
93
85
|
itpay --agent-type <agent_type> checkout --id <checkout_id> --token <display_token> --json
|
|
94
86
|
itpay --agent-type <agent_type> refund get <refund_request_id> --json
|
|
95
|
-
itpay --agent-type <agent_type> device recover --confirm-backend-reset --json
|
|
96
87
|
```
|
|
97
88
|
|
|
89
|
+
Reuse the same Execution and Checkout. Never start another Execution, create another Checkout, change payment route, or replay a capability to bypass quota, selection, payment, delivery, grant, or refund state.
|
|
90
|
+
|
|
98
91
|
## Safety
|
|
99
92
|
|
|
100
93
|
- Never invent service, capability, item, Checkout, Order, grant, or refund IDs.
|
|
101
94
|
- Never expose Provider credentials, raw payloads, display tokens as standalone chat data, Buyer bearer tokens, or Device private keys.
|
|
102
95
|
- Never bypass ownership, compatibility, quota, grant, or refund-lock errors.
|
|
103
96
|
- Do not use `services events` in a normal flow; it is a bounded redacted diagnostic command.
|
|
104
|
-
-
|
|
97
|
+
- Keep retries, sandbox diagnosis, and command translation out of the user response. Report useful progress, results, and genuine blockers.
|
|
105
98
|
|
|
106
99
|
## Built-In Help
|
|
107
100
|
|
|
@@ -112,4 +105,4 @@ itpay docs show <topic> --json
|
|
|
112
105
|
itpay skill show itpay-buyer --json
|
|
113
106
|
```
|
|
114
107
|
|
|
115
|
-
|
|
108
|
+
Normative command contracts are packaged under `docs/cli-reference`.
|