@itpay/cli 2.1.34 → 2.1.41

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 (112) hide show
  1. package/README.md +5 -12
  2. package/dist/src/client/backend.js +2 -33
  3. package/dist/src/client/http.js +0 -2
  4. package/dist/src/commands/buy.js +4 -7
  5. package/dist/src/commands/cart.js +15 -15
  6. package/dist/src/commands/catalog.js +7 -17
  7. package/dist/src/commands/checkout.js +5 -146
  8. package/dist/src/commands/checkout_handoff.js +10 -48
  9. package/dist/src/commands/docs.js +18 -7
  10. package/dist/src/commands/guidance.js +19 -69
  11. package/dist/src/commands/install.js +3 -6
  12. package/dist/src/commands/order.js +1 -14
  13. package/dist/src/commands/orders.js +9 -4
  14. package/dist/src/commands/pay.js +1 -1
  15. package/dist/src/commands/readyz.js +2 -2
  16. package/dist/src/commands/refund.js +5 -5
  17. package/dist/src/commands/services.js +167 -1712
  18. package/dist/src/commands/skill.js +5 -8
  19. package/dist/src/commands/vault.js +20 -27
  20. package/dist/src/commands/vault_handoff.js +0 -18
  21. package/dist/src/main.js +58 -294
  22. package/dist/src/render/markdown.js +3 -0
  23. package/dist/src/render/qr.js +1 -1
  24. package/dist/src/state/cart_session.js +1 -4
  25. package/dist/src/state/client_context.js +0 -72
  26. package/dist/src/state/config.js +14 -44
  27. package/dist/src/state/device_authority.js +41 -141
  28. package/docs/agent/buyer/cart-checkout.json +5 -6
  29. package/docs/agent/buyer/catalog-list.json +6 -24
  30. package/docs/agent/buyer/identity-and-sessions.json +9 -25
  31. package/docs/agent/buyer/install-and-setup.json +15 -11
  32. package/docs/agent/buyer/orders-refunds.json +5 -12
  33. package/docs/agent/buyer/payment-flow.json +3 -4
  34. package/docs/agent/buyer/purchased-content.json +4 -18
  35. package/docs/agent/buyer/quickstart.json +16 -24
  36. package/docs/agent/buyer/render-hosts.json +12 -23
  37. package/docs/agent/buyer/service-feedback.json +1 -2
  38. package/docs/cli-reference/agent-types.md +2 -7
  39. package/docs/cli-reference/commands/buy.md +1 -2
  40. package/docs/cli-reference/commands/cart/add.md +1 -1
  41. package/docs/cli-reference/commands/cart/clear.md +1 -1
  42. package/docs/cli-reference/commands/cart/index.md +1 -1
  43. package/docs/cli-reference/commands/cart/next.md +1 -1
  44. package/docs/cli-reference/commands/cart/remove.md +1 -1
  45. package/docs/cli-reference/commands/cart/show.md +1 -1
  46. package/docs/cli-reference/commands/catalog/index.md +1 -1
  47. package/docs/cli-reference/commands/catalog/list.md +2 -2
  48. package/docs/cli-reference/commands/checkout.md +2 -12
  49. package/docs/cli-reference/commands/device.md +11 -18
  50. package/docs/cli-reference/commands/docs/index.md +1 -1
  51. package/docs/cli-reference/commands/docs/list.md +1 -1
  52. package/docs/cli-reference/commands/docs/search.md +1 -1
  53. package/docs/cli-reference/commands/docs/show.md +1 -1
  54. package/docs/cli-reference/commands/install.md +6 -8
  55. package/docs/cli-reference/commands/next.md +1 -1
  56. package/docs/cli-reference/commands/order.md +1 -1
  57. package/docs/cli-reference/commands/pay.md +1 -1
  58. package/docs/cli-reference/commands/readyz.md +10 -58
  59. package/docs/cli-reference/commands/refund/cancel.md +1 -1
  60. package/docs/cli-reference/commands/refund/create.md +1 -1
  61. package/docs/cli-reference/commands/refund/get.md +2 -2
  62. package/docs/cli-reference/commands/refund/index.md +1 -1
  63. package/docs/cli-reference/commands/refund/list.md +1 -1
  64. package/docs/cli-reference/commands/refund/watch.md +1 -1
  65. package/docs/cli-reference/commands/services/action.md +3 -24
  66. package/docs/cli-reference/commands/services/checkout.md +2 -13
  67. package/docs/cli-reference/commands/services/events.md +1 -1
  68. package/docs/cli-reference/commands/services/get.md +2 -4
  69. package/docs/cli-reference/commands/services/index.md +3 -4
  70. package/docs/cli-reference/commands/services/invoke.md +4 -6
  71. package/docs/cli-reference/commands/services/list.md +1 -1
  72. package/docs/cli-reference/commands/services/next.md +12 -105
  73. package/docs/cli-reference/commands/services/quote.md +3 -3
  74. package/docs/cli-reference/commands/services/read-result.md +6 -17
  75. package/docs/cli-reference/commands/services/start.md +2 -2
  76. package/docs/cli-reference/commands/vault/access.md +1 -5
  77. package/docs/cli-reference/conventions.md +6 -4
  78. package/docs/cli-reference/index.md +1 -8
  79. package/docs/skill-bundle-rollout/01-mcp-authentication.md +134 -0
  80. package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +210 -0
  81. package/docs/skill-bundle-rollout/03-platform-publishing.md +161 -0
  82. package/docs/skill-bundle-rollout/04-first-wave-platforms.md +69 -0
  83. package/docs/skill-bundle-rollout/05-sync-operations.md +62 -0
  84. package/docs/skill-bundle-rollout/README.md +151 -0
  85. package/package.json +13 -20
  86. package/skills/itpay/SKILL.md +105 -101
  87. package/assets/sell-preview/index.html +0 -6
  88. package/assets/sell-preview/preview.js +0 -9
  89. package/bin/darwin-amd64/itpay-sell +0 -0
  90. package/bin/darwin-arm64/itpay-sell +0 -0
  91. package/bin/linux-amd64/itpay-sell +0 -0
  92. package/bin/linux-arm64/itpay-sell +0 -0
  93. package/bin/windows-amd64/itpay-sell.exe +0 -0
  94. package/dist/src/commands/presentation.js +0 -153
  95. package/dist/src/commands/rail_catalog.js +0 -261
  96. package/dist/src/render/browser.js +0 -65
  97. package/dist/src/sell/commands.js +0 -80
  98. package/dist/src/sell/contract.js +0 -86
  99. package/dist/src/sell/local.js +0 -260
  100. package/dist/src/sell/mcp.js +0 -49
  101. package/dist/src/sell/preview.js +0 -70
  102. package/dist/src/sell/sync.js +0 -145
  103. package/dist/src/state/account_auth.js +0 -332
  104. package/dist/src/state/task_journal.js +0 -90
  105. package/docs/agent/buyer/rail-booking.json +0 -162
  106. package/docs/agent/buyer/rail-fast-checkout.json +0 -48
  107. package/docs/cli-reference/commands/auth.md +0 -96
  108. package/docs/cli-reference/commands/sell.md +0 -1364
  109. package/docs/cli-reference/commands/services/page.md +0 -71
  110. package/docs/cli-reference/commands/services/run.md +0 -156
  111. package/docs/sell.md +0 -84
  112. /package/bin/{itp.mjs → itp} +0 -0
@@ -1,204 +1,33 @@
1
- import { HttpError } from "../client/http.js";
2
- import { HttpTransportError } from "../client/transport.js";
3
- import { DeviceLockBusyError, DeviceStateError } from "../state/device_authority.js";
4
- import { operationID, taskJournalPath } from "../state/config.js";
5
- import { TaskJournal } from "../state/task_journal.js";
1
+ import { operationID } from "../state/config.js";
6
2
  import { validateContext } from "../state/client_context.js";
7
3
  import { dispatchRender } from "../render/index.js";
8
4
  import { ensureIdeImageAttach } from "../render/ide.js";
9
5
  import { buildCheckoutHandoff, shouldPrepareLocalCheckoutImage } from "./checkout_handoff.js";
10
6
  import { localizeCardURL, normalizeCardLocale } from "../render/locale.js";
11
7
  import { buildAgentChatHandoff } from "../render/markdown.js";
12
- import { refundStateEnvelope } from "./refund.js";
13
- import { decodeRailCatalogJourneys } from "./rail_catalog.js";
14
8
  import { platformKeyForHost } from "../render/plan.js";
15
9
  import { renderTerminalQR } from "../render/qr.js";
16
- // Declared at execution creation: the client understands the rail.progressive.v2
17
- // response format (rail_planning projection, snapshot paging, rsel_ handles).
18
- // The server alone decides service version, quota and provider credentials.
19
- const RAIL_PROGRESSIVE_FEATURES = ["rail.progressive.v2"];
20
10
  import { buildCheckoutQRPlan } from "./buy.js";
21
- import { appendFeedbackPostmortemInstruction, CommandContractError, isTerminalServiceExecutionStatus, writeCommandEnvelope, shellArgument, plainValueLines, } from "./guidance.js";
22
- // AUTH04: record the pause point of each Service Execution so a later
23
- // `itpay auth login` can resume the SAME task. Ids/stage/resume command only —
24
- // no chat content, PII or credentials. Best-effort: journaling never breaks
25
- // a command.
26
- function journalTaskState(serviceExecutionID, envelope, env) {
27
- try {
28
- const journal = new TaskJournal(taskJournalPath(env));
29
- const serviceID = typeof envelope.result?.service_id === "string" ? envelope.result.service_id : undefined;
30
- journal.observe(serviceExecutionID, envelope.status, serviceID ? `itpay services run ${serviceID} --execution ${serviceExecutionID} --json` : undefined);
31
- }
32
- catch {
33
- /* local journal is advisory; never fail the envelope */
34
- }
35
- }
11
+ import { appendFeedbackPostmortemInstruction, CommandContractError, isTerminalServiceExecutionStatus, writeCommandEnvelope, } from "./guidance.js";
36
12
  const serviceActionStatuses = new Set(["pending", "approved", "rejected", "expired", "cancelled"]);
37
- const RAIL_ENDPOINT_BRIEF = "查询前先留下2–4行端点理解摘要:两端各指哪里、是车站/行政范围/具体地点、依据及未确定事实,再说明日期/人数/时限与Exact/Smart选择。对话或现有route-brief.txt二选一;已有摘要沿用,只更新变化项。含义不清先澄清,定位待核验不等于含义不清;勿凭字尾猜类型。";
38
- const RAIL_SERVICE_GUIDANCE = {
39
- "itpay-rail-exact": {
40
- when_to_use: "已知可信站对时先用本服务核验直达;城市需求可先形成有依据的站对假设。只覆盖这一站对,不代表全城市最优。只有明确可靠路线知识可直接用;大概率判断先短查,受阻且清晰双站可Exact核验。查询前记录两端含义、类别、依据、未知事实和Exact/Smart选择;格式见已读rail-booking。",
41
- input_fields: [
42
- { name: "origin", required: true, description: "已核实出发铁路站完整名称,不能以同名站代替区域", example: "<已核实出发站>" },
43
- { name: "destination", required: true, description: "已核实到达铁路站完整名称", example: "<已核实到达站>" },
44
- { name: "travel_date", required: true, description: "出行日期 YYYY-MM-DD;字段名必须是 travel_date,'date' 等别名无效", example: "<用户日期 YYYY-MM-DD>" },
45
- ],
46
- input_example: { origin: "<已核实出发站名>", destination: "<已核实到达站名>", travel_date: "<用户出行日期 YYYY-MM-DD>" },
47
- notes: [
48
- "本服务只接受这三个字段,多传字段会被供应商拒绝",
49
- "查询为空只说明这一已解析站对;允许中转可用Smart保持双站;改变用户指定站须先取得同意",
50
- ],
51
- },
52
- "itpay-rail-smart": {
53
- when_to_use: "需要广泛站点比较、中转或具体地址接驳时使用;有可信站对且只需先核验直达可用 Exact。只有明确可靠路线知识可直接用;大概率判断先短查,受阻且清晰双站可Exact核验。查询前记录两端含义、类别、依据、未知事实和Exact/Smart选择;格式见已读rail-booking。",
54
- input_fields: [
55
- { name: "origin", required: true, description: "出发地:已知最完整的位置名或区域(城市/区县/地址均可)", example: "<原始完整地点>" },
56
- { name: "destination", required: true, description: "目的地:与 origin 同样的位置规则", example: "<已核实到达站>" },
57
- { name: "travel_date", required: true, description: "出行日期 YYYY-MM-DD;字段名必须是 travel_date", example: "<用户日期 YYYY-MM-DD>" },
58
- ],
59
- optional_fields: [
60
- { name: "origin_city", description: "出发地城市提示,辅助位置解析" },
61
- { name: "destination_city", description: "目的地城市提示" },
62
- { name: "origin_location", description: "已知坐标对象(高德 GCJ-02)" },
63
- { name: "destination_location", description: "目的地坐标对象" },
64
- { name: "depart_after", description: "不早于该时间出发" },
65
- { name: "depart_before", description: "不晚于该时间出发" },
66
- { name: "arrive_before", description: "不晚于该时间到达" },
67
- { name: "user_query", description: "旅行需求原话,最多4096 UTF-8字节;时间等明确条件仍填写对应结构化字段" },
68
- { name: "seat_preferences", description: "已确认的席别代码数组,不填写旅客身份" },
69
- { name: "priority", enum: "balanced|fastest|cheapest|safest|flexible", description: "方案偏好" },
70
- { name: "duration_basis", enum: "rail|door_to_door", description: "最快默认按铁路全程含换乘;明确要求且接驳证据完整时可按门到门" },
71
- { name: "max_transfers", description: "允许中转次数,0-2" },
72
- { name: "passengers", description: "成人乘客整数 1-5;不填写身份资料" },
73
- ],
74
- input_example: { origin: "<用户出发地点>", destination: "<用户目的地>", travel_date: "<用户出行日期 YYYY-MM-DD>", priority: "fastest", passengers: 1 },
75
- notes: [
76
- "位置有歧义时会进入位置确认步骤,请用户选定后继续同一执行",
77
- "推荐结果含每趟车的席别与余票;购票在后续受保护流程完成",
78
- ],
79
- },
80
- };
81
- function railServiceGuidance(serviceID, schema) {
82
- const guidance = RAIL_SERVICE_GUIDANCE[serviceID];
83
- if (serviceID !== "itpay-rail-smart" || !guidance)
84
- return guidance;
85
- const properties = schema?.properties;
86
- if (!properties?.endpoints)
87
- return properties?.origin && properties?.destination ? guidance : undefined;
88
- return {
89
- ...guidance,
90
- input_fields: [
91
- { name: "endpoints", required: true, description: "origin/destination 各填完整text;类别有依据时填kind(area/place/station)。仅对象含义明确、类别确实待核验时可省略kind;完整名称合法,坐标可选,不靠省略kind猜城市" },
92
- { name: "travel_date", required: true, description: "用户出行日期 YYYY-MM-DD" },
93
- ],
94
- optional_fields: (guidance.optional_fields ?? []).filter((field) => !["origin_city", "destination_city", "origin_location", "destination_location"].includes(field.name)).concat([
95
- { name: "reuse_from", description: "同 owner 的已保存 Exact result_item_id 或 Smart snapshot_id;端点/日期新查询可复用有效事实" },
96
- ]).filter(field => properties[field.name] !== undefined),
97
- input_example: { endpoints: { origin: { kind: "place", text: "<用户原始完整具体地点>" }, destination: { kind: "area", text: "<用户原始完整行政范围>" } }, travel_date: "<用户出行日期 YYYY-MM-DD>" },
98
- notes: ["仅用当前 input_schema 支持的端点写法;不要与旧 origin/destination 混用", "唯一地点直接解析;真实候选只确认有歧义一端;地图等待按可续用状态恢复,终态有新证据才用返回新查询模板,无动作则停止"],
99
- };
100
- }
101
- // Generate only declared required fields and preserve safe known query facts.
102
- // Authorization and passenger identity are never synthesized into templates.
103
- function serviceInputTemplate(schema, known = {}) {
104
- const properties = schema.properties;
105
- if (properties?.endpoints && !properties.origin && !properties.destination && !known.endpoints && (typeof known.origin === "string" || typeof known.destination === "string")) {
106
- known = { ...known, endpoints: { origin: { text: known.origin }, destination: { text: known.destination } } };
107
- }
108
- if (properties?.travel_date && !known.travel_date && typeof known.date === "string")
109
- known = { ...known, travel_date: known.date };
110
- const result = {};
111
- for (const field of new Set([...requiredInputFields(schema), ...Object.keys(known).filter(field => properties?.[field] !== undefined)])) {
112
- const rule = properties?.[field] ?? {};
113
- if (field === "accept_non_guaranteed") {
114
- result[field] = false;
115
- continue;
116
- }
117
- if (rule.type === "object") {
118
- const value = known[field];
119
- result[field] = serviceInputTemplate(rule, value && typeof value === "object" && !Array.isArray(value) ? value : {});
120
- }
121
- else if (known[field] !== undefined && (rule.type === undefined || rule.type === "array" && Array.isArray(known[field]) || rule.type === "string" && typeof known[field] === "string" || rule.type === "boolean" && typeof known[field] === "boolean" || ["integer", "number"].includes(String(rule.type)) && typeof known[field] === "number"))
122
- result[field] = known[field];
123
- else if (rule.const !== undefined)
124
- result[field] = rule.const;
125
- else if (rule.default !== undefined)
126
- result[field] = rule.default;
127
- else if (Array.isArray(rule.enum) && rule.enum.length)
128
- result[field] = rule.enum[0];
129
- else if (rule.type === "integer" || rule.type === "number")
130
- result[field] = typeof rule.minimum === "number" ? rule.minimum : 1;
131
- else if (rule.type === "boolean")
132
- result[field] = false;
133
- else if (rule.type === "array")
134
- result[field] = [];
135
- else
136
- result[field] = `<${field}>`;
137
- }
138
- for (const field of ["kind", "text", "station_code", "poi_id", "adcode", "location", "travel_date", "passengers", "priority", "user_query", "depart_after", "depart_before", "arrive_by", "arrive_before", "seat_preferences", "max_transfers", "duration_basis"]) {
139
- if (properties?.[field] && known[field] !== undefined)
140
- result[field] = known[field];
141
- }
142
- return result;
143
- }
144
- const WORKFLOW_STEP_GUIDANCE = {
145
- input: { meaning: "输入校验", hint: "核对本服务的输入契约和服务端错误码;本执行的输入不能凭猜测修复" },
146
- quota: { meaning: "额度检查", hint: "查看实际额度及授权状态;只按返回的登录或等待动作处理" },
147
- geo: { meaning: "位置解析", hint: "查看实际地点解析错误;有歧义时让用户选择返回的候选" },
148
- geo_confirm: { meaning: "确认后位置解析", hint: "核对服务端记录的候选与用户实际选择" },
149
- resolved: { meaning: "位置解析复核", hint: "查看未满足的实际位置条件" },
150
- resolved_after_confirm: { meaning: "位置确认复核", hint: "核对已确认地点与服务端复核结果" },
151
- search: { meaning: "供应商车次检索", hint: "查看供应商返回的错误码与当前站对;不要推断是字段或站名错误" },
152
- catalog: { meaning: "可行车次计算", hint: "查看已保存结果和实际过滤原因;不要无原因改日期或站点" },
153
- recommend: { meaning: "方案推荐", hint: "查看已保存候选和实际推荐失败原因;不要无原因放宽条件" },
154
- delivery: { meaning: "交付", hint: "读取当前执行状态;已有订单或付款状态不明时先核实,不重建" },
155
- };
156
13
  export async function runServicesStart(backend, serviceID, options = {}) {
157
14
  const host = options.host ?? "terminal";
158
15
  const response = await backend.startServiceExecution({
159
16
  service_id: serviceID,
160
17
  client_context: {
161
18
  host,
162
- features: [...RAIL_PROGRESSIVE_FEATURES],
163
19
  ...(options.target ? { target: options.target } : {}),
164
20
  ...(options.clientContext ?? {}),
165
21
  },
166
22
  });
167
- if (response.workflow_entry) {
168
- const guidance = railServiceGuidance(serviceID, response.workflow_entry.input_schema);
169
- if (serviceID === "itpay-rail-smart" && !guidance) {
170
- writeCommandEnvelope({ status: "contract_unavailable", result: { service_execution_id: response.execution.service_execution_id, service_id: serviceID }, instruction: "当前执行缺少可用输入合同;只读取一次同执行合同,仍缺失则报告系统缺口并停止,不猜旧Smart写法。", next: { command: `itpay services get ${response.execution.service_execution_id} --json`, reason: "读取当前执行锁定的输入合同一次" }, recovery: [] }, options);
171
- return;
172
- }
173
- writeCommandEnvelope({
174
- status: "input_required",
175
- result: { service_execution_id: response.execution.service_execution_id, service_id: serviceID, input_schema: response.workflow_entry.input_schema, ...(guidance ? { guidance } : {}) },
176
- instruction: guidance ? RAIL_ENDPOINT_BRIEF + "按 result.guidance 逐项填写输入(when_to_use 说明本服务适用场景、input_fields 是必填契约、input_example 是待填示意,必须替换用户真实值),然后继续同一服务执行。不要臆造字段名。" : "根据服务声明填写输入,然后继续同一服务执行。",
177
- next: null,
178
- interaction: {
179
- schema_version: "itpay.interaction.v1",
180
- stage: "input_required",
181
- input_template: {
182
- command: `itpay services run ${serviceID} --execution ${response.execution.service_execution_id} --input-json <file> --json`,
183
- input: guidance?.input_example ?? serviceInputTemplate(response.workflow_entry.input_schema),
184
- required_input: ["file", ...requiredInputFields(response.workflow_entry.input_schema)],
185
- executable: false,
186
- },
187
- },
188
- recovery: []
189
- }, { ...options });
190
- return;
191
- }
192
23
  const capability = response.capabilities.find((item) => item.phase === response.execution.phase && !item.requires_payment);
193
24
  const requiredInput = requiredInputFields(capability?.input_schema);
194
- const needsInput = requiredInput.length > 0;
195
- const command = capability && !needsInput
196
- ? `itpay services invoke ${response.execution.service_execution_id} --capability ${capability.capability_id} --json`
25
+ const command = capability
26
+ ? `itpay services invoke ${response.execution.service_execution_id} --capability ${capability.capability_id}${requiredInput.map((field) => ` --input ${field}=<value>`).join("")} --json`
197
27
  : `itpay services next ${response.execution.service_execution_id} --json`;
198
28
  const capabilitySummary = capability ? {
199
29
  capability_id: capability.capability_id,
200
30
  required_input: requiredInput,
201
- input_schema: capability.input_schema,
202
31
  ...(capability.free_quota_limit !== undefined ? { free_quota_limit: capability.free_quota_limit } : {}),
203
32
  } : null;
204
33
  writeCommandEnvelope({
@@ -210,23 +39,12 @@ export async function runServicesStart(backend, serviceID, options = {}) {
210
39
  capability: capabilitySummary,
211
40
  },
212
41
  instruction: capability
213
- ? "填写首选 capability 的 required_input;一次只提交当前 execution 所代表的服务意图。" + locationInputInstruction(capability.input_schema)
42
+ ? "填写首选 capability 的 required_input;一次只提交当前 execution 所代表的服务意图。"
214
43
  : "当前没有可直接调用的 capability;读取服务端下一步,不要猜测 capability。",
215
44
  next: {
216
45
  command,
217
- reason: capability && !needsInput ? "执行当前允许的能力" : "读取服务端计算的下一步",
46
+ reason: capability ? "执行当前允许的能力" : "读取服务端计算的下一步",
218
47
  },
219
- ...(capability && needsInput ? {
220
- interaction: {
221
- schema_version: "itpay.interaction.v1",
222
- stage: "input_required",
223
- input_template: {
224
- command: `itpay services invoke ${response.execution.service_execution_id} --capability ${capability.capability_id}${requiredInput.map((field) => ` --input ${field}=<value>`).join("")} --json`,
225
- required_input: requiredInput,
226
- executable: false,
227
- },
228
- },
229
- } : {}),
230
48
  recovery: [],
231
49
  }, {
232
50
  ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
@@ -243,55 +61,10 @@ export async function runServicesStart(backend, serviceID, options = {}) {
243
61
  ],
244
62
  });
245
63
  }
246
- function locationConfirmationEnvelope(executionID, capabilityID, input, confirmation) {
247
- input = (confirmation.input ?? input);
248
- const endpoints = (confirmation.endpoints ?? []);
249
- const choices = Object.fromEntries(endpoints.map((endpoint) => [endpoint.side, "<用户选择的候选 id>"]));
250
- const missing = endpoints.filter((endpoint) => !endpoint.candidates?.length);
251
- const reason = missing[0]?.reason?.toUpperCase();
252
- const instruction = missing.length
253
- ? reason === "STATION_LOCATION_EVIDENCE_REQUIRED"
254
- ? "尚未查票。车站身份已知,但 Smart 缺少该站地理坐标证据;请说明此系统缺口。若用户仅要双站直达,可按原站名使用 Exact;不要要求用户换站。"
255
- : reason === "ADDRESS_REQUIRED" || reason === "ADMINISTRATIVE_AREA_UNRESOLVED"
256
- ? "尚未查票。先用已有上下文、地图或浏览补该端地点证据;含义仍不清且影响范围才问用户一个关键问题。保留另一端和原条件;只有返回完整新查询模板且已有新证据才另查,没有模板则停止,不猜旧执行输入。"
257
- : reason === "COORDINATE_IDENTITY_UNVERIFIED" || reason === "COORDINATE_NAME_MISMATCH"
258
- ? "尚未查票。提交的地点名称和坐标尚未得到一致证据;请核实该端点的真实名称或位置,保留另一端,不要把坐标声明当成人工确认。"
259
- : "尚未查票。地点证据不足;说明返回的原因与受影响端,先补真实证据,不改kind绕过、不把外部故障当用户歧义。当前无候选确认或完整新查询模板则停止。"
260
- : "尚未查票。仅向用户确认真实地点歧义,展示候选名称、地址和高德链接。具体住宅仍是目的地,附近车站只作候选;不得把选地点改成锁车站。用户选择后保留原输入,回到同一服务。";
261
- return {
262
- status: "location_confirmation_required",
263
- result: { service_execution_id: executionID, query: input, location_confirmation: confirmation },
264
- instruction,
265
- next: null,
266
- ...(confirmation.can_resume && !missing.length && endpoints.length ? {
267
- interaction: {
268
- schema_version: "itpay.interaction.v1",
269
- stage: "location_confirmation_required",
270
- input_template: {
271
- command: `itpay services invoke ${executionID} --capability ${capabilityID} --input-json <地点确认.json> --json`,
272
- input: { ...input, location_confirmation: { plan_id: confirmation.plan_id, token: confirmation.token, choices } },
273
- required_input: ["choices"],
274
- executable: false,
275
- },
276
- },
277
- } : {}),
278
- recovery: [],
279
- };
280
- }
281
64
  function requiredInputFields(schema) {
282
65
  const required = schema?.required;
283
66
  return Array.isArray(required) ? required.filter((field) => typeof field === "string") : [];
284
67
  }
285
- function locationInputInstruction(schema) {
286
- const properties = schema?.properties;
287
- if (properties?.endpoints)
288
- return " " + RAIL_ENDPOINT_BRIEF + " 两端分别填写 endpoints.origin/destination 的完整text;有类别依据就填kind,只有对象含义明确而类别待核验时可省略。区域保留范围、精确车站仅在目录验证后锁定。不要混用旧 origin/destination、手填锁站或伪造坐标。唯一地点会自动解析;只有真实多候选才让用户选返回的候选 id,地图失败应按当前执行恢复。";
289
- if (!properties?.origin_location || !properties?.destination_location)
290
- return "";
291
- if (!properties.location_confirmation)
292
- return " " + RAIL_ENDPOINT_BRIEF + " 地点保留用户已知最完整名称或城市范围;不要编造坐标或把城市改成同名车站。坐标及其他可选字段严格按当前 input_schema 提交。";
293
- return " " + RAIL_ENDPOINT_BRIEF + " 地点输入:保留两个地点或城市范围。已有可信高德 GCJ-02 坐标时,可用 --input 'origin_location={\"lng\":113.0,\"lat\":23.0,\"coordinate_system\":\"gcj02\",\"source\":\"amap\"}'(数值必须替换为真实查询结果,destination_location 同理);没有地图能力时,直接传用户已知最完整的 origin/destination,可附 origin_city/destination_city。不要编造坐标、补猜地址或把深圳等城市改成深圳站;精确站查须明确站名。用户只给城市、县或镇就保留区域意图,不追问门牌。明确地点由服务自动解析;只有返回 location_confirmation 才展示候选名称、地址和高德链接,等待用户选择,禁止自行选第一项。";
294
- }
295
68
  export async function runServicesInvoke(backend, config, serviceExecutionID, capabilityID, input, options = {}) {
296
69
  const readModel = await backend.getServiceExecution(serviceExecutionID);
297
70
  const requestedCapability = readModel.capabilities.find((capability) => capability.capability_id === capabilityID);
@@ -303,30 +76,19 @@ export async function runServicesInvoke(backend, config, serviceExecutionID, cap
303
76
  }
304
77
  const missingInput = missingRequiredInput(requestedCapability.input_schema, input);
305
78
  if (missingInput.length > 0) {
306
- throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "只补缺失字段到小JSON文件,再提交同一执行;本次尚未调用供应商。", [], { schema_version: "itpay.interaction.v1", stage: "input_required",
307
- input_template: { command: `itpay services invoke ${serviceExecutionID} --capability ${capabilityID} --input-json <file> --json`,
308
- required_input: ["file", ...missingInput], executable: false } });
79
+ const correctedInput = { ...input };
80
+ for (const field of missingInput)
81
+ correctedInput[field] = "<value>";
82
+ throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐 required_input 后重试同一个 execution;本次没有调用 Provider。", [{
83
+ command: `itpay services invoke ${serviceExecutionID} --capability ${capabilityID}${formatInputOptions(correctedInput)} --json`,
84
+ reason: "提交完整 capability 输入",
85
+ }]);
309
86
  }
310
87
  const idempotencyKey = await operationID(config, `service.invoke:${serviceExecutionID}:${capabilityID}:${stableInput(input)}`);
311
- let response;
312
- try {
313
- response = await backend.invokeServiceCapability(serviceExecutionID, capabilityID, {
314
- idempotency_key: idempotencyKey,
315
- redacted_summary: input,
316
- });
317
- }
318
- catch (error) {
319
- if (!(error instanceof HttpError) || error.code !== "verified_phone_required")
320
- throw error;
321
- writeCommandEnvelope({
322
- status: "human_action_required",
323
- result: { service_execution_id: serviceExecutionID, error_code: "verified_phone_required" },
324
- instruction: "需要在官方页面完成手机号验证后当前执行才能继续。运行 itpay auth login 打开官方登录页,完成手机号验证并绑定本设备后重试原命令;CLI 不接收手机号或验证码。",
325
- next: { command: "itpay auth login --json", reason: "完成官方手机号验证并绑定当前设备" },
326
- recovery: [{ command: `itpay services invoke ${serviceExecutionID} --capability ${capabilityID}${formatInputOptions(input)} --json`, reason: "仅在完成手机号验证与设备绑定后重试" }],
327
- }, { ...options, plainResult: ["手机号验证:itpay auth login"] });
328
- return;
329
- }
88
+ const response = await backend.invokeServiceCapability(serviceExecutionID, capabilityID, {
89
+ idempotency_key: idempotencyKey,
90
+ redacted_summary: input,
91
+ });
330
92
  const envelope = invokedEnvelope(response, requestedCapability, readModel.capabilities, input);
331
93
  writeCommandEnvelope(envelope.value, {
332
94
  ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
@@ -335,13 +97,6 @@ export async function runServicesInvoke(backend, config, serviceExecutionID, cap
335
97
  });
336
98
  }
337
99
  function invokedEnvelope(response, requestedCapability, capabilities, input) {
338
- if (response.execution_request_id) {
339
- const id = response.execution.service_execution_id;
340
- return { value: { status: "running", result: { service_execution_id: id, execution_request_id: response.execution_request_id },
341
- instruction: "查询已排队或正在运行。告知用户稍候,只读取同一任务的状态;不要重新发起查询,也不要把暂未返回结果说成没有车。",
342
- next: { command: `itpay services next ${id} --json`, reason: "稍后读取同一查询结果" }, recovery: [] },
343
- plainResult: ["status: running", `service_execution_id: ${id}`] };
344
- }
345
100
  const items = response.result_items.map((item) => ({
346
101
  rank: item.rank,
347
102
  title: item.display_title,
@@ -357,58 +112,11 @@ function invokedEnvelope(response, requestedCapability, capabilities, input) {
357
112
  items,
358
113
  ...(quota ? { quota } : {}),
359
114
  };
360
- const preview = response.invocation?.safe_result_preview;
361
- const notices = Array.isArray(preview?.notices) ? preview.notices.map((notice) => {
362
- if (!notice || typeof notice !== "object" || !("code" in notice) || !("message" in notice) || typeof notice.message !== "string")
363
- return notice;
364
- if (!["RAIL_TRANSFER_SCOPE_LIMIT", "RAIL_TRANSFER_SEARCH_INCOMPLETE"].includes(String(notice.code)))
365
- return notice;
366
- return { ...notice, message: notice.message.replace(/可按主要枢纽分段查询[。;]?/g, "") };
367
- }) : preview?.notices;
368
- if (!response.effective_quota?.exhausted && preview?.search_status === "LOCATION_CONFIRMATION_REQUIRED" && preview.location_confirmation) {
369
- return { value: locationConfirmationEnvelope(response.execution.service_execution_id, requestedCapability.capability_id, input, preview.location_confirmation),
370
- plainResult: plainValueLines(preview.location_confirmation) };
371
- }
372
- if (typeof preview?.catalog_total === "number") {
373
- baseResult.catalog = {
374
- total: preview.catalog_total,
375
- recommendation: preview.recommendation,
376
- decision_source: preview.decision_source,
377
- coverage: preview.coverage,
378
- notices,
379
- search_status: preview.search_status,
380
- searched_scope: preview.searched_scope,
381
- effective_policy_hash: preview.effective_policy_hash,
382
- api_cost: preview.api_cost,
383
- stage_timestamps: preview.stage_timestamps,
384
- page: preview.catalog_page,
385
- journey_summary: preview.journey_summary,
386
- journeys: preview.journeys,
387
- train_services: preview.train_services,
388
- resolved_locations: preview.resolved_locations,
389
- };
390
- }
391
115
  let status = items.length > 0 ? "result_ready" : "no_result";
392
116
  let instruction = items.length > 0
393
117
  ? "用编号、名称和可公开字段向用户说明候选;若候选列表已满足目标就停止。只有用户明确选择并希望继续时,才提交对应编号;不要向用户提及 safe_payload、Execution 或内部 ID。"
394
118
  : `没有找到与“${queryText(input)}”匹配的结果。向用户展示本次为 0 个结果并停止。不要修改、缩短或猜测其他输入;只有用户明确提供新输入后,才能启动新的查询。`;
395
- if (items.length === 0 && Array.isArray(notices)) {
396
- const railNotice = notices.find((notice) => {
397
- if (!notice || typeof notice !== "object")
398
- return false;
399
- const value = notice;
400
- return ["RAIL_TRANSFER_SCOPE_LIMIT", "RAIL_TRANSFER_SEARCH_INCOMPLETE"].includes(String(value.code)) && typeof value.message === "string";
401
- });
402
- if (railNotice)
403
- instruction = `向用户展示官方提示:${railNotice.message} 说明本次已查范围和未完成原因,按服务端提供的当前动作恢复;不能据此断言没有车,不默认扩大范围或让用户另选分段站点。`;
404
- }
405
119
  let next = null;
406
- if (items.length > 0 && baseResult.catalog) {
407
- instruction = "先向用户说明排在首位的推荐方案和其他方案的时间、费用与便利性取舍。items 包含本次返回的合格候选,用户不满意时继续从该列表比较,不必重复查票。搜索是否完成、目录是否截断、覆盖范围和模型降级以 catalog 为准;不能把部分结果说成完整搜索。费用尚需购票前核验。姓名、身份证和手机号仅在 ItPay 网页填写。";
408
- }
409
- if (items.length > 0 && preview?.journey_summary) {
410
- instruction = "先按 journey_summary 报告本次已查询范围内的可用车次、换乘走法和实际乘车组合数量,不能用 catalog.total 或 items 数量冒充车次或组合数。按 journeys 展示组合,推荐置顶,保留全部组合和 train_services 车次列表供用户查看。每组先说明乘坐哪些车、在哪里真正换车以及等待多久;rides.onboard_stops 是同车接续停站,无需下车,可能需车内换座,不计入换乘次数。席别、余票、价格及接驳是组合下的选择,通过 candidate_ids 查对应 items;确认具体选择后才使用该 item 的编号,不猜席别或自动付款。30 分钟只是在已确认便捷换乘站点的筛选下限,不是接续保证。不得把多段票称为已可购买的套票;以 purchase_supported 为准。覆盖不完整、截断和模型仅看短名单时必须说明。姓名、身份证和手机号只在 ItPay 网页填写。";
411
- }
412
120
  if (response.effective_quota?.exhausted) {
413
121
  status = "quota_exhausted";
414
122
  instruction = "免费额度已用完且本次没有调用 Provider。当前没有可购买的 continuation;只读取同一 Execution 的服务端恢复方向。";
@@ -437,31 +145,46 @@ function invokedEnvelope(response, requestedCapability, capabilities, input) {
437
145
  }
438
146
  }
439
147
  else if (items.length > 0 && requestedCapability.requires_human_action) {
440
- next = null;
148
+ next = {
149
+ command: `itpay services action ${response.execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
150
+ reason: "在当前 Execution 记录用户选择",
151
+ };
441
152
  }
442
153
  else if (items.length === 0) {
443
154
  next = null;
444
155
  }
445
- const interaction = items.length > 0 && requestedCapability.requires_human_action
446
- ? {
447
- schema_version: "itpay.interaction.v1",
448
- stage: "query_results_ready",
449
- input_template: {
450
- command: `itpay services action ${response.execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
451
- required_input: ["rank"],
452
- executable: false,
453
- },
454
- }
455
- : undefined;
456
- if (preview?.resolved_locations)
457
- instruction += " 按 resolved_locations 说明实际解析地点;区域级位置仅是范围代表点,接驳时间不是从用户精确位置计算的。";
458
156
  return {
459
- value: { status, result: baseResult, instruction, next, recovery: [], ...(interaction ? { interaction } : {}) },
157
+ value: { status, result: baseResult, instruction, next, recovery: [] },
460
158
  plainResult: serviceResultPlainLines(baseResult),
461
159
  };
462
160
  }
463
161
  function serviceResultPlainLines(result) {
464
- return plainValueLines(result);
162
+ const lines = [
163
+ `service_execution_id: ${String(result.service_execution_id)}`,
164
+ `capability_id: ${String(result.capability_id)}`,
165
+ ];
166
+ const items = result.items;
167
+ const query = result.query;
168
+ if (query) {
169
+ for (const [key, value] of Object.entries(query))
170
+ lines.push(`${key}: ${String(value)}`);
171
+ }
172
+ if (items.length === 0)
173
+ lines.push("results: 0");
174
+ if (result.quota)
175
+ lines.push(`quota: ${JSON.stringify(result.quota)}`);
176
+ if (result.checkout)
177
+ lines.push(`checkout: ${JSON.stringify(result.checkout)}`);
178
+ if (items.length > 0) {
179
+ lines.push("items:");
180
+ for (const item of items) {
181
+ lines.push(` ${item.rank}. ${item.title}`);
182
+ for (const [key, value] of Object.entries(item.safe_payload)) {
183
+ lines.push(` ${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
184
+ }
185
+ }
186
+ }
187
+ return lines;
465
188
  }
466
189
  function queryText(input) {
467
190
  const value = Object.values(input).find((item) => typeof item === "string" && item.trim() !== "");
@@ -483,8 +206,6 @@ function checkoutCommand(serviceExecutionID, capability, input, fillMissing = tr
483
206
  return `itpay services checkout ${serviceExecutionID} --capability ${capability.capability_id}${formatInputOptions(lockedInput)}${capability.delivery_email_required ? " --email <email>" : ""} --json`;
484
207
  }
485
208
  function capabilityPrice(capability) {
486
- if (capability.pricing_method === "rail_fare_plus_fee")
487
- return "实时票款 + 每张票 2 元服务费(最终金额以锁定报价为准)";
488
209
  return capability.price_amount_minor !== undefined && capability.price_currency
489
210
  ? formatMoney(capability.price_amount_minor, capability.price_currency)
490
211
  : "当前发布价格";
@@ -552,9 +273,14 @@ function formatInputOptions(input) {
552
273
  .sort(([left], [right]) => left.localeCompare(right))
553
274
  .map(([key, value]) => String(value) === "<value>"
554
275
  ? ` --input ${key}=<value>`
555
- : ` --input ${shellArgument(`${key}=${typeof value === "object" && value !== null ? JSON.stringify(value) : String(value)}`)}`)
276
+ : ` --input ${shellArgument(`${key}=${String(value)}`)}`)
556
277
  .join("");
557
278
  }
279
+ function shellArgument(value) {
280
+ if (/^[\p{L}\p{N}._:=/-]+$/u.test(value))
281
+ return value;
282
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
283
+ }
558
284
  export async function runServicesAction(backend, serviceExecutionID, actionType, input, options = {}) {
559
285
  const selection = await resolveCandidateSelection(backend, serviceExecutionID, actionType, options);
560
286
  const request = {
@@ -572,36 +298,7 @@ export async function runServicesAction(backend, serviceExecutionID, actionType,
572
298
  request.result_item_id = resultItemID;
573
299
  if (options.requiredBefore)
574
300
  request.required_before = options.requiredBefore;
575
- let response;
576
- try {
577
- response = await backend.recordServiceExecutionAction(serviceExecutionID, request);
578
- }
579
- catch (error) {
580
- if (error instanceof HttpError && ["booking_review_invalid", "booking_review_changed", "booking_review_locked"].includes(error.code)) {
581
- throw new CommandContractError(error.code, error.message, error.code === "booking_review_invalid"
582
- ? "确认输入与当前草稿不符。使用返回 interaction.input_template.input 当前review模板,席别使用返回的代码,不填显示名或猜新值。若仅修正代码写法,且车次、实际席别、人数、价格及条款等已确认内容未变,可沿用用户已有明确确认后提交;草稿实质变化或缺少真实同意时,先请用户确认变化内容。"
583
- : error.code === "booking_review_changed"
584
- ? "草稿版本已变化;使用同一booking的当前review模板。仅技术修正且车次、实际席别、人数、价格及条款未变时沿用已有明确同意;实质变化或缺少真实同意才补确认。"
585
- : "当前购买已有付款尝试;读取同一booking及订单状态,不创建新订单或重做付款。", error.payload?.review ? [] : [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取同一booking的当前状态与review" }], error.payload?.review?.input_template ? {
586
- schema_version: "itpay.interaction.v1", stage: "booking_review_required",
587
- input_template: { command: `itpay services action ${serviceExecutionID} --action ${actionType} --actor-type human --status approved --input-json <file> --json`, input: error.payload?.review.input_template, required_input: ["file"], executable: false },
588
- } : undefined, { service_execution_id: serviceExecutionID, review: error.payload?.review, input_errors: error.payload?.input_errors });
589
- }
590
- throw error;
591
- }
592
- if ("state" in response) {
593
- const updated = await backend.getServiceExecution(serviceExecutionID);
594
- const envelope = servicesNextEnvelope(updated);
595
- envelope.result = {
596
- ...envelope.result,
597
- planning_action: { action_id: response.action_id, state: response.state, replayed: response.replayed },
598
- };
599
- writeCommandEnvelope(envelope, {
600
- ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
601
- ...(options.output ? { output: options.output } : {}),
602
- });
603
- return;
604
- }
301
+ const response = await backend.recordServiceExecutionAction(serviceExecutionID, request);
605
302
  if (selection && actionType === "select_candidate" && response.status === "approved") {
606
303
  const updated = await backend.getServiceExecution(serviceExecutionID);
607
304
  const preferred = updated.allowed_actions?.[0];
@@ -687,10 +384,6 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
687
384
  ...(options.deliveryContact ?? {}),
688
385
  ...(options.email ? { email: options.email } : {}),
689
386
  };
690
- if (!options.resume && !capabilityID) {
691
- const model = await backend.getServiceExecution(serviceExecutionID);
692
- capabilityID = model.workflow_entry?.capability_id;
693
- }
694
387
  if (!options.resume && !capabilityID) {
695
388
  throw new CommandContractError("capability_required", "--capability is required when creating a service checkout", "使用当前 Service Execution 返回的付费 capability;恢复已有 Checkout 时改用 --resume。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前允许的付费 capability" }]);
696
389
  }
@@ -702,11 +395,14 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
702
395
  }
703
396
  const lockedInput = options.lockedInput ?? {};
704
397
  const missingInput = missingRequiredInput(capability.input_schema, lockedInput);
705
- if (missingInput.length > 0 && !readModel.workflow_entry && readModel.execution.next_action !== "create_checkout") {
706
- throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐付费 capability 的 required_input;本次没有创建 quote、Checkout 或订单。", [], { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command: checkoutCommand(serviceExecutionID, capability, lockedInput), required_input: missingInput, executable: false } });
398
+ if (missingInput.length > 0 && readModel.execution.next_action !== "create_checkout") {
399
+ throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐付费 capability 的 required_input;本次没有创建 quote、Checkout 或订单。", [{ command: checkoutCommand(serviceExecutionID, capability, lockedInput), reason: "提交完整且会被锁定的服务输入" }]);
707
400
  }
708
- if (capability.delivery_email_required && readModel.workflow_entry?.capability_id !== capability.capability_id && String(deliveryContact.email ?? "").trim() === "") {
709
- throw new CommandContractError("delivery_email_required", "delivery email is required before creating this service checkout", "该 capability 的交付链接会发送到用户邮箱;先向用户说明用途并询问邮箱,不要代填。", [], { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command: `itpay services checkout ${serviceExecutionID} --capability ${capability.capability_id}${formatInputOptions(lockedInput)} --email <email> --json`, required_input: ["email"], executable: false } });
401
+ if (capability.delivery_email_required && String(deliveryContact.email ?? "").trim() === "") {
402
+ throw new CommandContractError("delivery_email_required", "delivery email is required before creating this service checkout", "该 capability 的交付链接会发送到用户邮箱;先向用户说明用途并询问邮箱,不要代填。", [{
403
+ command: `itpay services checkout ${serviceExecutionID} --capability ${capability.capability_id}${formatInputOptions(lockedInput)} --email <email> --json`,
404
+ reason: "使用用户提供的邮箱创建 Checkout",
405
+ }]);
710
406
  }
711
407
  }
712
408
  const response = await backend.createServiceExecutionCheckout(serviceExecutionID, {
@@ -722,34 +418,7 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
722
418
  const checkoutURL = tokenizedCheckoutURL(checkout.checkout_url, displayToken, checkout.qr_payload);
723
419
  const cardURL = localizeCardURL(absolutePublicURL(config.baseURL, checkout.card_url ?? fallbackCardURL(config.baseURL, checkoutID, displayToken)), locale);
724
420
  const cardPNGURL = localizeCardURL(absolutePublicURL(config.baseURL, checkout.card_png_url ?? checkout.qr_png_url ?? fallbackCardPNGURL(config.baseURL, checkoutID, displayToken)), locale);
725
- let itineraryTitle;
726
- let itinerary;
727
- try {
728
- const booking = await backend.getServiceExecution(serviceExecutionID);
729
- if (booking.execution.service_id === "itpay-rail-booking") {
730
- const review = booking.workflow?.human_action?.context?.review;
731
- const presentation = await backend.getCheckoutPresentation(checkoutID, displayToken);
732
- const quote = presentation.rail_quote;
733
- const facts = quote?.legs ?? booking.rail_booking?.legs ?? (Array.isArray(review?.legs) ? review.legs : []);
734
- const legs = facts.filter(value => value && typeof value === "object").map(value => {
735
- const leg = value;
736
- return { travel_date: leg.travel_date, train_code: leg.train_code,
737
- from_station: leg.from || leg.from_station || `站点代码 ${String(leg.from_station_code ?? "未知")}(名称暂缺)`,
738
- to_station: leg.to || leg.to_station || `站点代码 ${String(leg.to_station_code ?? "未知")}(名称暂缺)`,
739
- departure: leg.departure ?? leg.departure_time, arrival: leg.arrival ?? leg.arrival_time,
740
- arrival_days: leg.arrival_days, seat_name: leg.seat_name || leg.seat_type_name || `席别代码 ${String(leg.seat_type ?? "未知")}(名称暂缺)` };
741
- });
742
- if (legs.length) {
743
- itinerary = { legs, passengers: quote?.passengers ?? review?.passengers ?? null, purchase_unit: "journey" };
744
- itineraryTitle = legs.map(leg => [leg.train_code, leg.from_station, "->", leg.to_station].filter(value => typeof value === "string" && value.length).join(" ")).join(" / ");
745
- }
746
- }
747
- }
748
- catch {
749
- // Checkout already exists; a display-only title lookup cannot hide its official handoff.
750
- }
751
421
  const plan = buildCheckoutQRPlan({
752
- localSimulation: backend.localSimulation,
753
422
  host,
754
423
  checkoutID,
755
424
  checkoutURL,
@@ -759,7 +428,7 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
759
428
  qrPNGURL: cardPNGURL,
760
429
  nextAction: checkout.checkout.next_action,
761
430
  orderItems: response.cart.items.map((item) => ({
762
- title: itineraryTitle || item.title,
431
+ title: item.title,
763
432
  quantity: item.quantity,
764
433
  amountMinor: item.amount_minor,
765
434
  currency: item.currency,
@@ -796,11 +465,13 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
796
465
  });
797
466
  }
798
467
  const envelope = buildServicesCheckoutEnvelope(response, checkoutURL, plan, options.agentType, options.target);
799
- if (itinerary) {
800
- envelope.result = { ...envelope.result, itinerary };
801
- envelope.instruction = "整程一次付款,各段分别出票;展示入口不等于付款,付款成功不等于全部出票。" + envelope.instruction;
802
- }
803
- const plainResult = plainValueLines(envelope.result);
468
+ const plainResult = [
469
+ `service_execution_id: ${response.binding.service_execution_id}`,
470
+ `checkout_id: ${checkoutID}`,
471
+ `capability_id: ${checkoutCapabilityID(response, capabilityID)}`,
472
+ `locked_input: ${JSON.stringify(response.locked_input)}`,
473
+ `amount: ${formatMoney(checkout.checkout.amount_minor, checkout.checkout.currency)}`,
474
+ ];
804
475
  if (!options.jsonOutput && platform === "terminal") {
805
476
  plainResult.push("qr:", await renderTerminalQR(checkoutURL, options.qrFormat ?? "terminal"));
806
477
  }
@@ -819,15 +490,15 @@ export async function runServicesQuote(backend, serviceExecutionID, capabilityID
819
490
  const selectionBacked = model.execution.status === "human_action_approved" &&
820
491
  model.allowed_actions?.some((action) => action.type === "prepare_quote" && action.capability_id === capabilityID);
821
492
  const missingInput = missingRequiredInput(capability.input_schema, input);
822
- if (missingInput.length > 0 && !selectionBacked && model.workflow_entry?.capability_id !== capability.capability_id) {
823
- throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐付费 capability 输入;本次没有创建 Quote、Cart 或 Checkout。", [], { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command: quoteCommand(serviceExecutionID, capability, input), required_input: missingInput, executable: false } });
493
+ if (missingInput.length > 0 && !selectionBacked) {
494
+ throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐付费 capability 输入;本次没有创建 Quote、Cart 或 Checkout。", [{ command: quoteCommand(serviceExecutionID, capability, input), reason: "提交完整且会被锁定的输入" }]);
824
495
  }
825
496
  const deliveryContact = {
826
497
  ...(options.deliveryContact ?? {}),
827
498
  ...(options.email ? { email: options.email } : {}),
828
499
  };
829
- if (capability.delivery_email_required && model.workflow_entry?.capability_id !== capability.capability_id && String(deliveryContact.email ?? "").trim() === "") {
830
- throw new CommandContractError("delivery_email_required", "delivery email is required before preparing this service quote", "交付链接会发送到用户邮箱;说明用途并询问邮箱,不要代填。", [], { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command: quoteCommand(serviceExecutionID, capability, input), required_input: ["email"], executable: false } });
500
+ if (capability.delivery_email_required && String(deliveryContact.email ?? "").trim() === "") {
501
+ throw new CommandContractError("delivery_email_required", "delivery email is required before preparing this service quote", "交付链接会发送到用户邮箱;说明用途并询问邮箱,不要代填。", [{ command: quoteCommand(serviceExecutionID, capability, input), reason: "使用用户提供的邮箱创建报价" }]);
831
502
  }
832
503
  const quote = await backend.prepareServiceQuote(serviceExecutionID, {
833
504
  capability_id: capabilityID,
@@ -871,7 +542,6 @@ export async function runServicesGet(backend, serviceExecutionID, options = {})
871
542
  const lockedRefund = response.refunds.find((refund) => refund.access_locked);
872
543
  const nextState = servicesNextEnvelope(response);
873
544
  const result = {
874
- ...nextState.result,
875
545
  service_execution_id: execution.service_execution_id,
876
546
  service_id: execution.service_id,
877
547
  status: execution.status,
@@ -879,8 +549,6 @@ export async function runServicesGet(backend, serviceExecutionID, options = {})
879
549
  ...(execution.current_capability_id ? { current_capability_id: execution.current_capability_id } : {}),
880
550
  updated_at: execution.updated_at,
881
551
  timeline,
882
- ...(response.workflow ? { workflow: response.workflow } : {}),
883
- ...(response.rail_booking ? { rail_booking: response.rail_booking } : {}),
884
552
  ...(response.events.length > timeline.length ? { timeline_truncated: true } : {}),
885
553
  ...(deliveryMode ? { delivery_mode: deliveryMode } : {}),
886
554
  ...(lockedRefund ? {
@@ -889,133 +557,35 @@ export async function runServicesGet(backend, serviceExecutionID, options = {})
889
557
  } : {}),
890
558
  };
891
559
  const envelope = {
892
- status: nextState.status,
560
+ status: "shown",
893
561
  result,
894
- instruction: nextState.instruction,
895
- next: nextState.next,
896
- ...(nextState.interaction ? { interaction: nextState.interaction } : {}),
897
- ...(nextState.communication ? { communication: nextState.communication } : {}),
898
- ...(nextState.handoff ? { handoff: nextState.handoff } : {}),
899
- recovery: nextState.recovery.filter(action => action.command !== `itpay services get ${serviceExecutionID} --json`),
562
+ instruction: lockedRefund || isTerminalServiceExecutionStatus(execution.status)
563
+ ? nextState.instruction
564
+ : "时间线仅用于解释和恢复;按当前首选动作继续,不要重放已完成步骤。",
565
+ next: nextState.next ? { command: nextState.next.command, reason: "继续当前首选动作" } : null,
566
+ recovery: [{ command: `itpay services events ${serviceExecutionID} --json`, reason: "仅在需要完整诊断事件时使用" }],
900
567
  };
901
568
  writeCommandEnvelope(envelope, {
902
569
  ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
903
570
  ...(options.output ? { output: options.output } : {}),
904
- plainResult: plainValueLines(envelope.result),
571
+ plainResult: [
572
+ `service_execution_id: ${execution.service_execution_id}`,
573
+ `service_id: ${execution.service_id}`,
574
+ `state: ${execution.status}/${execution.phase}`,
575
+ ...(execution.current_capability_id ? [`current_capability_id: ${execution.current_capability_id}`] : []),
576
+ ...timeline.map((event) => `${event.sequence}. ${event.step} ${event.status}/${event.phase} ${event.occurred_at}`),
577
+ ],
905
578
  });
906
579
  }
907
- function shouldWaitForServiceResult(model) {
908
- if (model.current_delivery || model.rail_booking || isTerminalServiceExecutionStatus(model.execution.status))
909
- return false;
910
- if (model.rail_planning) {
911
- const plan = model.rail_planning;
912
- return !plan.recommendation && !(plan.alternatives?.length) &&
913
- !["complete", "paused", "failed", "cancelled", "expired"].includes(plan.search?.expansion_status ?? "") &&
914
- !plan.available_actions?.some((action) => ["select_journey", "confirm_location", "expand_search"].includes(action.type));
915
- }
916
- return ["queued", "running", "delivery"].includes(model.workflow?.status ?? "");
917
- }
918
580
  export async function runServicesNext(backend, serviceExecutionID, options = {}) {
919
- const started = Date.now();
920
- const until = started + (options.timeoutSeconds ?? 0) * 1000;
921
- const sleep = options.sleep ?? ((milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds)));
922
- let response = await backend.getServiceExecution(serviceExecutionID, options.sinceSnapshot ? { sinceSnapshot: options.sinceSnapshot } : {});
923
- while (shouldWaitForServiceResult(response) && Date.now() < until) {
924
- await sleep(options.pollIntervalMS ?? Math.max(1500, response.rail_planning?.search?.poll_after_ms ?? 0));
925
- response = await backend.getServiceExecution(serviceExecutionID, options.sinceSnapshot ? { sinceSnapshot: options.sinceSnapshot } : {});
926
- }
581
+ const response = await backend.getServiceExecution(serviceExecutionID);
927
582
  const envelope = servicesNextEnvelope(response);
928
- if (shouldWaitForServiceResult(response) && options.timeoutSeconds) {
929
- envelope.result = { ...envelope.result, waited_seconds: Math.round((Date.now() - started) / 1000), wait_timed_out: true };
930
- envelope.next = { command: `itpay services next ${serviceExecutionID} --timeout 120 --json`, reason: "稍后继续等待同一任务;不重新提交查询" };
931
- }
932
- journalTaskState(serviceExecutionID, envelope, options.env ?? process.env);
933
583
  writeCommandEnvelope(envelope, {
934
584
  ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
935
585
  ...(options.output ? { output: options.output } : {}),
936
586
  plainResult: servicesNextPlainResult(envelope.result),
937
587
  });
938
588
  }
939
- export async function runServicesPage(backend, serviceExecutionID, resultItemID, options = {}) {
940
- const offset = options.offset ?? 0;
941
- const limit = options.limit ?? 20;
942
- if (!Number.isInteger(offset) || offset < 0) {
943
- throw new CommandContractError("offset_invalid", "--offset must be a non-negative integer", "offset 必须是非负整数;本次未读取服务端分页。", [{ command: `itpay services page ${serviceExecutionID} ${resultItemID} --json`, reason: "从第一页重新读取" }]);
944
- }
945
- if (!Number.isInteger(limit) || limit < 1 || limit > 20) {
946
- throw new CommandContractError("limit_invalid", "--limit must be an integer from 1 to 20", "limit 必须是 1 到 20 的整数;本次未读取服务端分页。", [{ command: `itpay services page ${serviceExecutionID} ${resultItemID} --offset ${offset} --json`, reason: "用默认页大小重试" }]);
947
- }
948
- const response = await backend.getServiceExecutionResultItemPage(serviceExecutionID, resultItemID, offset, limit, limit === 1 ? undefined : "summary");
949
- const page = response.page;
950
- const container = (page.result ?? page);
951
- const exactCandidate = limit === 1 && Array.isArray(container.candidates) && container.candidates.length === 1
952
- ? container.candidates[0] : undefined;
953
- if (exactCandidate && typeof exactCandidate.train_code === "string" && Array.isArray(exactCandidate.seats)) {
954
- const offer = exactCandidate.booking_offer;
955
- const { booking_offer: _bookingOffer, ...candidateFacts } = exactCandidate;
956
- const seats = exactCandidate.seats.filter((seat) => seat.purchase_supported === true);
957
- writeCommandEnvelope({
958
- status: "exact_train_detail",
959
- result: {
960
- service_execution_id: serviceExecutionID, result_item_id: resultItemID, result_offset: offset,
961
- candidate: candidateFacts,
962
- ...(offer?.service_id && offer.selection_token ? { booking_template: {
963
- command: `itpay services run ${offer.service_id} --input-json <file> --json`,
964
- executable: false, required_input: ["file", "passengers", "seat_type"],
965
- seat_choices: seats.map((choice) => ({ seat_type: choice.seat_type, seat_name: choice.seat_name })),
966
- input_example: { selection: { token: offer.selection_token, seat_type: "<用户授权席别对应代码>" }, passengers: "<真实人数>" },
967
- } } : {}),
968
- },
969
- instruction: offer?.selection_token
970
- ? "这是已保存车次的当前详情;只按 purchase_supported 席别代码、库存与报价状态选择。购票模板的人数须按用户真实需求填写;查询价需在后续报价确认。若用户仅要求比较,答复后停止。"
971
- : "这条车次当前没有可用的购买凭据;说明席别与余票事实,不要猜测代码或构造订单。",
972
- next: null, recovery: [],
973
- }, { ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
974
- ...(options.output ? { output: options.output } : {}),
975
- });
976
- return;
977
- }
978
- // rail.progressive.v2 saved pages key journeys/journey_page; legacy result
979
- // items key candidates/catalog_page. Both are committed, read-only slices.
980
- const v2Page = (container.journey_page ?? {});
981
- const catalogPage = (container.catalog_page ?? v2Page);
982
- const nextOffset = typeof catalogPage.next_offset === "number" ? catalogPage.next_offset : null;
983
- const journeys = Array.isArray(container.journeys) ? container.journeys : [];
984
- const candidates = Array.isArray(container.candidates) ? container.candidates : journeys;
985
- const envelope = {
986
- status: candidates.length > 0 ? "result_page" : "result_page_end",
987
- result: {
988
- service_execution_id: response.service_execution_id,
989
- ...(response.service_capability_result_item_id ? { service_capability_result_item_id: response.service_capability_result_item_id } : {}),
990
- ...(response.snapshot_id ? { snapshot_id: response.snapshot_id } : {}),
991
- offset: catalogPage.offset ?? offset,
992
- limit: catalogPage.limit ?? limit,
993
- total: catalogPage.total ?? candidates.length,
994
- requested_limit: limit,
995
- truncated_reason: container.journey_page?.truncated_reason ?? null,
996
- count: catalogPage.count ?? candidates.length,
997
- next_offset: nextOffset,
998
- ...(journeys.length ? { journeys: journeys.map(value => compactRailJourney(value, serviceExecutionID, resultItemID)) }
999
- : candidates.some(value => typeof value === "object" && value !== null && "train_code" in value)
1000
- ? { candidates: candidates.map((value, index) => exactTrainRow(value, serviceExecutionID, resultItemID, (catalogPage.offset ?? offset) + index)) } : { page }),
1001
- },
1002
- instruction: "读取的是已保存结果的同版本分页,不重新查询、不消耗额度。先读完所需页,再合并回答用户;不要逐页播报。",
1003
- next: nextOffset !== null
1004
- ? { command: response.snapshot_id
1005
- ? `itpay services page ${serviceExecutionID} ${resultItemID} --cursor rcur_${nextOffset} --limit ${limit} --json`
1006
- : `itpay services page ${serviceExecutionID} ${resultItemID} --offset ${nextOffset} --limit ${limit} --json`,
1007
- reason: "读取同版本结果的下一页" }
1008
- : null,
1009
- recovery: offset > 0
1010
- ? [{ command: `itpay services page ${serviceExecutionID} ${resultItemID} --json`, reason: "回到第一页" }]
1011
- : [],
1012
- };
1013
- writeCommandEnvelope(envelope, {
1014
- ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
1015
- ...(options.output ? { output: options.output } : {}),
1016
- plainResult: plainValueLines(envelope.result),
1017
- });
1018
- }
1019
589
  export async function runServicesList(backend, options = {}) {
1020
590
  const limit = options.limit ?? 10;
1021
591
  if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
@@ -1028,7 +598,6 @@ export async function runServicesList(backend, options = {}) {
1028
598
  status: execution.status,
1029
599
  phase: execution.phase,
1030
600
  updated_at: execution.updated_at,
1031
- reader: { command: `itpay services next ${execution.service_execution_id} --json`, reason: "读取这笔已有服务" },
1032
601
  }));
1033
602
  const latest = executions[0];
1034
603
  const envelope = {
@@ -1037,7 +606,7 @@ export async function runServicesList(backend, options = {}) {
1037
606
  instruction: executions.length === 1
1038
607
  ? "只有一条可恢复记录;继续读取同一笔服务。"
1039
608
  : latest
1040
- ? "按原任务中已明确的执行ID读取对应reader;只有无法确定属于哪一笔任务时才询问用户。"
609
+ ? "用服务和状态说明这些可恢复记录;多个结果必须让用户选择。"
1041
610
  : "当前设备没有可恢复的 Service Execution;先读取已发布目录,不要猜测 ID。",
1042
611
  next: executions.length === 1
1043
612
  ? { command: `itpay services next ${latest.service_execution_id} --json`, reason: "继续唯一可恢复的服务" }
@@ -1049,209 +618,10 @@ export async function runServicesList(backend, options = {}) {
1049
618
  writeCommandEnvelope(envelope, {
1050
619
  ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
1051
620
  ...(options.output ? { output: options.output } : {}),
621
+ plainResult: executions.map((execution) => `${execution.service_execution_id}: ${execution.service_id} ${execution.status}/${execution.phase} updated=${execution.updated_at}`),
1052
622
  });
1053
623
  }
1054
- // Bound the final qualified output by whole saved rows; the cursor follows the emitted rows.
1055
- function writeSavedRailPage(envelope, options, key, offset, continuation) {
1056
- const result = envelope.result ?? {};
1057
- const rows = result[key];
1058
- if (!Array.isArray(rows))
1059
- throw new Error("saved page has no rows");
1060
- const bound = (offset === 0 ? 12 : 32) * 1024;
1061
- const render = (output) => writeCommandEnvelope(envelope, { ...options, output });
1062
- if (!options.all) {
1063
- for (;;) {
1064
- let text = "";
1065
- render(value => { text += value; });
1066
- if (Buffer.byteLength(text, "utf8") <= bound)
1067
- break;
1068
- if (rows.length <= 1) {
1069
- throw new CommandContractError("saved_row_too_large", "saved row exceeds the readable page limit", "这条已保存记录超过当前页面大小;事实仍保留,请读取该条详情。", rows[0]?.detail?.command ? [rows[0].detail] : []);
1070
- }
1071
- rows.pop();
1072
- result.truncated_reason = "cli_output_byte_budget";
1073
- result.count = rows.length;
1074
- if (key === "trains")
1075
- result.read_count = rows.length;
1076
- result.next_offset = offset + rows.length;
1077
- envelope.next = continuation(offset + rows.length);
1078
- }
1079
- }
1080
- writeCommandEnvelope(envelope, options);
1081
- }
1082
624
  export async function runServicesReadResult(backend, serviceExecutionID, options = {}) {
1083
- if (!options.snapshot && !options.journey) {
1084
- const model = await backend.getServiceExecution(serviceExecutionID);
1085
- if (model.execution.service_id === "itpay-rail-exact") {
1086
- const item = (model.current_result_items?.length ? model.current_result_items : model.result_items)?.[0];
1087
- if (!item) {
1088
- const preview = model.provider_invocations.at(-1)?.safe_result_preview;
1089
- const template = exactSmartTemplate(preview?.resolved_station_pair, serviceExecutionID);
1090
- if (template && preview?.catalog_total === 0) {
1091
- writeCommandEnvelope({ status: "no_result", result: { service_execution_id: serviceExecutionID,
1092
- scope: "station_pair", resolved_station_pair: preview.resolved_station_pair,
1093
- total: 0, smart_template: template },
1094
- instruction: "这一站对的成功观测为0条,不代表附近区域无车。若用户允许中转或原需求范围更广,保留原始区域/地点意图并沿 smart_template 引用本次事实;只要直达时不要自动转中转。",
1095
- next: null, recovery: [] }, { ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
1096
- ...(options.output ? { output: options.output } : {}) });
1097
- return;
1098
- }
1099
- await runServicesNext(backend, serviceExecutionID, options);
1100
- return;
1101
- }
1102
- const rows = [];
1103
- const startOffset = options.offset ?? 0;
1104
- const limit = options.limit ?? 20;
1105
- if (!Number.isInteger(startOffset) || startOffset < 0 || !Number.isInteger(limit) || limit < 1 || limit > 20) {
1106
- throw new CommandContractError("page_invalid", "offset must be non-negative and limit 1-20", "页码无效;请按返回的 next.command 读取。", []);
1107
- }
1108
- let offset = startOffset;
1109
- let total = 0;
1110
- let resolvedStationPair;
1111
- let nextOffset = startOffset;
1112
- do {
1113
- const response = await backend.getServiceExecutionResultItemPage(serviceExecutionID, item.service_capability_result_item_id, offset, limit);
1114
- const container = (response.page.result ?? response.page);
1115
- const candidates = Array.isArray(container.candidates) ? container.candidates : [];
1116
- resolvedStationPair ??= container.resolved_station_pair;
1117
- rows.push(...candidates.map((value, index) => exactTrainRow(value, serviceExecutionID, item.service_capability_result_item_id, offset + index)));
1118
- const page = (container.catalog_page ?? {});
1119
- total = typeof container.catalog_total === "number" ? container.catalog_total : Math.max(total, rows.length);
1120
- nextOffset = typeof page.next_offset === "number" && page.next_offset > offset ? page.next_offset : null;
1121
- offset = nextOffset ?? offset;
1122
- } while (options.all && nextOffset !== null);
1123
- writeSavedRailPage({
1124
- status: "ready",
1125
- result: { service_execution_id: serviceExecutionID, scope: "station_pair", resolved_station_pair: resolvedStationPair,
1126
- total, offset: startOffset, count: rows.length, read_count: rows.length,
1127
- ...(exactSmartTemplate(resolvedStationPair, serviceExecutionID, item.service_capability_result_item_id)
1128
- ? { smart_template: exactSmartTemplate(resolvedStationPair, serviceExecutionID, item.service_capability_result_item_id) } : {}),
1129
- trains: rows, ...(nextOffset !== null ? { next_offset: nextOffset } : {}) },
1130
- instruction: "这是同一已保存站对的车次行。按用户时限、席别和人数筛选;零条仅代表这一站对。纯比较答复后结束;已有购票委托则执行所选行 detail.command 读取当前席别、库存和购买凭据。",
1131
- next: nextOffset !== null ? { command: `itpay services read-result ${serviceExecutionID} --offset ${nextOffset} --limit ${limit} --json`, reason: "继续读取同一结果的剩余车次" } : null,
1132
- recovery: [],
1133
- }, { ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
1134
- ...(options.output ? { output: options.output } : {}),
1135
- all: Boolean(options.all),
1136
- }, "trains", startOffset, next => ({ command: `itpay services read-result ${serviceExecutionID} --offset ${next} --limit ${limit} --json`, reason: "读取同一保存结果的下一页" }));
1137
- return;
1138
- }
1139
- }
1140
- // rail.progressive.v2 planning read: --journey/--snapshot select committed
1141
- // planning evidence — a free owner-validated read that never touches the
1142
- // grant/Vault path. Without the selectors the authorized-delivery flow is
1143
- // unchanged.
1144
- // rail.planning catalog read: --snapshot returns compact rows for the
1145
- // committed rail.catalog.v3; journey detail remains available separately.
1146
- if (options.snapshot && !options.journey) {
1147
- const offset = options.offset ?? 0;
1148
- const limit = options.limit ?? 3;
1149
- if (!Number.isInteger(offset) || offset < 0 || !Number.isInteger(limit) || limit < 1 || limit > 20) {
1150
- throw new CommandContractError("page_invalid", "offset must be non-negative and limit 1-20", "目录页码无效;请按返回的 next.command 读取。", []);
1151
- }
1152
- const journeys = [];
1153
- let nextOffset = offset;
1154
- let total = 0;
1155
- let truncatedReason = null;
1156
- let qualificationCounts;
1157
- let planID;
1158
- let queryRevision;
1159
- while (nextOffset !== null) {
1160
- let response;
1161
- try {
1162
- response = await backend.getServiceExecutionResultItemPage(serviceExecutionID, options.snapshot, nextOffset, limit, "summary");
1163
- }
1164
- catch (error) {
1165
- if (error instanceof HttpError && error.code === "result_not_pageable") {
1166
- const committed = await backend.getRailPlanningCatalog(serviceExecutionID, options.snapshot);
1167
- decodeRailCatalogJourneys(committed.catalog);
1168
- }
1169
- throw error;
1170
- }
1171
- if (response.snapshot_id !== options.snapshot)
1172
- throw new Error("saved page snapshot changed");
1173
- planID = response.plan_id;
1174
- queryRevision = response.query_revision;
1175
- const container = (response.page.result ?? response.page);
1176
- const page = container.journey_page;
1177
- const rows = Array.isArray(container.journeys) ? container.journeys : [];
1178
- total = typeof page?.total === "number" ? page.total : rows.length;
1179
- qualificationCounts = page?.qualification_counts ?? qualificationCounts;
1180
- truncatedReason = page?.truncated_reason ?? truncatedReason;
1181
- journeys.push(...rows.map((card) => compactRailJourney(card, serviceExecutionID, options.snapshot)));
1182
- const following = page?.next_offset;
1183
- if (typeof following === "number" && following <= nextOffset)
1184
- throw new Error("saved page cursor did not advance");
1185
- nextOffset = typeof following === "number" ? following : null;
1186
- if (!options.all)
1187
- break;
1188
- }
1189
- writeSavedRailPage({
1190
- status: "ready",
1191
- result: {
1192
- service_execution_id: serviceExecutionID,
1193
- plan_id: planID,
1194
- snapshot_id: options.snapshot,
1195
- query_revision: queryRevision,
1196
- total,
1197
- qualification_counts: qualificationCounts,
1198
- requested_limit: limit,
1199
- truncated_reason: truncatedReason,
1200
- offset,
1201
- count: journeys.length,
1202
- next_offset: nextOffset,
1203
- journeys,
1204
- },
1205
- instruction: qualificationCounts === undefined
1206
- ? "此历史快照没有完整资格计数;线路仅供核对铁路事实,不能据此认定可购买。沿同快照 detail.command 查看详情。"
1207
- : qualificationCounts.eligible === 0
1208
- ? `查到 ${total} 个时刻组合,当前 0 条满足购票资格。请读取诊断线路的 detail.command 查看具体原因;修改条件须依据用户意愿。`
1209
- : `已查范围内合格时刻组合 ${qualificationCounts.eligible};本次展示 ${journeys.length} 个方案(首屏优先既有主方案/代表,计数不是中转通道数)。最快/低价须按对应方案和比较范围说明;未全范围排序只能说已读方案中。更多条件优先同快照调整偏好或沿 next 有序读所需页,不随机翻页、不无条件读完组合。未知费用不能当免费;detail.command 读取席别和整程购买条件,购买前实时报价。`,
1210
- next: nextOffset === null ? null : { command: `itpay services read-result ${serviceExecutionID} --snapshot ${options.snapshot} --offset ${nextOffset} --limit ${limit} --json`, reason: "仅当前问题还需未读线路时继续;已有合格完整结果可答复或按委托继续" },
1211
- recovery: [],
1212
- }, {
1213
- ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
1214
- ...(options.output ? { output: options.output } : {}),
1215
- all: Boolean(options.all),
1216
- }, "journeys", offset, next => ({ command: `itpay services read-result ${serviceExecutionID} --snapshot ${options.snapshot} --offset ${next} --limit ${limit} --json`, reason: "仅当前问题还需未读线路时继续;已有合格完整结果可答复或按委托继续" }));
1217
- return;
1218
- }
1219
- if (options.journey) {
1220
- const detail = await backend.getRailJourneyDetail(serviceExecutionID, options.journey, options.snapshot);
1221
- const planOffset = options.offset ?? 0;
1222
- const planLimit = options.limit ?? 3;
1223
- if (!Number.isInteger(planOffset) || planOffset < 0 || !Number.isInteger(planLimit) || planLimit < 1 || planLimit > 20) {
1224
- throw new CommandContractError("page_invalid", "offset must be non-negative and limit 1-20", "席别方案页码无效;请按返回的 next.command 读取。", []);
1225
- }
1226
- const plans = detail.journey.ticket_plans ?? [];
1227
- const selected = options.all ? plans : plans.slice(planOffset, planOffset + planLimit);
1228
- const nextOffset = !options.all && planOffset + selected.length < plans.length ? planOffset + selected.length : null;
1229
- writeCommandEnvelope({
1230
- status: "ready",
1231
- result: {
1232
- service_execution_id: serviceExecutionID,
1233
- plan_id: detail.plan_id,
1234
- snapshot_id: detail.snapshot_id,
1235
- query_revision: detail.query_revision,
1236
- journey: compactRailJourney(detail.journey, serviceExecutionID, detail.snapshot_id),
1237
- ticket_plan_total: plans.length,
1238
- ticket_plan_offset: planOffset,
1239
- ticket_plans: selected.map(plan => compactRailTicketPlan(plan, detail.journey.qualification_status === "eligible")),
1240
- },
1241
- instruction: "同一线路的完整车次与席别方案。已有所需席别和有效凭据即可按真实委托继续;只需比较则答复并停止,需要其他席别才读下一页。使用所选 ticket_plan 的整程 booking_template:一个 booking、统一报价、一个官方付款入口,各段分别出票,无联合锁票或接续保证。未返回整程模板时不能拼不同方案的腿或退回逐腿付款,按同一执行恢复。身份资料只在受保护 Checkout 页面填写。",
1242
- next: nextOffset === null ? null : { command: `itpay services read-result ${serviceExecutionID} --snapshot ${detail.snapshot_id} --journey ${options.journey} --offset ${nextOffset} --limit ${planLimit} --json`, reason: "仅需要其他席别时读取;已满足用户条件则停止翻页" },
1243
- ...(detail.journey?.booking_support === "single_leg" ? { interaction: {
1244
- schema_version: "itpay.interaction.v1", stage: "query_results_ready",
1245
- select_user_chosen: `itpay services action ${serviceExecutionID} --action select_journey --actor-type human --status approved --input journey_id=${detail.journey.journey_id} --json`,
1246
- select_delegated: `itpay services action ${serviceExecutionID} --action select_journey --actor-type agent --status approved --input journey_id=${detail.journey.journey_id} --input selection_mode=delegated --json`,
1247
- } } : {}),
1248
- recovery: [],
1249
- }, {
1250
- ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
1251
- ...(options.output ? { output: options.output } : {}),
1252
- });
1253
- return;
1254
- }
1255
625
  const response = await backend.getGrantedServiceResult(serviceExecutionID);
1256
626
  let orderID;
1257
627
  try {
@@ -1268,869 +638,68 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
1268
638
  plainResult: servicesNextPlainResult(envelope.result),
1269
639
  });
1270
640
  }
1271
- function exactSmartTemplate(pair, executionID, resultItemID) {
1272
- if (!pair || typeof pair !== "object")
1273
- return null;
1274
- const value = pair;
1275
- if (!["origin_name", "origin_code", "destination_name", "destination_code", "travel_date"].every((key) => typeof value[key] === "string" && value[key]))
1276
- return null;
1277
- return { executable: false, verified_station_pair: value,
1278
- instruction: "已核验站对是复用事实,不是完整用户查询。仅用户明确锁站才填 kind=station;区域/地点从原始需求填回,保留真实人数、时限和偏好,不默认为一人。",
1279
- command: "itpay services run itpay-rail-smart --input-json <原始需求.json> --json",
1280
- required_input: ["endpoints", "passengers", "用户时限与偏好"],
1281
- input_example: { endpoints: { origin: { text: "<原始出发行政范围/地点/用户指定站>" }, destination: { text: "<原始到达行政范围/地点/用户指定站>" } },
1282
- travel_date: value.travel_date, passengers: "<真实人数>", reuse_from: { execution_id: executionID,
1283
- ...(resultItemID ? { result_item_id: resultItemID } : {}) } } };
1284
- }
1285
- function exactTrainRow(candidate, executionID, itemID, offset) {
1286
- return { ...compactExactTrain(candidate), result_item_id: itemID, result_offset: offset,
1287
- detail: { command: `itpay services page ${executionID} ${itemID} --offset ${offset} --limit 1 --json`, reason: "读取此车次席别与服务端购买模板" } };
1288
- }
1289
- function compactExactTrain(candidate) {
1290
- const seats = Array.isArray(candidate.seats) ? candidate.seats : [];
1291
- return {
1292
- candidate_id: candidate.candidate_id,
1293
- train_code: candidate.train_code,
1294
- from_station: candidate.from_station,
1295
- from_station_code: candidate.from_station_code,
1296
- to_station: candidate.to_station,
1297
- to_station_code: candidate.to_station_code,
1298
- travel_date: candidate.travel_date,
1299
- departure: candidate.departure,
1300
- arrival: candidate.arrival,
1301
- purchase_supported: candidate.purchase_supported ?? Boolean(candidate.booking_offer),
1302
- seats: seats.map((seat) => ({ seat_type: seat.seat_type, seat_name: seat.seat_name,
1303
- availability_text: seat.availability_text, remaining: seat.remaining,
1304
- fare_minor: seat.fare_minor, quoted_total_minor: seat.quoted_total_minor,
1305
- purchase_supported: seat.purchase_supported, requires_price_refresh: seat.requires_price_refresh })),
1306
- };
1307
- }
1308
- function railLegSeatSummary(seat) {
1309
- return {
1310
- passenger_index: seat.passenger_index,
1311
- ...(seat.seat_type_name ? { seat_type_name: seat.seat_type_name } : {}),
1312
- ...(seat.seat_label || seat.coach_no || seat.seat_no
1313
- ? { seat: seat.seat_label ?? [seat.coach_no ? `${seat.coach_no}车` : "", seat.seat_no ?? ""].join("") }
1314
- : {}),
1315
- confirmed: seat.confirmed,
1316
- };
1317
- }
1318
- function railBookingEnvelope(model) {
1319
- const rail = model.rail_booking;
1320
- const execution = model.execution;
1321
- const orderID = (model.current_delivery ?? model.delivery_bindings.at(-1))?.order_id;
1322
- const legs = rail.legs.map((leg) => ({
1323
- leg_index: leg.leg_index,
1324
- state: leg.state,
1325
- issued: leg.issued,
1326
- ...(leg.error_code ? { error_code: leg.error_code } : {}),
1327
- arrival_days: leg.arrival_days ?? 0,
1328
- ...(leg.details_pending ? { details_pending: true } : {}),
1329
- ...(leg.supplier_state ? { supplier_state: leg.supplier_state } : {}),
1330
- ...(leg.train_code ? { train_code: leg.train_code } : {}),
1331
- ...(leg.travel_date ? { travel_date: leg.travel_date } : {}),
1332
- ...(leg.from || leg.to ? { route: `${leg.from ?? ""} → ${leg.to ?? ""}` } : {}),
1333
- ...(leg.departure || leg.arrival ? { time: `${leg.departure ?? ""}–${leg.arrival ?? ""}` } : {}),
1334
- ...(leg.seat_name ? { seat_name: leg.seat_name } : {}),
1335
- ...(leg.seat_request ? { seat_request: leg.seat_request } : {}),
1336
- ...(leg.seat_preferences?.length ? { seat_preferences: leg.seat_preferences } : {}),
1337
- ...(leg.seats?.length ? { seats: leg.seats.map(railLegSeatSummary) } : {}),
1338
- }));
1339
- const result = {
1340
- service_execution_id: execution.service_execution_id,
1341
- ...(orderID ? { order_id: orderID } : {}),
1342
- rail: { state: rail.state, issued_legs: rail.issued_legs, total_legs: rail.legs.length, message: rail.message, legs },
1343
- };
1344
- if (rail.state === "issued" && rail.issued_legs === rail.legs.length && rail.legs.length > 0) {
1345
- const detailsPending = rail.legs.some((leg) => leg.details_pending);
1346
- return {
1347
- status: "issued",
1348
- result: { ...result, ...(orderID ? { aftercare: { command: `itpay order ${orderID} --json`, when: "仅用户请求核对订单或售后时" } } : {}) },
1349
- instruction: `告诉用户:车票已出票,座位以实际出票为准,平台不提供 12306 票号,可在订单页核对行程。不要在对话中索要乘车人身份信息。${detailsPending ? "部分席位信息未能同步,请前往 12306 核对行程;不要重复购买。" : ""}`,
1350
- next: null,
1351
- recovery: [],
1352
- };
1353
- }
1354
- if (rail.state === "manual_review") {
1355
- return {
1356
- status: "manual_review",
1357
- result,
1358
- instruction: `告诉用户:付款已确认,已出票 ${rail.issued_legs}/${rail.legs.length},其余需要人工核对;请勿重复付款或重新下单,已出票的车票会保留。未知结果先对账,退款按对应段处理,不承诺整笔退款或时效。`,
1359
- next: orderID ? { command: `itpay order ${orderID} --json`, reason: "查看同一订单当前状态" } : null,
1360
- recovery: [],
1361
- };
1362
- }
1363
- // A rail booking run only exists after a verified payment, so payment is
1364
- // confirmed here — but only `pending` actually means supplier issuance is in
1365
- // flight. Any state added later falls back to a conservative re-read instead
1366
- // of claiming issuance.
1367
- if (rail.state === "pending") {
1368
- return {
1369
- status: "issuing",
1370
- result,
1371
- instruction: `告诉用户:付款已确认,已出票 ${rail.issued_legs}/${rail.legs.length},后台继续处理;付款成功不代表整程出票,请勿重复购买或再次付款。稍后只读取同一任务。`,
1372
- next: { command: `itpay services next ${execution.service_execution_id} --json`, reason: "稍后读取同一出票任务" },
1373
- recovery: [],
1374
- };
1375
- }
1376
- return {
1377
- status: "processing",
1378
- result,
1379
- instruction: `告诉用户:付款已确认,订单处理状态待确认(${rail.state});付款成功不代表已出票,请勿重复购买或再次付款。稍后只读取同一任务。`,
1380
- next: { command: `itpay services next ${execution.service_execution_id} --json`, reason: "稍后读取同一出票任务" },
1381
- recovery: [],
1382
- };
1383
- }
1384
- // Public summaries and saved details use the same evidenced units and scopes.
1385
- function railJourneyFacts(card) {
1386
- const offer = card.representative_offer;
1387
- const metrics = card.representative_metrics;
1388
- const minor = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
1389
- const time = (value) => typeof value === "string" ? Date.parse(/(?:Z|[+-]\d{2}:\d{2})$/.test(value) ? value.replace(" ", "T") : `${value.replace(" ", "T")}+08:00`) : NaN;
1390
- const start = time(card.rides?.[0]?.departure), end = time(card.rides?.at(-1)?.arrival);
1391
- return {
1392
- rail_fare_fen: minor(offer?.fare_minor), service_fee_fen: minor(offer?.service_fee_minor),
1393
- rail_payable_fen: minor(offer?.rail_payable_minor), currency: offer?.currency ?? null,
1394
- price_basis: "铁路应付参考额=铁路票价+各票段服务费;不含未知地面费用,以新报价为准。",
1395
- rail_duration_minutes: Number.isFinite(start) && Number.isFinite(end) && end >= start ? Math.round((end - start) / 60000) : null,
1396
- door_to_door_minutes: metrics?.duration_basis === "door_to_door" ? minor(metrics.door_to_door_min) : null,
1397
- ...(metrics?.duration_basis !== "door_to_door" ? { ground_time_note: "接驳未完整核验,不能把铁路时长称为门到门时间。" } : {}),
1398
- estimated_trip_total: metrics?.estimated_total_cost ?? null,
1399
- estimate_basis: "仅是已核验费用范围的估算,不是含全部地面费用的实付额。",
1400
- };
1401
- }
1402
- function publicBookingSupport(card) {
1403
- if (card.booking_support !== "separate_legs_only")
1404
- return card.booking_support;
1405
- return card.qualification_status === "eligible" && card.ticket_plans?.some(plan => plan.qualification_status === "eligible" && plan.booking_input) ? "journey" : "read_only";
1406
- }
1407
- function compactRailJourney(card, serviceExecutionID, snapshotID) {
1408
- if (!card.journey_id)
1409
- throw new Error("saved journey has no stable ID");
1410
- const rides = (card.rides ?? []).map((ride) => ({
1411
- train_code: ride.train_code ?? (Array.isArray(ride.train_codes) ? ride.train_codes.join("/") : null),
1412
- from_station: ride.from_station ?? null, from_station_code: ride.from_station_code ?? null,
1413
- departure: ride.departure ?? null, to_station: ride.to_station ?? null,
1414
- to_station_code: ride.to_station_code ?? null, arrival: ride.arrival ?? null,
1415
- ...(typeof ride.wait_minutes === "number" ? { wait_minutes: ride.wait_minutes } : {}),
1416
- ...(typeof ride.minimum_connection_minutes === "number" ? { required_buffer_minutes: ride.minimum_connection_minutes } : {}),
1417
- ...(ride.same_run === true ? { same_run: true } : {}),
1418
- }));
1419
- const offer = card.representative_offer;
1420
- const metrics = card.representative_metrics;
1421
- const ticketSeats = (offer?.ticket_offers ?? []).map((raw) => ({
1422
- leg_index: raw.leg_index, seat_type: raw.seat_type, seat_name: raw.seat_name,
1423
- availability: raw.availability_text, remaining: raw.inventory_exact === true ? raw.remaining : null,
1424
- }));
1425
- return {
1426
- journey_ref: card.journey_id,
1427
- ...(card.profile_refs?.balanced ? { profile_ref: card.profile_refs.balanced } : {}),
1428
- default_layer: card.default_layer ?? "main",
1429
- ...(card.backup_reason ? { backup_reason: card.backup_reason } : {}),
1430
- ...(card.backup_baseline_min != null ? { backup_baseline_min: card.backup_baseline_min } : {}),
1431
- ...(card.backup_delta_min != null ? { backup_delta_min: card.backup_delta_min } : {}),
1432
- route: card.route_text ?? (card.route_names ?? card.route ?? []).join("→"),
1433
- rides,
1434
- ...(card.booking_support === "single_leg" && card.qualification_status === "eligible" && card.booking_offer ? {
1435
- booking_template: { command: `itpay services run ${card.booking_offer.service_id} --input-json <购票.json> --json`, executable: false,
1436
- seat_choices: ticketSeats.map(seat => ({ seat_type: seat.seat_type, seat_name: seat.seat_name })),
1437
- input_example: { selection: { token: card.booking_offer.selection_token, seat_type: "<返回席别代码>" }, passengers: card.passengers ?? offer?.ticket_offers?.[0]?.passengers ?? "<真实人数>" },
1438
- required_input: ["file", "真实席别选择及人数"] },
1439
- } : {}),
1440
- ...(ticketSeats.length ? { seats: ticketSeats } : {}),
1441
- booking_support: publicBookingSupport(card),
1442
- ...(publicBookingSupport(card) === "read_only" ? { purchase_recovery: { command: `itpay services next ${serviceExecutionID} --json`, reason: "历史快照无整程凭据;读取同一执行合法准备路径,不拆单" } } : {}),
1443
- ticket_plan_refs: (card.ticket_plans ?? []).map(plan => plan.ticket_plan_ref),
1444
- transfer_wait_minutes: card.transfer_wait_minutes ?? [],
1445
- ground_summary: card.ground_summary ?? null,
1446
- passengers: card.passengers ?? offer?.ticket_offers?.[0]?.passengers ?? null,
1447
- availability: card.availability ?? "unknown",
1448
- qualification_status: card.qualification_status ?? "unknown",
1449
- ...(card.qualification_reasons?.length ? { qualification_reasons: card.qualification_reasons } : {}),
1450
- ...railJourneyFacts(card),
1451
- cost_basis: metrics?.cost_basis ?? "unknown",
1452
- duration_basis: metrics?.duration_basis ?? "unknown",
1453
- timezone: "Asia/Shanghai",
1454
- ...(card.risk_notes?.length ? { risk_notes: card.risk_notes } : {}),
1455
- ...(card.observed_at ? { observed_at: card.observed_at } : {}),
1456
- detail: { command: `itpay services read-result ${serviceExecutionID} --snapshot ${snapshotID} --journey ${card.journey_id} --json`,
1457
- reason: card.qualification_status === "eligible" ? "读取此完整线路的席别、报价和购买条件" : "读取此线路的铁路事实和不可购买原因" },
1458
- };
1459
- }
1460
- function compactRailTicketPlan(raw, journeyEligible) {
1461
- const legs = Array.isArray(raw.legs) ? raw.legs : [];
1462
- const seats = Array.isArray(raw.seat_offers) ? raw.seat_offers : [];
1463
- const tickets = Array.isArray(raw.ticket_offers) ? raw.ticket_offers : [];
1464
- return {
1465
- ticket_plan_ref: raw.ticket_plan_ref,
1466
- passengers: raw.passenger_count ?? (Array.isArray(raw.ticket_offers) ? raw.ticket_offers[0]?.passengers : null) ?? null,
1467
- purchase_support: raw.purchase_support === "separate_legs_only" ? "read_only" : raw.purchase_support ?? "none",
1468
- ...(legs.length > 1 && !raw.booking_input ? { booking_unavailable: "这份快照只有历史铁路事实,没有完整整程购买凭据;不能拆腿或拼接其他快照凭据。读取同一执行当前状态,按其购买准备或过期恢复动作继续。" } : {}),
1469
- qualification_status: raw.qualification_status ?? "unknown",
1470
- qualification_reasons: raw.qualification_reasons ?? [],
1471
- rail_amount_fen: raw.rail_amount_fen ?? null,
1472
- service_fee_fen: raw.service_fee_fen ?? null,
1473
- price_verified: raw.rail_amount_verified === true,
1474
- ...(journeyEligible && raw.qualification_status === "eligible" && raw.booking_input && typeof raw.booking_input === "object" ? {
1475
- booking_template: { command: "itpay services run itpay-rail-booking --input-json <journey.json> --json", executable: false, ticket_plan_ref: raw.ticket_plan_ref, input_example: raw.booking_input, required_input: ["file", "passengers"] },
1476
- } : {}),
1477
- legs: legs.map((leg, index) => ({
1478
- leg_index: index,
1479
- travel_date: leg.travel_date ?? tickets.find(ticket => ticket.leg_index === index)?.travel_date ?? null,
1480
- ...(() => {
1481
- const ticket = tickets.find(ticket => ticket.leg_index === index) ?? (tickets[index]?.leg_index === undefined ? tickets[index] : undefined);
1482
- const offer = ticket?.booking_offer;
1483
- return legs.length === 1 && offer?.service_id && offer.selection_token && journeyEligible && raw.qualification_status === "eligible" ? {
1484
- booking_template: { command: `itpay services run ${offer.service_id} --input-json <购票.json> --json`, executable: false,
1485
- ticket_plan_ref: raw.ticket_plan_ref, leg_index: index,
1486
- seat_choices: [{ seat_type: ticket?.seat_type, seat_name: ticket?.seat_name }],
1487
- input_example: { selection: { token: offer.selection_token, seat_type: ticket?.seat_type ?? "<该方案席别代码>" },
1488
- passengers: ticket?.passengers ?? raw.passenger_count ?? "<真实人数>" },
1489
- required_input: ["file", "真实人数", "该方案席别代码"],
1490
- },
1491
- } : legs.length > 1 ? {} : { booking_unavailable: "本段没有当前可用的服务端凭据;不能猜代码或例行重查。保留快照,按当前执行的恢复动作处理;过期凭据需真实新验证。" };
1492
- })(),
1493
- train_code: leg.train_code ?? null,
1494
- from_station: leg.from_station ?? null, from_station_code: leg.from_station_code ?? null,
1495
- departure: leg.departure ?? null,
1496
- to_station: leg.to_station ?? null, to_station_code: leg.to_station_code ?? null,
1497
- arrival: leg.arrival ?? null,
1498
- seats: (seats[index] ?? []).map((seat) => ({ seat_type: seat.seat_type, seat_name: seat.seat_name,
1499
- availability: seat.availability_text, remaining: seat.inventory_exact === true ? seat.remaining : null,
1500
- unit_fare_fen: seat.fare_minor, eligible: seat.eligible === true })),
1501
- })),
1502
- };
1503
- }
1504
- function railJourneySummary(card, serviceExecutionID, snapshotID) {
1505
- const rides = Array.isArray(card.rides) ? card.rides : [];
1506
- const trains = rides.map((ride) => ride.train_code).filter(Boolean);
1507
- const first = rides[0];
1508
- const last = rides.at(-1);
1509
- const offer = card.representative_offer;
1510
- const metrics = card.representative_metrics;
1511
- return {
1512
- journey_id: card.journey_id,
1513
- ...(snapshotID ? (() => { const detail = compactRailJourney(card, serviceExecutionID, snapshotID); return { rides: detail.rides, seats: detail.seats, default_layer: detail.default_layer, detail: detail.detail, ...(detail.booking_template ? { booking_template: detail.booking_template } : {}) }; })() : {}),
1514
- route: (card.route_names?.length ? card.route_names : card.route ?? []).join("→"),
1515
- ...(trains.length ? { trains } : {}),
1516
- ...(first?.departure || last?.arrival ? { time: `${first?.departure ?? ""}–${last?.arrival ?? ""}` } : {}),
1517
- ...(offer && typeof offer.rail_payable_minor === "number" ? { price: formatMoney(offer.rail_payable_minor, offer.currency) } : { price: "unknown" }),
1518
- ...railJourneyFacts(card),
1519
- ...(metrics ? { calculation_scope: {
1520
- origin: metrics.origin_scope ?? "legacy_unknown", destination: metrics.destination_scope ?? "legacy_unknown",
1521
- duration_basis: metrics.duration_basis ?? "legacy_unknown", arrival_basis: metrics.arrival_basis ?? "legacy_unknown",
1522
- cost_basis: metrics.cost_basis ?? "legacy_unknown",
1523
- duration_minutes: metrics.duration_basis === "door_to_door" ? metrics.door_to_door_min ?? null : railJourneyFacts(card).rail_duration_minutes,
1524
- } } : {}),
1525
- ...(card.decision_role ? { decision_role: card.decision_role } : {}),
1526
- ...(card.explanation?.length ? { explanation: card.explanation } : {}),
1527
- ...(card.reason_codes?.length ? { reason_codes: card.reason_codes } : {}),
1528
- ...(card.tradeoff_codes?.length ? { tradeoff_codes: card.tradeoff_codes } : {}),
1529
- ...(card.recommended_profile ? { recommended_profile: card.recommended_profile,
1530
- recommended_price_basis: "推荐席别及已核验接驳估算以recommended_profile为准;按其ticket_plan_ref/offer_refs读取完整方案,新报价确认后下单。" } : {}),
1531
- availability: card.availability,
1532
- booking_support: publicBookingSupport(card),
1533
- ...(publicBookingSupport(card) === "read_only" ? { purchase_recovery: { command: `itpay services next ${serviceExecutionID} --json`, reason: "历史快照无整程凭据;读取同一执行当前合法购买准备路径,不拆单" } } : {}),
1534
- ...(card.observed_at ? { observed_at: card.observed_at } : {}),
1535
- ...(card.risk_notes?.length ? { risk_notes: card.risk_notes } : {}),
1536
- ...(card.booking_support === "single_leg"
1537
- ? {
1538
- select_user_chosen: `itpay services action ${serviceExecutionID} --action select_journey --actor-type human --status approved --input journey_id=${card.journey_id} --json`,
1539
- // Delegated selection under explicit user rules is an agent action —
1540
- // never record it as a human pick.
1541
- select_delegated: `itpay services action ${serviceExecutionID} --action select_journey --actor-type agent --status approved --input journey_id=${card.journey_id} --input selection_mode=delegated --json`,
1542
- }
1543
- : {}),
1544
- };
1545
- }
1546
- // rail.progressive.v2 read projection: committed snapshots only; polling never
1547
- // triggers supplier calls. Recommendation is absent until the decision stage
1548
- // commits — alternatives still render so users can compare early.
1549
- function railPlanningEnvelope(model) {
1550
- const plan = model.rail_planning;
1551
- const se = model.execution.service_execution_id;
1552
- const expansion = plan.search?.expansion_status ?? "running";
1553
- const counts = plan.search?.counts;
1554
- const journeys = [plan.recommendation, ...(plan.alternatives ?? [])].filter(Boolean);
1555
- const hasEligible = typeof counts?.journeys_eligible === "number"
1556
- ? counts.journeys_eligible > 0 : journeys.length > 0;
1557
- const changedScopeHint = plan.query_input && !/supplier|provider|dispatch|map|amap|permission/.test(plan.search?.reason ?? "")
1558
- ? "若原条件锁定车站,可提议仅放宽一端到原站周边;有已核实地点证据才建议具体站,不能说肯定有票。先询问用户是否接受该端范围变化;同意后按new_query_template开新查询,未同意则保持原站。"
1559
- : /supplier|provider|dispatch|map|amap|permission/.test(plan.search?.reason ?? "")
1560
- ? "依赖故障按原恢复动作处理,不通过换站掩盖故障。"
1561
- : "如需改变端点,先取得真实范围变更同意,再按正式铁路指南保留原条件发起新查询。";
1562
- const cards = journeys.filter(card => card.journey_id !== plan.recommendation?.journey_id)
1563
- .map((card) => railJourneySummary(card, se, plan.catalog_snapshot_id ?? plan.snapshot_id));
1564
- // Journey counts come from the committed catalog — real combination counts,
1565
- // never seat-row counts. Transfer buckets follow the verified transfer_count.
1566
- const journeyMix = counts && (counts.journeys_total ?? 0) > 0
1567
- ? `已保存${counts.journeys_total}个具体车次时刻组合,其中${counts.journeys_eligible ?? "未知"}条当前满足铁路资格(直达${counts.journeys_direct ?? 0}/1中转${counts.journeys_one_transfer ?? 0}/2中转${counts.journeys_multi_transfer ?? 0};这些不是不同站序的数量)`
1568
- : "";
1569
- const pairProgress = counts && (counts.pairs_total ?? 0) > 0
1570
- ? `已检查${counts.pairs_checked ?? 0}/${counts.pairs_total}个附近站对`
1571
- : "";
1572
- // §S5: failed pairs are named honestly — never implied by "checked" and
1573
- // never rephrased as "no direct exists".
1574
- // §S5: a rules-only recommendation after a failed model attempt is honest —
1575
- // "rules finished, AI didn't", never silently dressed as a model pick.
1576
- const modelDegradedHint = plan.search?.model_outcome === "fallback"
1577
- ? "规则推荐已完成,AI模型未完成;推荐有效,但置信度说明以规则结果为准。"
1578
- : "";
1579
- const failedPairsHint = counts && (counts.pairs_failed ?? 0) > 0
1580
- ? `有${counts.pairs_failed}个站对暂未查明,不能确认没有直达;已有结果仍可查看。`
1581
- : "";
1582
- const result = {
1583
- service_execution_id: se,
1584
- rail_planning: {
1585
- readiness: plan.readiness,
1586
- query_revision: plan.query_revision,
1587
- ...(plan.query_input ? { endpoints: plan.query_input.endpoints } : {}),
1588
- snapshot_id: plan.snapshot_id,
1589
- expansion_status: expansion,
1590
- ...(plan.search?.phase ? { phase: plan.search.phase } : {}),
1591
- ...(plan.search?.transfer_status ? { transfer_status: plan.search.transfer_status } : {}),
1592
- ...(plan.search?.transition_reason ? { transition_reason: plan.search.transition_reason } : {}),
1593
- ...(plan.search?.authorization ? { authorization: plan.search.authorization } : {}),
1594
- ...(plan.search?.expansion_target ? { expansion_target: plan.search.expansion_target } : {}),
1595
- ...(plan.search?.decision_source ? { decision_source: plan.search.decision_source } : {}),
1596
- ...(plan.search?.model_outcome ? { model_outcome: plan.search.model_outcome } : {}),
1597
- ...(plan.search?.reason ? { reason: plan.search.reason } : {}),
1598
- ...(plan.search?.poll_after_ms ? { poll_after_ms: plan.search.poll_after_ms } : {}),
1599
- ...(plan.result_not_updated ? { result_not_updated: true } : {}),
1600
- ...(counts ? { counts } : {}),
1601
- ...(plan.coverage ? { coverage: plan.coverage } : {}),
1602
- },
1603
- ...(plan.notices?.length ? { notices: plan.notices } : {}),
1604
- ...(plan.recommendation ? { recommendation: railJourneySummary(plan.recommendation, se, plan.catalog_snapshot_id ?? plan.snapshot_id) } : {}),
1605
- ...(cards.length ? { journeys: cards } : {}),
1606
- ...((plan.catalog_snapshot_id ?? plan.snapshot_id) ? { full_result: { command: `itpay services read-result ${se} --snapshot ${plan.catalog_snapshot_id ?? plan.snapshot_id} --json`, meaning: "读取已保存的完整目录;推荐和journeys仅是摘要" } } : {}),
1607
- ...(!hasEligible && ["paused", "complete", "expired"].includes(expansion) && plan.query_input && plan.catalog_snapshot_id ? { new_query_template: {
1608
- command: "itpay services run itpay-rail-smart --input-json <file> --json",
1609
- executable: false,
1610
- input_example: { ...plan.query_input, reuse_from: { execution_id: se, snapshot_id: plan.catalog_snapshot_id } },
1611
- meaning: "先取得真实范围变更同意,再复制完整input_example,仅改获准端。换具体站用station及匹配名称/站码;考虑原站周边用place及原站名,删除station_code,经正常地点解析或使用已核实POI/坐标;只有明确同意整个市才用area及完整行政名,删除POI/坐标/站码。日期、人数、时限及偏好保持;新执行不改变旧查询或订单。引用仅限同owner未过期事实,跨owner拒绝;日期变更不复用旧库存。",
1612
- } } : {}),
1613
- ...(plan.available_actions?.length ? { available_actions: plan.available_actions } : {}),
1614
- };
1615
- const nextPoll = {
1616
- command: `itpay services next ${se} --timeout 120${plan.snapshot_id ? ` --since-snapshot ${plan.snapshot_id}` : ""} --json`,
1617
- reason: "仍需更多结果时有界等待同一规划(不触发供应商调用)",
1618
- };
1619
- switch (expansion) {
1620
- case "complete":
1621
- if (plan.search?.reason === "ground_evidence_required") {
1622
- return {
1623
- status: "awaiting_input", result,
1624
- instruction: `已查到铁路班次,但具体地址的接驳或出站证据不足,${plan.query_input?.arrive_before || plan.query_input?.arrive_by ? "无法验证用户要求的到达时限" : "尚不能核验相应地点条件或门到门时间"};剩余站对未继续查询,不能说没有车。说明已查范围和缺失事实,请补充准确起终点地址或可验证的接驳/出站信息;补齐后按原要求规划,不默认扩大搜索。`,
1625
- next: null, recovery: [],
1626
- };
1627
- }
1628
- if (!hasEligible && !plan.result_not_updated) {
1629
- return {
1630
- status: "no_result", result,
1631
- instruction: `本次已查询范围内没有当前合格线路。原因以 notices、coverage 和 search.reason 为准;缺票、接续失败、依赖故障或未覆盖范围不能改说成没有车。已有铁路事实可从 full_result 读取。${changedScopeHint}后续使用 available_actions 中与用户意图一致的动作。`,
1632
- next: null, recovery: [],
1633
- };
1634
- }
1635
- return {
1636
- status: "ready",
1637
- result,
1638
- // §7.3: the lead line carries real combination counts bucketed by
1639
- // verified transfer count — seat rows never inflate the journey count.
1640
- instruction: `${plan.result_not_updated ? "快照没有更新,空增量不表示目录为空。" : ""}${hasEligible && journeys.length === 0 ? `已保存${counts?.journeys_eligible ?? "若干"}条合格线路,执行 full_result.command 读取。` : ""}${journeyMix ? `本次共${journeyMix}(席别不重复计数)。` : ""}${plan.search?.reason === "budget_exhausted" ? "本次查询已达到上限,以下是已查到的方案,搜索范围尚未全部核验。" : "本轮规划已完成。"}推荐和journeys是摘要;追问其他车次先读取 full_result 的已保存完整目录。比较需求说明首选与备选取舍后结束;已有明确代选委托则沿合格线路详情继续,只补缺失的真实同意,身份信息仍在官方页面填写。`,
1641
- next: null,
1642
- recovery: [],
1643
- };
1644
- case "paused": {
1645
- if (plan.search?.reason === "legacy_plan_requires_new_search") {
1646
- return {
1647
- status: "awaiting_input", result,
1648
- instruction: "这次规划使用旧算法快照,当前版本已暂停后续查询。已保存的结果仍可读取;若需继续覆盖未查范围,请按原出行条件发起一项新的 Smart 查询。不要把旧快照解释为新版完整结果。",
1649
- next: null, recovery: [],
1650
- };
1651
- }
1652
- // §7.1 case 1: usable directs committed while ranked scope remains. The
1653
- // message must carry real counts, and expansion is a user choice — the
1654
- // Agent must not treat "expandable" as consent already given.
1655
- const directHint = (plan.search?.reason === "direct_options_ready" || plan.search?.phase === "direct_ready") && journeyMix
1656
- ? `本次找到${counts?.journeys_direct ?? 0}趟可选直达(${journeyMix}),${pairProgress || "部分站对已核验"},中转尚未展开。可以直接选一趟;时间或价格不合适时可继续搜索其它直达及中转,等待会更久。`
1657
- : "";
1658
- // §2.1: a delivered-pause at the authorized transfer depth names what a
1659
- // consented expand buys next — verified-empty is "nothing at this
1660
- // depth", never "no routes exist".
1661
- const transferHint = (plan.search?.reason === "transfer_options_ready" || plan.search?.phase === "transfer_ready") && journeyMix
1662
- ? `一次中转范围已核验完毕并交付(${journeyMix})。可以直接选用;也可以继续比较两次中转方案,查询会明显更久。`
1663
- : plan.search?.reason === "no_options_verified"
1664
- ? `当前授权深度内的范围已核验完、未找到可用方案——这不等于没有其它路线。可授权继续更深的两次中转搜索(耗时显著增加),或停止。`
1665
- : "";
1666
- const expandHint = plan.search?.expansion_target === "two_transfer"
1667
- ? "expand_search 将授权一次更深的两次中转搜索(等待更久)"
1668
- : plan.search?.expansion_target === "more_direct_and_one_transfer"
1669
- ? "expand_search 将补齐剩余直达并展开一次中转"
1670
- : "expand_search 是唯一会再消耗供应商配额的命令,其余均为本地读取";
1671
- // §7.1 case 3 on a recoverable pause: unverified scope is named with real
1672
- // counts, and the phrasing never implies the whole nearby range ran.
1673
- const partialPause = !directHint && counts && (counts.pairs_total ?? 0) > (counts.pairs_checked ?? 0)
1674
- ? `已保存${counts.journeys_total ?? 0}个具体车次时刻组合,其中${counts.journeys_eligible ?? "未知"}条当前满足铁路资格;还有${(counts.pairs_total ?? 0) - (counts.pairs_checked ?? 0)}个站对/部分中转路径未核验。以下是已确认结果,不能据此断定没有其它走法。`
1675
- : "";
1676
- // §S5: a dispatch whose outcome is unknown pauses for reconciliation —
1677
- // the message names the state, never rephrases it as a normal pause or
1678
- // a no-directs result, and never offers a blind retry (expand_search is
1679
- // withheld by the projection too).
1680
- if (plan.search?.reason === "dispatch_outcome_unknown") {
1681
- return {
1682
- status: "awaiting_input",
1683
- result,
1684
- instruction: `${counts?.journeys_total ? `已保存${counts.journeys_total}个已核验组合仍可查看。` : ""}上一次查询发送结果未确认(可能已部分查询),系统已暂停并等待对账,不会自动重复发送,也不能据此断定没有车次。展示已有卡片,稍后可重新读取最新状态。`,
1685
- next: null,
1686
- recovery: [{ command: `itpay services next ${se} --json`, reason: "稍后读取对账后的最新状态" }],
1687
- };
1688
- }
1689
- return {
1690
- status: "awaiting_input",
1691
- result,
1692
- instruction: `${modelDegradedHint}${failedPairsHint}${directHint}${transferHint}${partialPause}${hasEligible ? "规划已发布可用结果并暂停扩展:展示现有卡片与 available_actions" : `当前已查范围尚未找到合格线路,搜索已暂停;未查范围与故障以coverage/notices为准,不是完整无解。${changedScopeHint}展示 available_actions`},等待用户意图(${expandHint})。ready 后 next 为空表示向用户汇报并等待,不是永远没有更多。比较需求说明首选与备选取舍后结束;已有代选委托且选项合格时沿详情继续到必要确认与官方入口。`,
1693
- next: null,
1694
- recovery: [],
1695
- };
1696
- }
1697
- case "failed":
1698
- case "cancelled":
1699
- case "expired": {
1700
- if (expansion === "failed" && !plan.snapshot_id &&
1701
- ["address_required", "administrative_area_unresolved", "station_location_evidence_required", "coordinate_identity_unverified", "coordinate_name_mismatch", "endpoint_adcode_name_conflict", "endpoint_station_name_code_conflict", "endpoint_poi_name_conflict"].includes(plan.search?.reason ?? "")) {
1702
- return {
1703
- status: "awaiting_input", result,
1704
- instruction: plan.search?.reason === "station_location_evidence_required"
1705
- ? "尚未查票。车站身份明确,但 Smart 缺少必要地理证据;这是系统证据缺口,不要求用户换站。若双锁站仅需直达,可使用 Exact。"
1706
- : "尚未查票。受影响端点的名称、归属或坐标尚不能一致确认;只补正该端点的真实信息,保留其余条件,不把此次失败说成无车。",
1707
- next: null, recovery: [],
1708
- };
1709
- }
1710
- // §7.1 case 3: when scope was only partially verified the honest wording
1711
- // is "N confirmed + X pairs unverified" — never the case-2 phrasing that
1712
- // implies the whole nearby range was checked.
1713
- const partial = counts && (counts.pairs_total ?? 0) > (counts.pairs_checked ?? 0)
1714
- ? `已保存${counts.journeys_total ?? 0}个具体车次时刻组合,其中${counts.journeys_eligible ?? "未知"}条当前满足铁路资格;还有${(counts.pairs_total ?? 0) - (counts.pairs_checked ?? 0)}个站对/部分中转路径未核验。以下是已确认结果,不能据此断定没有其它走法。`
1715
- : "";
1716
- // §S5: a processing failure with a committed catalog is "saved N
1717
- // verified combinations, later expansion/recommend incomplete" — the
1718
- // existing catalog stays readable and nothing implies no other routes.
1719
- const savedCatalog = expansion === "failed" && (counts?.journeys_total ?? 0) > 0
1720
- ? `已保存${counts?.journeys_total}个已核验组合;后续扩展/推荐未完成,原因是本次处理异常。已有方案仍可查看,不代表没有其它路线,也不需要重复提交订单或付款。`
1721
- : "";
1722
- return {
1723
- status: expansion,
1724
- result,
1725
- instruction: `${modelDegradedHint}${failedPairsHint}${savedCatalog}${partial}${expansion === "expired" ? "规划已过期,已保存目录仍可读取并回答历史结果追问;购买须重新核验实时余票与报价。" : "本次规划已结束,展示已有结果;需要新的供应商观测时再明确发起查询。"}`,
1726
- next: null,
1727
- recovery: plan.snapshot_id
1728
- ? [{ command: `itpay services read-result ${se} --snapshot ${plan.snapshot_id} --json`, reason: "读取同一任务已保存的完整目录" }]
1729
- : [{ command: `itpay services next ${se} --json`, reason: "读取同一任务当前状态" }],
1730
- };
1731
- }
1732
- default: {
1733
- // §7.1 case 2: the automatic phase is still running. When the verified
1734
- // stage is transfer the user must hear "direct scope found nothing
1735
- // usable, transfer search continues" — never re-submit.
1736
- const transferSearching = plan.search?.phase === "transfer"
1737
- || plan.search?.transition_reason === "no_usable_direct"
1738
- || plan.search?.transfer_status === "in_progress" || plan.search?.transfer_status === "pending";
1739
- // §S5: an expansion the user authorized keeps earlier directs and says
1740
- // so — it must not reuse the automatic "no usable direct" wording.
1741
- const manualExpansion = plan.search?.authorization === "manual"
1742
- ? `已保留之前的直达结果,正在按你的要求比较更多直达/中转及分段购票方案,需要多一点时间。`
1743
- : "";
1744
- const transferHint = !manualExpansion && transferSearching
1745
- ? `已检查本次附近站范围,暂未找到符合条件且有票的直达,正在继续搜索中转组合,需要多一点时间;不需要重新提交。`
1746
- : "";
1747
- return {
1748
- status: "planning",
1749
- result,
1750
- instruction: `${modelDegradedHint}${manualExpansion}${transferHint}${failedPairsHint}规划进行中:已提交的卡片可先向用户展示比较;稍后按 next 增量轮询同一执行(since-snapshot 命中时负载会被压缩,但阶段与计数仍返回最新值)。不要重新发起查询。`,
1751
- next: nextPoll,
1752
- recovery: [],
1753
- };
1754
- }
1755
- }
1756
- }
1757
- function terminalExecutionEnvelope(model) {
1758
- const execution = model.execution;
1759
- const currentDelivery = model.current_delivery ?? model.delivery_bindings.at(-1);
1760
- if (!isTerminalServiceExecutionStatus(execution.status) ||
1761
- (model.workflow_entry && (currentDelivery || serviceDeliveryMode(model) === "agent_visible_result") && ["completed", "delivery_completed"].includes(execution.status))) {
1762
- return null;
1763
- }
1764
- const paid = model.checkout_bindings.some((binding) => binding.status === "payment_verified") || Boolean(currentDelivery?.order_id);
1765
- const paidFailure = execution.status === "failed" && paid;
1766
- return {
1767
- status: execution.status,
1768
- result: {
1769
- service_execution_id: execution.service_execution_id,
1770
- service_id: execution.service_id,
1771
- phase: execution.phase,
1772
- ...(currentDelivery?.order_id ? { order_id: currentDelivery.order_id } : {}),
1773
- },
1774
- instruction: execution.status === "refunded"
1775
- ? "告诉用户这笔服务已经退款并永久结束。Agent 不重放服务步骤、不创建付款页面或尝试读取旧交付。"
1776
- : paidFailure
1777
- ? appendFeedbackPostmortemInstruction("告诉用户:付款和订单已经记录,但本次服务没有正常完成,不需要再次付款或重新下单。然后从同一订单检查退款状态;Agent 不重放服务步骤、创建付款页面或再次调用数据来源,也不把技术故障归咎于用户。", "failed")
1778
- : "告诉用户本次服务已经结束且没有可继续的交付。Agent 不重放服务步骤或创建付款页面。",
1779
- next: null,
1780
- recovery: [
1781
- ...(paidFailure
1782
- ? [{
1783
- command: currentDelivery?.order_id
1784
- ? `itpay order ${currentDelivery.order_id} --json`
1785
- : "itpay orders --json",
1786
- reason: "恢复同一笔已付款订单及其退款状态",
1787
- }]
1788
- : []),
1789
- {
1790
- command: `itpay services events ${execution.service_execution_id} --json`,
1791
- reason: "仅在需要诊断终止原因时读取事件",
1792
- },
1793
- ],
1794
- };
1795
- }
1796
- const RAIL_LOCATION_EVIDENCE_REASONS = new Set(["endpoint_kind_conflict", "place_identity_uncertain", "endpoint_adcode_name_conflict", "endpoint_poi_name_conflict", "endpoint_station_name_code_conflict", "location_evidence_required", "coordinate_name_mismatch", "area_scope_too_broad", "address_required", "administrative_area_unresolved", "station_location_evidence_required", "coordinate_identity_unverified"]);
1797
- const RAIL_QUOTE_FAILURE_GUIDANCE = {
1798
- quote_source_unavailable: "实时车票数据暂不可用;保留原选择,稍后按用户意愿重新核验,不创建替代订单。",
1799
- quote_train_changed: "所选车次的时刻或运行信息已变化;向用户说明并征询是否重新查票。",
1800
- quote_seat_changed: "所选席别已变化;向用户说明并征询新的席别选择。",
1801
- quote_seat_unavailable: "所选席别当前无足够余票;告知用户并由其决定是否换方案。",
1802
- quote_refresh_expired: "实时报价过程超时;保留原选择,不把旧价当可付款价格。",
1803
- };
1804
641
  function servicesNextEnvelope(model) {
1805
642
  const execution = model.execution;
1806
643
  const currentDelivery = model.current_delivery ?? model.delivery_bindings.at(-1);
1807
644
  const lockedRefund = model.refunds.find((refund) => refund.access_locked);
1808
645
  if (lockedRefund) {
1809
- const refundState = refundStateEnvelope(lockedRefund, "delivery_locked");
1810
- return { ...refundState, result: { ...refundState.result, service_execution_id: execution.service_execution_id, refund: { refund_request_id: lockedRefund.refund_request_id, status: lockedRefund.status, ...(lockedRefund.failure_class ? { failure_class: lockedRefund.failure_class } : {}) } },
1811
- next: refundState.next ? { command: `itpay refund get ${lockedRefund.refund_request_id} --json`, reason: "读取同一退款权威状态" } : null };
1812
- }
1813
- const latestRun = model.execution_requests?.filter((request) => request.execution_kind === "service.execution.run").at(-1);
1814
- if (latestRun && ["pending", "started"].includes(latestRun.status) && model.workflow?.status !== "failed") {
646
+ const terminal = lockedRefund.status === "succeeded";
1815
647
  return {
1816
- status: "running",
1817
- result: { service_execution_id: execution.service_execution_id, execution_request_id: latestRun.execution_request_id },
1818
- instruction: "任务仍在排队或执行中,稍后读取同一任务。不要重新查票、重复提交输入或把等待状态当作零结果。",
1819
- next: { command: `itpay services next ${execution.service_execution_id} --json`, reason: "稍后读取同一任务状态" },
648
+ status: "delivery_locked",
649
+ result: {
650
+ service_execution_id: execution.service_execution_id,
651
+ access_locked: true,
652
+ refund: {
653
+ refund_request_id: lockedRefund.refund_request_id,
654
+ status: lockedRefund.status,
655
+ },
656
+ },
657
+ instruction: terminal
658
+ ? "告诉用户退款已由 ItPay 确认成功,原交付永久关闭。Agent 停止读取和跟踪,不再创建授权。"
659
+ : "告诉用户退款仍在处理,原交付已按政策冻结。然后读取同一退款的权威状态;Agent 不读取交付、不创建授权或重复申请。",
660
+ next: terminal ? null : {
661
+ command: `itpay refund get ${lockedRefund.refund_request_id} --json`,
662
+ reason: "读取退款权威状态",
663
+ },
1820
664
  recovery: [],
1821
665
  };
1822
666
  }
1823
- // A terminal execution wins over the rail read model: a failed, cancelled,
1824
- // or refunded run must not render as still issuing. Completed executions
1825
- // keep the rail view so issued seats stay visible.
1826
- if (model.rail_booking && model.workflow?.human_action?.context?.quote_recovery !== true && isTerminalServiceExecutionStatus(execution.status) && !["completed", "delivery_completed"].includes(execution.status)) {
1827
- const terminal = terminalExecutionEnvelope(model);
1828
- if (terminal)
1829
- return terminal;
1830
- }
1831
- if (model.rail_booking)
1832
- return railBookingEnvelope(model);
1833
- if (model.rail_planning)
1834
- return railPlanningEnvelope(model);
1835
- if (latestRun && ["failed", "cancelled"].includes(latestRun.status) && !model.workflow_entry) {
667
+ if (isTerminalServiceExecutionStatus(execution.status)) {
668
+ const paid = model.checkout_bindings.some((binding) => binding.status === "payment_verified") || Boolean(currentDelivery?.order_id);
669
+ const paidFailure = execution.status === "failed" && paid;
1836
670
  return {
1837
- status: latestRun.status,
1838
- result: { service_execution_id: execution.service_execution_id },
1839
- instruction: "本次任务未完成,需要检查同一任务的处理记录;这不表示没有结果。不要自动重新发起。",
1840
- next: null,
1841
- recovery: [{ command: `itpay services events ${execution.service_execution_id} --json`, reason: "读取同一任务的处理记录" }],
1842
- };
1843
- }
1844
- if (model.workflow_entry && !["completed", "delivery"].includes(model.workflow?.status ?? "")) {
1845
- const id = execution.service_execution_id;
1846
- const workflowReason = model.workflow?.failure?.reason_code ?? model.workflow?.error_code;
1847
- const paymentVerified = model.payment_bindings.some((binding) => binding.status === "payment_verified") || model.checkout_bindings.some((binding) => binding.status === "payment_verified");
1848
- const state = model.workflow?.status === "payment" && paymentVerified ? "running" : model.workflow?.status ?? "input_required";
1849
- if (["quota_paused", "failed"].includes(state) && !RAIL_LOCATION_EVIDENCE_REASONS.has(workflowReason ?? "") && (model.workflow?.dependency ||
1850
- /^amap_/.test(workflowReason ?? ""))) {
1851
- const dependency = model.workflow?.dependency;
1852
- const resumable = state === "quota_paused" && dependency?.retryable === true;
1853
- return {
1854
- status: "dependency_unavailable",
1855
- result: { service_execution_id: id, service_id: execution.service_id, reason: workflowReason, failure: model.workflow?.failure, affected_endpoint: model.workflow?.failure?.endpoint ?? dependency?.endpoint, ...(dependency ? { dependency } : {}) },
1856
- instruction: resumable
1857
- ? "地图服务暂时限流。保留原条件及已解析端点;到 retry_at 后执行下一步,继续同一任务,不重新提交地点。"
1858
- : "地图服务本轮未完成解析,这是外部依赖故障,不要求用户换地址或自行找坐标。有界尝试已结束;保留原条件及已有结果,交由服务维护方处理该错误。",
1859
- next: resumable ? { command: `itpay services run ${execution.service_id} --execution ${id} --json`, reason: "等待所示恢复时间后继续同一执行" } : null,
1860
- recovery: [],
1861
- };
1862
- }
1863
- if (state === "failed" && RAIL_LOCATION_EVIDENCE_REASONS.has(workflowReason ?? "")) {
1864
- const code = workflowReason;
1865
- const affected = model.workflow?.failure?.endpoint ?? model.workflow?.dependency?.endpoint;
1866
- const query = model.workflow?.query_input ?? {};
1867
- const endpoints = query.endpoints && typeof query.endpoints === "object" ? query.endpoints : {};
1868
- const stationPair = [endpoints.origin, endpoints.destination].every(endpoint => endpoint?.kind === "station" && typeof endpoint.text === "string" && endpoint.text.trim() && typeof endpoint.station_code === "string" && /^[A-Z]{3}$/.test(endpoint.station_code));
1869
- if (code === "station_location_evidence_required") {
1870
- const exactInput = stationPair && typeof query.travel_date === "string" ? { origin: endpoints.origin.text, destination: endpoints.destination.text, travel_date: query.travel_date } : undefined;
1871
- return { status: "system_evidence_required", result: { service_execution_id: id, service_id: execution.service_id, reason: code, affected_endpoint: affected, failure: model.workflow?.failure, query,
1872
- ...(exactInput ? { new_query_template: { command: "itpay services run itpay-rail-exact --input-json <双锁站直达.json> --json", input: exactInput, executable: false, required_input: ["file"], instruction: "当前明确双锁站可核验直达;Exact只覆盖这一站对,仍按原人数、时限和偏好核对结果。" } } : {}) },
1873
- instruction: "尚未查票。车站身份已知,但Smart缺车站坐标证据;这是系统证据缺口,不要求用户填技术坐标或换站。" + (exactInput ? "当前双锁站可按完整模板另用Exact核验直达,原需求见query。" : "本路线Smart暂受系统证据限制,当前无可执行恢复;停止并交由维护方补证据。"), next: null, recovery: [] };
1874
- }
1875
- if (affected !== "origin" && affected !== "destination")
1876
- return { status: "input_evidence_required", result: { service_execution_id: id, reason: code, failed_step: model.workflow?.failure?.step_id, failure: model.workflow?.failure, query }, instruction: "地点问题未明确受影响侧,当前无可提交修正模板;保留原需求,停止并核对服务端事实,不猜另一端或坐标。", next: null, recovery: [] };
1877
- const properties = model.workflow_entry.input_schema.properties;
1878
- if (!properties?.endpoints && properties?.[affected] === undefined)
1879
- return { status: "contract_unavailable", result: { service_execution_id: id, reason: code, failed_step: model.workflow?.failure?.step_id, failure: model.workflow?.failure, query }, instruction: "旧查询已终止,当前锁定合同缺少可用地点字段;保留原需求并停止,不猜新旧模板或隐藏字段。", next: null, recovery: [] };
1880
- const input = { ...Object.fromEntries(Object.entries(query).filter(([key]) => properties?.[key] !== undefined)), ...serviceInputTemplate(model.workflow_entry.input_schema, query) };
1881
- const identityConflict = ["endpoint_kind_conflict", "endpoint_adcode_name_conflict", "endpoint_poi_name_conflict", "endpoint_station_name_code_conflict", "coordinate_name_mismatch"].includes(code);
1882
- const replacement = code === "area_scope_too_broad" ? { kind: "area", text: "<受影响端的市县镇完整名称>" } : { ...(endpoints[affected]?.kind ? { kind: endpoints[affected].kind } : {}), text: code === "coordinate_identity_unverified" ? "<已核实与地点身份一致的完整地址或地点名>" : "<已补充行政归属的完整地址或地点名>" };
1883
- const endpointContract = properties?.endpoints !== undefined && (query.endpoints !== undefined || !properties.origin || !properties.destination);
1884
- if (endpointContract)
1885
- input.endpoints = { ...endpoints, [affected]: replacement };
1886
- else {
1887
- input[affected] = replacement.text;
1888
- delete input[`${affected}_location`];
1889
- }
1890
- const originalPlace = endpoints[affected]?.text ?? query[affected];
1891
- return { status: "awaiting_input", result: { service_execution_id: id, service_id: execution.service_id, reason: code, failed_step: model.workflow?.failure?.step_id, classification: "location_evidence_required", affected_endpoint: affected, original_place: originalPlace, ...(model.workflow?.failure?.resolved_scope ? { resolved_scope: model.workflow.failure.resolved_scope } : {}), failure: model.workflow?.failure, query,
1892
- new_query_template: { command: `itpay services run ${execution.service_id} --input-json <补全原始需求.json> --json`, input, executable: false, required_input: ["file", endpointContract ? `endpoints.${affected}.text` : affected], ...(endpointContract ? { correction_fields: ["text", "kind", "poi_id", "location"].filter(field => { const side = properties?.endpoints?.properties?.[affected]; return !!side?.properties?.[field]; }) } : {}), instruction: "旧执行输入已锁定;补真实地点后将完整input写入JSON文件,按此命令提交新查询,保留另一端、日期、人数和所有原约束。" } },
1893
- instruction: `旧执行已终止,尚未查票。${identityConflict ? "名称、类别或位置证据存在冲突;先核实返回原因,不能修改kind绕过。" : "完整地点名称合法,但尚缺唯一身份证据;可先用地图或浏览短查,不要求必填门牌或坐标。"}新证据确定类别且原意未变可纠正kind;改变目的地或扩大范围须先取得真实同意。保留原地点及范围;${affected === "origin" ? "出发端" : "到达端"}原文见original_place。有新证据后只补充${affected === "origin" ? "出发端" : "到达端"}${code === "area_scope_too_broad" ? "的市县镇范围" : code === "coordinate_identity_unverified" ? "的已核实地点身份;已移除未核实坐标,不猜坐标" : "的完整地点和行政归属"}。使用当前完整new_query_template;不能在旧执行伪造修改,也不能直接提交占位符。`, next: null, recovery: [] };
1894
- }
1895
- if (state === "failed" && ["login_required", "rate_limited"].includes(workflowReason ?? "")) {
1896
- const login = workflowReason === "login_required";
1897
- return {
1898
- status: login ? "login_required" : "rate_limited",
1899
- result: { service_execution_id: id, service_id: execution.service_id },
1900
- instruction: login ? "匿名免费额度已用完。使用官方网页登录并绑定当前 Agent,完成后重新发起查询;不需要付款。" : "已达到登录账号每分钟查询上限。请等到下一分钟再发起查询,不要连续重试。",
1901
- next: login ? { command: "itpay auth login --json", reason: "登录继续免费查询" } : null,
1902
- recovery: [],
1903
- };
1904
- }
1905
- if (state === "quota_paused") {
1906
- const login = model.workflow?.error_code !== "rate_limited";
1907
- const resume = { command: `itpay services run ${execution.service_id} --execution ${id} --json`, reason: "登录或限流窗口后继续同一执行" };
1908
- const quota = model.quota ? { bucket: model.quota.bucket, subject_type: model.quota.subject_type, limit: model.quota.limit, remaining: model.quota.remaining } : undefined;
1909
- return {
1910
- status: login ? "login_required" : "rate_limited",
1911
- result: { service_execution_id: id, service_id: execution.service_id, ...(quota ? { quota } : {}) },
1912
- instruction: login ? "匿名免费额度已用完。使用 itpay auth login 完成官方登录并绑定当前 Agent,然后继续同一执行;不要重新发起查询,不需要付款。" : "已达到登录账号每分钟查询上限。请等到下一分钟后继续同一执行,不要连续重试。",
1913
- next: login ? { command: "itpay auth login --json", reason: "登录后继续同一查询执行" } : resume,
1914
- recovery: [resume],
1915
- };
1916
- }
1917
- if ((state === "human_action" || (state === "failed" && model.workflow?.human_action?.context?.quote_recovery === true)) && model.workflow?.human_action) {
1918
- const action = model.workflow.human_action;
1919
- const requiredFields = requiredInputFields(action.input_schema);
1920
- const rawPlaces = action.context?.places;
1921
- // Only the location-confirmation action carries origin/destination places;
1922
- // every other human action keeps the generic envelope.
1923
- const isLocationConfirmation = !!rawPlaces && typeof rawPlaces === "object" &&
1924
- ("origin" in rawPlaces || "destination" in rawPlaces);
1925
- if (isLocationConfirmation) {
1926
- const places = rawPlaces;
1927
- const sides = ["origin", "destination"].map((side) => {
1928
- const place = places[side];
1929
- const candidates = Array.isArray(place?.resolution_candidates) ? place.resolution_candidates : [];
1930
- const name = (item) => item.poi_name ?? item.query ?? item.formatted_address;
1931
- return {
1932
- side,
1933
- status: place?.resolution_status,
1934
- ...(place?.resolution_reason ? { reason: place.resolution_reason } : {}),
1935
- ...(place?.resolution_status === "resolved"
1936
- ? { resolved_place: { name: name(place), address: place.formatted_address, location: place.location } }
1937
- : {}),
1938
- ...(candidates.length
1939
- ? { candidates: candidates.slice(0, 3).map((item, index) => ({ id: item.id ?? `${side}_${index + 1}`, name: name(item), address: item.formatted_address, type: item.poi_type ?? item.type, location: item.location })) }
1940
- : {}),
1941
- };
1942
- });
1943
- return {
1944
- status: "confirmation_required",
1945
- result: { service_execution_id: id, service_id: execution.service_id,
1946
- human_action: { action_type: action.action_type, input_schema: action.input_schema },
1947
- required_fields: requiredFields, sides, query: model.workflow?.query_input,
1948
- ...(sides.some(side => side.status !== "resolved" && !side.candidates?.length && ["ADDRESS_REQUIRED", "ADMINISTRATIVE_AREA_UNRESOLVED"].includes(side.reason ?? "")) ? {
1949
- new_query_template: { command: `itpay services run ${execution.service_id} --input-json <补全原始需求.json> --json`, executable: false,
1950
- input: serviceInputTemplate(model.workflow_entry.input_schema, model.workflow?.query_input ?? {}),
1951
- required_input: ["补缺失端点", "保留原始日期人数时限偏好及已确认端点"], instruction: "原执行锁定输入;补全原需求后用新查询,不提交不存在的候选。" },
1952
- } : {}) },
1953
- instruction: sides.some(side => side.status !== "resolved" && !side.candidates?.length)
1954
- ? locationConfirmationEnvelope(id, "", {}, { endpoints: sides.filter(side => side.status !== "resolved") }).instruction
1955
- : requiredFields.includes("choices")
1956
- ? "尚未查票。仅向用户展示有真实歧义端点的候选编号、名称、行政地址与类型(内部 id 仅用于回填,不展示给人);用户选定后,把各未解析端点的候选 id 放入 choices 对象,继续同一执行。已解析端点无需再选;不得替用户决定,也不是购买确认。"
1957
- : "向用户展示有歧义一端的候选;按 required_fields 填写真实选择,已解析一端保留原值。继续同一执行;这不是购买确认。",
1958
- next: null,
1959
- ...(sides.some(side => side.status !== "resolved" && !side.candidates?.length) ? {} : { interaction: {
1960
- schema_version: "itpay.interaction.v1",
1961
- stage: "location_confirmation_required",
1962
- input_template: {
1963
- command: requiredFields.includes("choices")
1964
- ? `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved --input-json <地点确认.json> --json`
1965
- : `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved${requiredFields.map((field) => ` --input ${field}=<值>`).join("")} --json`,
1966
- input: { choices: Object.fromEntries(sides.filter(side => side.candidates?.length).map(side => [side.side, "<所选候选id>"])) },
1967
- required_input: requiredFields,
1968
- executable: false,
1969
- },
1970
- } }),
1971
- recovery: [],
1972
- };
1973
- }
1974
- const review = action.context?.review;
1975
- if (review) {
1976
- // W4: when the owner projects an official web review entry, present it
1977
- // and wait for the human there; otherwise keep the one consolidated
1978
- // chat review — never fabricate an accepted review.
1979
- const reviewURL = [review.web_review_url, review.entry_url, review.review_url]
1980
- .find((value) => typeof value === "string" && /^https:\/\//.test(value));
1981
- const requirementsRemaining = Array.isArray(review.requirements_remaining)
1982
- ? review.requirements_remaining
1983
- : undefined;
1984
- return {
1985
- status: state === "failed" ? "quote_recovery_required" : "booking_review_required",
1986
- result: { service_execution_id: id, service_id: execution.service_id, review, ...(state === "failed" ? { failure: model.workflow?.failure } : {}),
1987
- action_type: action.action_type, required_fields: requiredFields,
1988
- ...(requirementsRemaining ? { requirements_remaining: requirementsRemaining } : {}) },
1989
- ...(reviewURL ? { handoff: { url: reviewURL, kind: "booking_review" } } : {}),
1990
- instruction: `${state === "failed" ? `${typeof model.workflow?.failure?.affected_leg === "number" ? `第${model.workflow.failure.affected_leg + 1}段` : ""}报价失败:${RAIL_QUOTE_FAILURE_GUIDANCE[model.workflow?.failure?.reason_code ?? ""] ?? "报价核验未完成。"} 当前无完整报价、无付款入口。按用户意愿有界地重新核验一次同一购买;复制下方当前确认模板,未变条款沿用真实同意,价格/车次/席别/人数/条款变化须确认差异。无票或车次变化时先让用户决定,不能自动换路线。支付尝试后不得修订。 ` : ""}${reviewURL ? "官方确认页见 handoff.url,打开不等于确认;沿用已知人数和席别。" : "当前草稿见 review。"} 对照对话只补缺失选择或真实条款同意;无字母偏好自动分配,不默认问字母。仅代码修正且车次、实际席别、人数、价格及条款不变,沿用已有明确确认;实质变化只核对变化。复制 interaction.input_template.input;自动分配使用该对象数组,不传 null 或字符串。只有用户本人已接受当前条款,才把 accept_non_guaranteed 改为 true;选车或委托选票不等于条款同意,false是未同意保护而非模板错误。缺同意时仅用 review.consent_question 问一次;没有该文案时说明座位/卧铺偏好可能自动分配,并按当前notice补整程条款。已有充分同意且条件不变则不再问。身份信息只在官方 Checkout 页填写。`,
1991
- next: null,
1992
- interaction: {
1993
- schema_version: "itpay.interaction.v1",
1994
- stage: "booking_review_required",
1995
- input_template: {
1996
- command: `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved --input-json <file> --json`,
1997
- required_input: ["file", ...requiredFields],
1998
- input: review.input_template && typeof review.input_template === "object"
1999
- ? review.input_template : serviceInputTemplate(action.input_schema ?? {}, review),
2000
- executable: false,
2001
- },
2002
- },
2003
- recovery: [],
2004
- };
2005
- }
2006
- return {
2007
- status: "confirmation_required",
2008
- result: { service_execution_id: id, service_id: execution.service_id, human_action: action },
2009
- instruction: "请展示待确认的内容,请用户明确确认后,按 input_schema 填写 --input 字段。继续同一执行,不重新查询;这一步不是购买确认。",
2010
- next: null,
2011
- interaction: {
2012
- schema_version: "itpay.interaction.v1",
2013
- stage: "confirmation_required",
2014
- input_template: {
2015
- command: `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved${requiredFields.map((field) => ` --input ${field}=<值>`).join("")} --json`,
2016
- required_input: requiredFields,
2017
- executable: false,
2018
- },
2019
- },
2020
- recovery: [],
2021
- };
2022
- }
2023
- const recovery = state === "recovery_required" || state === "failed";
2024
- let command = `itpay services next ${id} --json`;
2025
- if (state === "payment")
2026
- command = `itpay services checkout ${id} --json`;
2027
- // input_required: the resume command needs a real file path — expose it as
2028
- // a non-executable input_template, not a fake `next.command`.
2029
- const guidance = railServiceGuidance(execution.service_id, model.workflow_entry?.input_schema);
2030
- const inputTemplate = state === "input_required"
2031
- ? {
2032
- command: `itpay services run ${execution.service_id} --execution ${id} --input-json <file> --json`,
2033
- input: serviceInputTemplate(model.workflow_entry?.input_schema ?? {}, Object.keys(model.workflow?.query_input ?? {}).length ? (model.workflow?.query_input ?? {}) : guidance?.input_example ?? {}),
2034
- required_input: ["file", ...requiredInputFields(model.workflow_entry?.input_schema)],
2035
- executable: false,
2036
- }
2037
- : undefined;
2038
- if (state === "input_required" && execution.service_id === "itpay-rail-smart" && !guidance) {
2039
- return { status: "contract_unavailable", result: { service_execution_id: id, service_id: execution.service_id }, instruction: "当前锁定合同缺失;读取一次同执行合同后仍缺失就停止,不猜新旧输入结构。", next: { command: `itpay services get ${id} --json`, reason: "获取锁定合同一次" }, recovery: [] };
2040
- }
2041
- const failedStep = recovery ? model.workflow?.failure?.step_id : undefined;
2042
- const failedStepGuidance = failedStep ? WORKFLOW_STEP_GUIDANCE[failedStep] : undefined;
2043
- const failedInvocation = recovery
2044
- ? [...model.provider_invocations].reverse().find((item) => typeof item.status === "string" && item.status.startsWith("failed"))
2045
- : undefined;
2046
- const providerErrorCode = typeof failedInvocation?.error_code === "string" && failedInvocation.error_code ? failedInvocation.error_code : undefined;
2047
- const quoteFailure = execution.service_id === "itpay-rail-booking" ? RAIL_QUOTE_FAILURE_GUIDANCE[(model.workflow?.failure?.reason_code === "workflow_failed" ? providerErrorCode : model.workflow?.failure?.reason_code) ?? providerErrorCode ?? ""] : undefined;
2048
- const failureCode = model.workflow?.failure?.reason_code === "workflow_failed" && providerErrorCode && RAIL_QUOTE_FAILURE_GUIDANCE[providerErrorCode] ? providerErrorCode : model.workflow?.failure?.reason_code ?? providerErrorCode ?? model.workflow?.error_code;
2049
- const failedInstruction = recovery
2050
- ? quoteFailure
2051
- ? `${typeof model.workflow?.failure?.affected_leg === "number" ? `第${model.workflow.failure.affected_leg + 1}段报价失败。` : ""}${quoteFailure} 当前整程未取得完整有效报价,没有付款入口;保留同一购买和未变化的选择,当前没有可提交恢复动作,停止;需要用户决定或系统处理,不能靠反复 get/next 重新报价,不能收取其余段款项或重复创建购买。`
2052
- : failedStepGuidance
2053
- ? `执行在「${failedStepGuidance.meaning}」步失败${failureCode ? `(错误码:${failureCode})` : ""}:${failedStepGuidance.hint}。先核对原因和当前执行状态,不要盲目重放。`
2054
- : state === "failed"
2055
- ? `执行已失败${failureCode ? `(错误码:${failureCode})` : ""}且不可续用;${execution.service_id === "itpay-rail-booking" || paymentVerified ? "核对服务端原因、订单与付款事实" : "保留原地点和约束,核对服务端原因;当前没有可执行恢复则停止,不重复读取终态"}后再决定下一步。`
2056
- : "执行未完成,请按步骤错误处理;不要重建执行或重复调用。"
2057
- : undefined;
2058
- return {
2059
- status: state,
671
+ status: execution.status,
2060
672
  result: {
2061
- service_execution_id: id,
673
+ service_execution_id: execution.service_execution_id,
2062
674
  service_id: execution.service_id,
2063
- workflow_state: state,
2064
- ...(model.workflow ? { current_step: model.workflow.current_step, workflow_revision: model.workflow.revision } : {}),
2065
- ...(model.workflow?.failure ? { failure: model.workflow.failure } : {}),
2066
- ...(state === "input_required" ? { input_schema: model.workflow_entry.input_schema } : {}),
2067
- ...(guidance && state === "input_required" ? { guidance } : {}),
2068
- ...(failedStep ? { failed_step: failedStep, ...(failedStepGuidance ? { failed_step_meaning: failedStepGuidance.meaning } : {}) } : {}),
2069
- ...(providerErrorCode ? { provider_error_code: providerErrorCode } : {}),
2070
- ...(providerErrorCode && !quoteFailure ? { diagnostic_code: providerErrorCode } : {}),
2071
- ...(recovery ? { retryable: false } : {}),
675
+ phase: execution.phase,
676
+ ...(currentDelivery?.order_id ? { order_id: currentDelivery.order_id } : {}),
2072
677
  },
2073
- instruction: failedInstruction
2074
- ?? (state === "payment"
2075
- ? (model.workflow?.human_action?.context?.review
2076
- ? "服务已到付款步骤,可直接 Checkout 付款;如需调整席别/座位偏好,先用 confirm_booking 动作修订(会重新报价并锁定新价),完成后再付款。"
2077
- : "服务已到付款步骤,使用现有 Checkout 完成扫码付款。")
2078
- : state === "input_required" && guidance
2079
- ? RAIL_ENDPOINT_BRIEF + "按 result.guidance 的字段契约填写输入后继续同一服务执行;不要臆造字段名。"
2080
- : "继续读取同一执行;缺少输入时按服务声明补齐。"),
2081
- next: recovery || inputTemplate ? null : { command, reason: "继续当前流程" },
2082
- ...(inputTemplate || (state === "payment" && model.workflow?.human_action?.context?.review) ? {
2083
- interaction: {
2084
- schema_version: "itpay.interaction.v1",
2085
- stage: inputTemplate ? "input_required" : "payment",
2086
- ...(inputTemplate ? { input_template: inputTemplate } : {}),
2087
- ...(state === "payment" && model.workflow?.human_action?.context?.review ? {
2088
- revise_template: {
2089
- command: `itpay services action ${id} --action ${model.workflow.human_action.action_type} --actor-type human --status approved --input-json <file> --json`,
2090
- required_input: ["file"],
2091
- executable: false,
2092
- },
2093
- } : {}),
678
+ instruction: execution.status === "refunded"
679
+ ? "告诉用户这笔服务已经退款并永久结束。Agent 不重放服务步骤、不创建付款页面或尝试读取旧交付。"
680
+ : paidFailure
681
+ ? appendFeedbackPostmortemInstruction("告诉用户:付款和订单已经记录,但本次服务没有正常完成,不需要再次付款或重新下单。然后从同一订单检查退款状态;Agent 不重放服务步骤、创建付款页面或再次调用数据来源,也不把技术故障归咎于用户。", "failed")
682
+ : "告诉用户本次服务已经结束且没有可继续的交付。Agent 不重放服务步骤或创建付款页面。",
683
+ next: null,
684
+ recovery: [
685
+ ...(paidFailure
686
+ ? [{
687
+ command: currentDelivery?.order_id
688
+ ? `itpay order ${currentDelivery.order_id} --json`
689
+ : "itpay orders --json",
690
+ reason: "恢复同一笔已付款订单及其退款状态",
691
+ }]
692
+ : []),
693
+ {
694
+ command: `itpay services events ${execution.service_execution_id} --json`,
695
+ reason: "仅在需要诊断终止原因时读取事件",
2094
696
  },
2095
- } : {}),
2096
- recovery: recovery && state === "failed" && !["itpay-rail-exact", "itpay-rail-smart"].includes(execution.service_id)
2097
- ? [{ command: `itpay services get ${id} --json`, reason: "核对本执行的错误、结果和付款事实" }]
2098
- : [],
697
+ ],
2099
698
  };
2100
699
  }
2101
- const terminalEnvelope = terminalExecutionEnvelope(model);
2102
- if (terminalEnvelope)
2103
- return terminalEnvelope;
2104
700
  const currentItems = model.current_result_items ?? [];
2105
- const latestInvocation = model.provider_invocations.at(-1);
2106
- const latestPreview = latestInvocation?.safe_result_preview;
2107
- if (currentItems.length === 0 && latestPreview?.search_status === "LOCATION_CONFIRMATION_REQUIRED" && latestPreview.location_confirmation) {
2108
- const capability = model.capabilities.find((item) => item.capability_id === latestInvocation?.capability_id && item.phase === execution.phase && !item.requires_payment);
2109
- if (capability)
2110
- return locationConfirmationEnvelope(execution.service_execution_id, capability.capability_id, (latestInvocation?.request_summary ?? {}), latestPreview.location_confirmation);
2111
- }
2112
701
  const delivery = currentDelivery;
2113
702
  const deliveryMode = serviceDeliveryMode(model);
2114
- if (execution.service_id === "itpay-rail-exact" && currentItems.length > 0) {
2115
- const item = currentItems[0];
2116
- const container = (item.safe_payload?.result ?? item.safe_payload);
2117
- const total = typeof container?.catalog_total === "number" ? container.catalog_total :
2118
- Array.isArray(container?.candidates) ? container.candidates.length : 0;
2119
- return {
2120
- status: total > 0 ? "result_ready" : "no_result",
2121
- result: { service_execution_id: execution.service_execution_id, scope: "station_pair",
2122
- resolved_station_pair: container?.resolved_station_pair, total,
2123
- result_item_id: item.service_capability_result_item_id,
2124
- trains: Array.isArray(container?.candidates) ? container.candidates.slice(0, 3).map((value, index) => exactTrainRow(value, execution.service_execution_id, item.service_capability_result_item_id, index)) : [],
2125
- ...(exactSmartTemplate(container?.resolved_station_pair, execution.service_execution_id, item.service_capability_result_item_id)
2126
- ? { smart_template: exactSmartTemplate(container?.resolved_station_pair, execution.service_execution_id, item.service_capability_result_item_id) } : {}) },
2127
- instruction: total > 0
2128
- ? "直达站对结果已保存。执行 next.command 一次读取紧凑车次行,按用户条件筛选;若只需比较,答复后结束;已有购票委托则读取所选行详情。"
2129
- : "这一已解析站对返回 0 条;不代表全城无车。若原需求需要更广覆盖,可核验另一可信站对或 Smart。",
2130
- next: { command: `itpay services read-result ${execution.service_execution_id} --json`, reason: "读取这一保存站对的紧凑分页;按返回的续页覆盖所需范围" },
2131
- recovery: [],
2132
- };
2133
- }
2134
703
  const candidateSelection = model.allowed_actions?.find((action) => action.type === "select_candidate");
2135
704
  if (candidateSelection && currentItems.length > 0) {
2136
705
  const paidCapability = delivery?.capability_id
@@ -2165,31 +734,6 @@ function servicesNextEnvelope(model) {
2165
734
  safe_payload: item.safe_payload,
2166
735
  }));
2167
736
  const selection = model.allowed_actions?.find((action) => action.type === "select_candidate");
2168
- const railCatalog = currentItems.some((item) => {
2169
- const container = (item.safe_payload?.result ?? item.safe_payload);
2170
- return container?.catalog_page !== undefined || container?.cost_semantics !== undefined;
2171
- });
2172
- const pageRecovery = currentItems.flatMap((item) => {
2173
- const container = (item.safe_payload?.result ?? item.safe_payload);
2174
- const catalogPage = container?.catalog_page;
2175
- const nextOffset = catalogPage?.next_offset;
2176
- return typeof nextOffset === "number"
2177
- ? [{ command: `itpay services page ${execution.service_execution_id} ${item.service_capability_result_item_id} --offset ${nextOffset} --json`, reason: "读取已保存结果的同版本下一页,不重新查询、不消耗额度" }]
2178
- : [];
2179
- });
2180
- const railCostGuidance = "铁路应付只含票价加服务费(quoted_total_minor);地面接驳是单独估算(estimated_ground_minor),两者相加是已知估算(estimated_door_to_door_minor),不是收款额。";
2181
- const instruction = (delivery?.order_id
2182
- ? appendFeedbackPostmortemInstruction(items.length > 0
2183
- ? selection
2184
- ? "搜索已完成。用编号、名称和可公开字段向用户说明结果,然后停止。只有用户明确选择候选并要求继续时才执行 next.command;不要提及 safe_payload。"
2185
- : "这一步的结果已经可用。用普通语言解释可公开字段并停止;不要提及 Arazzo、safe_payload 或内部 ID。"
2186
- : "告诉用户本次查询得到 0 个结果并停止。Agent 不读取其他交付、不重放当前查询、修改输入或创建新查询。", "delivered")
2187
- : items.length > 0
2188
- ? selection
2189
- ? "搜索已完成。用编号、名称和可公开字段向用户说明结果,然后停止。只有用户明确选择候选并要求继续时才执行 next.command;不要提及 safe_payload。"
2190
- : "这一步的结果已经可用。用普通语言解释可公开字段并停止;不要提及 Arazzo、safe_payload 或内部 ID。"
2191
- : "告诉用户本次查询得到 0 个结果并停止。Agent 不读取其他交付、不重放当前查询、修改输入或创建新查询。")
2192
- + (railCatalog && items.length > 0 ? ` ${railCostGuidance}` : "");
2193
737
  return {
2194
738
  status: items.length > 0 ? "result_ready" : "no_result",
2195
739
  result: {
@@ -2199,12 +743,22 @@ function servicesNextEnvelope(model) {
2199
743
  delivery_mode: deliveryMode,
2200
744
  items,
2201
745
  },
2202
- instruction,
746
+ instruction: delivery?.order_id
747
+ ? appendFeedbackPostmortemInstruction(items.length > 0
748
+ ? selection
749
+ ? "搜索已完成。用编号、名称和可公开字段向用户说明结果,然后停止。只有用户明确选择候选并要求继续时才执行 next.command;不要提及 safe_payload。"
750
+ : "这一步的结果已经可用。用普通语言解释可公开字段并停止;不要提及 Graph、safe_payload 或内部 ID。"
751
+ : "告诉用户本次查询得到 0 个结果并停止。Agent 不读取其他交付、不重放当前查询、修改输入或创建新查询。", "delivered")
752
+ : items.length > 0
753
+ ? selection
754
+ ? "搜索已完成。用编号、名称和可公开字段向用户说明结果,然后停止。只有用户明确选择候选并要求继续时才执行 next.command;不要提及 safe_payload。"
755
+ : "这一步的结果已经可用。用普通语言解释可公开字段并停止;不要提及 Graph、safe_payload 或内部 ID。"
756
+ : "告诉用户本次查询得到 0 个结果并停止。Agent 不读取其他交付、不重放当前查询、修改输入或创建新查询。",
2203
757
  next: selection ? {
2204
758
  command: `itpay services action ${execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
2205
759
  reason: "仅在用户明确选择后锁定来源候选",
2206
760
  } : null,
2207
- recovery: pageRecovery,
761
+ recovery: [],
2208
762
  };
2209
763
  }
2210
764
  if (deliveryMode === "vault_artifact") {
@@ -2274,7 +828,7 @@ function servicesNextEnvelope(model) {
2274
828
  ? "告诉用户付款和订单已经确认,结果仍在同一笔服务中处理,不需要再次付款;如果最终无法交付,将从原订单检查退款路径。稍后只执行 next.command;Agent 不创建新服务、付款页面或数据请求,也不承诺退款结果。"
2275
829
  : preferred?.requires_human
2276
830
  ? "当前下一步需要用户明确选择;先展示必要信息并等待确认。"
2277
- : preferred ? "执行服务端返回的唯一首选动作;不要猜测其他 capability。" + locationInputInstruction(model.capabilities.find((item) => item.capability_id === preferred.capability_id)?.input_schema) : "当前没有后续动作。",
831
+ : preferred ? "执行服务端返回的唯一首选动作;不要猜测其他 capability。" : "当前没有后续动作。",
2278
832
  next,
2279
833
  recovery: [{ command: `itpay services get ${execution.service_execution_id} --json`, reason: "仅在当前动作异常时检查时间线" }],
2280
834
  };
@@ -2314,9 +868,6 @@ function serviceAllowedActionCommand(model, action) {
2314
868
  }
2315
869
  function serviceDeliveryMode(model) {
2316
870
  const delivery = model.current_delivery ?? model.delivery_bindings.at(-1);
2317
- const entry = model.capabilities.find(capability => capability.capability_id === model.workflow_entry?.capability_id);
2318
- if (!delivery && model.workflow?.status === "completed" && entry?.requires_payment === false && !entry.vault_required)
2319
- return "agent_visible_result";
2320
871
  const explicit = String(delivery?.redacted_summary?.delivery_mode ?? "");
2321
872
  if (explicit)
2322
873
  return explicit;
@@ -2343,7 +894,21 @@ function grantedResultEnvelope(response, orderID) {
2343
894
  };
2344
895
  }
2345
896
  function servicesNextPlainResult(result) {
2346
- return plainValueLines(result);
897
+ const lines = [];
898
+ for (const [key, value] of Object.entries(result)) {
899
+ if (key === "items" && Array.isArray(value)) {
900
+ lines.push("items:");
901
+ for (const item of value) {
902
+ lines.push(` ${item.rank}. ${item.title}`);
903
+ for (const [field, fieldValue] of Object.entries(item.safe_payload)) {
904
+ lines.push(` ${field}: ${typeof fieldValue === "string" ? fieldValue : JSON.stringify(fieldValue)}`);
905
+ }
906
+ }
907
+ continue;
908
+ }
909
+ lines.push(`${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
910
+ }
911
+ return lines;
2347
912
  }
2348
913
  export async function runServicesEvents(backend, serviceExecutionID, options = {}) {
2349
914
  const afterSequence = options.afterSequence ?? 0;
@@ -2374,8 +939,11 @@ export async function runServicesEvents(backend, serviceExecutionID, options = {
2374
939
  returned_count: events.length,
2375
940
  events,
2376
941
  },
2377
- instruction: "这是已保存的诊断事件;事件不代表车票结果,也不授权重放业务步骤。已读完所需诊断时停止;实际任务沿先前返回的当前动作继续。",
2378
- next: null,
942
+ instruction: "事件仅用于诊断;不要从事件重放业务步骤,回到 services next 获取当前动作。",
943
+ next: {
944
+ command: `itpay services next ${serviceExecutionID} --json`,
945
+ reason: "恢复正常服务流程",
946
+ },
2379
947
  recovery: events.length === limit && events.length > 0
2380
948
  ? [{
2381
949
  command: `itpay services events ${serviceExecutionID} --after-sequence ${events.at(-1).sequence} --limit ${limit} --json`,
@@ -2408,17 +976,6 @@ export function collectOption(value, previous = []) {
2408
976
  return previous;
2409
977
  }
2410
978
  function parseValue(value) {
2411
- // Structured inputs (objects/arrays) are passed as JSON; a string that merely
2412
- // starts with a JSON delimiter but is not valid JSON stays a string — the
2413
- // backend schema validates the field type either way.
2414
- if (value.startsWith("{") || value.startsWith("[")) {
2415
- try {
2416
- return JSON.parse(value);
2417
- }
2418
- catch {
2419
- return value;
2420
- }
2421
- }
2422
979
  if (value === "true")
2423
980
  return true;
2424
981
  if (value === "false")
@@ -2434,7 +991,6 @@ function buildServicesCheckoutEnvelope(response, checkoutURL, plan, agentType, t
2434
991
  const presentationHandoff = buildCheckoutHandoff({
2435
992
  platform,
2436
993
  url: plan.linkOnlyURL ?? checkoutURL,
2437
- mobileUrl: checkoutURL,
2438
994
  amount,
2439
995
  plan,
2440
996
  ...(agentType ? { agentType } : {}),
@@ -2451,14 +1007,8 @@ function buildServicesCheckoutEnvelope(response, checkoutURL, plan, agentType, t
2451
1007
  service_execution_id: response.binding.service_execution_id,
2452
1008
  checkout_id: checkout.checkout.checkout_id,
2453
1009
  capability_id: checkoutCapabilityID(response),
2454
- items: plan.orderItems ?? [],
1010
+ locked_input: response.locked_input,
2455
1011
  amount,
2456
- ...(checkout.checkout.expires_at ? {
2457
- // The shared server-owned payable deadline D: refreshing the page or
2458
- // re-creating a checkout can never extend it; past it the checkout
2459
- // and pending order are terminally cancelled.
2460
- payment_deadline_at: checkout.checkout.expires_at,
2461
- } : {}),
2462
1012
  },
2463
1013
  handoff: presentationHandoff.handoff,
2464
1014
  instruction: presentationHandoff.instruction,
@@ -2516,98 +1066,3 @@ function tokenizedCheckoutURL(checkoutURL, displayToken, qrPayload) {
2516
1066
  return `${checkoutURL}${separator}display_token=${encodeURIComponent(displayToken)}`;
2517
1067
  }
2518
1068
  }
2519
- export async function runServicesRun(backend, config, serviceID, input, options = {}) {
2520
- let id = options.executionID;
2521
- try {
2522
- if (!id) {
2523
- const started = await backend.startServiceExecution({
2524
- service_id: serviceID,
2525
- client_context: { host: options.host ?? "terminal", features: [...RAIL_PROGRESSIVE_FEATURES], ...(options.target ? { target: options.target } : {}) },
2526
- ...(input !== undefined ? { input } : {}),
2527
- });
2528
- id = started.execution.service_execution_id;
2529
- if (!started.workflow_entry) {
2530
- await runServicesNext(backend, id, options);
2531
- return;
2532
- }
2533
- }
2534
- let model = await backend.getServiceExecution(id);
2535
- if (model.execution.service_id !== serviceID)
2536
- throw new Error("execution belongs to another service");
2537
- if (!model.workflow_entry || (input === undefined && !model.workflow)) {
2538
- await runServicesNext(backend, id, options);
2539
- return;
2540
- }
2541
- if (input !== undefined) {
2542
- model = await backend.advanceServiceExecution(id, input, `workflow-input:${id}`);
2543
- }
2544
- else if (model.workflow?.status === "quota_paused") {
2545
- model = await backend.advanceServiceExecution(id, undefined, `workflow-resume:${id}`);
2546
- }
2547
- const until = Date.now() + (options.timeoutSeconds ?? 120) * 1000;
2548
- const sleep = options.sleep ?? ((milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds)));
2549
- const paid = () => model.payment_bindings.some((binding) => binding.status === "payment_verified") || model.checkout_bindings.some((binding) => binding.status === "payment_verified");
2550
- // A planning projection can exist before any usable result or user action.
2551
- // Keep the existing bounded wait until there is something to hand over.
2552
- while ((["queued", "running", "delivery"].includes(model.workflow?.status ?? "") || (model.workflow?.status === "payment" && paid())) && shouldWaitForServiceResult(model) && Date.now() < until) {
2553
- await sleep(options.pollIntervalMS ?? 1500);
2554
- model = await backend.getServiceExecution(id);
2555
- }
2556
- if (model.refunds.some(refund => refund.access_locked)) {
2557
- await runServicesNext(backend, id, options);
2558
- return;
2559
- }
2560
- if (model.workflow?.status === "payment" && !paid()) {
2561
- await runServicesCheckout(backend, config, id, model.workflow_entry?.capability_id, {
2562
- ...options,
2563
- ...(config.agentType ? { agentType: config.agentType } : {}),
2564
- resume: model.checkout_bindings.length > 0,
2565
- });
2566
- return;
2567
- }
2568
- await runServicesNext(backend, id, options);
2569
- }
2570
- catch (cause) {
2571
- if (cause instanceof HttpError && cause.code === "capability_input_invalid") {
2572
- const schema = cause.payload?.input_schema;
2573
- const executionID = cause.payload?.service_execution_id ?? id;
2574
- throw new CommandContractError(cause.code, cause.message, "Agent按当前模板自行修正错误字段和JSON层级,保留已有摘要的端点含义及原地点/日期/人数,不重复研究或事后补写冒充查询前摘要;不要向用户询问技术字段。只有真实业务歧义才问人;使用返回的执行身份恢复,未创建时用原服务。", schema ? [] : [{ command: executionID ? `itpay services get ${executionID} --json` : "itpay catalog list --json", reason: "获取当前锁定合同;仍缺合同则停止,不猜旧写法" }], schema ? { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: {
2575
- command: `itpay services run ${serviceID}${executionID ? ` --execution ${executionID}` : ""} --input-json <file> --json`,
2576
- input: serviceInputTemplate(schema, input), required_input: ["file", ...requiredInputFields(schema)], executable: false,
2577
- } } : undefined, { service_id: serviceID, ...(executionID ? { service_execution_id: executionID } : {}), execution_created: cause.payload?.execution_created ?? Boolean(executionID), ...(typeof cause.payload?.provider_called === "boolean" ? { provider_called: cause.payload.provider_called } : {}), input_errors: cause.payload?.input_errors ?? [cause.message], ...(schema ? { input_schema: schema } : {}) });
2578
- }
2579
- if (cause instanceof HttpError && ["invalid_selection", "selection_wrong_buyer"].includes(cause.code ?? "")) {
2580
- const reason = cause.payload?.reason;
2581
- const inputInvalid = reason === "input_constraint_invalid";
2582
- throw new CommandContractError(cause.code, cause.message, inputInvalid ? "输入违反当前完整计划约束。读取原查询的完整购买模板,修正人数、完整段数和顺序;不要追加未声明字段或拆单。"
2583
- : cause.code === "selection_wrong_buyer" ? "当前身份无权使用这份购买凭据。停止;不要切换身份绕过,也不展示其他买家资料。"
2584
- : reason === "stored_selection_inconsistent" ? "服务端凭据与已存目录不一致,补参数无法修复。停止购买并提供诊断标识 stored_selection_inconsistent;不猜字段、不重新创建购买。"
2585
- : "购买凭据未通过校验,原因尚未确定且没有安全自动恢复。停止,不把推测报成原因或追加隐藏字段。", inputInvalid ? [{ command: "itpay services list --json", reason: "定位原查询并读取当前完整购买模板" }, { command: "itpay docs show rail-booking --json", reason: "读取正式输入约束" }] : [], undefined, { service_id: serviceID, ...(id ? { service_execution_id: id } : {}), ...(reason ? { reason } : {}), execution_created: Boolean(id) });
2586
- }
2587
- if (cause instanceof HttpError && cause.code === "rail_selection_expired") {
2588
- const expiry = cause.payload?.selection_expiry;
2589
- const bookingID = expiry?.booking_execution_id ?? id;
2590
- const refreshInput = !bookingID && expiry?.safe_new_query && ["itpay-rail-smart", "itpay-rail-exact"].includes(expiry.query_service_id) ? expiry.query_input : undefined;
2591
- const refresh = refreshInput !== undefined;
2592
- throw new CommandContractError(cause.code, cause.message, refresh ? "刚才余票信息已过时,按原条件刷新一次真实库存(可能消耗查询额度);不需要重复批准同条件查票。有价格、库存或条款变化再确认;不复用旧库存、不换站或日期。"
2593
- : bookingID ? "保留原购买执行,先读取订单与付款事实;不要重新查票、创建替代购买或重复付款。"
2594
- : "旧选择凭据已过期,历史详情不能续期;缺少完整原查询条件或安全购买状态证据,停止自动刷新。", bookingID ? [{ command: `itpay services next ${bookingID} --json`, reason: "核对原购买执行和付款状态" }] : [], refreshInput && expiry ? { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: {
2595
- command: `itpay services run ${expiry.query_service_id} --input-json <file> --json`, input: refreshInput, required_input: ["file"], executable: false,
2596
- } } : undefined, { service_id: serviceID, ...(bookingID ? { service_execution_id: bookingID } : {}), ...(expiry ? { selection_expiry: expiry } : {}) });
2597
- }
2598
- if (cause instanceof HttpError && cause.code === "rail_journey_selection_required") {
2599
- throw new CommandContractError(cause.code, cause.message, "整程必须使用原查询当前完整 ticket plan 的 selections 模板;手填 legs 只兼容单腿。缺少整程模板时读取原方案;不拆单、不拼腿、不擅自换站或日期。", [{ command: "itpay services list --json", reason: "找回原查询并读取其完整方案模板" }, { command: "itpay docs show rail-booking --json", reason: "读取正式整程输入合同" }], undefined, { service_id: serviceID, ...(id ? { service_execution_id: id } : {}) });
2600
- }
2601
- if (cause instanceof HttpError && cause.code === "rail_seat_code_invalid" && id) {
2602
- throw new CommandContractError(cause.code, cause.message, "席别必须使用原查询 booking_template.seat_choices 中与名称对应的代码,不能填“二等座”等显示名。用原 selection token 与真实人数修正输入,继续同一booking execution;若只是代码写法修正且车次、席别名称、人数等购买条件未变,沿用用户已给的确认,不再重复询问。", [{ command: `itpay services next ${id} --json`, reason: "读取同一booking当前状态后修正输入" }]);
2603
- }
2604
- if (cause instanceof CommandContractError || cause instanceof HttpError || cause instanceof DeviceLockBusyError || cause instanceof DeviceStateError)
2605
- throw cause;
2606
- if (!id) {
2607
- throw new CommandContractError("workflow_start_outcome_unknown", cause instanceof Error ? cause.message : "service creation outcome unknown", "创建服务执行时没有收到完整响应。先查询当前身份可见的执行;不要直接重跑并创建替代执行。", [{ command: "itpay services list --json", reason: "查找可能已经创建的服务执行" }]);
2608
- }
2609
- if (cause instanceof HttpTransportError)
2610
- throw new CommandContractError("workflow_outcome_unknown", cause.message, "未收到完整响应;只读同一执行的权威事实,不重放写操作或创建替代执行。", [{ command: `itpay services next ${id} --json`, reason: "核对同一执行" }]);
2611
- throw new CommandContractError("workflow_run_failed", cause instanceof Error ? cause.message : "workflow run failed", "保留当前执行并按错误处理,不要重复创建服务执行。", [{ command: `itpay services next ${id} --json`, reason: "读取同一执行的当前事实与恢复动作" }]);
2612
- }
2613
- }