@itpay/cli 2.0.24 → 2.0.26

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 (35) hide show
  1. package/README.md +3 -0
  2. package/dist/src/client/backend.js +3 -3
  3. package/dist/src/client/http.js +52 -15
  4. package/dist/src/client/transport.js +81 -0
  5. package/dist/src/commands/checkout_handoff.js +12 -3
  6. package/dist/src/commands/install.js +1 -1
  7. package/dist/src/commands/pay.js +7 -5
  8. package/dist/src/main.js +20 -4
  9. package/dist/src/state/config.js +1 -1
  10. package/docs/agent/buyer/install-and-setup.json +2 -1
  11. package/docs/agent/buyer/payment-flow.json +1 -1
  12. package/docs/agent/buyer/render-hosts.json +4 -3
  13. package/docs/cli-reference/agent-types.md +6 -4
  14. package/docs/cli-reference/commands/buy.md +4 -2
  15. package/docs/cli-reference/commands/catalog/list.md +1 -1
  16. package/docs/cli-reference/commands/checkout.md +4 -2
  17. package/docs/cli-reference/commands/device.md +35 -1
  18. package/docs/cli-reference/commands/install.md +1 -1
  19. package/docs/cli-reference/commands/order.md +1 -1
  20. package/docs/cli-reference/commands/orders.md +1 -1
  21. package/docs/cli-reference/commands/pay.md +3 -1
  22. package/docs/cli-reference/commands/readyz.md +1 -1
  23. package/docs/cli-reference/commands/refund/get.md +1 -1
  24. package/docs/cli-reference/commands/refund/watch.md +1 -1
  25. package/docs/cli-reference/commands/services/checkout.md +5 -4
  26. package/docs/cli-reference/commands/services/next.md +1 -1
  27. package/docs/cli-reference/commands/services/quote.md +1 -1
  28. package/docs/cli-reference/commands/skill.md +15 -1
  29. package/docs/cli-reference/conventions.md +34 -0
  30. package/docs/cli-reference/index.md +11 -0
  31. package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +6 -3
  32. package/docs/skill-bundle-rollout/04-first-wave-platforms.md +1 -1
  33. package/docs/skill-bundle-rollout/05-sync-operations.md +62 -0
  34. package/docs/skill-bundle-rollout/README.md +1 -0
  35. package/package.json +1 -1
package/README.md CHANGED
@@ -43,6 +43,8 @@ Normative per-command contracts: [CLI Command Reference](docs/cli-reference/inde
43
43
  | `claude-code-desktop` | `claude-code` |
44
44
  | `claude-code-cli` | `terminal` |
45
45
  | `workbuddy` | `plain-chat` |
46
+ | `kimi-code` | `terminal` |
47
+ | `openclaw` | 必须显式提供 |
46
48
 
47
49
  `--agent-type` identifies the stable runtime and registered Agent instance. Every returned ItPay command preserves it. Different windows or chats of the same type reuse one Agent Instance; they are not separate identities. `--host` only selects the human presentation surface, and `--target` only routes output to a Host destination. Use `itpay install <agent_type> --json` for the exact responsibility.
48
50
 
@@ -59,6 +61,7 @@ The local installation keeps one Ed25519 private key and a separate registration
59
61
  - `refund create/list/get/watch/cancel`: Refund Owner flow.
60
62
  - `services get/events`: redacted support diagnostics; normal flows should use `services next`.
61
63
  - `install`, `skill show`, `docs list/show/search`: offline packaged guidance.
64
+ - `device recover`: operator-confirmed Backend reset recovery that preserves the local private key.
62
65
  - `pay`: operator escape hatch only; normal buyers use the ItPay Checkout page.
63
66
 
64
67
  Run `itpay <command> --help` or browse [the command index](docs/cli-reference/index.md) for parameters.
@@ -84,16 +84,16 @@ export class BackendClient {
84
84
  return this.http.post("/v1/service-executions", input);
85
85
  }
86
86
  invokeServiceCapability(serviceExecutionID, capabilityID, input) {
87
- return this.http.post(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/capabilities/${encodeURIComponent(capabilityID)}/invoke`, input);
87
+ return this.http.post(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/capabilities/${encodeURIComponent(capabilityID)}/invoke`, input, { replaySafe: Boolean(input.idempotency_key) });
88
88
  }
89
89
  recordServiceExecutionAction(serviceExecutionID, input) {
90
90
  return this.http.post(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/actions`, input);
91
91
  }
92
92
  createServiceExecutionCheckout(serviceExecutionID, input) {
93
- return this.http.post(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/checkout`, input);
93
+ return this.http.post(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/checkout`, input, { replaySafe: true });
94
94
  }
95
95
  prepareServiceQuote(serviceExecutionID, input) {
96
- return this.http.post(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/quotes`, input);
96
+ return this.http.post(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/quotes`, input, { replaySafe: true });
97
97
  }
98
98
  getServiceExecution(serviceExecutionID) {
99
99
  return this.http.get(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}`);
@@ -1,6 +1,7 @@
1
- // Thin HTTP client for V3 backend. Keep this file boring on purpose:
2
- // no retries, no SDK abstractions, no business logic. Higher layers
3
- // own semantics (e.g. CLI commands, frontend feature hooks).
1
+ // Thin HTTP client for V3 backend. Keep retry policy transport-only: bounded
2
+ // retries are allowed only for reads or explicitly replay-safe writes.
3
+ // HTTP/business failures remain owned by higher-level commands.
4
+ import { asTransientTransportError, HttpTransportError } from "./transport.js";
4
5
  export class HttpError extends Error {
5
6
  status;
6
7
  code;
@@ -13,11 +14,13 @@ export class HttpError extends Error {
13
14
  }
14
15
  }
15
16
  export class HttpClient {
17
+ static MAX_TRANSPORT_RETRIES = 2;
16
18
  baseURL;
17
19
  fetchImpl;
18
20
  defaultHeaders;
19
21
  requestAuthorizer;
20
22
  recoverAuthorization;
23
+ transportRetryDelayMs;
21
24
  constructor(config) {
22
25
  this.baseURL = config.baseURL.replace(/\/$/, "");
23
26
  this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
@@ -28,39 +31,70 @@ export class HttpClient {
28
31
  };
29
32
  this.requestAuthorizer = config.requestAuthorizer;
30
33
  this.recoverAuthorization = config.recoverAuthorization;
34
+ this.transportRetryDelayMs = Math.max(0, config.transportRetryDelayMs ?? 200);
31
35
  }
32
36
  async request(path, options = {}) {
33
37
  const url = path.startsWith("http") ? path : this.baseURL + path;
34
38
  const method = options.method ?? "GET";
35
39
  const body = options.body !== undefined ? JSON.stringify(options.body) : "";
36
40
  const requestPath = new URL(url).pathname + new URL(url).search;
37
- for (let attempt = 0; attempt < 2; attempt += 1) {
41
+ const replaySafe = method === "GET" || Boolean(options.idempotencyKey) || options.replaySafe === true;
42
+ let authorizationRecovered = false;
43
+ let transportRetries = 0;
44
+ for (;;) {
38
45
  const headers = { ...this.defaultHeaders };
39
- if (this.requestAuthorizer) {
40
- Object.assign(headers, await this.requestAuthorizer({ method, path: requestPath, body }));
46
+ try {
47
+ if (this.requestAuthorizer) {
48
+ Object.assign(headers, await this.requestAuthorizer({ method, path: requestPath, body }));
49
+ }
50
+ }
51
+ catch (error) {
52
+ // Authorization can perform enrollment or session POSTs before the
53
+ // protected request exists. Classify their transport failure, but do
54
+ // not inherit the outer request's replay policy for those writes.
55
+ if (error instanceof HttpTransportError)
56
+ throw error;
57
+ throw asTransientTransportError(error, 1) ?? error;
41
58
  }
42
59
  if (options.bearer)
43
60
  headers.Authorization = `Bearer ${options.bearer}`;
44
61
  if (options.idempotencyKey)
45
62
  headers["Idempotency-Key"] = options.idempotencyKey;
46
- const response = await this.fetchImpl(url, {
47
- method,
48
- headers,
49
- ...(options.body !== undefined ? { body } : {}),
50
- ...(options.signal ? { signal: options.signal } : {}),
51
- });
52
- const text = await response.text();
63
+ let response;
64
+ let text;
65
+ try {
66
+ response = await this.fetchImpl(url, {
67
+ method,
68
+ headers,
69
+ ...(options.body !== undefined ? { body } : {}),
70
+ ...(options.signal ? { signal: options.signal } : {}),
71
+ });
72
+ text = await response.text();
73
+ }
74
+ catch (error) {
75
+ if (options.signal?.aborted)
76
+ throw error;
77
+ const transportError = asTransientTransportError(error, transportRetries + 1);
78
+ if (!transportError)
79
+ throw error;
80
+ if (replaySafe && transportRetries < HttpClient.MAX_TRANSPORT_RETRIES) {
81
+ transportRetries += 1;
82
+ await delay(this.transportRetryDelayMs * (2 ** (transportRetries - 1)));
83
+ continue;
84
+ }
85
+ throw asTransientTransportError(error, transportRetries + 1) ?? error;
86
+ }
53
87
  const parsed = text.length > 0 ? safeParseJson(text) : undefined;
54
88
  if (response.ok)
55
89
  return parsed;
56
90
  const error = new HttpError(response.status, parsed, `HTTP ${response.status}`);
57
- if (attempt === 0 && error.status === 401 && error.code === "agent_device_session_required" && this.recoverAuthorization) {
91
+ if (!authorizationRecovered && error.status === 401 && error.code === "agent_device_session_required" && this.recoverAuthorization) {
92
+ authorizationRecovered = true;
58
93
  await this.recoverAuthorization();
59
94
  continue;
60
95
  }
61
96
  throw error;
62
97
  }
63
- throw new Error("unreachable HTTP retry state");
64
98
  }
65
99
  get(path, options = {}) {
66
100
  return this.request(path, { ...options, method: "GET" });
@@ -72,6 +106,9 @@ export class HttpClient {
72
106
  return this.request(path, { ...options, method: "DELETE" });
73
107
  }
74
108
  }
109
+ function delay(milliseconds) {
110
+ return milliseconds === 0 ? Promise.resolve() : new Promise((resolve) => setTimeout(resolve, milliseconds));
111
+ }
75
112
  function safeParseJson(text) {
76
113
  try {
77
114
  return JSON.parse(text);
@@ -0,0 +1,81 @@
1
+ export class HttpTransportError extends Error {
2
+ code;
3
+ attempts;
4
+ causeCode;
5
+ retryable = true;
6
+ constructor(code, attempts, causeCode, cause) {
7
+ const suffix = attempts > 1 ? ` after ${attempts} attempts` : "";
8
+ super(`${transportMessage(code)} before a complete HTTP response was received${suffix}`, { cause });
9
+ this.code = code;
10
+ this.attempts = attempts;
11
+ this.causeCode = causeCode;
12
+ this.name = "HttpTransportError";
13
+ }
14
+ }
15
+ export function asTransientTransportError(error, attempts) {
16
+ if (isAbort(error))
17
+ return undefined;
18
+ const causeCode = findCauseCode(error);
19
+ const code = causeCode ? classifyCauseCode(causeCode) : classifyFetchFailure(error);
20
+ return code ? new HttpTransportError(code, attempts, causeCode, error) : undefined;
21
+ }
22
+ function classifyCauseCode(code) {
23
+ switch (code) {
24
+ case "ECONNRESET":
25
+ return "network_connection_reset";
26
+ case "ETIMEDOUT":
27
+ case "UND_ERR_CONNECT_TIMEOUT":
28
+ case "UND_ERR_HEADERS_TIMEOUT":
29
+ case "UND_ERR_BODY_TIMEOUT":
30
+ return "network_timeout";
31
+ case "EAI_AGAIN":
32
+ return "network_dns_temporary";
33
+ case "ENETUNREACH":
34
+ case "EHOSTUNREACH":
35
+ return "network_unreachable";
36
+ case "ECONNREFUSED":
37
+ return "network_connection_refused";
38
+ case "UND_ERR_SOCKET":
39
+ return "network_socket_error";
40
+ default:
41
+ return undefined;
42
+ }
43
+ }
44
+ function classifyFetchFailure(error) {
45
+ return error instanceof TypeError && error.message === "fetch failed"
46
+ ? "network_transport_failed"
47
+ : undefined;
48
+ }
49
+ function findCauseCode(error) {
50
+ const seen = new Set();
51
+ let current = error;
52
+ while (current && typeof current === "object" && !seen.has(current)) {
53
+ seen.add(current);
54
+ const code = current.code;
55
+ if (typeof code === "string" && code !== "")
56
+ return code;
57
+ current = current.cause;
58
+ }
59
+ return undefined;
60
+ }
61
+ function isAbort(error) {
62
+ return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
63
+ }
64
+ function transportMessage(code) {
65
+ switch (code) {
66
+ case "network_connection_reset":
67
+ return "network connection was reset";
68
+ case "network_timeout":
69
+ return "network connection timed out";
70
+ case "network_dns_temporary":
71
+ return "DNS lookup was temporarily unavailable";
72
+ case "network_unreachable":
73
+ return "network was unreachable";
74
+ case "network_connection_refused":
75
+ return "network connection was refused";
76
+ case "network_socket_error":
77
+ return "network socket failed";
78
+ case "network_transport_failed":
79
+ return "network transport failed";
80
+ }
81
+ }
@@ -5,16 +5,25 @@ export function shouldPrepareLocalCheckoutImage(platform) {
5
5
  export function isWorkBuddyPlainChat(agentType, platform) {
6
6
  return agentType?.trim().toLowerCase() === "workbuddy" && platform === "plain_chat";
7
7
  }
8
+ export function buildWorkBuddyPresentFilesAction(url) {
9
+ return {
10
+ tool: "present_files",
11
+ arguments: { files: [url] },
12
+ };
13
+ }
8
14
  export function buildCheckoutHandoff(input) {
9
15
  const handoff = { url: input.url };
10
- const workBuddyLinkOnly = isWorkBuddyPlainChat(input.agentType, input.platform);
16
+ const workBuddyAction = isWorkBuddyPlainChat(input.agentType, input.platform);
17
+ if (workBuddyAction) {
18
+ handoff.agent_action = buildWorkBuddyPresentFilesAction(input.url);
19
+ }
11
20
  if (input.platform === "markdown") {
12
21
  if (input.localPath)
13
22
  handoff.qr_local_path = input.localPath;
14
23
  if (input.markdown)
15
24
  handoff.markdown = input.markdown;
16
25
  }
17
- else if (input.platform === "plain_chat" && input.qrImageURL && !workBuddyLinkOnly) {
26
+ else if (input.platform === "plain_chat" && input.qrImageURL && !workBuddyAction) {
18
27
  handoff.qr_image_url = input.qrImageURL;
19
28
  }
20
29
  else if (input.platform === "telegram" && input.qrImageURL) {
@@ -30,7 +39,7 @@ export function buildCheckoutHandoff(input) {
30
39
  }
31
40
  function checkoutHandoffInstruction(agentType, platform, amount) {
32
41
  if (isWorkBuddyPlainChat(agentType, platform)) {
33
- return `Backend 尚未确认付款。向用户说明金额 ${amount},直接发送并打开 handoff.url,然后停止等待。不要调用 present_files,不要检查本地文件,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
42
+ return `Backend 尚未确认付款。立即严格按 handoff.agent_action.tool 和 handoff.agent_action.arguments 原样执行一次,在右侧打开 handoff.url;确认工具调用成功后说明金额 ${amount},然后停止等待。若工具失败,只发送原始 handoff.url,报告未自动打开并停止。不要用 present_files 打开本地文件或二维码 PNG,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
34
43
  }
35
44
  if (platform === "markdown") {
36
45
  return `Backend 尚未确认付款。把 handoff.markdown 原样发送到当前桌面对话,确认二维码、付款链接和金额 ${amount} 均已实际对用户可见,然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
@@ -15,7 +15,7 @@ const INSTRUCTIONS = {
15
15
  "codex-cli": "在 Codex CLI 中始终传这个 Agent Type;付款交接只应显示在用户可见终端。",
16
16
  "claude-code-desktop": "在 Claude Code Desktop 中始终传这个 Agent Type;付款时把返回的二维码和链接实际展示到当前对话。",
17
17
  "claude-code-cli": "在 Claude Code CLI 中始终传这个 Agent Type;付款交接只应显示在用户可见终端。",
18
- workbuddy: "在 WorkBuddy 中始终传这个 Agent Type。运行会持久化 ~/.itpay-v3 的 ItPay Bash 命令前,把 Shell 工具参数 dangerouslyDisableSandbox 设为 true;用户拒绝权限时停止,不要切换 Node、删除 identity 或处理 lock。Checkout 只返回已渲染的付款 Card Link;直接打开 handoff.url,不要调用 present_files,也不要检查本地二维码文件。",
18
+ workbuddy: "在 WorkBuddy 中始终传这个 Agent Type。运行会持久化 ~/.itpay-v3 的 ItPay Bash 命令前,把 Shell 工具参数 dangerouslyDisableSandbox 设为 true;用户拒绝权限时停止,不要切换 Node、删除 identity 或处理 lock。Checkout 返回已渲染的付款 Card Link 和 present_files action;原样执行一次 action 打开 handoff.url,不要检查本地二维码文件。",
19
19
  "kimi-code": "保持 kimi-code Agent Type,使用标准 CLI 命令和终端付款交接;不要切换到其他平台身份。",
20
20
  openclaw: "保持 openclaw Agent Type;每个展示命令都从当前可信会话上下文显式传 --host,IM 入口同时传 --target。Telegram 使用返回的原生 message action,其他入口展示标准二维码和付款链接。",
21
21
  };
@@ -2,7 +2,7 @@
2
2
  // Checkout page; this command exists for controlled integration recovery.
3
3
  import { formatMoney } from "../render/output.js";
4
4
  import { writeCommandEnvelope } from "./guidance.js";
5
- import { isWorkBuddyPlainChat } from "./checkout_handoff.js";
5
+ import { buildWorkBuddyPresentFilesAction, isWorkBuddyPlainChat } from "./checkout_handoff.js";
6
6
  import { platformKeyForHost } from "../render/plan.js";
7
7
  export async function runPay(backend, options) {
8
8
  const intent = await backend.createPaymentIntent(options.checkoutID, {
@@ -20,11 +20,13 @@ function payEnvelope(intent, options) {
20
20
  const terminal = ["failed", "expired", "refunded"].includes(intent.status);
21
21
  const verified = intent.status === "verified" || intent.status === "partially_refunded";
22
22
  const handoff = {};
23
- const workBuddyLinkOnly = isWorkBuddyPlainChat(options.agentType, platformKeyForHost(options.host));
24
- if (!terminal && !verified && workBuddyLinkOnly) {
23
+ const workBuddyAction = isWorkBuddyPlainChat(options.agentType, platformKeyForHost(options.host));
24
+ if (!terminal && !verified && workBuddyAction) {
25
25
  const url = intent.action?.mobile_wallet_url ?? intent.action?.qr_image_url;
26
- if (url)
26
+ if (url) {
27
27
  handoff.url = url;
28
+ handoff.agent_action = buildWorkBuddyPresentFilesAction(url);
29
+ }
28
30
  }
29
31
  else {
30
32
  if (!terminal && !verified && intent.action?.qr_image_url)
@@ -60,7 +62,7 @@ function payInstruction(options, verified, terminal, hasAction, amount) {
60
62
  return "Payment Intent 尚未返回可展示动作;不要猜测渠道链接,回到同一 Checkout 查询。";
61
63
  const platform = platformKeyForHost(options.host);
62
64
  if (isWorkBuddyPlainChat(options.agentType, platform)) {
63
- return `这是受控逃生入口。说明金额 ${amount},把 handoff.url 作为可点击链接发送给用户,然后停止等待。不要调用 present_files,不要立即查询、创建替代 Checkout 或 Payment Intent。`;
65
+ return `这是受控逃生入口。立即严格按 handoff.agent_action 原样执行一次,在右侧打开 handoff.url;确认工具调用成功后说明金额 ${amount} 并停止等待。若工具失败,只发送原始 handoff.url,报告未自动打开并停止。不要用 present_files 打开本地文件或二维码 PNG,不要立即查询、创建替代 Checkout 或 Payment Intent。`;
64
66
  }
65
67
  if (options.host === "codex" || options.host === "claude-code")
66
68
  return "这是受控逃生入口;把 handoff 中的二维码或钱包链接实际发到当前桌面对话,然后停止等待。";
package/dist/src/main.js CHANGED
@@ -7,6 +7,7 @@ import { DeviceAuthority, DeviceAuthorizationError, DeviceStateError } from "./s
7
7
  import { CartSession } from "./state/cart_session.js";
8
8
  import { defaultHostForAgentType, normalizeHost, validateContext } from "./state/client_context.js";
9
9
  import { HttpError } from "./client/http.js";
10
+ import { HttpTransportError } from "./client/transport.js";
10
11
  import { runReadyz } from "./commands/readyz.js";
11
12
  import { runBuy } from "./commands/buy.js";
12
13
  import { runCatalogList } from "./commands/catalog.js";
@@ -93,6 +94,7 @@ function reportCLIError(error, contract) {
93
94
  const backendOverrideError = error instanceof BackendOverrideError ? error : undefined;
94
95
  const deviceError = error instanceof DeviceAuthorizationError ? error : undefined;
95
96
  const stateError = error instanceof DeviceStateError ? error : undefined;
97
+ const transportError = error instanceof HttpTransportError ? error : undefined;
96
98
  const httpRecovery = errorRecoveryActions(error).map((action) => ({
97
99
  command: action.command,
98
100
  reason: action.reason ?? action.label,
@@ -134,7 +136,7 @@ function reportCLIError(error, contract) {
134
136
  writeCommandEnvelope({
135
137
  status: "error",
136
138
  error: {
137
- code: incompatible ? "backend_contract_incompatible" : backendOverrideError?.code ?? commandError?.code ?? (error instanceof HttpError ? error.code : stateError?.code ?? deviceError?.code ?? contract?.code ?? "command_failed"),
139
+ code: incompatible ? "backend_contract_incompatible" : backendOverrideError?.code ?? commandError?.code ?? (error instanceof HttpError ? error.code : transportError?.code ?? stateError?.code ?? deviceError?.code ?? contract?.code ?? "command_failed"),
138
140
  message: error instanceof Error ? error.message : String(error),
139
141
  },
140
142
  ...(requiredCLIVersion ? {
@@ -142,6 +144,11 @@ function reportCLIError(error, contract) {
142
144
  current_cli_version: CLI_VERSION,
143
145
  required_cli_version: requiredCLIVersion,
144
146
  },
147
+ } : transportError ? {
148
+ result: {
149
+ attempts: transportError.attempts,
150
+ automatic_retry_performed: transportError.attempts > 1,
151
+ },
145
152
  } : error instanceof HttpError && error.payload?.service_execution_id ? {
146
153
  result: {
147
154
  service_execution_id: error.payload.service_execution_id,
@@ -172,9 +179,13 @@ function reportCLIError(error, contract) {
172
179
  ? "Provider 拒绝了本次请求,但未声明这是输入错误;向用户逐字报告 error.message 和 result.quota 并停止。不要修改输入、不要重试、不要创建新 Execution。"
173
180
  : capabilityInputInvalid
174
181
  ? "输入未通过本地校验,上游尚未被调用且用户额度未变化。向用户逐字报告 error.message 并停止,不要原样重试或运行其他恢复命令。用户提供修正后的输入后,继续使用当前未结束的 Execution。"
175
- : backendOverrideError
176
- ? "移除 ITPAY_BACKEND_URL 使用正式环境,或准确设置为 https://dev.itpay.ai。"
177
- : commandError?.instruction ?? authorizationInstruction ?? contract?.instruction ?? "检查命令参数后重试。",
182
+ : transportError
183
+ ? transportError.attempts > 1
184
+ ? "临时网络故障;CLI 已仅对可安全重放的操作完成有限自动重试,但仍未获得完整响应。按 recovery 查询同一资源的权威状态;不要创建替代 Checkout、Execution、Payment Refund。"
185
+ : "网络在完整响应前中断;当前写操作没有安全重放合同,因此 CLI 未自动重试。按 recovery 查询权威状态;不要原样重放或创建替代 Checkout、Execution、Payment 或 Refund。"
186
+ : backendOverrideError
187
+ ? "移除 ITPAY_BACKEND_URL 使用正式环境,或准确设置为 https://dev.itpay.ai。"
188
+ : commandError?.instruction ?? authorizationInstruction ?? contract?.instruction ?? "检查命令参数后重试。",
178
189
  next: null,
179
190
  recovery: incompatible
180
191
  ? requiredCLIVersion
@@ -196,6 +207,11 @@ function reportCLIError(error, contract) {
196
207
  process.exitCode = 1;
197
208
  return;
198
209
  }
210
+ if (error instanceof HttpTransportError) {
211
+ process.stderr.write(`[${error.code}] ${error.message}\n`);
212
+ process.exitCode = 1;
213
+ return;
214
+ }
199
215
  if (error instanceof Error) {
200
216
  process.stderr.write(`${error.message}\n`);
201
217
  process.exitCode = 1;
@@ -12,7 +12,7 @@ import { DeviceAuthority } from "./device_authority.js";
12
12
  import { OperationJournal } from "./operation_journal.js";
13
13
  export const DEFAULT_BASE_URL = "https://app.itpay.ai";
14
14
  export const DEV_BASE_URL = "https://dev.itpay.ai";
15
- export const CLI_VERSION = "2.0.24";
15
+ export const CLI_VERSION = "2.0.26";
16
16
  export const API_CONTRACT_REVISION = "sha256:d4e94049c01b38bc77dce76bf4e49e8243adfc588541e33af6477cd5722a5bc4";
17
17
  const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
18
18
  const CART_SESSION_FILENAME = "cart.json";
@@ -37,6 +37,7 @@
37
37
  "If backend_contract_incompatible includes result.required_cli_version, stop all ItPay business commands and use only the exact distribution-specific update recovery returned by the CLI; never replace its version with latest.",
38
38
  "After upgrading, require itpay --version to equal result.required_cli_version before running readyz again; never change Agent Type or Device identity to recover compatibility.",
39
39
  "Use one exact type: codex-desktop, codex-cli, claude-code-desktop, claude-code-cli, workbuddy, kimi-code, or openclaw.",
40
+ "For compatibility, a global --agent-type codex declaration is normalized immediately to codex-desktop; all registration, output, and returned commands use codex-desktop. Never generate the alias, and do not use codex as an install target.",
40
41
  "One local private key is reused, while Device registrations and quota lineage remain separate for app.itpay.ai and dev.itpay.ai.",
41
42
  "Within each official Backend registration, each Agent Type has one Agent Instance; all windows and chats of the same type reuse it.",
42
43
  "Agent Type identifies the runtime. Host controls rendering and target only identifies a presentation destination. OpenClaw requires an explicit Host and IM target; Kimi Code uses the standard terminal CLI contract.",
@@ -44,7 +45,7 @@
44
45
  "Do not change Agent Type or rotate local identity to reset quota or recover a failed command."
45
46
  ],
46
47
  "forbidden": [
47
- "Do not use codex, terminal, claude-code, or plain-chat as Agent Types.",
48
+ "Do not use terminal, claude-code, or plain-chat as Agent Types. Do not generate codex as an Agent Type; it is accepted only as a legacy global declaration and becomes codex-desktop before any request.",
48
49
  "Do not set or suggest any Backend except the default https://app.itpay.ai or the exact test override https://dev.itpay.ai.",
49
50
  "Do not claim that install writes host configuration or registers a device; it only prints instructions.",
50
51
  "Do not create a new identity for a different window, task, chat, or process of the same Agent Type."
@@ -31,7 +31,7 @@
31
31
  "Use only handoff fields returned for the current Host and make them actually visible to the human.",
32
32
  "Before creating a single-Service Checkout, send the exact price confirmation requested by the current instruction, stop, and wait for explicit human agreement.",
33
33
  "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
- "For workbuddy with plain-chat, send or open the fully rendered Card Link in handoff.url and never call present_files or create another payment resource.",
34
+ "For workbuddy with plain-chat, execute handoff.agent_action exactly once to open the fully rendered Card Link in handoff.url; if it fails, send only the original URL and stop without creating another payment resource.",
35
35
  "For openclaw with telegram, execute the returned native message action using the current trusted target. For any other explicit OpenClaw Host, show the returned HTTPS QR image and Checkout URL.",
36
36
  "Payment is verified only by Backend Checkout or Order state, never by QR rendering, redirect, or user claim.",
37
37
  "A terminal payment state must never display another payment handoff."
@@ -33,7 +33,7 @@
33
33
  {
34
34
  "agent_type": "workbuddy",
35
35
  "default_host": "plain-chat",
36
- "responsibility": "show the amount and send or open handoff.url, which is the fully rendered HTML Card Link; never call present_files; then stop"
36
+ "responsibility": "execute handoff.agent_action exactly once to open the fully rendered handoff.url with present_files; show the amount and stop"
37
37
  },
38
38
  {
39
39
  "agent_type": "kimi-code",
@@ -63,7 +63,8 @@
63
63
  "agent_rules": [
64
64
  "Agent Type is stable identity; Host is presentation and may be explicitly overridden.",
65
65
  "A local QR path is not visible until a desktop Agent attaches or renders that file on the human-facing surface; WorkBuddy never receives a local QR path.",
66
- "For workbuddy with plain-chat, send or open only the rendered handoff.url and never call present_files, inspect files, or rebuild a QR.",
66
+ "For workbuddy with plain-chat, execute handoff.agent_action exactly once; it calls present_files with handoff.url as the only files element and opens the rendered Card Link in the right-side browser.",
67
+ "If the WorkBuddy action fails, send the original handoff.url, report that it did not auto-open, and stop without creating another payment resource.",
67
68
  "Do not claim the handoff was shown until both a usable payment image or QR and the Checkout URL are visible.",
68
69
  "IM Hosts that require a target must receive --target before Checkout creation. OpenClaw has no default Host; Kimi Code uses terminal.",
69
70
  "OpenClaw Telegram uses flat url/value buttons for deployed-version compatibility. A grant_confirmed callback carries only the Checkout ID, never a display token, and never proves payment or an active grant."
@@ -72,7 +73,7 @@
72
73
  "Do not expose display tokens separately from the tokenized Checkout handoff.",
73
74
  "Do not print base64 images, mirror lists, renderer debug state, or provider QR URLs.",
74
75
  "Do not claim a desktop attachment exists when only a filesystem path was printed.",
75
- "Do not call present_files or inspect, download, or regenerate a local QR file for WorkBuddy."
76
+ "Do not use present_files for a local file, QR PNG, downloaded copy, or rebuilt URL in WorkBuddy."
76
77
  ],
77
78
  "next_docs": [
78
79
  {
@@ -14,13 +14,14 @@
14
14
  | `codex-cli` | `terminal` | 在用户可见终端渲染二维码并输出付款链接;若用户不看该终端,要求改用正确 Host。 |
15
15
  | `claude-code-desktop` | `claude-code` | 返回桌面对话可展示的 Markdown 图片和付款链接,要求先展示再等待。 |
16
16
  | `claude-code-cli` | `terminal` | 在用户可见终端输出二维码和链接,不声称已在桌面对话展示。 |
17
- | `workbuddy` | `plain-chat` | 只返回完整渲染的 HTML Card Link;直接打开链接,不调用 `present_files`,不返回或检查本地图片路径。 |
17
+ | `workbuddy` | `plain-chat` | 返回完整渲染的 HTML Card Link 和可原样执行的 `present_files` action;立即打开 Card Link,不返回或检查本地图片路径。 |
18
18
  | `kimi-code` | `terminal` | 使用标准 CLI 引导,在用户可见终端渲染二维码和付款链接。 |
19
19
  | `openclaw` | 无;必须显式传入 | `--host telegram` 使用 OpenClaw 原生 `message` action;其他入口返回标准 HTTPS 二维码和付款链接。 |
20
20
 
21
21
  ## 通用规则
22
22
 
23
23
  - commerce 命令必须传 `--agent-type` 或设置 `ITPAY_AGENT_TYPE`。
24
+ - 兼容入口 `--agent-type codex` / `ITPAY_AGENT_TYPE=codex` 仅在 CLI 参数解析边界规范化为 `codex-desktop`;登记、输出和后续命令始终使用 `codex-desktop`。新文档和 Agent 不得主动生成该别名。`itpay install codex` 仍是无效 target。
24
25
  - Agent Type 必须真实且稳定;同类型窗口复用同一实例,不得临时换名。
25
26
  - `next.command` 和 `recovery.command` 必须保留当前显式 Agent Type,不读取或回退到机器上其他类型。
26
27
  - 显式 `--host` 覆盖默认 Host,但不改变已登记的 Agent Type。
@@ -46,7 +47,8 @@
46
47
  "url": "<checkout_or_rendered_card_url>",
47
48
  "qr_local_path": "<desktop_optional_local_path>",
48
49
  "qr_image_url": "<non_workbuddy_optional_absolute_https_png>",
49
- "markdown": "<desktop_optional_host_ready_markdown>"
50
+ "markdown": "<desktop_optional_host_ready_markdown>",
51
+ "agent_action": "<host_optional_native_action>"
50
52
  },
51
53
  "instruction": "<agent-type-specific instruction>",
52
54
  "next": {
@@ -66,11 +68,11 @@
66
68
  | `claude-code-desktop / claude-code` | `url, qr_local_path, markdown` |
67
69
  | `codex-cli / terminal` | `url`;非 JSON 输出另外渲染终端二维码 |
68
70
  | `claude-code-cli / terminal` | `url`;非 JSON 输出另外渲染终端二维码 |
69
- | `workbuddy / plain-chat` | `url`(完整渲染的 HTML Card Link) |
71
+ | `workbuddy / plain-chat` | `url, agent_action`(`present_files(files=[url])`,打开完整渲染的 HTML Card Link) |
70
72
  | `kimi-code / terminal` | `url`;非 JSON 输出另外渲染终端二维码 |
71
73
  | `openclaw / telegram` | `url, qr_image_url, agent_action` |
72
74
  | `openclaw / other` | `url, qr_image_url` |
73
75
 
74
- WorkBuddy instruction 必须直接发送并打开 `handoff.url`,明确禁止 `present_files`。这个 URL 指向 Backend 已渲染的 HTML Card;不能检查本地文件、下载或重建二维码、调用 `pay` 或创建替代付款资源。显式 `--host` 仍覆盖默认展示方式。
76
+ WorkBuddy instruction 必须要求 Agent 原样执行一次 `handoff.agent_action`,即调用 `present_files(files=[handoff.url])` 在右侧打开 Backend 已渲染的 HTML Card。调用成功后说明金额并停止;调用失败时只发送原始 `handoff.url` 并如实报告未自动打开。禁止把 `present_files` 用于本地文件或二维码 PNG,也不能下载或重建二维码、调用 `pay` 或创建替代付款资源。显式 `--host` 仍覆盖默认展示方式。
75
77
 
76
78
  OpenClaw Telegram 的 `handoff.agent_action` 是可原样执行的原生 `message` tool action。`presentation` 只包含标准 `blocks.buttons`:`📱 手机点这儿支付` 使用扁平 `url`,`📋 已授权给我读` 使用扁平 `value=itp:grant_confirmed:<checkout_id>`;二维码单独使用 action 的 `media`。CLI `instruction` 必须要求 Agent 原样执行该 action,不得改写 Presentation、换用其他消息工具或声称普通文本回复等同于已发送按钮。收到授权 callback 后立即执行 `next.command` 查询同一 Checkout,再只跟随后端返回的同一 Execution grant 流程;callback 只携带 Checkout ID,不携带 display token,也不证明付款或 grant 已生效。OpenClaw `target` 使用原生 chat target(如 `5559456744` 或 `-1001234567890:topic:42`),不添加 `telegram:` 前缀。
@@ -40,6 +40,7 @@ itpay buy \
40
40
  --require-contact <email,phone>
41
41
  --host <host>
42
42
  --target <target>
43
+ --locale <zh-CN|en>
43
44
  --qr-format <unicode|utf8|ansi|terminal>
44
45
  --qr-file <path>
45
46
  --pay
@@ -64,6 +65,7 @@ itpay buy \
64
65
  | `--require-contact` | 否 | 只接受 `email`、`phone`;缺失时先询问用户,禁止 Agent 编造。 |
65
66
  | `--host` | 条件必填 | 通常由 `--agent-type` 推导;`openclaw` 必须显式传当前入口。只改变 handoff 展示,不改变交易事实。 |
66
67
  | `--target` | 条件必填 | 要求目标会话的 IM Host 必须提供;OpenClaw 从当前可信会话上下文传入。 |
68
+ | `--locale` | 否 | Payment Card 语言,默认 `zh-CN`;可使用 `en`。只改变 Card 文案,不改变交易事实。 |
67
69
  | `--qr-format` | 否 | 非 JSON 的终端渲染选项。 |
68
70
  | `--qr-file` | 否 | 非 JSON handoff 的明确二维码文件路径。 |
69
71
  | `--pay` | 否 | 创建 Payment Intent 的集成/运维入口;普通 Agent 流程只展示 Checkout。 |
@@ -153,7 +155,7 @@ itpay buy \
153
155
  | `buy_parameter_invalid` | quantity/timeout 非正整数,或 `--no-wait` 未配 `--pay`。 | 修正参数;本次不创建资源。 |
154
156
  | `service_quote_required` | Cart 含尚未绑定 Quote 的 Service Execution。 | 原样执行返回的 `services next <id> --json`;不要绕过 Quote 输入校验。 |
155
157
  | `idempotency_conflict` | 同一幂等操作被用于不同请求。 | 保留句柄并执行 `itpay next --json`,不要换键重建。 |
156
- | `buy_failed` | 网络或未知后端错误。 | 先执行 `itpay next --json` 或 `cart next --json` 恢复现有资源。 |
158
+ | `buy_failed` | 非传输层的未知命令错误。 | 先执行 `itpay next --json` 或 `cart next --json` 恢复现有资源。传输失败使用 conventions 中的稳定 `network_*` 错误码。 |
157
159
 
158
160
  所有参数错误都必须在 HTTP 和本地 Cart 变更前被拒绝。服务 Cart 只有每条 service-backed line 都绑定有效 Quote 时才能创建 Checkout;付款后 Backend 按 Order Item 分别推进 Execution。
159
161
 
@@ -165,7 +167,7 @@ itpay buy \
165
167
  | `codex-cli` | `terminal` | `url` | 非 JSON 模式在用户可见终端渲染二维码;始终保留付款链接。 |
166
168
  | `claude-code-desktop` | `claude-code` | `url`、可用时 `qr_local_path` 和 `markdown` | 把 Markdown handoff 发到当前桌面对话,不能只输出本地路径。 |
167
169
  | `claude-code-cli` | `terminal` | `url` | 在用户可见终端展示;不能声称桌面对话已收到图片。 |
168
- | `workbuddy` | `plain-chat` | `url` | `url` 是完整渲染的 HTML Card Link;直接发送/打开,不调用 `present_files`,然后停止。 |
170
+ | `workbuddy` | `plain-chat` | `url,agent_action` | 原样执行一次 `present_files(files=[url])`,在右侧打开完整渲染的 HTML Card Link,然后停止。 |
169
171
  | `kimi-code` | `terminal` | `url` | 使用标准 CLI 非 JSON 终端二维码和链接。 |
170
172
  | `openclaw` | 必须显式 | Telegram 返回 `url,qr_image_url,agent_action`;其他入口返回 `url,qr_image_url` | Telegram 执行原生 `message` action;其他入口直接展示图片和链接。 |
171
173
 
@@ -44,4 +44,4 @@ itpay catalog list [--json]
44
44
 
45
45
  ## Agent Type / Host
46
46
 
47
- `codex-desktop`、`codex-cli`、`claude-code-desktop`、`claude-code-cli`、`workbuddy` 使用同一产品内容;只允许排版不同。
47
+ `codex-desktop`、`codex-cli`、`claude-code-desktop`、`claude-code-cli`、`workbuddy`、`kimi-code`、`openclaw` 七种 Agent Type 使用同一产品内容;只允许排版不同。
@@ -13,13 +13,15 @@
13
13
 
14
14
  ```bash
15
15
  itpay checkout [--id <checkout_id>] [--token <display_token>]
16
- [--host <host>] [--target <target>] [--json]
16
+ [--host <host>] [--target <target>] [--locale <zh-CN|en>] [--json]
17
17
  ```
18
18
 
19
19
  省略 `--id/--token` 时只能使用本机保存的一组完整句柄;不得把其他 Checkout 的 token 拼接使用。
20
20
 
21
21
  `--host` 默认由 `--agent-type` 决定;`openclaw` 必须显式传当前入口。IM Host 必须提供 `--target`。`--json` 输出机器可读合同,不内嵌二维码字符画或图片二进制。
22
22
 
23
+ `--locale` 默认 `zh-CN`,可显式使用 `en`。它只改变重新渲染的 Card 文案,不改变 Checkout、付款、授权或恢复状态。
24
+
23
25
  ## 等待付款输出
24
26
 
25
27
  ```json
@@ -71,7 +73,7 @@ token 缺失或不匹配时使用本机句柄恢复。只有请求的 Checkout
71
73
  | `codex-cli` | `url`;普通文本模式渲染终端二维码。 |
72
74
  | `claude-code-desktop` | `url, qr_local_path, markdown`;原样发送 Markdown。 |
73
75
  | `claude-code-cli` | `url`;普通文本模式渲染终端二维码。 |
74
- | `workbuddy` | 只返回 `url`;它是完整渲染的 HTML Card Link。直接打开,不调用 `present_files`,不生成本地文件。 |
76
+ | `workbuddy` | 返回 `url,agent_action`;原样执行一次 `present_files(files=[url])` 打开完整渲染的 HTML Card Link,不生成本地文件。 |
75
77
  | `kimi-code` | `url`;普通文本模式渲染标准终端二维码。 |
76
78
  | `openclaw` | Telegram 为 `url,qr_image_url,agent_action`;instruction 强制原样执行 action。`📋 已授权给我读` callback 触发同一 Checkout 查询,再由 Backend 决定是否进入 grant 读取;其他显式 Host 为 `url,qr_image_url`。 |
77
79
 
@@ -1,9 +1,11 @@
1
- # `itpay device recover`
1
+ # `itpay device` / `itpay device recover`
2
2
 
3
3
  > **Product boundary:** `itpay` is the single public CLI entry point, and `$itpay` is its user-facing Skill invocation. Under that one product entry point, the two top-level commerce actions are `buy` and `sell`: Buyer workflows are available now; Seller workflows will use the same entry point and are not implemented yet.
4
4
 
5
5
  ## 范围
6
6
 
7
+ `itpay device` 只显示该命令组的帮助并退出,不访问 Backend、不读取或修改身份。当前唯一子命令是 `recover`。
8
+
7
9
  仅在运营明确确认当前 Backend 的 Device 登记数据库已重建或清空后,删除本地该 Backend 的 v2 registration:
8
10
 
9
11
  ```bash
@@ -13,3 +15,35 @@ itpay --agent-type <agent_type> device recover --confirm-backend-reset --json
13
15
  命令只作用于当前官方 Backend 的 Device registration,并保留本地 Ed25519 私钥、Cart 和业务资源。默认是 `https://app.itpay.ai`;显式测试可使用准确的 `ITPAY_BACKEND_URL=https://dev.itpay.ai`。该命令不访问 Backend、不自动创建新身份;返回的只读 `services list` 会保留同一 Backend,是重新登记入口。
14
16
 
15
17
  缺少确认参数返回 `backend_reset_confirmation_required`。普通 session 失效由 CLI 自动续期;revoked、quota、权限或未知 Backend 故障不得使用本命令。所有 Agent Type 使用相同输入和输出合同。
18
+
19
+ ## 参数
20
+
21
+ | 参数 | 必填 | 说明 |
22
+ |---|---:|---|
23
+ | 全局 `--agent-type <agent_type>` | 是 | 当前真实且稳定的 Agent Type;必须位于 `device` 前。 |
24
+ | `--confirm-backend-reset` | 是 | 确认运营已明确判定所选 Backend 的登记数据库被重建或清空。 |
25
+ | `--json` | 否 | 输出稳定 JSON 信封;否则输出同一事实的简洁文本。 |
26
+
27
+ ## 标准输出
28
+
29
+ ```json
30
+ {
31
+ "status": "backend_registration_removed",
32
+ "result": {
33
+ "backend": "https://app.itpay.ai",
34
+ "removed_agent_types": ["codex-desktop"],
35
+ "private_key_preserved": true,
36
+ "other_backend_registrations_preserved": true
37
+ },
38
+ "instruction": "只读列出 Service Executions,以同一私钥和 Agent Type 重新登记当前 Backend;不要删除 ~/.itpay-v3 或切换运行时。",
39
+ "next": {
40
+ "command": "itpay --agent-type codex-desktop services list --limit 1 --json",
41
+ "reason": "用无业务写入的签名请求重新登记当前 Backend"
42
+ },
43
+ "recovery": []
44
+ }
45
+ ```
46
+
47
+ 当前 Backend 已无本地登记时,`status` 为 `backend_registration_absent`、`removed_agent_types=[]`,其余合同不变。文本输出只包含 Backend、registration 状态、私钥保留和其他 Backend 保留四项,不输出私钥、公钥、Device ID、Agent Instance ID、session、token 或本地文件内容。
48
+
49
+ 缺少 Agent Type 返回 `agent_type_required`;缺少确认返回 `backend_reset_confirmation_required`。其他本地恢复失败返回 `device_recovery_failed`。所有失败都必须发生在删除 registration 前;本命令不得删除整把 Device identity、Cart、operation journal 或其他 Backend 登记。
@@ -117,7 +117,7 @@ OpenClaw 额外明确没有默认入口:
117
117
  | `codex-cli` | `terminal` | 只在用户可见终端展示付款交接。 |
118
118
  | `claude-code-desktop` | `claude-code` | 桌面对话必须实际展示二维码和付款链接。 |
119
119
  | `claude-code-cli` | `terminal` | 只在用户可见终端展示付款交接。 |
120
- | `workbuddy` | `plain-chat` | 会持久化 `~/.itpay-v3` 的 Bash 调用先设 Shell 工具参数 `dangerouslyDisableSandbox=true`;拒绝权限时停止。Checkout 只发送/打开渲染后的 Card Link,不调用 `present_files`。 |
120
+ | `workbuddy` | `plain-chat` | 会持久化 `~/.itpay-v3` 的 Bash 调用先设 Shell 工具参数 `dangerouslyDisableSandbox=true`;拒绝权限时停止。Checkout 原样执行返回的 `present_files` action,打开渲染后的 Card Link |
121
121
  | `kimi-code` | `terminal` | 使用标准 CLI 流程和终端展示,不增加 Kimi 专属命令。 |
122
122
  | `openclaw` | 无 | 必须显式提供当前入口;Telegram 使用原生 action,其他入口展示标准二维码和链接。 |
123
123
 
@@ -91,4 +91,4 @@ itpay services list --json
91
91
 
92
92
  ## Agent Type / Host
93
93
 
94
- `codex-desktop`、`codex-cli`、`claude-code-desktop`、`claude-code-cli`、`workbuddy` 返回完全相同的订单事实、instruction 和 next。`order` 是状态读取命令,不构造二维码、Markdown handoff 或 Host renderer 数据。
94
+ `codex-desktop`、`codex-cli`、`claude-code-desktop`、`claude-code-cli`、`workbuddy`、`kimi-code`、`openclaw` 七种 Agent Type 返回完全相同的订单事实、instruction 和 next。`order` 是状态读取命令,不构造二维码、Markdown handoff 或 Host renderer 数据。
@@ -82,4 +82,4 @@ itpay orders [--limit <n>] [--status <status>] [--json]
82
82
 
83
83
  ## Agent Type / Host
84
84
 
85
- `codex-desktop`、`codex-cli`、`claude-code-desktop`、`claude-code-cli`、`workbuddy` 行为相同;只允许展示格式差异。
85
+ `codex-desktop`、`codex-cli`、`claude-code-desktop`、`claude-code-cli`、`workbuddy`、`kimi-code`、`openclaw` 七种 Agent Type 行为相同;只允许展示格式差异。
@@ -100,6 +100,8 @@ API 安全合同要求后端验证 display token 是该 Checkout 当前有效的
100
100
  | `codex-cli` | `terminal` | 只在用户可见终端展示渠道动作。 |
101
101
  | `claude-code-desktop` | `claude-code` | 把安全 handoff 发到当前桌面对话。 |
102
102
  | `claude-code-cli` | `terminal` | 只在用户可见终端展示渠道动作。 |
103
- | `workbuddy` | `plain-chat` | 受控逃生入口只返回一个可点击的 `handoff.url`;不得调用 `present_files`。展示后停止,不立即查询或创建替代付款。 |
103
+ | `workbuddy` | `plain-chat` | 受控逃生入口返回 `handoff.url` 和可原样执行的 `present_files` action;打开一次后停止,不立即查询或创建替代付款。 |
104
+ | `kimi-code` | `terminal` | 使用标准 CLI 终端展示渠道动作。 |
105
+ | `openclaw` | 必须显式提供 | 按显式 Host 返回安全渠道动作;IM Host 仍要求真实 target。 |
104
106
 
105
107
  Host 只改变 instruction;Payment Intent ID、金额、状态、重试语义和权限必须一致。
@@ -45,7 +45,7 @@ itpay readyz [--json]
45
45
 
46
46
  ## 异常处理
47
47
 
48
- 连接失败时返回 `backend_unavailable`,要求等待当前官方 Backend 恢复后重试同一完整命令,不得在失败时切换环境或继续下单。
48
+ 已收到 Backend 不可用响应等非传输异常时返回 `backend_unavailable`。尚未收到完整 HTTP 响应的临时传输失败使用 conventions 中对应的稳定 `network_*` 错误码;`readyz` 是 GET,按全局安全传输合同最多自动重试两次。两类错误都要求等待当前官方 Backend 恢复后重试同一完整命令,不得在失败时切换环境或继续下单。
49
49
 
50
50
  非官方 URL 返回 `backend_override_forbidden`,且不提供自动 recovery:
51
51
 
@@ -64,4 +64,4 @@ CLI 不因为退款终态自行修改订单或 grant。
64
64
 
65
65
  ## Agent Type / Host
66
66
 
67
- `codex-desktop`、`codex-cli`、`claude-code-desktop`、`claude-code-cli`、`workbuddy` 返回完全相同的退款事实、instruction 和 next。Host 不改变 Refund Owner 状态或访问锁。
67
+ `codex-desktop`、`codex-cli`、`claude-code-desktop`、`claude-code-cli`、`workbuddy`、`kimi-code`、`openclaw` 七种 Agent Type 返回完全相同的退款事实、instruction 和 next。Host 不改变 Refund Owner 状态或访问锁。
@@ -72,4 +72,4 @@ Timeout 只表示本次 CLI 等待结束,不表示退款失败:
72
72
 
73
73
  ## Agent Type / Host
74
74
 
75
- `codex-desktop`、`codex-cli`、`claude-code-desktop`、`claude-code-cli`、`workbuddy` 返回相同退款事实。Desktop 不会把每次无变化轮询发送到用户对话;Host 不改变 timeout 或退款状态。
75
+ `codex-desktop`、`codex-cli`、`claude-code-desktop`、`claude-code-cli`、`workbuddy`、`kimi-code`、`openclaw` 七种 Agent Type 返回相同退款事实。Desktop 不会把每次无变化轮询发送到用户对话;Host 不改变 timeout 或退款状态。
@@ -14,10 +14,11 @@
14
14
  ```bash
15
15
  itpay services checkout <service_execution_id> --capability <capability_id>
16
16
  [--input <key=value> ...] [--email <delivery_email>]
17
- [--host <host>] [--target <target>] [--qr-format <format>] [--qr-file <path>] [--json]
17
+ [--host <host>] [--target <target>] [--locale <zh-CN|en>]
18
+ [--qr-format <format>] [--qr-file <path>] [--json]
18
19
 
19
20
  itpay services checkout <service_execution_id> --resume
20
- [--host <host>] [--target <target>] [--json]
21
+ [--host <host>] [--target <target>] [--locale <zh-CN|en>] [--json]
21
22
  ```
22
23
 
23
24
  创建时 `--capability` 必填。最终 locked input 必须满足 capability schema:显式输入来自 `--input`,服务端也可以按已发布 contract 从当前 Execution 的已批准 action 解析输入。解析后仍缺字段时,必须在创建 Quote、Cart 或 Checkout 前失败。只有 `delivery_email_required=true` 才要求 `--email`,并必须先向用户解释邮箱用于发送可 claim 的交付链接。`--resume` 复用同一个 Checkout 并轮换 handoff token,不再索取输入或邮箱。
@@ -81,14 +82,14 @@ itpay services checkout <service_execution_id> --resume
81
82
  | `codex-cli` | `handoff={url}`;普通文本模式在用户可见终端渲染二维码。 |
82
83
  | `claude-code-desktop` | `handoff={url,qr_local_path,markdown}`;把 `handoff.markdown` 原样发送到当前桌面对话。 |
83
84
  | `claude-code-cli` | `handoff={url}`;普通文本模式在用户可见终端渲染二维码。 |
84
- | `workbuddy` | `handoff={url}`;`url` 是完整渲染的 HTML Card Link。直接发送/打开并停止,不得调用 `present_files`、检查或生成本地文件。 |
85
+ | `workbuddy` | `handoff={url,agent_action}`;原样执行一次 `present_files(files=[url])` 打开完整渲染的 HTML Card Link,然后停止;不得检查或生成本地文件。 |
85
86
  | `kimi-code` | `handoff={url}`;复用标准 CLI 终端展示。 |
86
87
  | `openclaw` | 必须显式传 Host;Telegram 还必须传 OpenClaw 原生 Target,并返回必须原样执行的 `message` action;其他入口返回标准 `url,qr_image_url`。 |
87
88
 
88
89
  WorkBuddy 的准确 instruction 语义必须完整包含:
89
90
 
90
91
  ```text
91
- Backend 尚未确认付款。向用户说明金额,直接发送并打开 handoff.url,然后停止等待。不要调用 present_files,不要检查本地文件,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。
92
+ Backend 尚未确认付款。立即严格按 handoff.agent_action.tool 和 handoff.agent_action.arguments 原样执行一次,在右侧打开 handoff.url;确认工具调用成功后说明金额,然后停止等待。若工具失败,只发送原始 handoff.url,报告未自动打开并停止。不要用 present_files 打开本地文件或二维码 PNG,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。
92
93
  ```
93
94
 
94
95
  `--locale` 默认 `zh-CN`,可显式使用 `--locale en`。语言只影响 Card 渲染,不改变 Checkout、付款或恢复状态。
@@ -207,4 +207,4 @@ itpay services get <service_execution_id> --json
207
207
 
208
208
  ## Agent Type / Host
209
209
 
210
- `codex-desktop`、`codex-cli`、`claude-code-desktop`、`claude-code-cli`、`workbuddy` 返回完全相同的状态、safe payload、instruction 和 next。本命令不渲染二维码,也不包含 Host handoff。
210
+ `codex-desktop`、`codex-cli`、`claude-code-desktop`、`claude-code-cli`、`workbuddy`、`kimi-code`、`openclaw` 七种 Agent Type 返回完全相同的状态、safe payload、instruction 和 next。本命令不渲染二维码,也不包含 Host handoff。
@@ -62,4 +62,4 @@ itpay services quote <service_execution_id> --capability <capability_id>
62
62
 
63
63
  ## Agent Type / Host
64
64
 
65
- `codex-desktop`、`codex-cli`、`claude-code-desktop`、`claude-code-cli`、`workbuddy` 返回相同 Quote 事实、instruction 和 next。本命令不显示二维码;Agent Type 只作为设备与审计上下文,不改变价格或候选规则。
65
+ `codex-desktop`、`codex-cli`、`claude-code-desktop`、`claude-code-cli`、`workbuddy`、`kimi-code`、`openclaw` 七种 Agent Type 返回相同 Quote 事实、instruction 和 next。本命令不显示二维码;Agent Type 只作为设备与审计上下文,不改变价格或候选规则。
@@ -1,4 +1,4 @@
1
- # `itpay skill show`
1
+ # `itpay skill` / `itpay skill show`
2
2
 
3
3
  > **Product boundary:** `itpay` is the single public CLI entry point, and `$itpay` is its user-facing Skill invocation. Under that one product entry point, the two top-level commerce actions are `buy` and `sell`: Buyer workflows are available now; Seller workflows will use the same entry point and are not implemented yet.
4
4
 
@@ -6,12 +6,26 @@
6
6
 
7
7
  读取 npm 包内置的完整 ItPay Agent Skill。与按 topic 渐进读取的 `docs` 不同,本命令故意一次返回完整 `SKILL.md`,用于首次 onboarding 和身份/session 规则恢复;不访问 Backend,不修改宿主配置或本地身份。
8
8
 
9
+ `itpay skill` 只显示该命令组的帮助并退出,不读取 Skill 内容。
10
+
9
11
  ```bash
10
12
  itpay [--agent-type <agent_type>] skill show itpay [--json]
11
13
  ```
12
14
 
13
15
  当前只内置 `itpay`。该 Skill 是 Buyer 与未来 Seller 的共同入口,不再按角色拆分名称。`--json` 时完整 Markdown 位于 `result.content`;文本模式直接输出完整内容。
14
16
 
17
+ ```json
18
+ {
19
+ "status": "shown",
20
+ "result": { "skill": "itpay", "content": "<complete_packaged_SKILL.md>" },
21
+ "instruction": "完整读取并遵守 Skill;先如实选择当前运行环境对应的 Agent Type。",
22
+ "next": { "command": "itpay install --json", "reason": "选择真实且稳定的 Agent Type" },
23
+ "recovery": []
24
+ }
25
+ ```
26
+
27
+ 已声明 Agent Type 时 instruction 确认规范类型,`next.command` 保留该类型并指向 `catalog list --json`。除完整已发布 Skill 内容外,不得附加本地路径、安装目录、环境变量、Device 状态或 Backend 数据。
28
+
15
29
  未声明 Agent Type 时,`next` 是 `itpay install --json`。已声明时,`next` 是保留同一类型的 `catalog list --json`。未知名称返回 `skill_not_found`;包内文件缺失或损坏返回 `skill_unavailable` 并要求重装同版本 CLI。
16
30
 
17
31
  Skill 是操作和安全合同,不是服务端业务状态。执行时仍以每个命令当前 envelope 的 `result`、`instruction` 和 `next` 为准。
@@ -82,6 +82,40 @@ next: <one command>
82
82
  - `provider_input_rejected` 只表示 Provider 明确声明输入无效;`provider_contract_mismatch` 表示响应无法按已发布契约解释,绝不能归咎于用户输入。两者都必须停止且没有自动 recovery。
83
83
  - `backend_contract_incompatible` 只有在 Backend 返回合法 `minimum_cli_version` 时才能提供升级 recovery。npm 分发返回精确的 `npm install -g @itpay/cli@<version>`;平台 bundle 返回该平台的 Skill/plugin 更新动作。不得使用 `latest`、解析 message 猜版本或继续任何业务命令;升级后必须先用 `itpay --version` 核对完全一致,再重新运行 `readyz`。
84
84
 
85
+ ## 网络传输失败与自动重试
86
+
87
+ CLI 只把“尚未收到完整 HTTP 响应”的临时传输异常映射为稳定错误码:
88
+
89
+ - `network_connection_reset`
90
+ - `network_timeout`
91
+ - `network_dns_temporary`
92
+ - `network_unreachable`
93
+ - `network_connection_refused`
94
+ - `network_socket_error`
95
+ - `network_transport_failed`
96
+
97
+ 所有命令的 JSON 错误继续使用标准错误外壳,并额外返回:
98
+
99
+ ```json
100
+ {
101
+ "status": "error",
102
+ "error": { "code": "network_connection_reset", "message": "<bounded_transport_fact>" },
103
+ "result": { "attempts": 3, "automatic_retry_performed": true },
104
+ "instruction": "临时网络故障;CLI 已仅对可安全重放的操作完成有限自动重试,但仍未获得完整响应。按 recovery 查询同一资源的权威状态;不要创建替代 Checkout、Execution、Payment 或 Refund。",
105
+ "next": null,
106
+ "recovery": [{ "command": "itpay <same-resource-read-command>", "reason": "读取同一资源的权威状态" }]
107
+ }
108
+ ```
109
+
110
+ 重试合同必须同时满足以下边界:
111
+
112
+ - `GET`、携带稳定 `Idempotency-Key` 的写请求,以及 Backend 明确保证事务性安全重放的操作,使用短递增退避,最多自动重试两次(总共三次尝试)。
113
+ - 不具备上述合同的写请求绝不自动重放;错误中为 `attempts=1`、`automatic_retry_performed=false`,Agent 必须先查询同一资源状态。
114
+ - 每次尝试重新生成 Device 签名;不得复用过期时间、nonce 或 Authorization header。
115
+ - 首次 Device enrollment、Agent Instance 登记或 session challenge/verify 在生成请求签名前发生。它们的传输失败同样映射为稳定 `network_*` 错误,但这些内部 POST 没有外层业务请求的安全重放合同,因此不自动重试,并返回 `attempts=1`、`automatic_retry_performed=false`。Agent 修复网络后重跑原始命令,由 Device Authority 从已持久化状态安全恢复。
116
+ - `AbortSignal` 主动取消、证书/TLS 信任错误、已收到的 HTTP/业务错误和 Provider 业务失败不属于该重试。
117
+ - 稳定输出不得包含 Node/Undici cause、socket、DNS 地址、请求 header、token、签名 URL 或 Provider 原始响应。
118
+
85
119
  ## Instruction 模板
86
120
 
87
121
  Instruction 只回答当前最重要的一件事:
@@ -14,6 +14,16 @@
14
14
  - 所有 commerce 命令必须使用真实的 `--agent-type`,不得为刷新额度伪造类型。
15
15
  - 每条命令只返回当前步骤所需事实、一条 instruction、一个首选 next;异常时最多返回两个 recovery。
16
16
 
17
+ ## 根命令
18
+
19
+ ```bash
20
+ itpay [--agent-type <agent_type>] [--version] [--help]
21
+ ```
22
+
23
+ 不带子命令时显示帮助并退出,不访问 Backend、不读取或修改业务状态。`--version` 只输出当前 CLI 版本;`--help` 只输出命令树。全局 `--agent-type` 必须放在子命令前,并由每个返回的 ItPay `next.command` / `recovery.command` 原样保留其规范值。
24
+
25
+ Commander 自动提供的 `itpay help [command]` 与 `itpay <group> help [subcommand]` 只显示对应帮助,语义等同于在目标命令上使用 `--help`;它们不访问 Backend、不读写本地业务状态,也没有 JSON 输出合同。
26
+
17
27
  ## 命令目录
18
28
 
19
29
  ### 环境与发现
@@ -24,6 +34,7 @@
24
34
  - [`itpay catalog list`](commands/catalog/list.md)
25
35
  - [`itpay install`](commands/install.md) - 查看指定 Agent 的安装说明
26
36
  - [`itpay skill show`](commands/skill.md) - 一次读取完整内置 ItPay Skill
37
+ - [`itpay device recover`](commands/device.md) - 仅恢复运营已确认重建的 Backend registration
27
38
  - [`itpay docs`](commands/docs/index.md)
28
39
  - [`itpay docs list`](commands/docs/list.md)
29
40
  - [`itpay docs show`](commands/docs/show.md)
@@ -213,7 +213,9 @@ bundle 不包含凭据、.env、~/.itpay-v3 或 npm token
213
213
 
214
214
  ### Step 4:CLI 发布后创建同步 PR
215
215
 
216
- CLI 主仓库在 `main` 提供 reusable workflow。各平台仓库每小时错峰运行 caller workflow,并可手动触发;npm `dist-tags.latest` 或请求的 bundle format 与当前 `bundle.lock.json` 不同时更新。平台仓库自身的 `GITHUB_TOKEN` 写入本仓库,因此不需要跨仓 PAT,也不会在 CLI 发布失败时提前同步未发布版本。
216
+ CLI 主仓库在 `main` 提供 reusable workflow。各平台仓库每小时错峰运行 caller workflow,并可手动触发;npm `dist-tags.latest` 或请求的 bundle format 与当前 `bundle.lock.json` 不同时更新。同步优先使用只安装到分发仓库、只拥有 Contents/Pull requests 写权限的 `itpay-bundle-sync` GitHub App 短期 token;未配置 App 时回落到平台仓库自己的 `GITHUB_TOKEN`,但该模式创建的 PR checks 需要仓库写权限用户批准。禁止使用个人 PAT。
217
+
218
+ 同步决策同时读取 main 和当前版本的 open automation PR。目标版本、format 和 bundle directory 已存在于 open PR 时必须返回 `pr-current`,不得每小时重建、提交或 force-push 同一产物。
217
219
 
218
220
  检测到新版本后:
219
221
 
@@ -221,8 +223,9 @@ CLI 主仓库在 `main` 提供 reusable workflow。各平台仓库每小时错
221
223
  - 更新 manifest 版本、lock、changelog;
222
224
  - 跑全套测试;
223
225
  - 对启用 Skill 差异跟踪的平台,比较旧、新 `sourceGitSha` 的中心 `skills/itpay/SKILL.md`;有差异时创建 Draft PR、附 diff 和人工合并清单,但不覆盖平台 Skill;
224
- - 创建或刷新 `automation/itpay-cli-X.Y.Z` 分支和同步 PR;
225
- - PR 描述列出 CLI commit、integrity、平台测试和是否需要商店重新审核。
226
+ - 创建或刷新 `automation/itpay-cli-X.Y.Z` 分支和同步 PR;同版本的后续计划任务必须为 no-op
227
+ - PR 描述列出 CLI commit、integrity、dependency lock、同步 run、平台测试和是否需要商店重新审核;
228
+ - 新版本 PR 验证成功后,只关闭没有人工提交的旧机器人同步 PR;保留远程分支用于审计和恢复。
226
229
 
227
230
  同步 workflow 只开 PR,不合并、不打 tag、不发布平台商店版本。
228
231
 
@@ -21,7 +21,7 @@
21
21
 
22
22
  ## CLI 更新机制
23
23
 
24
- 四个平台仓库每小时错峰检查 npm `@itpay/cli` 的正式版。发现版本高于各自 `bundle.lock.json` 后,调用 CLI `main` 上的统一 reusable workflow,重建对应格式的 bundle、运行仓库测试,并以平台仓库自己的 `GITHUB_TOKEN` 创建更新 PR。该流程不需要 PAT,也不会自动合并或发布商店版本。
24
+ 五个平台仓库每小时错峰检查 npm `@itpay/cli` 的正式版。发现版本高于各自 `bundle.lock.json` 后,调用 CLI `main` 上的统一 reusable workflow,重建对应格式的 bundle并运行仓库测试。更新 PR 优先由最小权限 `itpay-bundle-sync` GitHub App 创建;仓库未配置 App 时回落到 `GITHUB_TOKEN` 并明确要求人工批准 PR checks。该流程禁止个人 PAT,不会自动合并或发布商店版本,同一个 CLI 版本已有有效 open PR 时不会重复 force-push。
25
25
 
26
26
  ## 已发现的现有资产
27
27
 
@@ -0,0 +1,62 @@
1
+ # Bundle 同步运维与事故处理
2
+
3
+ ## 正常状态
4
+
5
+ 每次同步必须在 Job Summary 中给出 published version、automation branch、同步决策、凭据模式、PR URL 和关闭的 superseded PR 数量。
6
+
7
+ 同步决策只有以下正常结果:
8
+
9
+ - `base-current`:main 已经包含目标版本,无需更新。
10
+ - `pr-current`:当前版本的 open PR 已包含正确 bundle,不得产生新 commit。
11
+ - `update-required`:目标版本不存在,需要构建并创建 PR。
12
+ - `open-pr-artifact-missing`:PR 分支缺少 lock,需要重新构建。
13
+ - `open-pr-artifact-mismatch`:PR 中的 version、format 或 bundle directory 不符合 caller 合同,需要重新构建。
14
+
15
+ ## 失败分类
16
+
17
+ ### GitHub Actions 启动故障
18
+
19
+ 以下证据表示仓库代码没有运行:
20
+
21
+ - run 的 `steps` 为空;
22
+ - 失败发生在 `Set up job`;
23
+ - `Failed to resolve action download info`;
24
+ - Hosted Runner 长时间 queued 后被 cancelled 或 timed out。
25
+
26
+ 先查看 GitHub Status。官方确认恢复后重跑原 SHA;不得为了这种故障修改 bundle、测试或业务代码。
27
+
28
+ ### Bundle 构建或测试故障
29
+
30
+ 以下步骤失败才属于仓库或产物问题:
31
+
32
+ - Resolve published version
33
+ - Rebuild bundle
34
+ - Inspect upstream Skill changes
35
+ - Verify platform bundle
36
+
37
+ 保留失败分支和日志,不创建或更新 PR,不跳过 `npm test`。
38
+
39
+ ### `action_required`
40
+
41
+ 由 `GITHUB_TOKEN` 创建或更新的 PR workflow 会进入 approval-required 状态。这不是测试失败,但测试尚未执行。仓库写权限用户可以批准该 run;要求零人工同步时,必须配置最小权限 GitHub App,禁止使用个人 PAT。
42
+
43
+ ## 重跑规则
44
+
45
+ 1. 只重跑仍指向当前 head SHA 的基础设施失败。
46
+ 2. 新 commit 已存在时不重跑旧 SHA。
47
+ 3. 同一个版本第二次同步必须保持 automation PR head SHA 不变。
48
+ 4. 不把 cancelled、action-required 或旧 SHA 的 success 当作当前版本通过。
49
+
50
+ ## Superseded PR
51
+
52
+ 新版本 PR 建立并通过平台 `npm test` 后,可以关闭旧机器人同步 PR。自动关闭只允许处理带同步标记、分支符合 `automation/itpay-cli-X.Y.Z`、且所有提交均由 `itpay-bundle-bot` 产生的 PR。检测到人工提交时必须保留并在 summary 中提示。关闭时保留远程分支,不自动删除历史。
53
+
54
+ ## GitHub App 权限
55
+
56
+ `itpay-bundle-sync` 只安装到分发仓库,权限限定为:
57
+
58
+ - Metadata: read
59
+ - Contents: read/write
60
+ - Pull requests: read/write
61
+
62
+ App 不需要 Actions、Secrets、Administration、Deployments 或 Packages 写权限。私钥只存于组织 Actions secrets,通过 reusable workflow 生成当前仓库、当前 job 有效的短期 token。
@@ -16,6 +16,7 @@
16
16
  - [任务二:多平台 Bundle Skill 仓库与同步](./02-platform-bundle-repositories.md)
17
17
  - [各平台制作、验证和上传手册](./03-platform-publishing.md)
18
18
  - [首批平台执行状态(2026-07-22)](./04-first-wave-platforms.md)
19
+ - [Bundle 同步运维与事故处理](./05-sync-operations.md)
19
20
 
20
21
  ## 已确认的当前状态
21
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itpay/cli",
3
- "version": "2.0.24",
3
+ "version": "2.0.26",
4
4
  "description": "The single ItPay CLI entry point for buy workflows and future sell workflows.",
5
5
  "type": "module",
6
6
  "bin": {