@itpay/cli 2.0.3 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +92 -142
  2. package/dist/src/client/backend.js +26 -8
  3. package/dist/src/commands/buy.js +84 -132
  4. package/dist/src/commands/cart.js +274 -169
  5. package/dist/src/commands/catalog.js +63 -38
  6. package/dist/src/commands/checkout.js +117 -78
  7. package/dist/src/commands/docs.js +97 -51
  8. package/dist/src/commands/guidance.js +87 -15
  9. package/dist/src/commands/install.js +50 -87
  10. package/dist/src/commands/next.js +45 -0
  11. package/dist/src/commands/order.js +44 -69
  12. package/dist/src/commands/orders.js +43 -15
  13. package/dist/src/commands/pay.js +51 -22
  14. package/dist/src/commands/readyz.js +8 -4
  15. package/dist/src/commands/refund.js +132 -11
  16. package/dist/src/commands/services.js +719 -148
  17. package/dist/src/main.js +711 -193
  18. package/dist/src/render/output.js +2 -3
  19. package/dist/src/state/cart_session.js +13 -17
  20. package/dist/src/state/client_context.js +4 -2
  21. package/dist/src/state/config.js +2 -4
  22. package/dist/src/state/device_authority.js +1 -1
  23. package/docs/agent/buyer/cart-checkout.json +27 -83
  24. package/docs/agent/buyer/install-and-setup.json +23 -67
  25. package/docs/agent/buyer/orders-refunds.json +31 -53
  26. package/docs/agent/buyer/payment-flow.json +24 -57
  27. package/docs/agent/buyer/quickstart.json +39 -162
  28. package/docs/agent/buyer/render-hosts.json +43 -57
  29. package/docs/cli-reference/agent-types.md +45 -0
  30. package/docs/cli-reference/commands/buy.md +167 -0
  31. package/docs/cli-reference/commands/cart/add.md +86 -0
  32. package/docs/cli-reference/commands/cart/clear.md +53 -0
  33. package/docs/cli-reference/commands/cart/index.md +30 -0
  34. package/docs/cli-reference/commands/cart/next.md +71 -0
  35. package/docs/cli-reference/commands/cart/remove.md +53 -0
  36. package/docs/cli-reference/commands/cart/show.md +65 -0
  37. package/docs/cli-reference/commands/catalog/index.md +26 -0
  38. package/docs/cli-reference/commands/catalog/list.md +45 -0
  39. package/docs/cli-reference/commands/checkout.md +74 -0
  40. package/docs/cli-reference/commands/docs/index.md +28 -0
  41. package/docs/cli-reference/commands/docs/list.md +51 -0
  42. package/docs/cli-reference/commands/docs/search.md +69 -0
  43. package/docs/cli-reference/commands/docs/show.md +68 -0
  44. package/docs/cli-reference/commands/install.md +112 -0
  45. package/docs/cli-reference/commands/next.md +87 -0
  46. package/docs/cli-reference/commands/order.md +92 -0
  47. package/docs/cli-reference/commands/orders.md +83 -0
  48. package/docs/cli-reference/commands/pay.md +103 -0
  49. package/docs/cli-reference/commands/readyz.md +39 -0
  50. package/docs/cli-reference/commands/refund/cancel.md +62 -0
  51. package/docs/cli-reference/commands/refund/create.md +85 -0
  52. package/docs/cli-reference/commands/refund/get.md +60 -0
  53. package/docs/cli-reference/commands/refund/index.md +33 -0
  54. package/docs/cli-reference/commands/refund/list.md +68 -0
  55. package/docs/cli-reference/commands/refund/watch.md +73 -0
  56. package/docs/cli-reference/commands/services/action.md +43 -0
  57. package/docs/cli-reference/commands/services/checkout.md +82 -0
  58. package/docs/cli-reference/commands/services/events.md +73 -0
  59. package/docs/cli-reference/commands/services/get.md +66 -0
  60. package/docs/cli-reference/commands/services/index.md +45 -0
  61. package/docs/cli-reference/commands/services/invoke.md +67 -0
  62. package/docs/cli-reference/commands/services/list.md +61 -0
  63. package/docs/cli-reference/commands/services/next.md +162 -0
  64. package/docs/cli-reference/commands/services/quote.md +59 -0
  65. package/docs/cli-reference/commands/services/read-result.md +98 -0
  66. package/docs/cli-reference/commands/services/start.md +53 -0
  67. package/docs/cli-reference/conventions.md +94 -0
  68. package/docs/cli-reference/index.md +64 -0
  69. package/package.json +1 -1
  70. package/skills/itpay-buyer/SKILL.md +47 -113
@@ -1,8 +1,8 @@
1
1
  // CLI cart commands. Server cart is canonical for agent purchase/free-trial
2
2
  // flows; local cart helpers are kept for explicit local draft compatibility.
3
- import { HttpError } from "../client/http.js";
3
+ import { formatMoney } from "../render/output.js";
4
4
  import { resolveOutput } from "../render/sink.js";
5
- import { attachAgentGuidance, buildCartGuidance, printAgentGuidance } from "./guidance.js";
5
+ import { writeCommandEnvelope } from "./guidance.js";
6
6
  export function runCartAdd(session, options) {
7
7
  const out = resolveOutput(options.output);
8
8
  const item = {
@@ -13,11 +13,25 @@ export function runCartAdd(session, options) {
13
13
  ...(options.input ? { input: options.input } : {}),
14
14
  };
15
15
  session.add(item);
16
- out(`added ${item.quantity} × variant=${item.catalogVariantID} offer=${item.offerID} to cart\n`);
16
+ const envelope = {
17
+ status: "added_local",
18
+ result: {
19
+ catalog_item_id: item.catalogItemID,
20
+ catalog_variant_id: item.catalogVariantID,
21
+ offer_id: item.offerID,
22
+ quantity: item.quantity,
23
+ },
24
+ instruction: "仅写入本地兼容草稿,未验证目录、价格或服务合同;不要把它当作 canonical Cart。",
25
+ next: { command: "itpay cart show --local", reason: "检查本地草稿" },
26
+ recovery: [],
27
+ };
28
+ writeCommandEnvelope(envelope, {
29
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
30
+ output: out,
31
+ });
17
32
  }
18
33
  export async function runCartAddServer(options) {
19
34
  const out = resolveOutput(options.output);
20
- const agentDeviceID = options.session.ensureAgentDeviceID(options.config.agentDeviceID);
21
35
  const item = {
22
36
  catalog_item_id: options.catalogItemID,
23
37
  catalog_variant_id: options.catalogVariantID,
@@ -28,17 +42,14 @@ export async function runCartAddServer(options) {
28
42
  const clientContext = {
29
43
  host: options.host,
30
44
  ...(options.target ? { target: options.target } : {}),
31
- agent_device_id: agentDeviceID,
32
45
  };
33
46
  const cart = options.session.lastCartID
34
47
  ? await options.backend.addCartItem(options.session.lastCartID, {
35
48
  ...item,
36
- agent_device_id: agentDeviceID,
37
49
  client_context: clientContext,
38
50
  })
39
51
  : await options.backend.createCart({
40
52
  currency: options.config.checkoutCurrency,
41
- agent_device_id: agentDeviceID,
42
53
  client_context: clientContext,
43
54
  items: [item],
44
55
  });
@@ -47,29 +58,99 @@ export async function runCartAddServer(options) {
47
58
  cartID: cart.cart_id,
48
59
  ...(line?.cart_item_id ? { cartItemID: line.cart_item_id } : {}),
49
60
  ...(line?.service_execution_id ? { serviceExecutionID: line.service_execution_id } : {}),
50
- agentDeviceID,
51
61
  });
52
- const serviceModel = line?.service_execution_id
53
- ? await getServiceReadModel(options.backend, line.service_execution_id)
54
- : undefined;
55
- const guidance = buildCartGuidance(cart, serviceModel);
56
- if (options.jsonOutput) {
57
- out(JSON.stringify(attachAgentGuidance(cart, guidance), null, 2) + "\n");
62
+ if (!line?.cart_item_id) {
63
+ throw new Error("backend did not return the added cart item");
58
64
  }
59
- else {
60
- out(`cart: ${cart.cart_id}\n`);
61
- if (line?.cart_item_id)
62
- out(`line: ${line.cart_item_id}\n`);
63
- if (line?.service_execution_id)
64
- out(`service execution: ${line.service_execution_id}\n`);
65
- printAgentGuidance(guidance, options.output);
65
+ const serviceExecutionID = line.service_execution_id;
66
+ const envelope = {
67
+ status: "added",
68
+ result: {
69
+ cart_id: cart.cart_id,
70
+ cart_item_id: line.cart_item_id,
71
+ ...(serviceExecutionID ? { service_execution_id: serviceExecutionID } : {}),
72
+ title: line.title,
73
+ amount: formatMoney(line.amount_minor, line.currency),
74
+ },
75
+ instruction: serviceExecutionID
76
+ ? "服务型项目已创建 Service Execution;先读取其当前步骤,不要直接进入普通 buy。"
77
+ : "普通项目已加入 canonical Cart;先检查购物车,再创建 Checkout。",
78
+ next: serviceExecutionID
79
+ ? { command: `itpay services next ${serviceExecutionID} --json`, reason: "读取服务执行的当前步骤" }
80
+ : { command: "itpay cart next --json", reason: "检查 canonical Cart" },
81
+ recovery: [],
82
+ };
83
+ writeCommandEnvelope(envelope, {
84
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
85
+ output: out,
86
+ plainResult: Object.entries(envelope.result).map(([key, value]) => `${key}: ${String(value)}`),
87
+ });
88
+ return cart;
89
+ }
90
+ export async function runCartAddQuoteServer(options) {
91
+ const out = resolveOutput(options.output);
92
+ const clientContext = {
93
+ host: options.host,
94
+ ...(options.target ? { target: options.target } : {}),
95
+ };
96
+ const quoteItem = { service_quote_lock_id: options.serviceQuoteLockID };
97
+ const cart = options.session.lastCartID
98
+ ? await options.backend.addCartItem(options.session.lastCartID, {
99
+ ...quoteItem,
100
+ client_context: clientContext,
101
+ })
102
+ : await options.backend.createCart({
103
+ currency: options.config.checkoutCurrency,
104
+ client_context: clientContext,
105
+ items: [quoteItem],
106
+ });
107
+ const line = cart.items[cart.items.length - 1];
108
+ if (!line?.cart_item_id) {
109
+ throw new Error("backend did not return the added cart item");
66
110
  }
111
+ options.session.rememberServerCart({
112
+ cartID: cart.cart_id,
113
+ cartItemID: line.cart_item_id,
114
+ ...(line.service_execution_id ? { serviceExecutionID: line.service_execution_id } : {}),
115
+ });
116
+ const envelope = {
117
+ status: "quote_added",
118
+ result: {
119
+ cart_id: cart.cart_id,
120
+ item_count: cart.items.length,
121
+ total: formatMoney(cart.amount_minor, cart.currency),
122
+ },
123
+ instruction: "付费服务报价已加入同一 Cart;每个项目仍保持独立 Execution 和交付。",
124
+ next: { command: `itpay buy --cart ${cart.cart_id} --json`, reason: "确认项目齐全后创建一次合并付款" },
125
+ recovery: [{ command: "itpay cart show --json", reason: "检查当前合并 Cart" }],
126
+ };
127
+ writeCommandEnvelope(envelope, {
128
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
129
+ output: out,
130
+ plainResult: [
131
+ `cart_id: ${cart.cart_id}`,
132
+ `item_count: ${cart.items.length}`,
133
+ `total: ${formatMoney(cart.amount_minor, cart.currency)}`,
134
+ ],
135
+ });
67
136
  return cart;
68
137
  }
69
138
  export function runCartRemove(session, options) {
70
139
  const out = resolveOutput(options.output);
71
140
  session.remove(options.catalogVariantID, options.offerID);
72
- out(`removed variant=${options.catalogVariantID} offer=${options.offerID}\n`);
141
+ writeCommandEnvelope({
142
+ status: "removed_local",
143
+ result: {
144
+ catalog_variant_id: options.catalogVariantID,
145
+ offer_id: options.offerID,
146
+ },
147
+ instruction: "本地兼容草稿已更新;这不代表任何 canonical Cart 或 Service Execution 已变更。",
148
+ next: { command: "itpay cart show --local --json", reason: "检查剩余本地草稿" },
149
+ recovery: [],
150
+ }, {
151
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
152
+ output: out,
153
+ });
73
154
  }
74
155
  export async function runCartRemoveServer(backend, session, cartItemID, options = {}) {
75
156
  const out = resolveOutput(options.output);
@@ -82,183 +163,207 @@ export async function runCartRemoveServer(backend, session, cartItemID, options
82
163
  throw new Error("missing cart item id; pass --line <cart_item_id>");
83
164
  }
84
165
  const cart = await backend.removeCartItem(cartID, lineID);
85
- out(`removed line=${lineID} from cart=${cart.cart_id}\n`);
86
- out(`cart ${cart.cart_id} (${cart.status}, ${cart.amount_minor} ${cart.currency})\n`);
166
+ const latestLine = cart.items[cart.items.length - 1];
167
+ session.rememberServerCart({
168
+ cartID: cart.cart_id,
169
+ ...(latestLine?.cart_item_id ? { cartItemID: latestLine.cart_item_id } : {}),
170
+ ...(latestLine?.service_execution_id ? { serviceExecutionID: latestLine.service_execution_id } : {}),
171
+ });
172
+ const envelope = {
173
+ status: "removed",
174
+ result: {
175
+ cart_id: cart.cart_id,
176
+ cart_item_id: lineID,
177
+ remaining_item_count: cart.items.length,
178
+ },
179
+ instruction: "canonical Cart 已更新;被删除的最后一个 service-backed line 对应执行会由服务端一致性事务取消。",
180
+ next: { command: "itpay cart next --json", reason: "检查剩余内容" },
181
+ recovery: [],
182
+ };
183
+ writeCommandEnvelope(envelope, {
184
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
185
+ output: out,
186
+ });
87
187
  return cart;
88
188
  }
89
189
  export function runCartShow(session, options = {}) {
90
- const out = resolveOutput(options.output);
91
190
  const snap = session.show();
92
- out(`cart (currency=${snap.currency})\n`);
93
- if (snap.items.length === 0) {
94
- out(" (empty)\n");
95
- return;
96
- }
97
- for (const item of snap.items) {
98
- out(` - ${item.quantity} × variant=${item.catalogVariantID} offer=${item.offerID}` +
99
- (item.input ? ` input=${JSON.stringify(item.input)}` : "") +
100
- "\n");
101
- }
102
- if (snap.lastCheckoutID) {
103
- out(`last checkout: ${snap.lastCheckoutID}\n`);
104
- if (snap.lastCheckoutURL) {
105
- out(`checkout url: ${snap.lastCheckoutURL}\n`);
106
- }
107
- }
191
+ const items = snap.items.map((item) => ({
192
+ catalog_item_id: item.catalogItemID,
193
+ catalog_variant_id: item.catalogVariantID,
194
+ offer_id: item.offerID,
195
+ quantity: item.quantity,
196
+ }));
197
+ const envelope = {
198
+ status: items.length > 0 ? "shown_local" : "local_empty",
199
+ result: { currency: snap.currency, items },
200
+ instruction: items.length > 0
201
+ ? "这是未验证的本地兼容草稿,只能用于明确的普通商品流程;不要把它当作 canonical Cart。"
202
+ : "本地兼容草稿为空;从已发布目录重新选择项目。",
203
+ next: items.length > 0
204
+ ? { command: "itpay buy --json", reason: "将普通本地草稿提交为 canonical Cart" }
205
+ : { command: "itpay catalog list --json", reason: "读取已发布目录" },
206
+ recovery: [],
207
+ };
208
+ writeCommandEnvelope(envelope, {
209
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
210
+ ...(options.output ? { output: options.output } : {}),
211
+ });
108
212
  }
109
213
  export async function runCartShowServer(backend, session, options = {}) {
110
- const out = resolveOutput(options.output);
111
214
  if (!session.lastCartID) {
112
- runCartShow(session, options);
215
+ writeCommandEnvelope({
216
+ status: "cart_handle_missing",
217
+ result: {},
218
+ instruction: "本地没有 canonical Cart 句柄;不要把本地草稿默认为服务端 Cart。",
219
+ next: { command: "itpay catalog list --json", reason: "读取已发布目录" },
220
+ recovery: [{ command: "itpay cart show --local --json", reason: "仅在明确需要时检查本地兼容草稿" }],
221
+ }, {
222
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
223
+ ...(options.output ? { output: options.output } : {}),
224
+ });
113
225
  return undefined;
114
226
  }
115
227
  const cart = await backend.getCart(session.lastCartID);
116
- out(`cart ${cart.cart_id} (${cart.status}, ${cart.amount_minor} ${cart.currency})\n`);
117
- if (cart.items.length === 0) {
118
- out(" (empty)\n");
119
- return cart;
120
- }
121
- for (const item of cart.items) {
122
- out(` - ${item.quantity} × ${item.title} ${item.amount_minor} ${item.currency}\n`);
123
- out(` line=${item.cart_item_id ?? ""} variant=${item.catalog_variant_id ?? ""} offer=${item.offer_id ?? ""}\n`);
124
- if (item.service_execution_id) {
125
- out(` service_execution=${item.service_execution_id} next_action=${item.next_action ?? ""} checkout_required=${String(item.checkout_required ?? false)}\n`);
126
- }
127
- if (item.service_quote_lock_id) {
128
- out(` service_quote_lock=${item.service_quote_lock_id}\n`);
129
- }
130
- }
131
- const serviceModel = await getServiceReadModelForCart(backend, cart);
132
- printAgentGuidance(buildCartGuidance(cart, serviceModel), options.output);
228
+ const items = cart.items.map((item) => ({
229
+ ...(item.cart_item_id ? { cart_item_id: item.cart_item_id } : {}),
230
+ title: item.title,
231
+ quantity: item.quantity,
232
+ ...(item.service_execution_id ? { service_execution_id: item.service_execution_id } : {}),
233
+ }));
234
+ const envelope = {
235
+ status: "shown",
236
+ result: {
237
+ cart_id: cart.cart_id,
238
+ status: cart.status,
239
+ amount: formatMoney(cart.amount_minor, cart.currency),
240
+ items,
241
+ },
242
+ instruction: items.length > 0
243
+ ? "使用 line execution 句柄继续;不要使用内部 quote lock ID。"
244
+ : "canonical Cart 当前为空;从已发布目录选择项目。",
245
+ next: items.length > 0
246
+ ? { command: "itpay cart next --json", reason: "取得当前首选动作" }
247
+ : { command: "itpay catalog list --json", reason: "读取已发布目录" },
248
+ recovery: [],
249
+ };
250
+ writeCommandEnvelope(envelope, {
251
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
252
+ ...(options.output ? { output: options.output } : {}),
253
+ plainResult: [
254
+ `cart_id: ${cart.cart_id}`,
255
+ `state: ${cart.status}`,
256
+ `amount: ${formatMoney(cart.amount_minor, cart.currency)}`,
257
+ ...items.map((item) => `${item.cart_item_id ?? "line"}: ${item.title} x${item.quantity}${item.service_execution_id ? ` service_execution=${item.service_execution_id}` : ""}`),
258
+ ],
259
+ });
133
260
  return cart;
134
261
  }
135
262
  export function runCartClear(session, options = {}) {
136
- const out = resolveOutput(options.output);
137
263
  session.clear();
138
- out("cart cleared\n");
264
+ const envelope = {
265
+ status: "cleared_local",
266
+ result: { server_abandoned: false, local_state_cleared: true },
267
+ instruction: "仅清除了本地草稿和恢复句柄;Backend Cart、Checkout 和 Service Execution 均未改变。",
268
+ next: { command: "itpay catalog list --json", reason: "仅在用户提出新需求时重新选择" },
269
+ recovery: [],
270
+ };
271
+ writeCommandEnvelope(envelope, {
272
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
273
+ ...(options.output ? { output: options.output } : {}),
274
+ });
139
275
  }
140
276
  export async function runCartAbandonServer(backend, session, options = {}) {
141
- const out = resolveOutput(options.output);
142
277
  if (!session.lastCartID) {
143
- runCartClear(session, options);
278
+ writeCommandEnvelope({
279
+ status: "cart_handle_missing",
280
+ result: {},
281
+ instruction: "本地没有 canonical Cart 句柄;未修改任何 Backend 或本地资源。",
282
+ next: { command: "itpay next --json", reason: "检查其他可恢复句柄" },
283
+ recovery: [{ command: "itpay cart clear --local --json", reason: "仅在明确放弃本地草稿和句柄时执行" }],
284
+ }, {
285
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
286
+ ...(options.output ? { output: options.output } : {}),
287
+ });
144
288
  return undefined;
145
289
  }
146
- let cart;
147
- try {
148
- cart = await backend.abandonCart(session.lastCartID);
149
- }
150
- catch (error) {
151
- if (error instanceof HttpError && (error.code === "cart_item_locked" || error.status === 409)) {
152
- const cartID = session.lastCartID;
153
- session.clear();
154
- out(`cart local handle cleared; server cart ${cartID} is locked by quote/checkout\n`);
155
- return undefined;
156
- }
157
- throw error;
158
- }
290
+ const cart = await backend.abandonCart(session.lastCartID);
159
291
  session.clear();
160
- out(`cart abandoned: ${cart.cart_id}\n`);
292
+ const envelope = {
293
+ status: "abandoned",
294
+ result: { cart_id: cart.cart_id, server_abandoned: true },
295
+ instruction: "canonical Cart 已放弃;Backend 已在同一事务中软删除 active lines,并取消其未付款 Service Execution。",
296
+ next: { command: "itpay catalog list --json", reason: "仅在用户提出新需求时重新选择" },
297
+ recovery: [],
298
+ };
299
+ writeCommandEnvelope(envelope, {
300
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
301
+ ...(options.output ? { output: options.output } : {}),
302
+ plainResult: [`cart_id: ${cart.cart_id}`, "server_abandoned: true"],
303
+ });
161
304
  return cart;
162
305
  }
163
306
  export async function runCartNext(backend, session, options = {}) {
164
- const out = resolveOutput(options.output);
165
- if (session.lastCartID) {
166
- const cart = await backend.getCart(session.lastCartID);
167
- const guidance = buildCartGuidance(cart, await getServiceReadModelForCart(backend, cart));
168
- if (options.jsonOutput) {
169
- out(JSON.stringify(guidance, null, 2) + "\n");
170
- }
171
- else {
172
- printAgentGuidance(guidance, options.output);
173
- }
174
- return;
175
- }
176
- const snap = session.show();
177
- if (snap.lastServiceExecutionID) {
178
- const model = await getServiceReadModel(backend, snap.lastServiceExecutionID);
179
- const guidance = model
180
- ? buildCartGuidance({
181
- cart_id: snap.lastCartID ?? "<unknown_cart>",
182
- status: "unknown",
183
- amount_minor: 0,
184
- currency: snap.currency,
185
- items: [{
186
- title: "Service Execution",
187
- quantity: 1,
188
- amount_minor: 0,
189
- currency: snap.currency,
190
- service_execution_id: snap.lastServiceExecutionID,
191
- }],
192
- }, model)
193
- : buildCartGuidance({
194
- cart_id: snap.lastCartID ?? "<unknown_cart>",
195
- status: "unknown",
196
- amount_minor: 0,
197
- currency: snap.currency,
198
- items: [{
199
- title: "Service Execution",
200
- quantity: 1,
201
- amount_minor: 0,
202
- currency: snap.currency,
203
- service_execution_id: snap.lastServiceExecutionID,
204
- }],
205
- });
206
- if (options.jsonOutput) {
207
- out(JSON.stringify(guidance, null, 2) + "\n");
208
- }
209
- else {
210
- printAgentGuidance(guidance, options.output);
211
- }
307
+ if (!session.lastCartID) {
308
+ writeCartNextEnvelope({
309
+ status: "cart_handle_missing",
310
+ result: {},
311
+ instruction: "本地没有 canonical Cart 句柄;先恢复已有资源,不要创建重复 Cart。",
312
+ next: { command: "itpay next --json", reason: "检查其他可恢复句柄" },
313
+ recovery: [{ command: "itpay services list --json", reason: "从服务端恢复当前设备的执行" }],
314
+ }, options);
212
315
  return;
213
316
  }
214
- if (snap.lastCheckoutID && snap.lastDisplayToken) {
215
- const guidance = {
216
- kind: "checkout_handle",
217
- summary: `checkout ${snap.lastCheckoutID}: continue human checkout`,
218
- state: { checkout_id: snap.lastCheckoutID },
219
- next_actions: [{
220
- id: "open_checkout",
221
- label: "Open saved checkout presentation",
222
- command: `itpay checkout --id ${snap.lastCheckoutID} --token ${snap.lastDisplayToken}`,
223
- requires_human: true,
224
- }],
317
+ const cart = await backend.getCart(session.lastCartID);
318
+ const unquotedServiceLine = [...cart.items].reverse().find((item) => item.service_execution_id && !item.service_quote_lock_id);
319
+ let envelope;
320
+ if (unquotedServiceLine?.service_execution_id) {
321
+ envelope = {
322
+ status: "action_available",
323
+ result: {
324
+ cart_id: cart.cart_id,
325
+ cart_status: cart.status,
326
+ service_execution_id: unquotedServiceLine.service_execution_id,
327
+ },
328
+ instruction: "该 Cart 包含 service-backed line;继续 Service Execution,不要从 Cart 猜 capability。",
329
+ next: {
330
+ command: `itpay services next ${unquotedServiceLine.service_execution_id} --json`,
331
+ reason: "读取服务端最新执行状态",
332
+ },
225
333
  recovery: [],
226
334
  };
227
- if (options.jsonOutput) {
228
- out(JSON.stringify(guidance, null, 2) + "\n");
229
- }
230
- else {
231
- printAgentGuidance(guidance, options.output);
232
- }
233
- return;
234
335
  }
235
- const guidance = {
236
- kind: "no_active_handle",
237
- summary: "no active server cart, service execution, or checkout handle remembered",
238
- state: {},
239
- next_actions: [{
240
- id: "browse_catalog",
241
- label: "Browse services",
242
- command: "itpay catalog list",
243
- }],
244
- recovery: [],
245
- };
246
- if (options.jsonOutput) {
247
- out(JSON.stringify(guidance, null, 2) + "\n");
336
+ else if (cart.items.length === 0) {
337
+ envelope = {
338
+ status: "cart_empty",
339
+ result: { cart_id: cart.cart_id, cart_status: cart.status },
340
+ instruction: "Cart 当前为空;先读取已发布目录,不要猜测 item 或 variant。",
341
+ next: { command: "itpay catalog list --json", reason: "选择已发布服务" },
342
+ recovery: [],
343
+ };
248
344
  }
249
345
  else {
250
- printAgentGuidance(guidance, options.output);
346
+ envelope = {
347
+ status: "action_available",
348
+ result: {
349
+ cart_id: cart.cart_id,
350
+ cart_status: cart.status,
351
+ item_count: cart.items.length,
352
+ amount_minor: cart.amount_minor,
353
+ currency: cart.currency,
354
+ },
355
+ instruction: cart.items.some((item) => item.service_quote_lock_id)
356
+ ? "服务报价已锁定输入和价格;确认项目齐全后使用同一 Cart 创建一次 Checkout。"
357
+ : "该 Cart 是普通购买流程;使用同一 Cart 创建 Checkout,不要重复添加商品。",
358
+ next: { command: `itpay buy --cart ${cart.cart_id} --json`, reason: "继续 canonical Cart 结算" },
359
+ recovery: [],
360
+ };
251
361
  }
362
+ writeCartNextEnvelope(envelope, options);
252
363
  }
253
- async function getServiceReadModelForCart(backend, cart) {
254
- const serviceExecutionID = [...cart.items].reverse().find((item) => item.service_execution_id)?.service_execution_id;
255
- return serviceExecutionID ? getServiceReadModel(backend, serviceExecutionID) : undefined;
256
- }
257
- async function getServiceReadModel(backend, serviceExecutionID) {
258
- try {
259
- return await backend.getServiceExecution(serviceExecutionID);
260
- }
261
- catch {
262
- return undefined;
263
- }
364
+ function writeCartNextEnvelope(envelope, options) {
365
+ writeCommandEnvelope(envelope, {
366
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
367
+ ...(options.output ? { output: options.output } : {}),
368
+ });
264
369
  }
@@ -1,46 +1,71 @@
1
- import { resolveOutput } from "../render/sink.js";
1
+ import { writeCommandEnvelope } from "./guidance.js";
2
2
  export async function runCatalogList(backend, options = {}) {
3
3
  const manifest = await backend.getCatalogManifest();
4
- const items = manifest.manifest.items;
5
- const out = resolveOutput(options.output);
6
- if (options.jsonOutput) {
7
- out(JSON.stringify(manifest, null, 2) + "\n");
8
- return;
9
- }
10
- out(`ItPay 当前上线 ${items.length} 个服务(目录版本 ${manifest.version})\n\n`);
11
- for (const item of items) {
12
- out(` ${item.title}\n`);
13
- if (item.description)
14
- out(` ${item.description}\n`);
15
- if (item.service_flow) {
16
- const discovery = item.service_flow.discovery;
17
- const primary = item.service_flow.primary_service;
18
- out(`\n 先做什么:${discovery.title}\n`);
19
- out(` ${discovery.description}\n`);
20
- if (discovery.free_quota_limit !== undefined) {
21
- out(` 每台已登记设备可免费使用 ${discovery.free_quota_limit} 次。\n`);
22
- }
23
- if (discovery.paid_continuation) {
24
- const continuation = discovery.paid_continuation;
25
- out(` 免费次数用完后:${formatProductMoney(continuation.amount_minor, continuation.currency)}/次,继续使用该辅助步骤;结果直接返回给 agent${continuation.delivery_email_required ? ",需要用户提供收件邮箱" : ",不需要邮箱"}。\n`);
26
- }
27
- out(`\n 确认主体后:${primary.title},${formatProductMoney(primary.amount_minor, primary.currency)}/次\n`);
28
- out(` ${primary.description}\n`);
29
- if (primary.delivery_description)
30
- out(` ${primary.delivery_description}\n`);
4
+ const services = manifest.manifest.items.map(summarizeService);
5
+ const firstServiceID = manifest.manifest.items.find((item) => item.service_id)?.service_id;
6
+ const empty = services.length === 0;
7
+ writeCommandEnvelope({
8
+ status: empty ? "catalog_empty" : "listed",
9
+ result: { catalog_version: manifest.version, services },
10
+ instruction: empty
11
+ ? "当前没有已发布服务;稍后重试,不要猜测 service_id。"
12
+ : "向用户解释主服务、辅助步骤和价格;得到用户意图后再启动对应 service_id。",
13
+ next: empty
14
+ ? { command: "itpay catalog list", reason: "稍后重新读取已发布目录" }
15
+ : {
16
+ command: `itpay services start ${services.length === 1 && firstServiceID ? firstServiceID : "<service_id>"}`,
17
+ reason: "启动用户选择的服务",
18
+ },
19
+ recovery: [],
20
+ }, {
21
+ ...options,
22
+ plainResult: catalogPlainLines(manifest.version, services),
23
+ });
24
+ }
25
+ function summarizeService(item) {
26
+ const flow = item.service_flow;
27
+ return {
28
+ service_id: item.service_id ?? null,
29
+ title: item.title,
30
+ description: item.description ?? "",
31
+ ...(flow ? {
32
+ discovery: {
33
+ title: flow.discovery.title,
34
+ description: flow.discovery.description,
35
+ ...(flow.discovery.free_quota_limit !== undefined ? { free_quota: flow.discovery.free_quota_limit } : {}),
36
+ ...(flow.discovery.paid_continuation ? {
37
+ paid_price: formatProductMoney(flow.discovery.paid_continuation.amount_minor, flow.discovery.paid_continuation.currency),
38
+ } : {}),
39
+ },
40
+ primary_offer: {
41
+ title: flow.primary_service.title,
42
+ description: flow.primary_service.description,
43
+ price: formatProductMoney(flow.primary_service.amount_minor, flow.primary_service.currency),
44
+ },
45
+ } : {}),
46
+ };
47
+ }
48
+ function catalogPlainLines(version, services) {
49
+ const lines = [`catalog_version: ${version}`];
50
+ for (const service of services) {
51
+ lines.push(`service: ${String(service.title)}`);
52
+ lines.push(` service_id: ${String(service.service_id ?? "unavailable")}`);
53
+ if (service.description)
54
+ lines.push(` description: ${String(service.description)}`);
55
+ const discovery = service.discovery;
56
+ if (discovery) {
57
+ const quota = discovery.free_quota !== undefined ? `; free_quota: ${String(discovery.free_quota)}` : "";
58
+ const paid = discovery.paid_price ? `; paid_price: ${String(discovery.paid_price)}` : "";
59
+ lines.push(` discovery: ${String(discovery.title)}${quota}${paid}`);
60
+ lines.push(` ${String(discovery.description)}`);
31
61
  }
32
- out(`\n 服务 ID:${item.service_id ?? "未发布"}`);
33
- if (item.service_id)
34
- out(`(启动:itpay services start ${item.service_id})`);
35
- out("\n");
36
- if (item.variants.length > 0) {
37
- out(` 可购买项目:\n`);
38
- for (const variant of item.variants) {
39
- out(` - ${variant.catalog_variant_id} ${variant.title} ${variant.offer_id} ${formatProductMoney(variant.amount_minor, variant.currency)}\n`);
40
- }
62
+ const primary = service.primary_offer;
63
+ if (primary) {
64
+ lines.push(` primary_offer: ${String(primary.title)}; price: ${String(primary.price)}`);
65
+ lines.push(` ${String(primary.description)}`);
41
66
  }
42
- out("\n");
43
67
  }
68
+ return lines;
44
69
  }
45
70
  function formatProductMoney(amountMinor, currency) {
46
71
  return new Intl.NumberFormat("zh-CN", {