@itpay/cli 2.1.19 → 2.1.29

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 (86) hide show
  1. package/README.md +2 -2
  2. package/dist/src/commands/buy.js +3 -3
  3. package/dist/src/commands/cart.js +15 -15
  4. package/dist/src/commands/catalog.js +10 -7
  5. package/dist/src/commands/checkout.js +8 -11
  6. package/dist/src/commands/docs.js +3 -14
  7. package/dist/src/commands/guidance.js +41 -25
  8. package/dist/src/commands/install.js +2 -2
  9. package/dist/src/commands/order.js +4 -3
  10. package/dist/src/commands/orders.js +4 -9
  11. package/dist/src/commands/pay.js +1 -1
  12. package/dist/src/commands/rail_catalog.js +160 -11
  13. package/dist/src/commands/refund.js +5 -5
  14. package/dist/src/commands/services.js +658 -361
  15. package/dist/src/commands/skill.js +1 -1
  16. package/dist/src/commands/vault.js +27 -20
  17. package/dist/src/main.js +39 -24
  18. package/dist/src/render/markdown.js +0 -3
  19. package/dist/src/state/account_auth.js +9 -5
  20. package/dist/src/state/cart_session.js +4 -1
  21. package/dist/src/state/config.js +1 -1
  22. package/dist/src/state/task_journal.js +2 -0
  23. package/docs/agent/buyer/cart-checkout.json +5 -4
  24. package/docs/agent/buyer/catalog-list.json +24 -6
  25. package/docs/agent/buyer/identity-and-sessions.json +16 -15
  26. package/docs/agent/buyer/install-and-setup.json +5 -4
  27. package/docs/agent/buyer/orders-refunds.json +11 -5
  28. package/docs/agent/buyer/payment-flow.json +3 -3
  29. package/docs/agent/buyer/purchased-content.json +18 -4
  30. package/docs/agent/buyer/quickstart.json +22 -14
  31. package/docs/agent/buyer/rail-booking.json +125 -171
  32. package/docs/agent/buyer/rail-fast-checkout.json +21 -44
  33. package/docs/agent/buyer/render-hosts.json +9 -6
  34. package/docs/agent/buyer/service-feedback.json +2 -1
  35. package/docs/cli-reference/agent-types.md +1 -1
  36. package/docs/cli-reference/commands/auth.md +2 -0
  37. package/docs/cli-reference/commands/buy.md +1 -1
  38. package/docs/cli-reference/commands/cart/add.md +1 -1
  39. package/docs/cli-reference/commands/cart/clear.md +1 -1
  40. package/docs/cli-reference/commands/cart/index.md +1 -1
  41. package/docs/cli-reference/commands/cart/next.md +1 -1
  42. package/docs/cli-reference/commands/cart/remove.md +1 -1
  43. package/docs/cli-reference/commands/cart/show.md +1 -1
  44. package/docs/cli-reference/commands/catalog/index.md +1 -1
  45. package/docs/cli-reference/commands/catalog/list.md +1 -1
  46. package/docs/cli-reference/commands/checkout.md +1 -1
  47. package/docs/cli-reference/commands/device.md +8 -7
  48. package/docs/cli-reference/commands/docs/index.md +1 -1
  49. package/docs/cli-reference/commands/docs/list.md +1 -1
  50. package/docs/cli-reference/commands/docs/search.md +1 -1
  51. package/docs/cli-reference/commands/docs/show.md +1 -1
  52. package/docs/cli-reference/commands/install.md +2 -2
  53. package/docs/cli-reference/commands/next.md +1 -1
  54. package/docs/cli-reference/commands/order.md +1 -1
  55. package/docs/cli-reference/commands/pay.md +1 -1
  56. package/docs/cli-reference/commands/readyz.md +1 -1
  57. package/docs/cli-reference/commands/refund/cancel.md +1 -1
  58. package/docs/cli-reference/commands/refund/create.md +1 -1
  59. package/docs/cli-reference/commands/refund/get.md +2 -2
  60. package/docs/cli-reference/commands/refund/index.md +1 -1
  61. package/docs/cli-reference/commands/refund/list.md +1 -1
  62. package/docs/cli-reference/commands/refund/watch.md +1 -1
  63. package/docs/cli-reference/commands/services/action.md +3 -3
  64. package/docs/cli-reference/commands/services/checkout.md +1 -1
  65. package/docs/cli-reference/commands/services/events.md +1 -1
  66. package/docs/cli-reference/commands/services/get.md +1 -1
  67. package/docs/cli-reference/commands/services/index.md +1 -1
  68. package/docs/cli-reference/commands/services/invoke.md +5 -13
  69. package/docs/cli-reference/commands/services/list.md +1 -1
  70. package/docs/cli-reference/commands/services/next.md +12 -10
  71. package/docs/cli-reference/commands/services/page.md +9 -7
  72. package/docs/cli-reference/commands/services/quote.md +1 -1
  73. package/docs/cli-reference/commands/services/read-result.md +16 -9
  74. package/docs/cli-reference/commands/services/run.md +4 -2
  75. package/docs/cli-reference/commands/services/start.md +1 -1
  76. package/docs/cli-reference/conventions.md +2 -2
  77. package/package.json +7 -5
  78. package/skills/itpay/SKILL.md +77 -238
  79. package/docs/reviews/agent-ux-handoff-2026-09-23/IMPLEMENTATION-EVIDENCE.md +0 -174
  80. package/docs/skill-bundle-rollout/01-mcp-authentication.md +0 -134
  81. package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +0 -210
  82. package/docs/skill-bundle-rollout/03-platform-publishing.md +0 -161
  83. package/docs/skill-bundle-rollout/04-first-wave-platforms.md +0 -70
  84. package/docs/skill-bundle-rollout/05-sync-operations.md +0 -62
  85. package/docs/skill-bundle-rollout/README.md +0 -151
  86. /package/bin/{itp → itp.mjs} +0 -0
package/README.md CHANGED
@@ -4,7 +4,7 @@ The official V3 CLI and the single ItPay entry point for services, purchases, ac
4
4
 
5
5
  ## One Entry Point, Several Human Intents
6
6
 
7
- `itpay` is the only public CLI entry point, and `$itpay` is its user-facing Skill invocation. The Agent first distinguishes a new service request, previously purchased content, order history, or a refund. Seller workflows will use the same entry point later and are not implemented yet.
7
+ `itpay` is the only public CLI entry point, and `$itpay` is its user-facing Skill invocation. The Agent first distinguishes a new service request, previously purchased content, order history, or a refund. Seller workflows use the existing `itpay sell` entry point.
8
8
 
9
9
  ```bash
10
10
  npm install -g @itpay/cli
@@ -68,7 +68,7 @@ The local installation keeps one Ed25519 private key and a separate registration
68
68
  - `services start/invoke/action/checkout/next`: generic Service Execution flow.
69
69
  - `cart add/show/remove/clear/next`, `buy`: canonical Cart and ordinary Checkout flow.
70
70
  - `checkout`: authoritative payment and fulfillment recovery.
71
- - `services read-result`: read one human-granted protected result.
71
+ - `services read-result`: read saved railway query pages or human-granted protected content.
72
72
  - `order`, `orders`: exact order and account order views.
73
73
  - `vault list/access/read`: find and read previously purchased content after time-limited human authorization.
74
74
  - `refund create/list/get/watch/cancel`: Refund Owner flow.
@@ -135,7 +135,7 @@ export async function runBuy(backend, config, options) {
135
135
  });
136
136
  }
137
137
  // --- Output ---
138
- if (options.jsonOutput) {
138
+ if (options.jsonOutput || platformKeyForHost(options.host) === "plain_chat") {
139
139
  const envelope = buildBuyEnvelope({
140
140
  cart,
141
141
  checkoutID,
@@ -148,7 +148,7 @@ export async function runBuy(backend, config, options) {
148
148
  ...(options.agentType ? { agentType: options.agentType } : {}),
149
149
  ...(options.target ? { target: options.target } : {}),
150
150
  });
151
- writeCommandEnvelope(envelope, { jsonOutput: true, ...(options.output ? { output: options.output } : {}) });
151
+ writeCommandEnvelope(envelope, { jsonOutput: Boolean(options.jsonOutput), ...(options.output ? { output: options.output } : {}), ...(options.agentType ? { agentType: options.agentType } : {}) });
152
152
  return {
153
153
  kind: "checkout_rendered",
154
154
  plan,
@@ -219,7 +219,7 @@ function buildBuyEnvelope(input) {
219
219
  result,
220
220
  handoff: presentationHandoff.handoff,
221
221
  instruction: presentationHandoff.instruction,
222
- next: { command: input.plan.afterActionCommand ?? `itpay checkout --id ${input.checkoutID} --token ${input.displayToken} --json`, reason: "稍后查询同一笔 Checkout 状态" },
222
+ next: { command: input.plan.afterActionCommand ?? `itpay checkout --id ${input.checkoutID} --token ${input.displayToken} --json`, reason: "仅用户完成付款或要求查询时,读取同一笔Checkout权威状态" },
223
223
  recovery: [],
224
224
  };
225
225
  }
@@ -21,7 +21,7 @@ export function runCartAdd(session, options) {
21
21
  offer_id: item.offerID,
22
22
  quantity: item.quantity,
23
23
  },
24
- instruction: "仅写入本地兼容草稿,未验证目录、价格或服务合同;不要把它当作 canonical Cart。",
24
+ instruction: "仅写入本地兼容草稿,未验证目录、价格或服务合同;不要把它当作 服务端购物车。",
25
25
  next: { command: "itpay cart show --local", reason: "检查本地草稿" },
26
26
  recovery: [],
27
27
  };
@@ -74,10 +74,10 @@ export async function runCartAddServer(options) {
74
74
  },
75
75
  instruction: serviceExecutionID
76
76
  ? "服务型项目已创建 Service Execution;先读取其当前步骤,不要直接进入普通 buy。"
77
- : "普通项目已加入 canonical Cart;先检查购物车,再创建 Checkout。",
77
+ : "普通项目已加入 服务端购物车;先检查购物车,再创建 Checkout。",
78
78
  next: serviceExecutionID
79
79
  ? { command: `itpay services next ${serviceExecutionID} --json`, reason: "读取服务执行的当前步骤" }
80
- : { command: "itpay cart next --json", reason: "检查 canonical Cart" },
80
+ : { command: "itpay cart next --json", reason: "检查 服务端购物车" },
81
81
  recovery: [],
82
82
  };
83
83
  writeCommandEnvelope(envelope, {
@@ -121,7 +121,7 @@ export async function runCartAddQuoteServer(options) {
121
121
  total: formatMoney(cart.amount_minor, cart.currency),
122
122
  },
123
123
  instruction: "付费服务报价已加入同一 Cart;每个项目仍保持独立 Execution 和交付。",
124
- next: { command: `itpay buy --cart ${cart.cart_id} --json`, reason: "确认项目齐全后创建一次合并付款" },
124
+ next: { command: `itpay buy --cart ${cart.cart_id} --json`, reason: "仅已有购买授权且项目齐全时创建一次合并付款" },
125
125
  recovery: [{ command: "itpay cart show --json", reason: "检查当前合并 Cart" }],
126
126
  };
127
127
  writeCommandEnvelope(envelope, {
@@ -144,7 +144,7 @@ export function runCartRemove(session, options) {
144
144
  catalog_variant_id: options.catalogVariantID,
145
145
  offer_id: options.offerID,
146
146
  },
147
- instruction: "本地兼容草稿已更新;这不代表任何 canonical Cart 或 Service Execution 已变更。",
147
+ instruction: "本地兼容草稿已更新;这不代表任何 服务端购物车 或 Service Execution 已变更。",
148
148
  next: { command: "itpay cart show --local --json", reason: "检查剩余本地草稿" },
149
149
  recovery: [],
150
150
  }, {
@@ -176,7 +176,7 @@ export async function runCartRemoveServer(backend, session, cartItemID, options
176
176
  cart_item_id: lineID,
177
177
  remaining_item_count: cart.items.length,
178
178
  },
179
- instruction: "canonical Cart 已更新;被删除的最后一个 service-backed line 对应执行会由服务端一致性事务取消。",
179
+ instruction: "服务端购物车 已更新;被删除的最后一个 服务项目 对应执行会由服务端同时取消。",
180
180
  next: { command: "itpay cart next --json", reason: "检查剩余内容" },
181
181
  recovery: [],
182
182
  };
@@ -198,10 +198,10 @@ export function runCartShow(session, options = {}) {
198
198
  status: items.length > 0 ? "shown_local" : "local_empty",
199
199
  result: { currency: snap.currency, items },
200
200
  instruction: items.length > 0
201
- ? "这是未验证的本地兼容草稿,只能用于明确的普通商品流程;不要把它当作 canonical Cart。"
201
+ ? "这是未验证的本地兼容草稿,只能用于明确的普通商品流程;不要把它当作 服务端购物车。"
202
202
  : "本地兼容草稿为空;从已发布目录重新选择项目。",
203
203
  next: items.length > 0
204
- ? { command: "itpay buy --json", reason: "将普通本地草稿提交为 canonical Cart" }
204
+ ? { command: "itpay buy --json", reason: "将普通本地草稿提交为 服务端购物车" }
205
205
  : { command: "itpay catalog list --json", reason: "读取已发布目录" },
206
206
  recovery: [],
207
207
  };
@@ -215,7 +215,7 @@ export async function runCartShowServer(backend, session, options = {}) {
215
215
  writeCommandEnvelope({
216
216
  status: "cart_handle_missing",
217
217
  result: {},
218
- instruction: "本地没有 canonical Cart 句柄;不要把本地草稿默认为服务端 Cart。",
218
+ instruction: "本地没有 服务端购物车 句柄;不要把本地草稿默认为服务端 Cart。",
219
219
  next: { command: "itpay catalog list --json", reason: "读取已发布目录" },
220
220
  recovery: [{ command: "itpay cart show --local --json", reason: "仅在明确需要时检查本地兼容草稿" }],
221
221
  }, {
@@ -241,7 +241,7 @@ export async function runCartShowServer(backend, session, options = {}) {
241
241
  },
242
242
  instruction: items.length > 0
243
243
  ? "使用 line 或 execution 句柄继续;不要使用内部 quote lock ID。"
244
- : "canonical Cart 当前为空;从已发布目录选择项目。",
244
+ : "服务端购物车 当前为空;从已发布目录选择项目。",
245
245
  next: items.length > 0
246
246
  ? { command: "itpay cart next --json", reason: "取得当前首选动作" }
247
247
  : { command: "itpay catalog list --json", reason: "读取已发布目录" },
@@ -278,7 +278,7 @@ export async function runCartAbandonServer(backend, session, options = {}) {
278
278
  writeCommandEnvelope({
279
279
  status: "cart_handle_missing",
280
280
  result: {},
281
- instruction: "本地没有 canonical Cart 句柄;未修改任何 Backend 或本地资源。",
281
+ instruction: "本地没有 服务端购物车 句柄;未修改任何 Backend 或本地资源。",
282
282
  next: { command: "itpay next --json", reason: "检查其他可恢复句柄" },
283
283
  recovery: [{ command: "itpay cart clear --local --json", reason: "仅在明确放弃本地草稿和句柄时执行" }],
284
284
  }, {
@@ -292,7 +292,7 @@ export async function runCartAbandonServer(backend, session, options = {}) {
292
292
  const envelope = {
293
293
  status: "abandoned",
294
294
  result: { cart_id: cart.cart_id, server_abandoned: true },
295
- instruction: "canonical Cart 已放弃;Backend 已在同一事务中软删除 active lines,并取消其未付款 Service Execution。",
295
+ instruction: "服务端购物车 已放弃;服务端已清空有效项目,并取消关联的未付款服务执行。",
296
296
  next: { command: "itpay catalog list --json", reason: "仅在用户提出新需求时重新选择" },
297
297
  recovery: [],
298
298
  };
@@ -308,7 +308,7 @@ export async function runCartNext(backend, session, options = {}) {
308
308
  writeCartNextEnvelope({
309
309
  status: "cart_handle_missing",
310
310
  result: {},
311
- instruction: "本地没有 canonical Cart 句柄;先恢复已有资源,不要创建重复 Cart。",
311
+ instruction: "本地没有 服务端购物车 句柄;先恢复已有资源,不要创建重复 Cart。",
312
312
  next: { command: "itpay next --json", reason: "检查其他可恢复句柄" },
313
313
  recovery: [{ command: "itpay services list --json", reason: "从服务端恢复当前设备的执行" }],
314
314
  }, options);
@@ -325,7 +325,7 @@ export async function runCartNext(backend, session, options = {}) {
325
325
  cart_status: cart.status,
326
326
  service_execution_id: unquotedServiceLine.service_execution_id,
327
327
  },
328
- instruction: "该 Cart 包含 service-backed line;继续 Service Execution,不要从 Cart 猜 capability。",
328
+ instruction: "该 Cart 包含 服务项目;继续 Service Execution,不要从 Cart 猜 capability。",
329
329
  next: {
330
330
  command: `itpay services next ${unquotedServiceLine.service_execution_id} --json`,
331
331
  reason: "读取服务端最新执行状态",
@@ -355,7 +355,7 @@ export async function runCartNext(backend, session, options = {}) {
355
355
  instruction: cart.items.some((item) => item.service_quote_lock_id)
356
356
  ? "服务报价已锁定输入和价格;确认项目齐全后使用同一 Cart 创建一次 Checkout。"
357
357
  : "该 Cart 是普通购买流程;使用同一 Cart 创建 Checkout,不要重复添加商品。",
358
- next: { command: `itpay buy --cart ${cart.cart_id} --json`, reason: "继续 canonical Cart 结算" },
358
+ next: { command: `itpay buy --cart ${cart.cart_id} --json`, reason: "仅已有购买授权时继续服务端购物车结算" },
359
359
  recovery: [],
360
360
  };
361
361
  }
@@ -9,14 +9,11 @@ export async function runCatalogList(backend, options = {}) {
9
9
  status: empty ? "catalog_empty" : "listed",
10
10
  result: { catalog_version: manifest.version, services },
11
11
  instruction: empty
12
- ? "当前没有已发布服务;稍后重试,不要猜测 service_id。"
12
+ ? "当前没有已发布服务;本次目录读取结束,不猜测服务或自动重试。"
13
13
  : "向用户解释主服务、辅助步骤和价格;得到用户意图后再启动对应 service_id。",
14
- next: empty
15
- ? { command: `itpay catalog list${jsonFlag}`, reason: "稍后重新读取已发布目录" }
16
- : {
17
- command: `itpay services start ${services.length === 1 && firstServiceID ? firstServiceID : "<service_id>"}${jsonFlag}`,
18
- reason: "启动用户选择的服务",
19
- },
14
+ next: services.length === 1 && firstServiceID
15
+ ? { command: `itpay services start ${firstServiceID}${jsonFlag}`, reason: "仅当当前服务符合用户请求时读取输入合同;不表示购买同意" }
16
+ : null,
20
17
  recovery: [],
21
18
  }, {
22
19
  ...options,
@@ -28,8 +25,10 @@ function summarizeService(item) {
28
25
  const offer = item.variants?.[0];
29
26
  return {
30
27
  service_id: item.service_id ?? null,
28
+ ...(item.service_id ? { entry: `itpay services start ${item.service_id} --json` } : {}),
31
29
  title: item.title,
32
30
  description: item.description ?? "",
31
+ ...(item.service_id?.startsWith("itpay-rail-") ? { guide: "itpay docs show rail-booking" } : {}),
33
32
  ...(flow ? {
34
33
  discovery: {
35
34
  title: flow.discovery.title,
@@ -58,6 +57,10 @@ function catalogPlainLines(version, services) {
58
57
  for (const service of services) {
59
58
  lines.push(`service: ${String(service.title)}`);
60
59
  lines.push(` service_id: ${String(service.service_id ?? "unavailable")}`);
60
+ if (service.entry)
61
+ lines.push(` entry: ${String(service.entry)}`);
62
+ if (service.guide)
63
+ lines.push(` entry: ${String(service.guide)}`);
61
64
  if (service.description)
62
65
  lines.push(` description: ${String(service.description)}`);
63
66
  const discovery = service.discovery;
@@ -10,7 +10,7 @@ import { hostCapabilities, normalizeViewer } from "../state/client_context.js";
10
10
  import { DEFAULT_BASE_URL } from "../state/config.js";
11
11
  import { buildCheckoutQRPlan } from "./buy.js";
12
12
  import { buildCheckoutHandoff, shouldPrepareLocalCheckoutImage } from "./checkout_handoff.js";
13
- import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
13
+ import { CommandContractError, plainValueLines, writeCommandEnvelope } from "./guidance.js";
14
14
  import { resolvePresentation, resolveRelay } from "./presentation.js";
15
15
  export async function runCheckoutPresentation(backend, options) {
16
16
  const present = options.present;
@@ -81,10 +81,6 @@ export async function runCheckoutPresentation(backend, options) {
81
81
  });
82
82
  }
83
83
  const envelope = pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand, options.agentType, options.target);
84
- const plainResult = checkoutPlainResult(envelope.result);
85
- if (!options.jsonOutput && platformKeyForHost(host) === "terminal") {
86
- plainResult.push("qr:", await renderTerminalQR(checkoutURL, "terminal"));
87
- }
88
84
  // W5: deterministic presentation plan — business state first, then declared
89
85
  // capabilities. `present` is the explicit display request; without it (or
90
86
  // under --json alone) nothing opens.
@@ -143,6 +139,10 @@ export async function runCheckoutPresentation(backend, options) {
143
139
  wait_for: decision.communication.wait_for,
144
140
  must_convey: decision.communication.must_convey,
145
141
  };
142
+ const plainResult = checkoutPlainResult(envelope.result);
143
+ if (!options.jsonOutput && platformKeyForHost(host) === "terminal") {
144
+ plainResult.push("qr:", await renderTerminalQR(checkoutURL, "terminal"));
145
+ }
146
146
  writeCommandEnvelope(envelope, {
147
147
  ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
148
148
  ...(options.output ? { output: options.output } : {}),
@@ -213,11 +213,8 @@ function pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand, a
213
213
  instruction: railPassengersPending
214
214
  ? `${presentationHandoff.instruction} 请用户在受保护网页填写乘车人并确认报价;姓名、证件和手机号只在网页填写,不要贴到对话中。座位偏好仅为购票请求、购票时才提交给供应商且不保证满足;无法逐人提交的偏好将自动分配座位,以实际出票为准。`
215
215
  : presentationHandoff.instruction,
216
- next: { command: nextCommand, reason: "稍后只查询同一 Checkout" },
217
- recovery: [{
218
- command: nextCommand,
219
- reason: "付款时限由服务端统一确定,刷新不会延长;到期未支付自动取消后须重新核价下单,不要重发旧付款动作",
220
- }],
216
+ next: { command: nextCommand, reason: "展示后停止;仅用户完成操作或要求核对时读取同一 Checkout" },
217
+ recovery: [],
221
218
  };
222
219
  }
223
220
  function terminalCheckoutEnvelope(presentation) {
@@ -263,7 +260,7 @@ function checkoutNeedsHumanHandoff(status) {
263
260
  return !new Set(["payment_succeeded", "completed", "failed", "expired", "refunded"]).has(status);
264
261
  }
265
262
  function checkoutPlainResult(result) {
266
- return Object.entries(result).map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
263
+ return plainValueLines(result);
267
264
  }
268
265
  function formatMoney(amountMinor, currency) {
269
266
  return `${(amountMinor / 100).toFixed(2)} ${currency}`;
@@ -1,8 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { resolveOutput } from "../render/sink.js";
5
- import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
4
+ import { CommandContractError, shellArgument, writeCommandEnvelope } from "./guidance.js";
6
5
  const commandDir = dirname(fileURLToPath(import.meta.url));
7
6
  export function runDocsList(options = {}) {
8
7
  const topics = loadDocs().map(({ topic, title, purpose }) => ({ topic, title, purpose }));
@@ -23,7 +22,7 @@ export function runDocsShow(topic, options = {}) {
23
22
  if (!doc) {
24
23
  throw new CommandContractError("doc_not_found", `doc topic not found: ${topic}`, "使用稳定 topic 名称;不要根据标题猜 topic。", [
25
24
  { command: "itpay docs list --json", reason: "列出全部 topic" },
26
- { command: `itpay docs search ${shellWord(normalized || "topic")} --json`, reason: "按关键词重新搜索" },
25
+ { command: `itpay docs search ${shellArgument(normalized || "topic")} --json`, reason: "按关键词重新搜索" },
27
26
  ]);
28
27
  }
29
28
  const envelope = {
@@ -33,14 +32,7 @@ export function runDocsShow(topic, options = {}) {
33
32
  next: null,
34
33
  recovery: [],
35
34
  };
36
- if (options.jsonOutput) {
37
- writeCommandEnvelope(envelope, options);
38
- return;
39
- }
40
- const out = resolveOutput(options.output);
41
- out("shown\n");
42
- out(`${JSON.stringify(doc, null, 2)}\n`);
43
- out(`instruction: ${envelope.instruction}\n`);
35
+ writeCommandEnvelope(envelope, options);
44
36
  }
45
37
  export function runDocsSearch(query, options = {}) {
46
38
  const normalized = query.trim().toLowerCase();
@@ -103,6 +95,3 @@ function parseDoc(raw, file) {
103
95
  function searchableText(doc) {
104
96
  return [doc.topic, doc.title, doc.purpose, ...(doc.search_terms ?? [])].join(" ").toLowerCase();
105
97
  }
106
- function shellWord(value) {
107
- return /^[a-zA-Z0-9._-]+$/.test(value) ? value : JSON.stringify(value);
108
- }
@@ -15,56 +15,79 @@ export class CommandContractError extends Error {
15
15
  code;
16
16
  instruction;
17
17
  recovery;
18
- constructor(code, message, instruction, recovery) {
18
+ interaction;
19
+ constructor(code, message, instruction, recovery, interaction) {
19
20
  super(message);
20
21
  this.code = code;
21
22
  this.instruction = instruction;
22
23
  this.recovery = recovery;
24
+ this.interaction = interaction;
23
25
  this.name = "CommandContractError";
24
26
  }
25
27
  }
26
- // stdoutEnvelopeLimit is the hard byte budget for one emitted JSON envelope:
27
- // public rail pages/projections must fit 32 KiB end-to-end, so oversized
28
- // pretty output falls back to a compact line rather than spilling.
29
- const stdoutEnvelopeLimit = 32 * 1024;
28
+ // Domain projections and pages own byte limits; serialization never truncates facts.
29
+ export function plainValueLines(value, label = "", depth = 0) {
30
+ const indent = " ".repeat(depth);
31
+ if (value === null || typeof value !== "object")
32
+ return [`${indent}${label ? `${label}: ` : ""}${String(value ?? "unknown")}`];
33
+ const lines = label ? [`${indent}${label}:`] : [];
34
+ const entries = Array.isArray(value) ? value.map((v, i) => [String(i + 1), v]) : Object.entries(value);
35
+ for (const [key, child] of entries)
36
+ lines.push(...plainValueLines(child, key, depth + (label ? 1 : 0)));
37
+ if (!entries.length)
38
+ lines.push(`${indent} (无记录)`);
39
+ return lines;
40
+ }
41
+ export function shellArgument(value) {
42
+ if (/^[\p{L}\p{N}._:=/-]+$/u.test(value))
43
+ return value;
44
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
45
+ }
30
46
  export function writeCommandEnvelope(value, options = {}) {
31
47
  const out = resolveOutput(options.output);
32
48
  const agentType = options.agentType ?? declaredAgentType();
33
49
  const qualified = qualifyEnvelope(value, agentType);
34
50
  if (options.jsonOutput) {
35
51
  const pretty = JSON.stringify(qualified, null, 2) + "\n";
36
- out(pretty.length <= stdoutEnvelopeLimit ? pretty : JSON.stringify(qualified) + "\n");
52
+ out(pretty);
37
53
  return;
38
54
  }
39
55
  out(`${qualified.status}\n`);
40
56
  const facts = "error" in qualified ? qualified.error : qualified.result ?? {};
41
57
  if (options.plainResult) {
42
- for (const line of options.plainResult)
43
- out(`${line}\n`);
58
+ for (const line of options.plainResult) {
59
+ const command = line.match(/^(\s*(?:command|reader|entry): )(itpay .*)$/);
60
+ out(`${command ? command[1] + qualifyBackendCommand(qualifyItPayCommand(command[2], agentType)) : line}\n`);
61
+ }
44
62
  }
45
63
  else {
46
64
  for (const [key, fact] of Object.entries(facts)) {
47
- out(`${key}: ${typeof fact === "string" ? fact : JSON.stringify(fact)}\n`);
65
+ for (const line of plainValueLines(fact, key))
66
+ out(`${line}\n`);
48
67
  }
49
68
  if ("error" in qualified && qualified.result) {
50
69
  for (const [key, fact] of Object.entries(qualified.result)) {
51
- out(`${key}: ${typeof fact === "string" ? fact : JSON.stringify(fact)}\n`);
70
+ for (const line of plainValueLines(fact, key))
71
+ out(`${line}\n`);
52
72
  }
53
73
  }
54
74
  }
55
75
  if ("handoff" in qualified && qualified.handoff) {
56
76
  for (const [key, fact] of Object.entries(qualified.handoff)) {
57
- out(`handoff.${key}: ${typeof fact === "string" ? fact : JSON.stringify(fact)}\n`);
77
+ for (const line of plainValueLines(fact, `handoff.${key}`))
78
+ out(`${line}\n`);
58
79
  }
59
80
  }
60
81
  out(`instruction: ${qualified.instruction}\n`);
61
82
  if (qualified.next)
62
- out(`next: ${qualified.next.command}\n`);
83
+ out(`next: ${qualified.next.command}\n when: ${qualified.next.reason}\n`);
63
84
  if ("interaction" in qualified && qualified.interaction) {
64
- out(`interaction: ${JSON.stringify(qualified.interaction)}\n`);
85
+ for (const line of plainValueLines(qualified.interaction, "interaction"))
86
+ out(`${line}\n`);
65
87
  }
66
88
  if ("communication" in qualified && qualified.communication) {
67
- out(`communication: ${JSON.stringify(qualified.communication)}\n`);
89
+ for (const line of plainValueLines(qualified.communication, "communication"))
90
+ out(`${line}\n`);
68
91
  }
69
92
  if (qualified.recovery.length > 0) {
70
93
  out("recovery:\n");
@@ -123,19 +146,12 @@ export function errorRecoveryActions(error) {
123
146
  return [{
124
147
  id: "inspect_service_execution",
125
148
  label: "Inspect Service Execution before checkout",
126
- command: "itpay services next <service_execution_id> --json",
149
+ command: "itpay services list --json",
127
150
  }];
128
151
  }
129
- if (error.code === "cart_item_locked" || error.status === 409) {
130
- return [
131
- { id: "show_cart", label: "Inspect the canonical server cart", command: "itpay cart show" },
132
- { id: "continue_checkout", label: "Continue the last locally remembered checkout", command: "itpay checkout" },
133
- {
134
- id: "recover_service_execution",
135
- label: "List recoverable Service Executions if the local handoff is missing",
136
- command: "itpay services list",
137
- },
138
- ];
152
+ if (error.code === "cart_item_locked") {
153
+ return [{ id: "show_cart", label: "Inspect the locked cart", command: "itpay cart show --json",
154
+ reason: "读取已有购物内容和订单锁,不创建替代 checkout。" }];
139
155
  }
140
156
  if (error.status === 404) {
141
157
  return [{
@@ -17,7 +17,7 @@ const INSTRUCTIONS = {
17
17
  "codex-cli": "在 Codex CLI 中始终传这个 Agent Type;付款交接只应显示在用户可见终端。",
18
18
  "claude-code-desktop": "在 Claude Code Desktop 中始终传这个 Agent Type;付款时把返回的二维码和链接实际展示到当前对话。",
19
19
  "claude-code-cli": "在 Claude Code CLI 中始终传这个 Agent Type;付款交接只应显示在用户可见终端。",
20
- workbuddy: "在 WorkBuddy 中始终传这个 Agent Type。运行会持久化 ~/.itpay-v3 的 ItPay Bash 命令前,把 Shell 工具参数 dangerouslyDisableSandbox 设为 true;用户拒绝权限时停止,不要切换 Node、删除 identity 或处理 lock。Checkout 返回已渲染的付款 Card Link 和 present_files action;原样执行一次 action 打开 handoff.url,不要检查本地二维码文件。",
20
+ workbuddy: "在 WorkBuddy 中始终传这个 Agent Type。运行会持久化 ~/.itpay-v3 的 ItPay Bash 命令前,把 Shell 工具参数 dangerouslyDisableSandbox 设为 true;用户拒绝权限时停止,不要切换 Node、删除 identity;锁忙只用官方 device repair-lock。Checkout 返回已渲染的付款 Card Link 和 present_files action;原样执行一次 action 打开 handoff.url,不要检查本地二维码文件。",
21
21
  zcode: "在 ZCode 中始终传这个 Agent Type。Checkout 或授权返回 handoff.url 后,立即用 ZCode 内置浏览器打开;不要只粘贴文字链接或重建二维码。只有内置浏览器不可用时才展示同一个可点击链接。",
22
22
  "doubao-work": "在豆包工作中始终传这个 Agent Type。Checkout 或授权返回后,同时展示 handoff.url 和 handoff.qr_image_url 两个有标签的官方入口;优先让用户在手机上直接打开链接,二维码供保存或另一台设备扫码。",
23
23
  "kimi-code": "保持 kimi-code Agent Type,使用标准 CLI 命令和终端付款交接;不要切换到其他平台身份。",
@@ -53,7 +53,7 @@ export function runInstall(target, options = {}) {
53
53
  command: `itpay --agent-type ${normalized} readyz --json`,
54
54
  reason: "验证当前官方 ItPay API 的可用性",
55
55
  },
56
- recovery: [{ command: "itpay docs show install-and-setup", reason: "查看官方 Backend 和首次使用说明" }],
56
+ recovery: [],
57
57
  }, options);
58
58
  }
59
59
  function isInstallAgentType(value) {
@@ -1,3 +1,4 @@
1
+ import { refundStateEnvelope } from "./refund.js";
1
2
  import { formatMoney } from "../render/output.js";
2
3
  import { appendFeedbackPostmortemInstruction, writeCommandEnvelope } from "./guidance.js";
3
4
  export async function runOrder(backend, orderID, options = {}) {
@@ -19,7 +20,7 @@ function orderEnvelope(order, delivery, lockedRefund) {
19
20
  let instruction = "订单状态已读取;当前没有可用交付入口。";
20
21
  let next = null;
21
22
  if (lockedRefund) {
22
- instruction = "告诉用户退款处理中,原交付已按政策冻结。然后读取同一退款的权威状态;Agent 不读取交付、不创建授权或重复申请退款。";
23
+ instruction = refundStateEnvelope(lockedRefund, "shown").instruction;
23
24
  if (!refundTerminal) {
24
25
  next = { command: `itpay refund get ${lockedRefund.refund_request_id} --json`, reason: "读取退款的服务器状态" };
25
26
  }
@@ -41,9 +42,9 @@ function orderEnvelope(order, delivery, lockedRefund) {
41
42
  else if (order.status === "pending_payment") {
42
43
  const remaining = typeof order.payment_remaining_seconds === "number" && order.payment_remaining_seconds <= 0;
43
44
  instruction = remaining
44
- ? "先告诉用户这笔订单的付款时限已到,可能已被取消;如刚完成付款请说明系统正在核对到账,不要重复支付;否则引导用户重新核价下单,刷新不会延长付款期限。"
45
+ ? "付款时限已到,但订单权威状态仍为待付款;读取同一订单核对到账或终止结果。不要重复支付、重建订单或推测已取消,刷新不会延长期限。"
45
46
  : `先告诉用户订单正在等待付款${order.payment_deadline_at ? `,付款截止时间为 ${order.payment_deadline_at}` : ""},超时未支付会被自动取消;不要创建替代订单,也不要重复发起支付授权。`;
46
- next = { command: `itpay order ${order.order_id} --json`, reason: "刷新订单支付状态" };
47
+ next = { command: `itpay order ${order.order_id} --json`, reason: "展示后停止;仅用户说已付款或要求核对时读取同一订单,到期可核对原单" };
47
48
  }
48
49
  else if (!["delivered", "refunded", "failed", "cancelled"].includes(order.status)) {
49
50
  instruction = "先告诉用户订单仍在处理,已记录的付款和订单不需要重复创建;稍后查询同一订单,不要创建替代订单。";
@@ -1,8 +1,8 @@
1
1
  import { HttpError } from "../client/http.js";
2
2
  import { formatMoney } from "../render/output.js";
3
3
  import { resolveOutput } from "../render/sink.js";
4
- import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
5
- import { accessContextInstruction, vaultAccessCommand } from "./vault.js";
4
+ import { CommandContractError, shellArgument, writeCommandEnvelope } from "./guidance.js";
5
+ import { accessContextInstruction, vaultAccessGuidance } from "./vault.js";
6
6
  const ORDER_STATUSES = new Set([
7
7
  "pending_payment",
8
8
  "paid",
@@ -30,8 +30,8 @@ export async function runListOrders(backend, config, options) {
30
30
  writeCommandEnvelope({
31
31
  status: "human_authorization_required",
32
32
  result: { intent: "list_purchase_history" },
33
- instruction: `需要用户确认一次身份和只读权限。执行 next.command 生成官方入口;用户完成后重新运行原始 orders 命令。${accessContextInstruction(options)}`,
34
- next: { command: vaultAccessCommand(undefined, options), reason: "创建一次账号读取授权" },
33
+ instruction: `需要用户确认一次身份和只读权限。按当前动作或待填模板生成官方入口;用户完成后重新运行原始 orders 命令。${accessContextInstruction(options)}`,
34
+ ...vaultAccessGuidance(undefined, options),
35
35
  recovery: [],
36
36
  }, {
37
37
  ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
@@ -92,8 +92,3 @@ function ordersPageCommand(cursor, options) {
92
92
  parts.push("--json");
93
93
  return parts.join(" ");
94
94
  }
95
- function shellArgument(value) {
96
- if (/^[\p{L}\p{N}._:=/-]+$/u.test(value))
97
- return value;
98
- return `'${value.replaceAll("'", `'"'"'`)}'`;
99
- }
@@ -53,7 +53,7 @@ function payEnvelope(intent, options) {
53
53
  instruction: payInstruction(options, verified, terminal, hasAction, amount),
54
54
  next: {
55
55
  command: `itpay checkout --id ${options.checkoutID} --token ${options.displayToken} --json`,
56
- reason: verified ? "读取权威订单和履约状态" : "读取同一 Checkout 的权威付款状态",
56
+ reason: verified ? "读取权威订单和履约状态" : "展示后停止;仅用户完成付款或要求核对时读取同一 Checkout",
57
57
  },
58
58
  recovery: [],
59
59
  };