@itpay/cli 2.1.33 → 2.1.40

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 (75) hide show
  1. package/dist/src/client/backend.js +5 -0
  2. package/dist/src/commands/aftercare.js +12 -0
  3. package/dist/src/commands/buy.js +12 -56
  4. package/dist/src/commands/cart.js +2 -0
  5. package/dist/src/commands/catalog.js +24 -9
  6. package/dist/src/commands/checkout_handoff.js +11 -99
  7. package/dist/src/commands/guidance.js +1 -184
  8. package/dist/src/commands/install.js +2 -1
  9. package/dist/src/commands/order.js +20 -2
  10. package/dist/src/commands/rail_catalog.js +1 -261
  11. package/dist/src/commands/readyz.js +2 -2
  12. package/dist/src/commands/refund.js +10 -54
  13. package/dist/src/commands/services/actions.js +133 -0
  14. package/dist/src/commands/services/commerce.js +197 -0
  15. package/dist/src/commands/services/lifecycle.js +418 -0
  16. package/dist/src/commands/services/results.js +130 -0
  17. package/dist/src/commands/services.js +14 -2584
  18. package/dist/src/commands/skill.js +3 -0
  19. package/dist/src/main.js +82 -31
  20. package/dist/src/render/checkout_handoff.js +101 -0
  21. package/dist/src/render/checkout_plan.js +47 -0
  22. package/dist/src/services/adapter.js +1 -0
  23. package/dist/src/services/presentation.js +198 -0
  24. package/dist/src/services/rail/actions.js +51 -0
  25. package/dist/src/services/rail/adapter.js +27 -0
  26. package/dist/src/services/rail/aftercare.js +45 -0
  27. package/dist/src/services/rail/catalog.js +261 -0
  28. package/dist/src/services/rail/checkout.js +19 -0
  29. package/dist/src/services/rail/fulfillment.js +76 -0
  30. package/dist/src/services/rail/guide.js +110 -0
  31. package/dist/src/services/rail/invocation.js +54 -0
  32. package/dist/src/services/rail/page.js +76 -0
  33. package/dist/src/services/rail/pagination.js +6 -0
  34. package/dist/src/services/rail/query.js +71 -0
  35. package/dist/src/services/rail/readers.js +107 -0
  36. package/dist/src/services/rail/reading.js +125 -0
  37. package/dist/src/services/rail/recovery.js +51 -0
  38. package/dist/src/services/rail/results.js +378 -0
  39. package/dist/src/services/rail/review.js +268 -0
  40. package/dist/src/services/rail/saved.js +49 -0
  41. package/dist/src/services/rail/wait.js +11 -0
  42. package/dist/src/services/registry.js +31 -0
  43. package/dist/src/services/schema.js +21 -0
  44. package/dist/src/services/shared/envelope.js +184 -0
  45. package/dist/src/services/shared/input.js +39 -0
  46. package/dist/src/services/shared/pagination.js +28 -0
  47. package/dist/src/services/shared/platform.js +365 -0
  48. package/dist/src/services/shared/refund.js +40 -0
  49. package/dist/src/services/shared/workflow.js +28 -0
  50. package/dist/src/state/account_auth.js +25 -10
  51. package/dist/src/state/config.js +2 -2
  52. package/dist/src/state/device_authority.js +5 -3
  53. package/docs/agent/buyer/cart-checkout.json +2 -1
  54. package/docs/agent/buyer/catalog-list.json +6 -4
  55. package/docs/agent/buyer/identity-and-sessions.json +3 -2
  56. package/docs/agent/buyer/install-and-setup.json +8 -3
  57. package/docs/agent/buyer/orders-refunds.json +14 -5
  58. package/docs/agent/buyer/payment-flow.json +2 -1
  59. package/docs/agent/buyer/quickstart.json +3 -2
  60. package/docs/agent/buyer/rail-booking.json +41 -27
  61. package/docs/agent/buyer/render-hosts.json +1 -1
  62. package/docs/agent/buyer/service-feedback.json +2 -2
  63. package/docs/cli-reference/commands/aftercare.md +15 -0
  64. package/docs/cli-reference/commands/cart/add.md +4 -0
  65. package/docs/cli-reference/commands/cart/next.md +4 -0
  66. package/docs/cli-reference/commands/readyz.md +2 -50
  67. package/docs/cli-reference/commands/services/checkout.md +4 -0
  68. package/docs/cli-reference/commands/services/next.md +8 -35
  69. package/docs/cli-reference/commands/services/quote.md +1 -1
  70. package/docs/cli-reference/commands/services/run.md +8 -12
  71. package/docs/cli-reference/commands/services/start.md +3 -1
  72. package/docs/cli-reference/conventions.md +1 -1
  73. package/docs/cli-reference/index.md +2 -0
  74. package/package.json +4 -3
  75. package/skills/itpay/SKILL.md +53 -19
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { CODEX_WAIT_GUIDANCE } from "./install.js";
4
5
  import { declaredAgentType } from "../state/agent_type.js";
5
6
  import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
6
7
  const commandDir = dirname(fileURLToPath(import.meta.url));
@@ -19,6 +20,8 @@ export function runSkillShow(name, options = {}) {
19
20
  }
20
21
  validateSkill(content);
21
22
  const agentType = options.agentType ?? declaredAgentType();
23
+ if (agentType?.startsWith("codex-"))
24
+ content += `\n## Codex command continuation\n\n${CODEX_WAIT_GUIDANCE}\n`;
22
25
  const envelope = {
23
26
  status: "shown",
24
27
  result: { skill: ITPAY_SKILL, content },
package/dist/src/main.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { runAftercareList, runAftercareCreate, runAftercareGet, runAftercareRespond } from "./commands/aftercare.js";
1
2
  import { agentAuth } from "./state/account_auth.js";
2
3
  import { registerSell } from "./sell/commands.js";
3
4
  import { readFileSync as readWorkflowInputFile, statSync as statWorkflowInputFile } from "node:fs";
@@ -6,7 +7,7 @@ import { runServicesRun } from "./commands/services.js";
6
7
  // services/backend/internal/httpapi/handlers/*.go. Commands only
7
8
  // orchestrate; HTTP and rendering live in src/client and src/render.
8
9
  import { Command } from "commander";
9
- import { BackendOverrideError, CLI_VERSION, cliDistribution, loadConfig, cartSessionPath, newBackendClient } from "./state/config.js";
10
+ import { BackendOverrideError, API_CONTRACT_REVISION, CLI_VERSION, cliDistribution, loadConfig, cartSessionPath, newBackendClient } from "./state/config.js";
10
11
  import { DeviceAuthority, DeviceAuthorizationError, DeviceLockBusyError, DeviceStateError } from "./state/device_authority.js";
11
12
  import { CartSession } from "./state/cart_session.js";
12
13
  import { defaultHostForAgentType, normalizeHost, validateContext } from "./state/client_context.js";
@@ -128,9 +129,6 @@ function reportCLIError(error, contract) {
128
129
  error.code === "client_compatibility_headers_required" ||
129
130
  error.code === "platform_release_unavailable" ||
130
131
  (error.status === 404 && error.code === "unknown_error"));
131
- const requiredCLIVersion = incompatible && error instanceof HttpError && /^\d+\.\d+\.\d+$/.test(error.payload?.minimum_cli_version ?? "")
132
- ? error.payload.minimum_cli_version
133
- : undefined;
134
132
  const backendInternal = error instanceof HttpError && error.status === 500 && error.code === "internal_error";
135
133
  const providerConnectionUnavailable = error instanceof HttpError && error.code === "provider_connection_unavailable";
136
134
  const providerTemporary = error instanceof HttpError && error.code === "provider_temporarily_unavailable";
@@ -155,7 +153,7 @@ function reportCLIError(error, contract) {
155
153
  reason: "检查锁持有者;只回收已退出进程留下的锁,不改变设备身份",
156
154
  }] : [];
157
155
  const authorizationInstruction = stateError
158
- ? "当前运行环境无法写入 owner-only Device 状态;请保持同一 Node、CLI 和 Agent Type,在允许持久写入 ~/.itpay-v3 的执行环境中重试。不要手工创建 lock、删除 identity 或换运行时碰运气。"
156
+ ? `当前运行环境无法写入 owner-only Device 状态;受阻路径 ${stateError.blockedPath ?? "~/.itpay-v3"},操作 ${stateError.operation}(${stateError.causeCode})。保持同一 Node、CLI 和 Agent Type,修复该路径持久写权限后重试原命令;不要手工创建 lock、删除 identity、随意更换 HOME 或换运行时。`
159
157
  : lockError
160
158
  ? "另一个进程仍在更新本地身份。执行 recovery 检查;若持有者仍活跃,等待后继续原任务,不要删除锁或重建身份。"
161
159
  : error instanceof HttpError && error.code === "agent_device_session_required"
@@ -176,10 +174,13 @@ function reportCLIError(error, contract) {
176
174
  code: incompatible ? "backend_contract_incompatible" : backendOverrideError?.code ?? commandError?.code ?? (error instanceof HttpError ? error.code : transportError?.code ?? stateError?.code ?? lockError?.code ?? deviceError?.code ?? contract?.code ?? "command_failed"),
177
175
  message: error instanceof Error ? error.message : String(error),
178
176
  },
179
- ...(commandError?.result ? { result: commandError.result } : requiredCLIVersion ? {
177
+ ...(commandError?.result ? { result: commandError.result } : incompatible && error instanceof HttpError ? {
180
178
  result: {
181
179
  current_cli_version: CLI_VERSION,
182
- required_cli_version: requiredCLIVersion,
180
+ current_api_contract_revision: API_CONTRACT_REVISION,
181
+ platform_api_contract_revision: error.payload?.api_contract_revision ?? null,
182
+ minimum_cli_version: error.payload?.minimum_cli_version ?? null,
183
+ maximum_cli_major: error.payload?.maximum_cli_major ?? null,
183
184
  },
184
185
  } : transportError ? {
185
186
  result: {
@@ -199,9 +200,7 @@ function reportCLIError(error, contract) {
199
200
  },
200
201
  } : {}),
201
202
  instruction: incompatible
202
- ? requiredCLIVersion
203
- ? distributionUpgradeInstruction()
204
- : "告诉用户 ItPay 当前版本信息无法验证,因此本次服务不能安全继续。停止所有 ItPay 和 npm 操作;不要转述技术错误、猜版本、切换 Agent Type 或删除 Device 身份。"
203
+ ? "当前 CLI 与 Backend 合约不兼容或无法核验。minimum_cli_version 仅是协议门槛,不是已验证的推荐安装包。停止本轮,请维护方提供与当前 Backend 配套的制品;不要猜版本、降级、切换 Agent Type 或删除 Device 身份。"
205
204
  : backendInternal
206
205
  ? "告诉用户 ItPay 当前暂时无法完成这项服务并停止。不要转述内部错误、重试、修改身份、创建替代服务或进入任何付费路径。"
207
206
  : providerConnectionUnavailable
@@ -217,18 +216,18 @@ function reportCLIError(error, contract) {
217
216
  : capabilityInputInvalid
218
217
  ? "Agent按当前合同模板修正字段和JSON层级,保留用户原始需求,继续同一次服务。不要让用户修技术输入,只有真实业务歧义才询问。"
219
218
  : transportError
220
- ? transportError.attempts > 1
221
- ? "临时网络故障;CLI 已仅对可安全重放的操作完成有限自动重试,但仍未获得完整响应。按 recovery 查询同一资源的权威状态;不要创建替代 Checkout、Execution、Payment 或 Refund。"
222
- : "网络在完整响应前中断;当前写操作没有安全重放合同,因此 CLI 未自动重试。按 recovery 查询权威状态;不要原样重放或创建替代 Checkout、Execution、Payment 或 Refund。"
219
+ ? contract?.initialization
220
+ ? "本轮初始化检查未完成;CLI 已结束有限的网络尝试,尚未确认后端或设备就绪。结束本轮,网络恢复后可重新检查;不要继续登录或业务操作。"
221
+ : transportError.attempts > 1
222
+ ? "临时网络故障;CLI 已仅对可安全重放的操作完成有限自动重试,但仍未获得完整响应。按 recovery 查询同一资源的权威状态;不要创建替代 Checkout、Execution、Payment 或 Refund。"
223
+ : "网络在完整响应前中断;当前写操作没有安全重放合同,因此 CLI 未自动重试。按 recovery 查询权威状态;不要原样重放或创建替代 Checkout、Execution、Payment 或 Refund。"
223
224
  : backendOverrideError
224
225
  ? "移除 ITPAY_BACKEND_URL 使用正式环境,或按当前测试目标准确设置为 https://sandbox.itpay.ai 或 https://dev.itpay.ai。不要通过切换环境规避当前错误。"
225
226
  : commandError?.instruction ?? authorizationInstruction ?? contract?.instruction ?? "检查命令参数后重试。",
226
227
  next: null,
227
228
  ...(commandError?.interaction ? { interaction: commandError.interaction } : {}),
228
- recovery: incompatible
229
- ? requiredCLIVersion
230
- ? [distributionUpgradeAction(requiredCLIVersion)]
231
- : []
229
+ recovery: incompatible || (transportError && contract?.initialization)
230
+ ? []
232
231
  : backendInternal || providerConnectionUnavailable || providerTemporary || providerInputRejected || providerContractMismatch || providerRejected || capabilityInputInvalid
233
232
  ? []
234
233
  : backendOverrideError ? [] : commandError?.recovery ?? (stateError ? stateRecovery : lockError ? lockRecovery : deviceError ? deviceRecovery : identityRecovery ? httpRecovery : contract?.recovery ?? []),
@@ -275,15 +274,9 @@ function distributionUpgradeAction(version) {
275
274
  reason: `更新包含 ItPay CLI ${version} 的 Kimi plugin`,
276
275
  };
277
276
  default:
278
- return { command: `npm install -g @itpay/cli@${version}`, reason: "安装 Backend 指定的兼容 CLI 版本" };
277
+ return { command: `npm install -g @itpay/cli@${version}`, reason: "恢复当前 CLI 版本的完整安装包" };
279
278
  }
280
279
  }
281
- function distributionUpgradeInstruction() {
282
- if (cliDistribution() === "npm") {
283
- return "当前 CLI 与 Backend 合约不兼容。停止所有 ItPay 业务命令;只执行 recovery.command,将 @itpay/cli 更新到 Backend 指定的精确版本。安装完成后确认 itpay --version 与 result.required_cli_version 完全一致,再重新运行 readyz。不要安装 latest、猜测版本、切换 Agent Type 或删除 Device 身份。";
284
- }
285
- return "当前平台 bundle 与 Backend 合约不兼容。停止所有 ItPay 业务命令;只执行 recovery.command,更新平台 Skill/plugin 后启动新会话,确认 itpay --version 与 result.required_cli_version 完全一致,再重新运行 readyz。不要运行 npm、猜测版本、切换 Agent Type 或删除 Device 身份。";
286
- }
287
280
  program
288
281
  .command("readyz")
289
282
  .description("Probe the V3 backend readiness endpoint")
@@ -301,10 +294,9 @@ program
301
294
  reportCLIError(error, {
302
295
  jsonOutput: Boolean(options.json),
303
296
  code: "backend_unavailable",
297
+ initialization: true,
304
298
  instruction: `当前官方 Backend ${config.baseURL} 不可用;恢复前不要继续下单,也不要切换环境。`,
305
- recovery: [
306
- { command: "itpay readyz", reason: "重试当前官方 Backend 的可用性检查" },
307
- ],
299
+ recovery: [],
308
300
  });
309
301
  }
310
302
  });
@@ -328,7 +320,7 @@ for (const action of ["login", "status"]) {
328
320
  writeCommandEnvelope(envelope, { jsonOutput: Boolean(options.json) });
329
321
  }
330
322
  catch (error) {
331
- reportCLIError(error, { jsonOutput: Boolean(options.json), code: "account_login_failed", instruction: "完成官方网页登录后重试 itpay auth status;不要清除设备登记。" });
323
+ reportCLIError(error, { jsonOutput: Boolean(options.json), code: "account_login_failed", initialization: true, instruction: "本轮授权检查未完成;保留设备及已有授权状态,按明确错误原因恢复。不要自动发起新的登录。" });
332
324
  }
333
325
  });
334
326
  }
@@ -1141,6 +1133,49 @@ feedback
1141
1133
  });
1142
1134
  }
1143
1135
  });
1136
+ const aftercare = program.command("aftercare").description("Railway manual refund/change requests; never automatic supplier operations");
1137
+ aftercare.command("list").requiredOption("--order <order_id>").option("--json").action(async (options) => {
1138
+ try {
1139
+ const config = loadConfig();
1140
+ writeCommandEnvelope(await runAftercareList(newBackendClient(config), options.order), { jsonOutput: Boolean(options.json) });
1141
+ }
1142
+ catch (error) {
1143
+ reportCLIError(error, { jsonOutput: Boolean(options.json), code: "aftercare_read_failed", instruction: "核对原订单与当前身份;铁路人工售后不改变平台内容授权,不换身份绕过。", recovery: [{ command: "itpay services list --json", reason: "找回当前身份原订单" }] });
1144
+ }
1145
+ });
1146
+ aftercare.command("create").requiredOption("--order <order_id>").requiredOption("--input-json <file>").option("--json").action(async (options) => {
1147
+ const recovery = [{ command: `itpay aftercare list --order ${options.order} --json`, reason: "读取原订单当前行程与完整申请模板" }];
1148
+ try {
1149
+ const config = loadConfig();
1150
+ const input = readInputJsonObject(options.inputJson, "rail_aftercare_input_invalid", recovery);
1151
+ writeCommandEnvelope(await runAftercareCreate(newBackendClient(config), config, options.order, input), { jsonOutput: Boolean(options.json) });
1152
+ }
1153
+ catch (error) {
1154
+ reportCLIError(error, { jsonOutput: Boolean(options.json), code: "aftercare_create_failed", instruction: "保留原申请与用户范围,按实际错误读取当前模板或办理状态;不猜金额/身份,不重复未知操作。", recovery });
1155
+ }
1156
+ });
1157
+ aftercare.command("get").argument("<case_id>").option("--json").action(async (id, options) => {
1158
+ try {
1159
+ const config = loadConfig();
1160
+ writeCommandEnvelope(await runAftercareGet(newBackendClient(config), id), { jsonOutput: Boolean(options.json) });
1161
+ }
1162
+ catch (error) {
1163
+ reportCLIError(error, { jsonOutput: Boolean(options.json), code: "aftercare_read_failed", instruction: "核对当前身份的原申请,不换身份或探测其他申请。", recovery: [] });
1164
+ }
1165
+ });
1166
+ for (const action of ["respond", "cancel"]) {
1167
+ aftercare.command(action).argument("<case_id>").requiredOption("--input-json <file>").option("--json").action(async (id, options) => {
1168
+ const recovery = [{ command: `itpay aftercare get ${id} --json`, reason: "读取原申请当前版本与明确下一步" }];
1169
+ try {
1170
+ const config = loadConfig();
1171
+ const input = readInputJsonObject(options.inputJson, "rail_aftercare_input_invalid", recovery);
1172
+ writeCommandEnvelope(await runAftercareRespond(newBackendClient(config), id, input, action === "cancel"), { jsonOutput: Boolean(options.json) });
1173
+ }
1174
+ catch (error) {
1175
+ reportCLIError(error, { jsonOutput: Boolean(options.json), code: "aftercare_response_failed", instruction: "按原申请当前版本处理;取消不撤回供应商操作,未知资金不重试。", recovery });
1176
+ }
1177
+ });
1178
+ }
1144
1179
  const refund = program
1145
1180
  .command("refund")
1146
1181
  .enablePositionalOptions()
@@ -1211,7 +1246,7 @@ refund.command("get").argument("<refund_request_id>").option("--json", "output J
1211
1246
  });
1212
1247
  }
1213
1248
  });
1214
- refund.command("watch").argument("<refund_request_id>").option("--interval <seconds>", "poll interval", Number, 2).option("--timeout <seconds>", "timeout", Number, 120).option("--json", "output JSON instead of terminal text").action(async (id, options) => {
1249
+ refund.command("watch").argument("<refund_request_id>").option("--interval <seconds>", "poll interval", Number, 2).option("--timeout <seconds>", "bounded wait; returns on change/manual attention", Number, 20).option("--json", "output JSON instead of terminal text").action(async (id, options) => {
1215
1250
  const config = loadConfig();
1216
1251
  try {
1217
1252
  await runWatchRefund(newBackendClient(config), id, {
@@ -1371,7 +1406,8 @@ function readInputJsonObject(file, code, recovery) {
1371
1406
  const services = program.command("services").description("Generic V3 Service Execution commands");
1372
1407
  services
1373
1408
  .command("run")
1374
- .description("Run a published service and present Checkout when payment is required")
1409
+ .description("Run a declared service operation; preparation approval is required before Checkout")
1410
+ .option("--operation <operation_id>", "operation declared by the public service definition")
1375
1411
  .argument("<service_id>")
1376
1412
  .option("--input-json <file>", "JSON object containing the service input")
1377
1413
  .option("--execution <execution_id>", "resume the same execution")
@@ -1382,16 +1418,31 @@ services
1382
1418
  .action(async (serviceID, options) => {
1383
1419
  const config = loadConfig();
1384
1420
  try {
1385
- const input = options.inputJson
1421
+ let input = options.inputJson
1386
1422
  ? readInputJsonObject(options.inputJson, "workflow_input_invalid", [
1387
1423
  { command: `itpay services run ${serviceID} --json`, reason: "重新读取服务输入声明" },
1388
1424
  ])
1389
1425
  : undefined;
1426
+ let continuation;
1427
+ if (options.operation && input !== undefined) {
1428
+ const parameters = input.input;
1429
+ if (typeof parameters !== "object" || parameters === null || Array.isArray(parameters))
1430
+ throw new CommandContractError("operation_input_invalid", "operation JSON must include an input object", "使用公开操作返回的完整JSON模板,顶层input是业务输入;保留服务端continuation。", []);
1431
+ const reference = input.continuation;
1432
+ if (reference !== undefined) {
1433
+ if (typeof reference !== "object" || reference === null || Array.isArray(reference) || !("service_execution_id" in reference) || !("snapshot_id" in reference) || typeof reference.service_execution_id !== "string" || typeof reference.snapshot_id !== "string")
1434
+ throw new CommandContractError("operation_input_invalid", "invalid continuation reference", "保留结果详情返回的continuation,不猜执行或snapshot。", []);
1435
+ continuation = { service_execution_id: reference.service_execution_id, snapshot_id: reference.snapshot_id };
1436
+ }
1437
+ input = Object.fromEntries(Object.entries(parameters));
1438
+ }
1390
1439
  if (!Number.isFinite(options.timeout) || options.timeout < 0 || options.timeout > 600) {
1391
1440
  throw new Error("timeout must be between 0 and 600 seconds");
1392
1441
  }
1393
1442
  await runServicesRun(newBackendClient(config), config, serviceID, input, {
1394
1443
  ...(options.execution ? { executionID: options.execution } : {}),
1444
+ ...(options.operation ? { operationID: options.operation } : {}),
1445
+ ...(continuation ? { continuation } : {}),
1395
1446
  jsonOutput: Boolean(options.json),
1396
1447
  host: withHost(options.host, config.agentType, options.target),
1397
1448
  ...(options.target ? { target: options.target } : {}),
@@ -0,0 +1,101 @@
1
+ import { buildOpenClawTelegramAction } from "./telegram.js";
2
+ export function shouldPrepareLocalCheckoutImage(platform) {
3
+ return platform === "markdown";
4
+ }
5
+ export function isWorkBuddyPlainChat(agentType, platform) {
6
+ return agentType?.trim().toLowerCase() === "workbuddy" && platform === "plain_chat";
7
+ }
8
+ export function isZCodePlainChat(agentType, platform) {
9
+ return agentType?.trim().toLowerCase() === "zcode" && platform === "plain_chat";
10
+ }
11
+ export function isLinkOnlyBrowserAgent(agentType, platform) {
12
+ return isWorkBuddyPlainChat(agentType, platform) || isZCodePlainChat(agentType, platform);
13
+ }
14
+ export function buildWorkBuddyPresentFilesAction(url) {
15
+ return {
16
+ tool: "present_files",
17
+ arguments: { files: [url] },
18
+ };
19
+ }
20
+ function hasCompleteCheckoutTokens(url) {
21
+ if (!url) {
22
+ return false;
23
+ }
24
+ try {
25
+ const parsed = new URL(url);
26
+ return Boolean(parsed.searchParams.get("display_token") && parsed.searchParams.get("complete_exchange_token"));
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ export function buildCheckoutHandoff(input) {
33
+ const url = input.plan?.localSimulation && input.mobileUrl && hasCompleteCheckoutTokens(input.mobileUrl) ? input.mobileUrl : input.url;
34
+ const handoff = { url, ...(input.plan?.localSimulation ? { environment: "local_simulation", query: "live", transactions: "simulated" } : {}) };
35
+ // mobile_url is only emitted when the link carries the complete token pair;
36
+ // a display-token-only link cannot finish the same-device exchange after payment.
37
+ const mobileUrl = !input.plan?.localSimulation && hasCompleteCheckoutTokens(input.mobileUrl) ? input.mobileUrl : undefined;
38
+ if (mobileUrl) {
39
+ handoff.mobile_url = mobileUrl;
40
+ }
41
+ const workBuddyAction = isWorkBuddyPlainChat(input.agentType, input.platform);
42
+ if (workBuddyAction) {
43
+ handoff.agent_action = buildWorkBuddyPresentFilesAction(url);
44
+ }
45
+ if (input.platform === "markdown") {
46
+ if (input.localPath)
47
+ handoff.qr_local_path = input.localPath;
48
+ if (input.markdown)
49
+ handoff.markdown = input.markdown;
50
+ }
51
+ else if (input.platform === "plain_chat" && input.qrImageURL && !isLinkOnlyBrowserAgent(input.agentType, input.platform)) {
52
+ handoff.qr_image_url = input.qrImageURL;
53
+ }
54
+ else if (input.platform === "telegram" && input.qrImageURL) {
55
+ handoff.qr_image_url = input.qrImageURL;
56
+ }
57
+ if (input.agentType?.trim().toLowerCase() === "openclaw" && input.platform === "telegram" && input.plan && input.target) {
58
+ handoff.agent_action = buildOpenClawTelegramAction(input.plan, input.target);
59
+ }
60
+ return {
61
+ handoff,
62
+ instruction: input.plan?.localSimulation
63
+ ? `本地实验:铁路与地图查询真实,授权、通知和交易模拟,不会扣款。在本机打开 handoff.url 模拟付款入口,金额 ${input.amount};仅本机桌面可达。${workBuddyAction ? "按 handoff.agent_action 原样打开一次。" : "展示原入口。"}等待用户操作后读取同一 Checkout;不创建替代订单,用户声明不证明模拟付款已确认。`
64
+ : `先展示官方入口,再做非必要记忆或日志;保留必需安全检查和宿主权限。${checkoutHandoffInstruction(input.agentType, input.platform, input.amount, Boolean(mobileUrl))}`,
65
+ };
66
+ }
67
+ function checkoutHandoffInstruction(agentType, platform, amount, hasMobileLink) {
68
+ if (isWorkBuddyPlainChat(agentType, platform)) {
69
+ const mobileSentence = hasMobileLink
70
+ ? ",然后把 handoff.mobile_url 作为可点击链接发给用户,说明“手机端点开可直接跳转支付宝或微信支付;小程序内打不开时复制到手机浏览器打开即可”"
71
+ : "";
72
+ const failLinks = hasMobileLink ? "发送 handoff.url 与 handoff.mobile_url" : "发送原始 handoff.url";
73
+ return `Backend 尚未确认付款。立即严格按 handoff.agent_action.tool 和 handoff.agent_action.arguments 原样执行一次,在右侧打开 handoff.url;确认工具调用成功后说明金额 ${amount}${mobileSentence},然后停止等待。若工具失败,只${failLinks},报告未自动打开并停止。不要用 present_files 打开本地文件或二维码 PNG,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
74
+ }
75
+ if (isZCodePlainChat(agentType, platform)) {
76
+ const mobileSentence = hasMobileLink
77
+ ? ",并把 handoff.mobile_url 发给用户备用(手机端点开可直接跳转支付宝或微信支付)"
78
+ : "";
79
+ const fallbackLinks = hasMobileLink ? "同一个可点击 handoff.url 与 handoff.mobile_url" : "同一个可点击 handoff.url";
80
+ return `Backend 尚未确认付款。立即用 ZCode 内置浏览器打开 handoff.url,让用户完成付款;确认已发起打开后说明金额 ${amount}${mobileSentence},然后停止等待。不要只粘贴文字链接,不要下载、解析或重建二维码,不要创建新 Checkout、Payment Intent 或 Execution。只有内置浏览器明确不可用时,才展示${fallbackLinks}。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
81
+ }
82
+ if (agentType?.trim().toLowerCase() === "doubao-work" && platform === "plain_chat") {
83
+ const directLink = hasMobileLink ? "handoff.mobile_url" : "handoff.url";
84
+ const directLabel = hasMobileLink ? "手机直接打开收银台(可跳转支付宝/微信)" : "手机直接打开收银台";
85
+ return `Backend 尚未确认付款。在当前豆包工作对话中展示两个有标签的官方入口:将 ${directLink} 标为“${directLabel}”,将 handoff.qr_image_url 标为“二维码图片(保存或用另一台设备扫码)”。说明金额 ${amount} 后停止等待。不要解析或单独输出 URL credential,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
86
+ }
87
+ if (platform === "markdown") {
88
+ const mobileSentence = hasMobileLink ? ",并附 handoff.mobile_url 供手机端点击直达支付" : "";
89
+ return `Backend 尚未确认付款。把 handoff.markdown 原样发送到当前桌面对话,确认二维码、付款链接和金额 ${amount} 均已实际对用户可见${mobileSentence},然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
90
+ }
91
+ if (platform === "terminal") {
92
+ const mobileSentence = hasMobileLink ? ",并展示 handoff.mobile_url 供手机端点击直达支付" : "";
93
+ return `Backend 尚未确认付款。在用户可见终端展示当前 Checkout 的二维码、handoff.url 和金额 ${amount}${mobileSentence},然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
94
+ }
95
+ if (agentType?.trim().toLowerCase() === "openclaw" && platform === "telegram") {
96
+ const failLinks = hasMobileLink ? "handoff.qr_image_url、金额、handoff.url 与 handoff.mobile_url" : "handoff.qr_image_url、金额和 handoff.url";
97
+ return `Backend 尚未确认付款或授权状态。立即使用 OpenClaw 原生 message tool,严格按 handoff.agent_action.tool 和 handoff.agent_action.arguments 原样执行;它会发送金额 ${amount}、二维码、📱 手机点这儿支付和 📋 已授权给我读。不得改写 presentation、换用其他消息工具、拆开按钮发送或用普通回复声称按钮已经发送。确认 message tool 调用成功后停止等待。收到 callback_data "itp:grant_confirmed:<checkout_id>" 时,表示用户声明已在收银台授权读取;立即执行 next.command 查询同一 Checkout,再只执行 Backend 返回的 next.command 进入同一 Execution 的 grant 流程。该 callback 不证明付款成功或 grant 已生效;Backend 未返回 grant_active 前不得读取或猜测结果。若原生 message tool 明确失败或当前 Telegram 未启用 inline buttons,只发送现有 ${failLinks},报告按钮不可用并停止;不要创建新的 Checkout、Payment Intent 或 Execution。`;
98
+ }
99
+ const urlList = hasMobileLink ? "handoff.url、handoff.mobile_url(手机端点击直达支付)和可用的 handoff.qr_image_url" : "handoff.url 和可用的 handoff.qr_image_url";
100
+ return `Backend 尚未确认付款。把 ${urlList} 实际发送到当前会话,说明金额 ${amount},然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
101
+ }
@@ -0,0 +1,47 @@
1
+ import { platformKeyForHost } from "./plan.js";
2
+ import { isLinkOnlyBrowserAgent } from "./checkout_handoff.js";
3
+ import { qualifyItPayCommand } from "../state/agent_type.js";
4
+ // --- checkout QR plan ---
5
+ export function buildCheckoutQRPlan(input) {
6
+ const summary = input.localSimulation ? "在本机打开模拟付款入口;铁路与地图查询真实,授权、通知和交易模拟,不会扣款。" : `Scan the QR or open ${input.checkoutURL} to start the human checkout flow.`;
7
+ const isPayment = input.paymentIntentID != null;
8
+ const afterCommand = qualifyItPayCommand(`itpay checkout --id ${input.checkoutID} --token ${input.displayToken}${input.locale === "en" ? " --locale en" : ""} --json`, input.agentType);
9
+ const platform = {
10
+ text: summary,
11
+ links: [
12
+ { label: "打开付款页面", url: input.checkoutURL },
13
+ ],
14
+ buttons: [
15
+ { label: "打开收银台", kind: "url", url: input.checkoutURL },
16
+ ...(input.checkoutID
17
+ ? [{ label: "查询 Checkout 状态", kind: "callback", intent: "check_checkout_status", ref: input.checkoutID }]
18
+ : []),
19
+ ],
20
+ blocks: [],
21
+ ...(input.qrPNGURL ? { media: [{ url: input.qrPNGURL, label: "Branded QR", mimeType: "image/png" }] } : {}),
22
+ };
23
+ const plan = {
24
+ kind: isPayment ? "payment_qr" : "checkout_qr",
25
+ ...(input.localSimulation ? { localSimulation: true } : {}),
26
+ host: input.host,
27
+ summary,
28
+ url: input.qrPayload,
29
+ ...(isLinkOnlyBrowserAgent(input.agentType, platformKeyForHost(input.host)) && input.cardURL ? { linkOnlyURL: input.cardURL } : {}),
30
+ preferredQRSources: [input.qrPNGURL ?? input.qrPayload],
31
+ checkoutID: input.checkoutID,
32
+ platform,
33
+ afterActionCommand: afterCommand,
34
+ afterActionLabel: "扫码或点击链接完成支付后,执行以下命令查询状态:",
35
+ };
36
+ if (input.orderItems)
37
+ plan.orderItems = input.orderItems;
38
+ if (input.orderCurrency)
39
+ plan.orderCurrency = input.orderCurrency;
40
+ if (input.paymentMethod)
41
+ plan.paymentMethod = input.paymentMethod;
42
+ if (input.paymentStatus)
43
+ plan.paymentStatus = input.paymentStatus;
44
+ if (input.paymentIntentID)
45
+ plan.paymentIntentID = input.paymentIntentID;
46
+ return plan;
47
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,198 @@
1
+ import { serviceAdapter } from "./registry.js";
2
+ import { refundStateEnvelope } from "./shared/refund.js";
3
+ import { appendFeedbackPostmortemInstruction, isTerminalServiceExecutionStatus } from "./shared/envelope.js";
4
+ import { terminalExecutionEnvelope, serviceDeliveryMode, normalizeGrantStatus, paidContinuation, purchaseConfirmationInstruction, serviceAllowedActionCommand } from "./shared/platform.js";
5
+ export function servicesNextEnvelope(model) {
6
+ const execution = model.execution;
7
+ const adapter = serviceAdapter(execution);
8
+ const currentDelivery = model.current_delivery ?? model.delivery_bindings.at(-1);
9
+ const lockedRefund = model.refunds.find((refund) => refund.access_locked);
10
+ if (lockedRefund) {
11
+ const refundState = refundStateEnvelope(lockedRefund, "delivery_locked");
12
+ return { ...refundState, result: { ...refundState.result, service_execution_id: execution.service_execution_id, refund: { refund_request_id: lockedRefund.refund_request_id, status: lockedRefund.status, ...(lockedRefund.failure_class ? { failure_class: lockedRefund.failure_class } : {}) } },
13
+ next: refundState.next ? { command: `itpay refund get ${lockedRefund.refund_request_id} --json`, reason: "读取同一退款权威状态" } : null };
14
+ }
15
+ const unsettledPayment = model.payment_bindings.some(binding => typeof binding.status === "string" && ["pending", "outcome_unknown", "unknown", "processing"].includes(binding.status));
16
+ if (unsettledPayment)
17
+ return { status: "payment_outcome_pending", result: { service_execution_id: execution.service_execution_id, payment_state: "unsettled", checkout_ids: model.checkout_bindings.map(binding => binding.checkout_id) }, instruction: "原付款结果尚未确认。保留同一执行、Checkout和订单,只核对权威事实;不创建新报价、购物车、付款入口或购买,也不宣称已付款或服务已完成。", next: null, recovery: [{ command: `itpay services get ${execution.service_execution_id} --json`, reason: "仅用户要求或平台恢复后核对同一付款事实" }] };
18
+ // Terminal facts take priority unless the locked adapter supports same-execution recovery.
19
+ if (!(adapter.preservesTerminalRecovery(model)) && isTerminalServiceExecutionStatus(execution.status) && !["completed", "delivery_completed"].includes(execution.status)) {
20
+ // Preserve the registered adapter's safe failure facts even at execution terminal.
21
+ const failedWorkflow = execution.status === "failed" && model.workflow?.status === "failed" ? adapter.workflow(model) : undefined;
22
+ if (failedWorkflow)
23
+ return { ...failedWorkflow, next: null };
24
+ const terminal = terminalExecutionEnvelope(model);
25
+ if (terminal)
26
+ return terminal;
27
+ }
28
+ const latestRun = model.execution_requests?.filter((request) => request.execution_kind === "service.execution.run").at(-1);
29
+ if (latestRun && ["pending", "started"].includes(latestRun.status) && model.workflow?.status !== "failed") {
30
+ return {
31
+ status: "running",
32
+ result: { service_execution_id: execution.service_execution_id, execution_request_id: latestRun.execution_request_id },
33
+ instruction: "任务仍在排队或执行中,稍后读取同一任务。不要重新查询、重复提交输入或把等待状态当作零结果。",
34
+ next: { command: `itpay services next ${execution.service_execution_id} --json`, reason: "稍后读取同一任务状态" },
35
+ recovery: [],
36
+ };
37
+ }
38
+ const businessState = adapter.projectState(model);
39
+ if (businessState)
40
+ return businessState;
41
+ if (latestRun && ["failed", "cancelled"].includes(latestRun.status) && !model.workflow_entry) {
42
+ return {
43
+ status: latestRun.status,
44
+ result: { service_execution_id: execution.service_execution_id },
45
+ instruction: "本次任务未完成,需要检查同一任务的处理记录;这不表示没有结果。不要自动重新发起。",
46
+ next: null,
47
+ recovery: [{ command: `itpay services events ${execution.service_execution_id} --json`, reason: "读取同一任务的处理记录" }],
48
+ };
49
+ }
50
+ if (!adapter.supported)
51
+ return { status: "presentation_contract_unavailable", result: { service_execution_id: execution.service_execution_id, service_id: execution.service_id, service_contract_version_id: execution.service_contract_version_id, public_binding: execution.public_binding }, instruction: "当前锁定呈现合同不受本候选支持;保留执行与已有结果并停止,取得兼容CLI后读取同一执行,不猜模式或启动新购买。", next: null, recovery: [] };
52
+ const workflowEnvelope = adapter.workflow(model);
53
+ if (workflowEnvelope)
54
+ return workflowEnvelope;
55
+ const terminalEnvelope = terminalExecutionEnvelope(model);
56
+ if (terminalEnvelope)
57
+ return terminalEnvelope;
58
+ const currentItems = model.current_result_items ?? [];
59
+ const savedRailResult = adapter.savedResult(model);
60
+ if (savedRailResult)
61
+ return savedRailResult;
62
+ const delivery = currentDelivery;
63
+ const deliveryMode = serviceDeliveryMode(model);
64
+ const candidateSelection = model.allowed_actions?.find((action) => action.type === "select_candidate");
65
+ if (candidateSelection && currentItems.length > 0) {
66
+ const paidCapability = delivery?.capability_id
67
+ ? model.capabilities.find((capability) => capability.capability_id === delivery.capability_id && capability.requires_payment)
68
+ : undefined;
69
+ return {
70
+ status: "candidate_selection_available",
71
+ result: {
72
+ service_execution_id: execution.service_execution_id,
73
+ ...(delivery?.capability_id ? { capability_id: delivery.capability_id } : {}),
74
+ ...(deliveryMode ? { delivery_mode: deliveryMode } : {}),
75
+ items: currentItems.map((item) => ({
76
+ rank: item.rank,
77
+ title: item.display_title,
78
+ safe_payload: item.safe_payload,
79
+ })),
80
+ },
81
+ instruction: paidCapability
82
+ ? "付费搜索已完成。用编号、名称和可公开字段向用户说明结果,然后停止。只有用户明确选择候选并要求继续时才提交 interaction 中的对应编号;不要提及 safe_payload 或自动购买后续服务。"
83
+ : "用编号、名称和可公开字段向用户说明候选;若候选列表已满足目标就停止。只有用户明确选择并希望继续时才提交对应编号;不要提及 safe_payload、Execution 或内部 ID。",
84
+ next: null,
85
+ interaction: candidateInteraction(execution.service_execution_id),
86
+ recovery: [],
87
+ };
88
+ }
89
+ if (deliveryMode === "agent_visible_result") {
90
+ const items = currentItems.map((item) => ({
91
+ rank: item.rank,
92
+ title: item.display_title,
93
+ safe_payload: item.safe_payload,
94
+ }));
95
+ const selection = model.allowed_actions?.find((action) => action.type === "select_candidate");
96
+ const deliveryGuidance = adapter.deliveryGuidance(model);
97
+ const instruction = (delivery?.order_id
98
+ ? appendFeedbackPostmortemInstruction(items.length > 0
99
+ ? selection
100
+ ? "搜索已完成。用编号、名称和可公开字段向用户说明结果,然后停止。只有用户明确选择候选并要求继续时才提交 interaction 中的对应编号;不要提及 safe_payload。"
101
+ : "这一步的结果已经可用。用普通语言解释可公开字段并停止;不要提及 Arazzo、safe_payload 或内部 ID。"
102
+ : "告诉用户本次查询得到 0 个结果并停止。Agent 不读取其他交付、不重放当前查询、修改输入或创建新查询。", "delivered")
103
+ : items.length > 0
104
+ ? selection
105
+ ? "搜索已完成。用编号、名称和可公开字段向用户说明结果,然后停止。只有用户明确选择候选并要求继续时才提交 interaction 中的对应编号;不要提及 safe_payload。"
106
+ : "这一步的结果已经可用。用普通语言解释可公开字段并停止;不要提及 Arazzo、safe_payload 或内部 ID。"
107
+ : "告诉用户本次查询得到 0 个结果并停止。Agent 不读取其他交付、不重放当前查询、修改输入或创建新查询。")
108
+ + (deliveryGuidance?.instruction && items.length > 0 ? ` ${deliveryGuidance.instruction}` : "");
109
+ return {
110
+ status: items.length > 0 ? "result_ready" : "no_result",
111
+ result: {
112
+ service_execution_id: execution.service_execution_id,
113
+ ...(delivery?.capability_id ? { capability_id: delivery.capability_id } : {}),
114
+ ...(delivery?.order_id ? { order_id: delivery.order_id, aftercare: { optional: true, command: `itpay feedback submit --order ${delivery.order_id} --json`, reason: "结果答复后按需复盘;不猜评分" } } : {}),
115
+ delivery_mode: deliveryMode,
116
+ items,
117
+ },
118
+ instruction,
119
+ next: null,
120
+ ...(selection ? { interaction: candidateInteraction(execution.service_execution_id) } : {}),
121
+ recovery: deliveryGuidance?.recovery ?? [],
122
+ };
123
+ }
124
+ if (deliveryMode === "vault_artifact") {
125
+ const grantStatus = normalizeGrantStatus(delivery?.grant_status);
126
+ const grantActive = grantStatus === "active";
127
+ const grantPending = grantStatus === "pending";
128
+ return {
129
+ status: grantActive ? "grant_active" : grantPending ? "result_preparing" : "human_authorization_required",
130
+ result: {
131
+ service_execution_id: execution.service_execution_id,
132
+ ...(delivery?.capability_id ? { capability_id: delivery.capability_id } : {}),
133
+ delivery_mode: deliveryMode,
134
+ grant_status: grantStatus,
135
+ ...(delivery?.preparation ? { preparation: delivery.preparation } : {}),
136
+ ...(grantActive && delivery?.grant_expires_at ? { grant_expires_at: delivery.grant_expires_at } : {}),
137
+ },
138
+ instruction: grantActive
139
+ ? "先告诉用户付费内容已经准备好且当前读取授权有效;立即读取并只解释授权字段,遵守范围与到期时间。"
140
+ : grantPending
141
+ ? "告诉用户:授权已经完成,付费结果仍在同一订单下准备,不需要再次付款或授权。然后只执行 next.command 查询同一笔服务;Agent 不创建新服务、付款页面或数据请求,也不提前读取。"
142
+ : "先告诉用户付费内容已经归入当前订单,但需要本人确认一次读取授权;请用户在订单页面授权,未授权前不要读取或猜测内容。",
143
+ next: grantPending ? {
144
+ command: `itpay services next ${execution.service_execution_id} --json`,
145
+ reason: "等待同一 Execution 的交付准备完成",
146
+ } : {
147
+ command: `itpay services read-result ${execution.service_execution_id} --json`,
148
+ reason: grantActive ? "读取当前有效 grant 的结果" : "仅在用户确认授权后执行",
149
+ },
150
+ recovery: [],
151
+ };
152
+ }
153
+ const allowedActions = model.allowed_actions ?? [];
154
+ const preferred = allowedActions[0];
155
+ if (preferred?.type === "prepare_quote") {
156
+ const continuation = paidContinuation(model, preferred, {});
157
+ if (continuation) {
158
+ return {
159
+ status: execution.status,
160
+ result: {
161
+ service_execution_id: execution.service_execution_id,
162
+ service_id: execution.service_id,
163
+ phase: execution.phase,
164
+ checkout: continuation.checkout,
165
+ },
166
+ instruction: purchaseConfirmationInstruction(execution.status === "quota_exhausted" ? "quota_exhausted" : "candidate_selected", continuation.price, continuation.capability.delivery_email_required, continuation.capability.delivery_email_purpose),
167
+ next: continuation.next,
168
+ recovery: [],
169
+ };
170
+ }
171
+ }
172
+ const next = preferred ? serviceAllowedActionCommand(model, preferred) : null;
173
+ return {
174
+ status: execution.status,
175
+ result: {
176
+ service_execution_id: execution.service_execution_id,
177
+ service_id: execution.service_id,
178
+ phase: execution.phase,
179
+ allowed_actions: allowedActions.map((action) => ({
180
+ type: action.type,
181
+ ...(action.capability_id ? { capability_id: action.capability_id } : {}),
182
+ requires_human: action.requires_human,
183
+ })),
184
+ },
185
+ instruction: preferred?.type === "resume_checkout"
186
+ ? "这笔服务已经有付款页面。只执行 next.command 恢复并展示同一个入口;Agent 不创建新的报价、购物车、付款页面或服务。"
187
+ : preferred?.type === "wait"
188
+ ? "告诉用户付款和订单已经确认,结果仍在同一笔服务中处理,不需要再次付款;如果最终无法交付,将从原订单检查退款路径。稍后只执行 next.command;Agent 不创建新服务、付款页面或数据请求,也不承诺退款结果。"
189
+ : preferred?.requires_human
190
+ ? "当前下一步需要用户明确选择;先展示必要信息并等待确认。"
191
+ : preferred ? "执行服务端返回的唯一首选动作;不要猜测其他 capability。" + adapter.locationInstruction(model.capabilities.find((item) => item.capability_id === preferred.capability_id)?.input_schema) : "当前没有后续动作。",
192
+ next,
193
+ recovery: [{ command: `itpay services get ${execution.service_execution_id} --json`, reason: "仅在当前动作异常时检查时间线" }],
194
+ };
195
+ }
196
+ function candidateInteraction(id) {
197
+ return { schema_version: "itpay.interaction.v1", stage: "candidate_selection_required", input_template: { command: `itpay services action ${id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`, required_input: ["rank"], executable: false } };
198
+ }