@itpay/cli 2.0.3 → 2.0.7
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 +96 -142
- package/dist/src/client/backend.js +26 -8
- package/dist/src/client/http.js +29 -23
- package/dist/src/commands/buy.js +84 -132
- package/dist/src/commands/cart.js +274 -169
- package/dist/src/commands/catalog.js +64 -38
- package/dist/src/commands/checkout.js +128 -79
- package/dist/src/commands/docs.js +97 -51
- package/dist/src/commands/guidance.js +112 -16
- package/dist/src/commands/install.js +50 -87
- package/dist/src/commands/next.js +45 -0
- package/dist/src/commands/order.js +44 -69
- package/dist/src/commands/orders.js +43 -15
- package/dist/src/commands/pay.js +51 -22
- package/dist/src/commands/readyz.js +8 -4
- package/dist/src/commands/refund.js +132 -11
- package/dist/src/commands/services.js +799 -147
- package/dist/src/commands/skill.js +55 -0
- package/dist/src/main.js +820 -193
- package/dist/src/render/output.js +2 -3
- package/dist/src/state/agent_type.js +19 -0
- package/dist/src/state/cart_session.js +13 -17
- package/dist/src/state/client_context.js +4 -2
- package/dist/src/state/config.js +5 -15
- package/dist/src/state/device_authority.js +175 -57
- package/docs/agent/buyer/cart-checkout.json +27 -83
- package/docs/agent/buyer/catalog-list.json +2 -1
- package/docs/agent/buyer/identity-and-sessions.json +64 -0
- package/docs/agent/buyer/install-and-setup.json +35 -65
- package/docs/agent/buyer/orders-refunds.json +31 -53
- package/docs/agent/buyer/payment-flow.json +28 -57
- package/docs/agent/buyer/quickstart.json +46 -161
- package/docs/agent/buyer/render-hosts.json +43 -57
- package/docs/cli-reference/agent-types.md +51 -0
- package/docs/cli-reference/commands/buy.md +167 -0
- package/docs/cli-reference/commands/cart/add.md +86 -0
- package/docs/cli-reference/commands/cart/clear.md +53 -0
- package/docs/cli-reference/commands/cart/index.md +30 -0
- package/docs/cli-reference/commands/cart/next.md +71 -0
- package/docs/cli-reference/commands/cart/remove.md +53 -0
- package/docs/cli-reference/commands/cart/show.md +65 -0
- package/docs/cli-reference/commands/catalog/index.md +26 -0
- package/docs/cli-reference/commands/catalog/list.md +45 -0
- package/docs/cli-reference/commands/checkout.md +74 -0
- package/docs/cli-reference/commands/device.md +13 -0
- package/docs/cli-reference/commands/docs/index.md +28 -0
- package/docs/cli-reference/commands/docs/list.md +51 -0
- package/docs/cli-reference/commands/docs/search.md +69 -0
- package/docs/cli-reference/commands/docs/show.md +68 -0
- package/docs/cli-reference/commands/install.md +114 -0
- package/docs/cli-reference/commands/next.md +87 -0
- package/docs/cli-reference/commands/order.md +92 -0
- package/docs/cli-reference/commands/orders.md +83 -0
- package/docs/cli-reference/commands/pay.md +103 -0
- package/docs/cli-reference/commands/readyz.md +38 -0
- package/docs/cli-reference/commands/refund/cancel.md +62 -0
- package/docs/cli-reference/commands/refund/create.md +85 -0
- package/docs/cli-reference/commands/refund/get.md +60 -0
- package/docs/cli-reference/commands/refund/index.md +33 -0
- package/docs/cli-reference/commands/refund/list.md +68 -0
- package/docs/cli-reference/commands/refund/watch.md +73 -0
- package/docs/cli-reference/commands/services/action.md +48 -0
- package/docs/cli-reference/commands/services/checkout.md +82 -0
- package/docs/cli-reference/commands/services/events.md +73 -0
- package/docs/cli-reference/commands/services/get.md +66 -0
- package/docs/cli-reference/commands/services/index.md +45 -0
- package/docs/cli-reference/commands/services/invoke.md +67 -0
- package/docs/cli-reference/commands/services/list.md +61 -0
- package/docs/cli-reference/commands/services/next.md +181 -0
- package/docs/cli-reference/commands/services/quote.md +63 -0
- package/docs/cli-reference/commands/services/read-result.md +98 -0
- package/docs/cli-reference/commands/services/start.md +55 -0
- package/docs/cli-reference/commands/skill.md +17 -0
- package/docs/cli-reference/conventions.md +97 -0
- package/docs/cli-reference/index.md +65 -0
- package/package.json +1 -1
- package/skills/itpay-buyer/SKILL.md +71 -110
package/dist/src/commands/buy.js
CHANGED
|
@@ -11,54 +11,55 @@
|
|
|
11
11
|
import { operationID } from "../state/config.js";
|
|
12
12
|
import { validateContext } from "../state/client_context.js";
|
|
13
13
|
import { dispatchRender } from "../render/index.js";
|
|
14
|
-
import {
|
|
15
|
-
import { ensureIdeImageAttach
|
|
14
|
+
import { platformKeyForHost } from "../render/plan.js";
|
|
15
|
+
import { ensureIdeImageAttach } from "../render/ide.js";
|
|
16
16
|
import { buildAgentChatHandoff } from "../render/markdown.js";
|
|
17
|
+
import { formatMoney } from "../render/output.js";
|
|
18
|
+
import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
|
|
17
19
|
export async function runBuy(backend, config, options) {
|
|
18
20
|
const err = validateContext(options.host, options.target);
|
|
19
21
|
if (err) {
|
|
20
22
|
throw new Error(`${err.code}: ${err.message}`);
|
|
21
23
|
}
|
|
22
24
|
const snap = options.cartSession.show();
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
const resumableCartID = !options.cartID && snap.items.length === 0 ? snap.lastCartID : undefined;
|
|
26
|
+
if (!options.cartID && !resumableCartID && snap.items.length === 0) {
|
|
27
|
+
throw new CommandContractError("cart_empty", "no local draft or canonical cart is available", "没有可购买的普通 Cart;从已发布目录选择项目,不要猜测 item、variant 或 offer。", [{ command: "itpay catalog list --json", reason: "读取已发布项目" }]);
|
|
25
28
|
}
|
|
26
29
|
const missingContactFields = findMissingContactFields(options.contact, options.requiredContactFields ?? []);
|
|
27
30
|
if (missingContactFields.length > 0) {
|
|
28
|
-
|
|
29
|
-
await dispatchInteractionRequest(options.host, interactionRequest, {
|
|
30
|
-
...(options.isTTY !== undefined ? { isTTY: options.isTTY } : {}),
|
|
31
|
-
...(options.target ? { target: options.target } : {}),
|
|
32
|
-
...(options.output ? { output: options.output } : {}),
|
|
33
|
-
});
|
|
34
|
-
return {
|
|
35
|
-
kind: "interaction_requested",
|
|
36
|
-
interactionRequest,
|
|
37
|
-
};
|
|
31
|
+
throw new CommandContractError("missing_contact", `missing required contact fields: ${missingContactFields.join(", ")}`, `向用户询问 ${missingContactFields.join(" 和 ")},然后在同一 buy 命令补充对应 contact 参数;禁止编造。`, [{ command: "itpay buy --help", reason: "查看 contact 参数" }]);
|
|
38
32
|
}
|
|
39
33
|
let cart;
|
|
40
|
-
if (options.cartID) {
|
|
41
|
-
cart = await backend.getCart(options.cartID);
|
|
34
|
+
if (options.cartID || resumableCartID) {
|
|
35
|
+
cart = await backend.getCart(options.cartID ?? resumableCartID);
|
|
42
36
|
}
|
|
43
37
|
else {
|
|
44
38
|
const request = options.cartSession.toCreateCartRequest();
|
|
45
39
|
request.client_context = {
|
|
46
40
|
host: options.host,
|
|
47
41
|
target: options.target,
|
|
48
|
-
agent_device_id: options.cartSession.ensureAgentDeviceID(config.agentDeviceID),
|
|
49
42
|
};
|
|
50
|
-
const agentDeviceID = options.cartSession.ensureAgentDeviceID(config.agentDeviceID);
|
|
51
|
-
request.agent_device_id = agentDeviceID;
|
|
52
43
|
cart = await backend.createCart(request);
|
|
53
44
|
}
|
|
45
|
+
const latestLine = cart.items[cart.items.length - 1];
|
|
46
|
+
options.cartSession.rememberServerCart({
|
|
47
|
+
cartID: cart.cart_id,
|
|
48
|
+
...(latestLine?.cart_item_id ? { cartItemID: latestLine.cart_item_id } : {}),
|
|
49
|
+
...(latestLine?.service_execution_id ? { serviceExecutionID: latestLine.service_execution_id } : {}),
|
|
50
|
+
});
|
|
51
|
+
const unquotedServiceLine = cart.items.find((item) => item.service_execution_id && !item.service_quote_lock_id);
|
|
52
|
+
if (unquotedServiceLine?.service_execution_id) {
|
|
53
|
+
throw new CommandContractError("service_quote_required", `cart ${cart.cart_id} contains an unquoted service-backed line`, "该服务项目尚未绑定 Quote Lock;回到来源 Execution 完成候选选择和报价。", [{ command: `itpay services next ${unquotedServiceLine.service_execution_id} --json`, reason: "读取当前合法动作" }]);
|
|
54
|
+
}
|
|
55
|
+
const idempotencyKey = await operationID(config, `checkout.create:${cart.cart_id}`);
|
|
54
56
|
const checkoutRequest = {
|
|
55
57
|
cart_id: cart.cart_id,
|
|
56
|
-
client_reference_id: options.clientReferenceID ??
|
|
58
|
+
client_reference_id: options.clientReferenceID ?? idempotencyKey,
|
|
57
59
|
...(options.contact ? { delivery_contact: options.contact } : {}),
|
|
58
60
|
};
|
|
59
|
-
const checkout = await backend.createCheckout(checkoutRequest);
|
|
61
|
+
const checkout = await backend.createCheckout(checkoutRequest, idempotencyKey);
|
|
60
62
|
options.cartSession.rememberCheckout({
|
|
61
|
-
cartID: cart.cart_id,
|
|
62
63
|
checkoutID: checkout.checkout.checkout_id,
|
|
63
64
|
displayToken: checkout.display_token,
|
|
64
65
|
checkoutURL: tokenizedCheckoutURL(checkout.checkout_url, checkout.display_token, checkout.qr_payload),
|
|
@@ -77,7 +78,7 @@ export async function runBuy(backend, config, options) {
|
|
|
77
78
|
let waitStatus = "skipped";
|
|
78
79
|
if (options.pay) {
|
|
79
80
|
const method = options.payMethod ?? "alipay";
|
|
80
|
-
paymentIntent = await backend.createPaymentIntent(checkoutID, { payment_method_type: method, display_token: displayToken }
|
|
81
|
+
paymentIntent = await backend.createPaymentIntent(checkoutID, { payment_method_type: method, display_token: displayToken });
|
|
81
82
|
if (!options.jsonOutput) {
|
|
82
83
|
process.stdout.write("\n--- payment intent ---\n");
|
|
83
84
|
process.stdout.write(` id: ${paymentIntent.payment_intent_id}\n`);
|
|
@@ -124,34 +125,23 @@ export async function runBuy(backend, config, options) {
|
|
|
124
125
|
});
|
|
125
126
|
// --- Output ---
|
|
126
127
|
if (options.jsonOutput) {
|
|
127
|
-
const
|
|
128
|
-
checkout: { ...checkout, checkout_url: checkoutURL },
|
|
128
|
+
const envelope = buildBuyEnvelope({
|
|
129
129
|
cart,
|
|
130
|
-
|
|
130
|
+
checkoutID,
|
|
131
|
+
checkoutURL,
|
|
132
|
+
displayToken,
|
|
131
133
|
plan,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
jsonInput.ideImageAttach = {
|
|
138
|
-
localPath: plan.ideImageAttach.localPath,
|
|
139
|
-
mirrors: plan.ideImageAttach.mirrors,
|
|
140
|
-
mimeType: plan.ideImageAttach.mimeType,
|
|
141
|
-
...(plan.ideImageAttach.caption ? { caption: plan.ideImageAttach.caption } : {}),
|
|
142
|
-
mustRenderReason: plan.ideImageAttach.mustRenderReason,
|
|
143
|
-
status: plan.ideImageAttach.status,
|
|
144
|
-
...(plan.ideImageAttach.error ? { error: plan.ideImageAttach.error } : {}),
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
const json = buildJSONOutput(jsonInput);
|
|
148
|
-
(options.output ?? ((line) => process.stdout.write(line + "\n")))(JSON.stringify(json, null, 2) + "\n");
|
|
134
|
+
waitStatus,
|
|
135
|
+
...(checkout.qr_png_url ? { qrPNGURL: checkout.qr_png_url } : {}),
|
|
136
|
+
...(paymentIntent ? { paymentIntent } : {}),
|
|
137
|
+
});
|
|
138
|
+
writeCommandEnvelope(envelope, { jsonOutput: true, ...(options.output ? { output: options.output } : {}) });
|
|
149
139
|
return {
|
|
150
140
|
kind: "checkout_rendered",
|
|
151
141
|
plan,
|
|
152
142
|
checkoutID,
|
|
153
143
|
displayToken,
|
|
154
|
-
|
|
144
|
+
envelope,
|
|
155
145
|
};
|
|
156
146
|
}
|
|
157
147
|
// Text output — always render QR for non-JSON mode
|
|
@@ -173,6 +163,55 @@ export async function runBuy(backend, config, options) {
|
|
|
173
163
|
displayToken,
|
|
174
164
|
};
|
|
175
165
|
}
|
|
166
|
+
function buildBuyEnvelope(input) {
|
|
167
|
+
const verified = input.waitStatus === "verified";
|
|
168
|
+
const platform = platformKeyForHost(input.plan.host);
|
|
169
|
+
const handoff = { url: input.checkoutURL };
|
|
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
|
+
}
|
|
179
|
+
const result = {
|
|
180
|
+
checkout_id: input.checkoutID,
|
|
181
|
+
payment: verified ? "verified" : "pending",
|
|
182
|
+
amount: formatMoney(input.cart.amount_minor, input.cart.currency),
|
|
183
|
+
item_count: input.cart.items.length,
|
|
184
|
+
...(input.paymentIntent ? {
|
|
185
|
+
payment_intent_id: input.paymentIntent.payment_intent_id,
|
|
186
|
+
payment_intent_status: input.paymentIntent.status,
|
|
187
|
+
} : {}),
|
|
188
|
+
...(input.waitStatus === "timeout" ? { wait_status: "timeout" } : {}),
|
|
189
|
+
};
|
|
190
|
+
if (verified) {
|
|
191
|
+
return {
|
|
192
|
+
status: "payment_event_observed",
|
|
193
|
+
result,
|
|
194
|
+
instruction: "已观察到付款确认事件;读取同一 Checkout 的权威完成状态,不要再次付款。",
|
|
195
|
+
next: { command: `itpay checkout --id ${input.checkoutID} --token ${input.displayToken} --json`, reason: "读取订单和履约句柄" },
|
|
196
|
+
recovery: [],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
status: "human_checkout_required",
|
|
201
|
+
result,
|
|
202
|
+
handoff,
|
|
203
|
+
instruction: buyHandoffInstruction(platform),
|
|
204
|
+
next: { command: `itpay checkout --id ${input.checkoutID} --token ${input.displayToken} --json`, reason: "稍后查询同一笔 Checkout 状态" },
|
|
205
|
+
recovery: [],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
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
|
+
}
|
|
176
215
|
// --- SSE wait for payment verification ---
|
|
177
216
|
async function waitForPaymentSSE(backend, checkoutID, displayToken, timeoutSec) {
|
|
178
217
|
return new Promise((resolve) => {
|
|
@@ -194,81 +233,11 @@ async function waitForPaymentSSE(backend, checkoutID, displayToken, timeoutSec)
|
|
|
194
233
|
});
|
|
195
234
|
});
|
|
196
235
|
}
|
|
197
|
-
export function buildJSONOutput(input) {
|
|
198
|
-
const output = {
|
|
199
|
-
kind: "checkout_created",
|
|
200
|
-
checkout_id: input.checkout.checkout.checkout_id,
|
|
201
|
-
checkout_url: input.checkout.checkout_url,
|
|
202
|
-
display_token: input.checkout.display_token,
|
|
203
|
-
qr_payload: input.checkout.qr_payload,
|
|
204
|
-
checkout_status: input.checkout.checkout.status,
|
|
205
|
-
wait_status: input.waitStatus,
|
|
206
|
-
};
|
|
207
|
-
if (input.checkout.qr_png_url) {
|
|
208
|
-
output.qr_png_url = input.checkout.qr_png_url;
|
|
209
|
-
}
|
|
210
|
-
const serviceExecutions = input.cart?.items
|
|
211
|
-
.filter((item) => item.service_execution_id)
|
|
212
|
-
.map((item) => ({
|
|
213
|
-
service_execution_id: item.service_execution_id,
|
|
214
|
-
...(item.service_capability_id ? { service_capability_id: item.service_capability_id } : {}),
|
|
215
|
-
...(item.title ? { title: item.title } : {}),
|
|
216
|
-
}));
|
|
217
|
-
if (serviceExecutions && serviceExecutions.length > 0) {
|
|
218
|
-
output.service_executions = serviceExecutions;
|
|
219
|
-
}
|
|
220
|
-
if (input.paymentIntent) {
|
|
221
|
-
output.payment_intent_id = input.paymentIntent.payment_intent_id;
|
|
222
|
-
output.payment_status = input.paymentIntent.status;
|
|
223
|
-
if (input.paymentIntent.action) {
|
|
224
|
-
const pa = {};
|
|
225
|
-
if (input.paymentIntent.action.qr_image_url)
|
|
226
|
-
pa.qr_image_url = input.paymentIntent.action.qr_image_url;
|
|
227
|
-
if (input.paymentIntent.action.mobile_wallet_url)
|
|
228
|
-
pa.mobile_wallet_url = input.paymentIntent.action.mobile_wallet_url;
|
|
229
|
-
output.payment_action = pa;
|
|
230
|
-
}
|
|
231
|
-
output.kind = "payment_handoff_required";
|
|
232
|
-
}
|
|
233
|
-
if (input.waitStatus === "verified") {
|
|
234
|
-
output.kind = "payment_verified";
|
|
235
|
-
}
|
|
236
|
-
if (input.ideImageAttach) {
|
|
237
|
-
output.brand_qr_status = input.ideImageAttach.status;
|
|
238
|
-
if (input.ideImageAttach.localPath) {
|
|
239
|
-
output.brand_qr_local_path = input.ideImageAttach.localPath;
|
|
240
|
-
const dataURL = readFileAsDataURL(input.ideImageAttach.localPath, input.ideImageAttach.mimeType);
|
|
241
|
-
if (dataURL)
|
|
242
|
-
output.brand_qr_data_url = dataURL;
|
|
243
|
-
const stableName = input.ideImageAttach.localPath.split("/").pop();
|
|
244
|
-
if (stableName)
|
|
245
|
-
output.brand_qr_stable_name = stableName;
|
|
246
|
-
}
|
|
247
|
-
if (input.ideImageAttach.mirrors.length > 0) {
|
|
248
|
-
output.brand_qr_mirrors = [...input.ideImageAttach.mirrors];
|
|
249
|
-
}
|
|
250
|
-
output.brand_qr_mime_type = input.ideImageAttach.mimeType;
|
|
251
|
-
if (input.ideImageAttach.caption)
|
|
252
|
-
output.brand_qr_caption = input.ideImageAttach.caption;
|
|
253
|
-
if (input.ideImageAttach.error)
|
|
254
|
-
output.brand_qr_error = input.ideImageAttach.error;
|
|
255
|
-
output.brand_qr_must_render_reason = input.ideImageAttach.mustRenderReason;
|
|
256
|
-
output.brand_qr_render_action = "agent_must_read_local_path_into_ide_chat";
|
|
257
|
-
}
|
|
258
|
-
output.next = input.waitStatus === "verified"
|
|
259
|
-
? "itpay orders"
|
|
260
|
-
: `itpay checkout --id ${input.checkout.checkout.checkout_id} --token ${input.checkout.display_token}`;
|
|
261
|
-
if (input.plan)
|
|
262
|
-
output.agent_action = buildAgentChatHandoff(input.plan);
|
|
263
|
-
return output;
|
|
264
|
-
}
|
|
265
236
|
// --- checkout QR plan ---
|
|
266
237
|
export function buildCheckoutQRPlan(input) {
|
|
267
238
|
const summary = `Scan the QR or open ${input.checkoutURL} to start the human checkout flow.`;
|
|
268
239
|
const isPayment = input.paymentIntentID != null;
|
|
269
|
-
const afterCommand =
|
|
270
|
-
? `itpay checkout --id ${input.checkoutID} --token ${input.displayToken}`
|
|
271
|
-
: `itpay checkout --id ${input.checkoutID} --token ${input.displayToken}`;
|
|
240
|
+
const afterCommand = `itpay checkout --id ${input.checkoutID} --token ${input.displayToken} --json`;
|
|
272
241
|
const platform = {
|
|
273
242
|
text: summary,
|
|
274
243
|
links: [
|
|
@@ -307,23 +276,6 @@ export function buildCheckoutQRPlan(input) {
|
|
|
307
276
|
return plan;
|
|
308
277
|
}
|
|
309
278
|
// --- contact field interaction ---
|
|
310
|
-
export function buildMissingContactInteractionRequest(fields) {
|
|
311
|
-
return {
|
|
312
|
-
kind: "input",
|
|
313
|
-
id: "collect_delivery_contact",
|
|
314
|
-
title: "Collect buyer contact",
|
|
315
|
-
prompt: "Before creating the checkout, ask the buyer to provide the missing contact details.",
|
|
316
|
-
fields: fields.map((field) => ({
|
|
317
|
-
id: field,
|
|
318
|
-
label: field === "email" ? "Email" : "Phone number",
|
|
319
|
-
inputType: field === "email" ? "email" : "phone",
|
|
320
|
-
required: true,
|
|
321
|
-
placeholder: field === "email" ? "buyer@example.com" : "+86 138...",
|
|
322
|
-
description: field === "email" ? "Used for receipts or delivery follow-up." : "Used when the order requires buyer verification.",
|
|
323
|
-
})),
|
|
324
|
-
submitLabel: "Submit contact",
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
279
|
function findMissingContactFields(contact, fields) {
|
|
328
280
|
return fields.filter((field) => {
|
|
329
281
|
const value = contact?.[field];
|