@itpay/cli 2.1.4 → 2.1.6

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.
@@ -123,6 +123,9 @@ export class BackendClient {
123
123
  advanceServiceExecution(id, input, idempotencyKey) {
124
124
  return this.http.post(`/v1/service-executions/${encodeURIComponent(id)}/advance`, { input, idempotency_key: idempotencyKey });
125
125
  }
126
+ createRailPhoneLink() {
127
+ return this.http.post("/v1/rail/phone-links", {});
128
+ }
126
129
  invokeServiceCapability(serviceExecutionID, capabilityID, input) {
127
130
  return this.http.post(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/capabilities/${encodeURIComponent(capabilityID)}/invoke`, input, { replaySafe: Boolean(input.idempotency_key) });
128
131
  }
@@ -78,15 +78,36 @@ function pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand, a
78
78
  : {}),
79
79
  ...(platform === "markdown" ? { markdown: buildAgentChatHandoff(plan).markdown } : {}),
80
80
  });
81
+ const railQuote = presentation.rail_quote;
82
+ const railPassengersPending = presentation.checkout_details === "rail_passengers" && !presentation.rail_passengers_confirmed;
81
83
  return {
82
84
  status: "human_checkout_required",
83
85
  result: {
84
86
  checkout_id: presentation.checkout.checkout_id,
85
87
  payment: "pending",
86
88
  amount,
89
+ ...(railQuote ? { rail_quote: {
90
+ passengers: railQuote.passengers,
91
+ expires_at: railQuote.expires_at,
92
+ legs: railQuote.legs.map((leg) => ({
93
+ train_code: leg.train_code,
94
+ travel_date: leg.travel_date,
95
+ route: `${leg.from} → ${leg.to}`,
96
+ time: `${leg.departure}–${leg.arrival}`,
97
+ seat_name: leg.seat_name,
98
+ ...(leg.seat_preferences?.length ? { seat_preferences: leg.seat_preferences } : {}),
99
+ ...(leg.seat_request_planned ? {
100
+ seat_request_planned: leg.seat_request_planned,
101
+ auto_reason: leg.auto_reason ?? "none",
102
+ } : {}),
103
+ })),
104
+ } } : {}),
105
+ ...(railPassengersPending ? { rail_passengers_confirmed: false } : {}),
87
106
  },
88
107
  handoff: presentationHandoff.handoff,
89
- instruction: presentationHandoff.instruction,
108
+ instruction: railPassengersPending
109
+ ? `${presentationHandoff.instruction} 请用户在受保护网页填写乘车人并确认报价;姓名、证件和手机号只在网页填写,不要贴到对话中。座位偏好仅为购票请求、购票时才提交给供应商且不保证满足;无法逐人提交的偏好将自动分配座位,以实际出票为准。`
110
+ : presentationHandoff.instruction,
90
111
  next: { command: nextCommand, reason: "稍后只查询同一 Checkout" },
91
112
  recovery: [],
92
113
  };
@@ -12,6 +12,65 @@ import { renderTerminalQR } from "../render/qr.js";
12
12
  import { buildCheckoutQRPlan } from "./buy.js";
13
13
  import { appendFeedbackPostmortemInstruction, CommandContractError, isTerminalServiceExecutionStatus, writeCommandEnvelope, } from "./guidance.js";
14
14
  const serviceActionStatuses = new Set(["pending", "approved", "rejected", "expired", "cancelled"]);
15
+ const RAIL_SERVICE_GUIDANCE = {
16
+ "itpay-rail-exact": {
17
+ when_to_use: "出发和到达都是明确的火车站名时使用本服务(直达查询,最快)。只有城市、模糊位置或想要方案推荐时改用 itpay-rail-smart。",
18
+ input_fields: [
19
+ { name: "origin", required: true, description: "出发火车站名(如'古镇'),不能是城市或地址", example: "古镇" },
20
+ { name: "destination", required: true, description: "到达火车站名(如'广州南')", example: "广州南" },
21
+ { name: "travel_date", required: true, description: "出行日期 YYYY-MM-DD;字段名必须是 travel_date,'date' 等别名无效", example: "2026-09-19" },
22
+ ],
23
+ input_example: { origin: "古镇", destination: "广州南", travel_date: "2026-09-19" },
24
+ notes: [
25
+ "本服务只接受这三个字段,多传字段会被供应商拒绝",
26
+ "站名不确定就改用 itpay-rail-smart 先解析位置,不要在两个服务之间反复试错",
27
+ ],
28
+ },
29
+ "itpay-rail-smart": {
30
+ when_to_use: "只有城市名、模糊位置或需要中转规划与方案推荐时使用本服务;平台负责把位置解析到车站。",
31
+ input_fields: [
32
+ { name: "origin", required: true, description: "出发地:已知最完整的位置名或区域(城市/区县/地址均可)", example: "中山古镇" },
33
+ { name: "destination", required: true, description: "目的地:与 origin 同样的位置规则", example: "广州南" },
34
+ { name: "travel_date", required: true, description: "出行日期 YYYY-MM-DD;字段名必须是 travel_date", example: "2026-09-19" },
35
+ ],
36
+ optional_fields: [
37
+ { name: "origin_city", description: "出发地城市提示,辅助位置解析" },
38
+ { name: "destination_city", description: "目的地城市提示" },
39
+ { name: "origin_location", description: "已知坐标对象(高德 GCJ-02)" },
40
+ { name: "destination_location", description: "目的地坐标对象" },
41
+ { name: "depart_after", description: "不早于该时间出发" },
42
+ { name: "arrive_before", description: "不晚于该时间到达" },
43
+ { name: "priority", enum: "balanced|fastest|cheapest|safest|flexible", description: "方案偏好" },
44
+ { name: "max_transfers", description: "允许中转次数,0-2" },
45
+ ],
46
+ input_example: { origin: "中山古镇", destination: "广州南", travel_date: "2026-09-19", priority: "fastest" },
47
+ notes: [
48
+ "位置有歧义时会进入位置确认步骤,请用户选定后继续同一执行",
49
+ "推荐结果含每趟车的席别与余票;购票在后续受保护流程完成",
50
+ ],
51
+ },
52
+ };
53
+ function railServiceGuidance(serviceID) {
54
+ return RAIL_SERVICE_GUIDANCE[serviceID];
55
+ }
56
+ const WORKFLOW_STEP_GUIDANCE = {
57
+ input: { meaning: "输入校验", hint: "对照服务声明的输入契约补齐字段后重新发起" },
58
+ quota: { meaning: "额度检查", hint: "免费额度或限流未通过;登录或稍后重试" },
59
+ geo: { meaning: "位置解析", hint: "检查 origin/destination 是否为真实地名;可附 origin_city 或坐标对象提示" },
60
+ search: { meaning: "供应商车次检索", hint: "最常见是字段名错误(必须是 travel_date)或站名不存在;按 itpay docs show rail-booking 核对输入" },
61
+ catalog: { meaning: "可行车次计算", hint: "位置已解析但无可行车次;换日期或换站点重试" },
62
+ recommend: { meaning: "方案推荐", hint: "候选集无法产出推荐;放宽条件或换日期重试" },
63
+ delivery: { meaning: "交付", hint: "结果组装失败;稍后重试或联系运营" },
64
+ };
65
+ function failedWorkflowStep(steps) {
66
+ if (!steps)
67
+ return undefined;
68
+ for (const [step, status] of Object.entries(steps)) {
69
+ if (status === "failure" && step !== "failure")
70
+ return step;
71
+ }
72
+ return undefined;
73
+ }
15
74
  export async function runServicesStart(backend, serviceID, options = {}) {
16
75
  const host = options.host ?? "terminal";
17
76
  const response = await backend.startServiceExecution({
@@ -23,7 +82,8 @@ export async function runServicesStart(backend, serviceID, options = {}) {
23
82
  },
24
83
  });
25
84
  if (response.workflow_entry) {
26
- writeCommandEnvelope({ status: "input_required", result: { service_execution_id: response.execution.service_execution_id, service_id: serviceID, input_schema: response.workflow_entry.input_schema }, instruction: "根据服务声明填写输入,然后继续同一服务执行。", next: { command: `itpay services run ${serviceID} --execution ${response.execution.service_execution_id} --input-json <file> --json`, reason: "提交买家输入" }, recovery: [] }, { ...options });
85
+ const guidance = railServiceGuidance(serviceID);
86
+ writeCommandEnvelope({ status: "input_required", result: { service_execution_id: response.execution.service_execution_id, service_id: serviceID, input_schema: response.workflow_entry.input_schema, ...(guidance ? { guidance } : {}) }, instruction: guidance ? "按 result.guidance 逐项填写输入(when_to_use 说明本服务适用场景、input_fields 是必填契约、input_example 可直接照抄),然后继续同一服务执行。不要臆造字段名。" : "根据服务声明填写输入,然后继续同一服务执行。", next: { command: `itpay services run ${serviceID} --execution ${response.execution.service_execution_id} --input-json <file> --json`, reason: "提交买家输入" }, recovery: [] }, { ...options });
27
87
  return;
28
88
  }
29
89
  const capability = response.capabilities.find((item) => item.phase === response.execution.phase && !item.requires_payment);
@@ -34,6 +94,7 @@ export async function runServicesStart(backend, serviceID, options = {}) {
34
94
  const capabilitySummary = capability ? {
35
95
  capability_id: capability.capability_id,
36
96
  required_input: requiredInput,
97
+ input_schema: capability.input_schema,
37
98
  ...(capability.free_quota_limit !== undefined ? { free_quota_limit: capability.free_quota_limit } : {}),
38
99
  } : null;
39
100
  writeCommandEnvelope({
@@ -45,7 +106,7 @@ export async function runServicesStart(backend, serviceID, options = {}) {
45
106
  capability: capabilitySummary,
46
107
  },
47
108
  instruction: capability
48
- ? "填写首选 capability 的 required_input;一次只提交当前 execution 所代表的服务意图。"
109
+ ? "填写首选 capability 的 required_input;一次只提交当前 execution 所代表的服务意图。" + locationInputInstruction(capability.input_schema)
49
110
  : "当前没有可直接调用的 capability;读取服务端下一步,不要猜测 capability。",
50
111
  next: {
51
112
  command,
@@ -67,10 +128,34 @@ export async function runServicesStart(backend, serviceID, options = {}) {
67
128
  ],
68
129
  });
69
130
  }
131
+ function locationConfirmationEnvelope(executionID, capabilityID, input, confirmation) {
132
+ input = (confirmation.input ?? input);
133
+ const endpoints = (confirmation.endpoints ?? []);
134
+ const choices = Object.fromEntries(endpoints.map((endpoint) => [endpoint.side, "<用户选择的候选 id>"]));
135
+ return {
136
+ status: "location_confirmation_required",
137
+ result: { service_execution_id: executionID, query: input, location_confirmation: confirmation },
138
+ instruction: "尚未查票。仅向用户确认 endpoints 中有歧义的地点,展示真实候选名称、地址和高德链接。不要自行选择候选、把区域改成车站或重复调用原查询。用户选择后保留原输入,带 location_confirmation 回到同一服务。没有候选时请用户补充已知城市或准确地名,再使用修正输入查询。",
139
+ next: null,
140
+ recovery: confirmation.can_resume ? [{
141
+ command: `itpay services invoke ${executionID} --capability ${capabilityID}${formatInputOptions({ ...input,
142
+ location_confirmation: { plan_id: confirmation.plan_id, token: confirmation.token, choices } })} --json`,
143
+ reason: "仅在用户明确选择后替换 choices 并执行;30 分钟有效",
144
+ }] : [],
145
+ };
146
+ }
70
147
  function requiredInputFields(schema) {
71
148
  const required = schema?.required;
72
149
  return Array.isArray(required) ? required.filter((field) => typeof field === "string") : [];
73
150
  }
151
+ function locationInputInstruction(schema) {
152
+ const properties = schema?.properties;
153
+ if (!properties?.origin_location || !properties?.destination_location)
154
+ return "";
155
+ if (!properties.location_confirmation)
156
+ return " 地点保留用户已知最完整名称或城市范围;不要编造坐标或把城市改成同名车站。坐标及其他可选字段严格按当前 input_schema 提交。";
157
+ return " 地点输入:先向用户说明本服务接受两个地点或城市范围。已有可信高德 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 才展示候选名称、地址和高德链接,等待用户选择,禁止自行选第一项。";
158
+ }
74
159
  export async function runServicesInvoke(backend, config, serviceExecutionID, capabilityID, input, options = {}) {
75
160
  const readModel = await backend.getServiceExecution(serviceExecutionID);
76
161
  const requestedCapability = readModel.capabilities.find((capability) => capability.capability_id === capabilityID);
@@ -91,10 +176,26 @@ export async function runServicesInvoke(backend, config, serviceExecutionID, cap
91
176
  }]);
92
177
  }
93
178
  const idempotencyKey = await operationID(config, `service.invoke:${serviceExecutionID}:${capabilityID}:${stableInput(input)}`);
94
- const response = await backend.invokeServiceCapability(serviceExecutionID, capabilityID, {
95
- idempotency_key: idempotencyKey,
96
- redacted_summary: input,
97
- });
179
+ let response;
180
+ try {
181
+ response = await backend.invokeServiceCapability(serviceExecutionID, capabilityID, {
182
+ idempotency_key: idempotencyKey,
183
+ redacted_summary: input,
184
+ });
185
+ }
186
+ catch (error) {
187
+ if (!(error instanceof HttpError) || error.code !== "verified_phone_required")
188
+ throw error;
189
+ const link = await backend.createRailPhoneLink();
190
+ writeCommandEnvelope({
191
+ status: "human_action_required",
192
+ result: { service_execution_id: serviceExecutionID, ...link },
193
+ instruction: "请用户打开 verification_url,在 ItPay 网页验证手机号并确认连接当前 Agent。CLI 不接收手机号或验证码;等待用户完成后再重试查询。",
194
+ next: null,
195
+ recovery: [{ command: `itpay services invoke ${serviceExecutionID} --capability ${capabilityID}${formatInputOptions(input)} --json`, reason: "仅在用户完成网页验证后重试" }],
196
+ }, { ...options, plainResult: [`手机号验证:${link.verification_url}`] });
197
+ return;
198
+ }
98
199
  const envelope = invokedEnvelope(response, requestedCapability, readModel.capabilities, input);
99
200
  writeCommandEnvelope(envelope.value, {
100
201
  ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
@@ -103,6 +204,13 @@ export async function runServicesInvoke(backend, config, serviceExecutionID, cap
103
204
  });
104
205
  }
105
206
  function invokedEnvelope(response, requestedCapability, capabilities, input) {
207
+ if (response.execution_request_id) {
208
+ const id = response.execution.service_execution_id;
209
+ return { value: { status: "running", result: { service_execution_id: id, execution_request_id: response.execution_request_id },
210
+ instruction: "查询已排队或正在运行。告知用户稍候,只读取同一任务的状态;不要重新发起查询,也不要把暂未返回结果说成没有车。",
211
+ next: { command: `itpay services next ${id} --json`, reason: "稍后读取同一查询结果" }, recovery: [] },
212
+ plainResult: ["status: running", `service_execution_id: ${id}`] };
213
+ }
106
214
  const items = response.result_items.map((item) => ({
107
215
  rank: item.rank,
108
216
  title: item.display_title,
@@ -118,11 +226,51 @@ function invokedEnvelope(response, requestedCapability, capabilities, input) {
118
226
  items,
119
227
  ...(quota ? { quota } : {}),
120
228
  };
229
+ const preview = response.invocation?.safe_result_preview;
230
+ if (!response.effective_quota?.exhausted && preview?.search_status === "LOCATION_CONFIRMATION_REQUIRED" && preview.location_confirmation) {
231
+ return { value: locationConfirmationEnvelope(response.execution.service_execution_id, requestedCapability.capability_id, input, preview.location_confirmation),
232
+ plainResult: [JSON.stringify(preview.location_confirmation)] };
233
+ }
234
+ if (typeof preview?.catalog_total === "number") {
235
+ baseResult.catalog = {
236
+ total: preview.catalog_total,
237
+ recommendation: preview.recommendation,
238
+ decision_source: preview.decision_source,
239
+ coverage: preview.coverage,
240
+ notices: preview.notices,
241
+ search_status: preview.search_status,
242
+ searched_scope: preview.searched_scope,
243
+ effective_policy_hash: preview.effective_policy_hash,
244
+ api_cost: preview.api_cost,
245
+ stage_timestamps: preview.stage_timestamps,
246
+ page: preview.catalog_page,
247
+ journey_summary: preview.journey_summary,
248
+ journeys: preview.journeys,
249
+ train_services: preview.train_services,
250
+ resolved_locations: preview.resolved_locations,
251
+ };
252
+ }
121
253
  let status = items.length > 0 ? "result_ready" : "no_result";
122
254
  let instruction = items.length > 0
123
255
  ? "用编号、名称和可公开字段向用户说明候选;若候选列表已满足目标就停止。只有用户明确选择并希望继续时,才提交对应编号;不要向用户提及 safe_payload、Execution 或内部 ID。"
124
256
  : `没有找到与“${queryText(input)}”匹配的结果。向用户展示本次为 0 个结果并停止。不要修改、缩短或猜测其他输入;只有用户明确提供新输入后,才能启动新的查询。`;
257
+ if (items.length === 0 && Array.isArray(preview?.notices)) {
258
+ const railNotice = preview.notices.find((notice) => {
259
+ if (!notice || typeof notice !== "object")
260
+ return false;
261
+ const value = notice;
262
+ return ["RAIL_TRANSFER_SCOPE_LIMIT", "RAIL_TRANSFER_SEARCH_INCOMPLETE"].includes(String(value.code)) && typeof value.message === "string";
263
+ });
264
+ if (railNotice)
265
+ instruction = `向用户展示官方提示:${railNotice.message} 不要断言该行程必须多次中转或没有车。等待用户选择分段查询的起终点,不自动更换输入或重试。`;
266
+ }
125
267
  let next = null;
268
+ if (items.length > 0 && baseResult.catalog) {
269
+ instruction = "先向用户说明排在首位的推荐方案和其他方案的时间、费用与便利性取舍。items 包含本次返回的合格候选,用户不满意时继续从该列表比较,不必重复查票。搜索是否完成、目录是否截断、覆盖范围和模型降级以 catalog 为准;不能把部分结果说成完整搜索。费用尚需购票前核验。姓名、身份证和手机号仅在 ItPay 网页填写。";
270
+ }
271
+ if (items.length > 0 && preview?.journey_summary) {
272
+ instruction = "先按 journey_summary 报告本次已查询范围内的可用车次、换乘走法和实际乘车组合数量,不能用 catalog.total 或 items 数量冒充车次或组合数。按 journeys 展示组合,推荐置顶,保留全部组合和 train_services 车次列表供用户查看。每组先说明乘坐哪些车、在哪里真正换车以及等待多久;rides.onboard_stops 是同车接续停站,无需下车,可能需车内换座,不计入换乘次数。席别、余票、价格及接驳是组合下的选择,通过 candidate_ids 查对应 items;确认具体选择后才使用该 item 的编号,不猜席别或自动付款。30 分钟只是在已确认便捷换乘站点的筛选下限,不是接续保证。不得把多段票称为已可购买的套票;以 purchase_supported 为准。覆盖不完整、截断和模型仅看短名单时必须说明。姓名、身份证和手机号只在 ItPay 网页填写。";
273
+ }
126
274
  if (response.effective_quota?.exhausted) {
127
275
  status = "quota_exhausted";
128
276
  instruction = "免费额度已用完且本次没有调用 Provider。当前没有可购买的 continuation;只读取同一 Execution 的服务端恢复方向。";
@@ -159,6 +307,8 @@ function invokedEnvelope(response, requestedCapability, capabilities, input) {
159
307
  else if (items.length === 0) {
160
308
  next = null;
161
309
  }
310
+ if (preview?.resolved_locations)
311
+ instruction += " 按 resolved_locations 说明实际解析地点;区域级位置仅是范围代表点,接驳时间不是从用户精确位置计算的。";
162
312
  return {
163
313
  value: { status, result: baseResult, instruction, next, recovery: [] },
164
314
  plainResult: serviceResultPlainLines(baseResult),
@@ -177,6 +327,14 @@ function serviceResultPlainLines(result) {
177
327
  }
178
328
  if (items.length === 0)
179
329
  lines.push("results: 0");
330
+ const catalog = result.catalog;
331
+ if (catalog?.resolved_locations)
332
+ lines.push(`resolved_locations: ${JSON.stringify(catalog.resolved_locations)}`);
333
+ if (catalog?.journey_summary) {
334
+ for (const key of ["journey_summary", "train_services", "journeys"]) {
335
+ lines.push(`${key}: ${JSON.stringify(catalog[key])}`);
336
+ }
337
+ }
180
338
  if (result.quota)
181
339
  lines.push(`quota: ${JSON.stringify(result.quota)}`);
182
340
  if (result.checkout)
@@ -212,6 +370,8 @@ function checkoutCommand(serviceExecutionID, capability, input, fillMissing = tr
212
370
  return `itpay services checkout ${serviceExecutionID} --capability ${capability.capability_id}${formatInputOptions(lockedInput)}${capability.delivery_email_required ? " --email <email>" : ""} --json`;
213
371
  }
214
372
  function capabilityPrice(capability) {
373
+ if (capability.pricing_method === "rail_fare_plus_fee")
374
+ return "实时票款 + 每张票 2 元服务费(最终金额以锁定报价为准)";
215
375
  return capability.price_amount_minor !== undefined && capability.price_currency
216
376
  ? formatMoney(capability.price_amount_minor, capability.price_currency)
217
377
  : "当前发布价格";
@@ -279,7 +439,7 @@ function formatInputOptions(input) {
279
439
  .sort(([left], [right]) => left.localeCompare(right))
280
440
  .map(([key, value]) => String(value) === "<value>"
281
441
  ? ` --input ${key}=<value>`
282
- : ` --input ${shellArgument(`${key}=${String(value)}`)}`)
442
+ : ` --input ${shellArgument(`${key}=${typeof value === "object" && value !== null ? JSON.stringify(value) : String(value)}`)}`)
283
443
  .join("");
284
444
  }
285
445
  function shellArgument(value) {
@@ -652,11 +812,176 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
652
812
  plainResult: servicesNextPlainResult(envelope.result),
653
813
  });
654
814
  }
815
+ function railLegSeatSummary(seat) {
816
+ return {
817
+ passenger_index: seat.passenger_index,
818
+ ...(seat.seat_type_name ? { seat_type_name: seat.seat_type_name } : {}),
819
+ ...(seat.seat_label || seat.coach_no || seat.seat_no
820
+ ? { seat: seat.seat_label ?? [seat.coach_no ? `${seat.coach_no}车` : "", seat.seat_no ?? ""].join("") }
821
+ : {}),
822
+ confirmed: seat.confirmed,
823
+ };
824
+ }
825
+ function railBookingEnvelope(model) {
826
+ const rail = model.rail_booking;
827
+ const execution = model.execution;
828
+ const orderID = (model.current_delivery ?? model.delivery_bindings.at(-1))?.order_id;
829
+ const legs = rail.legs.map((leg) => ({
830
+ leg_index: leg.leg_index,
831
+ state: leg.state,
832
+ issued: leg.issued,
833
+ ...(leg.details_pending ? { details_pending: true } : {}),
834
+ ...(leg.supplier_state ? { supplier_state: leg.supplier_state } : {}),
835
+ ...(leg.train_code ? { train_code: leg.train_code } : {}),
836
+ ...(leg.travel_date ? { travel_date: leg.travel_date } : {}),
837
+ ...(leg.from || leg.to ? { route: `${leg.from ?? ""} → ${leg.to ?? ""}` } : {}),
838
+ ...(leg.departure || leg.arrival ? { time: `${leg.departure ?? ""}–${leg.arrival ?? ""}` } : {}),
839
+ ...(leg.seat_name ? { seat_name: leg.seat_name } : {}),
840
+ ...(leg.seat_request ? { seat_request: leg.seat_request } : {}),
841
+ ...(leg.seat_preferences?.length ? { seat_preferences: leg.seat_preferences } : {}),
842
+ ...(leg.seats?.length ? { seats: leg.seats.map(railLegSeatSummary) } : {}),
843
+ }));
844
+ const result = {
845
+ service_execution_id: execution.service_execution_id,
846
+ ...(orderID ? { order_id: orderID } : {}),
847
+ rail: { state: rail.state, issued_legs: rail.issued_legs, legs },
848
+ ...(model.workflow ? { workflow: model.workflow } : {}),
849
+ };
850
+ if (rail.state === "issued") {
851
+ const detailsPending = rail.legs.some((leg) => leg.details_pending);
852
+ return {
853
+ status: "issued",
854
+ result,
855
+ instruction: `告诉用户:车票已出票,座位以实际出票为准,平台不提供 12306 票号,可在订单页核对行程。不要在对话中索要乘车人身份信息。${detailsPending ? "部分席位信息未能同步,请前往 12306 核对行程;不要重复购买。" : ""}`,
856
+ next: orderID ? { command: `itpay order ${orderID} --json`, reason: "查看订单及退款入口" } : null,
857
+ recovery: [],
858
+ };
859
+ }
860
+ if (rail.state === "manual_review") {
861
+ return {
862
+ status: "manual_review",
863
+ result,
864
+ instruction: "告诉用户:付款已确认,订单需要人工核对;请勿重复付款或重新下单,已出票的车票会保留。不要承诺退款结果或时效。",
865
+ next: orderID ? { command: `itpay order ${orderID} --json`, reason: "查看同一订单当前状态" } : null,
866
+ recovery: [],
867
+ };
868
+ }
869
+ // A rail booking run only exists after a verified payment, so payment is
870
+ // confirmed here — but only `pending` actually means supplier issuance is in
871
+ // flight. Any state added later falls back to a conservative re-read instead
872
+ // of claiming issuance.
873
+ if (rail.state === "pending") {
874
+ return {
875
+ status: "issuing",
876
+ result,
877
+ instruction: "告诉用户:付款已确认,后台正在出票;付款成功不代表已出票,请勿重复购买或再次付款。稍后只读取同一任务。",
878
+ next: { command: `itpay services next ${execution.service_execution_id} --json`, reason: "稍后读取同一出票任务" },
879
+ recovery: [],
880
+ };
881
+ }
882
+ return {
883
+ status: "processing",
884
+ result,
885
+ instruction: `告诉用户:付款已确认,订单处理状态待确认(${rail.state});付款成功不代表已出票,请勿重复购买或再次付款。稍后只读取同一任务。`,
886
+ next: { command: `itpay services next ${execution.service_execution_id} --json`, reason: "稍后读取同一出票任务" },
887
+ recovery: [],
888
+ };
889
+ }
890
+ function terminalExecutionEnvelope(model) {
891
+ const execution = model.execution;
892
+ const currentDelivery = model.current_delivery ?? model.delivery_bindings.at(-1);
893
+ if (!isTerminalServiceExecutionStatus(execution.status) ||
894
+ (model.workflow_entry && (currentDelivery || serviceDeliveryMode(model) === "agent_visible_result") && ["completed", "delivery_completed"].includes(execution.status))) {
895
+ return null;
896
+ }
897
+ const paid = model.checkout_bindings.some((binding) => binding.status === "payment_verified") || Boolean(currentDelivery?.order_id);
898
+ const paidFailure = execution.status === "failed" && paid;
899
+ return {
900
+ status: execution.status,
901
+ result: {
902
+ service_execution_id: execution.service_execution_id,
903
+ service_id: execution.service_id,
904
+ phase: execution.phase,
905
+ ...(currentDelivery?.order_id ? { order_id: currentDelivery.order_id } : {}),
906
+ },
907
+ instruction: execution.status === "refunded"
908
+ ? "告诉用户这笔服务已经退款并永久结束。Agent 不重放服务步骤、不创建付款页面或尝试读取旧交付。"
909
+ : paidFailure
910
+ ? appendFeedbackPostmortemInstruction("告诉用户:付款和订单已经记录,但本次服务没有正常完成,不需要再次付款或重新下单。然后从同一订单检查退款状态;Agent 不重放服务步骤、创建付款页面或再次调用数据来源,也不把技术故障归咎于用户。", "failed")
911
+ : "告诉用户本次服务已经结束且没有可继续的交付。Agent 不重放服务步骤或创建付款页面。",
912
+ next: null,
913
+ recovery: [
914
+ ...(paidFailure
915
+ ? [{
916
+ command: currentDelivery?.order_id
917
+ ? `itpay order ${currentDelivery.order_id} --json`
918
+ : "itpay orders --json",
919
+ reason: "恢复同一笔已付款订单及其退款状态",
920
+ }]
921
+ : []),
922
+ {
923
+ command: `itpay services events ${execution.service_execution_id} --json`,
924
+ reason: "仅在需要诊断终止原因时读取事件",
925
+ },
926
+ ],
927
+ };
928
+ }
655
929
  function servicesNextEnvelope(model) {
656
930
  const execution = model.execution;
657
931
  const currentDelivery = model.current_delivery ?? model.delivery_bindings.at(-1);
658
932
  const lockedRefund = model.refunds.find((refund) => refund.access_locked);
659
- if (model.workflow_entry && !lockedRefund && !["completed", "delivery"].includes(model.workflow?.status ?? "")) {
933
+ if (lockedRefund) {
934
+ const terminal = lockedRefund.status === "succeeded";
935
+ return {
936
+ status: "delivery_locked",
937
+ result: {
938
+ service_execution_id: execution.service_execution_id,
939
+ access_locked: true,
940
+ refund: {
941
+ refund_request_id: lockedRefund.refund_request_id,
942
+ status: lockedRefund.status,
943
+ },
944
+ },
945
+ instruction: terminal
946
+ ? "告诉用户退款已由 ItPay 确认成功,原交付永久关闭。Agent 停止读取和跟踪,不再创建授权。"
947
+ : "告诉用户退款仍在处理,原交付已按政策冻结。然后读取同一退款的权威状态;Agent 不读取交付、不创建授权或重复申请。",
948
+ next: terminal ? null : {
949
+ command: `itpay refund get ${lockedRefund.refund_request_id} --json`,
950
+ reason: "读取退款权威状态",
951
+ },
952
+ recovery: [],
953
+ };
954
+ }
955
+ const latestRun = model.execution_requests?.filter((request) => request.execution_kind === "service.execution.run").at(-1);
956
+ if (latestRun && ["pending", "started"].includes(latestRun.status)) {
957
+ return {
958
+ status: "running",
959
+ result: { service_execution_id: execution.service_execution_id, execution_request_id: latestRun.execution_request_id },
960
+ instruction: "任务仍在排队或执行中,稍后读取同一任务。不要重新查票、重复提交输入或把等待状态当作零结果。",
961
+ next: { command: `itpay services next ${execution.service_execution_id} --json`, reason: "稍后读取同一任务状态" },
962
+ recovery: [],
963
+ };
964
+ }
965
+ // A terminal execution wins over the rail read model: a failed, cancelled,
966
+ // or refunded run must not render as still issuing. Completed executions
967
+ // keep the rail view so issued seats stay visible.
968
+ if (model.rail_booking && isTerminalServiceExecutionStatus(execution.status) && !["completed", "delivery_completed"].includes(execution.status)) {
969
+ const terminal = terminalExecutionEnvelope(model);
970
+ if (terminal)
971
+ return terminal;
972
+ }
973
+ if (model.rail_booking)
974
+ return railBookingEnvelope(model);
975
+ if (latestRun && ["failed", "cancelled"].includes(latestRun.status) && !model.workflow_entry) {
976
+ return {
977
+ status: latestRun.status,
978
+ result: { service_execution_id: execution.service_execution_id },
979
+ instruction: "本次任务未完成,需要检查同一任务的处理记录;这不表示没有结果。不要自动重新发起。",
980
+ next: null,
981
+ recovery: [{ command: `itpay services events ${execution.service_execution_id} --json`, reason: "读取同一任务的处理记录" }],
982
+ };
983
+ }
984
+ if (model.workflow_entry && !["completed", "delivery"].includes(model.workflow?.status ?? "")) {
660
985
  const id = execution.service_execution_id;
661
986
  const paymentVerified = model.payment_bindings.some((binding) => binding.status === "payment_verified") || model.checkout_bindings.some((binding) => binding.status === "payment_verified");
662
987
  const state = model.workflow?.status === "payment" && paymentVerified ? "running" : model.workflow?.status ?? "input_required";
@@ -685,6 +1010,21 @@ function servicesNextEnvelope(model) {
685
1010
  command = `itpay services checkout ${id} --json`;
686
1011
  if (state === "input_required")
687
1012
  command = `itpay services run ${execution.service_id} --execution ${id} --input-json <file> --json`;
1013
+ const guidance = railServiceGuidance(execution.service_id);
1014
+ const failedStep = recovery ? failedWorkflowStep(model.workflow?.steps) : undefined;
1015
+ const failedStepGuidance = failedStep ? WORKFLOW_STEP_GUIDANCE[failedStep] : undefined;
1016
+ const failedInvocation = recovery
1017
+ ? [...model.provider_invocations].reverse().find((item) => typeof item.status === "string" && item.status.startsWith("failed"))
1018
+ : undefined;
1019
+ const errorDetail = typeof failedInvocation?.error_message === "string" && failedInvocation.error_message ? failedInvocation.error_message : undefined;
1020
+ const providerErrorCode = typeof failedInvocation?.error_code === "string" && failedInvocation.error_code ? failedInvocation.error_code : undefined;
1021
+ const failedInstruction = recovery
1022
+ ? failedStepGuidance
1023
+ ? `执行在「${failedStepGuidance.meaning}」步失败:${failedStepGuidance.hint}${errorDetail ? `。供应商返回:${errorDetail}` : ""}。本执行已终止不能续用;修正后用同一服务新建执行重试,不要盲目重放。`
1024
+ : state === "failed"
1025
+ ? `执行已失败${failedStep ? `(失败步骤:${failedStep})` : ""}且不可续用;修正输入后用同一服务新建执行重试。`
1026
+ : "执行未完成,请按步骤错误处理;不要重建执行或重复调用。"
1027
+ : undefined;
688
1028
  return {
689
1029
  status: state,
690
1030
  result: {
@@ -692,72 +1032,38 @@ function servicesNextEnvelope(model) {
692
1032
  service_id: execution.service_id,
693
1033
  workflow: model.workflow,
694
1034
  ...(state === "input_required" ? { input_schema: model.workflow_entry.input_schema } : {}),
1035
+ ...(guidance && state === "input_required" ? { guidance } : {}),
1036
+ ...(failedStep ? { failed_step: failedStep, ...(failedStepGuidance ? { failed_step_meaning: failedStepGuidance.meaning } : {}) } : {}),
1037
+ ...(providerErrorCode ? { provider_error_code: providerErrorCode } : {}),
1038
+ ...(errorDetail ? { error_detail: errorDetail } : {}),
1039
+ ...(recovery ? { retryable: state === "failed" } : {}),
695
1040
  },
696
- instruction: recovery
697
- ? "执行未完成,请按步骤错误处理;不要重建执行或重复调用。"
698
- : state === "payment"
1041
+ instruction: failedInstruction
1042
+ ?? (state === "payment"
699
1043
  ? "服务已到付款步骤,使用现有 Checkout 完成扫码付款。"
700
- : "继续读取同一执行;缺少输入时按服务声明补齐。",
1044
+ : guidance
1045
+ ? "按 result.guidance 的字段契约填写输入后继续同一服务执行;不要臆造字段名。"
1046
+ : "继续读取同一执行;缺少输入时按服务声明补齐。"),
701
1047
  next: recovery ? null : { command, reason: "继续当前流程" },
702
- recovery: [],
703
- };
704
- }
705
- if (lockedRefund) {
706
- const terminal = lockedRefund.status === "succeeded";
707
- return {
708
- status: "delivery_locked",
709
- result: {
710
- service_execution_id: execution.service_execution_id,
711
- access_locked: true,
712
- refund: {
713
- refund_request_id: lockedRefund.refund_request_id,
714
- status: lockedRefund.status,
715
- },
716
- },
717
- instruction: terminal
718
- ? "告诉用户退款已由 ItPay 确认成功,原交付永久关闭。Agent 停止读取和跟踪,不再创建授权。"
719
- : "告诉用户退款仍在处理,原交付已按政策冻结。然后读取同一退款的权威状态;Agent 不读取交付、不创建授权或重复申请。",
720
- next: terminal ? null : {
721
- command: `itpay refund get ${lockedRefund.refund_request_id} --json`,
722
- reason: "读取退款权威状态",
723
- },
724
- recovery: [],
725
- };
726
- }
727
- if (isTerminalServiceExecutionStatus(execution.status) && !(model.workflow_entry && (currentDelivery || serviceDeliveryMode(model) === "agent_visible_result") && ["completed", "delivery_completed"].includes(execution.status))) {
728
- const paid = model.checkout_bindings.some((binding) => binding.status === "payment_verified") || Boolean(currentDelivery?.order_id);
729
- const paidFailure = execution.status === "failed" && paid;
730
- return {
731
- status: execution.status,
732
- result: {
733
- service_execution_id: execution.service_execution_id,
734
- service_id: execution.service_id,
735
- phase: execution.phase,
736
- ...(currentDelivery?.order_id ? { order_id: currentDelivery.order_id } : {}),
737
- },
738
- instruction: execution.status === "refunded"
739
- ? "告诉用户这笔服务已经退款并永久结束。Agent 不重放服务步骤、不创建付款页面或尝试读取旧交付。"
740
- : paidFailure
741
- ? appendFeedbackPostmortemInstruction("告诉用户:付款和订单已经记录,但本次服务没有正常完成,不需要再次付款或重新下单。然后从同一订单检查退款状态;Agent 不重放服务步骤、创建付款页面或再次调用数据来源,也不把技术故障归咎于用户。", "failed")
742
- : "告诉用户本次服务已经结束且没有可继续的交付。Agent 不重放服务步骤或创建付款页面。",
743
- next: null,
744
- recovery: [
745
- ...(paidFailure
746
- ? [{
747
- command: currentDelivery?.order_id
748
- ? `itpay order ${currentDelivery.order_id} --json`
749
- : "itpay orders --json",
750
- reason: "恢复同一笔已付款订单及其退款状态",
751
- }]
752
- : []),
753
- {
754
- command: `itpay services events ${execution.service_execution_id} --json`,
755
- reason: "仅在需要诊断终止原因时读取事件",
756
- },
757
- ],
1048
+ recovery: recovery && state === "failed"
1049
+ ? [
1050
+ { command: `itpay services start ${execution.service_id} --json`, reason: "修正输入后重新发起(本执行已终止不能续用)" },
1051
+ ...(guidance ? [{ command: "itpay docs show rail-booking --json", reason: "查看本服务输入字段契约与示例" }] : []),
1052
+ ]
1053
+ : [],
758
1054
  };
759
1055
  }
1056
+ const terminalEnvelope = terminalExecutionEnvelope(model);
1057
+ if (terminalEnvelope)
1058
+ return terminalEnvelope;
760
1059
  const currentItems = model.current_result_items ?? [];
1060
+ const latestInvocation = model.provider_invocations.at(-1);
1061
+ const latestPreview = latestInvocation?.safe_result_preview;
1062
+ if (currentItems.length === 0 && latestPreview?.search_status === "LOCATION_CONFIRMATION_REQUIRED" && latestPreview.location_confirmation) {
1063
+ const capability = model.capabilities.find((item) => item.capability_id === latestInvocation?.capability_id && item.phase === execution.phase && !item.requires_payment);
1064
+ if (capability)
1065
+ return locationConfirmationEnvelope(execution.service_execution_id, capability.capability_id, (latestInvocation?.request_summary ?? {}), latestPreview.location_confirmation);
1066
+ }
761
1067
  const delivery = currentDelivery;
762
1068
  const deliveryMode = serviceDeliveryMode(model);
763
1069
  const candidateSelection = model.allowed_actions?.find((action) => action.type === "select_candidate");
@@ -888,7 +1194,7 @@ function servicesNextEnvelope(model) {
888
1194
  ? "告诉用户付款和订单已经确认,结果仍在同一笔服务中处理,不需要再次付款;如果最终无法交付,将从原订单检查退款路径。稍后只执行 next.command;Agent 不创建新服务、付款页面或数据请求,也不承诺退款结果。"
889
1195
  : preferred?.requires_human
890
1196
  ? "当前下一步需要用户明确选择;先展示必要信息并等待确认。"
891
- : preferred ? "执行服务端返回的唯一首选动作;不要猜测其他 capability。" : "当前没有后续动作。",
1197
+ : preferred ? "执行服务端返回的唯一首选动作;不要猜测其他 capability。" + locationInputInstruction(model.capabilities.find((item) => item.capability_id === preferred.capability_id)?.input_schema) : "当前没有后续动作。",
892
1198
  next,
893
1199
  recovery: [{ command: `itpay services get ${execution.service_execution_id} --json`, reason: "仅在当前动作异常时检查时间线" }],
894
1200
  };
@@ -1039,6 +1345,17 @@ export function collectOption(value, previous = []) {
1039
1345
  return previous;
1040
1346
  }
1041
1347
  function parseValue(value) {
1348
+ // Structured inputs (objects/arrays) are passed as JSON; a string that merely
1349
+ // starts with a JSON delimiter but is not valid JSON stays a string — the
1350
+ // backend schema validates the field type either way.
1351
+ if (value.startsWith("{") || value.startsWith("[")) {
1352
+ try {
1353
+ return JSON.parse(value);
1354
+ }
1355
+ catch {
1356
+ return value;
1357
+ }
1358
+ }
1042
1359
  if (value === "true")
1043
1360
  return true;
1044
1361
  if (value === "false")
@@ -15,8 +15,8 @@ import { OperationJournal } from "./operation_journal.js";
15
15
  export const DEFAULT_BASE_URL = "https://app.itpay.ai";
16
16
  export const DEV_BASE_URL = "https://dev.itpay.ai";
17
17
  export const SANDBOX_BASE_URL = "https://sandbox.itpay.ai";
18
- export const CLI_VERSION = "2.1.4";
19
- export const API_CONTRACT_REVISION = "sha256:7f8e09ecee0ab32e3eed3690341fa5bbf6d955dbb464fd79ce0e340667b1085e";
18
+ export const CLI_VERSION = "2.1.6";
19
+ export const API_CONTRACT_REVISION = "sha256:42659989d533d5a43c4ab92eb2455bcbbc6456e799fa4c2b6587de77f06b95f7";
20
20
  const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
21
21
  const CART_SESSION_FILENAME = "cart.json";
22
22
  const OPERATION_JOURNAL_FILENAME = "operations.json";
@@ -2,7 +2,7 @@ import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, ran
2
2
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, resolve } from "node:path";
5
- const PROTECTED_PATHS = ["/v1/agent-device-account-bindings", "/v1/carts", "/v1/service-executions", "/v1/agent-instances", "/v1/orders", "/v1/refunds", "/v1/me", "/v1/vault"];
5
+ const PROTECTED_PATHS = ["/v1/agent-device-account-bindings", "/v1/carts", "/v1/service-executions", "/v1/agent-instances", "/v1/orders", "/v1/refunds", "/v1/me", "/v1/vault", "/v1/rail/phone-links"];
6
6
  export class DeviceAuthority {
7
7
  baseURL;
8
8
  backendKey;
@@ -0,0 +1,80 @@
1
+ {
2
+ "schema_version": "itp.agent_doc.v1",
3
+ "role": "buyer",
4
+ "product_scope": "itpay is the single public CLI entry point, and $itpay is its user-facing Skill invocation. Under that one product entry point, the two top-level commerce actions are buy and sell: Buyer workflows are available now; Seller workflows will use the same entry point and are not implemented yet.",
5
+ "topic": "rail-booking",
6
+ "title": "Railway Query, Planning, and Ticket Booking",
7
+ "purpose": "Choose the right railway service for the buyer's situation, submit the exact input contract each service expects, and follow the protected booking flow without inventing field names.",
8
+ "when_to_use": [
9
+ "The buyer wants train schedules, routes, or tickets.",
10
+ "A railway service execution fails at the geo, search, catalog, or recommend step.",
11
+ "The buyer only knows a city or fuzzy location rather than a station name.",
12
+ "The buyer asks about seat preferences, refunds, or ticket issuance status."
13
+ ],
14
+ "service_selection": {
15
+ "itpay-rail-exact": "Use when the buyer knows BOTH the exact departure and arrival railway station names (e.g. 古镇 → 广州南). Direct train query, fastest path. Do NOT use it with only a city or fuzzy location.",
16
+ "itpay-rail-smart": "Use when the buyer has a city name, an area, an address, or wants transfer planning and recommendations. The platform resolves locations to stations itself; never silently convert a city into its namesake station.",
17
+ "svc_qizhidao_company_lookup": "Not a railway service; do not mix intents."
18
+ },
19
+ "input_contract": {
20
+ "itpay-rail-exact": {
21
+ "required": {
22
+ "origin": "Departure railway station name only, e.g. 古镇. Not a city or an address.",
23
+ "destination": "Arrival railway station name only, e.g. 广州南.",
24
+ "travel_date": "Travel date in YYYY-MM-DD. The field name must be exactly travel_date; aliases like date are rejected."
25
+ },
26
+ "optional": "None. additionalProperties is false — extra fields are rejected by the provider.",
27
+ "example": {"origin": "古镇", "destination": "广州南", "travel_date": "2026-09-19"}
28
+ },
29
+ "itpay-rail-smart": {
30
+ "required": {
31
+ "origin": "Fullest known departure location: city, district, area, or address is all acceptable.",
32
+ "destination": "Fullest known arrival location, same rule as origin.",
33
+ "travel_date": "Travel date in YYYY-MM-DD. The field name must be exactly travel_date."
34
+ },
35
+ "optional": {
36
+ "origin_city": "Departure city hint that helps location resolution.",
37
+ "destination_city": "Arrival city hint.",
38
+ "origin_location": "Known coordinate object (AMap GCJ-02).",
39
+ "destination_location": "Destination coordinate object.",
40
+ "depart_after": "Do not depart before this time.",
41
+ "arrive_before": "Do not arrive after this time.",
42
+ "priority": "One of balanced | fastest | cheapest | safest | flexible.",
43
+ "max_transfers": "Integer 0-2, allowed transfer count."
44
+ },
45
+ "example": {"origin": "中山古镇", "destination": "广州南", "travel_date": "2026-09-19", "priority": "fastest"}
46
+ }
47
+ },
48
+ "flow": [
49
+ "services start <service> --json returns input_required plus the guidance block for rail services.",
50
+ "Submit input with services run <service> --execution <id> --input-json <file> --json on the SAME execution.",
51
+ "itpay-rail-smart may stop at a location confirmation step; show the candidate places to the buyer, then continue the same execution after the human confirms.",
52
+ "After a train and seat are chosen, the booking flow moves to a protected checkout page: passenger identity data is entered by the human on that page, never through the CLI.",
53
+ "Payment success does not mean the ticket is issued. Issuance is asynchronous; keep reading the same execution with services next.",
54
+ "If seat details are still syncing after issuance, advise the buyer to verify the itinerary on 12306 instead of repeating the query."
55
+ ],
56
+ "failure_handling": {
57
+ "failed_step": "The CLI reports failed_step and failed_step_meaning in the result when a workflow fails.",
58
+ "geo": "Location resolution failed: check origin/destination are real place names, or attach origin_city/coordinate hints, then start a NEW execution (a failed execution is terminal).",
59
+ "search": "Supplier query failed: most commonly a wrong field name (must be travel_date) or a nonexistent station name; recheck input against this doc.",
60
+ "catalog_or_recommend": "Location resolved but no feasible trains: change the date or stations.",
61
+ "general_rule": "Never replay a failed execution and never guess different field names; correct the input and start a new execution of the same service."
62
+ },
63
+ "seat_preferences": {
64
+ "meaning": "A buyer's seat-letter preference may be planned per leg before payment. The plan is submitted to the supplier when encodable; unsupported combinations fall back to automatic assignment.",
65
+ "letters": "Evidence-backed letters only: O=ABCDF (second class), M=ACDF (first class), 9=ACF (business class). Other layouts auto-assign.",
66
+ "boundary": "Never claim a preference was fulfilled before supplier confirmation; check the issued seat in the order/status output."
67
+ },
68
+ "agent_rules": [
69
+ "Pick the service by the buyer's input precision: exact station names → itpay-rail-exact; anything fuzzier → itpay-rail-smart.",
70
+ "Never invent input field names. If the guidance block or this doc does not list a field, do not send it.",
71
+ "Never ask the buyer for passenger names, ID numbers, or phone numbers in chat; those are collected only on the protected checkout page.",
72
+ "Do not repeat or replay a failed execution; correct the input and start a new one.",
73
+ "Do not claim payment equals ticket issuance; issuance is asynchronous and confirmed only by the issued seat status."
74
+ ],
75
+ "human_language": {
76
+ "prefer": ["车次", "出发站", "到达站", "出行日期", "座位", "余票", "出票中", "已出票"],
77
+ "avoid": ["workflow", "execution", "provider", "seat_source", "candidate_id"]
78
+ },
79
+ "search_terms": ["rail", "railway", "train", "ticket", "火车", "火车票", "高铁", "车票", "车次", "seat", "booking", "rail-booking"]
80
+ }
@@ -20,6 +20,14 @@ itpay services invoke <service_execution_id> --capability <capability_id>
20
20
 
21
21
  ## 有结果输出
22
22
 
23
+ 火车票目录:`items` 保留服务端返回的全部合格方案,不按推荐数量截断。
24
+ `result.catalog` 带搜索覆盖范围、模型来源和推荐引用;`recommended=true` 的方案排在首位。
25
+ 本地 Agent 应先解释首推及不同取舍,用户不满意时可继续查看同一次返回的完整列表。
26
+ 智能规划返回 `verified_phone_required` 时,先完成 ItPay 网页手机号验证,不在 CLI 收集手机号、身份证或姓名。
27
+ 该错误发生在供应商调用和额度预占之前。
28
+ 本地验证环境中 CLI 会返回 `verification_url`,用户在该网页验证手机号并确认连接 Agent。
29
+ 完成后重试同一条 invoke;不在终端输入或回传手机验证码。
30
+
23
31
  ```json
24
32
  {
25
33
  "status": "result_ready",
@@ -153,3 +161,5 @@ Provider 已收到请求时,Backend 返回同一 Execution 的权威调用和
153
161
  ## Agent Type / Host
154
162
 
155
163
  所有正式支持的 Local Agent Type 的 safe result 一致。instruction 可以适配对话表述,但不得隐藏 quota、价格或 schema 错误。
164
+
165
+ 本地模拟购票阶段:用户选定候选后才提交选择,并使用 `services quote --capability book_ticket` 获取实时票款加每张 2 元服务费的锁定报价。精确查询需在报价输入中指定候选里可售的 `seat_type`;智能方案沿用所选席别。`pricing_method=rail_fare_plus_fee` 不代表总价 2 元。乘客资料仅在 Checkout 网页填写。本地出票结果明确标记 `simulation=true`,不表示真实出票。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itpay/cli",
3
- "version": "2.1.4",
3
+ "version": "2.1.6",
4
4
  "description": "The ItPay CLI for services, orders, and human-authorized purchased content.",
5
5
  "type": "module",
6
6
  "bin": {