@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
@@ -0,0 +1,418 @@
1
+ import { serviceAdapter, serviceClientFeatures } from "../../services/registry.js";
2
+ import { HttpError } from "../../client/http.js";
3
+ import { HttpTransportError } from "../../client/transport.js";
4
+ import { DeviceLockBusyError, DeviceStateError } from "../../state/device_authority.js";
5
+ import { operationID } from "../../state/config.js";
6
+ import { CommandContractError, isTerminalServiceExecutionStatus, writeCommandEnvelope, plainValueLines } from "../../services/shared/envelope.js";
7
+ import { requiredInputFields, missingRequiredInput, stableInput, formatInputOptions, queryText, capabilityPrice, purchaseConfirmationInstruction, checkoutCommand, serviceResultPlainLines, serviceDeliveryMode, journalTaskState, servicesNextPlainResult } from "../../services/shared/platform.js";
8
+ import { servicesNextEnvelope } from "../../services/presentation.js";
9
+ import { executionInputContext, operationInputTemplate } from "../../services/shared/input.js";
10
+ import { runServicesCheckout } from "./commerce.js";
11
+ export async function runServicesStart(backend, serviceID, options = {}) {
12
+ const host = options.host ?? "terminal";
13
+ const response = await backend.startServiceExecution({
14
+ service_id: serviceID,
15
+ client_context: {
16
+ host,
17
+ features: serviceClientFeatures(),
18
+ ...(options.target ? { target: options.target } : {}),
19
+ ...(options.clientContext ?? {}),
20
+ },
21
+ });
22
+ if ("overview_only" in response) {
23
+ writeServiceOverview(response.service_definition, options);
24
+ return;
25
+ }
26
+ if (!serviceAdapter(response.execution).supported) {
27
+ writeCommandEnvelope({ status: "presentation_contract_unavailable", result: { service_execution_id: response.execution.service_execution_id, service_id: response.execution.service_id, service_contract_version_id: response.execution.service_contract_version_id }, instruction: "当前锁定合同不受支持;保留原执行与已有结果并停止,使用兼容CLI,不猜模式或输入。", next: null, recovery: [] }, options);
28
+ return;
29
+ }
30
+ if (response.workflow_entry) {
31
+ const guidance = serviceAdapter(response.execution).guidance(response.execution.service_id, response.workflow_entry.input_schema);
32
+ if (serviceAdapter(response.execution).requiresGuidance(serviceID) && !guidance) {
33
+ writeCommandEnvelope({ status: "contract_unavailable", result: { service_execution_id: response.execution.service_execution_id, service_id: serviceID }, instruction: "当前执行缺少可用输入合同;只读取一次同执行合同,仍缺失则报告系统缺口并停止,不猜缺失字段或旧输入写法。", next: { command: `itpay services get ${response.execution.service_execution_id} --json`, reason: "读取当前执行锁定的输入合同一次" }, recovery: [] }, options);
34
+ return;
35
+ }
36
+ writeCommandEnvelope({
37
+ status: "input_required",
38
+ result: { service_execution_id: response.execution.service_execution_id, service_id: serviceID, input_schema: response.workflow_entry.input_schema, ...(guidance ? { guidance } : {}) },
39
+ instruction: serviceAdapter(response.execution).entryInstruction,
40
+ next: null,
41
+ interaction: {
42
+ schema_version: "itpay.interaction.v1",
43
+ stage: "input_required",
44
+ input_template: operationInputTemplate(executionInputContext(response.execution), guidance?.input_example ?? serviceAdapter(response.execution).inputTemplate(response.workflow_entry.input_schema), requiredInputFields(response.workflow_entry.input_schema)),
45
+ },
46
+ recovery: []
47
+ }, { ...options });
48
+ return;
49
+ }
50
+ const capability = response.capabilities.find((item) => item.phase === response.execution.phase && !item.requires_payment);
51
+ const requiredInput = requiredInputFields(capability?.input_schema);
52
+ const needsInput = requiredInput.length > 0;
53
+ const command = capability && !needsInput
54
+ ? `itpay services invoke ${response.execution.service_execution_id} --capability ${capability.capability_id} --json`
55
+ : `itpay services next ${response.execution.service_execution_id} --json`;
56
+ const capabilitySummary = capability ? {
57
+ capability_id: capability.capability_id,
58
+ required_input: requiredInput,
59
+ input_schema: capability.input_schema,
60
+ ...(capability.free_quota_limit !== undefined ? { free_quota_limit: capability.free_quota_limit } : {}),
61
+ } : null;
62
+ writeCommandEnvelope({
63
+ status: "ready",
64
+ result: {
65
+ service_execution_id: response.execution.service_execution_id,
66
+ service_id: response.execution.service_id,
67
+ phase: response.execution.phase,
68
+ capability: capabilitySummary,
69
+ },
70
+ instruction: capability
71
+ ? "填写首选 capability 的 required_input;一次只提交当前 execution 所代表的服务意图。" + serviceAdapter(response.execution).locationInstruction(capability.input_schema)
72
+ : "当前没有可直接调用的 capability;读取服务端下一步,不要猜测 capability。",
73
+ next: {
74
+ command,
75
+ reason: capability && !needsInput ? "执行当前允许的能力" : "读取服务端计算的下一步",
76
+ },
77
+ ...(capability && needsInput ? {
78
+ interaction: {
79
+ schema_version: "itpay.interaction.v1",
80
+ stage: "input_required",
81
+ input_template: {
82
+ command: `itpay services invoke ${response.execution.service_execution_id} --capability ${capability.capability_id}${requiredInput.map((field) => ` --input ${field}=<value>`).join("")} --json`,
83
+ required_input: requiredInput,
84
+ executable: false,
85
+ },
86
+ },
87
+ } : {}),
88
+ recovery: [],
89
+ }, {
90
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
91
+ ...(options.output ? { output: options.output } : {}),
92
+ plainResult: [
93
+ `service_execution_id: ${response.execution.service_execution_id}`,
94
+ `service_id: ${response.execution.service_id}`,
95
+ `phase: ${response.execution.phase}`,
96
+ ...(capability ? [
97
+ `capability: ${capability.capability_id}`,
98
+ `required_input: ${requiredInput.length > 0 ? requiredInput.join(",") : "none"}`,
99
+ ...(capability.free_quota_limit !== undefined ? [`free_quota_limit: ${capability.free_quota_limit}`] : []),
100
+ ] : []),
101
+ ],
102
+ });
103
+ }
104
+ export async function runServicesInvoke(backend, config, serviceExecutionID, capabilityID, input, options = {}) {
105
+ const readModel = await backend.getServiceExecution(serviceExecutionID);
106
+ const requestedCapability = readModel.capabilities.find((capability) => capability.capability_id === capabilityID);
107
+ if (!requestedCapability) {
108
+ 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" }]);
109
+ }
110
+ if (requestedCapability.requires_payment) {
111
+ 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 的当前合法动作" }]);
112
+ }
113
+ const missingInput = missingRequiredInput(requestedCapability.input_schema, input);
114
+ if (missingInput.length > 0) {
115
+ throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "只补缺失字段到小JSON文件,再提交同一执行;本次尚未调用供应商。", [], { schema_version: "itpay.interaction.v1", stage: "input_required",
116
+ input_template: { command: `itpay services invoke ${serviceExecutionID} --capability ${capabilityID} --input-json <file> --json`,
117
+ required_input: ["file", ...missingInput], executable: false } });
118
+ }
119
+ const idempotencyKey = await operationID(config, `service.invoke:${serviceExecutionID}:${capabilityID}:${stableInput(input)}`);
120
+ let response;
121
+ try {
122
+ response = await backend.invokeServiceCapability(serviceExecutionID, capabilityID, {
123
+ idempotency_key: idempotencyKey,
124
+ redacted_summary: input,
125
+ });
126
+ }
127
+ catch (error) {
128
+ if (!(error instanceof HttpError) || error.code !== "verified_phone_required")
129
+ throw error;
130
+ writeCommandEnvelope({
131
+ status: "human_action_required",
132
+ result: { service_execution_id: serviceExecutionID, error_code: "verified_phone_required" },
133
+ instruction: "需要在官方页面完成手机号验证后当前执行才能继续。运行 itpay auth login 打开官方登录页,完成手机号验证并绑定本设备后重试原命令;CLI 不接收手机号或验证码。",
134
+ next: { command: "itpay auth login --json", reason: "完成官方手机号验证并绑定当前设备" },
135
+ recovery: [{ command: `itpay services invoke ${serviceExecutionID} --capability ${capabilityID}${formatInputOptions(input)} --json`, reason: "仅在完成手机号验证与设备绑定后重试" }],
136
+ }, { ...options, plainResult: ["手机号验证:itpay auth login"] });
137
+ return;
138
+ }
139
+ const envelope = invokedEnvelope(response, requestedCapability, readModel.capabilities, input);
140
+ writeCommandEnvelope(envelope.value, {
141
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
142
+ ...(options.output ? { output: options.output } : {}),
143
+ plainResult: envelope.plainResult,
144
+ });
145
+ }
146
+ export function invokedEnvelope(response, requestedCapability, capabilities, input) {
147
+ if (response.execution_request_id) {
148
+ const id = response.execution.service_execution_id;
149
+ return { value: { status: "running", result: { service_execution_id: id, execution_request_id: response.execution_request_id },
150
+ instruction: "查询已排队或正在运行。告知用户稍候,只读取同一任务的状态;不要重新发起查询,也不要把暂未返回结果说成没有车。",
151
+ next: { command: `itpay services next ${id} --json`, reason: "稍后读取同一查询结果" }, recovery: [] },
152
+ plainResult: ["status: running", `service_execution_id: ${id}`] };
153
+ }
154
+ const items = response.result_items.map((item) => ({
155
+ rank: item.rank,
156
+ title: item.display_title,
157
+ safe_payload: item.safe_payload,
158
+ }));
159
+ const quota = response.effective_quota
160
+ ? { remaining: response.effective_quota.remaining, limit: response.effective_quota.limit }
161
+ : undefined;
162
+ const baseResult = {
163
+ service_execution_id: response.execution.service_execution_id,
164
+ capability_id: requestedCapability.capability_id,
165
+ query: input,
166
+ items,
167
+ ...(quota ? { quota } : {}),
168
+ };
169
+ const businessProjection = serviceAdapter(response.execution).invocation(response, requestedCapability, input, items.length);
170
+ if (businessProjection?.early)
171
+ return businessProjection.early;
172
+ if (businessProjection?.catalog)
173
+ baseResult.catalog = businessProjection.catalog;
174
+ let status = items.length > 0 ? "result_ready" : "no_result";
175
+ let instruction = items.length > 0
176
+ ? "用编号、名称和可公开字段向用户说明候选;若候选列表已满足目标就停止。只有用户明确选择并希望继续时,才提交对应编号;不要向用户提及 safe_payload、Execution 或内部 ID。"
177
+ : `没有找到与“${queryText(input)}”匹配的结果。向用户展示本次为 0 个结果并停止。不要修改、缩短或猜测其他输入;只有用户明确提供新输入后,才能启动新的查询。`;
178
+ if (businessProjection?.instruction)
179
+ instruction = businessProjection.instruction;
180
+ let next = null;
181
+ if (response.effective_quota?.exhausted) {
182
+ status = "quota_exhausted";
183
+ instruction = "免费额度已用完且本次没有调用 Provider。当前没有可购买的 continuation;只读取同一 Execution 的服务端恢复方向。";
184
+ const checkoutAction = response.next_actions?.find((action) => action.kind === "create_checkout");
185
+ const checkoutCapability = capabilities.find((capability) => capability.capability_id === checkoutAction?.capability_id);
186
+ if (checkoutCapability) {
187
+ baseResult.checkout = {
188
+ capability_id: checkoutCapability.capability_id,
189
+ ...(checkoutCapability.price_amount_minor !== undefined && checkoutCapability.price_currency ? {
190
+ price: { amount_minor: checkoutCapability.price_amount_minor, currency: checkoutCapability.price_currency },
191
+ } : {}),
192
+ delivery_email_required: checkoutCapability.delivery_email_required,
193
+ };
194
+ const price = capabilityPrice(checkoutCapability);
195
+ instruction = purchaseConfirmationInstruction("quota_exhausted", price, checkoutCapability.delivery_email_required, checkoutCapability.delivery_email_purpose);
196
+ next = {
197
+ command: checkoutCommand(response.execution.service_execution_id, checkoutCapability, input),
198
+ reason: `仅在用户明确同意支付 ${price} 后执行;否则停止`,
199
+ };
200
+ }
201
+ else {
202
+ next = {
203
+ command: `itpay services next ${response.execution.service_execution_id} --json`,
204
+ reason: "读取服务端提供的付费恢复入口",
205
+ };
206
+ }
207
+ }
208
+ else if (items.length > 0 && requestedCapability.requires_human_action) {
209
+ next = null;
210
+ }
211
+ else if (items.length === 0) {
212
+ next = null;
213
+ }
214
+ const interaction = items.length > 0 && requestedCapability.requires_human_action
215
+ ? {
216
+ schema_version: "itpay.interaction.v1",
217
+ stage: "query_results_ready",
218
+ input_template: {
219
+ command: `itpay services action ${response.execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
220
+ required_input: ["rank"],
221
+ executable: false,
222
+ },
223
+ }
224
+ : undefined;
225
+ if (businessProjection?.suffix)
226
+ instruction += businessProjection.suffix;
227
+ return {
228
+ value: { status, result: baseResult, instruction, next, recovery: [], ...(interaction ? { interaction } : {}) },
229
+ plainResult: serviceResultPlainLines(baseResult),
230
+ };
231
+ }
232
+ export async function runServicesGet(backend, serviceExecutionID, options = {}) {
233
+ const response = await backend.getServiceExecution(serviceExecutionID);
234
+ const execution = response.execution;
235
+ const timeline = response.events.slice(-20).map((event) => ({
236
+ sequence: event.sequence,
237
+ step: event.type,
238
+ status: event.status,
239
+ phase: event.phase,
240
+ ...(event.capability_id ? { capability_id: event.capability_id } : {}),
241
+ occurred_at: event.occurred_at,
242
+ }));
243
+ const deliveryMode = serviceDeliveryMode(response);
244
+ const lockedRefund = response.refunds.find((refund) => refund.access_locked);
245
+ const nextState = servicesNextEnvelope(response);
246
+ const result = {
247
+ ...nextState.result,
248
+ service_execution_id: execution.service_execution_id,
249
+ service_id: execution.service_id,
250
+ status: execution.status,
251
+ phase: execution.phase,
252
+ ...(execution.current_capability_id ? { current_capability_id: execution.current_capability_id } : {}),
253
+ updated_at: execution.updated_at,
254
+ timeline,
255
+ ...(response.workflow ? { workflow: response.workflow } : {}),
256
+ ...serviceAdapter(response.execution).extraFacts(response),
257
+ ...(response.events.length > timeline.length ? { timeline_truncated: true } : {}),
258
+ ...(deliveryMode ? { delivery_mode: deliveryMode } : {}),
259
+ ...(lockedRefund ? {
260
+ access_locked: true,
261
+ refund: { refund_request_id: lockedRefund.refund_request_id, status: lockedRefund.status },
262
+ } : {}),
263
+ };
264
+ const envelope = {
265
+ status: nextState.status,
266
+ result,
267
+ instruction: nextState.instruction,
268
+ next: nextState.next,
269
+ ...(nextState.interaction ? { interaction: nextState.interaction } : {}),
270
+ ...(nextState.communication ? { communication: nextState.communication } : {}),
271
+ ...(nextState.handoff ? { handoff: nextState.handoff } : {}),
272
+ recovery: nextState.recovery.filter(action => action.command !== `itpay services get ${serviceExecutionID} --json`),
273
+ };
274
+ writeCommandEnvelope(envelope, {
275
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
276
+ ...(options.output ? { output: options.output } : {}),
277
+ plainResult: plainValueLines(envelope.result),
278
+ });
279
+ }
280
+ export function shouldWaitForServiceResult(model) {
281
+ if (model.current_delivery || isTerminalServiceExecutionStatus(model.execution.status))
282
+ return false;
283
+ const adapter = serviceAdapter(model.execution);
284
+ if (!adapter.supported)
285
+ return false;
286
+ const wait = adapter.shouldWait(model);
287
+ if (wait !== undefined)
288
+ return wait;
289
+ return ["queued", "running", "delivery"].includes(model.workflow?.status ?? "");
290
+ }
291
+ export async function runServicesNext(backend, serviceExecutionID, options = {}) {
292
+ const started = Date.now();
293
+ const until = started + (options.timeoutSeconds ?? 0) * 1000;
294
+ const sleep = options.sleep ?? ((milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds)));
295
+ let response = await backend.getServiceExecution(serviceExecutionID, options.sinceSnapshot ? { sinceSnapshot: options.sinceSnapshot } : {});
296
+ while (shouldWaitForServiceResult(response) && Date.now() < until) {
297
+ await sleep(options.pollIntervalMS ?? Math.max(1500, serviceAdapter(response.execution).pollAfter(response)));
298
+ response = await backend.getServiceExecution(serviceExecutionID, options.sinceSnapshot ? { sinceSnapshot: options.sinceSnapshot } : {});
299
+ }
300
+ const envelope = servicesNextEnvelope(response);
301
+ if (shouldWaitForServiceResult(response) && options.timeoutSeconds) {
302
+ envelope.result = { ...envelope.result, waited_seconds: Math.round((Date.now() - started) / 1000), wait_timed_out: true };
303
+ envelope.next = { command: `itpay services next ${serviceExecutionID} --timeout 120 --json`, reason: "稍后继续等待同一任务;不重新提交查询" };
304
+ }
305
+ journalTaskState(serviceExecutionID, envelope, options.env ?? process.env);
306
+ writeCommandEnvelope(envelope, {
307
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
308
+ ...(options.output ? { output: options.output } : {}),
309
+ plainResult: servicesNextPlainResult(envelope.result),
310
+ });
311
+ }
312
+ export async function runServicesRun(backend, config, serviceID, input, options = {}) {
313
+ let id = options.executionID;
314
+ let adapter = serviceAdapter({ service_id: serviceID, service_contract_version_id: "" });
315
+ let inputContext = { serviceID, operationID: options.operationID, executionID: options.executionID, continuation: options.continuation };
316
+ try {
317
+ if (!id) {
318
+ const started = await backend.startServiceExecution({
319
+ service_id: serviceID,
320
+ ...(options.operationID ? { operation_id: options.operationID } : {}),
321
+ ...(options.continuation ? { continuation: options.continuation } : {}),
322
+ client_context: { host: options.host ?? "terminal", features: serviceClientFeatures(), ...(options.target ? { target: options.target } : {}) },
323
+ ...(input !== undefined ? { input } : {}),
324
+ });
325
+ if ("overview_only" in started) {
326
+ writeServiceOverview(started.service_definition, options);
327
+ return;
328
+ }
329
+ id = started.execution.service_execution_id;
330
+ if (!started.workflow_entry) {
331
+ await runServicesNext(backend, id, options);
332
+ return;
333
+ }
334
+ }
335
+ let model = await backend.getServiceExecution(id);
336
+ adapter = serviceAdapter(model.execution);
337
+ if (model.execution.public_binding)
338
+ inputContext = { ...inputContext, ...executionInputContext(model.execution), ...(options.continuation ? { continuation: options.continuation } : {}) };
339
+ if (model.execution.service_id !== serviceID && model.execution.public_binding?.public_service_id !== serviceID)
340
+ throw new Error("execution belongs to another service");
341
+ if (options.operationID && model.execution.public_binding?.operation_id !== options.operationID)
342
+ throw new Error("execution belongs to another operation");
343
+ if (!model.workflow_entry || (input === undefined && !model.workflow)) {
344
+ await runServicesNext(backend, id, options);
345
+ return;
346
+ }
347
+ if (!serviceAdapter(model.execution).supported) {
348
+ writeCommandEnvelope(servicesNextEnvelope(model), options);
349
+ return;
350
+ }
351
+ if (isTerminalServiceExecutionStatus(model.execution.status)) {
352
+ // A restored terminal purchase cannot advance or obtain a new checkout.
353
+ writeCommandEnvelope(servicesNextEnvelope(model), options);
354
+ return;
355
+ }
356
+ if (input !== undefined) {
357
+ model = await backend.advanceServiceExecution(id, input, `workflow-input:${id}`);
358
+ }
359
+ else if (model.workflow?.status === "quota_paused") {
360
+ model = await backend.advanceServiceExecution(id, undefined, `workflow-resume:${id}`);
361
+ }
362
+ const until = Date.now() + (options.timeoutSeconds ?? 120) * 1000;
363
+ const sleep = options.sleep ?? ((milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds)));
364
+ const paid = () => model.payment_bindings.some((binding) => binding.status === "payment_verified") || model.checkout_bindings.some((binding) => binding.status === "payment_verified");
365
+ // A planning projection can exist before any usable result or user action.
366
+ // Keep the existing bounded wait until there is something to hand over.
367
+ while ((["queued", "running", "delivery"].includes(model.workflow?.status ?? "") || (model.workflow?.status === "payment" && paid())) && shouldWaitForServiceResult(model) && Date.now() < until) {
368
+ await sleep(options.pollIntervalMS ?? 1500);
369
+ model = await backend.getServiceExecution(id);
370
+ }
371
+ if (model.refunds.some(refund => refund.access_locked)) {
372
+ await runServicesNext(backend, id, options);
373
+ return;
374
+ }
375
+ if (model.workflow?.status === "payment" && !paid()) {
376
+ await runServicesCheckout(backend, config, id, model.workflow_entry?.capability_id, {
377
+ ...options,
378
+ ...(config.agentType ? { agentType: config.agentType } : {}),
379
+ resume: model.checkout_bindings.length > 0,
380
+ });
381
+ return;
382
+ }
383
+ await runServicesNext(backend, id, options);
384
+ }
385
+ catch (cause) {
386
+ if (cause instanceof HttpError && cause.payload?.service_id && cause.payload.service_contract_version_id)
387
+ adapter = serviceAdapter({ service_id: cause.payload.service_id, service_contract_version_id: cause.payload.service_contract_version_id });
388
+ if (cause instanceof HttpError && cause.code === "public_operation_invalid")
389
+ throw new CommandContractError(cause.code, cause.message, "按返回的固定service_definition核对声明操作;购买仍使用原结果详情的完整continuation模板,不改owner/snapshot、不猜operation、不重建原购买。", cause.payload?.service_definition ? [] : [{ command: `itpay services start ${serviceID} --json`, reason: "只读当前公开定义;不创建执行" }], undefined, { service_id: serviceID, ...(cause.payload?.service_definition ? { service_definition: cause.payload.service_definition } : {}), execution_created: false, provider_called: false });
390
+ if (cause instanceof HttpError && cause.code === "capability_input_invalid") {
391
+ const schema = cause.payload?.input_schema;
392
+ const executionID = cause.payload?.service_execution_id ?? id;
393
+ const errorAdapter = cause.payload?.service_id && cause.payload?.service_contract_version_id ? serviceAdapter({ service_id: cause.payload.service_id, service_contract_version_id: cause.payload.service_contract_version_id }) : adapter;
394
+ if (!errorAdapter.supported)
395
+ throw new CommandContractError("presentation_contract_unavailable", cause.message, "当前锁定合同不受此CLI支持;保留原输入和执行并停止,使用兼容CLI,不猜业务或提交修正模板。", [], undefined, { service_id: serviceID, ...(executionID ? { service_execution_id: executionID } : {}), original_error_code: cause.code });
396
+ throw new CommandContractError(cause.code, cause.message, errorAdapter.inputInstruction, schema ? [] : [{ command: executionID ? `itpay services get ${executionID} --json` : "itpay catalog list --json", reason: "获取当前锁定合同;仍缺合同则停止,不猜旧写法" }], schema ? { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: operationInputTemplate({ ...inputContext, executionID }, errorAdapter.inputTemplate(schema, input), requiredInputFields(schema)) } : undefined, { service_id: serviceID, ...(executionID ? { service_execution_id: executionID } : {}), execution_created: cause.payload?.execution_created ?? Boolean(executionID), ...(typeof cause.payload?.provider_called === "boolean" ? { provider_called: cause.payload.provider_called } : {}), input_errors: cause.payload?.input_errors ?? [cause.message], ...(schema ? { input_schema: schema } : {}) });
397
+ }
398
+ adapter.runError(cause, serviceID, id);
399
+ if (cause instanceof CommandContractError || cause instanceof HttpError || cause instanceof DeviceLockBusyError || cause instanceof DeviceStateError)
400
+ throw cause;
401
+ if (!id) {
402
+ throw new CommandContractError("workflow_start_outcome_unknown", cause instanceof Error ? cause.message : "service creation outcome unknown", "创建服务执行时没有收到完整响应。先查询当前身份可见的执行;不要直接重跑并创建替代执行。", [{ command: "itpay services list --json", reason: "查找可能已经创建的服务执行" }]);
403
+ }
404
+ if (cause instanceof HttpTransportError)
405
+ throw new CommandContractError("workflow_outcome_unknown", cause.message, "未收到完整响应;只读同一执行的权威事实,不重放写操作或创建替代执行。", [{ command: `itpay services next ${id} --json`, reason: "核对同一执行" }]);
406
+ throw new CommandContractError("workflow_run_failed", cause instanceof Error ? cause.message : "workflow run failed", "保留当前执行并按错误处理,不要重复创建服务执行。", [{ command: `itpay services next ${id} --json`, reason: "读取同一执行的当前事实与恢复动作" }]);
407
+ }
408
+ }
409
+ export function writeServiceOverview(definition, options) {
410
+ const adapter = serviceAdapter({ service_id: definition.service_id, service_contract_version_id: definition.definition_version, public_binding: { public_service_id: definition.service_id, service_definition_version: definition.definition_version, operation_id: "overview", adapter_key: definition.adapter_key } });
411
+ if (!adapter.supported) {
412
+ writeCommandEnvelope({ status: "presentation_contract_unavailable", result: { service_id: definition.service_id, definition_version: definition.definition_version }, instruction: "当前公开定义不受此CLI支持;保留原任务并停止,使用兼容CLI,不猜操作或输入合同。", next: null, recovery: [] }, options);
413
+ return;
414
+ }
415
+ const operations = definition.operations.map(op => ({ operation_id: op.id, title: op.display_names["zh-CN"], role: op.role, billing_mode: op.billing_mode, requires_continuation: op.requires_continuation ?? false, ...(!op.requires_continuation ? { input_schema: op.input_schema, input_template: operationInputTemplate({ serviceID: definition.service_id, operationID: op.id }, adapter.inputTemplate(op.input_schema), requiredInputFields(op.input_schema)) } : { instruction: "使用已有结果详情返回的完整准备购买模板、输入合同和continuation;不自行拼凭据或重新发现购买服务。" }) }));
416
+ const result = { service_id: definition.service_id, title: definition.display_names["zh-CN"], definition_version: definition.definition_version, ...(definition.guide ? { guide: `itpay docs show ${definition.guide.topic}` } : {}), operations };
417
+ writeCommandEnvelope({ status: "service_overview", result, instruction: adapter.overviewInstruction(definition.guide?.topic ?? ""), next: definition.guide ? { command: `itpay docs show ${definition.guide.topic}`, reason: "首次操作前读取服务的完整指导" } : null, recovery: [] }, { ...options, plainResult: plainValueLines(result) });
418
+ }
@@ -0,0 +1,130 @@
1
+ import { serviceAdapter } from "../../services/registry.js";
2
+ import { CommandContractError, writeCommandEnvelope, plainValueLines } from "../../services/shared/envelope.js";
3
+ import { grantedResultEnvelope, servicesNextPlainResult } from "../../services/shared/platform.js";
4
+ import { runServicesNext } from "./lifecycle.js";
5
+ export async function runServicesPage(backend, serviceExecutionID, resultItemID, options = {}) {
6
+ const offset = options.offset ?? 0;
7
+ const limit = options.limit ?? 20;
8
+ if (!Number.isInteger(offset) || offset < 0) {
9
+ throw new CommandContractError("offset_invalid", "--offset must be a non-negative integer", "offset 必须是非负整数;本次未读取服务端分页。", [{ command: `itpay services page ${serviceExecutionID} ${resultItemID} --json`, reason: "从第一页重新读取" }]);
10
+ }
11
+ if (!Number.isInteger(limit) || limit < 1 || limit > 20) {
12
+ throw new CommandContractError("limit_invalid", "--limit must be an integer from 1 to 20", "limit 必须是 1 到 20 的整数;本次未读取服务端分页。", [{ command: `itpay services page ${serviceExecutionID} ${resultItemID} --offset ${offset} --json`, reason: "用默认页大小重试" }]);
13
+ }
14
+ const response = await backend.getServiceExecutionResultItemPage(serviceExecutionID, resultItemID, offset, limit, limit === 1 ? undefined : "summary");
15
+ const execution = response.service_id && response.service_contract_version_id
16
+ ? { service_id: response.service_id, service_contract_version_id: response.service_contract_version_id, ...(response.public_binding ? { public_binding: response.public_binding } : {}) }
17
+ : (await backend.getServiceExecution(serviceExecutionID)).execution;
18
+ const adapter = serviceAdapter(execution);
19
+ if (adapter.renderPage(response, serviceExecutionID, resultItemID, offset, limit, options))
20
+ return;
21
+ const envelope = { status: !adapter.supported ? "presentation_contract_unavailable" : "result_page", result: { service_execution_id: serviceExecutionID, page: response.page }, instruction: !adapter.supported ? "当前锁定合同不受支持;保留已有结果并停止,使用兼容CLI读取同一执行。" : "这是同一已保存结果的分页;直接读取事实,不重查。", next: null, recovery: [] };
22
+ writeCommandEnvelope(envelope, { ...options, plainResult: plainValueLines(envelope.result) });
23
+ }
24
+ export async function runServicesList(backend, options = {}) {
25
+ const limit = options.limit ?? 10;
26
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
27
+ 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: "使用默认上限重试" }]);
28
+ }
29
+ const response = await backend.listServiceExecutions(limit);
30
+ const executions = response.executions.map(({ execution }) => ({
31
+ service_execution_id: execution.service_execution_id,
32
+ service_id: execution.service_id,
33
+ status: execution.status,
34
+ phase: execution.phase,
35
+ updated_at: execution.updated_at,
36
+ reader: { command: `itpay services next ${execution.service_execution_id} --json`, reason: "读取这笔已有服务" },
37
+ }));
38
+ const latest = executions[0];
39
+ const envelope = {
40
+ status: latest ? "listed" : "no_executions",
41
+ result: { executions },
42
+ instruction: executions.length === 1
43
+ ? "只有一条可恢复记录;继续读取同一笔服务。"
44
+ : latest
45
+ ? "按原任务中已明确的执行ID读取对应reader;只有无法确定属于哪一笔任务时才询问用户。"
46
+ : "当前设备没有可恢复的 Service Execution;先读取已发布目录,不要猜测 ID。",
47
+ next: executions.length === 1
48
+ ? { command: `itpay services next ${latest.service_execution_id} --json`, reason: "继续唯一可恢复的服务" }
49
+ : latest
50
+ ? null
51
+ : { command: "itpay catalog list --json", reason: "选择已发布服务" },
52
+ recovery: [],
53
+ };
54
+ writeCommandEnvelope(envelope, {
55
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
56
+ ...(options.output ? { output: options.output } : {}),
57
+ });
58
+ }
59
+ export async function runServicesReadResult(backend, serviceExecutionID, options = {}) {
60
+ const model = await backend.getServiceExecution(serviceExecutionID);
61
+ const outcome = await serviceAdapter(model.execution).readResult(backend, model, serviceExecutionID, options);
62
+ if (outcome === "handled")
63
+ return;
64
+ if (outcome === "state") {
65
+ await runServicesNext(backend, serviceExecutionID, options);
66
+ return;
67
+ }
68
+ const response = await backend.getGrantedServiceResult(serviceExecutionID);
69
+ let orderID;
70
+ try {
71
+ const model = await backend.getServiceExecution(serviceExecutionID);
72
+ orderID = (model.current_delivery ?? model.delivery_bindings.at(-1))?.order_id;
73
+ }
74
+ catch {
75
+ // Feedback context is optional and must never block an authorized result.
76
+ }
77
+ const envelope = grantedResultEnvelope(response, orderID);
78
+ writeCommandEnvelope(envelope, {
79
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
80
+ ...(options.output ? { output: options.output } : {}),
81
+ plainResult: servicesNextPlainResult(envelope.result),
82
+ });
83
+ }
84
+ export async function runServicesEvents(backend, serviceExecutionID, options = {}) {
85
+ const afterSequence = options.afterSequence ?? 0;
86
+ const limit = options.limit ?? 50;
87
+ if (!serviceExecutionID.trim()) {
88
+ throw new CommandContractError("service_execution_id_required", "service execution id is required", "使用 services list 返回的 execution ID;不要猜测。", [{ command: "itpay services list --json", reason: "列出当前身份可见执行" }]);
89
+ }
90
+ if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) {
91
+ throw new CommandContractError("events_parameter_invalid", "after_sequence must be a non-negative integer", "--after-sequence 必须是非负整数;本次未读取事件。", [{ command: `itpay services events ${serviceExecutionID} --help`, reason: "查看诊断参数" }]);
92
+ }
93
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
94
+ 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: "查看诊断参数" }]);
95
+ }
96
+ const response = await backend.listServiceExecutionEvents(serviceExecutionID, afterSequence, limit);
97
+ const events = response.events.map((event) => ({
98
+ sequence: event.sequence,
99
+ type: event.type,
100
+ status: event.status,
101
+ phase: event.phase,
102
+ ...(event.capability_id ? { capability_id: event.capability_id } : {}),
103
+ occurred_at: event.occurred_at,
104
+ }));
105
+ writeCommandEnvelope({
106
+ status: "listed",
107
+ result: {
108
+ service_execution_id: serviceExecutionID,
109
+ after_sequence: afterSequence,
110
+ returned_count: events.length,
111
+ events,
112
+ },
113
+ instruction: "这是已保存的诊断事件;事件不代表业务结果,也不授权重放业务步骤。已读完所需诊断时停止;实际任务沿先前返回的当前动作继续。",
114
+ next: null,
115
+ recovery: events.length === limit && events.length > 0
116
+ ? [{
117
+ command: `itpay services events ${serviceExecutionID} --after-sequence ${events.at(-1).sequence} --limit ${limit} --json`,
118
+ reason: "继续读取下一页诊断事件",
119
+ }]
120
+ : [],
121
+ }, {
122
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
123
+ ...(options.output ? { output: options.output } : {}),
124
+ plainResult: [
125
+ `service_execution_id: ${serviceExecutionID}`,
126
+ `returned_count: ${events.length}`,
127
+ ...events.map((event) => `${event.sequence} ${event.occurred_at} ${event.type} ${event.status}/${event.phase}`),
128
+ ],
129
+ });
130
+ }