@itpay/cli 2.1.19 → 2.1.29
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.
- package/README.md +2 -2
- package/dist/src/commands/buy.js +3 -3
- package/dist/src/commands/cart.js +15 -15
- package/dist/src/commands/catalog.js +10 -7
- package/dist/src/commands/checkout.js +8 -11
- package/dist/src/commands/docs.js +3 -14
- package/dist/src/commands/guidance.js +41 -25
- package/dist/src/commands/install.js +2 -2
- package/dist/src/commands/order.js +4 -3
- package/dist/src/commands/orders.js +4 -9
- package/dist/src/commands/pay.js +1 -1
- package/dist/src/commands/rail_catalog.js +160 -11
- package/dist/src/commands/refund.js +5 -5
- package/dist/src/commands/services.js +658 -361
- package/dist/src/commands/skill.js +1 -1
- package/dist/src/commands/vault.js +27 -20
- package/dist/src/main.js +39 -24
- package/dist/src/render/markdown.js +0 -3
- package/dist/src/state/account_auth.js +9 -5
- package/dist/src/state/cart_session.js +4 -1
- package/dist/src/state/config.js +1 -1
- package/dist/src/state/task_journal.js +2 -0
- package/docs/agent/buyer/cart-checkout.json +5 -4
- package/docs/agent/buyer/catalog-list.json +24 -6
- package/docs/agent/buyer/identity-and-sessions.json +16 -15
- package/docs/agent/buyer/install-and-setup.json +5 -4
- package/docs/agent/buyer/orders-refunds.json +11 -5
- package/docs/agent/buyer/payment-flow.json +3 -3
- package/docs/agent/buyer/purchased-content.json +18 -4
- package/docs/agent/buyer/quickstart.json +22 -14
- package/docs/agent/buyer/rail-booking.json +125 -171
- package/docs/agent/buyer/rail-fast-checkout.json +21 -44
- package/docs/agent/buyer/render-hosts.json +9 -6
- package/docs/agent/buyer/service-feedback.json +2 -1
- package/docs/cli-reference/agent-types.md +1 -1
- package/docs/cli-reference/commands/auth.md +2 -0
- package/docs/cli-reference/commands/buy.md +1 -1
- package/docs/cli-reference/commands/cart/add.md +1 -1
- package/docs/cli-reference/commands/cart/clear.md +1 -1
- package/docs/cli-reference/commands/cart/index.md +1 -1
- package/docs/cli-reference/commands/cart/next.md +1 -1
- package/docs/cli-reference/commands/cart/remove.md +1 -1
- package/docs/cli-reference/commands/cart/show.md +1 -1
- package/docs/cli-reference/commands/catalog/index.md +1 -1
- package/docs/cli-reference/commands/catalog/list.md +1 -1
- package/docs/cli-reference/commands/checkout.md +1 -1
- package/docs/cli-reference/commands/device.md +8 -7
- package/docs/cli-reference/commands/docs/index.md +1 -1
- package/docs/cli-reference/commands/docs/list.md +1 -1
- package/docs/cli-reference/commands/docs/search.md +1 -1
- package/docs/cli-reference/commands/docs/show.md +1 -1
- package/docs/cli-reference/commands/install.md +2 -2
- package/docs/cli-reference/commands/next.md +1 -1
- package/docs/cli-reference/commands/order.md +1 -1
- package/docs/cli-reference/commands/pay.md +1 -1
- package/docs/cli-reference/commands/readyz.md +1 -1
- package/docs/cli-reference/commands/refund/cancel.md +1 -1
- package/docs/cli-reference/commands/refund/create.md +1 -1
- package/docs/cli-reference/commands/refund/get.md +2 -2
- package/docs/cli-reference/commands/refund/index.md +1 -1
- package/docs/cli-reference/commands/refund/list.md +1 -1
- package/docs/cli-reference/commands/refund/watch.md +1 -1
- package/docs/cli-reference/commands/services/action.md +3 -3
- package/docs/cli-reference/commands/services/checkout.md +1 -1
- package/docs/cli-reference/commands/services/events.md +1 -1
- package/docs/cli-reference/commands/services/get.md +1 -1
- package/docs/cli-reference/commands/services/index.md +1 -1
- package/docs/cli-reference/commands/services/invoke.md +5 -13
- package/docs/cli-reference/commands/services/list.md +1 -1
- package/docs/cli-reference/commands/services/next.md +12 -10
- package/docs/cli-reference/commands/services/page.md +9 -7
- package/docs/cli-reference/commands/services/quote.md +1 -1
- package/docs/cli-reference/commands/services/read-result.md +16 -9
- package/docs/cli-reference/commands/services/run.md +4 -2
- package/docs/cli-reference/commands/services/start.md +1 -1
- package/docs/cli-reference/conventions.md +2 -2
- package/package.json +7 -5
- package/skills/itpay/SKILL.md +77 -238
- package/docs/reviews/agent-ux-handoff-2026-09-23/IMPLEMENTATION-EVIDENCE.md +0 -174
- package/docs/skill-bundle-rollout/01-mcp-authentication.md +0 -134
- package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +0 -210
- package/docs/skill-bundle-rollout/03-platform-publishing.md +0 -161
- package/docs/skill-bundle-rollout/04-first-wave-platforms.md +0 -70
- package/docs/skill-bundle-rollout/05-sync-operations.md +0 -62
- package/docs/skill-bundle-rollout/README.md +0 -151
- /package/bin/{itp → itp.mjs} +0 -0
|
@@ -9,6 +9,7 @@ import { ensureIdeImageAttach } from "../render/ide.js";
|
|
|
9
9
|
import { buildCheckoutHandoff, shouldPrepareLocalCheckoutImage } from "./checkout_handoff.js";
|
|
10
10
|
import { localizeCardURL, normalizeCardLocale } from "../render/locale.js";
|
|
11
11
|
import { buildAgentChatHandoff } from "../render/markdown.js";
|
|
12
|
+
import { refundStateEnvelope } from "./refund.js";
|
|
12
13
|
import { decodeRailCatalogJourneys } from "./rail_catalog.js";
|
|
13
14
|
import { platformKeyForHost } from "../render/plan.js";
|
|
14
15
|
import { renderTerminalQR } from "../render/qr.js";
|
|
@@ -17,7 +18,7 @@ import { renderTerminalQR } from "../render/qr.js";
|
|
|
17
18
|
// The server alone decides service version, quota and provider credentials.
|
|
18
19
|
const RAIL_PROGRESSIVE_FEATURES = ["rail.progressive.v2"];
|
|
19
20
|
import { buildCheckoutQRPlan } from "./buy.js";
|
|
20
|
-
import { appendFeedbackPostmortemInstruction, CommandContractError, isTerminalServiceExecutionStatus, writeCommandEnvelope, } from "./guidance.js";
|
|
21
|
+
import { appendFeedbackPostmortemInstruction, CommandContractError, isTerminalServiceExecutionStatus, writeCommandEnvelope, shellArgument, plainValueLines, } from "./guidance.js";
|
|
21
22
|
// AUTH04: record the pause point of each Service Execution so a later
|
|
22
23
|
// `itpay auth login` can resume the SAME task. Ids/stage/resume command only —
|
|
23
24
|
// no chat content, PII or credentials. Best-effort: journaling never breaks
|
|
@@ -35,24 +36,24 @@ function journalTaskState(serviceExecutionID, envelope, env) {
|
|
|
35
36
|
const serviceActionStatuses = new Set(["pending", "approved", "rejected", "expired", "cancelled"]);
|
|
36
37
|
const RAIL_SERVICE_GUIDANCE = {
|
|
37
38
|
"itpay-rail-exact": {
|
|
38
|
-
when_to_use: "
|
|
39
|
+
when_to_use: "已知可信站对时先用本服务核验直达;城市需求可先形成有依据的站对假设。只覆盖这一站对,不代表全城市最优。首次查询前先留2–4行路线摘要,具体方法:itpay docs show rail-booking。",
|
|
39
40
|
input_fields: [
|
|
40
|
-
{ name: "origin", required: true, description: "
|
|
41
|
-
{ name: "destination", required: true, description: "
|
|
42
|
-
{ name: "travel_date", required: true, description: "出行日期 YYYY-MM-DD;字段名必须是 travel_date,'date' 等别名无效", example: "
|
|
41
|
+
{ name: "origin", required: true, description: "已核实出发铁路站完整名称,不能以同名站代替区域", example: "<已核实出发站>" },
|
|
42
|
+
{ name: "destination", required: true, description: "已核实到达铁路站完整名称", example: "<已核实到达站>" },
|
|
43
|
+
{ name: "travel_date", required: true, description: "出行日期 YYYY-MM-DD;字段名必须是 travel_date,'date' 等别名无效", example: "<用户日期 YYYY-MM-DD>" },
|
|
43
44
|
],
|
|
44
|
-
input_example: { origin: "
|
|
45
|
+
input_example: { origin: "<已核实出发站名>", destination: "<已核实到达站名>", travel_date: "<用户出行日期 YYYY-MM-DD>" },
|
|
45
46
|
notes: [
|
|
46
47
|
"本服务只接受这三个字段,多传字段会被供应商拒绝",
|
|
47
|
-
"
|
|
48
|
+
"查询为空只说明这一已解析站对;允许中转可用Smart保持双站;改变用户指定站须先取得同意",
|
|
48
49
|
],
|
|
49
50
|
},
|
|
50
51
|
"itpay-rail-smart": {
|
|
51
|
-
when_to_use: "
|
|
52
|
+
when_to_use: "需要广泛站点比较、中转或具体地址接驳时使用;有可信站对且只需先核验直达可用 Exact。首次查询前先留2–4行路线摘要,具体方法:itpay docs show rail-booking。",
|
|
52
53
|
input_fields: [
|
|
53
|
-
{ name: "origin", required: true, description: "出发地:已知最完整的位置名或区域(城市/区县/地址均可)", example: "
|
|
54
|
-
{ name: "destination", required: true, description: "目的地:与 origin 同样的位置规则", example: "
|
|
55
|
-
{ name: "travel_date", required: true, description: "出行日期 YYYY-MM-DD;字段名必须是 travel_date", example: "
|
|
54
|
+
{ name: "origin", required: true, description: "出发地:已知最完整的位置名或区域(城市/区县/地址均可)", example: "<原始完整地点>" },
|
|
55
|
+
{ name: "destination", required: true, description: "目的地:与 origin 同样的位置规则", example: "<已核实到达站>" },
|
|
56
|
+
{ name: "travel_date", required: true, description: "出行日期 YYYY-MM-DD;字段名必须是 travel_date", example: "<用户日期 YYYY-MM-DD>" },
|
|
56
57
|
],
|
|
57
58
|
optional_fields: [
|
|
58
59
|
{ name: "origin_city", description: "出发地城市提示,辅助位置解析" },
|
|
@@ -60,48 +61,51 @@ const RAIL_SERVICE_GUIDANCE = {
|
|
|
60
61
|
{ name: "origin_location", description: "已知坐标对象(高德 GCJ-02)" },
|
|
61
62
|
{ name: "destination_location", description: "目的地坐标对象" },
|
|
62
63
|
{ name: "depart_after", description: "不早于该时间出发" },
|
|
64
|
+
{ name: "depart_before", description: "不晚于该时间出发" },
|
|
63
65
|
{ name: "arrive_before", description: "不晚于该时间到达" },
|
|
66
|
+
{ name: "user_query", description: "旅行需求原话,最多4096 UTF-8字节;时间等明确条件仍填写对应结构化字段" },
|
|
67
|
+
{ name: "seat_preferences", description: "已确认的席别代码数组,不填写旅客身份" },
|
|
64
68
|
{ name: "priority", enum: "balanced|fastest|cheapest|safest|flexible", description: "方案偏好" },
|
|
69
|
+
{ name: "duration_basis", enum: "rail|door_to_door", description: "最快默认按铁路全程含换乘;明确要求且接驳证据完整时可按门到门" },
|
|
65
70
|
{ name: "max_transfers", description: "允许中转次数,0-2" },
|
|
71
|
+
{ name: "passengers", description: "成人乘客整数 1-5;不填写身份资料" },
|
|
66
72
|
],
|
|
67
|
-
input_example: { origin: "
|
|
73
|
+
input_example: { origin: "<用户出发地点>", destination: "<用户目的地>", travel_date: "<用户出行日期 YYYY-MM-DD>", priority: "fastest", passengers: 1 },
|
|
68
74
|
notes: [
|
|
69
75
|
"位置有歧义时会进入位置确认步骤,请用户选定后继续同一执行",
|
|
70
76
|
"推荐结果含每趟车的席别与余票;购票在后续受保护流程完成",
|
|
71
77
|
],
|
|
72
78
|
},
|
|
73
79
|
};
|
|
74
|
-
function railServiceGuidance(serviceID) {
|
|
75
|
-
|
|
80
|
+
function railServiceGuidance(serviceID, schema) {
|
|
81
|
+
const guidance = RAIL_SERVICE_GUIDANCE[serviceID];
|
|
82
|
+
if (serviceID !== "itpay-rail-smart" || !guidance || !schema?.properties?.endpoints)
|
|
83
|
+
return guidance;
|
|
84
|
+
return {
|
|
85
|
+
...guidance,
|
|
86
|
+
input_fields: [
|
|
87
|
+
{ name: "endpoints", required: true, description: "origin/destination 各填 kind(area/place/station) 与完整 text;不确定 kind 可省略" },
|
|
88
|
+
{ name: "travel_date", required: true, description: "用户出行日期 YYYY-MM-DD" },
|
|
89
|
+
],
|
|
90
|
+
optional_fields: (guidance.optional_fields ?? []).filter((field) => !["origin_city", "destination_city", "origin_location", "destination_location"].includes(field.name)).concat([
|
|
91
|
+
{ name: "reuse_from", description: "同 owner 的已保存 Exact result_item_id 或 Smart snapshot_id;端点/日期新查询可复用有效事实" },
|
|
92
|
+
]),
|
|
93
|
+
input_example: { endpoints: { origin: { kind: "area", text: "<用户出发市县镇完整名称>" }, destination: { kind: "station", text: "<已核实到达站名>" } }, travel_date: "<用户出行日期 YYYY-MM-DD>" },
|
|
94
|
+
notes: ["仅用当前 input_schema 支持的端点写法;不要与旧 origin/destination 混用", "唯一地点直接解析;真实歧义只询问有缺口的一端,外部故障按当前执行恢复"],
|
|
95
|
+
};
|
|
76
96
|
}
|
|
77
97
|
const WORKFLOW_STEP_GUIDANCE = {
|
|
78
|
-
input: { meaning: "输入校验", hint: "
|
|
79
|
-
quota: { meaning: "额度检查", hint: "
|
|
80
|
-
geo: { meaning: "位置解析", hint: "
|
|
81
|
-
geo_confirm: { meaning: "确认后位置解析", hint: "
|
|
82
|
-
resolved: { meaning: "位置解析复核", hint: "
|
|
83
|
-
resolved_after_confirm: { meaning: "位置确认复核", hint: "
|
|
84
|
-
search: { meaning: "供应商车次检索", hint: "
|
|
85
|
-
catalog: { meaning: "可行车次计算", hint: "
|
|
86
|
-
recommend: { meaning: "方案推荐", hint: "
|
|
87
|
-
delivery: { meaning: "交付", hint: "
|
|
98
|
+
input: { meaning: "输入校验", hint: "核对本服务的输入契约和服务端错误码;本执行的输入不能凭猜测修复" },
|
|
99
|
+
quota: { meaning: "额度检查", hint: "查看实际额度及授权状态;只按返回的登录或等待动作处理" },
|
|
100
|
+
geo: { meaning: "位置解析", hint: "查看实际地点解析错误;有歧义时让用户选择返回的候选" },
|
|
101
|
+
geo_confirm: { meaning: "确认后位置解析", hint: "核对服务端记录的候选与用户实际选择" },
|
|
102
|
+
resolved: { meaning: "位置解析复核", hint: "查看未满足的实际位置条件" },
|
|
103
|
+
resolved_after_confirm: { meaning: "位置确认复核", hint: "核对已确认地点与服务端复核结果" },
|
|
104
|
+
search: { meaning: "供应商车次检索", hint: "查看供应商返回的错误码与当前站对;不要推断是字段或站名错误" },
|
|
105
|
+
catalog: { meaning: "可行车次计算", hint: "查看已保存结果和实际过滤原因;不要无原因改日期或站点" },
|
|
106
|
+
recommend: { meaning: "方案推荐", hint: "查看已保存候选和实际推荐失败原因;不要无原因放宽条件" },
|
|
107
|
+
delivery: { meaning: "交付", hint: "读取当前执行状态;已有订单或付款状态不明时先核实,不重建" },
|
|
88
108
|
};
|
|
89
|
-
function failedWorkflowStep(steps, errorCode) {
|
|
90
|
-
if (!steps)
|
|
91
|
-
return undefined;
|
|
92
|
-
for (const [step, status] of Object.entries(steps)) {
|
|
93
|
-
if (status === "failure" && step !== "failure")
|
|
94
|
-
return step;
|
|
95
|
-
}
|
|
96
|
-
// Only a backend-recorded condition_unmet proves a false branch routed to failure.
|
|
97
|
-
if (errorCode === "condition_unmet") {
|
|
98
|
-
for (const [step, status] of Object.entries(steps)) {
|
|
99
|
-
if (status === "false" && step !== "failure")
|
|
100
|
-
return step;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
109
|
export async function runServicesStart(backend, serviceID, options = {}) {
|
|
106
110
|
const host = options.host ?? "terminal";
|
|
107
111
|
const response = await backend.startServiceExecution({
|
|
@@ -114,11 +118,11 @@ export async function runServicesStart(backend, serviceID, options = {}) {
|
|
|
114
118
|
},
|
|
115
119
|
});
|
|
116
120
|
if (response.workflow_entry) {
|
|
117
|
-
const guidance = railServiceGuidance(serviceID);
|
|
121
|
+
const guidance = railServiceGuidance(serviceID, response.workflow_entry.input_schema);
|
|
118
122
|
writeCommandEnvelope({
|
|
119
123
|
status: "input_required",
|
|
120
124
|
result: { service_execution_id: response.execution.service_execution_id, service_id: serviceID, input_schema: response.workflow_entry.input_schema, ...(guidance ? { guidance } : {}) },
|
|
121
|
-
instruction: guidance ? "按 result.guidance 逐项填写输入(when_to_use 说明本服务适用场景、input_fields 是必填契约、input_example
|
|
125
|
+
instruction: guidance ? "按 result.guidance 逐项填写输入(when_to_use 说明本服务适用场景、input_fields 是必填契约、input_example 是待填示意,必须替换用户真实值),然后继续同一服务执行。不要臆造字段名。" : "根据服务声明填写输入,然后继续同一服务执行。",
|
|
122
126
|
next: null,
|
|
123
127
|
interaction: {
|
|
124
128
|
schema_version: "itpay.interaction.v1",
|
|
@@ -191,18 +195,29 @@ function locationConfirmationEnvelope(executionID, capabilityID, input, confirma
|
|
|
191
195
|
input = (confirmation.input ?? input);
|
|
192
196
|
const endpoints = (confirmation.endpoints ?? []);
|
|
193
197
|
const choices = Object.fromEntries(endpoints.map((endpoint) => [endpoint.side, "<用户选择的候选 id>"]));
|
|
198
|
+
const missing = endpoints.filter((endpoint) => !endpoint.candidates?.length);
|
|
199
|
+
const reason = missing[0]?.reason?.toUpperCase();
|
|
200
|
+
const instruction = missing.length
|
|
201
|
+
? reason === "STATION_LOCATION_EVIDENCE_REQUIRED"
|
|
202
|
+
? "尚未查票。车站身份已知,但 Smart 缺少该站地理坐标证据;请说明此系统缺口。若用户仅要双站直达,可按原站名使用 Exact;不要要求用户换站。"
|
|
203
|
+
: reason === "ADDRESS_REQUIRED" || reason === "ADMINISTRATIVE_AREA_UNRESOLVED"
|
|
204
|
+
? "尚未查票。请只向用户询问该端点缺少的具体地址或行政归属;保留另一端和已有条件,得到信息后发起关联新查询。"
|
|
205
|
+
: reason === "COORDINATE_IDENTITY_UNVERIFIED" || reason === "COORDINATE_NAME_MISMATCH"
|
|
206
|
+
? "尚未查票。提交的地点名称和坐标尚未得到一致证据;请核实该端点的真实名称或位置,保留另一端,不要把坐标声明当成人工确认。"
|
|
207
|
+
: "尚未查票。地点证据不足;请说明返回的具体原因与受影响端点,不要把外部故障当作用户地点歧义。"
|
|
208
|
+
: "尚未查票。仅向用户确认有真实歧义的端点,展示候选名称、地址和高德链接。用户选择后保留原输入,回到同一服务。";
|
|
194
209
|
return {
|
|
195
210
|
status: "location_confirmation_required",
|
|
196
211
|
result: { service_execution_id: executionID, query: input, location_confirmation: confirmation },
|
|
197
|
-
instruction
|
|
212
|
+
instruction,
|
|
198
213
|
next: null,
|
|
199
|
-
...(confirmation.can_resume ? {
|
|
214
|
+
...(confirmation.can_resume && !missing.length && endpoints.length ? {
|
|
200
215
|
interaction: {
|
|
201
216
|
schema_version: "itpay.interaction.v1",
|
|
202
217
|
stage: "location_confirmation_required",
|
|
203
218
|
input_template: {
|
|
204
|
-
command: `itpay services invoke ${executionID} --capability ${capabilityID}
|
|
205
|
-
|
|
219
|
+
command: `itpay services invoke ${executionID} --capability ${capabilityID} --input-json <地点确认.json> --json`,
|
|
220
|
+
input: { ...input, location_confirmation: { plan_id: confirmation.plan_id, token: confirmation.token, choices } },
|
|
206
221
|
required_input: ["choices"],
|
|
207
222
|
executable: false,
|
|
208
223
|
},
|
|
@@ -217,6 +232,8 @@ function requiredInputFields(schema) {
|
|
|
217
232
|
}
|
|
218
233
|
function locationInputInstruction(schema) {
|
|
219
234
|
const properties = schema?.properties;
|
|
235
|
+
if (properties?.endpoints)
|
|
236
|
+
return " 两端分别填写 endpoints.origin/destination 的 kind 与完整 text;不确定 kind 可省略。区域保留范围、精确车站仅在目录验证后锁定。不要混用旧 origin/destination、手填锁站或伪造坐标。唯一地点会自动解析;只有真实多候选才让用户选返回的候选 id,地图失败应按当前执行恢复。";
|
|
220
237
|
if (!properties?.origin_location || !properties?.destination_location)
|
|
221
238
|
return "";
|
|
222
239
|
if (!properties.location_confirmation)
|
|
@@ -234,13 +251,9 @@ export async function runServicesInvoke(backend, config, serviceExecutionID, cap
|
|
|
234
251
|
}
|
|
235
252
|
const missingInput = missingRequiredInput(requestedCapability.input_schema, input);
|
|
236
253
|
if (missingInput.length > 0) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐 required_input 后重试同一个 execution;本次没有调用 Provider。", [{
|
|
241
|
-
command: `itpay services invoke ${serviceExecutionID} --capability ${capabilityID}${formatInputOptions(correctedInput)} --json`,
|
|
242
|
-
reason: "提交完整 capability 输入",
|
|
243
|
-
}]);
|
|
254
|
+
throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "只补缺失字段到小JSON文件,再提交同一执行;本次尚未调用供应商。", [], { schema_version: "itpay.interaction.v1", stage: "input_required",
|
|
255
|
+
input_template: { command: `itpay services invoke ${serviceExecutionID} --capability ${capabilityID} --input-json <file> --json`,
|
|
256
|
+
required_input: ["file", ...missingInput], executable: false } });
|
|
244
257
|
}
|
|
245
258
|
const idempotencyKey = await operationID(config, `service.invoke:${serviceExecutionID}:${capabilityID}:${stableInput(input)}`);
|
|
246
259
|
let response;
|
|
@@ -293,9 +306,16 @@ function invokedEnvelope(response, requestedCapability, capabilities, input) {
|
|
|
293
306
|
...(quota ? { quota } : {}),
|
|
294
307
|
};
|
|
295
308
|
const preview = response.invocation?.safe_result_preview;
|
|
309
|
+
const notices = Array.isArray(preview?.notices) ? preview.notices.map((notice) => {
|
|
310
|
+
if (!notice || typeof notice !== "object" || !("code" in notice) || !("message" in notice) || typeof notice.message !== "string")
|
|
311
|
+
return notice;
|
|
312
|
+
if (!["RAIL_TRANSFER_SCOPE_LIMIT", "RAIL_TRANSFER_SEARCH_INCOMPLETE"].includes(String(notice.code)))
|
|
313
|
+
return notice;
|
|
314
|
+
return { ...notice, message: notice.message.replace(/可按主要枢纽分段查询[。;]?/g, "") };
|
|
315
|
+
}) : preview?.notices;
|
|
296
316
|
if (!response.effective_quota?.exhausted && preview?.search_status === "LOCATION_CONFIRMATION_REQUIRED" && preview.location_confirmation) {
|
|
297
317
|
return { value: locationConfirmationEnvelope(response.execution.service_execution_id, requestedCapability.capability_id, input, preview.location_confirmation),
|
|
298
|
-
plainResult:
|
|
318
|
+
plainResult: plainValueLines(preview.location_confirmation) };
|
|
299
319
|
}
|
|
300
320
|
if (typeof preview?.catalog_total === "number") {
|
|
301
321
|
baseResult.catalog = {
|
|
@@ -303,7 +323,7 @@ function invokedEnvelope(response, requestedCapability, capabilities, input) {
|
|
|
303
323
|
recommendation: preview.recommendation,
|
|
304
324
|
decision_source: preview.decision_source,
|
|
305
325
|
coverage: preview.coverage,
|
|
306
|
-
notices
|
|
326
|
+
notices,
|
|
307
327
|
search_status: preview.search_status,
|
|
308
328
|
searched_scope: preview.searched_scope,
|
|
309
329
|
effective_policy_hash: preview.effective_policy_hash,
|
|
@@ -320,15 +340,15 @@ function invokedEnvelope(response, requestedCapability, capabilities, input) {
|
|
|
320
340
|
let instruction = items.length > 0
|
|
321
341
|
? "用编号、名称和可公开字段向用户说明候选;若候选列表已满足目标就停止。只有用户明确选择并希望继续时,才提交对应编号;不要向用户提及 safe_payload、Execution 或内部 ID。"
|
|
322
342
|
: `没有找到与“${queryText(input)}”匹配的结果。向用户展示本次为 0 个结果并停止。不要修改、缩短或猜测其他输入;只有用户明确提供新输入后,才能启动新的查询。`;
|
|
323
|
-
if (items.length === 0 && Array.isArray(
|
|
324
|
-
const railNotice =
|
|
343
|
+
if (items.length === 0 && Array.isArray(notices)) {
|
|
344
|
+
const railNotice = notices.find((notice) => {
|
|
325
345
|
if (!notice || typeof notice !== "object")
|
|
326
346
|
return false;
|
|
327
347
|
const value = notice;
|
|
328
348
|
return ["RAIL_TRANSFER_SCOPE_LIMIT", "RAIL_TRANSFER_SEARCH_INCOMPLETE"].includes(String(value.code)) && typeof value.message === "string";
|
|
329
349
|
});
|
|
330
350
|
if (railNotice)
|
|
331
|
-
instruction = `向用户展示官方提示:${railNotice.message}
|
|
351
|
+
instruction = `向用户展示官方提示:${railNotice.message} 说明本次已查范围和未完成原因,按服务端提供的当前动作恢复;不能据此断言没有车,不默认扩大范围或让用户另选分段站点。`;
|
|
332
352
|
}
|
|
333
353
|
let next = null;
|
|
334
354
|
if (items.length > 0 && baseResult.catalog) {
|
|
@@ -374,13 +394,6 @@ function invokedEnvelope(response, requestedCapability, capabilities, input) {
|
|
|
374
394
|
? {
|
|
375
395
|
schema_version: "itpay.interaction.v1",
|
|
376
396
|
stage: "query_results_ready",
|
|
377
|
-
by_goal: {
|
|
378
|
-
compare: { action: "present_options", max_options: 3, stop_after_presentation: true },
|
|
379
|
-
prepare_checkout: {
|
|
380
|
-
action: "select_under_user_rules_then_prepare",
|
|
381
|
-
requires: ["current_delegation", "eligible_current_selection", "owner_permits_next_step"],
|
|
382
|
-
},
|
|
383
|
-
},
|
|
384
397
|
input_template: {
|
|
385
398
|
command: `itpay services action ${response.execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
|
|
386
399
|
required_input: ["rank"],
|
|
@@ -396,40 +409,7 @@ function invokedEnvelope(response, requestedCapability, capabilities, input) {
|
|
|
396
409
|
};
|
|
397
410
|
}
|
|
398
411
|
function serviceResultPlainLines(result) {
|
|
399
|
-
|
|
400
|
-
`service_execution_id: ${String(result.service_execution_id)}`,
|
|
401
|
-
`capability_id: ${String(result.capability_id)}`,
|
|
402
|
-
];
|
|
403
|
-
const items = result.items;
|
|
404
|
-
const query = result.query;
|
|
405
|
-
if (query) {
|
|
406
|
-
for (const [key, value] of Object.entries(query))
|
|
407
|
-
lines.push(`${key}: ${String(value)}`);
|
|
408
|
-
}
|
|
409
|
-
if (items.length === 0)
|
|
410
|
-
lines.push("results: 0");
|
|
411
|
-
const catalog = result.catalog;
|
|
412
|
-
if (catalog?.resolved_locations)
|
|
413
|
-
lines.push(`resolved_locations: ${JSON.stringify(catalog.resolved_locations)}`);
|
|
414
|
-
if (catalog?.journey_summary) {
|
|
415
|
-
for (const key of ["journey_summary", "train_services", "journeys"]) {
|
|
416
|
-
lines.push(`${key}: ${JSON.stringify(catalog[key])}`);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
if (result.quota)
|
|
420
|
-
lines.push(`quota: ${JSON.stringify(result.quota)}`);
|
|
421
|
-
if (result.checkout)
|
|
422
|
-
lines.push(`checkout: ${JSON.stringify(result.checkout)}`);
|
|
423
|
-
if (items.length > 0) {
|
|
424
|
-
lines.push("items:");
|
|
425
|
-
for (const item of items) {
|
|
426
|
-
lines.push(` ${item.rank}. ${item.title}`);
|
|
427
|
-
for (const [key, value] of Object.entries(item.safe_payload)) {
|
|
428
|
-
lines.push(` ${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
return lines;
|
|
412
|
+
return plainValueLines(result);
|
|
433
413
|
}
|
|
434
414
|
function queryText(input) {
|
|
435
415
|
const value = Object.values(input).find((item) => typeof item === "string" && item.trim() !== "");
|
|
@@ -523,11 +503,6 @@ function formatInputOptions(input) {
|
|
|
523
503
|
: ` --input ${shellArgument(`${key}=${typeof value === "object" && value !== null ? JSON.stringify(value) : String(value)}`)}`)
|
|
524
504
|
.join("");
|
|
525
505
|
}
|
|
526
|
-
function shellArgument(value) {
|
|
527
|
-
if (/^[\p{L}\p{N}._:=/-]+$/u.test(value))
|
|
528
|
-
return value;
|
|
529
|
-
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
530
|
-
}
|
|
531
506
|
export async function runServicesAction(backend, serviceExecutionID, actionType, input, options = {}) {
|
|
532
507
|
const selection = await resolveCandidateSelection(backend, serviceExecutionID, actionType, options);
|
|
533
508
|
const request = {
|
|
@@ -673,13 +648,10 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
673
648
|
const lockedInput = options.lockedInput ?? {};
|
|
674
649
|
const missingInput = missingRequiredInput(capability.input_schema, lockedInput);
|
|
675
650
|
if (missingInput.length > 0 && !readModel.workflow_entry && readModel.execution.next_action !== "create_checkout") {
|
|
676
|
-
throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐付费 capability 的 required_input;本次没有创建 quote、Checkout 或订单。", [{ command: checkoutCommand(serviceExecutionID, capability, lockedInput),
|
|
651
|
+
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 } });
|
|
677
652
|
}
|
|
678
653
|
if (capability.delivery_email_required && readModel.workflow_entry?.capability_id !== capability.capability_id && String(deliveryContact.email ?? "").trim() === "") {
|
|
679
|
-
throw new CommandContractError("delivery_email_required", "delivery email is required before creating this service checkout", "该 capability 的交付链接会发送到用户邮箱;先向用户说明用途并询问邮箱,不要代填。", [{
|
|
680
|
-
command: `itpay services checkout ${serviceExecutionID} --capability ${capability.capability_id}${formatInputOptions(lockedInput)} --email <email> --json`,
|
|
681
|
-
reason: "使用用户提供的邮箱创建 Checkout",
|
|
682
|
-
}]);
|
|
654
|
+
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 } });
|
|
683
655
|
}
|
|
684
656
|
}
|
|
685
657
|
const response = await backend.createServiceExecutionCheckout(serviceExecutionID, {
|
|
@@ -695,6 +667,33 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
695
667
|
const checkoutURL = tokenizedCheckoutURL(checkout.checkout_url, displayToken, checkout.qr_payload);
|
|
696
668
|
const cardURL = localizeCardURL(absolutePublicURL(config.baseURL, checkout.card_url ?? fallbackCardURL(config.baseURL, checkoutID, displayToken)), locale);
|
|
697
669
|
const cardPNGURL = localizeCardURL(absolutePublicURL(config.baseURL, checkout.card_png_url ?? checkout.qr_png_url ?? fallbackCardPNGURL(config.baseURL, checkoutID, displayToken)), locale);
|
|
670
|
+
let itineraryTitle;
|
|
671
|
+
let itinerary;
|
|
672
|
+
try {
|
|
673
|
+
const booking = await backend.getServiceExecution(serviceExecutionID);
|
|
674
|
+
if (booking.execution.service_id === "itpay-rail-booking") {
|
|
675
|
+
const leg = booking.rail_booking?.legs?.[0];
|
|
676
|
+
const review = booking.workflow?.human_action?.context?.review;
|
|
677
|
+
const reviewLeg = Array.isArray(review?.legs) ? review.legs[0] : undefined;
|
|
678
|
+
const facts = leg ?? reviewLeg;
|
|
679
|
+
if (facts) {
|
|
680
|
+
const safe = facts;
|
|
681
|
+
itinerary = { travel_date: safe.travel_date, train_code: safe.train_code,
|
|
682
|
+
from_station: safe.from ?? safe.from_station, to_station: safe.to ?? safe.to_station,
|
|
683
|
+
departure: safe.departure, arrival: safe.arrival,
|
|
684
|
+
seat_name: safe.seat_name ?? safe.seat_type_name,
|
|
685
|
+
passengers: review?.passengers ?? safe.passengers ?? null };
|
|
686
|
+
itineraryTitle = [facts.travel_date, facts.train_code,
|
|
687
|
+
facts.from ?? facts.from_station,
|
|
688
|
+
"→", facts.to ?? facts.to_station,
|
|
689
|
+
facts.seat_name ?? facts.seat_type_name]
|
|
690
|
+
.filter((value) => typeof value === "string" && value.length > 0).join(" ");
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
catch {
|
|
695
|
+
// Checkout already exists; a display-only title lookup cannot hide its official handoff.
|
|
696
|
+
}
|
|
698
697
|
const plan = buildCheckoutQRPlan({
|
|
699
698
|
host,
|
|
700
699
|
checkoutID,
|
|
@@ -705,7 +704,7 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
705
704
|
qrPNGURL: cardPNGURL,
|
|
706
705
|
nextAction: checkout.checkout.next_action,
|
|
707
706
|
orderItems: response.cart.items.map((item) => ({
|
|
708
|
-
title: item.title,
|
|
707
|
+
title: itineraryTitle || item.title,
|
|
709
708
|
quantity: item.quantity,
|
|
710
709
|
amountMinor: item.amount_minor,
|
|
711
710
|
currency: item.currency,
|
|
@@ -742,13 +741,9 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
742
741
|
});
|
|
743
742
|
}
|
|
744
743
|
const envelope = buildServicesCheckoutEnvelope(response, checkoutURL, plan, options.agentType, options.target);
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
`capability_id: ${checkoutCapabilityID(response, capabilityID)}`,
|
|
749
|
-
`locked_input: ${JSON.stringify(response.locked_input)}`,
|
|
750
|
-
`amount: ${formatMoney(checkout.checkout.amount_minor, checkout.checkout.currency)}`,
|
|
751
|
-
];
|
|
744
|
+
if (itinerary)
|
|
745
|
+
envelope.result = { ...envelope.result, itinerary };
|
|
746
|
+
const plainResult = plainValueLines(envelope.result);
|
|
752
747
|
if (!options.jsonOutput && platform === "terminal") {
|
|
753
748
|
plainResult.push("qr:", await renderTerminalQR(checkoutURL, options.qrFormat ?? "terminal"));
|
|
754
749
|
}
|
|
@@ -768,14 +763,14 @@ export async function runServicesQuote(backend, serviceExecutionID, capabilityID
|
|
|
768
763
|
model.allowed_actions?.some((action) => action.type === "prepare_quote" && action.capability_id === capabilityID);
|
|
769
764
|
const missingInput = missingRequiredInput(capability.input_schema, input);
|
|
770
765
|
if (missingInput.length > 0 && !selectionBacked && model.workflow_entry?.capability_id !== capability.capability_id) {
|
|
771
|
-
throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐付费 capability 输入;本次没有创建 Quote、Cart 或 Checkout。", [{ command: quoteCommand(serviceExecutionID, capability, input),
|
|
766
|
+
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 } });
|
|
772
767
|
}
|
|
773
768
|
const deliveryContact = {
|
|
774
769
|
...(options.deliveryContact ?? {}),
|
|
775
770
|
...(options.email ? { email: options.email } : {}),
|
|
776
771
|
};
|
|
777
772
|
if (capability.delivery_email_required && model.workflow_entry?.capability_id !== capability.capability_id && String(deliveryContact.email ?? "").trim() === "") {
|
|
778
|
-
throw new CommandContractError("delivery_email_required", "delivery email is required before preparing this service quote", "交付链接会发送到用户邮箱;说明用途并询问邮箱,不要代填。", [{ command: quoteCommand(serviceExecutionID, capability, input),
|
|
773
|
+
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 } });
|
|
779
774
|
}
|
|
780
775
|
const quote = await backend.prepareServiceQuote(serviceExecutionID, {
|
|
781
776
|
capability_id: capabilityID,
|
|
@@ -840,27 +835,44 @@ export async function runServicesGet(backend, serviceExecutionID, options = {})
|
|
|
840
835
|
status: nextState.status,
|
|
841
836
|
result,
|
|
842
837
|
instruction: nextState.instruction,
|
|
843
|
-
next: nextState.next
|
|
844
|
-
|
|
838
|
+
next: nextState.next,
|
|
839
|
+
...(nextState.interaction ? { interaction: nextState.interaction } : {}),
|
|
840
|
+
...(nextState.communication ? { communication: nextState.communication } : {}),
|
|
841
|
+
...(nextState.handoff ? { handoff: nextState.handoff } : {}),
|
|
842
|
+
recovery: nextState.recovery.filter(action => action.command !== `itpay services get ${serviceExecutionID} --json`),
|
|
845
843
|
};
|
|
846
844
|
writeCommandEnvelope(envelope, {
|
|
847
845
|
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
848
846
|
...(options.output ? { output: options.output } : {}),
|
|
849
|
-
plainResult:
|
|
850
|
-
`service_execution_id: ${execution.service_execution_id}`,
|
|
851
|
-
`service_id: ${execution.service_id}`,
|
|
852
|
-
`state: ${execution.status}/${execution.phase}`,
|
|
853
|
-
...(execution.current_capability_id ? [`current_capability_id: ${execution.current_capability_id}`] : []),
|
|
854
|
-
...timeline.map((event) => `${event.sequence}. ${event.step} ${event.status}/${event.phase} ${event.occurred_at}`),
|
|
855
|
-
],
|
|
847
|
+
plainResult: plainValueLines(envelope.result),
|
|
856
848
|
});
|
|
857
849
|
}
|
|
850
|
+
function shouldWaitForServiceResult(model) {
|
|
851
|
+
if (model.current_delivery || model.rail_booking || isTerminalServiceExecutionStatus(model.execution.status))
|
|
852
|
+
return false;
|
|
853
|
+
if (model.rail_planning) {
|
|
854
|
+
const plan = model.rail_planning;
|
|
855
|
+
return !plan.recommendation && !(plan.alternatives?.length) &&
|
|
856
|
+
!["complete", "paused", "failed", "cancelled", "expired"].includes(plan.search?.expansion_status ?? "") &&
|
|
857
|
+
!plan.available_actions?.some((action) => ["select_journey", "confirm_location", "expand_search"].includes(action.type));
|
|
858
|
+
}
|
|
859
|
+
return ["queued", "running", "delivery"].includes(model.workflow?.status ?? "");
|
|
860
|
+
}
|
|
858
861
|
export async function runServicesNext(backend, serviceExecutionID, options = {}) {
|
|
859
|
-
const
|
|
862
|
+
const started = Date.now();
|
|
863
|
+
const until = started + (options.timeoutSeconds ?? 0) * 1000;
|
|
864
|
+
const sleep = options.sleep ?? ((milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds)));
|
|
865
|
+
let response = await backend.getServiceExecution(serviceExecutionID, options.sinceSnapshot ? { sinceSnapshot: options.sinceSnapshot } : {});
|
|
866
|
+
while (shouldWaitForServiceResult(response) && Date.now() < until) {
|
|
867
|
+
await sleep(options.pollIntervalMS ?? Math.max(1500, response.rail_planning?.search?.poll_after_ms ?? 0));
|
|
868
|
+
response = await backend.getServiceExecution(serviceExecutionID, options.sinceSnapshot ? { sinceSnapshot: options.sinceSnapshot } : {});
|
|
869
|
+
}
|
|
860
870
|
const envelope = servicesNextEnvelope(response);
|
|
871
|
+
if (shouldWaitForServiceResult(response) && options.timeoutSeconds) {
|
|
872
|
+
envelope.result = { ...envelope.result, waited_seconds: Math.round((Date.now() - started) / 1000), wait_timed_out: true };
|
|
873
|
+
envelope.next = { command: `itpay services next ${serviceExecutionID} --timeout 120 --json`, reason: "稍后继续等待同一任务;不重新提交查询" };
|
|
874
|
+
}
|
|
861
875
|
journalTaskState(serviceExecutionID, envelope, options.env ?? process.env);
|
|
862
|
-
if (response.rail_booking)
|
|
863
|
-
envelope.result = { ...envelope.result, rail_booking: response.rail_booking };
|
|
864
876
|
writeCommandEnvelope(envelope, {
|
|
865
877
|
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
866
878
|
...(options.output ? { output: options.output } : {}),
|
|
@@ -879,6 +891,33 @@ export async function runServicesPage(backend, serviceExecutionID, resultItemID,
|
|
|
879
891
|
const response = await backend.getServiceExecutionResultItemPage(serviceExecutionID, resultItemID, offset, limit);
|
|
880
892
|
const page = response.page;
|
|
881
893
|
const container = (page.result ?? page);
|
|
894
|
+
const exactCandidate = limit === 1 && Array.isArray(container.candidates) && container.candidates.length === 1
|
|
895
|
+
? container.candidates[0] : undefined;
|
|
896
|
+
if (exactCandidate && typeof exactCandidate.train_code === "string" && Array.isArray(exactCandidate.seats)) {
|
|
897
|
+
const offer = exactCandidate.booking_offer;
|
|
898
|
+
const { booking_offer: _bookingOffer, ...candidateFacts } = exactCandidate;
|
|
899
|
+
const seats = exactCandidate.seats.filter((seat) => seat.purchase_supported === true);
|
|
900
|
+
writeCommandEnvelope({
|
|
901
|
+
status: "exact_train_detail",
|
|
902
|
+
result: {
|
|
903
|
+
service_execution_id: serviceExecutionID, result_item_id: resultItemID, result_offset: offset,
|
|
904
|
+
candidate: candidateFacts,
|
|
905
|
+
...(offer?.service_id && offer.selection_token ? { booking_template: {
|
|
906
|
+
command: `itpay services run ${offer.service_id} --input-json <file> --json`,
|
|
907
|
+
executable: false, required_input: ["file", "passengers", "seat_type"],
|
|
908
|
+
seat_choices: seats.map((choice) => ({ seat_type: choice.seat_type, seat_name: choice.seat_name })),
|
|
909
|
+
input_example: { selection: { token: offer.selection_token, seat_type: "<用户授权席别对应代码>" }, passengers: "<真实人数>" },
|
|
910
|
+
} } : {}),
|
|
911
|
+
},
|
|
912
|
+
instruction: offer?.selection_token
|
|
913
|
+
? "这是已保存车次的当前详情;只按 purchase_supported 席别代码、库存与报价状态选择。购票模板的人数须按用户真实需求填写;查询价需在后续报价确认。若用户仅要求比较,答复后停止。"
|
|
914
|
+
: "这条车次当前没有可用的购买凭据;说明席别与余票事实,不要猜测代码或构造订单。",
|
|
915
|
+
next: null, recovery: [],
|
|
916
|
+
}, { ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
917
|
+
...(options.output ? { output: options.output } : {}),
|
|
918
|
+
});
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
882
921
|
// rail.progressive.v2 saved pages key journeys/journey_page; legacy result
|
|
883
922
|
// items key candidates/catalog_page. Both are committed, read-only slices.
|
|
884
923
|
const v2Page = (container.journey_page ?? {});
|
|
@@ -897,7 +936,9 @@ export async function runServicesPage(backend, serviceExecutionID, resultItemID,
|
|
|
897
936
|
total: catalogPage.total ?? candidates.length,
|
|
898
937
|
count: catalogPage.count ?? candidates.length,
|
|
899
938
|
next_offset: nextOffset,
|
|
900
|
-
|
|
939
|
+
...(journeys.length ? { journeys: journeys.map(value => compactRailJourney(value, serviceExecutionID, resultItemID)) }
|
|
940
|
+
: candidates.some(value => typeof value === "object" && value !== null && "train_code" in value)
|
|
941
|
+
? { candidates: candidates.map((value, index) => exactTrainRow(value, serviceExecutionID, resultItemID, (catalogPage.offset ?? offset) + index)) } : { page }),
|
|
901
942
|
},
|
|
902
943
|
instruction: "读取的是已保存结果的同版本分页,不重新查询、不消耗额度。先读完所需页,再合并回答用户;不要逐页播报。",
|
|
903
944
|
next: nextOffset !== null
|
|
@@ -913,11 +954,7 @@ export async function runServicesPage(backend, serviceExecutionID, resultItemID,
|
|
|
913
954
|
writeCommandEnvelope(envelope, {
|
|
914
955
|
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
915
956
|
...(options.output ? { output: options.output } : {}),
|
|
916
|
-
plainResult:
|
|
917
|
-
const c = candidate;
|
|
918
|
-
const title = typeof c.title === "string" ? c.title : JSON.stringify(c);
|
|
919
|
-
return `${title}`;
|
|
920
|
-
}),
|
|
957
|
+
plainResult: plainValueLines(envelope.result),
|
|
921
958
|
});
|
|
922
959
|
}
|
|
923
960
|
export async function runServicesList(backend, options = {}) {
|
|
@@ -932,6 +969,7 @@ export async function runServicesList(backend, options = {}) {
|
|
|
932
969
|
status: execution.status,
|
|
933
970
|
phase: execution.phase,
|
|
934
971
|
updated_at: execution.updated_at,
|
|
972
|
+
reader: { command: `itpay services next ${execution.service_execution_id} --json`, reason: "读取这笔已有服务" },
|
|
935
973
|
}));
|
|
936
974
|
const latest = executions[0];
|
|
937
975
|
const envelope = {
|
|
@@ -940,7 +978,7 @@ export async function runServicesList(backend, options = {}) {
|
|
|
940
978
|
instruction: executions.length === 1
|
|
941
979
|
? "只有一条可恢复记录;继续读取同一笔服务。"
|
|
942
980
|
: latest
|
|
943
|
-
? "
|
|
981
|
+
? "按原任务中已明确的执行ID读取对应reader;只有无法确定属于哪一笔任务时才询问用户。"
|
|
944
982
|
: "当前设备没有可恢复的 Service Execution;先读取已发布目录,不要猜测 ID。",
|
|
945
983
|
next: executions.length === 1
|
|
946
984
|
? { command: `itpay services next ${latest.service_execution_id} --json`, reason: "继续唯一可恢复的服务" }
|
|
@@ -952,55 +990,178 @@ export async function runServicesList(backend, options = {}) {
|
|
|
952
990
|
writeCommandEnvelope(envelope, {
|
|
953
991
|
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
954
992
|
...(options.output ? { output: options.output } : {}),
|
|
955
|
-
plainResult: executions.map((execution) => `${execution.service_execution_id}: ${execution.service_id} ${execution.status}/${execution.phase} updated=${execution.updated_at}`),
|
|
956
993
|
});
|
|
957
994
|
}
|
|
995
|
+
// Bound the final qualified output by whole saved rows; the cursor follows the emitted rows.
|
|
996
|
+
function writeSavedRailPage(envelope, options, key, offset, continuation) {
|
|
997
|
+
const result = envelope.result ?? {};
|
|
998
|
+
const rows = result[key];
|
|
999
|
+
if (!Array.isArray(rows))
|
|
1000
|
+
throw new Error("saved page has no rows");
|
|
1001
|
+
const bound = (offset === 0 ? 12 : 32) * 1024;
|
|
1002
|
+
const render = (output) => writeCommandEnvelope(envelope, { ...options, output });
|
|
1003
|
+
if (!options.all) {
|
|
1004
|
+
for (;;) {
|
|
1005
|
+
let text = "";
|
|
1006
|
+
render(value => { text += value; });
|
|
1007
|
+
if (Buffer.byteLength(text, "utf8") <= bound)
|
|
1008
|
+
break;
|
|
1009
|
+
if (rows.length <= 1) {
|
|
1010
|
+
throw new CommandContractError("saved_row_too_large", "saved row exceeds the readable page limit", "这条已保存记录超过当前页面大小;事实仍保留,请读取该条详情。", rows[0]?.detail?.command ? [rows[0].detail] : []);
|
|
1011
|
+
}
|
|
1012
|
+
rows.pop();
|
|
1013
|
+
result.count = rows.length;
|
|
1014
|
+
if (key === "trains")
|
|
1015
|
+
result.read_count = rows.length;
|
|
1016
|
+
result.next_offset = offset + rows.length;
|
|
1017
|
+
envelope.next = continuation(offset + rows.length);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
writeCommandEnvelope(envelope, options);
|
|
1021
|
+
}
|
|
958
1022
|
export async function runServicesReadResult(backend, serviceExecutionID, options = {}) {
|
|
1023
|
+
if (!options.snapshot && !options.journey) {
|
|
1024
|
+
const model = await backend.getServiceExecution(serviceExecutionID);
|
|
1025
|
+
if (model.execution.service_id === "itpay-rail-exact") {
|
|
1026
|
+
const item = (model.current_result_items?.length ? model.current_result_items : model.result_items)?.[0];
|
|
1027
|
+
if (!item) {
|
|
1028
|
+
const preview = model.provider_invocations.at(-1)?.safe_result_preview;
|
|
1029
|
+
const template = exactSmartTemplate(preview?.resolved_station_pair, serviceExecutionID);
|
|
1030
|
+
if (template && preview?.catalog_total === 0) {
|
|
1031
|
+
writeCommandEnvelope({ status: "no_result", result: { service_execution_id: serviceExecutionID,
|
|
1032
|
+
scope: "station_pair", resolved_station_pair: preview.resolved_station_pair,
|
|
1033
|
+
total: 0, smart_template: template },
|
|
1034
|
+
instruction: "这一站对的成功观测为0条,不代表附近区域无车。若用户允许中转或原需求范围更广,保留原始区域/地点意图并沿 smart_template 引用本次事实;只要直达时不要自动转中转。",
|
|
1035
|
+
next: null, recovery: [] }, { ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
1036
|
+
...(options.output ? { output: options.output } : {}) });
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
await runServicesNext(backend, serviceExecutionID, options);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
const rows = [];
|
|
1043
|
+
const startOffset = options.offset ?? 0;
|
|
1044
|
+
const limit = options.limit ?? 20;
|
|
1045
|
+
if (!Number.isInteger(startOffset) || startOffset < 0 || !Number.isInteger(limit) || limit < 1 || limit > 20) {
|
|
1046
|
+
throw new CommandContractError("page_invalid", "offset must be non-negative and limit 1-20", "页码无效;请按返回的 next.command 读取。", []);
|
|
1047
|
+
}
|
|
1048
|
+
let offset = startOffset;
|
|
1049
|
+
let total = 0;
|
|
1050
|
+
let resolvedStationPair;
|
|
1051
|
+
let nextOffset = startOffset;
|
|
1052
|
+
do {
|
|
1053
|
+
const response = await backend.getServiceExecutionResultItemPage(serviceExecutionID, item.service_capability_result_item_id, offset, limit);
|
|
1054
|
+
const container = (response.page.result ?? response.page);
|
|
1055
|
+
const candidates = Array.isArray(container.candidates) ? container.candidates : [];
|
|
1056
|
+
resolvedStationPair ??= container.resolved_station_pair;
|
|
1057
|
+
rows.push(...candidates.map((value, index) => exactTrainRow(value, serviceExecutionID, item.service_capability_result_item_id, offset + index)));
|
|
1058
|
+
const page = (container.catalog_page ?? {});
|
|
1059
|
+
total = typeof container.catalog_total === "number" ? container.catalog_total : Math.max(total, rows.length);
|
|
1060
|
+
nextOffset = typeof page.next_offset === "number" && page.next_offset > offset ? page.next_offset : null;
|
|
1061
|
+
offset = nextOffset ?? offset;
|
|
1062
|
+
} while (options.all && nextOffset !== null);
|
|
1063
|
+
writeSavedRailPage({
|
|
1064
|
+
status: "ready",
|
|
1065
|
+
result: { service_execution_id: serviceExecutionID, scope: "station_pair", resolved_station_pair: resolvedStationPair,
|
|
1066
|
+
total, offset: startOffset, count: rows.length, read_count: rows.length,
|
|
1067
|
+
...(exactSmartTemplate(resolvedStationPair, serviceExecutionID, item.service_capability_result_item_id)
|
|
1068
|
+
? { smart_template: exactSmartTemplate(resolvedStationPair, serviceExecutionID, item.service_capability_result_item_id) } : {}),
|
|
1069
|
+
trains: rows, ...(nextOffset !== null ? { next_offset: nextOffset } : {}) },
|
|
1070
|
+
instruction: "这是同一已保存站对的车次行。按用户时限、席别和人数筛选;零条仅代表这一站对。纯比较答复后结束;已有购票委托则执行所选行 detail.command 读取当前席别、库存和购买凭据。",
|
|
1071
|
+
next: nextOffset !== null ? { command: `itpay services read-result ${serviceExecutionID} --offset ${nextOffset} --limit ${limit} --json`, reason: "继续读取同一结果的剩余车次" } : null,
|
|
1072
|
+
recovery: [],
|
|
1073
|
+
}, { ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
1074
|
+
...(options.output ? { output: options.output } : {}),
|
|
1075
|
+
all: Boolean(options.all),
|
|
1076
|
+
}, "trains", startOffset, next => ({ command: `itpay services read-result ${serviceExecutionID} --offset ${next} --limit ${limit} --json`, reason: "读取同一保存结果的下一页" }));
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
959
1080
|
// rail.progressive.v2 planning read: --journey/--snapshot select committed
|
|
960
1081
|
// planning evidence — a free owner-validated read that never touches the
|
|
961
1082
|
// grant/Vault path. Without the selectors the authorized-delivery flow is
|
|
962
1083
|
// unchanged.
|
|
963
|
-
// rail.planning
|
|
964
|
-
// committed rail.catalog.v3
|
|
965
|
-
// preserved verbatim; one catalog, never split into candidates+journeys.
|
|
1084
|
+
// rail.planning catalog read: --snapshot returns compact rows for the
|
|
1085
|
+
// committed rail.catalog.v3; journey detail remains available separately.
|
|
966
1086
|
if (options.snapshot && !options.journey) {
|
|
967
|
-
const
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1087
|
+
const offset = options.offset ?? 0;
|
|
1088
|
+
const limit = options.limit ?? 3;
|
|
1089
|
+
if (!Number.isInteger(offset) || offset < 0 || !Number.isInteger(limit) || limit < 1 || limit > 20) {
|
|
1090
|
+
throw new CommandContractError("page_invalid", "offset must be non-negative and limit 1-20", "目录页码无效;请按返回的 next.command 读取。", []);
|
|
1091
|
+
}
|
|
1092
|
+
const journeys = [];
|
|
1093
|
+
let nextOffset = offset;
|
|
1094
|
+
let total = 0;
|
|
1095
|
+
let qualificationCounts;
|
|
1096
|
+
let planID;
|
|
1097
|
+
let queryRevision;
|
|
1098
|
+
while (nextOffset !== null) {
|
|
1099
|
+
let response;
|
|
1100
|
+
try {
|
|
1101
|
+
response = await backend.getServiceExecutionResultItemPage(serviceExecutionID, options.snapshot, nextOffset, limit);
|
|
1102
|
+
}
|
|
1103
|
+
catch (error) {
|
|
1104
|
+
if (error instanceof HttpError && error.code === "result_not_pageable") {
|
|
1105
|
+
const committed = await backend.getRailPlanningCatalog(serviceExecutionID, options.snapshot);
|
|
1106
|
+
decodeRailCatalogJourneys(committed.catalog);
|
|
1107
|
+
}
|
|
1108
|
+
throw error;
|
|
1109
|
+
}
|
|
1110
|
+
if (response.snapshot_id !== options.snapshot)
|
|
1111
|
+
throw new Error("saved page snapshot changed");
|
|
1112
|
+
planID = response.plan_id;
|
|
1113
|
+
queryRevision = response.query_revision;
|
|
1114
|
+
const container = (response.page.result ?? response.page);
|
|
1115
|
+
const page = container.journey_page;
|
|
1116
|
+
const rows = Array.isArray(container.journeys) ? container.journeys : [];
|
|
1117
|
+
total = typeof page?.total === "number" ? page.total : rows.length;
|
|
1118
|
+
qualificationCounts = page?.qualification_counts ?? qualificationCounts;
|
|
1119
|
+
journeys.push(...rows.map((card) => compactRailJourney(card, serviceExecutionID, options.snapshot)));
|
|
1120
|
+
const following = page?.next_offset;
|
|
1121
|
+
if (typeof following === "number" && following <= nextOffset)
|
|
1122
|
+
throw new Error("saved page cursor did not advance");
|
|
1123
|
+
nextOffset = typeof following === "number" ? following : null;
|
|
1124
|
+
if (!options.all)
|
|
1125
|
+
break;
|
|
1126
|
+
}
|
|
1127
|
+
writeSavedRailPage({
|
|
979
1128
|
status: "ready",
|
|
980
1129
|
result: {
|
|
981
1130
|
service_execution_id: serviceExecutionID,
|
|
982
|
-
plan_id:
|
|
983
|
-
snapshot_id:
|
|
984
|
-
query_revision:
|
|
985
|
-
|
|
1131
|
+
plan_id: planID,
|
|
1132
|
+
snapshot_id: options.snapshot,
|
|
1133
|
+
query_revision: queryRevision,
|
|
1134
|
+
total,
|
|
1135
|
+
qualification_counts: qualificationCounts,
|
|
1136
|
+
offset,
|
|
1137
|
+
count: journeys.length,
|
|
1138
|
+
next_offset: nextOffset,
|
|
1139
|
+
journeys,
|
|
986
1140
|
},
|
|
987
|
-
instruction:
|
|
988
|
-
|
|
1141
|
+
instruction: qualificationCounts === undefined
|
|
1142
|
+
? "此历史快照没有完整资格计数;线路仅供核对铁路事实,不能据此认定可购买。沿同快照 detail.command 查看详情。"
|
|
1143
|
+
: qualificationCounts.eligible === 0
|
|
1144
|
+
? `查到 ${total} 个时刻组合,当前 0 条满足购票资格。请读取诊断线路的 detail.command 查看具体原因;修改条件须依据用户意愿。`
|
|
1145
|
+
: "这是同一已保存快照的完整线路分页;合格线路已排在前面。若问题需要全部方案,沿 next.command 读完再一次回答;未知费用不能当免费。购票条件用合格线路的 detail.command,价格与余票在购买前实时报价。",
|
|
1146
|
+
next: nextOffset === null ? null : { command: `itpay services read-result ${serviceExecutionID} --snapshot ${options.snapshot} --offset ${nextOffset} --limit ${limit} --json`, reason: "仅当前问题还需未读线路时继续;已有合格完整结果可答复或按委托继续" },
|
|
989
1147
|
recovery: [],
|
|
990
1148
|
}, {
|
|
991
1149
|
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
992
1150
|
...(options.output ? { output: options.output } : {}),
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
`${journeyCount} combinations:`,
|
|
996
|
-
...journeys.map((j) => `${j.defaultLayer === "backup" ? "[备选] " : "[主选择] "}${j.ref} ${j.route}`),
|
|
997
|
-
]
|
|
998
|
-
: [`catalog: ${committed.snapshot_id} (no combinations)`],
|
|
999
|
-
});
|
|
1151
|
+
all: Boolean(options.all),
|
|
1152
|
+
}, "journeys", offset, next => ({ command: `itpay services read-result ${serviceExecutionID} --snapshot ${options.snapshot} --offset ${next} --limit ${limit} --json`, reason: "仅当前问题还需未读线路时继续;已有合格完整结果可答复或按委托继续" }));
|
|
1000
1153
|
return;
|
|
1001
1154
|
}
|
|
1002
1155
|
if (options.journey) {
|
|
1003
1156
|
const detail = await backend.getRailJourneyDetail(serviceExecutionID, options.journey, options.snapshot);
|
|
1157
|
+
const planOffset = options.offset ?? 0;
|
|
1158
|
+
const planLimit = options.limit ?? 3;
|
|
1159
|
+
if (!Number.isInteger(planOffset) || planOffset < 0 || !Number.isInteger(planLimit) || planLimit < 1 || planLimit > 20) {
|
|
1160
|
+
throw new CommandContractError("page_invalid", "offset must be non-negative and limit 1-20", "席别方案页码无效;请按返回的 next.command 读取。", []);
|
|
1161
|
+
}
|
|
1162
|
+
const plans = detail.journey.ticket_plans ?? [];
|
|
1163
|
+
const selected = options.all ? plans : plans.slice(planOffset, planOffset + planLimit);
|
|
1164
|
+
const nextOffset = !options.all && planOffset + selected.length < plans.length ? planOffset + selected.length : null;
|
|
1004
1165
|
writeCommandEnvelope({
|
|
1005
1166
|
status: "ready",
|
|
1006
1167
|
result: {
|
|
@@ -1008,17 +1169,22 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
1008
1169
|
plan_id: detail.plan_id,
|
|
1009
1170
|
snapshot_id: detail.snapshot_id,
|
|
1010
1171
|
query_revision: detail.query_revision,
|
|
1011
|
-
journey: detail.journey,
|
|
1172
|
+
journey: compactRailJourney(detail.journey, serviceExecutionID, detail.snapshot_id),
|
|
1173
|
+
ticket_plan_total: plans.length,
|
|
1174
|
+
ticket_plan_offset: planOffset,
|
|
1175
|
+
ticket_plans: selected.map(plan => compactRailTicketPlan(plan, detail.journey.qualification_status === "eligible")),
|
|
1012
1176
|
},
|
|
1013
|
-
instruction: "
|
|
1014
|
-
next: detail.journey
|
|
1015
|
-
|
|
1016
|
-
|
|
1177
|
+
instruction: "同一线路的完整车次与席别方案。已有所需席别和有效凭据即可按真实委托继续;只需比较则答复并停止,需要其他席别才读下一页。中转各段独立购买,无联合锁票或接续保证;购买前受保护 Checkout 实时报价。",
|
|
1178
|
+
next: nextOffset === null ? null : { command: `itpay services read-result ${serviceExecutionID} --snapshot ${detail.snapshot_id} --journey ${options.journey} --offset ${nextOffset} --limit ${planLimit} --json`, reason: "仅需要其他席别时读取;已满足用户条件则停止翻页" },
|
|
1179
|
+
...(detail.journey?.booking_support === "single_leg" ? { interaction: {
|
|
1180
|
+
schema_version: "itpay.interaction.v1", stage: "query_results_ready",
|
|
1181
|
+
select_user_chosen: `itpay services action ${serviceExecutionID} --action select_journey --actor-type human --status approved --input journey_id=${detail.journey.journey_id} --json`,
|
|
1182
|
+
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`,
|
|
1183
|
+
} } : {}),
|
|
1017
1184
|
recovery: [],
|
|
1018
1185
|
}, {
|
|
1019
1186
|
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
1020
1187
|
...(options.output ? { output: options.output } : {}),
|
|
1021
|
-
plainResult: servicesNextPlainResult(detail.journey),
|
|
1022
1188
|
});
|
|
1023
1189
|
return;
|
|
1024
1190
|
}
|
|
@@ -1038,6 +1204,43 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
1038
1204
|
plainResult: servicesNextPlainResult(envelope.result),
|
|
1039
1205
|
});
|
|
1040
1206
|
}
|
|
1207
|
+
function exactSmartTemplate(pair, executionID, resultItemID) {
|
|
1208
|
+
if (!pair || typeof pair !== "object")
|
|
1209
|
+
return null;
|
|
1210
|
+
const value = pair;
|
|
1211
|
+
if (!["origin_name", "origin_code", "destination_name", "destination_code", "travel_date"].every((key) => typeof value[key] === "string" && value[key]))
|
|
1212
|
+
return null;
|
|
1213
|
+
return { executable: false, verified_station_pair: value,
|
|
1214
|
+
instruction: "已核验站对是复用事实,不是完整用户查询。仅用户明确锁站才填 kind=station;区域/地点从原始需求填回,保留真实人数、时限和偏好,不默认为一人。",
|
|
1215
|
+
command: "itpay services run itpay-rail-smart --input-json <原始需求.json> --json",
|
|
1216
|
+
required_input: ["endpoints", "passengers", "用户时限与偏好"],
|
|
1217
|
+
input_example: { endpoints: { origin: { text: "<原始出发行政范围/地点/用户指定站>" }, destination: { text: "<原始到达行政范围/地点/用户指定站>" } },
|
|
1218
|
+
travel_date: value.travel_date, passengers: "<真实人数>", reuse_from: { execution_id: executionID,
|
|
1219
|
+
...(resultItemID ? { result_item_id: resultItemID } : {}) } } };
|
|
1220
|
+
}
|
|
1221
|
+
function exactTrainRow(candidate, executionID, itemID, offset) {
|
|
1222
|
+
return { ...compactExactTrain(candidate), result_item_id: itemID, result_offset: offset,
|
|
1223
|
+
detail: { command: `itpay services page ${executionID} ${itemID} --offset ${offset} --limit 1 --json`, reason: "读取此车次席别与服务端购买模板" } };
|
|
1224
|
+
}
|
|
1225
|
+
function compactExactTrain(candidate) {
|
|
1226
|
+
const seats = Array.isArray(candidate.seats) ? candidate.seats : [];
|
|
1227
|
+
return {
|
|
1228
|
+
candidate_id: candidate.candidate_id,
|
|
1229
|
+
train_code: candidate.train_code,
|
|
1230
|
+
from_station: candidate.from_station,
|
|
1231
|
+
from_station_code: candidate.from_station_code,
|
|
1232
|
+
to_station: candidate.to_station,
|
|
1233
|
+
to_station_code: candidate.to_station_code,
|
|
1234
|
+
travel_date: candidate.travel_date,
|
|
1235
|
+
departure: candidate.departure,
|
|
1236
|
+
arrival: candidate.arrival,
|
|
1237
|
+
purchase_supported: candidate.purchase_supported ?? Boolean(candidate.booking_offer),
|
|
1238
|
+
seats: seats.map((seat) => ({ seat_type: seat.seat_type, seat_name: seat.seat_name,
|
|
1239
|
+
availability_text: seat.availability_text, remaining: seat.remaining,
|
|
1240
|
+
fare_minor: seat.fare_minor, quoted_total_minor: seat.quoted_total_minor,
|
|
1241
|
+
purchase_supported: seat.purchase_supported, requires_price_refresh: seat.requires_price_refresh })),
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1041
1244
|
function railLegSeatSummary(seat) {
|
|
1042
1245
|
return {
|
|
1043
1246
|
passenger_index: seat.passenger_index,
|
|
@@ -1071,15 +1274,14 @@ function railBookingEnvelope(model) {
|
|
|
1071
1274
|
service_execution_id: execution.service_execution_id,
|
|
1072
1275
|
...(orderID ? { order_id: orderID } : {}),
|
|
1073
1276
|
rail: { state: rail.state, issued_legs: rail.issued_legs, legs },
|
|
1074
|
-
...(model.workflow ? { workflow: model.workflow } : {}),
|
|
1075
1277
|
};
|
|
1076
1278
|
if (rail.state === "issued") {
|
|
1077
1279
|
const detailsPending = rail.legs.some((leg) => leg.details_pending);
|
|
1078
1280
|
return {
|
|
1079
1281
|
status: "issued",
|
|
1080
|
-
result,
|
|
1282
|
+
result: { ...result, ...(orderID ? { aftercare: { command: `itpay order ${orderID} --json`, when: "仅用户请求核对订单或售后时" } } : {}) },
|
|
1081
1283
|
instruction: `告诉用户:车票已出票,座位以实际出票为准,平台不提供 12306 票号,可在订单页核对行程。不要在对话中索要乘车人身份信息。${detailsPending ? "部分席位信息未能同步,请前往 12306 核对行程;不要重复购买。" : ""}`,
|
|
1082
|
-
next:
|
|
1284
|
+
next: null,
|
|
1083
1285
|
recovery: [],
|
|
1084
1286
|
};
|
|
1085
1287
|
}
|
|
@@ -1113,18 +1315,124 @@ function railBookingEnvelope(model) {
|
|
|
1113
1315
|
recovery: [],
|
|
1114
1316
|
};
|
|
1115
1317
|
}
|
|
1116
|
-
function
|
|
1318
|
+
function compactRailJourney(card, serviceExecutionID, snapshotID) {
|
|
1319
|
+
if (!card.journey_id)
|
|
1320
|
+
throw new Error("saved journey has no stable ID");
|
|
1321
|
+
const rides = (card.rides ?? []).map((ride) => ({
|
|
1322
|
+
train_code: ride.train_code ?? (Array.isArray(ride.train_codes) ? ride.train_codes.join("/") : null),
|
|
1323
|
+
from_station: ride.from_station ?? null, from_station_code: ride.from_station_code ?? null,
|
|
1324
|
+
departure: ride.departure ?? null, to_station: ride.to_station ?? null,
|
|
1325
|
+
to_station_code: ride.to_station_code ?? null, arrival: ride.arrival ?? null,
|
|
1326
|
+
...(typeof ride.wait_minutes === "number" ? { wait_minutes: ride.wait_minutes } : {}),
|
|
1327
|
+
...(typeof ride.minimum_connection_minutes === "number" ? { required_buffer_minutes: ride.minimum_connection_minutes } : {}),
|
|
1328
|
+
...(ride.same_run === true ? { same_run: true } : {}),
|
|
1329
|
+
}));
|
|
1330
|
+
const offer = card.representative_offer;
|
|
1331
|
+
const metrics = card.representative_metrics;
|
|
1332
|
+
const ticketSeats = (offer?.ticket_offers ?? []).map((raw) => ({
|
|
1333
|
+
leg_index: raw.leg_index, seat_type: raw.seat_type, seat_name: raw.seat_name,
|
|
1334
|
+
availability: raw.availability_text, remaining: raw.inventory_exact === true ? raw.remaining : null,
|
|
1335
|
+
}));
|
|
1336
|
+
const railwayTime = (value) => {
|
|
1337
|
+
if (typeof value !== "string")
|
|
1338
|
+
return NaN;
|
|
1339
|
+
const time = value.replace(" ", "T");
|
|
1340
|
+
return Date.parse(/(?:Z|[+-]\d{2}:\d{2})$/.test(time) ? time : `${time}Z`);
|
|
1341
|
+
};
|
|
1342
|
+
const start = railwayTime(rides[0]?.departure);
|
|
1343
|
+
const end = railwayTime(rides.at(-1)?.arrival);
|
|
1344
|
+
return {
|
|
1345
|
+
journey_ref: card.journey_id,
|
|
1346
|
+
...(card.profile_refs?.balanced ? { profile_ref: card.profile_refs.balanced } : {}),
|
|
1347
|
+
default_layer: card.default_layer ?? "main",
|
|
1348
|
+
...(card.backup_reason ? { backup_reason: card.backup_reason } : {}),
|
|
1349
|
+
...(card.backup_baseline_min != null ? { backup_baseline_min: card.backup_baseline_min } : {}),
|
|
1350
|
+
...(card.backup_delta_min != null ? { backup_delta_min: card.backup_delta_min } : {}),
|
|
1351
|
+
route: card.route_text ?? (card.route_names ?? card.route ?? []).join("→"),
|
|
1352
|
+
rides,
|
|
1353
|
+
...(card.booking_support === "single_leg" && card.qualification_status === "eligible" && card.booking_offer ? {
|
|
1354
|
+
booking_template: { command: `itpay services run ${card.booking_offer.service_id} --input-json <购票.json> --json`, executable: false,
|
|
1355
|
+
seat_choices: ticketSeats.map(seat => ({ seat_type: seat.seat_type, seat_name: seat.seat_name })),
|
|
1356
|
+
input_example: { selection: { token: card.booking_offer.selection_token, seat_type: "<返回席别代码>" }, passengers: card.passengers ?? offer?.ticket_offers?.[0]?.passengers ?? "<真实人数>" },
|
|
1357
|
+
required_input: ["file", "真实席别选择及人数"] },
|
|
1358
|
+
} : {}),
|
|
1359
|
+
...(ticketSeats.length ? { seats: ticketSeats } : {}),
|
|
1360
|
+
passengers: card.passengers ?? offer?.ticket_offers?.[0]?.passengers ?? null,
|
|
1361
|
+
availability: card.availability ?? "unknown",
|
|
1362
|
+
qualification_status: card.qualification_status ?? "unknown",
|
|
1363
|
+
...(card.qualification_reasons?.length ? { qualification_reasons: card.qualification_reasons } : {}),
|
|
1364
|
+
...(offer ? { rail_payable_fen: offer.rail_payable_minor, currency: offer.currency } : { rail_payable_fen: null }),
|
|
1365
|
+
estimated_trip_total: metrics?.estimated_total_cost ?? null,
|
|
1366
|
+
cost_basis: metrics?.cost_basis ?? "unknown",
|
|
1367
|
+
duration_basis: metrics?.duration_basis ?? "unknown",
|
|
1368
|
+
timezone: "Asia/Shanghai",
|
|
1369
|
+
...(card.risk_notes?.length ? { risk_notes: card.risk_notes } : {}),
|
|
1370
|
+
rail_duration_minutes: Number.isFinite(start) && Number.isFinite(end) && end >= start ? Math.round((end - start) / 60000) : null,
|
|
1371
|
+
...(card.observed_at ? { observed_at: card.observed_at } : {}),
|
|
1372
|
+
detail: { command: `itpay services read-result ${serviceExecutionID} --snapshot ${snapshotID} --journey ${card.journey_id} --json`,
|
|
1373
|
+
reason: card.qualification_status === "eligible" ? "读取此完整线路的席别、报价和购买条件" : "读取此线路的铁路事实和不可购买原因" },
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
function compactRailTicketPlan(raw, journeyEligible) {
|
|
1377
|
+
const legs = Array.isArray(raw.legs) ? raw.legs : [];
|
|
1378
|
+
const seats = Array.isArray(raw.seat_offers) ? raw.seat_offers : [];
|
|
1379
|
+
const tickets = Array.isArray(raw.ticket_offers) ? raw.ticket_offers : [];
|
|
1380
|
+
return {
|
|
1381
|
+
ticket_plan_ref: raw.ticket_plan_ref,
|
|
1382
|
+
passengers: raw.passenger_count ?? (Array.isArray(raw.ticket_offers) ? raw.ticket_offers[0]?.passengers : null) ?? null,
|
|
1383
|
+
purchase_support: raw.purchase_support ?? "none",
|
|
1384
|
+
qualification_status: raw.qualification_status ?? "unknown",
|
|
1385
|
+
qualification_reasons: raw.qualification_reasons ?? [],
|
|
1386
|
+
rail_amount_fen: raw.rail_amount_fen ?? null,
|
|
1387
|
+
service_fee_fen: raw.service_fee_fen ?? null,
|
|
1388
|
+
price_verified: raw.rail_amount_verified === true,
|
|
1389
|
+
legs: legs.map((leg, index) => ({
|
|
1390
|
+
leg_index: index,
|
|
1391
|
+
travel_date: leg.travel_date ?? tickets.find(ticket => ticket.leg_index === index)?.travel_date ?? null,
|
|
1392
|
+
...(() => {
|
|
1393
|
+
const ticket = tickets.find(ticket => ticket.leg_index === index) ?? (tickets[index]?.leg_index === undefined ? tickets[index] : undefined);
|
|
1394
|
+
const offer = ticket?.booking_offer;
|
|
1395
|
+
return offer?.service_id && offer.selection_token && journeyEligible && raw.qualification_status === "eligible" ? {
|
|
1396
|
+
booking_template: { command: `itpay services run ${offer.service_id} --input-json <购票.json> --json`, executable: false,
|
|
1397
|
+
ticket_plan_ref: raw.ticket_plan_ref, leg_index: index,
|
|
1398
|
+
seat_choices: [{ seat_type: ticket?.seat_type, seat_name: ticket?.seat_name }],
|
|
1399
|
+
input_example: { selection: { token: offer.selection_token, seat_type: ticket?.seat_type ?? "<该方案席别代码>" },
|
|
1400
|
+
passengers: ticket?.passengers ?? raw.passenger_count ?? "<真实人数>" },
|
|
1401
|
+
required_input: ["file", "真实人数", "该方案席别代码"],
|
|
1402
|
+
},
|
|
1403
|
+
} : { booking_unavailable: "本段没有当前可用的服务端凭据;不能猜代码或例行重查。保留快照,按当前执行的恢复动作处理;过期凭据需真实新验证。" };
|
|
1404
|
+
})(),
|
|
1405
|
+
train_code: leg.train_code ?? null,
|
|
1406
|
+
from_station: leg.from_station ?? null, from_station_code: leg.from_station_code ?? null,
|
|
1407
|
+
departure: leg.departure ?? null,
|
|
1408
|
+
to_station: leg.to_station ?? null, to_station_code: leg.to_station_code ?? null,
|
|
1409
|
+
arrival: leg.arrival ?? null,
|
|
1410
|
+
seats: (seats[index] ?? []).map((seat) => ({ seat_type: seat.seat_type, seat_name: seat.seat_name,
|
|
1411
|
+
availability: seat.availability_text, remaining: seat.inventory_exact === true ? seat.remaining : null,
|
|
1412
|
+
unit_fare_fen: seat.fare_minor, eligible: seat.eligible === true })),
|
|
1413
|
+
})),
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
function railJourneySummary(card, serviceExecutionID, snapshotID) {
|
|
1117
1417
|
const rides = Array.isArray(card.rides) ? card.rides : [];
|
|
1118
1418
|
const trains = rides.map((ride) => ride.train_code).filter(Boolean);
|
|
1119
1419
|
const first = rides[0];
|
|
1120
1420
|
const last = rides.at(-1);
|
|
1121
1421
|
const offer = card.representative_offer;
|
|
1422
|
+
const metrics = card.representative_metrics;
|
|
1122
1423
|
return {
|
|
1123
1424
|
journey_id: card.journey_id,
|
|
1425
|
+
...(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 } : {}) }; })() : {}),
|
|
1124
1426
|
route: (card.route_names?.length ? card.route_names : card.route ?? []).join("→"),
|
|
1125
1427
|
...(trains.length ? { trains } : {}),
|
|
1126
1428
|
...(first?.departure || last?.arrival ? { time: `${first?.departure ?? ""}–${last?.arrival ?? ""}` } : {}),
|
|
1127
1429
|
...(offer ? { price: formatMoney(offer.rail_payable_minor, offer.currency) } : { price: "unknown" }),
|
|
1430
|
+
...(metrics ? { calculation_scope: {
|
|
1431
|
+
origin: metrics.origin_scope ?? "legacy_unknown", destination: metrics.destination_scope ?? "legacy_unknown",
|
|
1432
|
+
duration_basis: metrics.duration_basis ?? "legacy_unknown", arrival_basis: metrics.arrival_basis ?? "legacy_unknown",
|
|
1433
|
+
cost_basis: metrics.cost_basis ?? "legacy_unknown",
|
|
1434
|
+
duration_minutes: metrics.door_to_door_min ?? null,
|
|
1435
|
+
} } : {}),
|
|
1128
1436
|
...(card.decision_role ? { decision_role: card.decision_role } : {}),
|
|
1129
1437
|
...(card.explanation?.length ? { explanation: card.explanation } : {}),
|
|
1130
1438
|
...(card.reason_codes?.length ? { reason_codes: card.reason_codes } : {}),
|
|
@@ -1137,35 +1445,12 @@ function railJourneySummary(card, serviceExecutionID) {
|
|
|
1137
1445
|
...(card.risk_notes?.length ? { risk_notes: card.risk_notes } : {}),
|
|
1138
1446
|
...(card.booking_support === "single_leg"
|
|
1139
1447
|
? {
|
|
1140
|
-
|
|
1448
|
+
select_user_chosen: `itpay services action ${serviceExecutionID} --action select_journey --actor-type human --status approved --input journey_id=${card.journey_id} --json`,
|
|
1141
1449
|
// Delegated selection under explicit user rules is an agent action —
|
|
1142
1450
|
// never record it as a human pick.
|
|
1143
1451
|
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`,
|
|
1144
1452
|
}
|
|
1145
1453
|
: {}),
|
|
1146
|
-
...(card.booking_offer
|
|
1147
|
-
? {
|
|
1148
|
-
booking_offer: card.booking_offer,
|
|
1149
|
-
booking_template: {
|
|
1150
|
-
command: `itpay services run ${card.booking_offer.service_id} --input-json <file> --json`,
|
|
1151
|
-
required_input: ["file"],
|
|
1152
|
-
executable: false,
|
|
1153
|
-
seat_choices: (card.representative_offer?.ticket_offers ?? []).map((offer) => ({
|
|
1154
|
-
seat_type: offer.seat_type,
|
|
1155
|
-
seat_name: offer.seat_name,
|
|
1156
|
-
})).filter((seat) => typeof seat.seat_type === "string"),
|
|
1157
|
-
input_example: {
|
|
1158
|
-
selection: {
|
|
1159
|
-
token: card.booking_offer.selection_token,
|
|
1160
|
-
...(typeof card.representative_offer?.ticket_offers?.[0]?.seat_type === "string"
|
|
1161
|
-
? { seat_type: card.representative_offer.ticket_offers[0].seat_type }
|
|
1162
|
-
: {}),
|
|
1163
|
-
},
|
|
1164
|
-
passengers: card.passengers ?? 1,
|
|
1165
|
-
},
|
|
1166
|
-
},
|
|
1167
|
-
}
|
|
1168
|
-
: {}),
|
|
1169
1454
|
};
|
|
1170
1455
|
}
|
|
1171
1456
|
// rail.progressive.v2 read projection: committed snapshots only; polling never
|
|
@@ -1177,11 +1462,19 @@ function railPlanningEnvelope(model) {
|
|
|
1177
1462
|
const expansion = plan.search?.expansion_status ?? "running";
|
|
1178
1463
|
const counts = plan.search?.counts;
|
|
1179
1464
|
const journeys = [plan.recommendation, ...(plan.alternatives ?? [])].filter(Boolean);
|
|
1180
|
-
const
|
|
1465
|
+
const hasEligible = typeof counts?.journeys_eligible === "number"
|
|
1466
|
+
? counts.journeys_eligible > 0 : journeys.length > 0;
|
|
1467
|
+
const changedScopeHint = plan.query_input && !/supplier|provider|dispatch|map|amap|permission/.test(plan.search?.reason ?? "")
|
|
1468
|
+
? "若原条件锁定车站,可提议仅放宽一端到原站周边;有已核实地点证据才建议具体站,不能说肯定有票。先询问用户是否接受该端范围变化;同意后按new_query_template开新查询,未同意则保持原站。"
|
|
1469
|
+
: /supplier|provider|dispatch|map|amap|permission/.test(plan.search?.reason ?? "")
|
|
1470
|
+
? "依赖故障按原恢复动作处理,不通过换站掩盖故障。"
|
|
1471
|
+
: "如需改变端点,先取得真实范围变更同意,再按正式铁路指南保留原条件发起新查询。";
|
|
1472
|
+
const cards = journeys.filter(card => card.journey_id !== plan.recommendation?.journey_id)
|
|
1473
|
+
.map((card) => railJourneySummary(card, se, plan.catalog_snapshot_id ?? plan.snapshot_id));
|
|
1181
1474
|
// Journey counts come from the committed catalog — real combination counts,
|
|
1182
1475
|
// never seat-row counts. Transfer buckets follow the verified transfer_count.
|
|
1183
1476
|
const journeyMix = counts && (counts.journeys_total ?? 0) > 0
|
|
1184
|
-
?
|
|
1477
|
+
? `已保存${counts.journeys_total}个具体车次时刻组合,其中${counts.journeys_eligible ?? "未知"}条当前满足铁路资格(直达${counts.journeys_direct ?? 0}/1中转${counts.journeys_one_transfer ?? 0}/2中转${counts.journeys_multi_transfer ?? 0};这些不是不同站序的数量)`
|
|
1185
1478
|
: "";
|
|
1186
1479
|
const pairProgress = counts && (counts.pairs_total ?? 0) > 0
|
|
1187
1480
|
? `已检查${counts.pairs_checked ?? 0}/${counts.pairs_total}个附近站对`
|
|
@@ -1201,6 +1494,7 @@ function railPlanningEnvelope(model) {
|
|
|
1201
1494
|
rail_planning: {
|
|
1202
1495
|
readiness: plan.readiness,
|
|
1203
1496
|
query_revision: plan.query_revision,
|
|
1497
|
+
...(plan.query_input ? { endpoints: plan.query_input.endpoints } : {}),
|
|
1204
1498
|
snapshot_id: plan.snapshot_id,
|
|
1205
1499
|
expansion_status: expansion,
|
|
1206
1500
|
...(plan.search?.phase ? { phase: plan.search.phase } : {}),
|
|
@@ -1214,83 +1508,57 @@ function railPlanningEnvelope(model) {
|
|
|
1214
1508
|
...(plan.search?.poll_after_ms ? { poll_after_ms: plan.search.poll_after_ms } : {}),
|
|
1215
1509
|
...(plan.result_not_updated ? { result_not_updated: true } : {}),
|
|
1216
1510
|
...(counts ? { counts } : {}),
|
|
1217
|
-
...(plan.budgets ? { budgets: plan.budgets } : {}),
|
|
1218
1511
|
...(plan.coverage ? { coverage: plan.coverage } : {}),
|
|
1219
1512
|
},
|
|
1220
1513
|
...(plan.notices?.length ? { notices: plan.notices } : {}),
|
|
1221
|
-
...(plan.recommendation ? { recommendation: railJourneySummary(plan.recommendation, se) } : {}),
|
|
1514
|
+
...(plan.recommendation ? { recommendation: railJourneySummary(plan.recommendation, se, plan.catalog_snapshot_id ?? plan.snapshot_id) } : {}),
|
|
1222
1515
|
...(cards.length ? { journeys: cards } : {}),
|
|
1223
1516
|
...(plan.snapshot_id ? { full_result: { command: `itpay services read-result ${se} --snapshot ${plan.snapshot_id} --json`, meaning: "读取已保存的完整目录;推荐和journeys仅是摘要" } } : {}),
|
|
1517
|
+
...(!hasEligible && ["paused", "complete", "expired"].includes(expansion) && plan.query_input && plan.catalog_snapshot_id ? { new_query_template: {
|
|
1518
|
+
command: "itpay services run itpay-rail-smart --input-json <file> --json",
|
|
1519
|
+
executable: false,
|
|
1520
|
+
input_example: { ...plan.query_input, reuse_from: { execution_id: se, snapshot_id: plan.catalog_snapshot_id } },
|
|
1521
|
+
meaning: "先取得真实范围变更同意,再复制完整input_example,仅改获准端。换具体站用station及匹配名称/站码;考虑原站周边用place及原站名,删除station_code,经正常地点解析或使用已核实POI/坐标;只有明确同意整个市才用area及完整行政名,删除POI/坐标/站码。日期、人数、时限及偏好保持;新执行不改变旧查询或订单。引用仅限同owner未过期事实,跨owner拒绝;日期变更不复用旧库存。",
|
|
1522
|
+
} } : {}),
|
|
1224
1523
|
...(plan.available_actions?.length ? { available_actions: plan.available_actions } : {}),
|
|
1225
1524
|
};
|
|
1226
1525
|
const nextPoll = {
|
|
1227
|
-
command: `itpay services next ${se}${plan.snapshot_id ? ` --since-snapshot ${plan.snapshot_id}` : ""} --json`,
|
|
1228
|
-
reason: "
|
|
1526
|
+
command: `itpay services next ${se} --timeout 120${plan.snapshot_id ? ` --since-snapshot ${plan.snapshot_id}` : ""} --json`,
|
|
1527
|
+
reason: "仍需更多结果时有界等待同一规划(不触发供应商调用)",
|
|
1229
1528
|
};
|
|
1230
|
-
// Two-goal interaction contract (spec 03 §2): the local Agent picks compare
|
|
1231
|
-
// or prepare_checkout from full context — the envelope teaches both branches
|
|
1232
|
-
// so a delegated purchase keeps moving and a comparison never buys.
|
|
1233
|
-
const bookable = journeys.find((card) => card.booking_support === "single_leg" && card.booking_offer);
|
|
1234
|
-
const queryResultsInteraction = journeys.length > 0
|
|
1235
|
-
? {
|
|
1236
|
-
schema_version: "itpay.interaction.v1",
|
|
1237
|
-
stage: "query_results_ready",
|
|
1238
|
-
by_goal: {
|
|
1239
|
-
compare: { action: "present_options", max_options: 3, stop_after_presentation: true },
|
|
1240
|
-
prepare_checkout: {
|
|
1241
|
-
action: "select_under_user_rules_then_prepare",
|
|
1242
|
-
recipe: "rail.selected-to-checkout.v1",
|
|
1243
|
-
requires: ["current_delegation", "eligible_current_selection", "owner_permits_next_step"],
|
|
1244
|
-
...(bookable
|
|
1245
|
-
? {
|
|
1246
|
-
steps: [
|
|
1247
|
-
"按用户明确规则在 journeys 中选择合格者(卡片 select_delegated 提交 agent 委托选择;人类亲自选择用 select)",
|
|
1248
|
-
`用所选卡片的 booking_template 写 owner-only 临时输入(selection token + 席别 + 人数,不含身份信息),运行 itpay services run itpay-rail-booking --input-json <file> --json`,
|
|
1249
|
-
"owner 要求聊天 review 时一次合并补齐必要字段,真实确认后提交原 workflow:confirm_booking;不得伪造 accept",
|
|
1250
|
-
`确认后恢复同一购买执行:itpay services run itpay-rail-booking --execution <实际ID> --json,沿用有界等待与受保护 Checkout`,
|
|
1251
|
-
],
|
|
1252
|
-
}
|
|
1253
|
-
: { unavailable: "当前结果无可整段购买映射;分别说明各段风险,不得合成原子联程。" }),
|
|
1254
|
-
},
|
|
1255
|
-
},
|
|
1256
|
-
}
|
|
1257
|
-
: undefined;
|
|
1258
|
-
const queryResultsCommunication = journeys.length > 0
|
|
1259
|
-
? {
|
|
1260
|
-
schema_version: "itpay.communication.v1",
|
|
1261
|
-
status_line: expansion === "paused"
|
|
1262
|
-
? "首批结果已就绪,扩展搜索已暂停等待用户决定"
|
|
1263
|
-
: "查询结果就绪",
|
|
1264
|
-
recommended_reason: plan.recommendation?.explanation?.[0],
|
|
1265
|
-
human_steps: [
|
|
1266
|
-
"compare:比较首选与最多两个备选后告知取舍",
|
|
1267
|
-
"prepare_checkout:委托仍在时确认所选,继续官方确认页",
|
|
1268
|
-
"姓名、证件、手机号只在受保护 Checkout 页填写",
|
|
1269
|
-
],
|
|
1270
|
-
next_expectation: "compare:用户选定其一;prepare_checkout:委托仍在时继续到官方确认入口,人完成review与付款",
|
|
1271
|
-
must_convey: [
|
|
1272
|
-
"推荐与备选的时间/费用/便利性取舍",
|
|
1273
|
-
"金额为查询时参考价,下单前须核验",
|
|
1274
|
-
"姓名、证件、手机号只在受保护网页填写",
|
|
1275
|
-
],
|
|
1276
|
-
}
|
|
1277
|
-
: undefined;
|
|
1278
1529
|
switch (expansion) {
|
|
1279
1530
|
case "complete":
|
|
1531
|
+
if (plan.search?.reason === "ground_evidence_required") {
|
|
1532
|
+
return {
|
|
1533
|
+
status: "awaiting_input", result,
|
|
1534
|
+
instruction: "已查到铁路班次,但具体地址的接驳或出站证据不足,无法验证到达时限;剩余站对未继续查询,不能说没有车。向用户说明已查范围,并请其补充准确起终点地址或可验证的接驳/出站信息;取得新事实后再按其要求重新规划,不要默认扩大铁路搜索。",
|
|
1535
|
+
next: null, recovery: [],
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
if (!hasEligible && !plan.result_not_updated) {
|
|
1539
|
+
return {
|
|
1540
|
+
status: "no_result", result,
|
|
1541
|
+
instruction: `本次已查询范围内没有当前合格线路。原因以 notices、coverage 和 search.reason 为准;缺票、接续失败、依赖故障或未覆盖范围不能改说成没有车。已有铁路事实可从 full_result 读取。${changedScopeHint}后续使用 available_actions 中与用户意图一致的动作。`,
|
|
1542
|
+
next: null, recovery: [],
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1280
1545
|
return {
|
|
1281
1546
|
status: "ready",
|
|
1282
1547
|
result,
|
|
1283
1548
|
// §7.3: the lead line carries real combination counts bucketed by
|
|
1284
1549
|
// verified transfer count — seat rows never inflate the journey count.
|
|
1285
|
-
instruction: `${journeyMix ? `本次共${journeyMix}(席别不重复计数)。` : ""}${plan.search?.reason === "budget_exhausted" ? "本次查询已达到上限,以下是已查到的方案,搜索范围尚未全部核验。" : "本轮规划已完成。"}推荐和journeys是摘要;追问其他车次先读取 full_result
|
|
1286
|
-
next:
|
|
1287
|
-
? { command: railJourneySummary(journeys[0], se).select, reason: "用户在 compare 后选定推荐行程时执行;prepare_checkout 且规则命中时用 select_delegated" }
|
|
1288
|
-
: null,
|
|
1289
|
-
...(queryResultsInteraction ? { interaction: queryResultsInteraction } : {}),
|
|
1290
|
-
...(queryResultsCommunication ? { communication: queryResultsCommunication } : {}),
|
|
1550
|
+
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 的已保存完整目录。比较需求说明首选与备选取舍后结束;已有明确代选委托则沿合格线路详情继续,只补缺失的真实同意,身份信息仍在官方页面填写。`,
|
|
1551
|
+
next: null,
|
|
1291
1552
|
recovery: [],
|
|
1292
1553
|
};
|
|
1293
1554
|
case "paused": {
|
|
1555
|
+
if (plan.search?.reason === "legacy_plan_requires_new_search") {
|
|
1556
|
+
return {
|
|
1557
|
+
status: "awaiting_input", result,
|
|
1558
|
+
instruction: "这次规划使用旧算法快照,当前版本已暂停后续查询。已保存的结果仍可读取;若需继续覆盖未查范围,请按原出行条件发起一项新的 Smart 查询。不要把旧快照解释为新版完整结果。",
|
|
1559
|
+
next: null, recovery: [],
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1294
1562
|
// §7.1 case 1: usable directs committed while ranked scope remains. The
|
|
1295
1563
|
// message must carry real counts, and expansion is a user choice — the
|
|
1296
1564
|
// Agent must not treat "expandable" as consent already given.
|
|
@@ -1313,7 +1581,7 @@ function railPlanningEnvelope(model) {
|
|
|
1313
1581
|
// §7.1 case 3 on a recoverable pause: unverified scope is named with real
|
|
1314
1582
|
// counts, and the phrasing never implies the whole nearby range ran.
|
|
1315
1583
|
const partialPause = !directHint && counts && (counts.pairs_total ?? 0) > (counts.pairs_checked ?? 0)
|
|
1316
|
-
?
|
|
1584
|
+
? `已保存${counts.journeys_total ?? 0}个具体车次时刻组合,其中${counts.journeys_eligible ?? "未知"}条当前满足铁路资格;还有${(counts.pairs_total ?? 0) - (counts.pairs_checked ?? 0)}个站对/部分中转路径未核验。以下是已确认结果,不能据此断定没有其它走法。`
|
|
1317
1585
|
: "";
|
|
1318
1586
|
// §S5: a dispatch whose outcome is unknown pauses for reconciliation —
|
|
1319
1587
|
// the message names the state, never rephrases it as a normal pause or
|
|
@@ -1331,21 +1599,29 @@ function railPlanningEnvelope(model) {
|
|
|
1331
1599
|
return {
|
|
1332
1600
|
status: "awaiting_input",
|
|
1333
1601
|
result,
|
|
1334
|
-
instruction: `${modelDegradedHint}${failedPairsHint}${directHint}${transferHint}${partialPause}
|
|
1602
|
+
instruction: `${modelDegradedHint}${failedPairsHint}${directHint}${transferHint}${partialPause}${hasEligible ? "规划已发布可用结果并暂停扩展:展示现有卡片与 available_actions" : `当前已查范围尚未找到合格线路,搜索已暂停;未查范围与故障以coverage/notices为准,不是完整无解。${changedScopeHint}展示 available_actions`},等待用户意图(${expandHint})。ready 后 next 为空表示向用户汇报并等待,不是永远没有更多。比较需求说明首选与备选取舍后结束;已有代选委托且选项合格时沿详情继续到必要确认与官方入口。`,
|
|
1335
1603
|
next: null,
|
|
1336
|
-
...(queryResultsInteraction ? { interaction: queryResultsInteraction } : {}),
|
|
1337
|
-
...(queryResultsCommunication ? { communication: queryResultsCommunication } : {}),
|
|
1338
1604
|
recovery: [],
|
|
1339
1605
|
};
|
|
1340
1606
|
}
|
|
1341
1607
|
case "failed":
|
|
1342
1608
|
case "cancelled":
|
|
1343
1609
|
case "expired": {
|
|
1610
|
+
if (expansion === "failed" && !plan.snapshot_id &&
|
|
1611
|
+
["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 ?? "")) {
|
|
1612
|
+
return {
|
|
1613
|
+
status: "awaiting_input", result,
|
|
1614
|
+
instruction: plan.search?.reason === "station_location_evidence_required"
|
|
1615
|
+
? "尚未查票。车站身份明确,但 Smart 缺少必要地理证据;这是系统证据缺口,不要求用户换站。若双锁站仅需直达,可使用 Exact。"
|
|
1616
|
+
: "尚未查票。受影响端点的名称、归属或坐标尚不能一致确认;只补正该端点的真实信息,保留其余条件,不把此次失败说成无车。",
|
|
1617
|
+
next: null, recovery: [],
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1344
1620
|
// §7.1 case 3: when scope was only partially verified the honest wording
|
|
1345
1621
|
// is "N confirmed + X pairs unverified" — never the case-2 phrasing that
|
|
1346
1622
|
// implies the whole nearby range was checked.
|
|
1347
1623
|
const partial = counts && (counts.pairs_total ?? 0) > (counts.pairs_checked ?? 0)
|
|
1348
|
-
?
|
|
1624
|
+
? `已保存${counts.journeys_total ?? 0}个具体车次时刻组合,其中${counts.journeys_eligible ?? "未知"}条当前满足铁路资格;还有${(counts.pairs_total ?? 0) - (counts.pairs_checked ?? 0)}个站对/部分中转路径未核验。以下是已确认结果,不能据此断定没有其它走法。`
|
|
1349
1625
|
: "";
|
|
1350
1626
|
// §S5: a processing failure with a committed catalog is "saved N
|
|
1351
1627
|
// verified combinations, later expansion/recommend incomplete" — the
|
|
@@ -1383,17 +1659,6 @@ function railPlanningEnvelope(model) {
|
|
|
1383
1659
|
result,
|
|
1384
1660
|
instruction: `${modelDegradedHint}${manualExpansion}${transferHint}${failedPairsHint}规划进行中:已提交的卡片可先向用户展示比较;稍后按 next 增量轮询同一执行(since-snapshot 命中时负载会被压缩,但阶段与计数仍返回最新值)。不要重新发起查询。`,
|
|
1385
1661
|
next: nextPoll,
|
|
1386
|
-
communication: {
|
|
1387
|
-
schema_version: "itpay.communication.v1",
|
|
1388
|
-
status_line: transferSearching && !manualExpansion
|
|
1389
|
-
? "直达范围暂未找到可用方案,中转组合仍在搜索"
|
|
1390
|
-
: "规划进行中,已确认的方案可先向用户展示",
|
|
1391
|
-
next_expectation: "按 next 增量轮询同一执行;不重新发起查询",
|
|
1392
|
-
must_convey: [
|
|
1393
|
-
"当前是进行中的部分结果,不代表最终答复",
|
|
1394
|
-
...(failedPairsHint ? ["部分站对尚未查明,不能说'没有车'"] : []),
|
|
1395
|
-
],
|
|
1396
|
-
},
|
|
1397
1662
|
recovery: [],
|
|
1398
1663
|
};
|
|
1399
1664
|
}
|
|
@@ -1443,26 +1708,9 @@ function servicesNextEnvelope(model) {
|
|
|
1443
1708
|
const currentDelivery = model.current_delivery ?? model.delivery_bindings.at(-1);
|
|
1444
1709
|
const lockedRefund = model.refunds.find((refund) => refund.access_locked);
|
|
1445
1710
|
if (lockedRefund) {
|
|
1446
|
-
const
|
|
1447
|
-
return {
|
|
1448
|
-
|
|
1449
|
-
result: {
|
|
1450
|
-
service_execution_id: execution.service_execution_id,
|
|
1451
|
-
access_locked: true,
|
|
1452
|
-
refund: {
|
|
1453
|
-
refund_request_id: lockedRefund.refund_request_id,
|
|
1454
|
-
status: lockedRefund.status,
|
|
1455
|
-
},
|
|
1456
|
-
},
|
|
1457
|
-
instruction: terminal
|
|
1458
|
-
? "告诉用户退款已由 ItPay 确认成功,原交付永久关闭。Agent 停止读取和跟踪,不再创建授权。"
|
|
1459
|
-
: "告诉用户退款仍在处理,原交付已按政策冻结。然后读取同一退款的权威状态;Agent 不读取交付、不创建授权或重复申请。",
|
|
1460
|
-
next: terminal ? null : {
|
|
1461
|
-
command: `itpay refund get ${lockedRefund.refund_request_id} --json`,
|
|
1462
|
-
reason: "读取退款权威状态",
|
|
1463
|
-
},
|
|
1464
|
-
recovery: [],
|
|
1465
|
-
};
|
|
1711
|
+
const refundState = refundStateEnvelope(lockedRefund, "delivery_locked");
|
|
1712
|
+
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 } : {}) } },
|
|
1713
|
+
next: refundState.next ? { command: `itpay refund get ${lockedRefund.refund_request_id} --json`, reason: "读取同一退款权威状态" } : null };
|
|
1466
1714
|
}
|
|
1467
1715
|
const latestRun = model.execution_requests?.filter((request) => request.execution_kind === "service.execution.run").at(-1);
|
|
1468
1716
|
if (latestRun && ["pending", "started"].includes(latestRun.status)) {
|
|
@@ -1497,10 +1745,48 @@ function servicesNextEnvelope(model) {
|
|
|
1497
1745
|
}
|
|
1498
1746
|
if (model.workflow_entry && !["completed", "delivery"].includes(model.workflow?.status ?? "")) {
|
|
1499
1747
|
const id = execution.service_execution_id;
|
|
1748
|
+
const workflowReason = model.workflow?.failure?.reason_code ?? model.workflow?.error_code;
|
|
1500
1749
|
const paymentVerified = model.payment_bindings.some((binding) => binding.status === "payment_verified") || model.checkout_bindings.some((binding) => binding.status === "payment_verified");
|
|
1501
1750
|
const state = model.workflow?.status === "payment" && paymentVerified ? "running" : model.workflow?.status ?? "input_required";
|
|
1502
|
-
if (
|
|
1503
|
-
|
|
1751
|
+
if (["quota_paused", "failed"].includes(state) && workflowReason !== "area_scope_too_broad" && (model.workflow?.dependency ||
|
|
1752
|
+
/^amap_/.test(workflowReason ?? ""))) {
|
|
1753
|
+
const dependency = model.workflow?.dependency;
|
|
1754
|
+
const resumable = state === "quota_paused" && dependency?.retryable === true;
|
|
1755
|
+
return {
|
|
1756
|
+
status: "dependency_unavailable",
|
|
1757
|
+
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 } : {}) },
|
|
1758
|
+
instruction: resumable
|
|
1759
|
+
? "地图服务暂时限流。保留原条件及已解析端点;到 retry_at 后执行下一步,继续同一任务,不重新提交地点。"
|
|
1760
|
+
: "地图服务本轮未完成解析,这是外部依赖故障,不要求用户换地址或自行找坐标。有界尝试已结束;保留原条件及已有结果,交由服务维护方处理该错误。",
|
|
1761
|
+
next: resumable ? { command: `itpay services run ${execution.service_id} --execution ${id} --json`, reason: "等待所示恢复时间后继续同一执行" } : null,
|
|
1762
|
+
recovery: [],
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
if (state === "failed" && workflowReason === "area_scope_too_broad") {
|
|
1766
|
+
return { status: "awaiting_input", result: { service_execution_id: id, reason: "area_scope_too_broad", failure: model.workflow?.failure, affected_endpoint: model.workflow?.failure?.endpoint ?? model.workflow?.dependency?.endpoint, input_template: { endpoints: { [model.workflow?.dependency?.endpoint ?? "<受影响侧>"]: { kind: "area", text: "<市县镇完整名称>" } } } },
|
|
1767
|
+
instruction: "区域范围是省或国家。只补充受影响端点的市、县或镇;保留另一端、日期与用户约束。修正模板:endpoints.<受影响侧>={kind:area,text:<市县镇完整名称>}。",
|
|
1768
|
+
next: null, recovery: [] };
|
|
1769
|
+
}
|
|
1770
|
+
if (state === "failed" && ["address_required", "administrative_area_unresolved", "station_location_evidence_required", "coordinate_identity_unverified"].includes(workflowReason ?? "")) {
|
|
1771
|
+
const code = workflowReason;
|
|
1772
|
+
return {
|
|
1773
|
+
status: "awaiting_input",
|
|
1774
|
+
result: { service_execution_id: id, service_id: execution.service_id, reason: code, affected_endpoint: model.workflow?.failure?.endpoint, failure: model.workflow?.failure,
|
|
1775
|
+
...(["address_required", "administrative_area_unresolved"].includes(code) ? { new_query_template: {
|
|
1776
|
+
command: `itpay services run ${execution.service_id} --input-json <补全原始需求.json> --json`, executable: false,
|
|
1777
|
+
required_input: ["仅补受影响端点", "原始日期人数时限偏好"],
|
|
1778
|
+
instruction: "原执行输入已锁定;按原需求保留另一端与所有约束,只补缺失信息后提交新输入,不在旧执行伪造修改。"
|
|
1779
|
+
} } : {}) },
|
|
1780
|
+
instruction: code === "station_location_evidence_required"
|
|
1781
|
+
? "尚未查票。车站身份已知,Smart 缺少车站坐标证据;请说明此系统缺口,不要求用户换站。双锁站直达可另用 Exact。"
|
|
1782
|
+
: code === "coordinate_identity_unverified"
|
|
1783
|
+
? "尚未查票。名称与提交坐标缺少一致证据;请核实受影响端点的准确地点,保留其余出行条件。"
|
|
1784
|
+
: "尚未查票。地点缺少必要地址或行政归属;只请用户补充受影响端点,保留另一端及原条件。",
|
|
1785
|
+
next: null, recovery: [],
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
if (state === "failed" && ["login_required", "rate_limited"].includes(workflowReason ?? "")) {
|
|
1789
|
+
const login = workflowReason === "login_required";
|
|
1504
1790
|
return {
|
|
1505
1791
|
status: login ? "login_required" : "rate_limited",
|
|
1506
1792
|
result: { service_execution_id: id, service_id: execution.service_id },
|
|
@@ -1538,29 +1824,42 @@ function servicesNextEnvelope(model) {
|
|
|
1538
1824
|
return {
|
|
1539
1825
|
side,
|
|
1540
1826
|
status: place?.resolution_status,
|
|
1827
|
+
...(place?.resolution_reason ? { reason: place.resolution_reason } : {}),
|
|
1541
1828
|
...(place?.resolution_status === "resolved"
|
|
1542
|
-
? { resolved_place: { name: name(place), location: place.location } }
|
|
1829
|
+
? { resolved_place: { name: name(place), address: place.formatted_address, location: place.location } }
|
|
1543
1830
|
: {}),
|
|
1544
1831
|
...(candidates.length
|
|
1545
|
-
? { candidates: candidates.slice(0,
|
|
1832
|
+
? { 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 })) }
|
|
1546
1833
|
: {}),
|
|
1547
1834
|
};
|
|
1548
1835
|
});
|
|
1549
1836
|
return {
|
|
1550
1837
|
status: "confirmation_required",
|
|
1551
|
-
result: { service_execution_id: id, service_id: execution.service_id,
|
|
1552
|
-
|
|
1553
|
-
|
|
1838
|
+
result: { service_execution_id: id, service_id: execution.service_id,
|
|
1839
|
+
human_action: { action_type: action.action_type, input_schema: action.input_schema },
|
|
1840
|
+
required_fields: requiredFields, sides,
|
|
1841
|
+
...(sides.some(side => side.status !== "resolved" && !side.candidates?.length && ["ADDRESS_REQUIRED", "ADMINISTRATIVE_AREA_UNRESOLVED"].includes(side.reason ?? "")) ? {
|
|
1842
|
+
new_query_template: { command: `itpay services run ${execution.service_id} --input-json <补全原始需求.json> --json`, executable: false,
|
|
1843
|
+
required_input: ["补缺失端点", "保留原始日期人数时限偏好及已确认端点"], instruction: "原执行锁定输入;补全原需求后用新查询,不提交不存在的候选。" },
|
|
1844
|
+
} : {}) },
|
|
1845
|
+
instruction: sides.some(side => side.status !== "resolved" && !side.candidates?.length)
|
|
1846
|
+
? locationConfirmationEnvelope(id, "", {}, { endpoints: sides.filter(side => side.status !== "resolved") }).instruction
|
|
1847
|
+
: requiredFields.includes("choices")
|
|
1848
|
+
? "尚未查票。仅向用户展示有真实歧义端点的候选编号、名称、行政地址与类型(内部 id 仅用于回填,不展示给人);用户选定后,把各未解析端点的候选 id 放入 choices 对象,继续同一执行。已解析端点无需再选;不得替用户决定,也不是购买确认。"
|
|
1849
|
+
: "向用户展示有歧义一端的候选;按 required_fields 填写真实选择,已解析一端保留原值。继续同一执行;这不是购买确认。",
|
|
1554
1850
|
next: null,
|
|
1555
|
-
interaction: {
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1851
|
+
...(sides.some(side => side.status !== "resolved" && !side.candidates?.length) ? {} : { interaction: {
|
|
1852
|
+
schema_version: "itpay.interaction.v1",
|
|
1853
|
+
stage: "location_confirmation_required",
|
|
1854
|
+
input_template: {
|
|
1855
|
+
command: requiredFields.includes("choices")
|
|
1856
|
+
? `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved --input-json <地点确认.json> --json`
|
|
1857
|
+
: `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved${requiredFields.map((field) => ` --input ${field}=<值>`).join("")} --json`,
|
|
1858
|
+
input: { choices: Object.fromEntries(sides.filter(side => side.candidates?.length).map(side => [side.side, "<所选候选id>"])) },
|
|
1859
|
+
required_input: requiredFields,
|
|
1860
|
+
executable: false,
|
|
1861
|
+
},
|
|
1862
|
+
} }),
|
|
1564
1863
|
recovery: [],
|
|
1565
1864
|
};
|
|
1566
1865
|
}
|
|
@@ -1576,24 +1875,22 @@ function servicesNextEnvelope(model) {
|
|
|
1576
1875
|
: undefined;
|
|
1577
1876
|
return {
|
|
1578
1877
|
status: "booking_review_required",
|
|
1579
|
-
result: { service_execution_id: id, service_id: execution.service_id,
|
|
1580
|
-
required_fields: requiredFields,
|
|
1878
|
+
result: { service_execution_id: id, service_id: execution.service_id, review,
|
|
1879
|
+
action_type: action.action_type, required_fields: requiredFields,
|
|
1581
1880
|
...(requirementsRemaining ? { requirements_remaining: requirementsRemaining } : {}) },
|
|
1582
1881
|
...(reviewURL ? { handoff: { url: reviewURL, kind: "booking_review" } } : {}),
|
|
1583
|
-
instruction: reviewURL
|
|
1584
|
-
? "已在 handoff.url 提供官方行程确认页,请用户在该页核对行程、席别与座位偏好并确认;页面打开请求不代表用户已确认。该页失效或用户更愿在对话中确认时,仍可按下述 input_template 一次合并提交。乘客姓名/证件等身份信息不在此提交,仍走受保护 Checkout 页。"
|
|
1585
|
-
: "向用户展示 context.review 中的行程(legs)与可选席别/座位偏好词表(seat_options),并明确告知:座位偏好仅为请求、不保证分配(对应 notice_version 文案须经用户同意)。这是购买前的合并确认:把乘客人数、席别与每位乘客的座位偏好一次问清,然后按 required_fields 组装完整 JSON 对象,经 --input-json <file> 提交——draft_revision 必须等于服务端当前值,被拒绝(booking_review_changed)时重新读取后重试。未得真实确认不得提交,不能暗设 accept_non_guaranteed。乘客姓名/证件等身份信息不在此提交,仍走受保护 Checkout 页。",
|
|
1882
|
+
instruction: `${reviewURL ? "官方确认页见 handoff.url,打开不等于确认;沿用已知人数和席别。" : "当前草稿见 review。"} 对照对话只补缺失选择或真实条款同意;无字母偏好自动分配,不默认问字母。仅代码修正且车次、实际席别、人数、价格及条款不变,沿用已有明确确认;实质变化只核对变化。提交当前 draft_revision/notice_version 及必要字段,未得真实同意不能填 accept_non_guaranteed;座位请求不保证分配。身份信息只在官方 Checkout 页填写。`,
|
|
1586
1883
|
next: null,
|
|
1587
1884
|
interaction: {
|
|
1588
1885
|
schema_version: "itpay.interaction.v1",
|
|
1589
1886
|
stage: "booking_review_required",
|
|
1590
1887
|
input_template: {
|
|
1591
1888
|
command: `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved --input-json <file> --json`,
|
|
1592
|
-
required_input:
|
|
1889
|
+
required_input: ["file", ...requiredFields],
|
|
1593
1890
|
executable: false,
|
|
1594
1891
|
},
|
|
1595
1892
|
},
|
|
1596
|
-
recovery: [
|
|
1893
|
+
recovery: [],
|
|
1597
1894
|
};
|
|
1598
1895
|
}
|
|
1599
1896
|
return {
|
|
@@ -1626,8 +1923,8 @@ function servicesNextEnvelope(model) {
|
|
|
1626
1923
|
executable: false,
|
|
1627
1924
|
}
|
|
1628
1925
|
: undefined;
|
|
1629
|
-
const guidance = railServiceGuidance(execution.service_id);
|
|
1630
|
-
const failedStep = recovery ?
|
|
1926
|
+
const guidance = railServiceGuidance(execution.service_id, model.workflow_entry?.input_schema);
|
|
1927
|
+
const failedStep = recovery ? model.workflow?.failure?.step_id : undefined;
|
|
1631
1928
|
const failedStepGuidance = failedStep ? WORKFLOW_STEP_GUIDANCE[failedStep] : undefined;
|
|
1632
1929
|
const failedInvocation = recovery
|
|
1633
1930
|
? [...model.provider_invocations].reverse().find((item) => typeof item.status === "string" && item.status.startsWith("failed"))
|
|
@@ -1640,13 +1937,14 @@ function servicesNextEnvelope(model) {
|
|
|
1640
1937
|
quote_seat_unavailable: "所选席别当前无足够余票;告知用户并由其决定是否换方案。",
|
|
1641
1938
|
quote_refresh_expired: "实时报价过程超时;保留原选择,不把旧价当可付款价格。",
|
|
1642
1939
|
}[providerErrorCode ?? ""] : undefined;
|
|
1940
|
+
const failureCode = model.workflow?.failure?.reason_code ?? providerErrorCode ?? model.workflow?.error_code;
|
|
1643
1941
|
const failedInstruction = recovery
|
|
1644
1942
|
? quoteFailure
|
|
1645
1943
|
? quoteFailure
|
|
1646
1944
|
: failedStepGuidance
|
|
1647
|
-
? `执行在「${failedStepGuidance.meaning}
|
|
1945
|
+
? `执行在「${failedStepGuidance.meaning}」步失败${failureCode ? `(错误码:${failureCode})` : ""}:${failedStepGuidance.hint}。先核对原因和当前执行状态,不要盲目重放。`
|
|
1648
1946
|
: state === "failed"
|
|
1649
|
-
? `执行已失败${
|
|
1947
|
+
? `执行已失败${failureCode ? `(错误码:${failureCode})` : ""}且不可续用;核对服务端原因、订单与付款事实后再决定下一步。`
|
|
1650
1948
|
: "执行未完成,请按步骤错误处理;不要重建执行或重复调用。"
|
|
1651
1949
|
: undefined;
|
|
1652
1950
|
return {
|
|
@@ -1654,13 +1952,15 @@ function servicesNextEnvelope(model) {
|
|
|
1654
1952
|
result: {
|
|
1655
1953
|
service_execution_id: id,
|
|
1656
1954
|
service_id: execution.service_id,
|
|
1657
|
-
|
|
1955
|
+
workflow_state: state,
|
|
1956
|
+
...(model.workflow ? { current_step: model.workflow.current_step, workflow_revision: model.workflow.revision } : {}),
|
|
1957
|
+
...(model.workflow?.failure ? { failure: model.workflow.failure } : {}),
|
|
1658
1958
|
...(state === "input_required" ? { input_schema: model.workflow_entry.input_schema } : {}),
|
|
1659
1959
|
...(guidance && state === "input_required" ? { guidance } : {}),
|
|
1660
1960
|
...(failedStep ? { failed_step: failedStep, ...(failedStepGuidance ? { failed_step_meaning: failedStepGuidance.meaning } : {}) } : {}),
|
|
1661
1961
|
...(providerErrorCode ? { provider_error_code: providerErrorCode } : {}),
|
|
1662
1962
|
...(providerErrorCode && !quoteFailure ? { diagnostic_code: providerErrorCode } : {}),
|
|
1663
|
-
...(recovery ? { retryable:
|
|
1963
|
+
...(recovery ? { retryable: false } : {}),
|
|
1664
1964
|
},
|
|
1665
1965
|
instruction: failedInstruction
|
|
1666
1966
|
?? (state === "payment"
|
|
@@ -1670,7 +1970,7 @@ function servicesNextEnvelope(model) {
|
|
|
1670
1970
|
: guidance
|
|
1671
1971
|
? "按 result.guidance 的字段契约填写输入后继续同一服务执行;不要臆造字段名。"
|
|
1672
1972
|
: "继续读取同一执行;缺少输入时按服务声明补齐。"),
|
|
1673
|
-
next: recovery ? null : { command, reason: "继续当前流程" },
|
|
1973
|
+
next: recovery || inputTemplate ? null : { command, reason: "继续当前流程" },
|
|
1674
1974
|
...(inputTemplate || (state === "payment" && model.workflow?.human_action?.context?.review) ? {
|
|
1675
1975
|
interaction: {
|
|
1676
1976
|
schema_version: "itpay.interaction.v1",
|
|
@@ -1685,11 +1985,8 @@ function servicesNextEnvelope(model) {
|
|
|
1685
1985
|
} : {}),
|
|
1686
1986
|
},
|
|
1687
1987
|
} : {}),
|
|
1688
|
-
recovery: recovery && state === "failed"
|
|
1689
|
-
? [
|
|
1690
|
-
{ command: `itpay services start ${execution.service_id} --json`, reason: "修正输入后重新发起(本执行已终止不能续用)" },
|
|
1691
|
-
...(guidance ? [{ command: "itpay docs show rail-booking --json", reason: "查看本服务输入字段契约与示例" }] : []),
|
|
1692
|
-
]
|
|
1988
|
+
recovery: recovery && state === "failed"
|
|
1989
|
+
? [{ command: `itpay services get ${id} --json`, reason: "核对本执行的错误、结果和付款事实" }]
|
|
1693
1990
|
: [],
|
|
1694
1991
|
};
|
|
1695
1992
|
}
|
|
@@ -1706,6 +2003,26 @@ function servicesNextEnvelope(model) {
|
|
|
1706
2003
|
}
|
|
1707
2004
|
const delivery = currentDelivery;
|
|
1708
2005
|
const deliveryMode = serviceDeliveryMode(model);
|
|
2006
|
+
if (execution.service_id === "itpay-rail-exact" && currentItems.length > 0) {
|
|
2007
|
+
const item = currentItems[0];
|
|
2008
|
+
const container = (item.safe_payload?.result ?? item.safe_payload);
|
|
2009
|
+
const total = typeof container?.catalog_total === "number" ? container.catalog_total :
|
|
2010
|
+
Array.isArray(container?.candidates) ? container.candidates.length : 0;
|
|
2011
|
+
return {
|
|
2012
|
+
status: total > 0 ? "result_ready" : "no_result",
|
|
2013
|
+
result: { service_execution_id: execution.service_execution_id, scope: "station_pair",
|
|
2014
|
+
resolved_station_pair: container?.resolved_station_pair, total,
|
|
2015
|
+
result_item_id: item.service_capability_result_item_id,
|
|
2016
|
+
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)) : [],
|
|
2017
|
+
...(exactSmartTemplate(container?.resolved_station_pair, execution.service_execution_id, item.service_capability_result_item_id)
|
|
2018
|
+
? { smart_template: exactSmartTemplate(container?.resolved_station_pair, execution.service_execution_id, item.service_capability_result_item_id) } : {}) },
|
|
2019
|
+
instruction: total > 0
|
|
2020
|
+
? "直达站对结果已保存。执行 next.command 一次读取紧凑车次行,按用户条件筛选;若只需比较,答复后结束;已有购票委托则读取所选行详情。"
|
|
2021
|
+
: "这一已解析站对返回 0 条;不代表全城无车。若原需求需要更广覆盖,可核验另一可信站对或 Smart。",
|
|
2022
|
+
next: { command: `itpay services read-result ${execution.service_execution_id} --json`, reason: "读取这一保存站对的紧凑分页;按返回的续页覆盖所需范围" },
|
|
2023
|
+
recovery: [],
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
1709
2026
|
const candidateSelection = model.allowed_actions?.find((action) => action.type === "select_candidate");
|
|
1710
2027
|
if (candidateSelection && currentItems.length > 0) {
|
|
1711
2028
|
const paidCapability = delivery?.capability_id
|
|
@@ -1918,21 +2235,7 @@ function grantedResultEnvelope(response, orderID) {
|
|
|
1918
2235
|
};
|
|
1919
2236
|
}
|
|
1920
2237
|
function servicesNextPlainResult(result) {
|
|
1921
|
-
|
|
1922
|
-
for (const [key, value] of Object.entries(result)) {
|
|
1923
|
-
if (key === "items" && Array.isArray(value)) {
|
|
1924
|
-
lines.push("items:");
|
|
1925
|
-
for (const item of value) {
|
|
1926
|
-
lines.push(` ${item.rank}. ${item.title}`);
|
|
1927
|
-
for (const [field, fieldValue] of Object.entries(item.safe_payload)) {
|
|
1928
|
-
lines.push(` ${field}: ${typeof fieldValue === "string" ? fieldValue : JSON.stringify(fieldValue)}`);
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
continue;
|
|
1932
|
-
}
|
|
1933
|
-
lines.push(`${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
|
|
1934
|
-
}
|
|
1935
|
-
return lines;
|
|
2238
|
+
return plainValueLines(result);
|
|
1936
2239
|
}
|
|
1937
2240
|
export async function runServicesEvents(backend, serviceExecutionID, options = {}) {
|
|
1938
2241
|
const afterSequence = options.afterSequence ?? 0;
|
|
@@ -1963,11 +2266,8 @@ export async function runServicesEvents(backend, serviceExecutionID, options = {
|
|
|
1963
2266
|
returned_count: events.length,
|
|
1964
2267
|
events,
|
|
1965
2268
|
},
|
|
1966
|
-
instruction: "
|
|
1967
|
-
next:
|
|
1968
|
-
command: `itpay services next ${serviceExecutionID} --json`,
|
|
1969
|
-
reason: "恢复正常服务流程",
|
|
1970
|
-
},
|
|
2269
|
+
instruction: "这是已保存的诊断事件;事件不代表车票结果,也不授权重放业务步骤。已读完所需诊断时停止;实际任务沿先前返回的当前动作继续。",
|
|
2270
|
+
next: null,
|
|
1971
2271
|
recovery: events.length === limit && events.length > 0
|
|
1972
2272
|
? [{
|
|
1973
2273
|
command: `itpay services events ${serviceExecutionID} --after-sequence ${events.at(-1).sequence} --limit ${limit} --json`,
|
|
@@ -2043,7 +2343,7 @@ function buildServicesCheckoutEnvelope(response, checkoutURL, plan, agentType, t
|
|
|
2043
2343
|
service_execution_id: response.binding.service_execution_id,
|
|
2044
2344
|
checkout_id: checkout.checkout.checkout_id,
|
|
2045
2345
|
capability_id: checkoutCapabilityID(response),
|
|
2046
|
-
|
|
2346
|
+
items: plan.orderItems ?? [],
|
|
2047
2347
|
amount,
|
|
2048
2348
|
...(checkout.checkout.expires_at ? {
|
|
2049
2349
|
// The shared server-owned payable deadline D: refreshing the page or
|
|
@@ -2141,10 +2441,7 @@ export async function runServicesRun(backend, config, serviceID, input, options
|
|
|
2141
2441
|
const paid = () => model.payment_bindings.some((binding) => binding.status === "payment_verified") || model.checkout_bindings.some((binding) => binding.status === "payment_verified");
|
|
2142
2442
|
// A planning projection can exist before any usable result or user action.
|
|
2143
2443
|
// Keep the existing bounded wait until there is something to hand over.
|
|
2144
|
-
|
|
2145
|
-
["complete", "paused", "failed", "cancelled", "expired"].includes(model.rail_planning.search?.expansion_status ?? "") ||
|
|
2146
|
-
model.rail_planning.available_actions?.some((action) => ["select_journey", "expand_search"].includes(action.type))));
|
|
2147
|
-
while ((["queued", "running", "delivery"].includes(model.workflow?.status ?? "") || (model.workflow?.status === "payment" && paid())) && !model.current_delivery && !planningDeliverable() && Date.now() < until) {
|
|
2444
|
+
while ((["queued", "running", "delivery"].includes(model.workflow?.status ?? "") || (model.workflow?.status === "payment" && paid())) && shouldWaitForServiceResult(model) && Date.now() < until) {
|
|
2148
2445
|
await sleep(options.pollIntervalMS ?? 1500);
|
|
2149
2446
|
model = await backend.getServiceExecution(id);
|
|
2150
2447
|
}
|
|
@@ -2168,11 +2465,11 @@ export async function runServicesRun(backend, config, serviceID, input, options
|
|
|
2168
2465
|
}
|
|
2169
2466
|
if (cause instanceof CommandContractError || cause instanceof HttpError || cause instanceof DeviceLockBusyError || cause instanceof DeviceStateError)
|
|
2170
2467
|
throw cause;
|
|
2171
|
-
if (
|
|
2172
|
-
throw new CommandContractError("workflow_start_outcome_unknown", cause.message, "创建服务执行时没有收到完整响应。先查询当前身份可见的执行;不要直接重跑并创建替代执行。", [{ command: "itpay services list --json", reason: "查找可能已经创建的服务执行" }]);
|
|
2468
|
+
if (!id) {
|
|
2469
|
+
throw new CommandContractError("workflow_start_outcome_unknown", cause instanceof Error ? cause.message : "service creation outcome unknown", "创建服务执行时没有收到完整响应。先查询当前身份可见的执行;不要直接重跑并创建替代执行。", [{ command: "itpay services list --json", reason: "查找可能已经创建的服务执行" }]);
|
|
2173
2470
|
}
|
|
2174
2471
|
if (cause instanceof HttpTransportError)
|
|
2175
|
-
throw cause;
|
|
2176
|
-
throw new CommandContractError("workflow_run_failed", cause instanceof Error ? cause.message : "workflow run failed", "保留当前执行并按错误处理,不要重复创建服务执行。", [{ command: `itpay services
|
|
2472
|
+
throw new CommandContractError("workflow_outcome_unknown", cause.message, "未收到完整响应;只读同一执行的权威事实,不重放写操作或创建替代执行。", [{ command: `itpay services next ${id} --json`, reason: "核对同一执行" }]);
|
|
2473
|
+
throw new CommandContractError("workflow_run_failed", cause instanceof Error ? cause.message : "workflow run failed", "保留当前执行并按错误处理,不要重复创建服务执行。", [{ command: `itpay services next ${id} --json`, reason: "读取同一执行的当前事实与恢复动作" }]);
|
|
2177
2474
|
}
|
|
2178
2475
|
}
|