@itpay/cli 2.1.22 → 2.1.33

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 (38) hide show
  1. package/README.md +2 -2
  2. package/dist/src/client/backend.js +4 -1
  3. package/dist/src/client/http.js +2 -0
  4. package/dist/src/commands/buy.js +6 -4
  5. package/dist/src/commands/cart.js +15 -15
  6. package/dist/src/commands/catalog.js +10 -8
  7. package/dist/src/commands/checkout.js +9 -11
  8. package/dist/src/commands/checkout_handoff.js +7 -4
  9. package/dist/src/commands/docs.js +3 -14
  10. package/dist/src/commands/guidance.js +43 -25
  11. package/dist/src/commands/install.js +2 -2
  12. package/dist/src/commands/order.js +4 -3
  13. package/dist/src/commands/orders.js +4 -9
  14. package/dist/src/commands/pay.js +1 -1
  15. package/dist/src/commands/refund.js +5 -5
  16. package/dist/src/commands/services.js +628 -390
  17. package/dist/src/commands/vault.js +27 -20
  18. package/dist/src/main.js +35 -24
  19. package/dist/src/render/qr.js +1 -1
  20. package/dist/src/state/account_auth.js +9 -6
  21. package/dist/src/state/config.js +4 -4
  22. package/dist/src/state/device_authority.js +2 -2
  23. package/docs/agent/buyer/cart-checkout.json +1 -1
  24. package/docs/agent/buyer/catalog-list.json +1 -1
  25. package/docs/agent/buyer/identity-and-sessions.json +3 -7
  26. package/docs/agent/buyer/orders-refunds.json +8 -1
  27. package/docs/agent/buyer/payment-flow.json +1 -0
  28. package/docs/agent/buyer/purchased-content.json +1 -1
  29. package/docs/agent/buyer/quickstart.json +6 -5
  30. package/docs/agent/buyer/rail-booking.json +96 -51
  31. package/docs/agent/buyer/rail-fast-checkout.json +6 -3
  32. package/docs/agent/buyer/render-hosts.json +1 -1
  33. package/docs/cli-reference/commands/device.md +4 -5
  34. package/docs/cli-reference/commands/services/invoke.md +2 -2
  35. package/docs/cli-reference/commands/services/read-result.md +12 -5
  36. package/docs/cli-reference/commands/services/run.md +2 -0
  37. package/package.json +5 -1
  38. package/skills/itpay/SKILL.md +25 -16
@@ -1,7 +1,7 @@
1
1
  import { HttpError } from "../client/http.js";
2
- import { requiresTarget } from "../state/client_context.js";
2
+ import { requiresTarget, validateContext } from "../state/client_context.js";
3
3
  import { formatMoney } from "../render/output.js";
4
- import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
4
+ import { CommandContractError, shellArgument, writeCommandEnvelope } from "./guidance.js";
5
5
  import { buildVaultHandoff } from "./vault_handoff.js";
6
6
  function outputOptions(options, plainResult) {
7
7
  return {
@@ -19,21 +19,21 @@ export async function runVaultList(backend, input) {
19
19
  const value = await backend.listBuyerVaultArtifacts(input);
20
20
  writeCommandEnvelope({
21
21
  status: value.items.length ? "vault_listed" : "no_vault_artifacts",
22
- result: { items: value.items, next_cursor: value.next_cursor || null },
22
+ result: { items: value.items.map(item => ({ ...item, amount: formatMoney(item.amount_minor, item.currency), reader: { command: vaultReadCommand(item.artifact_ref, [], input), reason: "读取这一已购内容" } })), next_cursor: value.next_cursor || null },
23
23
  instruction: value.items.length
24
- ? "用编号、服务名称、内容主体、购买时间、金额和订单号说明匹配结果,不要向用户显示内部内容标识。一个精确匹配可按用户原始查看意图继续读取;多个匹配必须让用户选择。"
24
+ ? "用编号、服务名称、内容主体、购买时间、金额和订单号说明匹配结果,不要向用户显示内部内容标识。一个精确匹配可按用户原始查看意图继续读取;已有任务可明确匹配时读取对应行;只有真实歧义才让用户选择。"
25
25
  : "当前账号没有匹配的已购内容。向用户说明没有找到,不要猜测内容标识、自动购买或发起新的服务查询。",
26
- next: null,
26
+ next: value.next_cursor ? { command: `itpay vault list --limit ${input.limit}${input.query ? ` --query ${shellArgument(input.query)}` : ""} --cursor ${shellArgument(value.next_cursor)}${contextFlags(input)} --json`, reason: "读取同一筛选的下一页" } : null,
27
27
  recovery: [],
28
- }, outputOptions(input, value.items.map((item, index) => `${index + 1}. ${item.service_title}${item.subject_label ? ` · ${item.subject_label}` : ""} · ${formatMoney(item.amount_minor, item.currency)} · ${item.purchased_at} · ${item.order_code} · ${item.order_status}`)));
28
+ }, outputOptions(input));
29
29
  }
30
30
  catch (error) {
31
31
  if (error instanceof HttpError && error.code === "vault_authorization_required") {
32
32
  writeCommandEnvelope({
33
33
  status: "human_authorization_required",
34
34
  result: { intent: "list_purchased_content", query: input.query ?? "" },
35
- instruction: `需要用户确认一次身份和只读权限。执行 next.command 生成官方入口,不要声称链接已经创建;用户完成后重新运行原始 vault list 命令。${accessContextInstruction(input)}`,
36
- next: { command: vaultAccessCommand(undefined, input), reason: "创建一次账号读取授权" }, recovery: [],
35
+ instruction: `需要用户确认一次身份和只读权限。按当前动作或待填模板生成官方入口,不要声称链接已经创建;用户完成后重新运行原始 vault list 命令。${accessContextInstruction(input)}`,
36
+ ...vaultAccessGuidance(undefined, input), recovery: [],
37
37
  }, outputOptions(input));
38
38
  return;
39
39
  }
@@ -41,6 +41,9 @@ export async function runVaultList(backend, input) {
41
41
  }
42
42
  }
43
43
  export async function runVaultAccess(backend, artifactRef, options) {
44
+ const issue = validateContext(options.host, options.target);
45
+ if (issue)
46
+ throw new CommandContractError(issue.code, issue.message, "先补齐当前可信会话的真实宿主和目标;尚未创建授权请求。", [], { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command: vaultAccessCommand(artifactRef, options), required_input: issue.code === "target_required" ? ["target"] : ["host"], executable: false } });
44
47
  const value = await backend.createVaultAccessRequest(artifactRef
45
48
  ? { purpose: "artifact_reveal", artifact_ref: artifactRef }
46
49
  : { purpose: "account_window" });
@@ -90,23 +93,23 @@ export async function runVaultRead(backend, artifactRef, sections, options) {
90
93
  : value.status === "result_preparing"
91
94
  ? "这份已购内容仍在准备。稍后只重试同一 read,不要重新授权、购买或调用 Provider。"
92
95
  : "这份已购内容当前不可用。停止,不要重试、重新购买或绕过退款锁。",
93
- next: null, recovery: [],
96
+ next: value.status === "result_preparing" ? { command: vaultReadCommand(artifactRef, normalized, options), reason: "稍后读取同一内容及原sections;不重新授权或购买" } : null, recovery: [],
94
97
  }, outputOptions(options));
95
98
  }
96
99
  catch (error) {
97
100
  if (error instanceof HttpError && error.code === "artifact_authorization_required") {
98
101
  writeCommandEnvelope({
99
102
  status: "human_authorization_required", result: { artifact_ref: artifactRef },
100
- instruction: `这份内容需要用户单独确认读取权限。执行 next.command 生成一次官方入口;用户完成后重新运行原始 read,不要重复创建授权请求。${accessContextInstruction(options)}`,
101
- next: { command: vaultAccessCommand(artifactRef, options), reason: "创建一次内容读取授权" }, recovery: [],
103
+ instruction: `这份内容需要用户单独确认读取权限。按当前动作或待填模板生成一次官方入口;用户完成后重新运行原始 read,不要重复创建授权请求。${accessContextInstruction(options)}`,
104
+ ...vaultAccessGuidance(artifactRef, options), recovery: [],
102
105
  }, outputOptions(options));
103
106
  return;
104
107
  }
105
108
  if (error instanceof HttpError && error.code === "vault_authorization_required") {
106
109
  writeCommandEnvelope({
107
110
  status: "human_authorization_required", result: { artifact_ref: artifactRef },
108
- instruction: `账号读取授权已缺失或过期。执行 next.command 生成一次官方入口;用户完成后重新运行原始 read。${accessContextInstruction(options)}`,
109
- next: { command: vaultAccessCommand(undefined, options), reason: "创建一次账号读取授权" }, recovery: [],
111
+ instruction: `账号读取授权已缺失或过期。按当前动作或待填模板生成一次官方入口;用户完成后重新运行原始 read。${accessContextInstruction(options)}`,
112
+ ...vaultAccessGuidance(undefined, options), recovery: [],
110
113
  }, outputOptions(options));
111
114
  return;
112
115
  }
@@ -121,7 +124,7 @@ export function vaultAccessCommand(artifactRef, options) {
121
124
  const host = options.host ?? (openClaw ? "<host>" : undefined);
122
125
  if (host)
123
126
  parts.push("--host", host);
124
- const target = options.target ?? (openClaw && (!options.host || requiresTarget(options.host)) ? "<target>" : undefined);
127
+ const target = options.target ?? (((openClaw && !options.host) || (options.host && requiresTarget(options.host))) ? "<target>" : undefined);
125
128
  if (target)
126
129
  parts.push("--target", shellArgument(target));
127
130
  parts.push("--json");
@@ -132,10 +135,14 @@ export function accessContextInstruction(options) {
132
135
  ? " 将 <host> 和 <target> 替换为当前可信 OpenClaw 会话的真实值,不要照抄占位符或猜测目标。"
133
136
  : "";
134
137
  }
135
- function shellArgument(value) {
136
- if (value.startsWith("<") && value.endsWith(">"))
137
- return value;
138
- if (/^[\p{L}\p{N}._:=/-]+$/u.test(value))
139
- return value;
140
- return `'${value.replaceAll("'", `'"'"'`)}'`;
138
+ function contextFlags(options) {
139
+ return `${options.host ? ` --host ${options.host}` : ""}${options.target ? ` --target ${shellArgument(options.target)}` : ""}`;
140
+ }
141
+ function vaultReadCommand(artifact, sections, options) {
142
+ return `itpay vault read --artifact ${shellArgument(artifact)}${sections.map(section => ` --section ${shellArgument(section)}`).join("")}${contextFlags(options)} --json`;
143
+ }
144
+ export function vaultAccessGuidance(artifact, options) {
145
+ const command = vaultAccessCommand(artifact, options);
146
+ const missing = [...(options.agentType === "openclaw" && !options.host ? ["host"] : []), ...(options.host && requiresTarget(options.host) && !options.target || options.agentType === "openclaw" && !options.host && !options.target ? ["target"] : [])];
147
+ return missing.length ? { next: null, interaction: { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command, required_input: missing, executable: false } } } : { next: { command, reason: "创建一次官方只读授权入口;用户完成后恢复原reader" } };
141
148
  }
package/dist/src/main.js CHANGED
@@ -25,7 +25,7 @@ import { runCancelRefund, runGetRefund, runListRefunds, runRefund, runWatchRefun
25
25
  import { runCartAdd, runCartAddQuoteServer, runCartAddServer, runCartAbandonServer, runCartClear, runCartNext, runCartRemove, runCartRemoveServer, runCartShow, runCartShowServer, } from "./commands/cart.js";
26
26
  import { CommandContractError, errorRecoveryActions, printErrorRecovery, writeCommandEnvelope } from "./commands/guidance.js";
27
27
  import { runDocsList, runDocsShow, runDocsSearch } from "./commands/docs.js";
28
- import { runInstall } from "./commands/install.js";
28
+ import { INSTALL_AGENT_TYPES, runInstall } from "./commands/install.js";
29
29
  import { runSkillShow } from "./commands/skill.js";
30
30
  import { runNext } from "./commands/next.js";
31
31
  import { runVaultAccess, runVaultList, runVaultRead } from "./commands/vault.js";
@@ -139,17 +139,12 @@ function reportCLIError(error, contract) {
139
139
  const providerContractMismatch = error instanceof HttpError && error.code === "provider_contract_mismatch";
140
140
  const capabilityInputInvalid = error instanceof HttpError && error.code === "capability_input_invalid";
141
141
  const deviceKeyResettable = Boolean(deviceError && (deviceError.code === "agent_device_key_rotated" ||
142
- deviceError.code === "agent_device_key_conflict" ||
143
- (deviceError.enrollmentFailed && deviceError.status === 500)));
142
+ deviceError.code === "agent_device_key_conflict"));
144
143
  const deviceRecovery = deviceError ? [
145
144
  {
146
145
  command: "itpay skill show itpay --json",
147
146
  reason: "读取 ItPay 身份边界;该错误需要用户或运营恢复 Backend 登记,不能通过换类型或删除本地身份绕过",
148
147
  },
149
- ...(deviceKeyResettable ? [{
150
- command: "itpay device reset-key --confirm-key-reset --json",
151
- reason: "服务端拒绝以当前私钥完成设备登记;生成全新 Ed25519 密钥并重新登记(旧设备身份在服务端保留为孤儿,不影响新身份和额度谱系)",
152
- }] : []),
153
148
  ] : [];
154
149
  const stateRecovery = stateError ? [{
155
150
  command: "itpay skill show itpay --json",
@@ -167,10 +162,10 @@ function reportCLIError(error, contract) {
167
162
  ? "CLI 已自动续期并重试同一请求一次,仍被拒绝;停止重试,不要切换 Agent Type 或旋转身份。"
168
163
  : deviceError?.code === "agent_device_revoked"
169
164
  ? "Backend 已撤销当前 Device 登记;CLI 没有自动创建替代身份。停止重试并请用户或运营恢复登记。"
170
- : deviceKeyResettable && deviceError?.status === 500
171
- ? "服务端未能完成设备登记。先原样重试一次原命令:服务端会把已登记的同一公钥幂等挂回原设备。若仍返回 internal_error(Backend 未含该修复),执行 itpay device reset-key --confirm-key-reset 生成全新密钥后重试。"
165
+ : deviceError?.enrollmentFailed && deviceError.status === 500
166
+ ? "服务端未能完成设备登记;保持原密钥和Agent Type,说明系统故障,待服务端恢复后继续原任务。不要重置身份。"
172
167
  : deviceKeyResettable
173
- ? "服务端记录显示当前设备私钥已不再有效(已轮换或与既有登记冲突)。执行 itpay device reset-key --confirm-key-reset 生成全新密钥并重新登记。"
168
+ ? "服务端拒绝当前设备私钥登记(轮换或冲突)。停止自动恢复;换钥会放弃旧身份,旧执行访问不会迁移。仅用户或运营明确决定放弃旧身份后,按 device reset-key 帮助执行。"
174
169
  : deviceError
175
170
  ? "Device 身份验证失败;停止重试,不要切换 Agent Type、删除状态或旋转私钥。"
176
171
  : undefined;
@@ -181,7 +176,7 @@ function reportCLIError(error, contract) {
181
176
  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"),
182
177
  message: error instanceof Error ? error.message : String(error),
183
178
  },
184
- ...(requiredCLIVersion ? {
179
+ ...(commandError?.result ? { result: commandError.result } : requiredCLIVersion ? {
185
180
  result: {
186
181
  current_cli_version: CLI_VERSION,
187
182
  required_cli_version: requiredCLIVersion,
@@ -220,7 +215,7 @@ function reportCLIError(error, contract) {
220
215
  : providerRejected
221
216
  ? "告诉用户数据来源没有接受本次请求,但没有说明是输入错误,并按 result.quota 说明额度状态,然后停止。不要转述内部错误、修改输入、重试或创建新查询。"
222
217
  : capabilityInputInvalid
223
- ? "告诉用户当前输入不完整或格式不正确;数据来源尚未调用,额度没有变化。不要转述技术错误或原样重试;用户提供修正信息后继续同一次服务。"
218
+ ? "Agent按当前合同模板修正字段和JSON层级,保留用户原始需求,继续同一次服务。不要让用户修技术输入,只有真实业务歧义才询问。"
224
219
  : transportError
225
220
  ? transportError.attempts > 1
226
221
  ? "临时网络故障;CLI 已仅对可安全重放的操作完成有限自动重试,但仍未获得完整响应。按 recovery 查询同一资源的权威状态;不要创建替代 Checkout、Execution、Payment 或 Refund。"
@@ -229,6 +224,7 @@ function reportCLIError(error, contract) {
229
224
  ? "移除 ITPAY_BACKEND_URL 使用正式环境,或按当前测试目标准确设置为 https://sandbox.itpay.ai 或 https://dev.itpay.ai。不要通过切换环境规避当前错误。"
230
225
  : commandError?.instruction ?? authorizationInstruction ?? contract?.instruction ?? "检查命令参数后重试。",
231
226
  next: null,
227
+ ...(commandError?.interaction ? { interaction: commandError.interaction } : {}),
232
228
  recovery: incompatible
233
229
  ? requiredCLIVersion
234
230
  ? [distributionUpgradeAction(requiredCLIVersion)]
@@ -238,7 +234,7 @@ function reportCLIError(error, contract) {
238
234
  : backendOverrideError ? [] : commandError?.recovery ?? (stateError ? stateRecovery : lockError ? lockRecovery : deviceError ? deviceRecovery : identityRecovery ? httpRecovery : contract?.recovery ?? []),
239
235
  }, {
240
236
  ...(contract?.jsonOutput !== undefined ? { jsonOutput: contract.jsonOutput } : backendOverrideError || lockError || stateError ? { jsonOutput: process.argv.includes("--json") } : {}),
241
- output: (text) => { process.stderr.write(text); },
237
+ output: (text) => { process.stdout.write(text); },
242
238
  });
243
239
  process.exitCode = 1;
244
240
  return;
@@ -323,7 +319,7 @@ for (const action of ["login", "status"]) {
323
319
  // and non-JSON output is human-readable instead of a raw JSON dump.
324
320
  const envelope = {
325
321
  status: String(result.status ?? "error"),
326
- result: (result.result ?? result),
322
+ result: (result.result ?? Object.fromEntries(Object.entries(result).filter(([key]) => !["status", "instruction", "next", "recovery", "handoff"].includes(key)))),
327
323
  ...(result.handoff ? { handoff: result.handoff } : {}),
328
324
  instruction: typeof result.instruction === "string" ? result.instruction : "按返回状态继续。",
329
325
  next: (result.next ?? null),
@@ -422,8 +418,11 @@ deviceCmd
422
418
  .action(async (options) => {
423
419
  const config = loadConfig();
424
420
  try {
421
+ if (!config.agentType || !INSTALL_AGENT_TYPES.some(type => type === config.agentType)) {
422
+ throw new CommandContractError("agent_type_required", "supported Agent Type required before key reset", "先按真实宿主配置正式支持的Agent Type;尚未修改本地密钥。", [{ command: "itpay install --json", reason: "读取当前宿主配置" }]);
423
+ }
425
424
  if (!options.confirmKeyReset) {
426
- throw new CommandContractError("key_reset_confirmation_required", "--confirm-key-reset is required", "仅当服务端拒绝以当前私钥完成设备登记(internal_error、agent_device_key_rotated 或 agent_device_key_conflict)时使用;会放弃本地设备身份并重新登记,旧设备在服务端保留为孤儿。", [{ command: "itpay docs show identity-and-sessions --json", reason: "检查适用边界" }]);
425
+ throw new CommandContractError("key_reset_confirmation_required", "--confirm-key-reset is required", "仅在用户或运营明确决定放弃旧身份后使用;会丢弃本地私钥,旧执行访问不会迁移。internal_error 需服务端恢复,不能换钥。", [{ command: "itpay docs show identity-and-sessions --json", reason: "检查适用边界" }]);
427
426
  }
428
427
  const reset = await new DeviceAuthority({
429
428
  baseURL: config.baseURL,
@@ -437,9 +436,9 @@ deviceCmd
437
436
  private_key_preserved: false,
438
437
  server_side_device: "orphaned_under_previous_key",
439
438
  },
440
- instruction: "本地设备私钥已重置;下一次需要设备身份的命令会以全新 Ed25519 密钥重新登记并获得新的额度谱系。服务端旧设备记录保留为孤儿;如需清理请走运营流程。",
439
+ instruction: "本地设备私钥已重置;下一次需要设备身份的命令会以全新 Ed25519 密钥重新登记;这不能恢复旧身份的订单或用于刷新额度。服务端旧设备记录保留为孤儿;如需清理请走运营流程。",
441
440
  next: {
442
- command: `itpay --agent-type ${config.agentType ?? "<agent_type>"} services list --limit 1 --json`,
441
+ command: `itpay --agent-type ${config.agentType} services list --limit 1 --json`,
443
442
  reason: "用无业务写入的签名请求完成新密钥的重新登记",
444
443
  },
445
444
  recovery: [],
@@ -1402,7 +1401,7 @@ services
1402
1401
  catch (error) {
1403
1402
  reportCLIError(error, {
1404
1403
  jsonOutput: Boolean(options.json), code: "workflow_run_failed",
1405
- instruction: "按服务输入声明补齐参数;已有 execution 时继续该执行。", recovery: [],
1404
+ instruction: "本次操作失败;按已返回的具体原因和恢复动作处理。没有恢复动作时停止,不猜字段或重复创建购买。", recovery: [],
1406
1405
  });
1407
1406
  }
1408
1407
  });
@@ -1477,12 +1476,18 @@ services
1477
1476
  .argument("<service_execution_id>")
1478
1477
  .requiredOption("--capability <capability_id>")
1479
1478
  .option("--input <key=value>", "redacted input summary", collectOption, [])
1479
+ .option("--input-json <file>", "JSON object containing the complete capability input")
1480
1480
  .option("--json", "output JSON")
1481
1481
  .action(async (serviceExecutionID, options) => {
1482
1482
  const config = loadConfig();
1483
1483
  const backend = newBackendClient(config);
1484
1484
  try {
1485
- await runServicesInvoke(backend, config, serviceExecutionID, options.capability, parseKeyValueList(options.input), { jsonOutput: Boolean(options.json) });
1485
+ const recovery = [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前动作要求" }];
1486
+ if (options.inputJson && options.input.length > 0) {
1487
+ throw new CommandContractError("capability_input_invalid", "--input and --input-json cannot be combined", "嵌套输入用 --input-json <file>,扁平输入用 --input key=value,二选一;本次未调用供应商。", recovery);
1488
+ }
1489
+ const input = options.inputJson ? readInputJsonObject(options.inputJson, "capability_input_invalid", recovery) : parseKeyValueList(options.input);
1490
+ await runServicesInvoke(backend, config, serviceExecutionID, options.capability, input, { jsonOutput: Boolean(options.json) });
1486
1491
  }
1487
1492
  catch (error) {
1488
1493
  reportCLIError(error, {
@@ -1687,22 +1692,28 @@ services
1687
1692
  });
1688
1693
  services
1689
1694
  .command("read-result")
1690
- .description("Read a human-granted service result for this agent")
1695
+ .description("Read saved railway results or human-granted service content")
1691
1696
  .argument("<service_execution_id>")
1692
- .option("--snapshot <snapshot_id>", "rail.progressive.v2: read one journey from a committed planning snapshot")
1697
+ .option("--snapshot <snapshot_id>", "Smart: saved snapshot catalog; add --journey for a journey detail. Exact: saved station-pair rows without --snapshot")
1693
1698
  .option("--journey <journey_id>", "rail.progressive.v2: journey id to read")
1699
+ .option("--offset <offset>", "saved journey offset", Number, 0)
1700
+ .option("--limit <limit>", "rows per page (1-20): Exact 20, Smart 3", Number)
1701
+ .option("--all", "read every saved journey page")
1694
1702
  .option("--json", "output JSON instead of terminal text")
1695
1703
  .action(async (serviceExecutionID, options) => {
1696
1704
  const config = loadConfig();
1697
1705
  const backend = newBackendClient(config);
1698
1706
  try {
1699
- await runServicesReadResult(backend, serviceExecutionID, { jsonOutput: Boolean(options.json), snapshot: options.snapshot, journey: options.journey });
1707
+ await runServicesReadResult(backend, serviceExecutionID, { jsonOutput: Boolean(options.json), snapshot: options.snapshot, journey: options.journey,
1708
+ offset: options.offset, limit: options.limit, all: Boolean(options.all) });
1700
1709
  }
1701
1710
  catch (error) {
1702
1711
  reportCLIError(error, {
1703
1712
  jsonOutput: Boolean(options.json),
1704
- code: "agent_access_denied",
1705
- instruction: "请用户在订单页面重新授权;不要使用开发者权限绕过授权或退款锁。",
1713
+ code: error instanceof HttpError && error.code === "agent_access_denied" ? "agent_access_denied" : "service_result_read_failed",
1714
+ instruction: error instanceof HttpError && error.code === "agent_access_denied"
1715
+ ? "请用户在订单页面重新授权;不要使用开发者权限绕过授权或退款锁。"
1716
+ : "读取同一服务的已保存结果;若快照已不可读,先查看该服务当前状态和有效动作。",
1706
1717
  recovery: [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "检查交付模式和 grant 状态" }],
1707
1718
  });
1708
1719
  }
@@ -20,7 +20,7 @@ export async function renderTerminalQR(url, format) {
20
20
  });
21
21
  }
22
22
  export async function writeLocalPNG(url) {
23
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), "itpay-v3-"));
23
+ const dir = await fs.mkdtemp(path.join(process.env.ITPAY_STATE_DIR || os.tmpdir(), "itpay-v3-"));
24
24
  const filePath = path.join(dir, `itpay-${randomUUID()}.png`);
25
25
  await QRCode.toFile(filePath, url, {
26
26
  type: "png",
@@ -1,12 +1,11 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { homedir } from 'node:os';
3
2
  import { dirname, resolve } from 'node:path';
4
3
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, rmSync } from 'node:fs';
5
4
  import { writeLocalPNG } from '../render/qr.js';
6
5
  import { TaskJournal } from './task_journal.js';
7
6
  import { taskJournalPath } from './config.js';
8
7
  export function sellerAuthPath(baseURL, env = process.env, purpose = "seller") {
9
- return resolve(env.HOME || homedir(), '.itpay-v3', `${purpose}-${createHash('sha256').update(baseURL).digest('hex').slice(0, 16)}.json`);
8
+ return resolve(stateDir(env), `${purpose}-${createHash('sha256').update(baseURL).digest('hex').slice(0, 16)}.json`);
10
9
  }
11
10
  function read(baseURL, env = process.env, purpose = "seller") {
12
11
  const path = sellerAuthPath(baseURL, env, purpose);
@@ -59,11 +58,11 @@ const TERMINAL_SESSION_STATES = {
59
58
  },
60
59
  cancelled: {
61
60
  status: 'auth_cancelled',
62
- instruction: '用户在官方页取消了授权。如仍需登录,重新运行 itpay auth login。',
61
+ instruction: '用户在官方页取消了授权;停止登录。只有用户明确重新要求授权时才发起新的登录。',
63
62
  },
64
63
  denied: {
65
64
  status: 'auth_denied',
66
- instruction: '官方授权被拒绝。重新运行 itpay auth login 生成新请求。',
65
+ instruction: '官方授权被拒绝;停止登录,不自动创建新请求。只有用户明确重新要求授权时才重新登录。',
67
66
  },
68
67
  };
69
68
  class AuthRequestError extends Error {
@@ -215,7 +214,10 @@ async function accountAuth(action, baseURL, env, fetcher, backend) {
215
214
  const url = new URL(result.start_url, baseURL);
216
215
  const sameOrigin = url.origin === new URL(baseURL).origin;
217
216
  const alipay = url.origin === 'https://openauth.alipay.com' && url.pathname === '/oauth2/publicAppAuthorize.htm' && !url.username && !url.password;
218
- if (!sameOrigin && !alipay)
217
+ const local = response.headers.get('X-ItPay-Environment') === 'local_simulation'
218
+ && new URL(baseURL).protocol === 'http:' && new URL(baseURL).hostname === '127.0.0.1'
219
+ && url.protocol === 'http:' && url.hostname === '127.0.0.1' && !url.username && !url.password;
220
+ if (!sameOrigin && !alipay && !local)
219
221
  throw new Error('Unexpected authorization origin');
220
222
  const fragment = new URLSearchParams(url.hash.replace(/^#dashboard-auth\?/, ''));
221
223
  const state = url.searchParams.get('state')?.split('.');
@@ -289,7 +291,7 @@ async function accountAuth(action, baseURL, env, fetcher, backend) {
289
291
  status: terminal.status,
290
292
  result: { dashboard_auth_session_id: state.sessionID, stage: progress.status },
291
293
  instruction: terminal.instruction,
292
- next: { command: 'itpay auth login --json', reason: '重新发起官方授权' },
294
+ next: ['denied', 'cancelled'].includes(progress.status) ? null : { command: 'itpay auth login --json', reason: '需要登录时重新发起官方授权' },
293
295
  recovery: [],
294
296
  };
295
297
  }
@@ -327,3 +329,4 @@ async function accountAuth(action, baseURL, env, fetcher, backend) {
327
329
  save({ baseURL, sessionToken: token, expiresAt: session.expires_at }, env);
328
330
  return { status: 'authenticated', base_url: baseURL, expires_at: session.expires_at };
329
331
  }
332
+ import { stateDir } from "./config.js";
@@ -15,8 +15,8 @@ import { OperationJournal } from "./operation_journal.js";
15
15
  export const DEFAULT_BASE_URL = "https://app.itpay.ai";
16
16
  export const DEV_BASE_URL = "https://dev.itpay.ai";
17
17
  export const SANDBOX_BASE_URL = "https://sandbox.itpay.ai";
18
- export const CLI_VERSION = "2.1.22";
19
- export const API_CONTRACT_REVISION = "sha256:2f9fbcdc5cd0630910abb52671304cc019e348e9a778017ec92ddc3513b3639f";
18
+ export const CLI_VERSION = "2.1.33";
19
+ export const API_CONTRACT_REVISION = "sha256:80d69dbad36debbe4bdfbe468127a934c87b657e7795f369bb04de54863da476";
20
20
  const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
21
21
  const CART_SESSION_FILENAME = "cart.json";
22
22
  const OPERATION_JOURNAL_FILENAME = "operations.json";
@@ -65,8 +65,8 @@ function stateFilename(filename, baseURL) {
65
65
  const dot = filename.lastIndexOf(".");
66
66
  return dot < 0 ? `${filename}.${suffix}` : `${filename.slice(0, dot)}.${suffix}${filename.slice(dot)}`;
67
67
  }
68
- function stateDir(env) {
69
- return resolve(env.HOME || homedir(), CART_SESSION_DEFAULT_DIR);
68
+ export function stateDir(env = process.env) {
69
+ return env.ITPAY_STATE_DIR ? resolve(env.ITPAY_STATE_DIR) : resolve(env.HOME || homedir(), CART_SESSION_DEFAULT_DIR);
70
70
  }
71
71
  export function cartSessionPath(env = process.env) {
72
72
  if (env.ITPAY_CART_SESSION_PATH) {
@@ -1,6 +1,5 @@
1
1
  import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, sign, } from "node:crypto";
2
2
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
- import { homedir } from "node:os";
4
3
  import { dirname, resolve } from "node:path";
5
4
  const PROTECTED_PATHS = ["/v1/agent-device-account-bindings", "/v1/carts", "/v1/service-executions", "/v1/agent-instances", "/v1/orders", "/v1/refunds", "/v1/me", "/v1/vault", "/v1/rail/phone-links"];
6
5
  export class DeviceAuthority {
@@ -17,7 +16,7 @@ export class DeviceAuthority {
17
16
  this.backendKey = normalizeBackendKey(options.baseURL);
18
17
  this.requestedAgentType = options.requestedAgentType;
19
18
  this.compatibilityHeaders = options.compatibilityHeaders;
20
- const root = resolve(homedir(), ".itpay-v3", "device");
19
+ const root = resolve(stateDir(), "device");
21
20
  this.statePath = options.statePath ?? resolve(root, "identity.json");
22
21
  this.privateKeyPath = options.privateKeyPath ?? resolve(root, "device-private.pem");
23
22
  this.fetchImpl = (options.fetchImpl ?? globalThis.fetch).bind(globalThis);
@@ -470,3 +469,4 @@ function asDeviceStatePathError(error, operation) {
470
469
  const code = error.code;
471
470
  return code === "EEXIST" ? new DeviceStateError(operation, code) : asDeviceStateError(error, operation);
472
471
  }
472
+ import { stateDir } from "./config.js";
@@ -35,7 +35,7 @@
35
35
  "The Backend Cart is business truth; the current backend-isolated local cart state file only stores local recovery handles and explicit drafts.",
36
36
  "Run the exact next.command returned by the CLI. Do not extract or invent a capability from a raw list.",
37
37
  "Railway query and booking use distinct Service Executions for one purchase journey. Start another execution for an independent intent.",
38
- "Ask for email only when the selected capability requires protected delivery; never invent contact data.",
38
+ "Only non-rail capabilities explicitly requiring delivery email may ask its purpose and value. Railway notification contact belongs to the protected checkout page with verified account email reuse; the Agent must not guess or collect it in chat.",
39
39
  "A lost Checkout response is retried with the same Cart and persisted idempotency operation.",
40
40
  "A single railway service checkout normally follows its booking execution; Cart is used only when the returned flow calls for it."
41
41
  ],
@@ -21,7 +21,7 @@
21
21
  "service_flow": "Explain discovery as an auxiliary identity-confirmation step, including free quota and paid continuation. Present primary_service as the actual paid outcome. Read all values from the manifest.",
22
22
  "variant_selection": "Internal IDs are for the Agent's next command only and are never shown or requested from the human.",
23
23
  "price_note": "amount_minor is in minor units (e.g., 10000 = 100.00 CNY). The CLI formats prices as XX.YY currency automatically.",
24
- "railway": "Railway entries point to itpay docs show rail-booking --json; use its query and booking contracts instead of a generic start→invoke→cart sequence."
24
+ "railway": "Railway entries point to itpay docs show rail-booking; use its query and booking contracts instead of a generic start→invoke→cart sequence."
25
25
  },
26
26
  "agent_rules": [
27
27
  "Use catalog list only when the human asks for a new service, not when they want an earlier purchase.",
@@ -23,7 +23,7 @@
23
23
  "When a previously unseen Agent Type is registered under an existing Device, the CLI skips locally known Agent Instances that the Backend reports as revoked and uses the first remaining valid Instance. It never reactivates a revoked Instance or replaces the Device; if none is valid, registration stops.",
24
24
  "When the Backend reports agent_device_not_found (HTTP 404) for a locally known registration — for example after the Backend registration database was rebuilt — the CLI drops only that Backend's registration and re-enrolls once with the same private key, re-attaching to the same device and quota lineage where the Backend still holds the key.",
25
25
  "A revoked v2 registration requires explicit operator recovery; it is never silently replaced.",
26
- "Re-enrolling a public key the Backend already holds re-attaches to the existing device and quota lineage after proof-of-possession; it never creates a duplicate device. If enrollment verification keeps failing with internal_error, or returns agent_device_key_rotated or agent_device_key_conflict, `itpay device reset-key --confirm-key-reset` is the supported way to start over with a fresh key."
26
+ "Re-enrolling a public key the Backend already holds re-attaches to the existing device and quota lineage after proof-of-possession; it never creates a duplicate device. An internal_error is a server failure: preserve the key and wait for service recovery. If enrollment returns agent_device_key_rotated or agent_device_key_conflict, stop automatic recovery. Only an explicit human/operator decision to abandon the old identity permits reset-key; old execution access does not migrate."
27
27
  ],
28
28
  "local_state_recovery": [
29
29
  "Device state uses an owner-only atomic file replacement and a lock that the CLI can inspect. On a stale lock, run the returned device repair-lock command; it only reclaims a proven stale owner.",
@@ -47,11 +47,6 @@
47
47
  "command": "itpay --agent-type <agent_type> device recover --confirm-backend-reset --json",
48
48
  "success_signal": "only the selected Backend registration is removed and the private key and other Backend registration remain"
49
49
  },
50
- {
51
- "intent": "abandon the local device key after the Backend refuses to complete enrollment with it (persistent internal_error, agent_device_key_rotated, or agent_device_key_conflict)",
52
- "command": "itpay device reset-key --confirm-key-reset --json",
53
- "success_signal": "the local private key is discarded and the next command enrolls as a brand-new device"
54
- },
55
50
  {
56
51
  "intent": "repair only a proven stale local device lock",
57
52
  "command": "itpay device repair-lock --json",
@@ -103,5 +98,6 @@
103
98
  "missing": "auth_session_missing means the cached session is gone. An explicit auth login opens one fresh official session in that call; auth status stays read-only.",
104
99
  "completed_resume": "After login completes, execute the returned command to resume the same paused service execution. Login does not silently run that command.",
105
100
  "path_b": "A buyer who reaches checkout without a login completes identity inside the protected checkout page; never force a separate itpay auth login first."
106
- }
101
+ },
102
+ "key_reset_decision": "Only after explicit user/operator acceptance of abandoning the old identity, consult device reset-key help; this is not automatic error recovery, and old execution access does not migrate."
107
103
  }
@@ -41,6 +41,7 @@
41
41
  "Act as the human's ItPay service representative: explain payment, delivery, access, and refund facts in plain language before giving the next action.",
42
42
  "After verified payment, say the Order is recorded and the human must not pay again. Recover that same Order or Service Execution if delivery is delayed or fails.",
43
43
  "ItPay derives payment, amount, currency, account ownership, and refund policy from the owned order; explain only their user-facing meaning.",
44
+ "A railway order can contain 1–3 legs paid together and issued separately. Report all-issued only when every leg is issued; retain issued tickets on a partial failure or unknown result. Railway return and platform refund follow per-leg supplier facts; do not promise a full refund or create replacement tickets.",
44
45
  "Read this order’s current refund policy. Railway return depends on supplier ticket facts and may require manual review; platform refund status alone does not prove a train ticket was returned.",
45
46
  "A refund request freezes all delivery paths and revokes existing grants immediately.",
46
47
  "Cancellation or rejection follows the returned order policy and never reactivates an old grant automatically.",
@@ -92,5 +93,11 @@
92
93
  "交付失败",
93
94
  "没有收到结果",
94
95
  "能不能退"
95
- ]
96
+ ],
97
+ "rail_notifications": {
98
+ "channels": "Delivery notices reach the checkout-confirmed contact: email always works; a verified mainland SMS recipient is optional and checkout-scoped. The passenger phone, the login phone and the notification recipient are independent facts.",
99
+ "status_read": "Use the current returned reader for ticket state; order summaries do not expose per-leg notification delivery records. Do not infer a sent or delivered notification.",
100
+ "truthfulness": "Provider acceptance is not delivery; an uncertain send outcome is reconciled, not silently retried. A notification failure never changes ticket or refund state; report it as a delivery detail, not an order failure.",
101
+ "unavailable": "If SMS is not configured or a recipient is unverified, email still satisfies the contact policy — never present an unverified phone as enabled."
102
+ }
96
103
  }
@@ -30,6 +30,7 @@
30
30
  "Normal buyer flow opens the ItPay Checkout page; itpay pay and buy --pay are operator escape hatches.",
31
31
  "Use only handoff fields returned for the current Host and make them actually visible to the human.",
32
32
  "Obtain only missing required agreement or material price/term changes. Reuse a valid explicit confirmation for unchanged terms; never invent it.",
33
+ "For a railway journey, show one official checkout for all legs: one total/payment, separate issuance. The page shares its authoritative payment_deadline_at/server_now/payment_remaining_seconds; reaching zero requires reading current status, not declaring cancellation.",
33
34
  "After presenting a Checkout, stop. Run the returned next.command only after the human completes the payment action or asks to query the same Checkout.",
34
35
  "Use only the handoff fields returned for the current Host. Load render-hosts if the official handoff is not visible.",
35
36
  "Payment is verified only by Backend Checkout or Order state, never by QR rendering, redirect, or user claim.",
@@ -35,7 +35,7 @@
35
35
  "authorization_flow": [
36
36
  "Run the original list, orders, or read command.",
37
37
  "OpenClaw must pass its current trusted host and required target on the original command so the authorization continuation preserves the real presentation destination.",
38
- "If it returns human_authorization_required, run only its next.command once.",
38
+ "If it returns human_authorization_required and context is complete, run its executable next.command once. If next is null or context/consent is missing, fill the returned interaction/input_template first; never invent host, target or consent.",
39
39
  "Make the returned handoff genuinely visible and stop.",
40
40
  "After the human says authorization is complete, rerun the original read command unchanged.",
41
41
  "Never rerun vault access as a status check."
@@ -27,7 +27,7 @@
27
27
  "refund_or_paid_problem": "Resume the known Order or Refund before creating anything new.",
28
28
  "service_feedback": "Finish the original order outcome first, then submit one safe Agent postmortem; include a rating or comment only when the human explicitly gives it.",
29
29
  "ambiguous": "Ask whether the human wants an earlier purchase or a new query before calling ItPay.",
30
- "railway": "Known railway intent: itpay docs show rail-booking --json. Read once, then follow its Exact/Smart route."
30
+ "railway": "Known railway intent: itpay docs show rail-booking. Read once, then follow its Exact/Smart route."
31
31
  },
32
32
  "commands": [
33
33
  {
@@ -39,11 +39,11 @@
39
39
  "agent_rules": [
40
40
  "Use the CLI as the control surface; infer the human's intent before the first command and never hardcode a service workflow.",
41
41
  "Treat result as current facts, instruction as how to use those facts, next as the preferred executable action, and recovery as exceptional paths.",
42
- "Run one state-changing command at a time. Treat next.command as the preferred continuation: execute it unchanged after filling required user data only when the current result has not already satisfied the user's goal.",
42
+ "Run one state-changing command at a time. Treat next.command as the preferred continuation: execute it unchanged only when its required values are already present; fill a non-executable input_template separately only when the current result has not already satisfied the user's goal.",
43
43
  "Keep the same explicit Agent Type through next and recovery commands; same-type windows reuse one Backend Agent Instance.",
44
44
  "When testing sandbox, execute every returned command unchanged so its ITPAY_BACKEND_URL=https://sandbox.itpay.ai prefix is preserved.",
45
- "Use --target only for a Host presentation destination. Put capability business values in --input key=value exactly as required by the schema.",
46
- "Ask the user for required email or contact values and explain their purpose; never invent them.",
45
+ "Use --target only for a Host presentation destination. Use --input key=value only for simple scalars; nested objects use a small JSON file with the existing --input-json command.",
46
+ "For non-rail delivery that explicitly requires email, ask only that missing value and explain its purpose. Railway identity and notification contact are handled on the protected official checkout page; never guess them.",
47
47
  "Explain useful service facts in ordinary language; never ask the human to run commands or learn internal IDs.",
48
48
  "Normal payment uses the returned ItPay Checkout handoff.",
49
49
  "A railway query execution and its separate booking execution may serve one purchase intent; keep both references."
@@ -106,5 +106,6 @@
106
106
  "rating",
107
107
  "target",
108
108
  "input"
109
- ]
109
+ ],
110
+ "reading_results": "Read normal CLI text directly, or parse the complete --json envelope with the host file reader. If saving stdout to a file, first check the command exit code, then read the whole file with cat or the existing file tool. Keep stderr separate; do not use head, tail, regex or a custom script to recover truncated results. Write complex input as a small JSON file and use --input-json."
110
111
  }