@itpay/cli 2.0.3 → 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.
Files changed (70) hide show
  1. package/README.md +92 -142
  2. package/dist/src/client/backend.js +26 -8
  3. package/dist/src/commands/buy.js +84 -132
  4. package/dist/src/commands/cart.js +274 -169
  5. package/dist/src/commands/catalog.js +63 -38
  6. package/dist/src/commands/checkout.js +117 -78
  7. package/dist/src/commands/docs.js +97 -51
  8. package/dist/src/commands/guidance.js +87 -15
  9. package/dist/src/commands/install.js +50 -87
  10. package/dist/src/commands/next.js +45 -0
  11. package/dist/src/commands/order.js +44 -69
  12. package/dist/src/commands/orders.js +43 -15
  13. package/dist/src/commands/pay.js +51 -22
  14. package/dist/src/commands/readyz.js +8 -4
  15. package/dist/src/commands/refund.js +132 -11
  16. package/dist/src/commands/services.js +719 -148
  17. package/dist/src/main.js +711 -193
  18. package/dist/src/render/output.js +2 -3
  19. package/dist/src/state/cart_session.js +13 -17
  20. package/dist/src/state/client_context.js +4 -2
  21. package/dist/src/state/config.js +2 -4
  22. package/dist/src/state/device_authority.js +1 -1
  23. package/docs/agent/buyer/cart-checkout.json +27 -83
  24. package/docs/agent/buyer/install-and-setup.json +23 -67
  25. package/docs/agent/buyer/orders-refunds.json +31 -53
  26. package/docs/agent/buyer/payment-flow.json +24 -57
  27. package/docs/agent/buyer/quickstart.json +39 -162
  28. package/docs/agent/buyer/render-hosts.json +43 -57
  29. package/docs/cli-reference/agent-types.md +45 -0
  30. package/docs/cli-reference/commands/buy.md +167 -0
  31. package/docs/cli-reference/commands/cart/add.md +86 -0
  32. package/docs/cli-reference/commands/cart/clear.md +53 -0
  33. package/docs/cli-reference/commands/cart/index.md +30 -0
  34. package/docs/cli-reference/commands/cart/next.md +71 -0
  35. package/docs/cli-reference/commands/cart/remove.md +53 -0
  36. package/docs/cli-reference/commands/cart/show.md +65 -0
  37. package/docs/cli-reference/commands/catalog/index.md +26 -0
  38. package/docs/cli-reference/commands/catalog/list.md +45 -0
  39. package/docs/cli-reference/commands/checkout.md +74 -0
  40. package/docs/cli-reference/commands/docs/index.md +28 -0
  41. package/docs/cli-reference/commands/docs/list.md +51 -0
  42. package/docs/cli-reference/commands/docs/search.md +69 -0
  43. package/docs/cli-reference/commands/docs/show.md +68 -0
  44. package/docs/cli-reference/commands/install.md +112 -0
  45. package/docs/cli-reference/commands/next.md +87 -0
  46. package/docs/cli-reference/commands/order.md +92 -0
  47. package/docs/cli-reference/commands/orders.md +83 -0
  48. package/docs/cli-reference/commands/pay.md +103 -0
  49. package/docs/cli-reference/commands/readyz.md +39 -0
  50. package/docs/cli-reference/commands/refund/cancel.md +62 -0
  51. package/docs/cli-reference/commands/refund/create.md +85 -0
  52. package/docs/cli-reference/commands/refund/get.md +60 -0
  53. package/docs/cli-reference/commands/refund/index.md +33 -0
  54. package/docs/cli-reference/commands/refund/list.md +68 -0
  55. package/docs/cli-reference/commands/refund/watch.md +73 -0
  56. package/docs/cli-reference/commands/services/action.md +43 -0
  57. package/docs/cli-reference/commands/services/checkout.md +82 -0
  58. package/docs/cli-reference/commands/services/events.md +73 -0
  59. package/docs/cli-reference/commands/services/get.md +66 -0
  60. package/docs/cli-reference/commands/services/index.md +45 -0
  61. package/docs/cli-reference/commands/services/invoke.md +67 -0
  62. package/docs/cli-reference/commands/services/list.md +61 -0
  63. package/docs/cli-reference/commands/services/next.md +162 -0
  64. package/docs/cli-reference/commands/services/quote.md +59 -0
  65. package/docs/cli-reference/commands/services/read-result.md +98 -0
  66. package/docs/cli-reference/commands/services/start.md +53 -0
  67. package/docs/cli-reference/conventions.md +94 -0
  68. package/docs/cli-reference/index.md +64 -0
  69. package/package.json +1 -1
  70. 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 { collectOption, parseKeyValueList, runServicesAction, runServicesCheckout, runServicesEvents, runServicesGet, runServicesInvoke, runServicesList, runServicesNext, runServicesReadResult, runServicesStart, } from "./commands/services.js";
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.3");
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 parsed = value
40
+ const values = value
40
41
  .split(",")
41
42
  .map((item) => item.trim().toLowerCase())
42
- .filter((item) => item === "email" || item === "phone");
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
- .action(async () => {
86
- await withBackend(async (backend) => runReadyz(backend));
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
- try {
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
- await withBackend(async (backend) => runCatalogList(backend, { jsonOutput: Boolean(options.json) }));
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]", "install target: claude-code, codex, terminal, telegram, feishu")
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
- runInstall(target);
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
- .action(() => {
126
- runDocsList();
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
- .action((topic) => {
133
- runDocsShow(topic);
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
- .action((query) => {
140
- runDocsSearch(query);
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
- .requiredOption("--item <catalog_item_id>")
148
- .requiredOption("--variant <catalog_variant_id>")
149
- .requiredOption("--offer <offer_id>")
150
- .option("--quantity <n>", "quantity", (value) => Number.parseInt(value, 10), 1)
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, ...)", "terminal")
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 addOptions = {
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
- await runCartAddServer({
174
- ...addOptions,
175
- backend,
176
- config,
177
- session,
178
- host: withHost(options.host),
179
- ...(options.target ? { target: options.target } : {}),
180
- jsonOutput: Boolean(options.json),
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
- reportCLIError(error);
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 Error("--local remove requires --variant and --offer");
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, options.line);
395
+ await runCartRemoveServer(backend, session, lineID, { jsonOutput });
230
396
  }
231
397
  }
232
398
  catch (error) {
233
- reportCLIError(error);
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
- .action(async () => {
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
- await runCartShowServer(backend, session);
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 draft fallback")
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
- reportCLIError(error);
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, ...)", "terminal")
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", (value) => Number.parseInt(value, 10), 1)
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", (value) => Number.parseInt(value, 10), 120)
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 host = withHost(options.host);
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
- if (error instanceof HttpError && error.status === 404 && snap.lastServiceExecutionID) {
374
- process.stderr.write(`[${error.status}] ${error.code}: ${error.message}\n`);
375
- process.stderr.write("recovery:\n");
376
- process.stderr.write(" - Reissue the existing Service Execution checkout handoff\n");
377
- process.stderr.write(` itpay services checkout ${snap.lastServiceExecutionID} --resume --json\n`);
378
- process.exitCode = 1;
379
- return;
380
- }
381
- reportCLIError(error);
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,58 +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("--provider <name>")
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 method = options.method === "wechatpay" ? "wechatpay" : "alipay";
396
- const payOptions = {
397
- checkoutID: options.checkout,
398
- method,
399
- ...(options.provider ? { preferredProvider: options.provider } : {}),
400
- ...(options.buyer ? { buyerID: options.buyer } : {}),
401
- ...(options.refresh ? { refreshAction: true } : {}),
402
- };
403
- await runPay(backend, config, payOptions);
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
- const host = withHost(options.host ?? "terminal");
412
- await withBackend(async (backend) => runOrder(backend, orderID, { host }));
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
- await runListOrders(backend, config, {
423
- limit: options.limit,
424
- status: options.status,
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
- .requiredOption("--order <order_id>")
719
+ .option("--order <order_id>")
431
720
  .option("--reason <reason>")
721
+ .option("--json", "output JSON instead of terminal text")
432
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
+ }
433
758
  const config = loadConfig();
434
- const backend = newBackendClient(config);
435
- const refundOptions = {
436
- orderID: options.order,
437
- ...(options.reason ? { reason: options.reason } : {}),
438
- };
439
- await runRefund(backend, config, refundOptions);
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
+ }
440
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
+ }
441
834
  // --- service execution ----------------------------------------------------
442
835
  const services = program.command("services").description("Generic V3 Service Execution commands");
443
836
  services
@@ -446,15 +839,28 @@ services
446
839
  .argument("<service_id>")
447
840
  .option("--host <host>", "client host")
448
841
  .option("--target <target>")
449
- .option("--buyer <buyer_id>")
842
+ .option("--json", "output JSON instead of terminal text")
450
843
  .action(async (serviceID, options) => {
451
844
  const config = loadConfig();
452
845
  const backend = newBackendClient(config);
453
- await runServicesStart(backend, config, serviceID, {
454
- host: withHost(options.host ?? defaultHostForAgentType(config.agentType)),
455
- ...(options.target ? { target: options.target } : {}),
456
- ...(options.buyer ? { buyerID: options.buyer } : {}),
457
- });
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
+ }
458
864
  });
459
865
  services
460
866
  .command("invoke")
@@ -466,7 +872,20 @@ services
466
872
  .action(async (serviceExecutionID, options) => {
467
873
  const config = loadConfig();
468
874
  const backend = newBackendClient(config);
469
- await runServicesInvoke(backend, config, serviceExecutionID, options.capability, parseKeyValueList(options.input), { jsonOutput: Boolean(options.json) });
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
+ }
470
889
  });
471
890
  services
472
891
  .command("action")
@@ -478,27 +897,63 @@ services
478
897
  .option("--status <status>", "pending, approved, rejected, expired, or cancelled")
479
898
  .option("--candidate <rank>", "select a displayed candidate by its rank", Number)
480
899
  .option("--result-item <service_capability_result_item_id>")
481
- .option("--selected-candidate-hash <hash>")
482
900
  .option("--required-before <step>")
483
901
  .option("--input <key=value>", "action input snapshot", collectOption, [])
902
+ .option("--json", "output JSON instead of terminal text")
484
903
  .action(async (serviceExecutionID, options) => {
485
904
  const config = loadConfig();
486
905
  const backend = newBackendClient(config);
487
- await runServicesAction(backend, serviceExecutionID, options.action, parseKeyValueList(options.input), {
488
- ...(options.actorType ? { actorType: options.actorType } : {}),
489
- ...(options.actorId ? { actorID: options.actorId } : {}),
490
- ...(options.status ? { status: options.status } : {}),
491
- ...(options.candidate !== undefined ? { candidateRank: options.candidate } : {}),
492
- ...(options.resultItem ? { resultItemID: options.resultItem } : {}),
493
- ...(options.selectedCandidateHash ? { selectedCandidateHash: options.selectedCandidateHash } : {}),
494
- ...(options.requiredBefore ? { requiredBefore: options.requiredBefore } : {}),
495
- });
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
+ }
496
950
  });
497
951
  services
498
952
  .command("checkout")
499
953
  .description("Create checkout from a service execution and render the ItPay checkout handoff")
500
954
  .argument("<service_execution_id>")
501
955
  .option("--capability <capability_id>")
956
+ .option("--input <key=value>", "input to lock into the paid service quote", collectOption, [])
502
957
  .option("--email <delivery_email>")
503
958
  .option("--resume", "reissue the existing checkout handoff without creating another checkout")
504
959
  .option("--host <host>", "client host (terminal, codex, telegram, feishu, lark, ...)")
@@ -511,35 +966,61 @@ services
511
966
  const backend = newBackendClient(config);
512
967
  const sessionPath = cartSessionPath();
513
968
  const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
514
- await runServicesCheckout(backend, config, serviceExecutionID, options.capability, {
515
- ...(options.email ? { email: options.email } : {}),
516
- resume: Boolean(options.resume),
517
- host: withHost(options.host ?? defaultHostForAgentType(config.agentType)),
518
- ...(options.target ? { target: options.target } : {}),
519
- ...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
520
- ...(options.qrFile ? { qrFilePath: options.qrFile } : {}),
521
- jsonOutput: Boolean(options.json),
522
- persistHandoff: (handoff) => {
523
- session.rememberCheckout({
524
- cartID: handoff.cartID,
525
- checkoutID: handoff.checkoutID,
526
- displayToken: handoff.displayToken,
527
- checkoutURL: handoff.checkoutURL,
528
- serviceExecutionID: handoff.serviceExecutionID,
529
- });
530
- session.saveToFile(sessionPath);
531
- },
532
- });
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
+ }
533
1001
  });
534
1002
  services
535
1003
  .command("list")
536
1004
  .description("Recover service executions visible to this enrolled device or account")
537
- .option("--limit <number>", "maximum executions", "50")
1005
+ .option("--limit <number>", "maximum executions", "10")
538
1006
  .option("--json", "output compact JSON")
539
1007
  .action(async (options) => {
540
1008
  const config = loadConfig();
541
1009
  const backend = newBackendClient(config);
542
- await runServicesList(backend, { limit: Number.parseInt(options.limit, 10), jsonOutput: Boolean(options.json) });
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
+ }
543
1024
  });
544
1025
  services
545
1026
  .command("get")
@@ -549,7 +1030,17 @@ services
549
1030
  .action(async (serviceExecutionID, options) => {
550
1031
  const config = loadConfig();
551
1032
  const backend = newBackendClient(config);
552
- await runServicesGet(backend, serviceExecutionID, { jsonOutput: Boolean(options.json) });
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
+ }
553
1044
  });
554
1045
  services
555
1046
  .command("next")
@@ -559,40 +1050,67 @@ services
559
1050
  .action(async (serviceExecutionID, options) => {
560
1051
  const config = loadConfig();
561
1052
  const backend = newBackendClient(config);
562
- await runServicesNext(backend, serviceExecutionID, { jsonOutput: Boolean(options.json) });
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
+ }
563
1064
  });
564
1065
  services
565
1066
  .command("read-result")
566
1067
  .description("Read a human-granted service result for this agent")
567
1068
  .argument("<service_execution_id>")
568
- .action(async (serviceExecutionID) => {
1069
+ .option("--json", "output JSON instead of terminal text")
1070
+ .action(async (serviceExecutionID, options) => {
569
1071
  const config = loadConfig();
570
1072
  const backend = newBackendClient(config);
571
- await runServicesReadResult(backend, serviceExecutionID);
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
+ }
572
1084
  });
573
1085
  services
574
1086
  .command("events")
575
1087
  .description("List redacted service execution events")
576
1088
  .argument("<service_execution_id>")
577
- .action(async (serviceExecutionID) => {
578
- const config = loadConfig();
579
- const backend = newBackendClient(config);
580
- await runServicesEvents(backend, serviceExecutionID);
581
- });
582
- 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) => {
583
1093
  const config = loadConfig();
584
1094
  const backend = newBackendClient(config);
585
1095
  try {
586
- await action(backend);
1096
+ await runServicesEvents(backend, serviceExecutionID, {
1097
+ afterSequence: Number(options.afterSequence),
1098
+ limit: Number(options.limit),
1099
+ jsonOutput: Boolean(options.json),
1100
+ });
587
1101
  }
588
1102
  catch (error) {
589
- if (error instanceof HttpError) {
590
- reportCLIError(error);
591
- return;
592
- }
593
- throw error;
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
+ });
594
1112
  }
595
- }
1113
+ });
596
1114
  program.parseAsync(process.argv).catch((error) => {
597
1115
  reportCLIError(error);
598
1116
  });