@itpay/cli 2.0.3 → 2.0.7

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