@itpay/cli 2.1.33 → 2.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/src/client/backend.js +5 -0
  2. package/dist/src/commands/aftercare.js +12 -0
  3. package/dist/src/commands/buy.js +12 -56
  4. package/dist/src/commands/cart.js +2 -0
  5. package/dist/src/commands/catalog.js +24 -9
  6. package/dist/src/commands/checkout_handoff.js +11 -99
  7. package/dist/src/commands/guidance.js +1 -184
  8. package/dist/src/commands/install.js +2 -1
  9. package/dist/src/commands/order.js +20 -2
  10. package/dist/src/commands/rail_catalog.js +1 -261
  11. package/dist/src/commands/readyz.js +2 -2
  12. package/dist/src/commands/refund.js +10 -54
  13. package/dist/src/commands/services/actions.js +133 -0
  14. package/dist/src/commands/services/commerce.js +197 -0
  15. package/dist/src/commands/services/lifecycle.js +418 -0
  16. package/dist/src/commands/services/results.js +130 -0
  17. package/dist/src/commands/services.js +14 -2584
  18. package/dist/src/commands/skill.js +3 -0
  19. package/dist/src/main.js +82 -31
  20. package/dist/src/render/checkout_handoff.js +101 -0
  21. package/dist/src/render/checkout_plan.js +47 -0
  22. package/dist/src/services/adapter.js +1 -0
  23. package/dist/src/services/presentation.js +198 -0
  24. package/dist/src/services/rail/actions.js +51 -0
  25. package/dist/src/services/rail/adapter.js +27 -0
  26. package/dist/src/services/rail/aftercare.js +45 -0
  27. package/dist/src/services/rail/catalog.js +261 -0
  28. package/dist/src/services/rail/checkout.js +19 -0
  29. package/dist/src/services/rail/fulfillment.js +76 -0
  30. package/dist/src/services/rail/guide.js +110 -0
  31. package/dist/src/services/rail/invocation.js +54 -0
  32. package/dist/src/services/rail/page.js +76 -0
  33. package/dist/src/services/rail/pagination.js +6 -0
  34. package/dist/src/services/rail/query.js +71 -0
  35. package/dist/src/services/rail/readers.js +107 -0
  36. package/dist/src/services/rail/reading.js +125 -0
  37. package/dist/src/services/rail/recovery.js +51 -0
  38. package/dist/src/services/rail/results.js +378 -0
  39. package/dist/src/services/rail/review.js +268 -0
  40. package/dist/src/services/rail/saved.js +49 -0
  41. package/dist/src/services/rail/wait.js +11 -0
  42. package/dist/src/services/registry.js +31 -0
  43. package/dist/src/services/schema.js +21 -0
  44. package/dist/src/services/shared/envelope.js +184 -0
  45. package/dist/src/services/shared/input.js +39 -0
  46. package/dist/src/services/shared/pagination.js +28 -0
  47. package/dist/src/services/shared/platform.js +365 -0
  48. package/dist/src/services/shared/refund.js +40 -0
  49. package/dist/src/services/shared/workflow.js +28 -0
  50. package/dist/src/state/account_auth.js +25 -10
  51. package/dist/src/state/config.js +2 -2
  52. package/dist/src/state/device_authority.js +5 -3
  53. package/docs/agent/buyer/cart-checkout.json +2 -1
  54. package/docs/agent/buyer/catalog-list.json +6 -4
  55. package/docs/agent/buyer/identity-and-sessions.json +3 -2
  56. package/docs/agent/buyer/install-and-setup.json +8 -3
  57. package/docs/agent/buyer/orders-refunds.json +14 -5
  58. package/docs/agent/buyer/payment-flow.json +2 -1
  59. package/docs/agent/buyer/quickstart.json +3 -2
  60. package/docs/agent/buyer/rail-booking.json +41 -27
  61. package/docs/agent/buyer/render-hosts.json +1 -1
  62. package/docs/agent/buyer/service-feedback.json +2 -2
  63. package/docs/cli-reference/commands/aftercare.md +15 -0
  64. package/docs/cli-reference/commands/cart/add.md +4 -0
  65. package/docs/cli-reference/commands/cart/next.md +4 -0
  66. package/docs/cli-reference/commands/readyz.md +2 -50
  67. package/docs/cli-reference/commands/services/checkout.md +4 -0
  68. package/docs/cli-reference/commands/services/next.md +8 -35
  69. package/docs/cli-reference/commands/services/quote.md +1 -1
  70. package/docs/cli-reference/commands/services/run.md +8 -12
  71. package/docs/cli-reference/commands/services/start.md +3 -1
  72. package/docs/cli-reference/conventions.md +1 -1
  73. package/docs/cli-reference/index.md +2 -0
  74. package/package.json +4 -3
  75. package/skills/itpay/SKILL.md +53 -19
@@ -103,6 +103,11 @@ export class BackendClient {
103
103
  readBuyerVaultArtifact(artifactRef, sections) {
104
104
  return this.http.post(`/v1/vault/artifacts/${encodeURIComponent(artifactRef)}/reads`, sections.length ? { sections } : {});
105
105
  }
106
+ listRailAftercare(orderID) { return this.http.get(`/v1/orders/${encodeURIComponent(orderID)}/rail-aftercare`); }
107
+ createRailAftercare(orderID, input, idempotencyKey) { return this.http.post(`/v1/orders/${encodeURIComponent(orderID)}/rail-aftercare`, input, { idempotencyKey }); }
108
+ getRailAftercare(id) { return this.http.get(`/v1/rail-aftercare/${encodeURIComponent(id)}`); }
109
+ respondRailAftercare(id, input) { return this.http.post(`/v1/rail-aftercare/${encodeURIComponent(id)}/respond`, input); }
110
+ cancelRailAftercare(id, input) { return this.http.post(`/v1/rail-aftercare/${encodeURIComponent(id)}/cancel`, input); }
106
111
  // --- Refund ---
107
112
  createRefund(orderID, input, bearer, idempotencyKey) {
108
113
  const options = { ...(bearer ? { bearer } : {}), ...(idempotencyKey ? { idempotencyKey } : {}) };
@@ -0,0 +1,12 @@
1
+ import { serviceAdapter } from "../services/registry.js";
2
+ import { CommandContractError } from "../services/shared/envelope.js";
3
+ function adapter(identity) {
4
+ const capability = serviceAdapter(identity).aftercare;
5
+ if (!capability)
6
+ throw new CommandContractError("aftercare_unsupported", "Locked service does not support this aftercare operation", "使用原服务声明的售后入口,不套用其他业务规则。", []);
7
+ return capability;
8
+ }
9
+ export async function runAftercareList(backend, orderID) { const value = await backend.listRailAftercare(orderID); return adapter(value.execution_identity).directory(value, orderID); }
10
+ export async function runAftercareGet(backend, id) { const value = await backend.getRailAftercare(id); return adapter(value.execution_identity).present(value); }
11
+ export async function runAftercareCreate(backend, config, orderID, input) { const value = await backend.listRailAftercare(orderID); return adapter(value.execution_identity).create(backend, config, orderID, input); }
12
+ export async function runAftercareRespond(backend, id, input, cancel = false) { const value = await backend.getRailAftercare(id); return adapter(value.execution_identity).respond(backend, id, input, cancel); }
@@ -1,13 +1,4 @@
1
- // V3 `buy` flow:
2
- // 1. validate client context (host/target)
3
- // 2. create cart via POST /v1/carts
4
- // 3. create checkout via POST /v1/checkouts (consumes the cart)
5
- // 4. build a checkout_qr render plan + dispatch to host renderer
6
- // 5. [--pay] create payment intent → render payment QR → optionally SSE wait
7
- // 6. [--json] output JSON instead of terminal text
8
- //
9
- // The display token is passed to the renderer and persisted in the local cart
10
- // session so a restarted agent can resume the same checkout.
1
+ import { buildCheckoutQRPlan } from "../render/checkout_plan.js";
11
2
  import { operationID } from "../state/config.js";
12
3
  import { validateContext } from "../state/client_context.js";
13
4
  import { dispatchRender } from "../render/index.js";
@@ -17,8 +8,7 @@ import { localizeCardURL, normalizeCardLocale } from "../render/locale.js";
17
8
  import { buildAgentChatHandoff } from "../render/markdown.js";
18
9
  import { formatMoney } from "../render/output.js";
19
10
  import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
20
- import { buildCheckoutHandoff, isLinkOnlyBrowserAgent, shouldPrepareLocalCheckoutImage } from "./checkout_handoff.js";
21
- import { qualifyItPayCommand } from "../state/agent_type.js";
11
+ import { assertCheckoutPayable, buildCheckoutHandoff, shouldPrepareLocalCheckoutImage } from "./checkout_handoff.js";
22
12
  export async function runBuy(backend, config, options) {
23
13
  const err = validateContext(options.host, options.target);
24
14
  if (err) {
@@ -62,6 +52,15 @@ export async function runBuy(backend, config, options) {
62
52
  ...(options.contact ? { delivery_contact: options.contact } : {}),
63
53
  };
64
54
  const checkout = await backend.createCheckout(checkoutRequest, idempotencyKey);
55
+ const authoritative = checkout.display_token
56
+ ? await backend.getCheckoutPresentation(checkout.checkout.checkout_id, checkout.display_token)
57
+ : undefined;
58
+ assertCheckoutPayable(checkout, authoritative, cart.cart_id, latestLine?.service_execution_id);
59
+ const current = authoritative?.checkout ?? checkout.checkout;
60
+ // Only the returned checkout's items are authoritative for the handoff.
61
+ if (!authoritative)
62
+ throw new Error("checkout presentation unavailable");
63
+ cart = { ...cart, items: authoritative.items, amount_minor: current.amount_minor, currency: current.currency };
65
64
  options.cartSession.rememberCheckout({
66
65
  checkoutID: checkout.checkout.checkout_id,
67
66
  displayToken: checkout.display_token,
@@ -245,50 +244,6 @@ async function waitForPaymentSSE(backend, checkoutID, displayToken, timeoutSec)
245
244
  });
246
245
  });
247
246
  }
248
- // --- checkout QR plan ---
249
- export function buildCheckoutQRPlan(input) {
250
- const summary = input.localSimulation ? "在本机打开模拟付款入口;铁路与地图查询真实,授权、通知和交易模拟,不会扣款。" : `Scan the QR or open ${input.checkoutURL} to start the human checkout flow.`;
251
- const isPayment = input.paymentIntentID != null;
252
- const afterCommand = qualifyItPayCommand(`itpay checkout --id ${input.checkoutID} --token ${input.displayToken}${input.locale === "en" ? " --locale en" : ""} --json`, input.agentType);
253
- const platform = {
254
- text: summary,
255
- links: [
256
- { label: "打开付款页面", url: input.checkoutURL },
257
- ],
258
- buttons: [
259
- { label: "打开收银台", kind: "url", url: input.checkoutURL },
260
- ...(input.checkoutID
261
- ? [{ label: "查询 Checkout 状态", kind: "callback", intent: "check_checkout_status", ref: input.checkoutID }]
262
- : []),
263
- ],
264
- blocks: [],
265
- ...(input.qrPNGURL ? { media: [{ url: input.qrPNGURL, label: "Branded QR", mimeType: "image/png" }] } : {}),
266
- };
267
- const plan = {
268
- kind: isPayment ? "payment_qr" : "checkout_qr",
269
- ...(input.localSimulation ? { localSimulation: true } : {}),
270
- host: input.host,
271
- summary,
272
- url: input.qrPayload,
273
- ...(isLinkOnlyBrowserAgent(input.agentType, platformKeyForHost(input.host)) && input.cardURL ? { linkOnlyURL: input.cardURL } : {}),
274
- preferredQRSources: [input.qrPNGURL ?? input.qrPayload],
275
- checkoutID: input.checkoutID,
276
- platform,
277
- afterActionCommand: afterCommand,
278
- afterActionLabel: "扫码或点击链接完成支付后,执行以下命令查询状态:",
279
- };
280
- if (input.orderItems)
281
- plan.orderItems = input.orderItems;
282
- if (input.orderCurrency)
283
- plan.orderCurrency = input.orderCurrency;
284
- if (input.paymentMethod)
285
- plan.paymentMethod = input.paymentMethod;
286
- if (input.paymentStatus)
287
- plan.paymentStatus = input.paymentStatus;
288
- if (input.paymentIntentID)
289
- plan.paymentIntentID = input.paymentIntentID;
290
- return plan;
291
- }
292
247
  function fallbackCardURL(baseURL, checkoutID, displayToken) {
293
248
  const root = baseURL.replace(/\/$/, "");
294
249
  return `${root}/v1/checkouts/${encodeURIComponent(checkoutID)}/card?display_token=${encodeURIComponent(displayToken)}`;
@@ -330,3 +285,4 @@ function absolutePublicURL(baseURL, value) {
330
285
  return value;
331
286
  }
332
287
  }
288
+ export { buildCheckoutQRPlan } from "../render/checkout_plan.js";
@@ -117,6 +117,8 @@ export async function runCartAddQuoteServer(options) {
117
117
  status: "quote_added",
118
118
  result: {
119
119
  cart_id: cart.cart_id,
120
+ service_quote_lock_id: options.serviceQuoteLockID,
121
+ service_execution_id: line.service_execution_id,
120
122
  item_count: cart.items.length,
121
123
  total: formatMoney(cart.amount_minor, cart.currency),
122
124
  },
@@ -1,7 +1,7 @@
1
1
  import { writeCommandEnvelope } from "./guidance.js";
2
2
  export async function runCatalogList(backend, options = {}) {
3
3
  const manifest = await backend.getCatalogManifest();
4
- const services = manifest.manifest.items.map(summarizeService);
4
+ const services = manifest.manifest.items.map(item => summarizeService(item, options.locale ?? "zh-CN"));
5
5
  const firstServiceID = manifest.manifest.items.find((item) => item.service_id)?.service_id;
6
6
  const empty = services.length === 0;
7
7
  const jsonFlag = options.jsonOutput ? " --json" : "";
@@ -10,7 +10,7 @@ export async function runCatalogList(backend, options = {}) {
10
10
  result: { catalog_version: manifest.version, services },
11
11
  instruction: empty
12
12
  ? "当前没有已发布服务;本次目录读取结束,不猜测服务或自动重试。"
13
- : "向用户解释主服务、辅助步骤和价格;得到用户意图后再启动对应 service_id。",
13
+ : options.locale === "en" ? "Explain the service, its operations and pricing; start only the declared operation matching the human request." : "向用户解释主服务、辅助步骤和价格;得到用户意图后再启动对应 service_id。",
14
14
  next: services.length === 1 && firstServiceID
15
15
  ? { command: `itpay services start ${firstServiceID}${jsonFlag}`, reason: "仅当当前服务符合用户请求时读取输入合同;不表示购买同意" }
16
16
  : null,
@@ -20,7 +20,18 @@ export async function runCatalogList(backend, options = {}) {
20
20
  plainResult: catalogPlainLines(manifest.version, services),
21
21
  });
22
22
  }
23
- function summarizeService(item) {
23
+ function summarizeService(item, locale) {
24
+ const definition = item.service_definition;
25
+ if (definition) {
26
+ if (definition.schema_version !== "itpay.service-definition.v1")
27
+ return { service_id: item.service_id, title: item.title, description: "服务定义版本不受支持;升级CLI后读取,不猜操作。" };
28
+ return {
29
+ service_id: definition.service_id, title: definition.display_names[locale] ?? item.title,
30
+ entry: `itpay services start ${definition.service_id} --json`, definition_version: definition.definition_version,
31
+ ...(definition.guide ? { guide: `itpay docs show ${definition.guide.topic}` } : {}),
32
+ operations: definition.operations.map(op => ({ operation_id: op.id, title: op.display_names[locale], role: op.role, price: op.billing_mode === "dynamic" ? (locale === "en" ? (definition.adapter_key === "rail.v1" ? "Live quote; free queries do not mean free tickets" : "Live quote; confirm the locked amount") : (definition.adapter_key === "rail.v1" ? "实时报价;免费查询不等于免费车票" : "实时报价,以锁定报价为准")) : op.billing_mode === "free" ? (locale === "en" ? "Free query" : "免费查询") : typeof op.price_amount_minor === "number" && op.price_currency ? formatProductMoney(op.price_amount_minor, op.price_currency) : (locale === "en" ? "Fixed price missing; stop purchase preparation" : "固定价格事实缺失;停止准备购买"), requires_continuation: op.requires_continuation ?? false })),
33
+ };
34
+ }
24
35
  const flow = item.service_flow;
25
36
  const offer = item.variants?.[0];
26
37
  return {
@@ -28,11 +39,10 @@ function summarizeService(item) {
28
39
  ...(item.service_id ? { entry: `itpay services start ${item.service_id} --json` } : {}),
29
40
  title: item.title,
30
41
  description: item.description ?? "",
31
- ...(item.service_id?.startsWith("itpay-rail-") ? { guide: "itpay docs show rail-booking" } : {}),
32
42
  ...(flow ? {
33
43
  discovery: {
34
44
  title: flow.discovery.title,
35
- description: flow.discovery.description,
45
+ description: flow.discovery.description ?? "",
36
46
  ...(flow.discovery.free_quota_limit !== undefined ? { free_quota: flow.discovery.free_quota_limit } : {}),
37
47
  ...(flow.discovery.paid_continuation ? {
38
48
  paid_price: formatProductMoney(flow.discovery.paid_continuation.amount_minor, flow.discovery.paid_continuation.currency),
@@ -40,7 +50,7 @@ function summarizeService(item) {
40
50
  },
41
51
  primary_offer: {
42
52
  title: flow.primary_service.title,
43
- description: flow.primary_service.description,
53
+ description: flow.primary_service.description ?? "",
44
54
  price: formatProductMoney(flow.primary_service.amount_minor, flow.primary_service.currency),
45
55
  },
46
56
  } : offer ? {
@@ -60,20 +70,25 @@ function catalogPlainLines(version, services) {
60
70
  if (service.entry)
61
71
  lines.push(` entry: ${String(service.entry)}`);
62
72
  if (service.guide)
63
- lines.push(` entry: ${String(service.guide)}`);
73
+ lines.push(` guide: ${String(service.guide)}`);
64
74
  if (service.description)
65
75
  lines.push(` description: ${String(service.description)}`);
76
+ if (Array.isArray(service.operations))
77
+ for (const op of service.operations)
78
+ lines.push(` operation: ${String(op.operation_id)} — ${String(op.title)}; ${String(op.price)}`);
66
79
  const discovery = service.discovery;
67
80
  if (discovery) {
68
81
  const quota = discovery.free_quota !== undefined ? `; free_quota: ${String(discovery.free_quota)}` : "";
69
82
  const paid = discovery.paid_price ? `; paid_price: ${String(discovery.paid_price)}` : "";
70
83
  lines.push(` discovery: ${String(discovery.title)}${quota}${paid}`);
71
- lines.push(` ${String(discovery.description)}`);
84
+ if (discovery.description)
85
+ lines.push(` ${String(discovery.description)}`);
72
86
  }
73
87
  const primary = service.primary_offer;
74
88
  if (primary) {
75
89
  lines.push(` primary_offer: ${String(primary.title)}; price: ${String(primary.price)}`);
76
- lines.push(` ${String(primary.description)}`);
90
+ if (primary.description)
91
+ lines.push(` ${String(primary.description)}`);
77
92
  }
78
93
  }
79
94
  return lines;
@@ -1,101 +1,13 @@
1
- import { buildOpenClawTelegramAction } from "../render/telegram.js";
2
- export function shouldPrepareLocalCheckoutImage(platform) {
3
- return platform === "markdown";
4
- }
5
- export function isWorkBuddyPlainChat(agentType, platform) {
6
- return agentType?.trim().toLowerCase() === "workbuddy" && platform === "plain_chat";
7
- }
8
- export function isZCodePlainChat(agentType, platform) {
9
- return agentType?.trim().toLowerCase() === "zcode" && platform === "plain_chat";
10
- }
11
- export function isLinkOnlyBrowserAgent(agentType, platform) {
12
- return isWorkBuddyPlainChat(agentType, platform) || isZCodePlainChat(agentType, platform);
13
- }
14
- export function buildWorkBuddyPresentFilesAction(url) {
15
- return {
16
- tool: "present_files",
17
- arguments: { files: [url] },
18
- };
19
- }
20
- function hasCompleteCheckoutTokens(url) {
21
- if (!url) {
22
- return false;
23
- }
24
- try {
25
- const parsed = new URL(url);
26
- return Boolean(parsed.searchParams.get("display_token") && parsed.searchParams.get("complete_exchange_token"));
27
- }
28
- catch {
29
- return false;
30
- }
31
- }
32
- export function buildCheckoutHandoff(input) {
33
- const url = input.plan?.localSimulation && input.mobileUrl && hasCompleteCheckoutTokens(input.mobileUrl) ? input.mobileUrl : input.url;
34
- const handoff = { url, ...(input.plan?.localSimulation ? { environment: "local_simulation", query: "live", transactions: "simulated" } : {}) };
35
- // mobile_url is only emitted when the link carries the complete token pair;
36
- // a display-token-only link cannot finish the same-device exchange after payment.
37
- const mobileUrl = !input.plan?.localSimulation && hasCompleteCheckoutTokens(input.mobileUrl) ? input.mobileUrl : undefined;
38
- if (mobileUrl) {
39
- handoff.mobile_url = mobileUrl;
40
- }
41
- const workBuddyAction = isWorkBuddyPlainChat(input.agentType, input.platform);
42
- if (workBuddyAction) {
43
- handoff.agent_action = buildWorkBuddyPresentFilesAction(url);
44
- }
45
- if (input.platform === "markdown") {
46
- if (input.localPath)
47
- handoff.qr_local_path = input.localPath;
48
- if (input.markdown)
49
- handoff.markdown = input.markdown;
50
- }
51
- else if (input.platform === "plain_chat" && input.qrImageURL && !isLinkOnlyBrowserAgent(input.agentType, input.platform)) {
52
- handoff.qr_image_url = input.qrImageURL;
53
- }
54
- else if (input.platform === "telegram" && input.qrImageURL) {
55
- handoff.qr_image_url = input.qrImageURL;
56
- }
57
- if (input.agentType?.trim().toLowerCase() === "openclaw" && input.platform === "telegram" && input.plan && input.target) {
58
- handoff.agent_action = buildOpenClawTelegramAction(input.plan, input.target);
59
- }
60
- return {
61
- handoff,
62
- instruction: input.plan?.localSimulation
63
- ? `本地实验:铁路与地图查询真实,授权、通知和交易模拟,不会扣款。在本机打开 handoff.url 模拟付款入口,金额 ${input.amount};仅本机桌面可达。${workBuddyAction ? "按 handoff.agent_action 原样打开一次。" : "展示原入口。"}等待用户操作后读取同一 Checkout;不创建替代订单,用户声明不证明模拟付款已确认。`
64
- : checkoutHandoffInstruction(input.agentType, input.platform, input.amount, Boolean(mobileUrl)),
65
- };
66
- }
67
- function checkoutHandoffInstruction(agentType, platform, amount, hasMobileLink) {
68
- if (isWorkBuddyPlainChat(agentType, platform)) {
69
- const mobileSentence = hasMobileLink
70
- ? ",然后把 handoff.mobile_url 作为可点击链接发给用户,说明“手机端点开可直接跳转支付宝或微信支付;小程序内打不开时复制到手机浏览器打开即可”"
71
- : "";
72
- const failLinks = hasMobileLink ? "发送 handoff.url 与 handoff.mobile_url" : "发送原始 handoff.url";
73
- return `Backend 尚未确认付款。立即严格按 handoff.agent_action.tool 和 handoff.agent_action.arguments 原样执行一次,在右侧打开 handoff.url;确认工具调用成功后说明金额 ${amount}${mobileSentence},然后停止等待。若工具失败,只${failLinks},报告未自动打开并停止。不要用 present_files 打开本地文件或二维码 PNG,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
74
- }
75
- if (isZCodePlainChat(agentType, platform)) {
76
- const mobileSentence = hasMobileLink
77
- ? ",并把 handoff.mobile_url 发给用户备用(手机端点开可直接跳转支付宝或微信支付)"
78
- : "";
79
- const fallbackLinks = hasMobileLink ? "同一个可点击 handoff.url 与 handoff.mobile_url" : "同一个可点击 handoff.url";
80
- return `Backend 尚未确认付款。立即用 ZCode 内置浏览器打开 handoff.url,让用户完成付款;确认已发起打开后说明金额 ${amount}${mobileSentence},然后停止等待。不要只粘贴文字链接,不要下载、解析或重建二维码,不要创建新 Checkout、Payment Intent 或 Execution。只有内置浏览器明确不可用时,才展示${fallbackLinks}。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
81
- }
82
- if (agentType?.trim().toLowerCase() === "doubao-work" && platform === "plain_chat") {
83
- const directLink = hasMobileLink ? "handoff.mobile_url" : "handoff.url";
84
- const directLabel = hasMobileLink ? "手机直接打开收银台(可跳转支付宝/微信)" : "手机直接打开收银台";
85
- return `Backend 尚未确认付款。在当前豆包工作对话中展示两个有标签的官方入口:将 ${directLink} 标为“${directLabel}”,将 handoff.qr_image_url 标为“二维码图片(保存或用另一台设备扫码)”。说明金额 ${amount} 后停止等待。不要解析或单独输出 URL credential,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
86
- }
87
- if (platform === "markdown") {
88
- const mobileSentence = hasMobileLink ? ",并附 handoff.mobile_url 供手机端点击直达支付" : "";
89
- return `Backend 尚未确认付款。把 handoff.markdown 原样发送到当前桌面对话,确认二维码、付款链接和金额 ${amount} 均已实际对用户可见${mobileSentence},然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
90
- }
91
- if (platform === "terminal") {
92
- const mobileSentence = hasMobileLink ? ",并展示 handoff.mobile_url 供手机端点击直达支付" : "";
93
- return `Backend 尚未确认付款。在用户可见终端展示当前 Checkout 的二维码、handoff.url 和金额 ${amount}${mobileSentence},然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
94
- }
95
- if (agentType?.trim().toLowerCase() === "openclaw" && platform === "telegram") {
96
- const failLinks = hasMobileLink ? "handoff.qr_image_url、金额、handoff.url 与 handoff.mobile_url" : "handoff.qr_image_url、金额和 handoff.url";
97
- return `Backend 尚未确认付款或授权状态。立即使用 OpenClaw 原生 message tool,严格按 handoff.agent_action.tool 和 handoff.agent_action.arguments 原样执行;它会发送金额 ${amount}、二维码、📱 手机点这儿支付和 📋 已授权给我读。不得改写 presentation、换用其他消息工具、拆开按钮发送或用普通回复声称按钮已经发送。确认 message tool 调用成功后停止等待。收到 callback_data "itp:grant_confirmed:<checkout_id>" 时,表示用户声明已在收银台授权读取;立即执行 next.command 查询同一 Checkout,再只执行 Backend 返回的 next.command 进入同一 Execution 的 grant 流程。该 callback 不证明付款成功或 grant 已生效;Backend 未返回 grant_active 前不得读取或猜测结果。若原生 message tool 明确失败或当前 Telegram 未启用 inline buttons,只发送现有 ${failLinks},报告按钮不可用并停止;不要创建新的 Checkout、Payment Intent 或 Execution。`;
1
+ export * from "../render/checkout_handoff.js";
2
+ import { CommandContractError } from "../services/shared/envelope.js";
3
+ // Presentation owns the clock, original cart and payment facts for every handoff.
4
+ export function assertCheckoutPayable(checkout, authoritative, cartID, sourceExecutionID) {
5
+ const current = authoritative?.checkout ?? checkout.checkout;
6
+ const payableStates = ["draft", "quote_bound", "auth_required", "payment_required"];
7
+ const deadlineClosed = authoritative?.payment_remaining_seconds === 0 ||
8
+ (current.expires_at !== undefined && (!authoritative?.server_now || Date.parse(current.expires_at) <= Date.parse(authoritative.server_now)));
9
+ if ((current.cart_id && current.cart_id !== cartID) || !payableStates.includes(current.status) || authoritative?.payment_intents.some(intent => !["failed", "cancelled", "expired"].includes(intent.status)) || deadlineClosed || !checkout.display_token) {
10
+ throw new CommandContractError("checkout_not_payable", "original checkout is not a current payable entry", "原Checkout不可作为新付款入口;核对原订单。过期不续期,付款处理中或未知不重放,不换购物车或幂等键创建替代购买。", sourceExecutionID ? [{ command: `itpay services next ${sourceExecutionID} --json`, reason: "读取来源执行的原订单与付款事实" }] : [], undefined, { cart_id: cartID, checkout_id: current.checkout_id, checkout_status: current.status,
11
+ reason: deadlineClosed ? "payment_window_closed" : "checkout_not_payable" });
98
12
  }
99
- const urlList = hasMobileLink ? "handoff.url、handoff.mobile_url(手机端点击直达支付)和可用的 handoff.qr_image_url" : "handoff.url 和可用的 handoff.qr_image_url";
100
- return `Backend 尚未确认付款。把 ${urlList} 实际发送到当前会话,说明金额 ${amount},然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
101
13
  }
@@ -1,184 +1 @@
1
- import { HttpError } from "../client/http.js";
2
- import { resolveOutput } from "../render/sink.js";
3
- import { declaredAgentType, qualifyItPayCommand } from "../state/agent_type.js";
4
- import { qualifyBackendCommand } from "../state/config.js";
5
- export function isTerminalServiceExecutionStatus(status) {
6
- return status === "failed" || status === "refunded" || status === "cancelled";
7
- }
8
- export function appendFeedbackPostmortemInstruction(instruction, outcome) {
9
- const invitation = outcome === "delivered"
10
- ? "结果解释完毕后,Agent 必须为同一订单提交一次安全服务复盘;用户评分和评论均可选,只在用户明确给出时忠实附上,不得猜分或上传对话、报告正文、联系方式和原始日志。"
11
- : "先完成同一订单的恢复和退款权利解释;处理清楚后,Agent 必须为同一订单提交一次安全服务复盘。只附上用户明确表达的评分或卡壳点,不得猜测。";
12
- return `${instruction} ${invitation}`;
13
- }
14
- export class CommandContractError extends Error {
15
- code;
16
- instruction;
17
- recovery;
18
- interaction;
19
- result;
20
- constructor(code, message, instruction, recovery, interaction, result) {
21
- super(message);
22
- this.code = code;
23
- this.instruction = instruction;
24
- this.recovery = recovery;
25
- this.interaction = interaction;
26
- this.result = result;
27
- this.name = "CommandContractError";
28
- }
29
- }
30
- // Domain projections and pages own byte limits; serialization never truncates facts.
31
- export function plainValueLines(value, label = "", depth = 0) {
32
- const indent = " ".repeat(depth);
33
- if (value === null || typeof value !== "object")
34
- return [`${indent}${label ? `${label}: ` : ""}${String(value ?? "unknown")}`];
35
- const lines = label ? [`${indent}${label}:`] : [];
36
- const entries = Array.isArray(value) ? value.map((v, i) => [String(i + 1), v]) : Object.entries(value);
37
- for (const [key, child] of entries)
38
- lines.push(...plainValueLines(child, key, depth + (label ? 1 : 0)));
39
- if (!entries.length)
40
- lines.push(`${indent} (无记录)`);
41
- return lines;
42
- }
43
- export function shellArgument(value) {
44
- if (/^[\p{L}\p{N}._:=/-]+$/u.test(value))
45
- return value;
46
- return `'${value.replaceAll("'", `'"'"'`)}'`;
47
- }
48
- export function writeCommandEnvelope(value, options = {}) {
49
- const out = resolveOutput(options.output);
50
- const agentType = options.agentType ?? declaredAgentType();
51
- const qualified = qualifyEnvelope(value, agentType);
52
- if (options.jsonOutput) {
53
- const pretty = JSON.stringify(qualified, null, 2) + "\n";
54
- out(pretty);
55
- return;
56
- }
57
- out(`${qualified.status}\n`);
58
- const facts = "error" in qualified ? qualified.error : qualified.result ?? {};
59
- if (options.plainResult) {
60
- for (const line of options.plainResult) {
61
- const command = line.match(/^(\s*(?:command|reader|entry): )(itpay .*)$/);
62
- out(`${command ? command[1] + qualifyBackendCommand(qualifyItPayCommand(command[2], agentType)) : line}\n`);
63
- }
64
- }
65
- else {
66
- for (const [key, fact] of Object.entries(facts)) {
67
- for (const line of plainValueLines(fact, key))
68
- out(`${line}\n`);
69
- }
70
- if ("error" in qualified && qualified.result) {
71
- for (const [key, fact] of Object.entries(qualified.result)) {
72
- for (const line of plainValueLines(fact, key))
73
- out(`${line}\n`);
74
- }
75
- }
76
- }
77
- if ("handoff" in qualified && qualified.handoff) {
78
- for (const [key, fact] of Object.entries(qualified.handoff)) {
79
- for (const line of plainValueLines(fact, `handoff.${key}`))
80
- out(`${line}\n`);
81
- }
82
- }
83
- out(`instruction: ${qualified.instruction}\n`);
84
- if (qualified.next)
85
- out(`next: ${qualified.next.command}\n when: ${qualified.next.reason}\n`);
86
- if ("interaction" in qualified && qualified.interaction) {
87
- for (const line of plainValueLines(qualified.interaction, "interaction"))
88
- out(`${line}\n`);
89
- }
90
- if ("communication" in qualified && qualified.communication) {
91
- for (const line of plainValueLines(qualified.communication, "communication"))
92
- out(`${line}\n`);
93
- }
94
- if (qualified.recovery.length > 0) {
95
- out("recovery:\n");
96
- for (const action of qualified.recovery) {
97
- out(` - ${action.command}\n`);
98
- out(` reason: ${action.reason}\n`);
99
- }
100
- }
101
- }
102
- function qualifyEnvelope(value, agentType) {
103
- return {
104
- ...value,
105
- result: qualifyCommandsDeep(value.result, agentType),
106
- ...("handoff" in value && value.handoff ? { handoff: qualifyCommandsDeep(value.handoff, agentType) } : {}),
107
- ...("interaction" in value && value.interaction ? { interaction: qualifyCommandsDeep(value.interaction, agentType) } : {}),
108
- ...("communication" in value && value.communication ? { communication: qualifyCommandsDeep(value.communication, agentType) } : {}),
109
- next: value.next ? { ...value.next, command: qualifyBackendCommand(qualifyItPayCommand(value.next.command, agentType)) } : null,
110
- recovery: value.recovery.map((action) => ({
111
- ...action,
112
- command: qualifyBackendCommand(qualifyItPayCommand(action.command, agentType)),
113
- })),
114
- };
115
- }
116
- // Every `itpay ` command string anywhere in the envelope — result fields,
117
- // interaction recipes/templates, available_actions — keeps the same Agent
118
- // Type / Backend qualification as top-level next/recovery. Instruction prose
119
- // never starts with "itpay " so prose stays untouched.
120
- function qualifyCommandsDeep(value, agentType) {
121
- if (typeof value === "string") {
122
- return value.startsWith("itpay ")
123
- ? qualifyBackendCommand(qualifyItPayCommand(value, agentType))
124
- : value;
125
- }
126
- if (Array.isArray(value))
127
- return value.map((item) => qualifyCommandsDeep(item, agentType));
128
- if (value && typeof value === "object") {
129
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, qualifyCommandsDeep(item, agentType)]));
130
- }
131
- return value;
132
- }
133
- export function errorRecoveryActions(error) {
134
- if (!(error instanceof HttpError))
135
- return [];
136
- if (error.code === "agent_identity_required") {
137
- return [{ id: "inspect_agent_setup", label: "Inspect supported Agent Type setup", command: "itpay install --json" }];
138
- }
139
- if (error.code === "agent_device_session_required") {
140
- return [{
141
- id: "read_agent_session_rules",
142
- label: "Read ItPay identity and session recovery rules",
143
- command: "itpay skill show itpay --json",
144
- reason: "The CLI already attempted one automatic session renewal; do not rotate identity or loop retries.",
145
- }];
146
- }
147
- if (error.code === "quota_exhausted" || error.code === "checkout_required") {
148
- return [{
149
- id: "inspect_service_execution",
150
- label: "Inspect Service Execution before checkout",
151
- command: "itpay services list --json",
152
- }];
153
- }
154
- if (error.code === "cart_item_locked") {
155
- return [{ id: "show_cart", label: "Inspect the locked cart", command: "itpay cart show --json",
156
- reason: "读取已有购物内容和订单锁,不创建替代 checkout。" }];
157
- }
158
- if (error.status === 404) {
159
- return [{
160
- id: "recover_service_executions",
161
- label: "List visible Service Executions and follow their next instruction",
162
- command: "itpay services list",
163
- }];
164
- }
165
- if (error.status === 502 || error.status === 503 || error.status === 504) {
166
- return [{
167
- id: "retry_after_backend_recovers",
168
- label: "Retry after the selected official Backend is reachable",
169
- command: "itpay readyz",
170
- }];
171
- }
172
- return [];
173
- }
174
- export function printErrorRecovery(error, output) {
175
- const recovery = errorRecoveryActions(error);
176
- if (recovery.length === 0)
177
- return;
178
- const out = resolveOutput(output);
179
- out("recovery:\n");
180
- for (const action of recovery) {
181
- out(` - ${action.label}\n`);
182
- out(` ${action.command}\n`);
183
- }
184
- }
1
+ export * from "../services/shared/envelope.js";
@@ -12,6 +12,7 @@ export const INSTALL_AGENT_TYPES = [
12
12
  "kimi-code",
13
13
  "openclaw",
14
14
  ];
15
+ export const CODEX_WAIT_GUIDANCE = "Codex 宿主等待:保留完整工具返回,按实际返回的会话句柄及当前宿主续读工具收取最终输出,不要只取 stdout。若使用 functions.exec 包装,可用 text(await tools.exec_command(...));内层有 session_id 时用 write_stdin,外层返回运行 cell 时才用 wait。没有该包装时按宿主实际工具操作。先收取最终输出,再执行 CLI 业务 next;不要重复提交。";
15
16
  const INSTRUCTIONS = {
16
17
  "codex-desktop": "在 Codex Desktop 中始终传这个 Agent Type;付款时把返回的二维码和链接实际展示到当前对话。",
17
18
  "codex-cli": "在 Codex CLI 中始终传这个 Agent Type;付款交接只应显示在用户可见终端。",
@@ -48,7 +49,7 @@ export function runInstall(target, options = {}) {
48
49
  ...installDefinition(normalized),
49
50
  default_api: DEFAULT_BASE_URL,
50
51
  },
51
- instruction: INSTRUCTIONS[normalized],
52
+ instruction: INSTRUCTIONS[normalized] + (normalized.startsWith("codex-") ? ` ${CODEX_WAIT_GUIDANCE}` : ""),
52
53
  next: {
53
54
  command: `itpay --agent-type ${normalized} readyz --json`,
54
55
  reason: "验证当前官方 ItPay API 的可用性",
@@ -1,14 +1,28 @@
1
+ import { HttpError } from "../client/http.js";
2
+ import { serviceAdapter } from "../services/registry.js";
1
3
  import { refundStateEnvelope } from "./refund.js";
2
4
  import { formatMoney } from "../render/output.js";
3
5
  import { appendFeedbackPostmortemInstruction, writeCommandEnvelope } from "./guidance.js";
4
6
  export async function runOrder(backend, orderID, options = {}) {
5
7
  const order = await backend.getOrder(orderID);
6
8
  const [delivery, refundResponse] = await Promise.all([
7
- order.status === "delivered" ? backend.getOrderDeliveryAccess(orderID) : Promise.resolve(undefined),
9
+ order.status === "delivered" ? backend.getOrderDeliveryAccess(orderID).catch(error => {
10
+ // Metadata ownership never grants account-protected delivery access.
11
+ if (error instanceof HttpError && ["not_found", "account_scope_required", "access_denied"].includes(error.code))
12
+ return undefined;
13
+ throw error;
14
+ }) : Promise.resolve(undefined),
8
15
  backend.listOrderRefunds(orderID),
9
16
  ]);
10
17
  const lockedRefund = refundResponse.refunds.find((refund) => refund.access_locked);
11
- const envelope = orderEnvelope(order, delivery, lockedRefund);
18
+ let envelope = orderEnvelope(order, delivery, lockedRefund);
19
+ for (const identity of order.service_identities ?? []) {
20
+ const capability = serviceAdapter(identity).aftercare;
21
+ if (capability) {
22
+ envelope = await capability.order(backend, order, envelope);
23
+ break;
24
+ }
25
+ }
12
26
  writeCommandEnvelope(envelope, {
13
27
  ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
14
28
  ...(options.output ? { output: options.output } : {}),
@@ -50,6 +64,10 @@ function orderEnvelope(order, delivery, lockedRefund) {
50
64
  instruction = "先告诉用户订单仍在处理,已记录的付款和订单不需要重复创建;稍后查询同一订单,不要创建替代订单。";
51
65
  next = { command: `itpay order ${order.order_id} --json`, reason: "刷新订单状态" };
52
66
  }
67
+ if (order.status === "partially_refunded") {
68
+ instruction = "订单已部分退款;核对本次退款金额及其他履约事实,不能推断整单全部退款。平台交付访问按返回权限保持锁定,不恢复旧授权。";
69
+ next = null;
70
+ }
53
71
  return {
54
72
  status: order.status,
55
73
  result: {