@itpay/cli 2.0.10 → 2.0.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 +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/guidance.js +14 -1
- package/dist/src/commands/install.js +1 -1
- package/dist/src/commands/pay.js +19 -8
- package/dist/src/commands/services.js +54 -35
- package/dist/src/main.js +18 -2
- package/dist/src/render/index.js +6 -4
- package/dist/src/state/config.js +3 -3
- package/docs/agent/buyer/install-and-setup.json +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 +2 -2
- package/docs/cli-reference/commands/pay.md +1 -1
- package/docs/cli-reference/commands/services/checkout.md +13 -5
- package/docs/cli-reference/commands/services/invoke.md +19 -0
- package/docs/cli-reference/commands/services/next.md +28 -1
- package/package.json +1 -1
- package/skills/itpay-buyer/SKILL.md +44 -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
|
|
14
|
+
The default API is `https://app.itpay.ai`. Set `ITPAY_BACKEND_URL` only for an intentional environment 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
|
+
}
|
|
@@ -404,7 +404,7 @@ function buildServiceGuidance(input) {
|
|
|
404
404
|
command: `itpay services checkout ${execution.service_execution_id} --capability ${capabilityID}${emailRequired ? " --email <email>" : ""} --json`,
|
|
405
405
|
requires_human: true,
|
|
406
406
|
reason: emailRequired
|
|
407
|
-
?
|
|
407
|
+
? deliveryEmailGuidance(checkoutCapability?.delivery_email_purpose)
|
|
408
408
|
: "This capability returns an agent-visible result after payment and does not require a delivery email.",
|
|
409
409
|
});
|
|
410
410
|
}
|
|
@@ -487,6 +487,7 @@ function buildServiceGuidance(input) {
|
|
|
487
487
|
requires_payment: capability.requires_payment,
|
|
488
488
|
vault_required: capability.vault_required,
|
|
489
489
|
delivery_email_required: capability.delivery_email_required,
|
|
490
|
+
delivery_email_purpose: capability.delivery_email_purpose,
|
|
490
491
|
price_amount_minor: capability.price_amount_minor,
|
|
491
492
|
price_currency: capability.price_currency,
|
|
492
493
|
free_quota_limit: capability.free_quota_limit,
|
|
@@ -506,6 +507,18 @@ function buildServiceGuidance(input) {
|
|
|
506
507
|
: {}),
|
|
507
508
|
};
|
|
508
509
|
}
|
|
510
|
+
function deliveryEmailGuidance(purpose) {
|
|
511
|
+
switch (purpose) {
|
|
512
|
+
case "receipt":
|
|
513
|
+
return "Ask the human for their email. It is used to send the order receipt; never invent or substitute an address.";
|
|
514
|
+
case "claim":
|
|
515
|
+
return "Ask the human for their email. It is used to send the protected result claim link; never invent or substitute an address.";
|
|
516
|
+
case "receipt_and_claim":
|
|
517
|
+
return "Ask the human for their email. It is used to send the order receipt and protected result claim link; never invent or substitute an address.";
|
|
518
|
+
default:
|
|
519
|
+
return "Ask the human for the required email and state only the Backend-declared purpose; never invent or substitute an address.";
|
|
520
|
+
}
|
|
521
|
+
}
|
|
509
522
|
function firstPrePurchaseCapability(capabilities) {
|
|
510
523
|
return capabilities.find((capability) => capability.agent_visible && !capability.requires_payment)
|
|
511
524
|
?? capabilities.find((capability) => capability.agent_visible);
|
|
@@ -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";
|
|
@@ -127,7 +128,7 @@ function invokedEnvelope(response, requestedCapability, capabilities, input) {
|
|
|
127
128
|
delivery_email_required: checkoutCapability.delivery_email_required,
|
|
128
129
|
};
|
|
129
130
|
const price = capabilityPrice(checkoutCapability);
|
|
130
|
-
instruction = purchaseConfirmationInstruction("quota_exhausted", price, checkoutCapability.delivery_email_required);
|
|
131
|
+
instruction = purchaseConfirmationInstruction("quota_exhausted", price, checkoutCapability.delivery_email_required, checkoutCapability.delivery_email_purpose);
|
|
131
132
|
next = {
|
|
132
133
|
command: checkoutCommand(response.execution.service_execution_id, checkoutCapability, input),
|
|
133
134
|
reason: `仅在用户明确同意支付 ${price} 后执行;否则停止`,
|
|
@@ -197,17 +198,30 @@ function capabilityPrice(capability) {
|
|
|
197
198
|
? formatMoney(capability.price_amount_minor, capability.price_currency)
|
|
198
199
|
: "当前发布价格";
|
|
199
200
|
}
|
|
200
|
-
function purchaseConfirmationInstruction(context, price, deliveryEmailRequired, candidateTitle = "") {
|
|
201
|
+
function purchaseConfirmationInstruction(context, price, deliveryEmailRequired, deliveryEmailPurpose, candidateTitle = "") {
|
|
202
|
+
const emailPurpose = deliveryEmailPurposeText(deliveryEmailPurpose);
|
|
201
203
|
if (context === "quota_exhausted") {
|
|
202
204
|
return deliveryEmailRequired
|
|
203
|
-
? `免费额度已用完,本次没有调用 Provider,也尚未创建 Quote 或 Checkout。现在只向用户说明:继续当前请求需要支付 ${price}
|
|
205
|
+
? `免费额度已用完,本次没有调用 Provider,也尚未创建 Quote 或 Checkout。现在只向用户说明:继续当前请求需要支付 ${price},并提供${emailPurpose};请确认是否购买并提供邮箱。然后停止并等待。用户明确同意并提供真实邮箱前,不要执行 next.command,不要新建 Execution,不要尝试其他 capability、quote、cart、buy、checkout 或 pay 命令。`
|
|
204
206
|
: `免费额度已用完,本次没有调用 Provider,也尚未创建 Quote 或 Checkout。现在只向用户说明:“继续当前请求需要支付 ${price},是否购买?”然后停止并等待用户明确回复。用户明确同意前,不要执行 next.command,不要新建 Execution,不要尝试其他 capability、quote、cart、buy、checkout 或 pay 命令。`;
|
|
205
207
|
}
|
|
206
208
|
const selected = candidateTitle ? `已选择 ${candidateTitle}。` : "当前候选已经确认。";
|
|
207
209
|
return deliveryEmailRequired
|
|
208
|
-
? `${selected}候选已绑定到当前 Execution,但尚未购买后续服务。现在只向用户说明:继续购买后续服务需要支付 ${price}
|
|
210
|
+
? `${selected}候选已绑定到当前 Execution,但尚未购买后续服务。现在只向用户说明:继续购买后续服务需要支付 ${price},并提供${emailPurpose};请确认是否购买并提供邮箱。然后停止。用户明确同意并提供真实邮箱前,不要执行 next.command,不要创建新 Execution 或 Checkout。`
|
|
209
211
|
: `${selected}候选已绑定到当前 Execution,但尚未购买后续服务。现在只向用户说明:“继续购买后续服务需要支付 ${price},是否购买?”然后停止。用户明确同意前,不要执行 next.command,不要创建新 Execution 或 Checkout。`;
|
|
210
212
|
}
|
|
213
|
+
function deliveryEmailPurposeText(purpose) {
|
|
214
|
+
switch (purpose) {
|
|
215
|
+
case "receipt":
|
|
216
|
+
return "用于发送订单收据的真实邮箱";
|
|
217
|
+
case "claim":
|
|
218
|
+
return "用于发送交付认领链接的真实邮箱";
|
|
219
|
+
case "receipt_and_claim":
|
|
220
|
+
return "用于发送订单收据和交付认领链接的真实邮箱";
|
|
221
|
+
default:
|
|
222
|
+
return "服务端声明用途的真实邮箱";
|
|
223
|
+
}
|
|
224
|
+
}
|
|
211
225
|
function paidContinuation(model, action, input) {
|
|
212
226
|
if (!action.capability_id)
|
|
213
227
|
return null;
|
|
@@ -225,6 +239,7 @@ function paidContinuation(model, action, input) {
|
|
|
225
239
|
price: { amount_minor: capability.price_amount_minor, currency: capability.price_currency },
|
|
226
240
|
} : {}),
|
|
227
241
|
delivery_email_required: capability.delivery_email_required,
|
|
242
|
+
...(capability.delivery_email_purpose ? { delivery_email_purpose: capability.delivery_email_purpose } : {}),
|
|
228
243
|
},
|
|
229
244
|
next: {
|
|
230
245
|
command: checkoutCommand(model.execution.service_execution_id, capability, input, !stateBacked),
|
|
@@ -287,7 +302,7 @@ export async function runServicesAction(backend, serviceExecutionID, actionType,
|
|
|
287
302
|
...(continuation ? { checkout: continuation.checkout } : {}),
|
|
288
303
|
},
|
|
289
304
|
instruction: continuation
|
|
290
|
-
? purchaseConfirmationInstruction("candidate_selected", continuation.price, continuation.capability.delivery_email_required, selection.title)
|
|
305
|
+
? purchaseConfirmationInstruction("candidate_selected", continuation.price, continuation.capability.delivery_email_required, continuation.capability.delivery_email_purpose, selection.title)
|
|
291
306
|
: "候选已绑定到来源 Execution;后续动作必须继续使用该 Execution。",
|
|
292
307
|
next,
|
|
293
308
|
recovery: [{
|
|
@@ -398,6 +413,7 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
398
413
|
currency: item.currency,
|
|
399
414
|
})),
|
|
400
415
|
orderCurrency: checkout.checkout.currency,
|
|
416
|
+
...(options.agentType ? { agentType: options.agentType } : {}),
|
|
401
417
|
});
|
|
402
418
|
options.persistHandoff?.({
|
|
403
419
|
serviceExecutionID,
|
|
@@ -419,12 +435,14 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
419
435
|
});
|
|
420
436
|
return;
|
|
421
437
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
438
|
+
if (shouldPrepareLocalCheckoutImage(platform)) {
|
|
439
|
+
await ensureIdeImageAttach(plan, {
|
|
440
|
+
enabled: config.ideImageAttach,
|
|
441
|
+
...(config.baseURL ? { baseURL: config.baseURL } : {}),
|
|
442
|
+
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
const envelope = buildServicesCheckoutEnvelope(response, checkoutURL, plan, config.baseURL, options.agentType);
|
|
428
446
|
const plainResult = [
|
|
429
447
|
`service_execution_id: ${response.binding.service_execution_id}`,
|
|
430
448
|
`checkout_id: ${checkoutID}`,
|
|
@@ -688,19 +706,26 @@ function servicesNextEnvelope(model) {
|
|
|
688
706
|
if (deliveryMode === "vault_artifact") {
|
|
689
707
|
const grantStatus = normalizeGrantStatus(delivery?.grant_status);
|
|
690
708
|
const grantActive = grantStatus === "active";
|
|
709
|
+
const grantPending = grantStatus === "pending";
|
|
691
710
|
return {
|
|
692
|
-
status: grantActive ? "grant_active" : "human_authorization_required",
|
|
711
|
+
status: grantActive ? "grant_active" : grantPending ? "result_preparing" : "human_authorization_required",
|
|
693
712
|
result: {
|
|
694
713
|
service_execution_id: execution.service_execution_id,
|
|
695
714
|
...(delivery?.capability_id ? { capability_id: delivery.capability_id } : {}),
|
|
696
715
|
delivery_mode: deliveryMode,
|
|
697
716
|
grant_status: grantStatus,
|
|
717
|
+
...(delivery?.preparation ? { preparation: delivery.preparation } : {}),
|
|
698
718
|
...(grantActive && delivery?.grant_expires_at ? { grant_expires_at: delivery.grant_expires_at } : {}),
|
|
699
719
|
},
|
|
700
720
|
instruction: grantActive
|
|
701
721
|
? "这是当前 Graph 步骤对应的交付;用户授权有效,立即读取并遵守字段范围与到期时间。"
|
|
702
|
-
:
|
|
703
|
-
|
|
722
|
+
: grantPending
|
|
723
|
+
? "用户已经完成授权,服务端正在按已发布执行图准备交付内容。不要再次付款、再次授权、新建 Execution 或调用 read-result;只执行 next.command 查询同一 Execution。"
|
|
724
|
+
: "这是当前 Graph 步骤对应的交付;请用户在订单页面授权,未授权前不要读取或猜测内容。",
|
|
725
|
+
next: grantPending ? {
|
|
726
|
+
command: `itpay services next ${execution.service_execution_id} --json`,
|
|
727
|
+
reason: "等待同一 Execution 的交付准备完成",
|
|
728
|
+
} : {
|
|
704
729
|
command: `itpay services read-result ${execution.service_execution_id} --json`,
|
|
705
730
|
reason: grantActive ? "读取当前有效 grant 的结果" : "仅在用户确认授权后执行",
|
|
706
731
|
},
|
|
@@ -720,7 +745,7 @@ function servicesNextEnvelope(model) {
|
|
|
720
745
|
phase: execution.phase,
|
|
721
746
|
checkout: continuation.checkout,
|
|
722
747
|
},
|
|
723
|
-
instruction: purchaseConfirmationInstruction(execution.status === "quota_exhausted" ? "quota_exhausted" : "candidate_selected", continuation.price, continuation.capability.delivery_email_required),
|
|
748
|
+
instruction: purchaseConfirmationInstruction(execution.status === "quota_exhausted" ? "quota_exhausted" : "candidate_selected", continuation.price, continuation.capability.delivery_email_required, continuation.capability.delivery_email_purpose),
|
|
724
749
|
next: continuation.next,
|
|
725
750
|
recovery: [],
|
|
726
751
|
};
|
|
@@ -898,20 +923,21 @@ function parseValue(value) {
|
|
|
898
923
|
return Number(value);
|
|
899
924
|
return value;
|
|
900
925
|
}
|
|
901
|
-
function buildServicesCheckoutEnvelope(response, checkoutURL, plan, baseURL) {
|
|
926
|
+
function buildServicesCheckoutEnvelope(response, checkoutURL, plan, baseURL, agentType) {
|
|
902
927
|
const checkout = response.checkout;
|
|
903
928
|
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
929
|
const amount = formatMoney(checkout.checkout.amount_minor, checkout.checkout.currency);
|
|
930
|
+
const presentationHandoff = buildCheckoutHandoff({
|
|
931
|
+
platform,
|
|
932
|
+
url: checkoutURL,
|
|
933
|
+
amount,
|
|
934
|
+
...(agentType ? { agentType } : {}),
|
|
935
|
+
...(checkout.qr_png_url ? { qrImageURL: absolutePublicURL(baseURL, checkout.qr_png_url) } : {}),
|
|
936
|
+
...(plan.ideImageAttach?.status === "downloaded" && plan.ideImageAttach.localPath
|
|
937
|
+
? { localPath: plan.ideImageAttach.localPath }
|
|
938
|
+
: {}),
|
|
939
|
+
...(platform === "markdown" ? { markdown: buildAgentChatHandoff(plan).markdown } : {}),
|
|
940
|
+
});
|
|
915
941
|
return {
|
|
916
942
|
status: "human_checkout_required",
|
|
917
943
|
result: {
|
|
@@ -921,8 +947,8 @@ function buildServicesCheckoutEnvelope(response, checkoutURL, plan, baseURL) {
|
|
|
921
947
|
locked_input: response.locked_input,
|
|
922
948
|
amount,
|
|
923
949
|
},
|
|
924
|
-
handoff,
|
|
925
|
-
instruction:
|
|
950
|
+
handoff: presentationHandoff.handoff,
|
|
951
|
+
instruction: presentationHandoff.instruction,
|
|
926
952
|
next: {
|
|
927
953
|
command: `itpay checkout --id ${checkout.checkout.checkout_id} --token ${checkout.display_token} --json`,
|
|
928
954
|
reason: "仅在用户完成付款操作或要求查询后,读取同一 Checkout 的权威状态",
|
|
@@ -933,13 +959,6 @@ function buildServicesCheckoutEnvelope(response, checkoutURL, plan, baseURL) {
|
|
|
933
959
|
function checkoutCapabilityID(response, fallback = "") {
|
|
934
960
|
return response.capability_id || fallback;
|
|
935
961
|
}
|
|
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
962
|
function absolutePublicURL(baseURL, value) {
|
|
944
963
|
try {
|
|
945
964
|
return new URL(value, baseURL.endsWith("/") ? baseURL : `${baseURL}/`).toString();
|
package/dist/src/main.js
CHANGED
|
@@ -96,6 +96,10 @@ function reportCLIError(error, contract) {
|
|
|
96
96
|
error.code === "platform_release_unavailable" ||
|
|
97
97
|
(error.status === 404 && error.code === "unknown_error"));
|
|
98
98
|
const backendInternal = error instanceof HttpError && error.status === 500 && error.code === "internal_error";
|
|
99
|
+
const providerConnectionUnavailable = error instanceof HttpError && error.code === "provider_connection_unavailable";
|
|
100
|
+
const providerTemporary = error instanceof HttpError && error.code === "provider_temporarily_unavailable";
|
|
101
|
+
const providerRejected = error instanceof HttpError && error.code === "provider_rejected";
|
|
102
|
+
const capabilityInputInvalid = error instanceof HttpError && error.code === "capability_input_invalid";
|
|
99
103
|
const deviceRecovery = deviceError ? [{
|
|
100
104
|
command: "itpay skill show itpay-buyer --json",
|
|
101
105
|
reason: "读取身份边界;该错误需要用户或运营恢复 Backend 登记,不能通过换类型或删除本地身份绕过",
|
|
@@ -124,9 +128,17 @@ function reportCLIError(error, contract) {
|
|
|
124
128
|
? "立即向用户报告 error.message 并结束本次任务。不要运行任何其他 itpay、npm、which、device、docs、cart、orders 或 services 命令;不要寻找、安装或切换其他 CLI。只有运营明确提供兼容 CLI 后,才能在新的任务中重新开始。"
|
|
125
129
|
: backendInternal
|
|
126
130
|
? "Backend 内部故障;立即停止并向用户报告。不要重试、检查或删除 Device 身份、创建替代 Execution、切换 Backend,或尝试 quote、checkout、cart、buy、pay 等付费路径。"
|
|
127
|
-
:
|
|
131
|
+
: providerConnectionUnavailable
|
|
132
|
+
? "Provider 请求未发出,预留免费额度已释放;当前 Execution 已失败。立即向用户报告 error.message 并停止,不要自动重试、不要继续同一 Execution,也不要进入任何付费路径。只有运营确认连接恢复且用户明确要求重新查询后,才启动新的 Service Execution。"
|
|
133
|
+
: providerTemporary
|
|
134
|
+
? "上游服务暂时不可用;向用户逐字报告 error.message 并停止,不要自动重试。请求是否可能计费以 Backend 的消费事实为准;只有用户明确要求再次查询后,才可按 next/recovery 重新开始。"
|
|
135
|
+
: providerRejected
|
|
136
|
+
? "上游拒绝了本次输入;向用户逐字报告 error.message 并停止,不要重试同一 Execution。Provider 已收到响应,本次请求可能计费;只有用户提供修正后的输入后,才可按 next/recovery 继续。"
|
|
137
|
+
: capabilityInputInvalid
|
|
138
|
+
? "输入未通过本地校验,上游尚未被调用且用户额度未变化。向用户逐字报告 error.message 并停止,不要原样重试或运行其他恢复命令。用户提供修正后的输入后,继续使用当前未结束的 Execution。"
|
|
139
|
+
: commandError?.instruction ?? authorizationInstruction ?? contract?.instruction ?? "检查命令参数后重试。",
|
|
128
140
|
next: null,
|
|
129
|
-
recovery: incompatible || backendInternal ? [] : commandError?.recovery ?? (stateError ? stateRecovery : deviceError ? deviceRecovery : identityRecovery ? httpRecovery : contract?.recovery ?? []),
|
|
141
|
+
recovery: incompatible || backendInternal || providerConnectionUnavailable || providerTemporary || providerRejected || capabilityInputInvalid ? [] : commandError?.recovery ?? (stateError ? stateRecovery : deviceError ? deviceRecovery : identityRecovery ? httpRecovery : contract?.recovery ?? []),
|
|
130
142
|
}, {
|
|
131
143
|
...(contract?.jsonOutput !== undefined ? { jsonOutput: contract.jsonOutput } : {}),
|
|
132
144
|
output: (text) => { process.stderr.write(text); },
|
|
@@ -668,6 +680,7 @@ program
|
|
|
668
680
|
const buyOptions = {
|
|
669
681
|
cartSession: session,
|
|
670
682
|
host,
|
|
683
|
+
...(config.agentType ? { agentType: config.agentType } : {}),
|
|
671
684
|
...(options.cart ? { cartID: options.cart } : {}),
|
|
672
685
|
...(options.target ? { target: options.target } : {}),
|
|
673
686
|
...(options.ref ? { clientReferenceID: options.ref } : {}),
|
|
@@ -720,6 +733,7 @@ program
|
|
|
720
733
|
checkoutID,
|
|
721
734
|
displayToken,
|
|
722
735
|
host,
|
|
736
|
+
...(config.agentType ? { agentType: config.agentType } : {}),
|
|
723
737
|
baseURL: config.baseURL,
|
|
724
738
|
jsonOutput: Boolean(options.json),
|
|
725
739
|
});
|
|
@@ -777,6 +791,7 @@ program
|
|
|
777
791
|
displayToken,
|
|
778
792
|
method: options.method,
|
|
779
793
|
host,
|
|
794
|
+
...(config.agentType ? { agentType: config.agentType } : {}),
|
|
780
795
|
...(options.refresh ? { refreshAction: true } : {}),
|
|
781
796
|
...(jsonOutput ? { jsonOutput: true } : {}),
|
|
782
797
|
});
|
|
@@ -1098,6 +1113,7 @@ services
|
|
|
1098
1113
|
lockedInput: parseKeyValueList(options.input),
|
|
1099
1114
|
resume: Boolean(options.resume),
|
|
1100
1115
|
host: withHost(options.host ?? defaultHostForAgentType(config.agentType)),
|
|
1116
|
+
...(config.agentType ? { agentType: config.agentType } : {}),
|
|
1101
1117
|
...(options.target ? { target: options.target } : {}),
|
|
1102
1118
|
...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
|
|
1103
1119
|
...(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
|
@@ -9,9 +9,9 @@ import { BackendClient } from "../client/backend.js";
|
|
|
9
9
|
import { declaredAgentType } from "./agent_type.js";
|
|
10
10
|
import { DeviceAuthority } from "./device_authority.js";
|
|
11
11
|
import { OperationJournal } from "./operation_journal.js";
|
|
12
|
-
export const DEFAULT_BASE_URL = "https://
|
|
13
|
-
export const CLI_VERSION = "2.0.
|
|
14
|
-
export const API_CONTRACT_REVISION = "sha256:
|
|
12
|
+
export const DEFAULT_BASE_URL = "https://app.itpay.ai";
|
|
13
|
+
export const CLI_VERSION = "2.0.12";
|
|
14
|
+
export const API_CONTRACT_REVISION = "sha256:ea7d716f444a01e9d0109f12a53b56ce16d5ab813795a18a7a63f24a8c752073";
|
|
15
15
|
const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
|
|
16
16
|
const CART_SESSION_FILENAME = "cart.json";
|
|
17
17
|
const OPERATION_JOURNAL_FILENAME = "operations.json";
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
],
|
|
33
33
|
"agent_rules": [
|
|
34
34
|
"Install with npm install -g @itpay/cli.",
|
|
35
|
-
"Use the default https://
|
|
35
|
+
"Use the default https://app.itpay.ai API unless an environment override is deliberate.",
|
|
36
36
|
"Use one exact type: codex-desktop, codex-cli, claude-code-desktop, claude-code-cli, or workbuddy.",
|
|
37
37
|
"One local private key is reused, but each exact Backend API base URL has its own server registration and quota lineage.",
|
|
38
38
|
"Within one Backend registration, each Agent Type has one Agent Instance; all windows and chats of the same type reuse it.",
|
|
@@ -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 都只返回同一状态和下一步,不渲染二维码。
|
|
@@ -29,7 +29,7 @@ itpay install [target] [--json]
|
|
|
29
29
|
"result": {
|
|
30
30
|
"agent_type": "codex-desktop",
|
|
31
31
|
"default_host": "codex",
|
|
32
|
-
"default_api": "https://
|
|
32
|
+
"default_api": "https://app.itpay.ai",
|
|
33
33
|
"install_command": "npm install -g @itpay/cli"
|
|
34
34
|
},
|
|
35
35
|
"instruction": "在 Codex Desktop 中始终传这个 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`,然后遵守相同停止和付款证明规则。
|
|
@@ -62,6 +62,25 @@ itpay services invoke <service_execution_id> --capability <capability_id>
|
|
|
62
62
|
|
|
63
63
|
缺少 required input 时返回 `capability_input_invalid`,recovery 给出带占位符的同一 invoke 命令;CLI 和 Backend 都必须在 Provider 调用前拒绝,Backend 还必须在 execution/event/quota/invocation 写入前拒绝。错误调用付费 capability 时不得给出购买旁路,只能回到同一 Execution 的 `services next`;execution 状态、event、ProviderCalled 均保持不变。
|
|
64
64
|
|
|
65
|
+
## Provider 请求前连接失败
|
|
66
|
+
|
|
67
|
+
如果 Backend 能确认请求未发出,返回固定的终态错误,不暴露 DNS、IP、Provider URL 或凭证诊断:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"status": "error",
|
|
72
|
+
"error": {
|
|
73
|
+
"code": "provider_connection_unavailable",
|
|
74
|
+
"message": "provider request was not sent; reserved quota was released"
|
|
75
|
+
},
|
|
76
|
+
"instruction": "Provider 请求未发出,预留免费额度已释放;当前 Execution 已失败。立即向用户报告 error.message 并停止,不要自动重试、不要继续同一 Execution,也不要进入任何付费路径。只有运营确认连接恢复且用户明确要求重新查询后,才启动新的 Service Execution。",
|
|
77
|
+
"next": null,
|
|
78
|
+
"recovery": []
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
该终态不允许 CLI 猜测网络修复、重复 invoke 或转入购买。连接恢复后也不能复用失败 Execution;必须同时满足“运营已确认恢复”和“用户明确要求再次查询”,才创建新 Execution。
|
|
83
|
+
|
|
65
84
|
## Agent Type / Host
|
|
66
85
|
|
|
67
86
|
`codex-desktop`、`codex-cli`、`claude-code-desktop`、`claude-code-cli`、`workbuddy` 五种 Agent Type 的 safe result 一致。instruction 可以适配对话表述,但不得隐藏 quota、价格或 schema 错误。
|
|
@@ -65,7 +65,7 @@ itpay services next <service_execution_id> [--json]
|
|
|
65
65
|
}
|
|
66
66
|
]
|
|
67
67
|
},
|
|
68
|
-
"instruction": "
|
|
68
|
+
"instruction": "付费 Agent-visible 搜索已完成。现在把 items 中的编号、title 和 safe_payload 展示给用户,然后停止。本结果是 agent-visible,不要调用 read-result。若用户的目标只是候选搜索,任务已经完成;只有用户之后明确选择某个候选并要求继续时,才执行 next.command。不要自动购买后续报告。",
|
|
69
69
|
"next": {
|
|
70
70
|
"command": "itpay services action <id> --action select_candidate --actor-type human --status approved --candidate <rank> --json",
|
|
71
71
|
"reason": "仅在用户明确选择候选并要求继续时执行"
|
|
@@ -98,6 +98,33 @@ itpay services next <service_execution_id> [--json]
|
|
|
98
98
|
}
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
用户已经授权、但服务端仍在按已发布执行图准备 Vault 交付时,必须只轮询同一 Execution:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"status": "result_preparing",
|
|
106
|
+
"result": {
|
|
107
|
+
"service_execution_id": "<id>",
|
|
108
|
+
"capability_id": "<capability_id>",
|
|
109
|
+
"delivery_mode": "vault_artifact",
|
|
110
|
+
"grant_status": "pending",
|
|
111
|
+
"preparation": {
|
|
112
|
+
"status": "running",
|
|
113
|
+
"total_nodes": 4,
|
|
114
|
+
"completed_nodes": 2,
|
|
115
|
+
"succeeded_nodes": 2,
|
|
116
|
+
"failed_nodes": 0
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"instruction": "用户已经完成授权,服务端正在按已发布执行图准备交付内容。不要再次付款、再次授权、新建 Execution 或调用 read-result;只执行 next.command 查询同一 Execution。",
|
|
120
|
+
"next": {
|
|
121
|
+
"command": "itpay services next <id> --json",
|
|
122
|
+
"reason": "等待同一 Execution 的交付准备完成"
|
|
123
|
+
},
|
|
124
|
+
"recovery": []
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
101
128
|
有效 grant 存在时:
|
|
102
129
|
|
|
103
130
|
```json
|
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,38 @@ 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
|
+
- If `services next` returns `result_preparing`, authorization is already complete. Run only its same-Execution `next.command`; do not pay, authorize, start, or call `read-result` again.
|
|
74
|
+
- An Execution may have delivery history; follow `services next` for the Backend-selected current delivery.
|
|
75
|
+
- A pending refund locks delivery and revokes active grants. Follow the returned refund command and state.
|
|
83
76
|
|
|
84
77
|
## Recovery
|
|
85
78
|
|
|
86
|
-
Before creating anything again:
|
|
79
|
+
Before creating anything again, use only the applicable read/resume command:
|
|
87
80
|
|
|
88
81
|
```bash
|
|
89
82
|
itpay --agent-type <agent_type> next --json
|
|
@@ -92,16 +85,19 @@ itpay --agent-type <agent_type> services next <service_execution_id> --json
|
|
|
92
85
|
itpay --agent-type <agent_type> services checkout <service_execution_id> --resume --json
|
|
93
86
|
itpay --agent-type <agent_type> checkout --id <checkout_id> --token <display_token> --json
|
|
94
87
|
itpay --agent-type <agent_type> refund get <refund_request_id> --json
|
|
95
|
-
itpay --agent-type <agent_type> device recover --confirm-backend-reset --json
|
|
96
88
|
```
|
|
97
89
|
|
|
90
|
+
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.
|
|
91
|
+
|
|
92
|
+
`provider_connection_unavailable` is a terminal exception: Backend confirms no Provider request was sent and releases the reservation, then fails that Execution. Stop with no recovery command. Only after an operator confirms connectivity is restored and the human explicitly asks to query again may you start a new Execution.
|
|
93
|
+
|
|
98
94
|
## Safety
|
|
99
95
|
|
|
100
96
|
- Never invent service, capability, item, Checkout, Order, grant, or refund IDs.
|
|
101
97
|
- Never expose Provider credentials, raw payloads, display tokens as standalone chat data, Buyer bearer tokens, or Device private keys.
|
|
102
98
|
- Never bypass ownership, compatibility, quota, grant, or refund-lock errors.
|
|
103
99
|
- Do not use `services events` in a normal flow; it is a bounded redacted diagnostic command.
|
|
104
|
-
-
|
|
100
|
+
- Keep retries, sandbox diagnosis, and command translation out of the user response. Report useful progress, results, and genuine blockers.
|
|
105
101
|
|
|
106
102
|
## Built-In Help
|
|
107
103
|
|
|
@@ -112,4 +108,4 @@ itpay docs show <topic> --json
|
|
|
112
108
|
itpay skill show itpay-buyer --json
|
|
113
109
|
```
|
|
114
110
|
|
|
115
|
-
|
|
111
|
+
Normative command contracts are packaged under `docs/cli-reference`.
|