@itpay/cli 2.0.2 → 2.0.5
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 +92 -142
- package/dist/src/client/backend.js +27 -9
- package/dist/src/commands/buy.js +84 -132
- package/dist/src/commands/cart.js +274 -169
- package/dist/src/commands/catalog.js +63 -38
- package/dist/src/commands/checkout.js +117 -78
- package/dist/src/commands/docs.js +97 -51
- package/dist/src/commands/guidance.js +87 -15
- 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 -14
- package/dist/src/commands/services.js +719 -148
- package/dist/src/main.js +711 -201
- package/dist/src/render/output.js +2 -3
- package/dist/src/state/cart_session.js +13 -17
- package/dist/src/state/client_context.js +4 -2
- package/dist/src/state/config.js +3 -5
- package/dist/src/state/device_authority.js +1 -1
- package/docs/agent/buyer/cart-checkout.json +27 -83
- package/docs/agent/buyer/install-and-setup.json +23 -67
- package/docs/agent/buyer/orders-refunds.json +31 -53
- package/docs/agent/buyer/payment-flow.json +24 -57
- package/docs/agent/buyer/quickstart.json +39 -162
- package/docs/agent/buyer/render-hosts.json +43 -57
- package/docs/cli-reference/agent-types.md +45 -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/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 +112 -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 +39 -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 +43 -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 +162 -0
- package/docs/cli-reference/commands/services/quote.md +59 -0
- package/docs/cli-reference/commands/services/read-result.md +98 -0
- package/docs/cli-reference/commands/services/start.md +53 -0
- package/docs/cli-reference/conventions.md +94 -0
- package/docs/cli-reference/index.md +64 -0
- package/package.json +1 -1
- package/skills/itpay-buyer/SKILL.md +47 -113
package/dist/src/main.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// services/backend/internal/httpapi/handlers/*.go. Commands only
|
|
3
3
|
// orchestrate; HTTP and rendering live in src/client and src/render.
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { loadConfig, cartSessionPath, newBackendClient } from "./state/config.js";
|
|
5
|
+
import { CLI_VERSION, loadConfig, cartSessionPath, newBackendClient } from "./state/config.js";
|
|
6
6
|
import { CartSession } from "./state/cart_session.js";
|
|
7
|
-
import { defaultHostForAgentType, normalizeHost } from "./state/client_context.js";
|
|
7
|
+
import { defaultHostForAgentType, normalizeHost, validateContext } from "./state/client_context.js";
|
|
8
8
|
import { HttpError } from "./client/http.js";
|
|
9
9
|
import { runReadyz } from "./commands/readyz.js";
|
|
10
10
|
import { runBuy } from "./commands/buy.js";
|
|
@@ -13,18 +13,19 @@ import { runCheckoutPresentation } from "./commands/checkout.js";
|
|
|
13
13
|
import { runPay } from "./commands/pay.js";
|
|
14
14
|
import { runOrder } from "./commands/order.js";
|
|
15
15
|
import { runListOrders } from "./commands/orders.js";
|
|
16
|
-
import { runRefund } from "./commands/refund.js";
|
|
17
|
-
import { runCartAdd, runCartAddServer, runCartAbandonServer, runCartClear, runCartNext, runCartRemove, runCartRemoveServer, runCartShowServer, } from "./commands/cart.js";
|
|
18
|
-
import { printErrorRecovery } from "./commands/guidance.js";
|
|
16
|
+
import { runCancelRefund, runGetRefund, runListRefunds, runRefund, runWatchRefund } from "./commands/refund.js";
|
|
17
|
+
import { runCartAdd, runCartAddQuoteServer, runCartAddServer, runCartAbandonServer, runCartClear, runCartNext, runCartRemove, runCartRemoveServer, runCartShow, runCartShowServer, } from "./commands/cart.js";
|
|
18
|
+
import { CommandContractError, printErrorRecovery, writeCommandEnvelope } from "./commands/guidance.js";
|
|
19
19
|
import { runDocsList, runDocsShow, runDocsSearch } from "./commands/docs.js";
|
|
20
20
|
import { runInstall } from "./commands/install.js";
|
|
21
|
-
import {
|
|
21
|
+
import { runNext } from "./commands/next.js";
|
|
22
|
+
import { collectOption, parseKeyValueList, runServicesAction, runServicesCheckout, runServicesEvents, runServicesGet, runServicesInvoke, runServicesList, runServicesNext, runServicesReadResult, runServicesQuote, runServicesStart, } from "./commands/services.js";
|
|
22
23
|
const program = new Command();
|
|
23
24
|
program
|
|
24
25
|
.name("itpay")
|
|
25
26
|
.description("V3 ItPay CLI — checkout, payment, order, and refund commands")
|
|
26
27
|
.option("--agent-type <type>", "agent runtime type used for device enrollment and client-specific guidance")
|
|
27
|
-
.version("2.0.
|
|
28
|
+
.version("2.0.5");
|
|
28
29
|
function withHost(value) {
|
|
29
30
|
const host = normalizeHost(value);
|
|
30
31
|
if (!host) {
|
|
@@ -36,12 +37,24 @@ function parseRequiredContactFields(value) {
|
|
|
36
37
|
if (!value) {
|
|
37
38
|
return undefined;
|
|
38
39
|
}
|
|
39
|
-
const
|
|
40
|
+
const values = value
|
|
40
41
|
.split(",")
|
|
41
42
|
.map((item) => item.trim().toLowerCase())
|
|
42
|
-
.filter(
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
const invalid = values.filter((item) => item !== "email" && item !== "phone");
|
|
45
|
+
if (invalid.length > 0) {
|
|
46
|
+
throw new CommandContractError("contact_field_invalid", `unsupported required contact fields: ${invalid.join(", ")}`, "--require-contact 只接受 email、phone 或二者组合。", [{ command: "itpay buy --help", reason: "查看 contact 参数" }]);
|
|
47
|
+
}
|
|
48
|
+
const parsed = values.filter((item) => item === "email" || item === "phone");
|
|
43
49
|
return parsed.length > 0 ? parsed : undefined;
|
|
44
50
|
}
|
|
51
|
+
function positiveInteger(value, name) {
|
|
52
|
+
const text = String(value ?? "");
|
|
53
|
+
if (!/^[1-9]\d*$/.test(text)) {
|
|
54
|
+
throw new CommandContractError("buy_parameter_invalid", `${name} must be a positive integer`, `${name} 必须是正整数;本次未创建或修改 Cart/Checkout。`, [{ command: "itpay buy --help", reason: "查看参数格式" }]);
|
|
55
|
+
}
|
|
56
|
+
return Number(text);
|
|
57
|
+
}
|
|
45
58
|
function resolveCheckoutPresentationArgs(input) {
|
|
46
59
|
if (input.requestedCheckoutID && input.requestedDisplayToken) {
|
|
47
60
|
return { checkoutID: input.requestedCheckoutID, displayToken: input.requestedDisplayToken };
|
|
@@ -65,7 +78,25 @@ function resolveCheckoutPresentationArgs(input) {
|
|
|
65
78
|
}
|
|
66
79
|
throw new Error("checkout id and display token are required; pass --id/--token or create a checkout first");
|
|
67
80
|
}
|
|
68
|
-
function reportCLIError(error) {
|
|
81
|
+
function reportCLIError(error, contract) {
|
|
82
|
+
const commandError = error instanceof CommandContractError ? error : undefined;
|
|
83
|
+
if (contract || commandError) {
|
|
84
|
+
writeCommandEnvelope({
|
|
85
|
+
status: "error",
|
|
86
|
+
error: {
|
|
87
|
+
code: commandError?.code ?? (error instanceof HttpError ? error.code : contract?.code ?? "command_failed"),
|
|
88
|
+
message: error instanceof Error ? error.message : String(error),
|
|
89
|
+
},
|
|
90
|
+
instruction: commandError?.instruction ?? contract?.instruction ?? "检查命令参数后重试。",
|
|
91
|
+
next: null,
|
|
92
|
+
recovery: commandError?.recovery ?? contract?.recovery ?? [],
|
|
93
|
+
}, {
|
|
94
|
+
...(contract?.jsonOutput !== undefined ? { jsonOutput: contract.jsonOutput } : {}),
|
|
95
|
+
output: (text) => { process.stderr.write(text); },
|
|
96
|
+
});
|
|
97
|
+
process.exitCode = 1;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
69
100
|
if (error instanceof HttpError) {
|
|
70
101
|
process.stderr.write(`[${error.status}] ${error.code}: ${error.message}\n`);
|
|
71
102
|
printErrorRecovery(error, (text) => process.stderr.write(text));
|
|
@@ -79,11 +110,34 @@ function reportCLIError(error) {
|
|
|
79
110
|
}
|
|
80
111
|
throw error;
|
|
81
112
|
}
|
|
113
|
+
function docsErrorFallback(jsonOutput) {
|
|
114
|
+
return {
|
|
115
|
+
jsonOutput,
|
|
116
|
+
code: "docs_unavailable",
|
|
117
|
+
instruction: "内置文档缺失或损坏;重新安装同版本 CLI 后重试。",
|
|
118
|
+
recovery: [{ command: `npm install -g @itpay/cli@${CLI_VERSION}`, reason: "恢复随包发布的文档" }],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
82
121
|
program
|
|
83
122
|
.command("readyz")
|
|
84
123
|
.description("Probe the V3 backend readiness endpoint")
|
|
85
|
-
.
|
|
86
|
-
|
|
124
|
+
.option("--json", "output JSON instead of terminal text")
|
|
125
|
+
.action(async (options) => {
|
|
126
|
+
const backend = newBackendClient(loadConfig());
|
|
127
|
+
try {
|
|
128
|
+
await runReadyz(backend, { jsonOutput: Boolean(options.json) });
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
reportCLIError(error, {
|
|
132
|
+
jsonOutput: Boolean(options.json),
|
|
133
|
+
code: "backend_unavailable",
|
|
134
|
+
instruction: "检查 ITPAY_BACKEND_URL 后重试;后端恢复前不要继续下单。",
|
|
135
|
+
recovery: [
|
|
136
|
+
{ command: "echo $ITPAY_BACKEND_URL", reason: "确认当前 Backend URL" },
|
|
137
|
+
{ command: "itpay readyz", reason: "重试可用性检查" },
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
}
|
|
87
141
|
});
|
|
88
142
|
program
|
|
89
143
|
.command("next")
|
|
@@ -91,14 +145,8 @@ program
|
|
|
91
145
|
.option("--json", "output JSON instead of terminal text")
|
|
92
146
|
.action(async (options) => {
|
|
93
147
|
const config = loadConfig();
|
|
94
|
-
const backend = newBackendClient(config);
|
|
95
148
|
const session = CartSession.loadFromFile(cartSessionPath(), config.checkoutCurrency);
|
|
96
|
-
|
|
97
|
-
await runCartNext(backend, session, { jsonOutput: Boolean(options.json) });
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
reportCLIError(error);
|
|
101
|
-
}
|
|
149
|
+
runNext(session, { jsonOutput: Boolean(options.json) });
|
|
102
150
|
});
|
|
103
151
|
// --- catalog --------------------------------------------------------------
|
|
104
152
|
const catalogCmd = program.command("catalog").description("Browse V3 service catalog");
|
|
@@ -107,49 +155,93 @@ catalogCmd
|
|
|
107
155
|
.description("List all available services from the published catalog manifest")
|
|
108
156
|
.option("--json", "output JSON instead of terminal text")
|
|
109
157
|
.action(async (options) => {
|
|
110
|
-
|
|
158
|
+
const backend = newBackendClient(loadConfig());
|
|
159
|
+
try {
|
|
160
|
+
await runCatalogList(backend, { jsonOutput: Boolean(options.json) });
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
reportCLIError(error, {
|
|
164
|
+
jsonOutput: Boolean(options.json),
|
|
165
|
+
code: "catalog_unavailable",
|
|
166
|
+
instruction: "确认 ItPay 可用后重试目录读取;不要猜测 service_id。",
|
|
167
|
+
recovery: [
|
|
168
|
+
{ command: "itpay readyz", reason: "确认 Backend 可用" },
|
|
169
|
+
{ command: "itpay catalog list", reason: "重新读取已发布目录" },
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
}
|
|
111
173
|
});
|
|
112
174
|
// --- install --------------------------------------------------------------
|
|
113
175
|
const installCmd = program.command("install").description("Show setup instructions for each agent host");
|
|
114
176
|
installCmd
|
|
115
|
-
.argument("[target]", "
|
|
177
|
+
.argument("[target]", "Agent Type, or list")
|
|
178
|
+
.option("--json", "output JSON instead of terminal text")
|
|
116
179
|
.description("Show agent-specific installation and configuration instructions")
|
|
117
|
-
.action((target) => {
|
|
118
|
-
|
|
180
|
+
.action((target, options) => {
|
|
181
|
+
try {
|
|
182
|
+
runInstall(target, { jsonOutput: Boolean(options.json) });
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
reportCLIError(error, {
|
|
186
|
+
jsonOutput: Boolean(options.json),
|
|
187
|
+
code: "install_failed",
|
|
188
|
+
instruction: "选择受支持的真实 Agent Type;本命令不会修改宿主配置。",
|
|
189
|
+
recovery: [{ command: "itpay install --json", reason: "列出支持类型" }],
|
|
190
|
+
});
|
|
191
|
+
}
|
|
119
192
|
});
|
|
120
193
|
// --- docs -----------------------------------------------------------------
|
|
121
194
|
const docsCmd = program.command("docs").description("Browse agent documentation");
|
|
122
195
|
docsCmd
|
|
123
196
|
.command("list")
|
|
124
197
|
.description("List all available agent doc topics")
|
|
125
|
-
.
|
|
126
|
-
|
|
198
|
+
.option("--json", "output JSON instead of terminal text")
|
|
199
|
+
.action((options) => {
|
|
200
|
+
try {
|
|
201
|
+
runDocsList({ jsonOutput: Boolean(options.json) });
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
reportCLIError(error, docsErrorFallback(Boolean(options.json)));
|
|
205
|
+
}
|
|
127
206
|
});
|
|
128
207
|
docsCmd
|
|
129
208
|
.command("show")
|
|
130
209
|
.description("Show a specific doc topic")
|
|
131
210
|
.argument("<topic>", "doc topic name")
|
|
132
|
-
.
|
|
133
|
-
|
|
211
|
+
.option("--json", "output JSON instead of terminal text")
|
|
212
|
+
.action((topic, options) => {
|
|
213
|
+
try {
|
|
214
|
+
runDocsShow(topic, { jsonOutput: Boolean(options.json) });
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
reportCLIError(error, docsErrorFallback(Boolean(options.json)));
|
|
218
|
+
}
|
|
134
219
|
});
|
|
135
220
|
docsCmd
|
|
136
221
|
.command("search")
|
|
137
222
|
.description("Search doc topics by keyword")
|
|
138
223
|
.argument("<query>", "search query")
|
|
139
|
-
.
|
|
140
|
-
|
|
224
|
+
.option("--json", "output JSON instead of terminal text")
|
|
225
|
+
.action((query, options) => {
|
|
226
|
+
try {
|
|
227
|
+
runDocsSearch(query, { jsonOutput: Boolean(options.json) });
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
reportCLIError(error, docsErrorFallback(Boolean(options.json)));
|
|
231
|
+
}
|
|
141
232
|
});
|
|
142
233
|
// --- cart ----------------------------------------------------------------
|
|
143
234
|
const cart = program.command("cart").description("V3 canonical server cart");
|
|
144
235
|
cart
|
|
145
236
|
.command("add")
|
|
146
237
|
.description("Add a variant/offer/quantity to the canonical server cart")
|
|
147
|
-
.
|
|
148
|
-
.
|
|
149
|
-
.
|
|
150
|
-
.option("--
|
|
238
|
+
.option("--item <catalog_item_id>")
|
|
239
|
+
.option("--variant <catalog_variant_id>")
|
|
240
|
+
.option("--offer <offer_id>")
|
|
241
|
+
.option("--quote <service_quote_lock_id>", "add a prepared service quote")
|
|
242
|
+
.option("--quantity <n>", "quantity", Number, 1)
|
|
151
243
|
.option("--input <json>")
|
|
152
|
-
.option("--host <host>", "client host (terminal, codex, telegram, feishu, lark, ...)"
|
|
244
|
+
.option("--host <host>", "client host (terminal, codex, telegram, feishu, lark, ...)")
|
|
153
245
|
.option("--target <target>", "chat id / channel id / open id for IM hosts")
|
|
154
246
|
.option("--json", "output JSON instead of terminal text")
|
|
155
247
|
.option("--local", "only add to the local draft cache; not valid for service-backed flows")
|
|
@@ -157,32 +249,82 @@ cart
|
|
|
157
249
|
const config = loadConfig();
|
|
158
250
|
const sessionPath = cartSessionPath();
|
|
159
251
|
const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
|
|
160
|
-
const
|
|
161
|
-
catalogItemID: options.item,
|
|
162
|
-
catalogVariantID: options.variant,
|
|
163
|
-
offerID: options.offer,
|
|
164
|
-
quantity: options.quantity,
|
|
165
|
-
...(options.input ? { input: JSON.parse(options.input) } : {}),
|
|
166
|
-
};
|
|
252
|
+
const jsonOutput = Boolean(options.json);
|
|
167
253
|
try {
|
|
254
|
+
const quoteMode = Boolean(options.quote);
|
|
255
|
+
if (quoteMode && (options.item || options.variant || options.offer || options.input || options.local)) {
|
|
256
|
+
throw new CommandContractError("cart_item_scope_invalid", "--quote cannot be combined with catalog fields, input, or --local", "服务报价只使用 services quote 返回的 quote ID;不要混入 Catalog 或 input 参数。", [{ command: "itpay cart add --help", reason: "查看两种添加方式" }]);
|
|
257
|
+
}
|
|
258
|
+
if (!quoteMode && (!options.item || !options.variant || !options.offer)) {
|
|
259
|
+
throw new CommandContractError("cart_item_required", "--item, --variant and --offer are required", "使用同一条 Catalog 记录返回的 item、variant 和 offer ID;不要猜测或混用。", [{ command: "itpay catalog list --json", reason: "读取已发布目录 ID" }]);
|
|
260
|
+
}
|
|
261
|
+
if (!quoteMode && (!Number.isInteger(options.quantity) || options.quantity < 1)) {
|
|
262
|
+
throw new CommandContractError("quantity_invalid", "--quantity must be a positive integer", "使用大于 0 的整数 quantity;本次未修改 Cart。", [{ command: "itpay cart show", reason: "确认当前 Cart 未变化" }]);
|
|
263
|
+
}
|
|
264
|
+
let input;
|
|
265
|
+
if (options.input) {
|
|
266
|
+
const parsed = JSON.parse(options.input);
|
|
267
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
268
|
+
throw new CommandContractError("cart_input_invalid", "--input must be a JSON object", "按照服务合同传入 JSON object;本次未修改 Cart。", [{ command: "itpay catalog list --json", reason: "重新确认服务入口" }]);
|
|
269
|
+
}
|
|
270
|
+
input = parsed;
|
|
271
|
+
}
|
|
272
|
+
const addOptions = {
|
|
273
|
+
catalogItemID: options.item,
|
|
274
|
+
catalogVariantID: options.variant,
|
|
275
|
+
offerID: options.offer,
|
|
276
|
+
quantity: options.quantity,
|
|
277
|
+
...(input ? { input } : {}),
|
|
278
|
+
};
|
|
168
279
|
if (options.local) {
|
|
169
|
-
runCartAdd(session, addOptions);
|
|
280
|
+
runCartAdd(session, { ...addOptions, jsonOutput });
|
|
170
281
|
}
|
|
171
282
|
else {
|
|
283
|
+
const host = withHost(options.host ?? defaultHostForAgentType(config.agentType));
|
|
284
|
+
const contextError = validateContext(host, options.target);
|
|
285
|
+
if (contextError) {
|
|
286
|
+
throw new CommandContractError(contextError.code, contextError.message, "补齐当前客户端所需的 Host/target;本次未创建或修改 Cart。", [{ command: "itpay cart add --help", reason: "查看客户端参数" }]);
|
|
287
|
+
}
|
|
172
288
|
const backend = newBackendClient(config);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
289
|
+
if (quoteMode) {
|
|
290
|
+
await runCartAddQuoteServer({
|
|
291
|
+
serviceQuoteLockID: options.quote,
|
|
292
|
+
backend, config, session, host,
|
|
293
|
+
...(options.target ? { target: options.target } : {}),
|
|
294
|
+
jsonOutput,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
await runCartAddServer({
|
|
299
|
+
...addOptions,
|
|
300
|
+
backend,
|
|
301
|
+
config,
|
|
302
|
+
session,
|
|
303
|
+
host,
|
|
304
|
+
...(options.target ? { target: options.target } : {}),
|
|
305
|
+
jsonOutput,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
182
308
|
}
|
|
183
309
|
}
|
|
184
310
|
catch (error) {
|
|
185
|
-
|
|
311
|
+
const quoteMode = Boolean(options.quote);
|
|
312
|
+
reportCLIError(error, {
|
|
313
|
+
jsonOutput,
|
|
314
|
+
code: "cart_add_failed",
|
|
315
|
+
instruction: quoteMode
|
|
316
|
+
? "Quote 是否已加入 Cart 尚未确认;先恢复当前 Cart 或来源 Execution,不要重复准备报价或直接创建 Checkout。"
|
|
317
|
+
: "核对 Catalog ID、输入和 Cart 状态;不要在失败后直接创建 Checkout。",
|
|
318
|
+
recovery: quoteMode
|
|
319
|
+
? [
|
|
320
|
+
{ command: "itpay cart show --json", reason: "检查当前 canonical Cart" },
|
|
321
|
+
{ command: "itpay services list --json", reason: "恢复 Quote 所属的 Service Execution" },
|
|
322
|
+
]
|
|
323
|
+
: [
|
|
324
|
+
{ command: "itpay catalog list --json", reason: "核对已发布项目" },
|
|
325
|
+
{ command: "itpay cart show", reason: "确认 canonical Cart 当前状态" },
|
|
326
|
+
],
|
|
327
|
+
});
|
|
186
328
|
}
|
|
187
329
|
finally {
|
|
188
330
|
session.saveToFile(sessionPath);
|
|
@@ -200,7 +342,15 @@ cart
|
|
|
200
342
|
await runCartNext(backend, session, { jsonOutput: Boolean(options.json) });
|
|
201
343
|
}
|
|
202
344
|
catch (error) {
|
|
203
|
-
reportCLIError(error
|
|
345
|
+
reportCLIError(error, {
|
|
346
|
+
jsonOutput: Boolean(options.json),
|
|
347
|
+
code: "cart_next_failed",
|
|
348
|
+
instruction: "canonical Cart 无法读取;不要猜测 Cart 内容或创建重复订单。",
|
|
349
|
+
recovery: [
|
|
350
|
+
{ command: "itpay services list --json", reason: "恢复当前设备可见的 Service Execution" },
|
|
351
|
+
{ command: "itpay catalog list --json", reason: "在没有可恢复执行时重新选择服务" },
|
|
352
|
+
],
|
|
353
|
+
});
|
|
204
354
|
}
|
|
205
355
|
});
|
|
206
356
|
cart
|
|
@@ -210,27 +360,51 @@ cart
|
|
|
210
360
|
.option("--variant <catalog_variant_id>", "local draft variant id, only with --local")
|
|
211
361
|
.option("--offer <offer_id>", "local draft offer id, only with --local")
|
|
212
362
|
.option("--local", "only remove from the explicit local draft cache")
|
|
363
|
+
.option("--json", "output JSON instead of terminal text")
|
|
213
364
|
.action(async (options) => {
|
|
214
365
|
const config = loadConfig();
|
|
215
366
|
const sessionPath = cartSessionPath();
|
|
216
367
|
const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
|
|
368
|
+
const jsonOutput = Boolean(options.json);
|
|
217
369
|
try {
|
|
218
370
|
if (options.local) {
|
|
219
371
|
if (!options.variant || !options.offer) {
|
|
220
|
-
throw new
|
|
372
|
+
throw new CommandContractError("local_cart_item_required", "--local remove requires --variant and --offer", "从 `cart show --local --json` 使用同一条草稿 line 的 variant 和 offer;不要猜测。", [{ command: "itpay cart show --local --json", reason: "读取本地草稿" }]);
|
|
373
|
+
}
|
|
374
|
+
if (options.line) {
|
|
375
|
+
throw new CommandContractError("cart_remove_scope_invalid", "--line cannot be combined with --local", "canonical line 使用 --line;本地草稿使用 --local --variant --offer,不要混用。", [{ command: "itpay cart remove --help", reason: "查看两种删除范围" }]);
|
|
221
376
|
}
|
|
222
377
|
runCartRemove(session, {
|
|
223
378
|
catalogVariantID: options.variant,
|
|
224
379
|
offerID: options.offer,
|
|
380
|
+
jsonOutput,
|
|
225
381
|
});
|
|
226
382
|
}
|
|
227
383
|
else {
|
|
384
|
+
if (options.variant || options.offer) {
|
|
385
|
+
throw new CommandContractError("cart_remove_scope_invalid", "--variant and --offer require --local", "canonical Cart 使用 cart_item_id;先从 `cart show --json` 读取 line。", [{ command: "itpay cart show --json", reason: "读取 canonical line 句柄" }]);
|
|
386
|
+
}
|
|
387
|
+
if (!session.lastCartID) {
|
|
388
|
+
throw new CommandContractError("cart_handle_missing", "no canonical server cart is remembered", "本地没有 canonical Cart 句柄;不要用 local draft 代替服务端删除。", [{ command: "itpay next --json", reason: "检查其他可恢复句柄" }]);
|
|
389
|
+
}
|
|
390
|
+
const lineID = options.line ?? session.lastCartItemID;
|
|
391
|
+
if (!lineID) {
|
|
392
|
+
throw new CommandContractError("cart_item_required", "cart item id is required", "从 `cart show --json` 选择 cart_item_id;不要使用 variant、offer 或 quote lock ID。", [{ command: "itpay cart show --json", reason: "读取 canonical line 句柄" }]);
|
|
393
|
+
}
|
|
228
394
|
const backend = newBackendClient(config);
|
|
229
|
-
await runCartRemoveServer(backend, session,
|
|
395
|
+
await runCartRemoveServer(backend, session, lineID, { jsonOutput });
|
|
230
396
|
}
|
|
231
397
|
}
|
|
232
398
|
catch (error) {
|
|
233
|
-
|
|
399
|
+
const locked = error instanceof HttpError && error.code === "cart_item_locked";
|
|
400
|
+
reportCLIError(error, {
|
|
401
|
+
jsonOutput,
|
|
402
|
+
code: "cart_remove_failed",
|
|
403
|
+
instruction: locked
|
|
404
|
+
? "该 line 已绑定 quote/Checkout,不能再删除;继续同一 Cart 的现有流程,不要清本地状态伪装成功。"
|
|
405
|
+
: "删除失败;重新读取同一 Cart,不要假设 line 或 Service Execution 已取消。",
|
|
406
|
+
recovery: [{ command: "itpay cart next --json", reason: "读取同一 Cart 的服务端首选动作" }],
|
|
407
|
+
});
|
|
234
408
|
}
|
|
235
409
|
finally {
|
|
236
410
|
session.saveToFile(sessionPath);
|
|
@@ -239,37 +413,60 @@ cart
|
|
|
239
413
|
cart
|
|
240
414
|
.command("show")
|
|
241
415
|
.description("Print the canonical server cart or local draft fallback")
|
|
242
|
-
.
|
|
416
|
+
.option("--json", "output JSON instead of terminal text")
|
|
417
|
+
.option("--local", "show only the explicit local compatibility draft")
|
|
418
|
+
.action(async (options) => {
|
|
243
419
|
const config = loadConfig();
|
|
244
|
-
const backend = newBackendClient(config);
|
|
245
420
|
const sessionPath = cartSessionPath();
|
|
246
421
|
const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
|
|
247
422
|
try {
|
|
248
|
-
|
|
423
|
+
if (options.local) {
|
|
424
|
+
runCartShow(session, { jsonOutput: Boolean(options.json) });
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
await runCartShowServer(newBackendClient(config), session, { jsonOutput: Boolean(options.json) });
|
|
428
|
+
}
|
|
249
429
|
}
|
|
250
430
|
catch (error) {
|
|
251
|
-
reportCLIError(error
|
|
431
|
+
reportCLIError(error, {
|
|
432
|
+
jsonOutput: Boolean(options.json),
|
|
433
|
+
code: "cart_show_failed",
|
|
434
|
+
instruction: "canonical Cart 无法读取;不要根据本地旧句柄猜测服务端状态。",
|
|
435
|
+
recovery: [
|
|
436
|
+
{ command: "itpay services list --json", reason: "恢复当前设备可见的 Service Execution" },
|
|
437
|
+
{ command: "itpay catalog list --json", reason: "在没有可恢复资源时重新选择" },
|
|
438
|
+
],
|
|
439
|
+
});
|
|
252
440
|
}
|
|
253
441
|
});
|
|
254
442
|
cart
|
|
255
443
|
.command("clear")
|
|
256
|
-
.description("Abandon the canonical server cart or clear local
|
|
444
|
+
.description("Abandon the canonical server cart or explicitly clear local state")
|
|
257
445
|
.option("--local", "only clear local handles and explicit local draft")
|
|
446
|
+
.option("--json", "output JSON instead of terminal text")
|
|
258
447
|
.action(async (options) => {
|
|
259
448
|
const config = loadConfig();
|
|
260
449
|
const sessionPath = cartSessionPath();
|
|
261
450
|
const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
|
|
262
451
|
try {
|
|
263
452
|
if (options.local) {
|
|
264
|
-
runCartClear(session);
|
|
453
|
+
runCartClear(session, { jsonOutput: Boolean(options.json) });
|
|
265
454
|
}
|
|
266
455
|
else {
|
|
267
456
|
const backend = newBackendClient(config);
|
|
268
|
-
await runCartAbandonServer(backend, session);
|
|
457
|
+
await runCartAbandonServer(backend, session, { jsonOutput: Boolean(options.json) });
|
|
269
458
|
}
|
|
270
459
|
}
|
|
271
460
|
catch (error) {
|
|
272
|
-
|
|
461
|
+
const locked = error instanceof HttpError && (error.code === "cart_item_locked" || error.status === 409);
|
|
462
|
+
reportCLIError(error, {
|
|
463
|
+
jsonOutput: Boolean(options.json),
|
|
464
|
+
code: "cart_clear_failed",
|
|
465
|
+
instruction: locked
|
|
466
|
+
? "该 Cart 已绑定 quote/Checkout,不能放弃;保留本地句柄并继续同一流程。"
|
|
467
|
+
: "放弃失败;不要清本地句柄或假设 Backend Cart 已改变。",
|
|
468
|
+
recovery: [{ command: "itpay cart next --json", reason: "恢复同一 canonical Cart" }],
|
|
469
|
+
});
|
|
273
470
|
}
|
|
274
471
|
finally {
|
|
275
472
|
session.saveToFile(sessionPath);
|
|
@@ -279,13 +476,13 @@ cart
|
|
|
279
476
|
program
|
|
280
477
|
.command("buy")
|
|
281
478
|
.description("Create a V3 cart and checkout, then render the checkout QR for the host")
|
|
282
|
-
.option("--host <host>", "client host (terminal, telegram, feishu, lark, ...)"
|
|
479
|
+
.option("--host <host>", "client host (terminal, telegram, feishu, lark, ...)")
|
|
283
480
|
.option("--target <target>", "chat id / channel id / open id for IM hosts")
|
|
284
481
|
.option("--item <catalog_item_id>")
|
|
285
482
|
.option("--variant <catalog_variant_id>")
|
|
286
483
|
.option("--offer <offer_id>")
|
|
287
484
|
.option("--cart <cart_id>", "existing canonical server cart id")
|
|
288
|
-
.option("--quantity <n>", "quantity",
|
|
485
|
+
.option("--quantity <n>", "quantity", "1")
|
|
289
486
|
.option("--ref <client_reference_id>")
|
|
290
487
|
.option("--contact-email <email>")
|
|
291
488
|
.option("--contact-phone <phone>")
|
|
@@ -295,48 +492,78 @@ program
|
|
|
295
492
|
.option("--pay", "also create a payment intent and optionally wait for verification")
|
|
296
493
|
.option("--method <alipay|wechatpay>", "payment method for --pay", "alipay")
|
|
297
494
|
.option("--no-wait", "do not wait for payment verification after --pay")
|
|
298
|
-
.option("--timeout <seconds>", "max seconds to wait for payment",
|
|
495
|
+
.option("--timeout <seconds>", "max seconds to wait for payment", "120")
|
|
299
496
|
.option("--json", "output JSON instead of terminal text")
|
|
300
497
|
.action(async (options) => {
|
|
301
498
|
const config = loadConfig();
|
|
302
499
|
const backend = newBackendClient(config);
|
|
303
500
|
const sessionPath = cartSessionPath();
|
|
304
501
|
const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
|
|
305
|
-
const
|
|
306
|
-
if (options.item && options.variant && options.offer) {
|
|
307
|
-
runCartAdd(session, {
|
|
308
|
-
catalogItemID: options.item,
|
|
309
|
-
catalogVariantID: options.variant,
|
|
310
|
-
offerID: options.offer,
|
|
311
|
-
quantity: options.quantity,
|
|
312
|
-
});
|
|
313
|
-
session.saveToFile(sessionPath);
|
|
314
|
-
}
|
|
315
|
-
const contact = {};
|
|
316
|
-
if (options.contactEmail)
|
|
317
|
-
contact.email = options.contactEmail;
|
|
318
|
-
if (options.contactPhone)
|
|
319
|
-
contact.phone = options.contactPhone;
|
|
320
|
-
const requiredContactFields = parseRequiredContactFields(options.requireContact);
|
|
321
|
-
const method = options.method === "wechatpay" ? "wechatpay" : "alipay";
|
|
322
|
-
const buyOptions = {
|
|
323
|
-
cartSession: session,
|
|
324
|
-
host,
|
|
325
|
-
...(options.cart ? { cartID: options.cart } : {}),
|
|
326
|
-
...(options.target ? { target: options.target } : {}),
|
|
327
|
-
...(options.ref ? { clientReferenceID: options.ref } : {}),
|
|
328
|
-
...(Object.keys(contact).length > 0 ? { contact } : {}),
|
|
329
|
-
...(requiredContactFields ? { requiredContactFields } : {}),
|
|
330
|
-
...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
|
|
331
|
-
...(options.qrFile ? { qrFilePath: options.qrFile } : {}),
|
|
332
|
-
...(options.pay ? { pay: true, payMethod: method, noWait: options.wait === false, payTimeoutSec: options.timeout } : {}),
|
|
333
|
-
...(options.json ? { jsonOutput: true } : {}),
|
|
334
|
-
};
|
|
502
|
+
const jsonOutput = Boolean(options.json);
|
|
335
503
|
try {
|
|
504
|
+
const inline = [options.item, options.variant, options.offer].filter(Boolean).length;
|
|
505
|
+
if (inline !== 0 && inline !== 3) {
|
|
506
|
+
throw new CommandContractError("buy_source_invalid", "--item, --variant and --offer must be provided together", "inline 购买必须同时使用 Catalog 返回的 item、variant 和 offer;不要猜测或部分提交。", [{ command: "itpay catalog list --json", reason: "读取已发布项目" }]);
|
|
507
|
+
}
|
|
508
|
+
if (options.cart && inline > 0) {
|
|
509
|
+
throw new CommandContractError("buy_source_invalid", "--cart cannot be combined with --item/--variant/--offer", "已有 canonical Cart 与 inline item 二选一;本次未修改任何资源。", [{ command: "itpay cart show --json", reason: "检查已有 Cart" }]);
|
|
510
|
+
}
|
|
511
|
+
if (options.method !== "alipay" && options.method !== "wechatpay") {
|
|
512
|
+
throw new CommandContractError("payment_method_invalid", `unsupported payment method: ${options.method}`, "--method 只接受 alipay 或 wechatpay。", [{ command: "itpay buy --help", reason: "查看付款参数" }]);
|
|
513
|
+
}
|
|
514
|
+
if (options.wait === false && !options.pay) {
|
|
515
|
+
throw new CommandContractError("buy_parameter_invalid", "--no-wait requires --pay", "普通 buy 不创建 Payment Intent;移除 --no-wait,或使用明确的付款运维流程。", [{ command: "itpay buy --help", reason: "查看参数关系" }]);
|
|
516
|
+
}
|
|
517
|
+
const quantity = positiveInteger(options.quantity, "--quantity");
|
|
518
|
+
const timeout = positiveInteger(options.timeout, "--timeout");
|
|
519
|
+
const host = withHost(options.host ?? defaultHostForAgentType(config.agentType));
|
|
520
|
+
const contact = {};
|
|
521
|
+
if (options.contactEmail)
|
|
522
|
+
contact.email = options.contactEmail;
|
|
523
|
+
if (options.contactPhone)
|
|
524
|
+
contact.phone = options.contactPhone;
|
|
525
|
+
const requiredContactFields = parseRequiredContactFields(options.requireContact);
|
|
526
|
+
const missingContactFields = (requiredContactFields ?? []).filter((field) => {
|
|
527
|
+
const value = contact[field];
|
|
528
|
+
return typeof value !== "string" || value.trim().length === 0;
|
|
529
|
+
});
|
|
530
|
+
if (missingContactFields.length > 0) {
|
|
531
|
+
throw new CommandContractError("missing_contact", `missing required contact fields: ${missingContactFields.join(", ")}`, `向用户询问 ${missingContactFields.join(" 和 ")},再补充对应 contact 参数重跑同一命令;禁止编造。`, [{ command: "itpay buy --help", reason: "查看 contact 参数" }]);
|
|
532
|
+
}
|
|
533
|
+
if (inline === 3) {
|
|
534
|
+
runCartAdd(session, {
|
|
535
|
+
catalogItemID: options.item,
|
|
536
|
+
catalogVariantID: options.variant,
|
|
537
|
+
offerID: options.offer,
|
|
538
|
+
quantity,
|
|
539
|
+
output: () => undefined,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
const buyOptions = {
|
|
543
|
+
cartSession: session,
|
|
544
|
+
host,
|
|
545
|
+
...(options.cart ? { cartID: options.cart } : {}),
|
|
546
|
+
...(options.target ? { target: options.target } : {}),
|
|
547
|
+
...(options.ref ? { clientReferenceID: options.ref } : {}),
|
|
548
|
+
...(Object.keys(contact).length > 0 ? { contact } : {}),
|
|
549
|
+
...(requiredContactFields ? { requiredContactFields } : {}),
|
|
550
|
+
...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
|
|
551
|
+
...(options.qrFile ? { qrFilePath: options.qrFile } : {}),
|
|
552
|
+
...(options.pay ? { pay: true, payMethod: options.method, noWait: options.wait === false, payTimeoutSec: timeout } : {}),
|
|
553
|
+
...(jsonOutput ? { jsonOutput: true } : {}),
|
|
554
|
+
};
|
|
336
555
|
await runBuy(backend, config, buyOptions);
|
|
337
556
|
}
|
|
338
557
|
catch (error) {
|
|
339
|
-
reportCLIError(error
|
|
558
|
+
reportCLIError(error, {
|
|
559
|
+
jsonOutput,
|
|
560
|
+
code: "buy_failed",
|
|
561
|
+
instruction: "Checkout 创建失败;保留当前 Cart/Checkout 句柄并按恢复命令继续,不要重复创建资源。",
|
|
562
|
+
recovery: [
|
|
563
|
+
{ command: "itpay next --json", reason: "恢复最近资源" },
|
|
564
|
+
{ command: "itpay cart next --json", reason: "检查 canonical Cart" },
|
|
565
|
+
],
|
|
566
|
+
});
|
|
340
567
|
}
|
|
341
568
|
finally {
|
|
342
569
|
session.saveToFile(sessionPath);
|
|
@@ -349,36 +576,44 @@ program
|
|
|
349
576
|
.option("--target <target>")
|
|
350
577
|
.option("--id <checkout_id>")
|
|
351
578
|
.option("--token <display_token>")
|
|
579
|
+
.option("--json", "output compact JSON")
|
|
352
580
|
.action(async (options) => {
|
|
353
581
|
const config = loadConfig();
|
|
354
582
|
const host = withHost(options.host ?? defaultHostForAgentType(config.agentType));
|
|
355
583
|
const session = CartSession.loadFromFile(cartSessionPath(), config.checkoutCurrency);
|
|
356
584
|
const snap = session.show();
|
|
357
|
-
const { checkoutID, displayToken } = resolveCheckoutPresentationArgs({
|
|
358
|
-
...(options.id ? { requestedCheckoutID: options.id } : {}),
|
|
359
|
-
...(options.token ? { requestedDisplayToken: options.token } : {}),
|
|
360
|
-
...(snap.lastCheckoutID ? { savedCheckoutID: snap.lastCheckoutID } : {}),
|
|
361
|
-
...(snap.lastDisplayToken ? { savedDisplayToken: snap.lastDisplayToken } : {}),
|
|
362
|
-
});
|
|
363
585
|
const backend = newBackendClient(config);
|
|
364
586
|
try {
|
|
587
|
+
const { checkoutID, displayToken } = resolveCheckoutPresentationArgs({
|
|
588
|
+
...(options.id ? { requestedCheckoutID: options.id } : {}),
|
|
589
|
+
...(options.token ? { requestedDisplayToken: options.token } : {}),
|
|
590
|
+
...(snap.lastCheckoutID ? { savedCheckoutID: snap.lastCheckoutID } : {}),
|
|
591
|
+
...(snap.lastDisplayToken ? { savedDisplayToken: snap.lastDisplayToken } : {}),
|
|
592
|
+
});
|
|
365
593
|
await runCheckoutPresentation(backend, {
|
|
366
594
|
checkoutID,
|
|
367
595
|
displayToken,
|
|
368
596
|
host,
|
|
369
597
|
baseURL: config.baseURL,
|
|
598
|
+
jsonOutput: Boolean(options.json),
|
|
370
599
|
});
|
|
371
600
|
}
|
|
372
601
|
catch (error) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
602
|
+
const canResumeSavedService = Boolean(snap.lastServiceExecutionID && (!options.id || options.id === snap.lastCheckoutID));
|
|
603
|
+
const recovery = canResumeSavedService
|
|
604
|
+
? [{
|
|
605
|
+
command: `itpay services checkout ${snap.lastServiceExecutionID} --resume --json`,
|
|
606
|
+
reason: "为同一个 Service Execution 轮换 Checkout handoff",
|
|
607
|
+
}]
|
|
608
|
+
: [{ command: "itpay services list --json", reason: "查找当前设备可恢复的 Service Execution" }];
|
|
609
|
+
reportCLIError(error, {
|
|
610
|
+
jsonOutput: Boolean(options.json),
|
|
611
|
+
code: "checkout_unavailable",
|
|
612
|
+
instruction: error instanceof HttpError && error.status === 404
|
|
613
|
+
? "当前 Checkout 句柄已失效或不匹配;恢复原 Service Execution,不要创建无关购物车。"
|
|
614
|
+
: "使用同一笔 Checkout 的完整 checkout_id 与 display token;不要拼接不同 Checkout 的句柄。",
|
|
615
|
+
recovery,
|
|
616
|
+
});
|
|
382
617
|
}
|
|
383
618
|
});
|
|
384
619
|
program
|
|
@@ -386,66 +621,216 @@ program
|
|
|
386
621
|
.description("Create a V3 payment intent (CLI escape hatch — usually done by the checkout page)")
|
|
387
622
|
.requiredOption("--checkout <checkout_id>")
|
|
388
623
|
.requiredOption("--method <alipay|wechatpay>")
|
|
389
|
-
.option("--
|
|
390
|
-
.option("--buyer <buyer_id>")
|
|
624
|
+
.option("--token <display_token>", "checkout display token; defaults only from the same saved checkout")
|
|
391
625
|
.option("--refresh", "request a fresh provider payment action for the existing intent")
|
|
626
|
+
.option("--host <host>", "client host")
|
|
627
|
+
.option("--target <target>")
|
|
628
|
+
.option("--json", "output compact JSON")
|
|
392
629
|
.action(async (options) => {
|
|
393
630
|
const config = loadConfig();
|
|
394
631
|
const backend = newBackendClient(config);
|
|
395
|
-
const
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
method
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
632
|
+
const session = CartSession.loadFromFile(cartSessionPath(), config.checkoutCurrency);
|
|
633
|
+
const jsonOutput = Boolean(options.json);
|
|
634
|
+
try {
|
|
635
|
+
if (options.method !== "alipay" && options.method !== "wechatpay") {
|
|
636
|
+
throw new CommandContractError("payment_method_invalid", `unsupported payment method: ${options.method}`, "--method 只接受 alipay 或 wechatpay;本次未创建 Payment Intent。", [{ command: "itpay pay --help", reason: "查看受支持参数" }]);
|
|
637
|
+
}
|
|
638
|
+
const displayToken = options.token ?? (session.lastCheckoutID === options.checkout ? session.lastDisplayToken : undefined);
|
|
639
|
+
if (!displayToken) {
|
|
640
|
+
throw new CommandContractError("checkout_token_required", "display token is required for this checkout", "提供同一 Checkout 的 display token;不要拼接其他 Checkout 的 token。", [{ command: "itpay next --json", reason: "恢复本机保存的同一 Checkout" }]);
|
|
641
|
+
}
|
|
642
|
+
const host = withHost(options.host ?? defaultHostForAgentType(config.agentType));
|
|
643
|
+
const contextError = validateContext(host, options.target);
|
|
644
|
+
if (contextError) {
|
|
645
|
+
throw new CommandContractError(contextError.code, contextError.message, "为当前 Host 提供有效 target;本次未创建 Payment Intent。", [
|
|
646
|
+
{ command: "itpay pay --help", reason: "查看 Host 参数" },
|
|
647
|
+
]);
|
|
648
|
+
}
|
|
649
|
+
await runPay(backend, {
|
|
650
|
+
checkoutID: options.checkout,
|
|
651
|
+
displayToken,
|
|
652
|
+
method: options.method,
|
|
653
|
+
host,
|
|
654
|
+
...(options.refresh ? { refreshAction: true } : {}),
|
|
655
|
+
...(jsonOutput ? { jsonOutput: true } : {}),
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
reportCLIError(error, {
|
|
660
|
+
jsonOutput,
|
|
661
|
+
code: "payment_intent_failed",
|
|
662
|
+
instruction: "不要创建替代 Checkout;恢复同一 Checkout 并由用户在 ItPay 页面继续付款。",
|
|
663
|
+
recovery: [{ command: "itpay next --json", reason: "恢复当前 Checkout" }],
|
|
664
|
+
});
|
|
665
|
+
}
|
|
404
666
|
});
|
|
405
667
|
program
|
|
406
668
|
.command("order")
|
|
407
669
|
.description("Read a V3 order by id")
|
|
408
670
|
.argument("<order_id>")
|
|
409
671
|
.option("--host <host>", "client host")
|
|
672
|
+
.option("--json", "output JSON instead of terminal text")
|
|
410
673
|
.action(async (orderID, options) => {
|
|
411
|
-
|
|
412
|
-
|
|
674
|
+
if (options.host)
|
|
675
|
+
withHost(options.host);
|
|
676
|
+
const config = loadConfig();
|
|
677
|
+
const backend = newBackendClient(config);
|
|
678
|
+
try {
|
|
679
|
+
await runOrder(backend, orderID, { ...(options.host ? { host: options.host } : {}), jsonOutput: Boolean(options.json) });
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
reportCLIError(error, {
|
|
683
|
+
jsonOutput: Boolean(options.json),
|
|
684
|
+
code: "order_read_failed",
|
|
685
|
+
instruction: "确认订单属于当前账号或已绑定 Agent;不要通过错误差异探测其他账号的订单。",
|
|
686
|
+
recovery: [{ command: "itpay services list --json", reason: "恢复当前身份可见的 Service Execution" }],
|
|
687
|
+
});
|
|
688
|
+
}
|
|
413
689
|
});
|
|
414
690
|
program
|
|
415
691
|
.command("orders")
|
|
416
692
|
.description("List V3 orders for the account-scoped bearer session")
|
|
417
693
|
.option("--limit <n>", "max orders", (value) => Number.parseInt(value, 10), 20)
|
|
418
694
|
.option("--status <status>")
|
|
695
|
+
.option("--json", "output JSON instead of terminal text")
|
|
419
696
|
.action(async (options) => {
|
|
420
697
|
const config = loadConfig();
|
|
421
698
|
const backend = newBackendClient(config);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
699
|
+
try {
|
|
700
|
+
await runListOrders(backend, config, {
|
|
701
|
+
limit: options.limit,
|
|
702
|
+
status: options.status,
|
|
703
|
+
jsonOutput: Boolean(options.json),
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
reportCLIError(error, {
|
|
708
|
+
jsonOutput: Boolean(options.json),
|
|
709
|
+
code: "orders_list_failed",
|
|
710
|
+
instruction: "订单历史只对 account-scoped Buyer session 开放;不要通过错误差异探测其他账号。",
|
|
711
|
+
recovery: [{ command: "itpay services list --json", reason: "恢复当前 Agent 设备可见的执行" }],
|
|
712
|
+
});
|
|
713
|
+
}
|
|
426
714
|
});
|
|
427
|
-
program
|
|
715
|
+
const refund = program
|
|
428
716
|
.command("refund")
|
|
717
|
+
.enablePositionalOptions()
|
|
429
718
|
.description("Create a V3 refund request for an order")
|
|
430
|
-
.
|
|
431
|
-
.requiredOption("--payment-intent <payment_intent_id>")
|
|
432
|
-
.requiredOption("--amount-minor <n>", "amount in minor units", (value) => Number.parseInt(value, 10))
|
|
433
|
-
.requiredOption("--currency <currency>")
|
|
719
|
+
.option("--order <order_id>")
|
|
434
720
|
.option("--reason <reason>")
|
|
435
|
-
.option("--
|
|
721
|
+
.option("--json", "output JSON instead of terminal text")
|
|
436
722
|
.action(async (options) => {
|
|
723
|
+
if (!options.order) {
|
|
724
|
+
process.stdout.write(refund.helpInformation());
|
|
725
|
+
process.stdout.write("\ninstruction: 使用 `itpay refund create --order <order_id>` 提交退款;本次未发送请求。\n");
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
await executeRefundCreate(options.order, options.reason, Boolean(options.json));
|
|
729
|
+
});
|
|
730
|
+
refund.command("create").option("--order <order_id>").option("--reason <reason>").option("--json", "output JSON instead of terminal text").action(async (options) => {
|
|
731
|
+
const inherited = refund.opts();
|
|
732
|
+
const orderID = options.order ?? inherited.order;
|
|
733
|
+
if (!orderID) {
|
|
734
|
+
reportCLIError(new Error("--order is required"), {
|
|
735
|
+
jsonOutput: Boolean(options.json ?? inherited.json),
|
|
736
|
+
code: "order_required",
|
|
737
|
+
instruction: "使用用户订单的 order_id;不要猜测或代填。",
|
|
738
|
+
recovery: [{ command: "itpay services list --json", reason: "恢复当前身份可见的 Service Execution" }],
|
|
739
|
+
});
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
const reason = options.reason ?? inherited.reason;
|
|
743
|
+
await executeRefundCreate(orderID, reason, Boolean(options.json ?? inherited.json));
|
|
744
|
+
});
|
|
745
|
+
refund.command("list").option("--order <order_id>").option("--json", "output JSON instead of terminal text").action(async (options) => {
|
|
746
|
+
const inherited = refund.opts();
|
|
747
|
+
const orderID = options.order ?? inherited.order;
|
|
748
|
+
const jsonOutput = Boolean(options.json ?? inherited.json);
|
|
749
|
+
if (!orderID) {
|
|
750
|
+
reportCLIError(new Error("--order is required"), {
|
|
751
|
+
jsonOutput,
|
|
752
|
+
code: "order_required",
|
|
753
|
+
instruction: "使用用户订单的 order_id;不要猜测或代填。",
|
|
754
|
+
recovery: [{ command: "itpay services list --json", reason: "恢复当前身份可见的 Service Execution" }],
|
|
755
|
+
});
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
437
758
|
const config = loadConfig();
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
759
|
+
try {
|
|
760
|
+
await runListRefunds(newBackendClient(config), { orderID, jsonOutput });
|
|
761
|
+
}
|
|
762
|
+
catch (error) {
|
|
763
|
+
reportCLIError(error, {
|
|
764
|
+
jsonOutput,
|
|
765
|
+
code: "refund_list_failed",
|
|
766
|
+
instruction: "确认订单属于当前账号或已绑定 Agent;不要探测其他账号的退款。",
|
|
767
|
+
recovery: [{ command: "itpay services list --json", reason: "恢复当前身份可见的 Service Execution" }],
|
|
768
|
+
});
|
|
769
|
+
}
|
|
448
770
|
});
|
|
771
|
+
refund.command("get").argument("<refund_request_id>").option("--json", "output JSON instead of terminal text").action(async (id, options) => {
|
|
772
|
+
const config = loadConfig();
|
|
773
|
+
try {
|
|
774
|
+
await runGetRefund(newBackendClient(config), id, { jsonOutput: Boolean(options.json ?? refund.opts().json) });
|
|
775
|
+
}
|
|
776
|
+
catch (error) {
|
|
777
|
+
reportCLIError(error, {
|
|
778
|
+
jsonOutput: Boolean(options.json ?? refund.opts().json),
|
|
779
|
+
code: "refund_read_failed",
|
|
780
|
+
instruction: "确认退款属于当前账号或已绑定 Agent;不要探测其他账号的退款。",
|
|
781
|
+
recovery: [{ command: "itpay services list --json", reason: "恢复当前身份可见的 Service Execution" }],
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
refund.command("watch").argument("<refund_request_id>").option("--interval <seconds>", "poll interval", Number, 2).option("--timeout <seconds>", "timeout", Number, 120).option("--json", "output JSON instead of terminal text").action(async (id, options) => {
|
|
786
|
+
const config = loadConfig();
|
|
787
|
+
try {
|
|
788
|
+
await runWatchRefund(newBackendClient(config), id, {
|
|
789
|
+
intervalSeconds: options.interval,
|
|
790
|
+
timeoutSeconds: options.timeout,
|
|
791
|
+
jsonOutput: Boolean(options.json ?? refund.opts().json),
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
reportCLIError(error, {
|
|
796
|
+
jsonOutput: Boolean(options.json ?? refund.opts().json),
|
|
797
|
+
code: "refund_watch_failed",
|
|
798
|
+
instruction: "检查退款 ID 和轮询参数后恢复同一退款;不要重复申请。",
|
|
799
|
+
recovery: [{ command: `itpay refund get ${id} --json`, reason: "读取当前权威状态" }],
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
refund.command("cancel").argument("<refund_request_id>").option("--reason <reason>").option("--json", "output JSON instead of terminal text").action(async (id, options) => {
|
|
804
|
+
const config = loadConfig();
|
|
805
|
+
try {
|
|
806
|
+
await runCancelRefund(newBackendClient(config), id, options.reason, { jsonOutput: Boolean(options.json ?? refund.opts().json) });
|
|
807
|
+
}
|
|
808
|
+
catch (error) {
|
|
809
|
+
reportCLIError(error, {
|
|
810
|
+
jsonOutput: Boolean(options.json ?? refund.opts().json),
|
|
811
|
+
code: "refund_cancel_failed",
|
|
812
|
+
instruction: "取消未生效;以 Refund Owner 当前状态为准,不要重复退款或自行解除交付锁。",
|
|
813
|
+
recovery: [{ command: `itpay refund get ${id} --json`, reason: "读取当前权威状态" }],
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
async function executeRefundCreate(orderID, reason, jsonOutput) {
|
|
818
|
+
const config = loadConfig();
|
|
819
|
+
try {
|
|
820
|
+
await runRefund(newBackendClient(config), config, { orderID, ...(reason ? { reason } : {}), jsonOutput });
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
reportCLIError(error, {
|
|
824
|
+
jsonOutput,
|
|
825
|
+
code: "refund_create_failed",
|
|
826
|
+
instruction: "确认订单属于当前账号且可退款;不要修改金额、支付或消费事实。",
|
|
827
|
+
recovery: [
|
|
828
|
+
{ command: `itpay order ${orderID} --json`, reason: "检查订单和交付锁" },
|
|
829
|
+
{ command: `itpay refund list --order ${orderID} --json`, reason: "检查已有退款" },
|
|
830
|
+
],
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
449
834
|
// --- service execution ----------------------------------------------------
|
|
450
835
|
const services = program.command("services").description("Generic V3 Service Execution commands");
|
|
451
836
|
services
|
|
@@ -454,15 +839,28 @@ services
|
|
|
454
839
|
.argument("<service_id>")
|
|
455
840
|
.option("--host <host>", "client host")
|
|
456
841
|
.option("--target <target>")
|
|
457
|
-
.option("--
|
|
842
|
+
.option("--json", "output JSON instead of terminal text")
|
|
458
843
|
.action(async (serviceID, options) => {
|
|
459
844
|
const config = loadConfig();
|
|
460
845
|
const backend = newBackendClient(config);
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
846
|
+
try {
|
|
847
|
+
await runServicesStart(backend, serviceID, {
|
|
848
|
+
host: withHost(options.host ?? defaultHostForAgentType(config.agentType)),
|
|
849
|
+
...(options.target ? { target: options.target } : {}),
|
|
850
|
+
jsonOutput: Boolean(options.json),
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
catch (error) {
|
|
854
|
+
reportCLIError(error, {
|
|
855
|
+
jsonOutput: Boolean(options.json),
|
|
856
|
+
code: "service_start_failed",
|
|
857
|
+
instruction: "只使用已发布 Catalog 返回的 service_id;设备身份问题应由 CLI 自动恢复。",
|
|
858
|
+
recovery: [
|
|
859
|
+
{ command: "itpay catalog list", reason: "重新取得有效 service_id" },
|
|
860
|
+
{ command: "itpay readyz", reason: "确认 Backend 可用" },
|
|
861
|
+
],
|
|
862
|
+
});
|
|
863
|
+
}
|
|
466
864
|
});
|
|
467
865
|
services
|
|
468
866
|
.command("invoke")
|
|
@@ -474,7 +872,20 @@ services
|
|
|
474
872
|
.action(async (serviceExecutionID, options) => {
|
|
475
873
|
const config = loadConfig();
|
|
476
874
|
const backend = newBackendClient(config);
|
|
477
|
-
|
|
875
|
+
try {
|
|
876
|
+
await runServicesInvoke(backend, config, serviceExecutionID, options.capability, parseKeyValueList(options.input), { jsonOutput: Boolean(options.json) });
|
|
877
|
+
}
|
|
878
|
+
catch (error) {
|
|
879
|
+
reportCLIError(error, {
|
|
880
|
+
jsonOutput: Boolean(options.json),
|
|
881
|
+
code: "service_invoke_failed",
|
|
882
|
+
instruction: "读取当前 Service Execution 的合法下一步后重试;不要复用已结束的 execution。",
|
|
883
|
+
recovery: [
|
|
884
|
+
{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前合法动作" },
|
|
885
|
+
{ command: `itpay services get ${serviceExecutionID} --json`, reason: "检查执行状态" },
|
|
886
|
+
],
|
|
887
|
+
});
|
|
888
|
+
}
|
|
478
889
|
});
|
|
479
890
|
services
|
|
480
891
|
.command("action")
|
|
@@ -486,27 +897,63 @@ services
|
|
|
486
897
|
.option("--status <status>", "pending, approved, rejected, expired, or cancelled")
|
|
487
898
|
.option("--candidate <rank>", "select a displayed candidate by its rank", Number)
|
|
488
899
|
.option("--result-item <service_capability_result_item_id>")
|
|
489
|
-
.option("--selected-candidate-hash <hash>")
|
|
490
900
|
.option("--required-before <step>")
|
|
491
901
|
.option("--input <key=value>", "action input snapshot", collectOption, [])
|
|
902
|
+
.option("--json", "output JSON instead of terminal text")
|
|
492
903
|
.action(async (serviceExecutionID, options) => {
|
|
493
904
|
const config = loadConfig();
|
|
494
905
|
const backend = newBackendClient(config);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
906
|
+
try {
|
|
907
|
+
await runServicesAction(backend, serviceExecutionID, options.action, parseKeyValueList(options.input), {
|
|
908
|
+
...(options.actorType ? { actorType: options.actorType } : {}),
|
|
909
|
+
...(options.actorId ? { actorID: options.actorId } : {}),
|
|
910
|
+
...(options.status ? { status: options.status } : {}),
|
|
911
|
+
...(options.candidate !== undefined ? { candidateRank: options.candidate } : {}),
|
|
912
|
+
...(options.resultItem ? { resultItemID: options.resultItem } : {}),
|
|
913
|
+
...(options.requiredBefore ? { requiredBefore: options.requiredBefore } : {}),
|
|
914
|
+
jsonOutput: Boolean(options.json),
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
catch (error) {
|
|
918
|
+
reportCLIError(error, {
|
|
919
|
+
jsonOutput: Boolean(options.json),
|
|
920
|
+
code: "service_action_failed",
|
|
921
|
+
instruction: "读取当前 Service Execution 的合法 action 后重试;不要猜测状态或候选 ID。",
|
|
922
|
+
recovery: [
|
|
923
|
+
{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前可选动作" },
|
|
924
|
+
{ command: `itpay services get ${serviceExecutionID} --json`, reason: "检查执行状态" },
|
|
925
|
+
],
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
services
|
|
930
|
+
.command("quote")
|
|
931
|
+
.description("Prepare a paid service quote without creating a Cart or Checkout")
|
|
932
|
+
.argument("<service_execution_id>")
|
|
933
|
+
.requiredOption("--capability <capability_id>")
|
|
934
|
+
.option("--input <key=value>", "input to lock into the paid service quote", collectOption, [])
|
|
935
|
+
.option("--email <delivery_email>")
|
|
936
|
+
.option("--json", "output compact JSON")
|
|
937
|
+
.action(async (serviceExecutionID, options) => {
|
|
938
|
+
const config = loadConfig();
|
|
939
|
+
try {
|
|
940
|
+
await runServicesQuote(newBackendClient(config), serviceExecutionID, options.capability, parseKeyValueList(options.input), { ...(options.email ? { email: options.email } : {}), jsonOutput: Boolean(options.json) });
|
|
941
|
+
}
|
|
942
|
+
catch (error) {
|
|
943
|
+
reportCLIError(error, {
|
|
944
|
+
jsonOutput: Boolean(options.json),
|
|
945
|
+
code: "service_quote_failed",
|
|
946
|
+
instruction: "按当前 Execution 的合法付费 capability 和可信输入重试;本次不要自行创建 Cart 或 Checkout。",
|
|
947
|
+
recovery: [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前合法动作" }],
|
|
948
|
+
});
|
|
949
|
+
}
|
|
504
950
|
});
|
|
505
951
|
services
|
|
506
952
|
.command("checkout")
|
|
507
953
|
.description("Create checkout from a service execution and render the ItPay checkout handoff")
|
|
508
954
|
.argument("<service_execution_id>")
|
|
509
955
|
.option("--capability <capability_id>")
|
|
956
|
+
.option("--input <key=value>", "input to lock into the paid service quote", collectOption, [])
|
|
510
957
|
.option("--email <delivery_email>")
|
|
511
958
|
.option("--resume", "reissue the existing checkout handoff without creating another checkout")
|
|
512
959
|
.option("--host <host>", "client host (terminal, codex, telegram, feishu, lark, ...)")
|
|
@@ -519,35 +966,61 @@ services
|
|
|
519
966
|
const backend = newBackendClient(config);
|
|
520
967
|
const sessionPath = cartSessionPath();
|
|
521
968
|
const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
969
|
+
try {
|
|
970
|
+
await runServicesCheckout(backend, config, serviceExecutionID, options.capability, {
|
|
971
|
+
...(options.email ? { email: options.email } : {}),
|
|
972
|
+
lockedInput: parseKeyValueList(options.input),
|
|
973
|
+
resume: Boolean(options.resume),
|
|
974
|
+
host: withHost(options.host ?? defaultHostForAgentType(config.agentType)),
|
|
975
|
+
...(options.target ? { target: options.target } : {}),
|
|
976
|
+
...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
|
|
977
|
+
...(options.qrFile ? { qrFilePath: options.qrFile } : {}),
|
|
978
|
+
jsonOutput: Boolean(options.json),
|
|
979
|
+
persistHandoff: (handoff) => {
|
|
980
|
+
session.rememberCheckout({
|
|
981
|
+
checkoutID: handoff.checkoutID,
|
|
982
|
+
displayToken: handoff.displayToken,
|
|
983
|
+
checkoutURL: handoff.checkoutURL,
|
|
984
|
+
serviceExecutionID: handoff.serviceExecutionID,
|
|
985
|
+
});
|
|
986
|
+
session.saveToFile(sessionPath);
|
|
987
|
+
},
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
catch (error) {
|
|
991
|
+
reportCLIError(error, {
|
|
992
|
+
jsonOutput: Boolean(options.json),
|
|
993
|
+
code: "service_checkout_failed",
|
|
994
|
+
instruction: "读取当前 Service Execution 后按服务端允许的 capability、输入和交付要求重试;不要创建替代 Checkout。",
|
|
995
|
+
recovery: [
|
|
996
|
+
{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前合法下一步" },
|
|
997
|
+
{ command: `itpay services get ${serviceExecutionID} --json`, reason: "检查 Checkout 与执行状态" },
|
|
998
|
+
],
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
541
1001
|
});
|
|
542
1002
|
services
|
|
543
1003
|
.command("list")
|
|
544
1004
|
.description("Recover service executions visible to this enrolled device or account")
|
|
545
|
-
.option("--limit <number>", "maximum executions", "
|
|
1005
|
+
.option("--limit <number>", "maximum executions", "10")
|
|
546
1006
|
.option("--json", "output compact JSON")
|
|
547
1007
|
.action(async (options) => {
|
|
548
1008
|
const config = loadConfig();
|
|
549
1009
|
const backend = newBackendClient(config);
|
|
550
|
-
|
|
1010
|
+
try {
|
|
1011
|
+
await runServicesList(backend, { limit: Number.parseInt(options.limit, 10), jsonOutput: Boolean(options.json) });
|
|
1012
|
+
}
|
|
1013
|
+
catch (error) {
|
|
1014
|
+
reportCLIError(error, {
|
|
1015
|
+
jsonOutput: Boolean(options.json),
|
|
1016
|
+
code: "services_list_failed",
|
|
1017
|
+
instruction: "确认当前设备身份和 Backend 后重试;不要猜测 Service Execution ID。",
|
|
1018
|
+
recovery: [
|
|
1019
|
+
{ command: "itpay readyz --json", reason: "确认 Backend 可用" },
|
|
1020
|
+
{ command: "itpay services list --limit 10 --json", reason: "重新读取最近执行" },
|
|
1021
|
+
],
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
551
1024
|
});
|
|
552
1025
|
services
|
|
553
1026
|
.command("get")
|
|
@@ -557,7 +1030,17 @@ services
|
|
|
557
1030
|
.action(async (serviceExecutionID, options) => {
|
|
558
1031
|
const config = loadConfig();
|
|
559
1032
|
const backend = newBackendClient(config);
|
|
560
|
-
|
|
1033
|
+
try {
|
|
1034
|
+
await runServicesGet(backend, serviceExecutionID, { jsonOutput: Boolean(options.json) });
|
|
1035
|
+
}
|
|
1036
|
+
catch (error) {
|
|
1037
|
+
reportCLIError(error, {
|
|
1038
|
+
jsonOutput: Boolean(options.json),
|
|
1039
|
+
code: "service_get_failed",
|
|
1040
|
+
instruction: "确认 execution 属于当前设备或账号;不要通过错误差异探测其他账号。",
|
|
1041
|
+
recovery: [{ command: "itpay services list --json", reason: "读取当前身份可见的执行" }],
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
561
1044
|
});
|
|
562
1045
|
services
|
|
563
1046
|
.command("next")
|
|
@@ -567,40 +1050,67 @@ services
|
|
|
567
1050
|
.action(async (serviceExecutionID, options) => {
|
|
568
1051
|
const config = loadConfig();
|
|
569
1052
|
const backend = newBackendClient(config);
|
|
570
|
-
|
|
1053
|
+
try {
|
|
1054
|
+
await runServicesNext(backend, serviceExecutionID, { jsonOutput: Boolean(options.json) });
|
|
1055
|
+
}
|
|
1056
|
+
catch (error) {
|
|
1057
|
+
reportCLIError(error, {
|
|
1058
|
+
jsonOutput: Boolean(options.json),
|
|
1059
|
+
code: "service_next_failed",
|
|
1060
|
+
instruction: "检查 Service Execution 是否属于当前设备或账号,然后读取完整时间线。",
|
|
1061
|
+
recovery: [{ command: `itpay services get ${serviceExecutionID} --json`, reason: "检查执行状态与归属" }],
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
571
1064
|
});
|
|
572
1065
|
services
|
|
573
1066
|
.command("read-result")
|
|
574
1067
|
.description("Read a human-granted service result for this agent")
|
|
575
1068
|
.argument("<service_execution_id>")
|
|
576
|
-
.
|
|
1069
|
+
.option("--json", "output JSON instead of terminal text")
|
|
1070
|
+
.action(async (serviceExecutionID, options) => {
|
|
577
1071
|
const config = loadConfig();
|
|
578
1072
|
const backend = newBackendClient(config);
|
|
579
|
-
|
|
1073
|
+
try {
|
|
1074
|
+
await runServicesReadResult(backend, serviceExecutionID, { jsonOutput: Boolean(options.json) });
|
|
1075
|
+
}
|
|
1076
|
+
catch (error) {
|
|
1077
|
+
reportCLIError(error, {
|
|
1078
|
+
jsonOutput: Boolean(options.json),
|
|
1079
|
+
code: "agent_access_denied",
|
|
1080
|
+
instruction: "请用户在订单页面重新授权;不要使用开发者权限绕过授权或退款锁。",
|
|
1081
|
+
recovery: [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "检查交付模式和 grant 状态" }],
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
580
1084
|
});
|
|
581
1085
|
services
|
|
582
1086
|
.command("events")
|
|
583
1087
|
.description("List redacted service execution events")
|
|
584
1088
|
.argument("<service_execution_id>")
|
|
585
|
-
.
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
});
|
|
590
|
-
async function withBackend(action) {
|
|
1089
|
+
.option("--after-sequence <number>", "return events after this sequence", "0")
|
|
1090
|
+
.option("--limit <number>", "maximum events (1-100)", "50")
|
|
1091
|
+
.option("--json", "output compact JSON")
|
|
1092
|
+
.action(async (serviceExecutionID, options) => {
|
|
591
1093
|
const config = loadConfig();
|
|
592
1094
|
const backend = newBackendClient(config);
|
|
593
1095
|
try {
|
|
594
|
-
await
|
|
1096
|
+
await runServicesEvents(backend, serviceExecutionID, {
|
|
1097
|
+
afterSequence: Number(options.afterSequence),
|
|
1098
|
+
limit: Number(options.limit),
|
|
1099
|
+
jsonOutput: Boolean(options.json),
|
|
1100
|
+
});
|
|
595
1101
|
}
|
|
596
1102
|
catch (error) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
1103
|
+
reportCLIError(error, {
|
|
1104
|
+
jsonOutput: Boolean(options.json),
|
|
1105
|
+
code: "service_events_failed",
|
|
1106
|
+
instruction: "确认 execution 属于当前身份;事件只用于诊断,不要据此重放业务步骤。",
|
|
1107
|
+
recovery: [
|
|
1108
|
+
{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前业务动作" },
|
|
1109
|
+
{ command: "itpay services list --json", reason: "列出当前身份可见执行" },
|
|
1110
|
+
],
|
|
1111
|
+
});
|
|
602
1112
|
}
|
|
603
|
-
}
|
|
1113
|
+
});
|
|
604
1114
|
program.parseAsync(process.argv).catch((error) => {
|
|
605
1115
|
reportCLIError(error);
|
|
606
1116
|
});
|