@itpay/cli 2.1.41 → 2.1.44

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 (148) hide show
  1. package/README.md +12 -5
  2. package/assets/sell-preview/index.html +6 -0
  3. package/assets/sell-preview/preview.js +9 -0
  4. package/bin/darwin-amd64/itpay-sell +0 -0
  5. package/bin/darwin-arm64/itpay-sell +0 -0
  6. package/bin/linux-amd64/itpay-sell +0 -0
  7. package/bin/linux-arm64/itpay-sell +0 -0
  8. package/bin/windows-amd64/itpay-sell.exe +0 -0
  9. package/dist/src/client/backend.js +38 -2
  10. package/dist/src/client/http.js +2 -0
  11. package/dist/src/commands/aftercare.js +12 -0
  12. package/dist/src/commands/buy.js +17 -58
  13. package/dist/src/commands/cart.js +17 -15
  14. package/dist/src/commands/catalog.js +39 -14
  15. package/dist/src/commands/checkout.js +146 -5
  16. package/dist/src/commands/checkout_handoff.js +11 -61
  17. package/dist/src/commands/docs.js +7 -18
  18. package/dist/src/commands/guidance.js +1 -134
  19. package/dist/src/commands/install.js +6 -3
  20. package/dist/src/commands/order.js +34 -3
  21. package/dist/src/commands/orders.js +4 -9
  22. package/dist/src/commands/pay.js +1 -1
  23. package/dist/src/commands/presentation.js +153 -0
  24. package/dist/src/commands/rail_catalog.js +1 -0
  25. package/dist/src/commands/readyz.js +1 -1
  26. package/dist/src/commands/refund.js +13 -57
  27. package/dist/src/commands/services/actions.js +133 -0
  28. package/dist/src/commands/services/commerce.js +197 -0
  29. package/dist/src/commands/services/lifecycle.js +418 -0
  30. package/dist/src/commands/services/results.js +130 -0
  31. package/dist/src/commands/services.js +14 -1068
  32. package/dist/src/commands/skill.js +8 -5
  33. package/dist/src/commands/vault.js +27 -20
  34. package/dist/src/commands/vault_handoff.js +18 -0
  35. package/dist/src/main.js +332 -46
  36. package/dist/src/render/browser.js +65 -0
  37. package/dist/src/render/checkout_handoff.js +101 -0
  38. package/dist/src/render/checkout_plan.js +47 -0
  39. package/dist/src/render/markdown.js +0 -3
  40. package/dist/src/render/qr.js +1 -1
  41. package/dist/src/sell/commands.js +80 -0
  42. package/dist/src/sell/contract.js +86 -0
  43. package/dist/src/sell/local.js +260 -0
  44. package/dist/src/sell/mcp.js +49 -0
  45. package/dist/src/sell/preview.js +70 -0
  46. package/dist/src/sell/sync.js +145 -0
  47. package/dist/src/services/adapter.js +1 -0
  48. package/dist/src/services/presentation.js +198 -0
  49. package/dist/src/services/rail/actions.js +51 -0
  50. package/dist/src/services/rail/adapter.js +36 -0
  51. package/dist/src/services/rail/aftercare.js +45 -0
  52. package/dist/src/services/rail/catalog.js +261 -0
  53. package/dist/src/services/rail/checkout.js +19 -0
  54. package/dist/src/services/rail/fulfillment.js +76 -0
  55. package/dist/src/services/rail/guide.js +110 -0
  56. package/dist/src/services/rail/invocation.js +54 -0
  57. package/dist/src/services/rail/page.js +76 -0
  58. package/dist/src/services/rail/pagination.js +6 -0
  59. package/dist/src/services/rail/query.js +71 -0
  60. package/dist/src/services/rail/readers.js +107 -0
  61. package/dist/src/services/rail/reading.js +125 -0
  62. package/dist/src/services/rail/recovery.js +51 -0
  63. package/dist/src/services/rail/results.js +378 -0
  64. package/dist/src/services/rail/review.js +268 -0
  65. package/dist/src/services/rail/saved.js +49 -0
  66. package/dist/src/services/rail/wait.js +11 -0
  67. package/dist/src/services/registry.js +31 -0
  68. package/dist/src/services/schema.js +21 -0
  69. package/dist/src/services/shared/envelope.js +184 -0
  70. package/dist/src/services/shared/input.js +39 -0
  71. package/dist/src/services/shared/pagination.js +28 -0
  72. package/dist/src/services/shared/platform.js +365 -0
  73. package/dist/src/services/shared/refund.js +40 -0
  74. package/dist/src/services/shared/workflow.js +28 -0
  75. package/dist/src/state/account_auth.js +354 -0
  76. package/dist/src/state/cart_session.js +4 -1
  77. package/dist/src/state/client_context.js +72 -0
  78. package/dist/src/state/config.js +44 -14
  79. package/dist/src/state/device_authority.js +402 -72
  80. package/dist/src/state/task_journal.js +90 -0
  81. package/docs/agent/buyer/cart-checkout.json +8 -6
  82. package/docs/agent/buyer/catalog-list.json +29 -9
  83. package/docs/agent/buyer/identity-and-sessions.json +26 -9
  84. package/docs/agent/buyer/install-and-setup.json +17 -15
  85. package/docs/agent/buyer/orders-refunds.json +25 -9
  86. package/docs/agent/buyer/payment-flow.json +6 -4
  87. package/docs/agent/buyer/purchased-content.json +18 -4
  88. package/docs/agent/buyer/quickstart.json +26 -17
  89. package/docs/agent/buyer/rail-booking.json +176 -0
  90. package/docs/agent/buyer/rail-fast-checkout.json +48 -0
  91. package/docs/agent/buyer/render-hosts.json +23 -12
  92. package/docs/agent/buyer/service-feedback.json +4 -3
  93. package/docs/cli-reference/agent-types.md +7 -2
  94. package/docs/cli-reference/commands/aftercare.md +15 -0
  95. package/docs/cli-reference/commands/auth.md +96 -0
  96. package/docs/cli-reference/commands/buy.md +2 -1
  97. package/docs/cli-reference/commands/cart/add.md +5 -1
  98. package/docs/cli-reference/commands/cart/clear.md +1 -1
  99. package/docs/cli-reference/commands/cart/index.md +1 -1
  100. package/docs/cli-reference/commands/cart/next.md +5 -1
  101. package/docs/cli-reference/commands/cart/remove.md +1 -1
  102. package/docs/cli-reference/commands/cart/show.md +1 -1
  103. package/docs/cli-reference/commands/catalog/index.md +1 -1
  104. package/docs/cli-reference/commands/catalog/list.md +2 -2
  105. package/docs/cli-reference/commands/checkout.md +12 -2
  106. package/docs/cli-reference/commands/device.md +18 -11
  107. package/docs/cli-reference/commands/docs/index.md +1 -1
  108. package/docs/cli-reference/commands/docs/list.md +1 -1
  109. package/docs/cli-reference/commands/docs/search.md +1 -1
  110. package/docs/cli-reference/commands/docs/show.md +1 -1
  111. package/docs/cli-reference/commands/install.md +8 -6
  112. package/docs/cli-reference/commands/next.md +1 -1
  113. package/docs/cli-reference/commands/order.md +1 -1
  114. package/docs/cli-reference/commands/pay.md +1 -1
  115. package/docs/cli-reference/commands/readyz.md +11 -11
  116. package/docs/cli-reference/commands/refund/cancel.md +1 -1
  117. package/docs/cli-reference/commands/refund/create.md +1 -1
  118. package/docs/cli-reference/commands/refund/get.md +2 -2
  119. package/docs/cli-reference/commands/refund/index.md +1 -1
  120. package/docs/cli-reference/commands/refund/list.md +1 -1
  121. package/docs/cli-reference/commands/refund/watch.md +1 -1
  122. package/docs/cli-reference/commands/sell.md +1364 -0
  123. package/docs/cli-reference/commands/services/action.md +24 -3
  124. package/docs/cli-reference/commands/services/checkout.md +17 -2
  125. package/docs/cli-reference/commands/services/events.md +1 -1
  126. package/docs/cli-reference/commands/services/get.md +4 -2
  127. package/docs/cli-reference/commands/services/index.md +4 -3
  128. package/docs/cli-reference/commands/services/invoke.md +6 -4
  129. package/docs/cli-reference/commands/services/list.md +1 -1
  130. package/docs/cli-reference/commands/services/next.md +78 -12
  131. package/docs/cli-reference/commands/services/page.md +71 -0
  132. package/docs/cli-reference/commands/services/quote.md +4 -4
  133. package/docs/cli-reference/commands/services/read-result.md +17 -6
  134. package/docs/cli-reference/commands/services/run.md +152 -0
  135. package/docs/cli-reference/commands/services/start.md +5 -3
  136. package/docs/cli-reference/commands/vault/access.md +5 -1
  137. package/docs/cli-reference/conventions.md +4 -6
  138. package/docs/cli-reference/index.md +10 -1
  139. package/docs/sell.md +84 -0
  140. package/package.json +20 -12
  141. package/skills/itpay/SKILL.md +112 -102
  142. package/docs/skill-bundle-rollout/01-mcp-authentication.md +0 -134
  143. package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +0 -210
  144. package/docs/skill-bundle-rollout/03-platform-publishing.md +0 -161
  145. package/docs/skill-bundle-rollout/04-first-wave-platforms.md +0 -69
  146. package/docs/skill-bundle-rollout/05-sync-operations.md +0 -62
  147. package/docs/skill-bundle-rollout/README.md +0 -151
  148. /package/bin/{itp → itp.mjs} +0 -0
@@ -1,1068 +1,14 @@
1
- import { operationID } from "../state/config.js";
2
- import { validateContext } from "../state/client_context.js";
3
- import { dispatchRender } from "../render/index.js";
4
- import { ensureIdeImageAttach } from "../render/ide.js";
5
- import { buildCheckoutHandoff, shouldPrepareLocalCheckoutImage } from "./checkout_handoff.js";
6
- import { localizeCardURL, normalizeCardLocale } from "../render/locale.js";
7
- import { buildAgentChatHandoff } from "../render/markdown.js";
8
- import { platformKeyForHost } from "../render/plan.js";
9
- import { renderTerminalQR } from "../render/qr.js";
10
- import { buildCheckoutQRPlan } from "./buy.js";
11
- import { appendFeedbackPostmortemInstruction, CommandContractError, isTerminalServiceExecutionStatus, writeCommandEnvelope, } from "./guidance.js";
12
- const serviceActionStatuses = new Set(["pending", "approved", "rejected", "expired", "cancelled"]);
13
- export async function runServicesStart(backend, serviceID, options = {}) {
14
- const host = options.host ?? "terminal";
15
- const response = await backend.startServiceExecution({
16
- service_id: serviceID,
17
- client_context: {
18
- host,
19
- ...(options.target ? { target: options.target } : {}),
20
- ...(options.clientContext ?? {}),
21
- },
22
- });
23
- const capability = response.capabilities.find((item) => item.phase === response.execution.phase && !item.requires_payment);
24
- const requiredInput = requiredInputFields(capability?.input_schema);
25
- const command = capability
26
- ? `itpay services invoke ${response.execution.service_execution_id} --capability ${capability.capability_id}${requiredInput.map((field) => ` --input ${field}=<value>`).join("")} --json`
27
- : `itpay services next ${response.execution.service_execution_id} --json`;
28
- const capabilitySummary = capability ? {
29
- capability_id: capability.capability_id,
30
- required_input: requiredInput,
31
- ...(capability.free_quota_limit !== undefined ? { free_quota_limit: capability.free_quota_limit } : {}),
32
- } : null;
33
- writeCommandEnvelope({
34
- status: "ready",
35
- result: {
36
- service_execution_id: response.execution.service_execution_id,
37
- service_id: response.execution.service_id,
38
- phase: response.execution.phase,
39
- capability: capabilitySummary,
40
- },
41
- instruction: capability
42
- ? "填写首选 capability 的 required_input;一次只提交当前 execution 所代表的服务意图。"
43
- : "当前没有可直接调用的 capability;读取服务端下一步,不要猜测 capability。",
44
- next: {
45
- command,
46
- reason: capability ? "执行当前允许的能力" : "读取服务端计算的下一步",
47
- },
48
- recovery: [],
49
- }, {
50
- ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
51
- ...(options.output ? { output: options.output } : {}),
52
- plainResult: [
53
- `service_execution_id: ${response.execution.service_execution_id}`,
54
- `service_id: ${response.execution.service_id}`,
55
- `phase: ${response.execution.phase}`,
56
- ...(capability ? [
57
- `capability: ${capability.capability_id}`,
58
- `required_input: ${requiredInput.length > 0 ? requiredInput.join(",") : "none"}`,
59
- ...(capability.free_quota_limit !== undefined ? [`free_quota_limit: ${capability.free_quota_limit}`] : []),
60
- ] : []),
61
- ],
62
- });
63
- }
64
- function requiredInputFields(schema) {
65
- const required = schema?.required;
66
- return Array.isArray(required) ? required.filter((field) => typeof field === "string") : [];
67
- }
68
- export async function runServicesInvoke(backend, config, serviceExecutionID, capabilityID, input, options = {}) {
69
- const readModel = await backend.getServiceExecution(serviceExecutionID);
70
- const requestedCapability = readModel.capabilities.find((capability) => capability.capability_id === capabilityID);
71
- if (!requestedCapability) {
72
- throw new CommandContractError("capability_not_found", `capability ${capabilityID} is not available on service execution ${serviceExecutionID}`, "使用 Service Execution 当前返回的 capability_id,不要猜测名称。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前可用 capability" }]);
73
- }
74
- if (requestedCapability.requires_payment) {
75
- throw new CommandContractError("checkout_required", `capability ${capabilityID} requires checkout and cannot be invoked directly`, "付费 capability 不能直接 invoke。不要尝试 quote、cart、buy、checkout 或 pay 作为旁路;只恢复同一 Execution 的当前合法动作。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取同一 Execution 的当前合法动作" }]);
76
- }
77
- const missingInput = missingRequiredInput(requestedCapability.input_schema, input);
78
- if (missingInput.length > 0) {
79
- const correctedInput = { ...input };
80
- for (const field of missingInput)
81
- correctedInput[field] = "<value>";
82
- throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐 required_input 后重试同一个 execution;本次没有调用 Provider。", [{
83
- command: `itpay services invoke ${serviceExecutionID} --capability ${capabilityID}${formatInputOptions(correctedInput)} --json`,
84
- reason: "提交完整 capability 输入",
85
- }]);
86
- }
87
- const idempotencyKey = await operationID(config, `service.invoke:${serviceExecutionID}:${capabilityID}:${stableInput(input)}`);
88
- const response = await backend.invokeServiceCapability(serviceExecutionID, capabilityID, {
89
- idempotency_key: idempotencyKey,
90
- redacted_summary: input,
91
- });
92
- const envelope = invokedEnvelope(response, requestedCapability, readModel.capabilities, input);
93
- writeCommandEnvelope(envelope.value, {
94
- ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
95
- ...(options.output ? { output: options.output } : {}),
96
- plainResult: envelope.plainResult,
97
- });
98
- }
99
- function invokedEnvelope(response, requestedCapability, capabilities, input) {
100
- const items = response.result_items.map((item) => ({
101
- rank: item.rank,
102
- title: item.display_title,
103
- safe_payload: item.safe_payload,
104
- }));
105
- const quota = response.effective_quota
106
- ? { remaining: response.effective_quota.remaining, limit: response.effective_quota.limit }
107
- : undefined;
108
- const baseResult = {
109
- service_execution_id: response.execution.service_execution_id,
110
- capability_id: requestedCapability.capability_id,
111
- query: input,
112
- items,
113
- ...(quota ? { quota } : {}),
114
- };
115
- let status = items.length > 0 ? "result_ready" : "no_result";
116
- let instruction = items.length > 0
117
- ? "用编号、名称和可公开字段向用户说明候选;若候选列表已满足目标就停止。只有用户明确选择并希望继续时,才提交对应编号;不要向用户提及 safe_payload、Execution 或内部 ID。"
118
- : `没有找到与“${queryText(input)}”匹配的结果。向用户展示本次为 0 个结果并停止。不要修改、缩短或猜测其他输入;只有用户明确提供新输入后,才能启动新的查询。`;
119
- let next = null;
120
- if (response.effective_quota?.exhausted) {
121
- status = "quota_exhausted";
122
- instruction = "免费额度已用完且本次没有调用 Provider。当前没有可购买的 continuation;只读取同一 Execution 的服务端恢复方向。";
123
- const checkoutAction = response.next_actions?.find((action) => action.kind === "create_checkout");
124
- const checkoutCapability = capabilities.find((capability) => capability.capability_id === checkoutAction?.capability_id);
125
- if (checkoutCapability) {
126
- baseResult.checkout = {
127
- capability_id: checkoutCapability.capability_id,
128
- ...(checkoutCapability.price_amount_minor !== undefined && checkoutCapability.price_currency ? {
129
- price: { amount_minor: checkoutCapability.price_amount_minor, currency: checkoutCapability.price_currency },
130
- } : {}),
131
- delivery_email_required: checkoutCapability.delivery_email_required,
132
- };
133
- const price = capabilityPrice(checkoutCapability);
134
- instruction = purchaseConfirmationInstruction("quota_exhausted", price, checkoutCapability.delivery_email_required, checkoutCapability.delivery_email_purpose);
135
- next = {
136
- command: checkoutCommand(response.execution.service_execution_id, checkoutCapability, input),
137
- reason: `仅在用户明确同意支付 ${price} 后执行;否则停止`,
138
- };
139
- }
140
- else {
141
- next = {
142
- command: `itpay services next ${response.execution.service_execution_id} --json`,
143
- reason: "读取服务端提供的付费恢复入口",
144
- };
145
- }
146
- }
147
- else if (items.length > 0 && requestedCapability.requires_human_action) {
148
- next = {
149
- command: `itpay services action ${response.execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
150
- reason: "在当前 Execution 记录用户选择",
151
- };
152
- }
153
- else if (items.length === 0) {
154
- next = null;
155
- }
156
- return {
157
- value: { status, result: baseResult, instruction, next, recovery: [] },
158
- plainResult: serviceResultPlainLines(baseResult),
159
- };
160
- }
161
- function serviceResultPlainLines(result) {
162
- const lines = [
163
- `service_execution_id: ${String(result.service_execution_id)}`,
164
- `capability_id: ${String(result.capability_id)}`,
165
- ];
166
- const items = result.items;
167
- const query = result.query;
168
- if (query) {
169
- for (const [key, value] of Object.entries(query))
170
- lines.push(`${key}: ${String(value)}`);
171
- }
172
- if (items.length === 0)
173
- lines.push("results: 0");
174
- if (result.quota)
175
- lines.push(`quota: ${JSON.stringify(result.quota)}`);
176
- if (result.checkout)
177
- lines.push(`checkout: ${JSON.stringify(result.checkout)}`);
178
- if (items.length > 0) {
179
- lines.push("items:");
180
- for (const item of items) {
181
- lines.push(` ${item.rank}. ${item.title}`);
182
- for (const [key, value] of Object.entries(item.safe_payload)) {
183
- lines.push(` ${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
184
- }
185
- }
186
- }
187
- return lines;
188
- }
189
- function queryText(input) {
190
- const value = Object.values(input).find((item) => typeof item === "string" && item.trim() !== "");
191
- return typeof value === "string" ? value : JSON.stringify(input);
192
- }
193
- function missingRequiredInput(schema, input) {
194
- return requiredInputFields(schema).filter((field) => {
195
- if (!(field in input) || input[field] === null || input[field] === undefined)
196
- return true;
197
- return typeof input[field] === "string" && String(input[field]).trim() === "";
198
- });
199
- }
200
- function checkoutCommand(serviceExecutionID, capability, input, fillMissing = true) {
201
- const lockedInput = { ...input };
202
- if (fillMissing) {
203
- for (const field of missingRequiredInput(capability.input_schema, lockedInput))
204
- lockedInput[field] = "<value>";
205
- }
206
- return `itpay services checkout ${serviceExecutionID} --capability ${capability.capability_id}${formatInputOptions(lockedInput)}${capability.delivery_email_required ? " --email <email>" : ""} --json`;
207
- }
208
- function capabilityPrice(capability) {
209
- return capability.price_amount_minor !== undefined && capability.price_currency
210
- ? formatMoney(capability.price_amount_minor, capability.price_currency)
211
- : "当前发布价格";
212
- }
213
- function purchaseConfirmationInstruction(context, price, deliveryEmailRequired, deliveryEmailPurpose, candidateTitle = "") {
214
- const emailPurpose = deliveryEmailPurposeText(deliveryEmailPurpose);
215
- if (context === "quota_exhausted") {
216
- return deliveryEmailRequired
217
- ? `免费额度已用完,本次没有发送到数据来源,也没有创建付款页面。只向用户说明:继续当前请求需要支付 ${price},并提供${emailPurpose};请确认是否购买并提供邮箱。然后停止等待。用户明确同意并提供真实邮箱前,Agent 不执行 next.command,也不创建或尝试其他购买路径。`
218
- : `免费额度已用完,本次没有发送到数据来源,也没有创建付款页面。只向用户说明:“继续当前请求需要支付 ${price},是否购买?”然后停止等待。用户明确同意前,Agent 不执行 next.command,也不创建或尝试其他购买路径。`;
219
- }
220
- const selected = candidateTitle ? `已选择 ${candidateTitle}。` : "当前候选已经确认。";
221
- return deliveryEmailRequired
222
- ? `${selected}后续服务尚未购买。只向用户说明:继续购买需要支付 ${price},并提供${emailPurpose};请确认是否购买并提供邮箱。然后停止。用户明确同意并提供真实邮箱前,Agent 不执行 next.command,也不创建新的服务或付款页面。`
223
- : `${selected}后续服务尚未购买。只向用户说明:“继续购买后续服务需要支付 ${price},是否购买?”然后停止。用户明确同意前,Agent 不执行 next.command,也不创建新的服务或付款页面。`;
224
- }
225
- function deliveryEmailPurposeText(purpose) {
226
- switch (purpose) {
227
- case "receipt":
228
- return "用于发送订单收据的真实邮箱";
229
- case "claim":
230
- return "用于发送交付认领链接的真实邮箱";
231
- case "receipt_and_claim":
232
- return "用于发送订单收据和交付认领链接的真实邮箱";
233
- default:
234
- return "服务端声明用途的真实邮箱";
235
- }
236
- }
237
- function paidContinuation(model, action, input) {
238
- if (!action.capability_id)
239
- return null;
240
- const capability = model.capabilities.find((item) => item.capability_id === action.capability_id && item.requires_payment);
241
- if (!capability)
242
- return null;
243
- const price = capabilityPrice(capability);
244
- const stateBacked = model.execution.status === "quota_exhausted" || model.execution.status === "human_action_approved";
245
- return {
246
- capability,
247
- price,
248
- checkout: {
249
- capability_id: capability.capability_id,
250
- ...(capability.price_amount_minor !== undefined && capability.price_currency ? {
251
- price: { amount_minor: capability.price_amount_minor, currency: capability.price_currency },
252
- } : {}),
253
- delivery_email_required: capability.delivery_email_required,
254
- ...(capability.delivery_email_purpose ? { delivery_email_purpose: capability.delivery_email_purpose } : {}),
255
- },
256
- next: {
257
- command: checkoutCommand(model.execution.service_execution_id, capability, input, !stateBacked),
258
- reason: `仅在用户明确同意支付 ${price}${capability.delivery_email_required ? " 并提供真实邮箱" : ""}后执行;否则停止`,
259
- },
260
- };
261
- }
262
- function quoteCommand(serviceExecutionID, capability, input) {
263
- const lockedInput = { ...input };
264
- for (const field of missingRequiredInput(capability.input_schema, lockedInput))
265
- lockedInput[field] = "<value>";
266
- return `itpay services quote ${serviceExecutionID} --capability ${capability.capability_id}${formatInputOptions(lockedInput)}${capability.delivery_email_required ? " --email <email>" : ""} --json`;
267
- }
268
- function stableInput(input) {
269
- return JSON.stringify(Object.fromEntries(Object.entries(input).sort(([left], [right]) => left.localeCompare(right))));
270
- }
271
- function formatInputOptions(input) {
272
- return Object.entries(input)
273
- .sort(([left], [right]) => left.localeCompare(right))
274
- .map(([key, value]) => String(value) === "<value>"
275
- ? ` --input ${key}=<value>`
276
- : ` --input ${shellArgument(`${key}=${String(value)}`)}`)
277
- .join("");
278
- }
279
- function shellArgument(value) {
280
- if (/^[\p{L}\p{N}._:=/-]+$/u.test(value))
281
- return value;
282
- return `'${value.replaceAll("'", `'"'"'`)}'`;
283
- }
284
- export async function runServicesAction(backend, serviceExecutionID, actionType, input, options = {}) {
285
- const selection = await resolveCandidateSelection(backend, serviceExecutionID, actionType, options);
286
- const request = {
287
- action_type: actionType,
288
- input_snapshot: input,
289
- };
290
- if (options.actorType)
291
- request.actor_type = options.actorType;
292
- if (options.actorID)
293
- request.actor_id = options.actorID;
294
- if (options.status)
295
- request.status = normalizeServiceActionStatus(options.status, serviceExecutionID);
296
- const resultItemID = selection?.resultItemID ?? options.resultItemID;
297
- if (resultItemID)
298
- request.result_item_id = resultItemID;
299
- if (options.requiredBefore)
300
- request.required_before = options.requiredBefore;
301
- const response = await backend.recordServiceExecutionAction(serviceExecutionID, request);
302
- if (selection && actionType === "select_candidate" && response.status === "approved") {
303
- const updated = await backend.getServiceExecution(serviceExecutionID);
304
- const preferred = updated.allowed_actions?.[0];
305
- const continuation = preferred?.type === "prepare_quote"
306
- ? paidContinuation(updated, preferred, {})
307
- : null;
308
- const next = continuation?.next ?? (preferred ? serviceAllowedActionCommand(updated, preferred) : null);
309
- writeCommandEnvelope({
310
- status: "candidate_selected",
311
- result: {
312
- service_execution_id: response.service_execution_id,
313
- candidate: { rank: selection.rank, title: selection.title },
314
- ...(continuation ? { checkout: continuation.checkout } : {}),
315
- },
316
- instruction: continuation
317
- ? purchaseConfirmationInstruction("candidate_selected", continuation.price, continuation.capability.delivery_email_required, continuation.capability.delivery_email_purpose, selection.title)
318
- : "候选已绑定到来源 Execution;后续动作必须继续使用该 Execution。",
319
- next,
320
- recovery: [{
321
- command: `itpay services next ${response.service_execution_id} --json`,
322
- reason: "重新读取服务端允许的动作",
323
- }],
324
- }, {
325
- ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
326
- ...(options.output ? { output: options.output } : {}),
327
- });
328
- return;
329
- }
330
- writeCommandEnvelope({
331
- status: "action_recorded",
332
- result: {
333
- service_execution_id: response.service_execution_id,
334
- action_type: response.action_type,
335
- action_status: response.status,
336
- },
337
- instruction: "动作已记录,读取服务端计算的新状态;不要自行假设下一 capability。",
338
- next: {
339
- command: `itpay services next ${response.service_execution_id} --json`,
340
- reason: "取得更新后的首选动作",
341
- },
342
- recovery: [],
343
- }, {
344
- ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
345
- ...(options.output ? { output: options.output } : {}),
346
- });
347
- }
348
- async function resolveCandidateSelection(backend, serviceExecutionID, actionType, options) {
349
- if (options.candidateRank === undefined)
350
- return undefined;
351
- if (actionType !== "select_candidate") {
352
- throw actionInputError(serviceExecutionID, "--candidate is only valid with --action select_candidate");
353
- }
354
- if (options.resultItemID) {
355
- throw actionInputError(serviceExecutionID, "--candidate cannot be combined with --result-item");
356
- }
357
- if (!Number.isInteger(options.candidateRank) || options.candidateRank < 1) {
358
- throw actionInputError(serviceExecutionID, "--candidate must be a positive integer result rank");
359
- }
360
- const execution = await backend.getServiceExecution(serviceExecutionID);
361
- const currentItems = execution.current_result_items ?? [];
362
- const result = currentItems.find((item) => item.rank === options.candidateRank);
363
- if (!result) {
364
- throw actionInputError(serviceExecutionID, `candidate ${options.candidateRank} is not available on service execution ${serviceExecutionID}`, "candidate_not_found");
365
- }
366
- return {
367
- resultItemID: result.service_capability_result_item_id,
368
- rank: result.rank,
369
- title: result.display_title,
370
- };
371
- }
372
- function actionInputError(serviceExecutionID, message, code = "service_action_invalid") {
373
- return new CommandContractError(code, message, code === "candidate_not_found"
374
- ? "当前 rank 不存在或当前候选集不可用。不要新建 Execution,不要重新 invoke,不要构造候选 ID;只恢复同一 Execution 当前仍然有效的候选。"
375
- : "使用当前 safe result 中的合法 action 和 candidate rank;需要人确认时先询问用户。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "重新读取同一 Execution 的当前可选动作" }]);
376
- }
377
- export async function runServicesCheckout(backend, config, serviceExecutionID, capabilityID, options = {}) {
378
- const host = options.host ?? "terminal";
379
- const contextError = validateContext(host, options.target);
380
- if (contextError) {
381
- throw new CommandContractError(contextError.code, contextError.message, "从当前可信会话上下文补齐 Host/target;本次未创建 Checkout。", []);
382
- }
383
- const deliveryContact = {
384
- ...(options.deliveryContact ?? {}),
385
- ...(options.email ? { email: options.email } : {}),
386
- };
387
- if (!options.resume && !capabilityID) {
388
- throw new CommandContractError("capability_required", "--capability is required when creating a service checkout", "使用当前 Service Execution 返回的付费 capability;恢复已有 Checkout 时改用 --resume。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前允许的付费 capability" }]);
389
- }
390
- if (!options.resume) {
391
- const readModel = await backend.getServiceExecution(serviceExecutionID);
392
- const capability = readModel.capabilities.find((item) => item.capability_id === capabilityID);
393
- if (!capability || !capability.requires_payment) {
394
- throw new CommandContractError("capability_not_checkoutable", `capability ${capabilityID} is not available for checkout on service execution ${serviceExecutionID}`, "只为当前 Service Execution 返回的 requires_payment capability 创建 Checkout。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前允许的下一步" }]);
395
- }
396
- const lockedInput = options.lockedInput ?? {};
397
- const missingInput = missingRequiredInput(capability.input_schema, lockedInput);
398
- if (missingInput.length > 0 && readModel.execution.next_action !== "create_checkout") {
399
- throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐付费 capability 的 required_input;本次没有创建 quote、Checkout 或订单。", [{ command: checkoutCommand(serviceExecutionID, capability, lockedInput), reason: "提交完整且会被锁定的服务输入" }]);
400
- }
401
- if (capability.delivery_email_required && String(deliveryContact.email ?? "").trim() === "") {
402
- throw new CommandContractError("delivery_email_required", "delivery email is required before creating this service checkout", "该 capability 的交付链接会发送到用户邮箱;先向用户说明用途并询问邮箱,不要代填。", [{
403
- command: `itpay services checkout ${serviceExecutionID} --capability ${capability.capability_id}${formatInputOptions(lockedInput)} --email <email> --json`,
404
- reason: "使用用户提供的邮箱创建 Checkout",
405
- }]);
406
- }
407
- }
408
- const response = await backend.createServiceExecutionCheckout(serviceExecutionID, {
409
- ...(capabilityID ? { capability_id: capabilityID } : {}),
410
- ...(Object.keys(deliveryContact).length > 0 ? { delivery_contact: deliveryContact } : {}),
411
- ...(options.lockedInput && Object.keys(options.lockedInput).length > 0 ? { locked_input: options.lockedInput } : {}),
412
- ...(options.resume ? { resume: true } : {}),
413
- });
414
- const checkout = response.checkout;
415
- const checkoutID = checkout.checkout.checkout_id;
416
- const displayToken = checkout.display_token;
417
- const locale = normalizeCardLocale(options.locale);
418
- const checkoutURL = tokenizedCheckoutURL(checkout.checkout_url, displayToken, checkout.qr_payload);
419
- const cardURL = localizeCardURL(absolutePublicURL(config.baseURL, checkout.card_url ?? fallbackCardURL(config.baseURL, checkoutID, displayToken)), locale);
420
- const cardPNGURL = localizeCardURL(absolutePublicURL(config.baseURL, checkout.card_png_url ?? checkout.qr_png_url ?? fallbackCardPNGURL(config.baseURL, checkoutID, displayToken)), locale);
421
- const plan = buildCheckoutQRPlan({
422
- host,
423
- checkoutID,
424
- checkoutURL,
425
- cardURL,
426
- displayToken,
427
- qrPayload: checkout.qr_payload,
428
- qrPNGURL: cardPNGURL,
429
- nextAction: checkout.checkout.next_action,
430
- orderItems: response.cart.items.map((item) => ({
431
- title: item.title,
432
- quantity: item.quantity,
433
- amountMinor: item.amount_minor,
434
- currency: item.currency,
435
- })),
436
- orderCurrency: checkout.checkout.currency,
437
- ...(options.agentType ? { agentType: options.agentType } : {}),
438
- locale,
439
- });
440
- options.persistHandoff?.({
441
- serviceExecutionID,
442
- cartID: response.cart.cart_id,
443
- checkoutID,
444
- displayToken,
445
- checkoutURL,
446
- });
447
- const platform = platformKeyForHost(plan.host);
448
- if (!options.jsonOutput && (platform === "telegram" || platform === "feishu" || platform === "lark")) {
449
- await dispatchRender(plan, {
450
- host,
451
- ...(options.target ? { target: options.target } : {}),
452
- ...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
453
- ...(options.qrFilePath ? { qrFilePath: options.qrFilePath } : {}),
454
- ...(options.output ? { output: options.output } : {}),
455
- ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
456
- baseURL: config.baseURL,
457
- });
458
- return;
459
- }
460
- if (shouldPrepareLocalCheckoutImage(platform)) {
461
- await ensureIdeImageAttach(plan, {
462
- enabled: config.ideImageAttach,
463
- ...(config.baseURL ? { baseURL: config.baseURL } : {}),
464
- ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
465
- });
466
- }
467
- const envelope = buildServicesCheckoutEnvelope(response, checkoutURL, plan, options.agentType, options.target);
468
- const plainResult = [
469
- `service_execution_id: ${response.binding.service_execution_id}`,
470
- `checkout_id: ${checkoutID}`,
471
- `capability_id: ${checkoutCapabilityID(response, capabilityID)}`,
472
- `locked_input: ${JSON.stringify(response.locked_input)}`,
473
- `amount: ${formatMoney(checkout.checkout.amount_minor, checkout.checkout.currency)}`,
474
- ];
475
- if (!options.jsonOutput && platform === "terminal") {
476
- plainResult.push("qr:", await renderTerminalQR(checkoutURL, options.qrFormat ?? "terminal"));
477
- }
478
- writeCommandEnvelope(envelope, {
479
- ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
480
- ...(options.output ? { output: options.output } : {}),
481
- plainResult,
482
- });
483
- }
484
- export async function runServicesQuote(backend, serviceExecutionID, capabilityID, input, options = {}) {
485
- const model = await backend.getServiceExecution(serviceExecutionID);
486
- const capability = model.capabilities.find((item) => item.capability_id === capabilityID);
487
- if (!capability || !capability.requires_payment) {
488
- throw new CommandContractError("capability_not_quoteable", `capability ${capabilityID} is not available for quote on service execution ${serviceExecutionID}`, "只为当前 Service Execution 返回的付费 capability 创建报价。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前合法动作" }]);
489
- }
490
- const selectionBacked = model.execution.status === "human_action_approved" &&
491
- model.allowed_actions?.some((action) => action.type === "prepare_quote" && action.capability_id === capabilityID);
492
- const missingInput = missingRequiredInput(capability.input_schema, input);
493
- if (missingInput.length > 0 && !selectionBacked) {
494
- throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐付费 capability 输入;本次没有创建 Quote、Cart 或 Checkout。", [{ command: quoteCommand(serviceExecutionID, capability, input), reason: "提交完整且会被锁定的输入" }]);
495
- }
496
- const deliveryContact = {
497
- ...(options.deliveryContact ?? {}),
498
- ...(options.email ? { email: options.email } : {}),
499
- };
500
- if (capability.delivery_email_required && String(deliveryContact.email ?? "").trim() === "") {
501
- throw new CommandContractError("delivery_email_required", "delivery email is required before preparing this service quote", "交付链接会发送到用户邮箱;说明用途并询问邮箱,不要代填。", [{ command: quoteCommand(serviceExecutionID, capability, input), reason: "使用用户提供的邮箱创建报价" }]);
502
- }
503
- const quote = await backend.prepareServiceQuote(serviceExecutionID, {
504
- capability_id: capabilityID,
505
- ...(Object.keys(deliveryContact).length > 0 ? { delivery_contact: deliveryContact } : {}),
506
- ...(Object.keys(input).length > 0 ? { locked_input: input } : {}),
507
- });
508
- const result = {
509
- service_quote_lock_id: quote.service_quote_lock_id,
510
- service_execution_id: quote.service_execution_id,
511
- capability_id: quote.capability_id,
512
- price: formatMoney(quote.amount_minor, quote.currency),
513
- expires_at: quote.expires_at,
514
- };
515
- writeCommandEnvelope({
516
- status: "quote_ready",
517
- result,
518
- instruction: "报价已锁定当前 Execution 的可信输入和价格;可单独付款,也可与其他独立 Execution 的报价合并。",
519
- next: {
520
- command: `itpay cart add --quote ${quote.service_quote_lock_id} --json`,
521
- reason: "加入 canonical Cart",
522
- },
523
- recovery: [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "重新读取当前 Execution 状态" }],
524
- }, {
525
- ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
526
- ...(options.output ? { output: options.output } : {}),
527
- plainResult: Object.entries(result).map(([key, value]) => `${key}: ${String(value)}`),
528
- });
529
- }
530
- export async function runServicesGet(backend, serviceExecutionID, options = {}) {
531
- const response = await backend.getServiceExecution(serviceExecutionID);
532
- const execution = response.execution;
533
- const timeline = response.events.slice(-20).map((event) => ({
534
- sequence: event.sequence,
535
- step: event.type,
536
- status: event.status,
537
- phase: event.phase,
538
- ...(event.capability_id ? { capability_id: event.capability_id } : {}),
539
- occurred_at: event.occurred_at,
540
- }));
541
- const deliveryMode = serviceDeliveryMode(response);
542
- const lockedRefund = response.refunds.find((refund) => refund.access_locked);
543
- const nextState = servicesNextEnvelope(response);
544
- const result = {
545
- service_execution_id: execution.service_execution_id,
546
- service_id: execution.service_id,
547
- status: execution.status,
548
- phase: execution.phase,
549
- ...(execution.current_capability_id ? { current_capability_id: execution.current_capability_id } : {}),
550
- updated_at: execution.updated_at,
551
- timeline,
552
- ...(response.events.length > timeline.length ? { timeline_truncated: true } : {}),
553
- ...(deliveryMode ? { delivery_mode: deliveryMode } : {}),
554
- ...(lockedRefund ? {
555
- access_locked: true,
556
- refund: { refund_request_id: lockedRefund.refund_request_id, status: lockedRefund.status },
557
- } : {}),
558
- };
559
- const envelope = {
560
- status: "shown",
561
- result,
562
- instruction: lockedRefund || isTerminalServiceExecutionStatus(execution.status)
563
- ? nextState.instruction
564
- : "时间线仅用于解释和恢复;按当前首选动作继续,不要重放已完成步骤。",
565
- next: nextState.next ? { command: nextState.next.command, reason: "继续当前首选动作" } : null,
566
- recovery: [{ command: `itpay services events ${serviceExecutionID} --json`, reason: "仅在需要完整诊断事件时使用" }],
567
- };
568
- writeCommandEnvelope(envelope, {
569
- ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
570
- ...(options.output ? { output: options.output } : {}),
571
- plainResult: [
572
- `service_execution_id: ${execution.service_execution_id}`,
573
- `service_id: ${execution.service_id}`,
574
- `state: ${execution.status}/${execution.phase}`,
575
- ...(execution.current_capability_id ? [`current_capability_id: ${execution.current_capability_id}`] : []),
576
- ...timeline.map((event) => `${event.sequence}. ${event.step} ${event.status}/${event.phase} ${event.occurred_at}`),
577
- ],
578
- });
579
- }
580
- export async function runServicesNext(backend, serviceExecutionID, options = {}) {
581
- const response = await backend.getServiceExecution(serviceExecutionID);
582
- const envelope = servicesNextEnvelope(response);
583
- writeCommandEnvelope(envelope, {
584
- ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
585
- ...(options.output ? { output: options.output } : {}),
586
- plainResult: servicesNextPlainResult(envelope.result),
587
- });
588
- }
589
- export async function runServicesList(backend, options = {}) {
590
- const limit = options.limit ?? 10;
591
- if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
592
- throw new CommandContractError("limit_invalid", "--limit must be an integer from 1 to 100", "使用 1 到 100 的整数 limit;本次未读取服务端列表。", [{ command: "itpay services list --limit 10 --json", reason: "使用默认上限重试" }]);
593
- }
594
- const response = await backend.listServiceExecutions(limit);
595
- const executions = response.executions.map(({ execution }) => ({
596
- service_execution_id: execution.service_execution_id,
597
- service_id: execution.service_id,
598
- status: execution.status,
599
- phase: execution.phase,
600
- updated_at: execution.updated_at,
601
- }));
602
- const latest = executions[0];
603
- const envelope = {
604
- status: latest ? "listed" : "no_executions",
605
- result: { executions },
606
- instruction: executions.length === 1
607
- ? "只有一条可恢复记录;继续读取同一笔服务。"
608
- : latest
609
- ? "用服务和状态说明这些可恢复记录;多个结果必须让用户选择。"
610
- : "当前设备没有可恢复的 Service Execution;先读取已发布目录,不要猜测 ID。",
611
- next: executions.length === 1
612
- ? { command: `itpay services next ${latest.service_execution_id} --json`, reason: "继续唯一可恢复的服务" }
613
- : latest
614
- ? null
615
- : { command: "itpay catalog list --json", reason: "选择已发布服务" },
616
- recovery: [],
617
- };
618
- writeCommandEnvelope(envelope, {
619
- ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
620
- ...(options.output ? { output: options.output } : {}),
621
- plainResult: executions.map((execution) => `${execution.service_execution_id}: ${execution.service_id} ${execution.status}/${execution.phase} updated=${execution.updated_at}`),
622
- });
623
- }
624
- export async function runServicesReadResult(backend, serviceExecutionID, options = {}) {
625
- const response = await backend.getGrantedServiceResult(serviceExecutionID);
626
- let orderID;
627
- try {
628
- const model = await backend.getServiceExecution(serviceExecutionID);
629
- orderID = (model.current_delivery ?? model.delivery_bindings.at(-1))?.order_id;
630
- }
631
- catch {
632
- // Feedback context is optional and must never block an authorized result.
633
- }
634
- const envelope = grantedResultEnvelope(response, orderID);
635
- writeCommandEnvelope(envelope, {
636
- ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
637
- ...(options.output ? { output: options.output } : {}),
638
- plainResult: servicesNextPlainResult(envelope.result),
639
- });
640
- }
641
- function servicesNextEnvelope(model) {
642
- const execution = model.execution;
643
- const currentDelivery = model.current_delivery ?? model.delivery_bindings.at(-1);
644
- const lockedRefund = model.refunds.find((refund) => refund.access_locked);
645
- if (lockedRefund) {
646
- const terminal = lockedRefund.status === "succeeded";
647
- return {
648
- status: "delivery_locked",
649
- result: {
650
- service_execution_id: execution.service_execution_id,
651
- access_locked: true,
652
- refund: {
653
- refund_request_id: lockedRefund.refund_request_id,
654
- status: lockedRefund.status,
655
- },
656
- },
657
- instruction: terminal
658
- ? "告诉用户退款已由 ItPay 确认成功,原交付永久关闭。Agent 停止读取和跟踪,不再创建授权。"
659
- : "告诉用户退款仍在处理,原交付已按政策冻结。然后读取同一退款的权威状态;Agent 不读取交付、不创建授权或重复申请。",
660
- next: terminal ? null : {
661
- command: `itpay refund get ${lockedRefund.refund_request_id} --json`,
662
- reason: "读取退款权威状态",
663
- },
664
- recovery: [],
665
- };
666
- }
667
- if (isTerminalServiceExecutionStatus(execution.status)) {
668
- const paid = model.checkout_bindings.some((binding) => binding.status === "payment_verified") || Boolean(currentDelivery?.order_id);
669
- const paidFailure = execution.status === "failed" && paid;
670
- return {
671
- status: execution.status,
672
- result: {
673
- service_execution_id: execution.service_execution_id,
674
- service_id: execution.service_id,
675
- phase: execution.phase,
676
- ...(currentDelivery?.order_id ? { order_id: currentDelivery.order_id } : {}),
677
- },
678
- instruction: execution.status === "refunded"
679
- ? "告诉用户这笔服务已经退款并永久结束。Agent 不重放服务步骤、不创建付款页面或尝试读取旧交付。"
680
- : paidFailure
681
- ? appendFeedbackPostmortemInstruction("告诉用户:付款和订单已经记录,但本次服务没有正常完成,不需要再次付款或重新下单。然后从同一订单检查退款状态;Agent 不重放服务步骤、创建付款页面或再次调用数据来源,也不把技术故障归咎于用户。", "failed")
682
- : "告诉用户本次服务已经结束且没有可继续的交付。Agent 不重放服务步骤或创建付款页面。",
683
- next: null,
684
- recovery: [
685
- ...(paidFailure
686
- ? [{
687
- command: currentDelivery?.order_id
688
- ? `itpay order ${currentDelivery.order_id} --json`
689
- : "itpay orders --json",
690
- reason: "恢复同一笔已付款订单及其退款状态",
691
- }]
692
- : []),
693
- {
694
- command: `itpay services events ${execution.service_execution_id} --json`,
695
- reason: "仅在需要诊断终止原因时读取事件",
696
- },
697
- ],
698
- };
699
- }
700
- const currentItems = model.current_result_items ?? [];
701
- const delivery = currentDelivery;
702
- const deliveryMode = serviceDeliveryMode(model);
703
- const candidateSelection = model.allowed_actions?.find((action) => action.type === "select_candidate");
704
- if (candidateSelection && currentItems.length > 0) {
705
- const paidCapability = delivery?.capability_id
706
- ? model.capabilities.find((capability) => capability.capability_id === delivery.capability_id && capability.requires_payment)
707
- : undefined;
708
- return {
709
- status: "candidate_selection_available",
710
- result: {
711
- service_execution_id: execution.service_execution_id,
712
- ...(delivery?.capability_id ? { capability_id: delivery.capability_id } : {}),
713
- ...(deliveryMode ? { delivery_mode: deliveryMode } : {}),
714
- items: currentItems.map((item) => ({
715
- rank: item.rank,
716
- title: item.display_title,
717
- safe_payload: item.safe_payload,
718
- })),
719
- },
720
- instruction: paidCapability
721
- ? "付费搜索已完成。用编号、名称和可公开字段向用户说明结果,然后停止。只有用户明确选择候选并要求继续时才执行 next.command;不要提及 safe_payload 或自动购买后续报告。"
722
- : "用编号、名称和可公开字段向用户说明候选;若候选列表已满足目标就停止。只有用户明确选择并希望继续时才提交对应编号;不要提及 safe_payload、Execution 或内部 ID。",
723
- next: {
724
- command: `itpay services action ${execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
725
- reason: paidCapability ? "仅在用户明确选择候选并要求继续时执行" : "仅在用户明确选择后锁定来源候选",
726
- },
727
- recovery: [],
728
- };
729
- }
730
- if (deliveryMode === "agent_visible_result") {
731
- const items = currentItems.map((item) => ({
732
- rank: item.rank,
733
- title: item.display_title,
734
- safe_payload: item.safe_payload,
735
- }));
736
- const selection = model.allowed_actions?.find((action) => action.type === "select_candidate");
737
- return {
738
- status: items.length > 0 ? "result_ready" : "no_result",
739
- result: {
740
- service_execution_id: execution.service_execution_id,
741
- ...(delivery?.capability_id ? { capability_id: delivery.capability_id } : {}),
742
- ...(delivery?.order_id ? { order_id: delivery.order_id } : {}),
743
- delivery_mode: deliveryMode,
744
- items,
745
- },
746
- instruction: delivery?.order_id
747
- ? appendFeedbackPostmortemInstruction(items.length > 0
748
- ? selection
749
- ? "搜索已完成。用编号、名称和可公开字段向用户说明结果,然后停止。只有用户明确选择候选并要求继续时才执行 next.command;不要提及 safe_payload。"
750
- : "这一步的结果已经可用。用普通语言解释可公开字段并停止;不要提及 Graph、safe_payload 或内部 ID。"
751
- : "告诉用户本次查询得到 0 个结果并停止。Agent 不读取其他交付、不重放当前查询、修改输入或创建新查询。", "delivered")
752
- : items.length > 0
753
- ? selection
754
- ? "搜索已完成。用编号、名称和可公开字段向用户说明结果,然后停止。只有用户明确选择候选并要求继续时才执行 next.command;不要提及 safe_payload。"
755
- : "这一步的结果已经可用。用普通语言解释可公开字段并停止;不要提及 Graph、safe_payload 或内部 ID。"
756
- : "告诉用户本次查询得到 0 个结果并停止。Agent 不读取其他交付、不重放当前查询、修改输入或创建新查询。",
757
- next: selection ? {
758
- command: `itpay services action ${execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
759
- reason: "仅在用户明确选择后锁定来源候选",
760
- } : null,
761
- recovery: [],
762
- };
763
- }
764
- if (deliveryMode === "vault_artifact") {
765
- const grantStatus = normalizeGrantStatus(delivery?.grant_status);
766
- const grantActive = grantStatus === "active";
767
- const grantPending = grantStatus === "pending";
768
- return {
769
- status: grantActive ? "grant_active" : grantPending ? "result_preparing" : "human_authorization_required",
770
- result: {
771
- service_execution_id: execution.service_execution_id,
772
- ...(delivery?.capability_id ? { capability_id: delivery.capability_id } : {}),
773
- delivery_mode: deliveryMode,
774
- grant_status: grantStatus,
775
- ...(delivery?.preparation ? { preparation: delivery.preparation } : {}),
776
- ...(grantActive && delivery?.grant_expires_at ? { grant_expires_at: delivery.grant_expires_at } : {}),
777
- },
778
- instruction: grantActive
779
- ? "先告诉用户付费内容已经准备好且当前读取授权有效;立即读取并只解释授权字段,遵守范围与到期时间。"
780
- : grantPending
781
- ? "告诉用户:授权已经完成,付费结果仍在同一订单下准备,不需要再次付款或授权。然后只执行 next.command 查询同一笔服务;Agent 不创建新服务、付款页面或数据请求,也不提前读取。"
782
- : "先告诉用户付费内容已经归入当前订单,但需要本人确认一次读取授权;请用户在订单页面授权,未授权前不要读取或猜测内容。",
783
- next: grantPending ? {
784
- command: `itpay services next ${execution.service_execution_id} --json`,
785
- reason: "等待同一 Execution 的交付准备完成",
786
- } : {
787
- command: `itpay services read-result ${execution.service_execution_id} --json`,
788
- reason: grantActive ? "读取当前有效 grant 的结果" : "仅在用户确认授权后执行",
789
- },
790
- recovery: [],
791
- };
792
- }
793
- const allowedActions = model.allowed_actions ?? [];
794
- const preferred = allowedActions[0];
795
- if (preferred?.type === "prepare_quote") {
796
- const continuation = paidContinuation(model, preferred, {});
797
- if (continuation) {
798
- return {
799
- status: execution.status,
800
- result: {
801
- service_execution_id: execution.service_execution_id,
802
- service_id: execution.service_id,
803
- phase: execution.phase,
804
- checkout: continuation.checkout,
805
- },
806
- instruction: purchaseConfirmationInstruction(execution.status === "quota_exhausted" ? "quota_exhausted" : "candidate_selected", continuation.price, continuation.capability.delivery_email_required, continuation.capability.delivery_email_purpose),
807
- next: continuation.next,
808
- recovery: [],
809
- };
810
- }
811
- }
812
- const next = preferred ? serviceAllowedActionCommand(model, preferred) : null;
813
- return {
814
- status: execution.status,
815
- result: {
816
- service_execution_id: execution.service_execution_id,
817
- service_id: execution.service_id,
818
- phase: execution.phase,
819
- allowed_actions: allowedActions.map((action) => ({
820
- type: action.type,
821
- ...(action.capability_id ? { capability_id: action.capability_id } : {}),
822
- requires_human: action.requires_human,
823
- })),
824
- },
825
- instruction: preferred?.type === "resume_checkout"
826
- ? "这笔服务已经有付款页面。只执行 next.command 恢复并展示同一个入口;Agent 不创建新的报价、购物车、付款页面或服务。"
827
- : preferred?.type === "wait"
828
- ? "告诉用户付款和订单已经确认,结果仍在同一笔服务中处理,不需要再次付款;如果最终无法交付,将从原订单检查退款路径。稍后只执行 next.command;Agent 不创建新服务、付款页面或数据请求,也不承诺退款结果。"
829
- : preferred?.requires_human
830
- ? "当前下一步需要用户明确选择;先展示必要信息并等待确认。"
831
- : preferred ? "执行服务端返回的唯一首选动作;不要猜测其他 capability。" : "当前没有后续动作。",
832
- next,
833
- recovery: [{ command: `itpay services get ${execution.service_execution_id} --json`, reason: "仅在当前动作异常时检查时间线" }],
834
- };
835
- }
836
- function serviceAllowedActionCommand(model, action) {
837
- const executionID = model.execution.service_execution_id;
838
- const capability = action.capability_id
839
- ? model.capabilities.find((item) => item.capability_id === action.capability_id)
840
- : undefined;
841
- switch (action.type) {
842
- case "invoke_capability": {
843
- if (!capability)
844
- return null;
845
- const input = Object.fromEntries(requiredInputFields(capability.input_schema).map((field) => [field, "<value>"]));
846
- return {
847
- command: `itpay services invoke ${executionID} --capability ${capability.capability_id}${formatInputOptions(input)} --json`,
848
- reason: "执行当前允许的 Agent-visible capability",
849
- };
850
- }
851
- case "select_candidate":
852
- return {
853
- command: `itpay services action ${executionID} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
854
- reason: "仅在用户明确选择后提交当前候选 rank",
855
- };
856
- case "prepare_quote": {
857
- return paidContinuation(model, action, {})?.next ?? null;
858
- }
859
- case "resume_checkout":
860
- return { command: `itpay services checkout ${executionID} --resume --json`, reason: "恢复同一 Checkout,不创建第二笔" };
861
- case "wait":
862
- return { command: `itpay services next ${executionID} --json`, reason: "等待 durable execution 推进" };
863
- case "view_delivery":
864
- return { command: `itpay services next ${executionID} --json`, reason: "读取当前交付模式" };
865
- default:
866
- return null;
867
- }
868
- }
869
- function serviceDeliveryMode(model) {
870
- const delivery = model.current_delivery ?? model.delivery_bindings.at(-1);
871
- const explicit = String(delivery?.redacted_summary?.delivery_mode ?? "");
872
- if (explicit)
873
- return explicit;
874
- return delivery?.vault_artifact_id ? "vault_artifact" : "";
875
- }
876
- function normalizeGrantStatus(status) {
877
- return !status || status === "missing" ? "none" : status;
878
- }
879
- function grantedResultEnvelope(response, orderID) {
880
- return {
881
- status: "granted_result_ready",
882
- result: {
883
- service_execution_id: response.service_execution_id,
884
- ...(orderID ? { order_id: orderID } : {}),
885
- ...(response.expires_at ? { grant_expires_at: response.expires_at } : {}),
886
- granted_fields: Object.keys(response.result),
887
- payload: response.result,
888
- },
889
- instruction: orderID
890
- ? appendFeedbackPostmortemInstruction("结果来自当前有效 Vault Grant;只使用本次授权字段,过期后停止读取并重新请求用户同意。", "delivered")
891
- : "结果来自当前有效 Vault Grant;只使用本次授权字段,过期后停止读取并重新请求用户同意。",
892
- next: null,
893
- recovery: [],
894
- };
895
- }
896
- function servicesNextPlainResult(result) {
897
- const lines = [];
898
- for (const [key, value] of Object.entries(result)) {
899
- if (key === "items" && Array.isArray(value)) {
900
- lines.push("items:");
901
- for (const item of value) {
902
- lines.push(` ${item.rank}. ${item.title}`);
903
- for (const [field, fieldValue] of Object.entries(item.safe_payload)) {
904
- lines.push(` ${field}: ${typeof fieldValue === "string" ? fieldValue : JSON.stringify(fieldValue)}`);
905
- }
906
- }
907
- continue;
908
- }
909
- lines.push(`${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
910
- }
911
- return lines;
912
- }
913
- export async function runServicesEvents(backend, serviceExecutionID, options = {}) {
914
- const afterSequence = options.afterSequence ?? 0;
915
- const limit = options.limit ?? 50;
916
- if (!serviceExecutionID.trim()) {
917
- throw new CommandContractError("service_execution_id_required", "service execution id is required", "使用 services list 返回的 execution ID;不要猜测。", [{ command: "itpay services list --json", reason: "列出当前身份可见执行" }]);
918
- }
919
- if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) {
920
- throw new CommandContractError("events_parameter_invalid", "after_sequence must be a non-negative integer", "--after-sequence 必须是非负整数;本次未读取事件。", [{ command: `itpay services events ${serviceExecutionID} --help`, reason: "查看诊断参数" }]);
921
- }
922
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
923
- throw new CommandContractError("events_parameter_invalid", "limit must be an integer between 1 and 100", "--limit 必须是 1 到 100 的整数;本次未读取事件。", [{ command: `itpay services events ${serviceExecutionID} --help`, reason: "查看诊断参数" }]);
924
- }
925
- const response = await backend.listServiceExecutionEvents(serviceExecutionID, afterSequence, limit);
926
- const events = response.events.map((event) => ({
927
- sequence: event.sequence,
928
- type: event.type,
929
- status: event.status,
930
- phase: event.phase,
931
- ...(event.capability_id ? { capability_id: event.capability_id } : {}),
932
- occurred_at: event.occurred_at,
933
- }));
934
- writeCommandEnvelope({
935
- status: "listed",
936
- result: {
937
- service_execution_id: serviceExecutionID,
938
- after_sequence: afterSequence,
939
- returned_count: events.length,
940
- events,
941
- },
942
- instruction: "事件仅用于诊断;不要从事件重放业务步骤,回到 services next 获取当前动作。",
943
- next: {
944
- command: `itpay services next ${serviceExecutionID} --json`,
945
- reason: "恢复正常服务流程",
946
- },
947
- recovery: events.length === limit && events.length > 0
948
- ? [{
949
- command: `itpay services events ${serviceExecutionID} --after-sequence ${events.at(-1).sequence} --limit ${limit} --json`,
950
- reason: "继续读取下一页诊断事件",
951
- }]
952
- : [],
953
- }, {
954
- ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
955
- ...(options.output ? { output: options.output } : {}),
956
- plainResult: [
957
- `service_execution_id: ${serviceExecutionID}`,
958
- `returned_count: ${events.length}`,
959
- ...events.map((event) => `${event.sequence} ${event.occurred_at} ${event.type} ${event.status}/${event.phase}`),
960
- ],
961
- });
962
- }
963
- export function parseKeyValueList(values) {
964
- const result = {};
965
- for (const value of values ?? []) {
966
- const index = value.indexOf("=");
967
- if (index <= 0) {
968
- throw new Error(`invalid --input "${value}", expected key=value`);
969
- }
970
- result[value.slice(0, index)] = parseValue(value.slice(index + 1));
971
- }
972
- return result;
973
- }
974
- export function collectOption(value, previous = []) {
975
- previous.push(value);
976
- return previous;
977
- }
978
- function parseValue(value) {
979
- if (value === "true")
980
- return true;
981
- if (value === "false")
982
- return false;
983
- if (/^-?\d+(\.\d+)?$/.test(value))
984
- return Number(value);
985
- return value;
986
- }
987
- function buildServicesCheckoutEnvelope(response, checkoutURL, plan, agentType, target) {
988
- const checkout = response.checkout;
989
- const platform = platformKeyForHost(plan.host);
990
- const amount = formatMoney(checkout.checkout.amount_minor, checkout.checkout.currency);
991
- const presentationHandoff = buildCheckoutHandoff({
992
- platform,
993
- url: plan.linkOnlyURL ?? checkoutURL,
994
- amount,
995
- plan,
996
- ...(agentType ? { agentType } : {}),
997
- ...(target ? { target } : {}),
998
- ...(plan.preferredQRSources[0] ? { qrImageURL: plan.preferredQRSources[0] } : {}),
999
- ...(plan.ideImageAttach?.status === "downloaded" && plan.ideImageAttach.localPath
1000
- ? { localPath: plan.ideImageAttach.localPath }
1001
- : {}),
1002
- ...(platform === "markdown" ? { markdown: buildAgentChatHandoff(plan).markdown } : {}),
1003
- });
1004
- return {
1005
- status: "human_checkout_required",
1006
- result: {
1007
- service_execution_id: response.binding.service_execution_id,
1008
- checkout_id: checkout.checkout.checkout_id,
1009
- capability_id: checkoutCapabilityID(response),
1010
- locked_input: response.locked_input,
1011
- amount,
1012
- },
1013
- handoff: presentationHandoff.handoff,
1014
- instruction: presentationHandoff.instruction,
1015
- next: {
1016
- command: plan.afterActionCommand ?? `itpay checkout --id ${checkout.checkout.checkout_id} --token ${checkout.display_token} --json`,
1017
- reason: "仅在用户完成付款操作或要求查询后,读取同一 Checkout 的权威状态",
1018
- },
1019
- recovery: [],
1020
- };
1021
- }
1022
- function fallbackCardURL(baseURL, checkoutID, displayToken) {
1023
- const root = baseURL.replace(/\/$/, "");
1024
- return `${root}/v1/checkouts/${encodeURIComponent(checkoutID)}/card?display_token=${encodeURIComponent(displayToken)}`;
1025
- }
1026
- function fallbackCardPNGURL(baseURL, checkoutID, displayToken) {
1027
- return `${fallbackCardURL(baseURL, checkoutID, displayToken)}.png`.replace("/card?", "/card.png?");
1028
- }
1029
- function checkoutCapabilityID(response, fallback = "") {
1030
- return response.capability_id || fallback;
1031
- }
1032
- function absolutePublicURL(baseURL, value) {
1033
- try {
1034
- return new URL(value, baseURL.endsWith("/") ? baseURL : `${baseURL}/`).toString();
1035
- }
1036
- catch {
1037
- return value;
1038
- }
1039
- }
1040
- function formatMoney(amountMinor, currency) {
1041
- return `${(amountMinor / 100).toFixed(2)} ${currency}`;
1042
- }
1043
- function normalizeServiceActionStatus(status, serviceExecutionID) {
1044
- const normalized = status.trim().toLowerCase();
1045
- if (!serviceActionStatuses.has(normalized)) {
1046
- throw actionInputError(serviceExecutionID, `invalid --status "${status}". Supported: pending, approved, rejected, expired, cancelled`);
1047
- }
1048
- return normalized;
1049
- }
1050
- function tokenizedCheckoutURL(checkoutURL, displayToken, qrPayload) {
1051
- if (qrPayload.trim().length > 0) {
1052
- return qrPayload;
1053
- }
1054
- if (checkoutURL.trim().length === 0 || displayToken.trim().length === 0) {
1055
- return checkoutURL;
1056
- }
1057
- try {
1058
- const parsed = new URL(checkoutURL);
1059
- if (!parsed.searchParams.has("display_token")) {
1060
- parsed.searchParams.set("display_token", displayToken);
1061
- }
1062
- return parsed.toString();
1063
- }
1064
- catch {
1065
- const separator = checkoutURL.includes("?") ? "&" : "?";
1066
- return `${checkoutURL}${separator}display_token=${encodeURIComponent(displayToken)}`;
1067
- }
1068
- }
1
+ export { runServicesStart } from "./services/lifecycle.js";
2
+ export { runServicesInvoke } from "./services/lifecycle.js";
3
+ export { runServicesAction } from "./services/actions.js";
4
+ export { runServicesCheckout } from "./services/commerce.js";
5
+ export { runServicesQuote } from "./services/commerce.js";
6
+ export { runServicesGet } from "./services/lifecycle.js";
7
+ export { runServicesNext } from "./services/lifecycle.js";
8
+ export { runServicesPage } from "./services/results.js";
9
+ export { runServicesList } from "./services/results.js";
10
+ export { runServicesReadResult } from "./services/results.js";
11
+ export { runServicesEvents } from "./services/results.js";
12
+ export { parseKeyValueList } from "../services/shared/platform.js";
13
+ export { collectOption } from "../services/shared/platform.js";
14
+ export { runServicesRun } from "./services/lifecycle.js";