@itpay/cli 2.1.29 → 2.1.34
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/dist/src/client/backend.js +4 -1
- package/dist/src/client/http.js +2 -0
- package/dist/src/commands/buy.js +3 -1
- package/dist/src/commands/checkout.js +1 -0
- package/dist/src/commands/checkout_handoff.js +7 -4
- package/dist/src/commands/guidance.js +3 -1
- package/dist/src/commands/install.js +2 -1
- package/dist/src/commands/services.js +244 -106
- package/dist/src/commands/skill.js +3 -0
- package/dist/src/main.js +4 -4
- package/dist/src/render/qr.js +1 -1
- package/dist/src/state/account_auth.js +6 -3
- package/dist/src/state/config.js +4 -4
- package/dist/src/state/device_authority.js +7 -5
- package/docs/agent/buyer/cart-checkout.json +1 -1
- package/docs/agent/buyer/orders-refunds.json +1 -0
- package/docs/agent/buyer/payment-flow.json +1 -0
- package/docs/agent/buyer/quickstart.json +1 -1
- package/docs/agent/buyer/rail-booking.json +25 -21
- package/docs/agent/buyer/rail-fast-checkout.json +6 -3
- package/docs/agent/buyer/render-hosts.json +1 -1
- package/package.json +5 -1
- package/skills/itpay/SKILL.md +40 -16
|
@@ -34,9 +34,10 @@ function journalTaskState(serviceExecutionID, envelope, env) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
const serviceActionStatuses = new Set(["pending", "approved", "rejected", "expired", "cancelled"]);
|
|
37
|
+
const RAIL_ENDPOINT_BRIEF = "查询前先留下2–4行端点理解摘要:两端各指哪里、是车站/行政范围/具体地点、依据及未确定事实,再说明日期/人数/时限与Exact/Smart选择。对话或现有route-brief.txt二选一;已有摘要沿用,只更新变化项。含义不清先澄清,定位待核验不等于含义不清;勿凭字尾猜类型。";
|
|
37
38
|
const RAIL_SERVICE_GUIDANCE = {
|
|
38
39
|
"itpay-rail-exact": {
|
|
39
|
-
when_to_use: "
|
|
40
|
+
when_to_use: "已知可信站对时先用本服务核验直达;城市需求可先形成有依据的站对假设。只覆盖这一站对,不代表全城市最优。只有明确可靠路线知识可直接用;大概率判断先短查,受阻且清晰双站可Exact核验。查询前记录两端含义、类别、依据、未知事实和Exact/Smart选择;格式见已读rail-booking。",
|
|
40
41
|
input_fields: [
|
|
41
42
|
{ name: "origin", required: true, description: "已核实出发铁路站完整名称,不能以同名站代替区域", example: "<已核实出发站>" },
|
|
42
43
|
{ name: "destination", required: true, description: "已核实到达铁路站完整名称", example: "<已核实到达站>" },
|
|
@@ -49,7 +50,7 @@ const RAIL_SERVICE_GUIDANCE = {
|
|
|
49
50
|
],
|
|
50
51
|
},
|
|
51
52
|
"itpay-rail-smart": {
|
|
52
|
-
when_to_use: "需要广泛站点比较、中转或具体地址接驳时使用;有可信站对且只需先核验直达可用 Exact
|
|
53
|
+
when_to_use: "需要广泛站点比较、中转或具体地址接驳时使用;有可信站对且只需先核验直达可用 Exact。只有明确可靠路线知识可直接用;大概率判断先短查,受阻且清晰双站可Exact核验。查询前记录两端含义、类别、依据、未知事实和Exact/Smart选择;格式见已读rail-booking。",
|
|
53
54
|
input_fields: [
|
|
54
55
|
{ name: "origin", required: true, description: "出发地:已知最完整的位置名或区域(城市/区县/地址均可)", example: "<原始完整地点>" },
|
|
55
56
|
{ name: "destination", required: true, description: "目的地:与 origin 同样的位置规则", example: "<已核实到达站>" },
|
|
@@ -79,21 +80,67 @@ const RAIL_SERVICE_GUIDANCE = {
|
|
|
79
80
|
};
|
|
80
81
|
function railServiceGuidance(serviceID, schema) {
|
|
81
82
|
const guidance = RAIL_SERVICE_GUIDANCE[serviceID];
|
|
82
|
-
if (serviceID !== "itpay-rail-smart" || !guidance
|
|
83
|
+
if (serviceID !== "itpay-rail-smart" || !guidance)
|
|
83
84
|
return guidance;
|
|
85
|
+
const properties = schema?.properties;
|
|
86
|
+
if (!properties?.endpoints)
|
|
87
|
+
return properties?.origin && properties?.destination ? guidance : undefined;
|
|
84
88
|
return {
|
|
85
89
|
...guidance,
|
|
86
90
|
input_fields: [
|
|
87
|
-
{ name: "endpoints", required: true, description: "origin/destination
|
|
91
|
+
{ name: "endpoints", required: true, description: "origin/destination 各填完整text;类别有依据时填kind(area/place/station)。仅对象含义明确、类别确实待核验时可省略kind;完整名称合法,坐标可选,不靠省略kind猜城市" },
|
|
88
92
|
{ name: "travel_date", required: true, description: "用户出行日期 YYYY-MM-DD" },
|
|
89
93
|
],
|
|
90
94
|
optional_fields: (guidance.optional_fields ?? []).filter((field) => !["origin_city", "destination_city", "origin_location", "destination_location"].includes(field.name)).concat([
|
|
91
95
|
{ name: "reuse_from", description: "同 owner 的已保存 Exact result_item_id 或 Smart snapshot_id;端点/日期新查询可复用有效事实" },
|
|
92
|
-
]),
|
|
93
|
-
input_example: { endpoints: { origin: { kind: "
|
|
94
|
-
notes: ["仅用当前 input_schema 支持的端点写法;不要与旧 origin/destination 混用", "
|
|
96
|
+
]).filter(field => properties[field.name] !== undefined),
|
|
97
|
+
input_example: { endpoints: { origin: { kind: "place", text: "<用户原始完整具体地点>" }, destination: { kind: "area", text: "<用户原始完整行政范围>" } }, travel_date: "<用户出行日期 YYYY-MM-DD>" },
|
|
98
|
+
notes: ["仅用当前 input_schema 支持的端点写法;不要与旧 origin/destination 混用", "唯一地点直接解析;真实候选只确认有歧义一端;地图等待按可续用状态恢复,终态有新证据才用返回新查询模板,无动作则停止"],
|
|
95
99
|
};
|
|
96
100
|
}
|
|
101
|
+
// Generate only declared required fields and preserve safe known query facts.
|
|
102
|
+
// Authorization and passenger identity are never synthesized into templates.
|
|
103
|
+
function serviceInputTemplate(schema, known = {}) {
|
|
104
|
+
const properties = schema.properties;
|
|
105
|
+
if (properties?.endpoints && !properties.origin && !properties.destination && !known.endpoints && (typeof known.origin === "string" || typeof known.destination === "string")) {
|
|
106
|
+
known = { ...known, endpoints: { origin: { text: known.origin }, destination: { text: known.destination } } };
|
|
107
|
+
}
|
|
108
|
+
if (properties?.travel_date && !known.travel_date && typeof known.date === "string")
|
|
109
|
+
known = { ...known, travel_date: known.date };
|
|
110
|
+
const result = {};
|
|
111
|
+
for (const field of new Set([...requiredInputFields(schema), ...Object.keys(known).filter(field => properties?.[field] !== undefined)])) {
|
|
112
|
+
const rule = properties?.[field] ?? {};
|
|
113
|
+
if (field === "accept_non_guaranteed") {
|
|
114
|
+
result[field] = false;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (rule.type === "object") {
|
|
118
|
+
const value = known[field];
|
|
119
|
+
result[field] = serviceInputTemplate(rule, value && typeof value === "object" && !Array.isArray(value) ? value : {});
|
|
120
|
+
}
|
|
121
|
+
else if (known[field] !== undefined && (rule.type === undefined || rule.type === "array" && Array.isArray(known[field]) || rule.type === "string" && typeof known[field] === "string" || rule.type === "boolean" && typeof known[field] === "boolean" || ["integer", "number"].includes(String(rule.type)) && typeof known[field] === "number"))
|
|
122
|
+
result[field] = known[field];
|
|
123
|
+
else if (rule.const !== undefined)
|
|
124
|
+
result[field] = rule.const;
|
|
125
|
+
else if (rule.default !== undefined)
|
|
126
|
+
result[field] = rule.default;
|
|
127
|
+
else if (Array.isArray(rule.enum) && rule.enum.length)
|
|
128
|
+
result[field] = rule.enum[0];
|
|
129
|
+
else if (rule.type === "integer" || rule.type === "number")
|
|
130
|
+
result[field] = typeof rule.minimum === "number" ? rule.minimum : 1;
|
|
131
|
+
else if (rule.type === "boolean")
|
|
132
|
+
result[field] = false;
|
|
133
|
+
else if (rule.type === "array")
|
|
134
|
+
result[field] = [];
|
|
135
|
+
else
|
|
136
|
+
result[field] = `<${field}>`;
|
|
137
|
+
}
|
|
138
|
+
for (const field of ["kind", "text", "station_code", "poi_id", "adcode", "location", "travel_date", "passengers", "priority", "user_query", "depart_after", "depart_before", "arrive_by", "arrive_before", "seat_preferences", "max_transfers", "duration_basis"]) {
|
|
139
|
+
if (properties?.[field] && known[field] !== undefined)
|
|
140
|
+
result[field] = known[field];
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
97
144
|
const WORKFLOW_STEP_GUIDANCE = {
|
|
98
145
|
input: { meaning: "输入校验", hint: "核对本服务的输入契约和服务端错误码;本执行的输入不能凭猜测修复" },
|
|
99
146
|
quota: { meaning: "额度检查", hint: "查看实际额度及授权状态;只按返回的登录或等待动作处理" },
|
|
@@ -119,17 +166,22 @@ export async function runServicesStart(backend, serviceID, options = {}) {
|
|
|
119
166
|
});
|
|
120
167
|
if (response.workflow_entry) {
|
|
121
168
|
const guidance = railServiceGuidance(serviceID, response.workflow_entry.input_schema);
|
|
169
|
+
if (serviceID === "itpay-rail-smart" && !guidance) {
|
|
170
|
+
writeCommandEnvelope({ status: "contract_unavailable", result: { service_execution_id: response.execution.service_execution_id, service_id: serviceID }, instruction: "当前执行缺少可用输入合同;只读取一次同执行合同,仍缺失则报告系统缺口并停止,不猜旧Smart写法。", next: { command: `itpay services get ${response.execution.service_execution_id} --json`, reason: "读取当前执行锁定的输入合同一次" }, recovery: [] }, options);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
122
173
|
writeCommandEnvelope({
|
|
123
174
|
status: "input_required",
|
|
124
175
|
result: { service_execution_id: response.execution.service_execution_id, service_id: serviceID, input_schema: response.workflow_entry.input_schema, ...(guidance ? { guidance } : {}) },
|
|
125
|
-
instruction: guidance ? "按 result.guidance 逐项填写输入(when_to_use 说明本服务适用场景、input_fields 是必填契约、input_example 是待填示意,必须替换用户真实值),然后继续同一服务执行。不要臆造字段名。" : "根据服务声明填写输入,然后继续同一服务执行。",
|
|
176
|
+
instruction: guidance ? RAIL_ENDPOINT_BRIEF + "按 result.guidance 逐项填写输入(when_to_use 说明本服务适用场景、input_fields 是必填契约、input_example 是待填示意,必须替换用户真实值),然后继续同一服务执行。不要臆造字段名。" : "根据服务声明填写输入,然后继续同一服务执行。",
|
|
126
177
|
next: null,
|
|
127
178
|
interaction: {
|
|
128
179
|
schema_version: "itpay.interaction.v1",
|
|
129
180
|
stage: "input_required",
|
|
130
181
|
input_template: {
|
|
131
182
|
command: `itpay services run ${serviceID} --execution ${response.execution.service_execution_id} --input-json <file> --json`,
|
|
132
|
-
|
|
183
|
+
input: guidance?.input_example ?? serviceInputTemplate(response.workflow_entry.input_schema),
|
|
184
|
+
required_input: ["file", ...requiredInputFields(response.workflow_entry.input_schema)],
|
|
133
185
|
executable: false,
|
|
134
186
|
},
|
|
135
187
|
},
|
|
@@ -201,11 +253,11 @@ function locationConfirmationEnvelope(executionID, capabilityID, input, confirma
|
|
|
201
253
|
? reason === "STATION_LOCATION_EVIDENCE_REQUIRED"
|
|
202
254
|
? "尚未查票。车站身份已知,但 Smart 缺少该站地理坐标证据;请说明此系统缺口。若用户仅要双站直达,可按原站名使用 Exact;不要要求用户换站。"
|
|
203
255
|
: reason === "ADDRESS_REQUIRED" || reason === "ADMINISTRATIVE_AREA_UNRESOLVED"
|
|
204
|
-
? "
|
|
256
|
+
? "尚未查票。先用已有上下文、地图或浏览补该端地点证据;含义仍不清且影响范围才问用户一个关键问题。保留另一端和原条件;只有返回完整新查询模板且已有新证据才另查,没有模板则停止,不猜旧执行输入。"
|
|
205
257
|
: reason === "COORDINATE_IDENTITY_UNVERIFIED" || reason === "COORDINATE_NAME_MISMATCH"
|
|
206
258
|
? "尚未查票。提交的地点名称和坐标尚未得到一致证据;请核实该端点的真实名称或位置,保留另一端,不要把坐标声明当成人工确认。"
|
|
207
|
-
: "
|
|
208
|
-
: "
|
|
259
|
+
: "尚未查票。地点证据不足;说明返回的原因与受影响端,先补真实证据,不改kind绕过、不把外部故障当用户歧义。当前无候选确认或完整新查询模板则停止。"
|
|
260
|
+
: "尚未查票。仅向用户确认真实地点歧义,展示候选名称、地址和高德链接。具体住宅仍是目的地,附近车站只作候选;不得把选地点改成锁车站。用户选择后保留原输入,回到同一服务。";
|
|
209
261
|
return {
|
|
210
262
|
status: "location_confirmation_required",
|
|
211
263
|
result: { service_execution_id: executionID, query: input, location_confirmation: confirmation },
|
|
@@ -233,12 +285,12 @@ function requiredInputFields(schema) {
|
|
|
233
285
|
function locationInputInstruction(schema) {
|
|
234
286
|
const properties = schema?.properties;
|
|
235
287
|
if (properties?.endpoints)
|
|
236
|
-
return " 两端分别填写 endpoints.origin/destination
|
|
288
|
+
return " " + RAIL_ENDPOINT_BRIEF + " 两端分别填写 endpoints.origin/destination 的完整text;有类别依据就填kind,只有对象含义明确而类别待核验时可省略。区域保留范围、精确车站仅在目录验证后锁定。不要混用旧 origin/destination、手填锁站或伪造坐标。唯一地点会自动解析;只有真实多候选才让用户选返回的候选 id,地图失败应按当前执行恢复。";
|
|
237
289
|
if (!properties?.origin_location || !properties?.destination_location)
|
|
238
290
|
return "";
|
|
239
291
|
if (!properties.location_confirmation)
|
|
240
|
-
return " 地点保留用户已知最完整名称或城市范围;不要编造坐标或把城市改成同名车站。坐标及其他可选字段严格按当前 input_schema 提交。";
|
|
241
|
-
return "
|
|
292
|
+
return " " + RAIL_ENDPOINT_BRIEF + " 地点保留用户已知最完整名称或城市范围;不要编造坐标或把城市改成同名车站。坐标及其他可选字段严格按当前 input_schema 提交。";
|
|
293
|
+
return " " + RAIL_ENDPOINT_BRIEF + " 地点输入:保留两个地点或城市范围。已有可信高德 GCJ-02 坐标时,可用 --input 'origin_location={\"lng\":113.0,\"lat\":23.0,\"coordinate_system\":\"gcj02\",\"source\":\"amap\"}'(数值必须替换为真实查询结果,destination_location 同理);没有地图能力时,直接传用户已知最完整的 origin/destination,可附 origin_city/destination_city。不要编造坐标、补猜地址或把深圳等城市改成深圳站;精确站查须明确站名。用户只给城市、县或镇就保留区域意图,不追问门牌。明确地点由服务自动解析;只有返回 location_confirmation 才展示候选名称、地址和高德链接,等待用户选择,禁止自行选第一项。";
|
|
242
294
|
}
|
|
243
295
|
export async function runServicesInvoke(backend, config, serviceExecutionID, capabilityID, input, options = {}) {
|
|
244
296
|
const readModel = await backend.getServiceExecution(serviceExecutionID);
|
|
@@ -527,10 +579,13 @@ export async function runServicesAction(backend, serviceExecutionID, actionType,
|
|
|
527
579
|
catch (error) {
|
|
528
580
|
if (error instanceof HttpError && ["booking_review_invalid", "booking_review_changed", "booking_review_locked"].includes(error.code)) {
|
|
529
581
|
throw new CommandContractError(error.code, error.message, error.code === "booking_review_invalid"
|
|
530
|
-
? "
|
|
582
|
+
? "确认输入与当前草稿不符。使用返回 interaction.input_template.input 当前review模板,席别使用返回的代码,不填显示名或猜新值。若仅修正代码写法,且车次、实际席别、人数、价格及条款等已确认内容未变,可沿用用户已有明确确认后提交;草稿实质变化或缺少真实同意时,先请用户确认变化内容。"
|
|
531
583
|
: error.code === "booking_review_changed"
|
|
532
|
-
? "
|
|
533
|
-
: "当前购买已有付款尝试;读取同一booking及订单状态,不创建新订单或重做付款。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取同一booking的当前状态与review" }]
|
|
584
|
+
? "草稿版本已变化;使用同一booking的当前review模板。仅技术修正且车次、实际席别、人数、价格及条款未变时沿用已有明确同意;实质变化或缺少真实同意才补确认。"
|
|
585
|
+
: "当前购买已有付款尝试;读取同一booking及订单状态,不创建新订单或重做付款。", error.payload?.review ? [] : [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取同一booking的当前状态与review" }], error.payload?.review?.input_template ? {
|
|
586
|
+
schema_version: "itpay.interaction.v1", stage: "booking_review_required",
|
|
587
|
+
input_template: { command: `itpay services action ${serviceExecutionID} --action ${actionType} --actor-type human --status approved --input-json <file> --json`, input: error.payload?.review.input_template, required_input: ["file"], executable: false },
|
|
588
|
+
} : undefined, { service_execution_id: serviceExecutionID, review: error.payload?.review, input_errors: error.payload?.input_errors });
|
|
534
589
|
}
|
|
535
590
|
throw error;
|
|
536
591
|
}
|
|
@@ -672,22 +727,21 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
672
727
|
try {
|
|
673
728
|
const booking = await backend.getServiceExecution(serviceExecutionID);
|
|
674
729
|
if (booking.execution.service_id === "itpay-rail-booking") {
|
|
675
|
-
const leg = booking.rail_booking?.legs?.[0];
|
|
676
730
|
const review = booking.workflow?.human_action?.context?.review;
|
|
677
|
-
const
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
731
|
+
const presentation = await backend.getCheckoutPresentation(checkoutID, displayToken);
|
|
732
|
+
const quote = presentation.rail_quote;
|
|
733
|
+
const facts = quote?.legs ?? booking.rail_booking?.legs ?? (Array.isArray(review?.legs) ? review.legs : []);
|
|
734
|
+
const legs = facts.filter(value => value && typeof value === "object").map(value => {
|
|
735
|
+
const leg = value;
|
|
736
|
+
return { travel_date: leg.travel_date, train_code: leg.train_code,
|
|
737
|
+
from_station: leg.from || leg.from_station || `站点代码 ${String(leg.from_station_code ?? "未知")}(名称暂缺)`,
|
|
738
|
+
to_station: leg.to || leg.to_station || `站点代码 ${String(leg.to_station_code ?? "未知")}(名称暂缺)`,
|
|
739
|
+
departure: leg.departure ?? leg.departure_time, arrival: leg.arrival ?? leg.arrival_time,
|
|
740
|
+
arrival_days: leg.arrival_days, seat_name: leg.seat_name || leg.seat_type_name || `席别代码 ${String(leg.seat_type ?? "未知")}(名称暂缺)` };
|
|
741
|
+
});
|
|
742
|
+
if (legs.length) {
|
|
743
|
+
itinerary = { legs, passengers: quote?.passengers ?? review?.passengers ?? null, purchase_unit: "journey" };
|
|
744
|
+
itineraryTitle = legs.map(leg => [leg.train_code, leg.from_station, "->", leg.to_station].filter(value => typeof value === "string" && value.length).join(" ")).join(" / ");
|
|
691
745
|
}
|
|
692
746
|
}
|
|
693
747
|
}
|
|
@@ -695,6 +749,7 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
695
749
|
// Checkout already exists; a display-only title lookup cannot hide its official handoff.
|
|
696
750
|
}
|
|
697
751
|
const plan = buildCheckoutQRPlan({
|
|
752
|
+
localSimulation: backend.localSimulation,
|
|
698
753
|
host,
|
|
699
754
|
checkoutID,
|
|
700
755
|
checkoutURL,
|
|
@@ -741,8 +796,10 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
741
796
|
});
|
|
742
797
|
}
|
|
743
798
|
const envelope = buildServicesCheckoutEnvelope(response, checkoutURL, plan, options.agentType, options.target);
|
|
744
|
-
if (itinerary)
|
|
799
|
+
if (itinerary) {
|
|
745
800
|
envelope.result = { ...envelope.result, itinerary };
|
|
801
|
+
envelope.instruction = "整程一次付款,各段分别出票;展示入口不等于付款,付款成功不等于全部出票。" + envelope.instruction;
|
|
802
|
+
}
|
|
746
803
|
const plainResult = plainValueLines(envelope.result);
|
|
747
804
|
if (!options.jsonOutput && platform === "terminal") {
|
|
748
805
|
plainResult.push("qr:", await renderTerminalQR(checkoutURL, options.qrFormat ?? "terminal"));
|
|
@@ -888,7 +945,7 @@ export async function runServicesPage(backend, serviceExecutionID, resultItemID,
|
|
|
888
945
|
if (!Number.isInteger(limit) || limit < 1 || limit > 20) {
|
|
889
946
|
throw new CommandContractError("limit_invalid", "--limit must be an integer from 1 to 20", "limit 必须是 1 到 20 的整数;本次未读取服务端分页。", [{ command: `itpay services page ${serviceExecutionID} ${resultItemID} --offset ${offset} --json`, reason: "用默认页大小重试" }]);
|
|
890
947
|
}
|
|
891
|
-
const response = await backend.getServiceExecutionResultItemPage(serviceExecutionID, resultItemID, offset, limit);
|
|
948
|
+
const response = await backend.getServiceExecutionResultItemPage(serviceExecutionID, resultItemID, offset, limit, limit === 1 ? undefined : "summary");
|
|
892
949
|
const page = response.page;
|
|
893
950
|
const container = (page.result ?? page);
|
|
894
951
|
const exactCandidate = limit === 1 && Array.isArray(container.candidates) && container.candidates.length === 1
|
|
@@ -934,6 +991,8 @@ export async function runServicesPage(backend, serviceExecutionID, resultItemID,
|
|
|
934
991
|
offset: catalogPage.offset ?? offset,
|
|
935
992
|
limit: catalogPage.limit ?? limit,
|
|
936
993
|
total: catalogPage.total ?? candidates.length,
|
|
994
|
+
requested_limit: limit,
|
|
995
|
+
truncated_reason: container.journey_page?.truncated_reason ?? null,
|
|
937
996
|
count: catalogPage.count ?? candidates.length,
|
|
938
997
|
next_offset: nextOffset,
|
|
939
998
|
...(journeys.length ? { journeys: journeys.map(value => compactRailJourney(value, serviceExecutionID, resultItemID)) }
|
|
@@ -1010,6 +1069,7 @@ function writeSavedRailPage(envelope, options, key, offset, continuation) {
|
|
|
1010
1069
|
throw new CommandContractError("saved_row_too_large", "saved row exceeds the readable page limit", "这条已保存记录超过当前页面大小;事实仍保留,请读取该条详情。", rows[0]?.detail?.command ? [rows[0].detail] : []);
|
|
1011
1070
|
}
|
|
1012
1071
|
rows.pop();
|
|
1072
|
+
result.truncated_reason = "cli_output_byte_budget";
|
|
1013
1073
|
result.count = rows.length;
|
|
1014
1074
|
if (key === "trains")
|
|
1015
1075
|
result.read_count = rows.length;
|
|
@@ -1092,13 +1152,14 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
1092
1152
|
const journeys = [];
|
|
1093
1153
|
let nextOffset = offset;
|
|
1094
1154
|
let total = 0;
|
|
1155
|
+
let truncatedReason = null;
|
|
1095
1156
|
let qualificationCounts;
|
|
1096
1157
|
let planID;
|
|
1097
1158
|
let queryRevision;
|
|
1098
1159
|
while (nextOffset !== null) {
|
|
1099
1160
|
let response;
|
|
1100
1161
|
try {
|
|
1101
|
-
response = await backend.getServiceExecutionResultItemPage(serviceExecutionID, options.snapshot, nextOffset, limit);
|
|
1162
|
+
response = await backend.getServiceExecutionResultItemPage(serviceExecutionID, options.snapshot, nextOffset, limit, "summary");
|
|
1102
1163
|
}
|
|
1103
1164
|
catch (error) {
|
|
1104
1165
|
if (error instanceof HttpError && error.code === "result_not_pageable") {
|
|
@@ -1116,6 +1177,7 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
1116
1177
|
const rows = Array.isArray(container.journeys) ? container.journeys : [];
|
|
1117
1178
|
total = typeof page?.total === "number" ? page.total : rows.length;
|
|
1118
1179
|
qualificationCounts = page?.qualification_counts ?? qualificationCounts;
|
|
1180
|
+
truncatedReason = page?.truncated_reason ?? truncatedReason;
|
|
1119
1181
|
journeys.push(...rows.map((card) => compactRailJourney(card, serviceExecutionID, options.snapshot)));
|
|
1120
1182
|
const following = page?.next_offset;
|
|
1121
1183
|
if (typeof following === "number" && following <= nextOffset)
|
|
@@ -1133,6 +1195,8 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
1133
1195
|
query_revision: queryRevision,
|
|
1134
1196
|
total,
|
|
1135
1197
|
qualification_counts: qualificationCounts,
|
|
1198
|
+
requested_limit: limit,
|
|
1199
|
+
truncated_reason: truncatedReason,
|
|
1136
1200
|
offset,
|
|
1137
1201
|
count: journeys.length,
|
|
1138
1202
|
next_offset: nextOffset,
|
|
@@ -1142,7 +1206,7 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
1142
1206
|
? "此历史快照没有完整资格计数;线路仅供核对铁路事实,不能据此认定可购买。沿同快照 detail.command 查看详情。"
|
|
1143
1207
|
: qualificationCounts.eligible === 0
|
|
1144
1208
|
? `查到 ${total} 个时刻组合,当前 0 条满足购票资格。请读取诊断线路的 detail.command 查看具体原因;修改条件须依据用户意愿。`
|
|
1145
|
-
:
|
|
1209
|
+
: `已查范围内合格时刻组合 ${qualificationCounts.eligible};本次展示 ${journeys.length} 个方案(首屏优先既有主方案/代表,计数不是中转通道数)。最快/低价须按对应方案和比较范围说明;未全范围排序只能说已读方案中。更多条件优先同快照调整偏好或沿 next 有序读所需页,不随机翻页、不无条件读完组合。未知费用不能当免费;detail.command 读取席别和整程购买条件,购买前实时报价。`,
|
|
1146
1210
|
next: nextOffset === null ? null : { command: `itpay services read-result ${serviceExecutionID} --snapshot ${options.snapshot} --offset ${nextOffset} --limit ${limit} --json`, reason: "仅当前问题还需未读线路时继续;已有合格完整结果可答复或按委托继续" },
|
|
1147
1211
|
recovery: [],
|
|
1148
1212
|
}, {
|
|
@@ -1174,7 +1238,7 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
1174
1238
|
ticket_plan_offset: planOffset,
|
|
1175
1239
|
ticket_plans: selected.map(plan => compactRailTicketPlan(plan, detail.journey.qualification_status === "eligible")),
|
|
1176
1240
|
},
|
|
1177
|
-
instruction: "
|
|
1241
|
+
instruction: "同一线路的完整车次与席别方案。已有所需席别和有效凭据即可按真实委托继续;只需比较则答复并停止,需要其他席别才读下一页。使用所选 ticket_plan 的整程 booking_template:一个 booking、统一报价、一个官方付款入口,各段分别出票,无联合锁票或接续保证。未返回整程模板时不能拼不同方案的腿或退回逐腿付款,按同一执行恢复。身份资料只在受保护 Checkout 页面填写。",
|
|
1178
1242
|
next: nextOffset === null ? null : { command: `itpay services read-result ${serviceExecutionID} --snapshot ${detail.snapshot_id} --journey ${options.journey} --offset ${nextOffset} --limit ${planLimit} --json`, reason: "仅需要其他席别时读取;已满足用户条件则停止翻页" },
|
|
1179
1243
|
...(detail.journey?.booking_support === "single_leg" ? { interaction: {
|
|
1180
1244
|
schema_version: "itpay.interaction.v1", stage: "query_results_ready",
|
|
@@ -1259,6 +1323,8 @@ function railBookingEnvelope(model) {
|
|
|
1259
1323
|
leg_index: leg.leg_index,
|
|
1260
1324
|
state: leg.state,
|
|
1261
1325
|
issued: leg.issued,
|
|
1326
|
+
...(leg.error_code ? { error_code: leg.error_code } : {}),
|
|
1327
|
+
arrival_days: leg.arrival_days ?? 0,
|
|
1262
1328
|
...(leg.details_pending ? { details_pending: true } : {}),
|
|
1263
1329
|
...(leg.supplier_state ? { supplier_state: leg.supplier_state } : {}),
|
|
1264
1330
|
...(leg.train_code ? { train_code: leg.train_code } : {}),
|
|
@@ -1273,9 +1339,9 @@ function railBookingEnvelope(model) {
|
|
|
1273
1339
|
const result = {
|
|
1274
1340
|
service_execution_id: execution.service_execution_id,
|
|
1275
1341
|
...(orderID ? { order_id: orderID } : {}),
|
|
1276
|
-
rail: { state: rail.state, issued_legs: rail.issued_legs, legs },
|
|
1342
|
+
rail: { state: rail.state, issued_legs: rail.issued_legs, total_legs: rail.legs.length, message: rail.message, legs },
|
|
1277
1343
|
};
|
|
1278
|
-
if (rail.state === "issued") {
|
|
1344
|
+
if (rail.state === "issued" && rail.issued_legs === rail.legs.length && rail.legs.length > 0) {
|
|
1279
1345
|
const detailsPending = rail.legs.some((leg) => leg.details_pending);
|
|
1280
1346
|
return {
|
|
1281
1347
|
status: "issued",
|
|
@@ -1289,7 +1355,7 @@ function railBookingEnvelope(model) {
|
|
|
1289
1355
|
return {
|
|
1290
1356
|
status: "manual_review",
|
|
1291
1357
|
result,
|
|
1292
|
-
instruction:
|
|
1358
|
+
instruction: `告诉用户:付款已确认,已出票 ${rail.issued_legs}/${rail.legs.length},其余需要人工核对;请勿重复付款或重新下单,已出票的车票会保留。未知结果先对账,退款按对应段处理,不承诺整笔退款或时效。`,
|
|
1293
1359
|
next: orderID ? { command: `itpay order ${orderID} --json`, reason: "查看同一订单当前状态" } : null,
|
|
1294
1360
|
recovery: [],
|
|
1295
1361
|
};
|
|
@@ -1302,7 +1368,7 @@ function railBookingEnvelope(model) {
|
|
|
1302
1368
|
return {
|
|
1303
1369
|
status: "issuing",
|
|
1304
1370
|
result,
|
|
1305
|
-
instruction:
|
|
1371
|
+
instruction: `告诉用户:付款已确认,已出票 ${rail.issued_legs}/${rail.legs.length},后台继续处理;付款成功不代表整程出票,请勿重复购买或再次付款。稍后只读取同一任务。`,
|
|
1306
1372
|
next: { command: `itpay services next ${execution.service_execution_id} --json`, reason: "稍后读取同一出票任务" },
|
|
1307
1373
|
recovery: [],
|
|
1308
1374
|
};
|
|
@@ -1315,6 +1381,29 @@ function railBookingEnvelope(model) {
|
|
|
1315
1381
|
recovery: [],
|
|
1316
1382
|
};
|
|
1317
1383
|
}
|
|
1384
|
+
// Public summaries and saved details use the same evidenced units and scopes.
|
|
1385
|
+
function railJourneyFacts(card) {
|
|
1386
|
+
const offer = card.representative_offer;
|
|
1387
|
+
const metrics = card.representative_metrics;
|
|
1388
|
+
const minor = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1389
|
+
const time = (value) => typeof value === "string" ? Date.parse(/(?:Z|[+-]\d{2}:\d{2})$/.test(value) ? value.replace(" ", "T") : `${value.replace(" ", "T")}+08:00`) : NaN;
|
|
1390
|
+
const start = time(card.rides?.[0]?.departure), end = time(card.rides?.at(-1)?.arrival);
|
|
1391
|
+
return {
|
|
1392
|
+
rail_fare_fen: minor(offer?.fare_minor), service_fee_fen: minor(offer?.service_fee_minor),
|
|
1393
|
+
rail_payable_fen: minor(offer?.rail_payable_minor), currency: offer?.currency ?? null,
|
|
1394
|
+
price_basis: "铁路应付参考额=铁路票价+各票段服务费;不含未知地面费用,以新报价为准。",
|
|
1395
|
+
rail_duration_minutes: Number.isFinite(start) && Number.isFinite(end) && end >= start ? Math.round((end - start) / 60000) : null,
|
|
1396
|
+
door_to_door_minutes: metrics?.duration_basis === "door_to_door" ? minor(metrics.door_to_door_min) : null,
|
|
1397
|
+
...(metrics?.duration_basis !== "door_to_door" ? { ground_time_note: "接驳未完整核验,不能把铁路时长称为门到门时间。" } : {}),
|
|
1398
|
+
estimated_trip_total: metrics?.estimated_total_cost ?? null,
|
|
1399
|
+
estimate_basis: "仅是已核验费用范围的估算,不是含全部地面费用的实付额。",
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
function publicBookingSupport(card) {
|
|
1403
|
+
if (card.booking_support !== "separate_legs_only")
|
|
1404
|
+
return card.booking_support;
|
|
1405
|
+
return card.qualification_status === "eligible" && card.ticket_plans?.some(plan => plan.qualification_status === "eligible" && plan.booking_input) ? "journey" : "read_only";
|
|
1406
|
+
}
|
|
1318
1407
|
function compactRailJourney(card, serviceExecutionID, snapshotID) {
|
|
1319
1408
|
if (!card.journey_id)
|
|
1320
1409
|
throw new Error("saved journey has no stable ID");
|
|
@@ -1333,14 +1422,6 @@ function compactRailJourney(card, serviceExecutionID, snapshotID) {
|
|
|
1333
1422
|
leg_index: raw.leg_index, seat_type: raw.seat_type, seat_name: raw.seat_name,
|
|
1334
1423
|
availability: raw.availability_text, remaining: raw.inventory_exact === true ? raw.remaining : null,
|
|
1335
1424
|
}));
|
|
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
1425
|
return {
|
|
1345
1426
|
journey_ref: card.journey_id,
|
|
1346
1427
|
...(card.profile_refs?.balanced ? { profile_ref: card.profile_refs.balanced } : {}),
|
|
@@ -1357,17 +1438,20 @@ function compactRailJourney(card, serviceExecutionID, snapshotID) {
|
|
|
1357
1438
|
required_input: ["file", "真实席别选择及人数"] },
|
|
1358
1439
|
} : {}),
|
|
1359
1440
|
...(ticketSeats.length ? { seats: ticketSeats } : {}),
|
|
1441
|
+
booking_support: publicBookingSupport(card),
|
|
1442
|
+
...(publicBookingSupport(card) === "read_only" ? { purchase_recovery: { command: `itpay services next ${serviceExecutionID} --json`, reason: "历史快照无整程凭据;读取同一执行合法准备路径,不拆单" } } : {}),
|
|
1443
|
+
ticket_plan_refs: (card.ticket_plans ?? []).map(plan => plan.ticket_plan_ref),
|
|
1444
|
+
transfer_wait_minutes: card.transfer_wait_minutes ?? [],
|
|
1445
|
+
ground_summary: card.ground_summary ?? null,
|
|
1360
1446
|
passengers: card.passengers ?? offer?.ticket_offers?.[0]?.passengers ?? null,
|
|
1361
1447
|
availability: card.availability ?? "unknown",
|
|
1362
1448
|
qualification_status: card.qualification_status ?? "unknown",
|
|
1363
1449
|
...(card.qualification_reasons?.length ? { qualification_reasons: card.qualification_reasons } : {}),
|
|
1364
|
-
...(
|
|
1365
|
-
estimated_trip_total: metrics?.estimated_total_cost ?? null,
|
|
1450
|
+
...railJourneyFacts(card),
|
|
1366
1451
|
cost_basis: metrics?.cost_basis ?? "unknown",
|
|
1367
1452
|
duration_basis: metrics?.duration_basis ?? "unknown",
|
|
1368
1453
|
timezone: "Asia/Shanghai",
|
|
1369
1454
|
...(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
1455
|
...(card.observed_at ? { observed_at: card.observed_at } : {}),
|
|
1372
1456
|
detail: { command: `itpay services read-result ${serviceExecutionID} --snapshot ${snapshotID} --journey ${card.journey_id} --json`,
|
|
1373
1457
|
reason: card.qualification_status === "eligible" ? "读取此完整线路的席别、报价和购买条件" : "读取此线路的铁路事实和不可购买原因" },
|
|
@@ -1380,19 +1464,23 @@ function compactRailTicketPlan(raw, journeyEligible) {
|
|
|
1380
1464
|
return {
|
|
1381
1465
|
ticket_plan_ref: raw.ticket_plan_ref,
|
|
1382
1466
|
passengers: raw.passenger_count ?? (Array.isArray(raw.ticket_offers) ? raw.ticket_offers[0]?.passengers : null) ?? null,
|
|
1383
|
-
purchase_support: raw.purchase_support ?? "none",
|
|
1467
|
+
purchase_support: raw.purchase_support === "separate_legs_only" ? "read_only" : raw.purchase_support ?? "none",
|
|
1468
|
+
...(legs.length > 1 && !raw.booking_input ? { booking_unavailable: "这份快照只有历史铁路事实,没有完整整程购买凭据;不能拆腿或拼接其他快照凭据。读取同一执行当前状态,按其购买准备或过期恢复动作继续。" } : {}),
|
|
1384
1469
|
qualification_status: raw.qualification_status ?? "unknown",
|
|
1385
1470
|
qualification_reasons: raw.qualification_reasons ?? [],
|
|
1386
1471
|
rail_amount_fen: raw.rail_amount_fen ?? null,
|
|
1387
1472
|
service_fee_fen: raw.service_fee_fen ?? null,
|
|
1388
1473
|
price_verified: raw.rail_amount_verified === true,
|
|
1474
|
+
...(journeyEligible && raw.qualification_status === "eligible" && raw.booking_input && typeof raw.booking_input === "object" ? {
|
|
1475
|
+
booking_template: { command: "itpay services run itpay-rail-booking --input-json <journey.json> --json", executable: false, ticket_plan_ref: raw.ticket_plan_ref, input_example: raw.booking_input, required_input: ["file", "passengers"] },
|
|
1476
|
+
} : {}),
|
|
1389
1477
|
legs: legs.map((leg, index) => ({
|
|
1390
1478
|
leg_index: index,
|
|
1391
1479
|
travel_date: leg.travel_date ?? tickets.find(ticket => ticket.leg_index === index)?.travel_date ?? null,
|
|
1392
1480
|
...(() => {
|
|
1393
1481
|
const ticket = tickets.find(ticket => ticket.leg_index === index) ?? (tickets[index]?.leg_index === undefined ? tickets[index] : undefined);
|
|
1394
1482
|
const offer = ticket?.booking_offer;
|
|
1395
|
-
return offer?.service_id && offer.selection_token && journeyEligible && raw.qualification_status === "eligible" ? {
|
|
1483
|
+
return legs.length === 1 && offer?.service_id && offer.selection_token && journeyEligible && raw.qualification_status === "eligible" ? {
|
|
1396
1484
|
booking_template: { command: `itpay services run ${offer.service_id} --input-json <购票.json> --json`, executable: false,
|
|
1397
1485
|
ticket_plan_ref: raw.ticket_plan_ref, leg_index: index,
|
|
1398
1486
|
seat_choices: [{ seat_type: ticket?.seat_type, seat_name: ticket?.seat_name }],
|
|
@@ -1400,7 +1488,7 @@ function compactRailTicketPlan(raw, journeyEligible) {
|
|
|
1400
1488
|
passengers: ticket?.passengers ?? raw.passenger_count ?? "<真实人数>" },
|
|
1401
1489
|
required_input: ["file", "真实人数", "该方案席别代码"],
|
|
1402
1490
|
},
|
|
1403
|
-
} : { booking_unavailable: "本段没有当前可用的服务端凭据;不能猜代码或例行重查。保留快照,按当前执行的恢复动作处理;过期凭据需真实新验证。" };
|
|
1491
|
+
} : legs.length > 1 ? {} : { booking_unavailable: "本段没有当前可用的服务端凭据;不能猜代码或例行重查。保留快照,按当前执行的恢复动作处理;过期凭据需真实新验证。" };
|
|
1404
1492
|
})(),
|
|
1405
1493
|
train_code: leg.train_code ?? null,
|
|
1406
1494
|
from_station: leg.from_station ?? null, from_station_code: leg.from_station_code ?? null,
|
|
@@ -1426,21 +1514,23 @@ function railJourneySummary(card, serviceExecutionID, snapshotID) {
|
|
|
1426
1514
|
route: (card.route_names?.length ? card.route_names : card.route ?? []).join("→"),
|
|
1427
1515
|
...(trains.length ? { trains } : {}),
|
|
1428
1516
|
...(first?.departure || last?.arrival ? { time: `${first?.departure ?? ""}–${last?.arrival ?? ""}` } : {}),
|
|
1429
|
-
...(offer ? { price: formatMoney(offer.rail_payable_minor, offer.currency) } : { price: "unknown" }),
|
|
1517
|
+
...(offer && typeof offer.rail_payable_minor === "number" ? { price: formatMoney(offer.rail_payable_minor, offer.currency) } : { price: "unknown" }),
|
|
1518
|
+
...railJourneyFacts(card),
|
|
1430
1519
|
...(metrics ? { calculation_scope: {
|
|
1431
1520
|
origin: metrics.origin_scope ?? "legacy_unknown", destination: metrics.destination_scope ?? "legacy_unknown",
|
|
1432
1521
|
duration_basis: metrics.duration_basis ?? "legacy_unknown", arrival_basis: metrics.arrival_basis ?? "legacy_unknown",
|
|
1433
1522
|
cost_basis: metrics.cost_basis ?? "legacy_unknown",
|
|
1434
|
-
duration_minutes: metrics.door_to_door_min ?? null,
|
|
1523
|
+
duration_minutes: metrics.duration_basis === "door_to_door" ? metrics.door_to_door_min ?? null : railJourneyFacts(card).rail_duration_minutes,
|
|
1435
1524
|
} } : {}),
|
|
1436
1525
|
...(card.decision_role ? { decision_role: card.decision_role } : {}),
|
|
1437
1526
|
...(card.explanation?.length ? { explanation: card.explanation } : {}),
|
|
1438
1527
|
...(card.reason_codes?.length ? { reason_codes: card.reason_codes } : {}),
|
|
1439
1528
|
...(card.tradeoff_codes?.length ? { tradeoff_codes: card.tradeoff_codes } : {}),
|
|
1440
1529
|
...(card.recommended_profile ? { recommended_profile: card.recommended_profile,
|
|
1441
|
-
|
|
1530
|
+
recommended_price_basis: "推荐席别及已核验接驳估算以recommended_profile为准;按其ticket_plan_ref/offer_refs读取完整方案,新报价确认后下单。" } : {}),
|
|
1442
1531
|
availability: card.availability,
|
|
1443
|
-
booking_support: card
|
|
1532
|
+
booking_support: publicBookingSupport(card),
|
|
1533
|
+
...(publicBookingSupport(card) === "read_only" ? { purchase_recovery: { command: `itpay services next ${serviceExecutionID} --json`, reason: "历史快照无整程凭据;读取同一执行当前合法购买准备路径,不拆单" } } : {}),
|
|
1444
1534
|
...(card.observed_at ? { observed_at: card.observed_at } : {}),
|
|
1445
1535
|
...(card.risk_notes?.length ? { risk_notes: card.risk_notes } : {}),
|
|
1446
1536
|
...(card.booking_support === "single_leg"
|
|
@@ -1513,7 +1603,7 @@ function railPlanningEnvelope(model) {
|
|
|
1513
1603
|
...(plan.notices?.length ? { notices: plan.notices } : {}),
|
|
1514
1604
|
...(plan.recommendation ? { recommendation: railJourneySummary(plan.recommendation, se, plan.catalog_snapshot_id ?? plan.snapshot_id) } : {}),
|
|
1515
1605
|
...(cards.length ? { journeys: cards } : {}),
|
|
1516
|
-
...(plan.snapshot_id ? { full_result: { command: `itpay services read-result ${se} --snapshot ${plan.snapshot_id} --json`, meaning: "读取已保存的完整目录;推荐和journeys仅是摘要" } } : {}),
|
|
1606
|
+
...((plan.catalog_snapshot_id ?? plan.snapshot_id) ? { full_result: { command: `itpay services read-result ${se} --snapshot ${plan.catalog_snapshot_id ?? plan.snapshot_id} --json`, meaning: "读取已保存的完整目录;推荐和journeys仅是摘要" } } : {}),
|
|
1517
1607
|
...(!hasEligible && ["paused", "complete", "expired"].includes(expansion) && plan.query_input && plan.catalog_snapshot_id ? { new_query_template: {
|
|
1518
1608
|
command: "itpay services run itpay-rail-smart --input-json <file> --json",
|
|
1519
1609
|
executable: false,
|
|
@@ -1531,7 +1621,7 @@ function railPlanningEnvelope(model) {
|
|
|
1531
1621
|
if (plan.search?.reason === "ground_evidence_required") {
|
|
1532
1622
|
return {
|
|
1533
1623
|
status: "awaiting_input", result,
|
|
1534
|
-
instruction: "
|
|
1624
|
+
instruction: `已查到铁路班次,但具体地址的接驳或出站证据不足,${plan.query_input?.arrive_before || plan.query_input?.arrive_by ? "无法验证用户要求的到达时限" : "尚不能核验相应地点条件或门到门时间"};剩余站对未继续查询,不能说没有车。说明已查范围和缺失事实,请补充准确起终点地址或可验证的接驳/出站信息;补齐后按原要求规划,不默认扩大搜索。`,
|
|
1535
1625
|
next: null, recovery: [],
|
|
1536
1626
|
};
|
|
1537
1627
|
}
|
|
@@ -1703,6 +1793,14 @@ function terminalExecutionEnvelope(model) {
|
|
|
1703
1793
|
],
|
|
1704
1794
|
};
|
|
1705
1795
|
}
|
|
1796
|
+
const RAIL_LOCATION_EVIDENCE_REASONS = new Set(["endpoint_kind_conflict", "place_identity_uncertain", "endpoint_adcode_name_conflict", "endpoint_poi_name_conflict", "endpoint_station_name_code_conflict", "location_evidence_required", "coordinate_name_mismatch", "area_scope_too_broad", "address_required", "administrative_area_unresolved", "station_location_evidence_required", "coordinate_identity_unverified"]);
|
|
1797
|
+
const RAIL_QUOTE_FAILURE_GUIDANCE = {
|
|
1798
|
+
quote_source_unavailable: "实时车票数据暂不可用;保留原选择,稍后按用户意愿重新核验,不创建替代订单。",
|
|
1799
|
+
quote_train_changed: "所选车次的时刻或运行信息已变化;向用户说明并征询是否重新查票。",
|
|
1800
|
+
quote_seat_changed: "所选席别已变化;向用户说明并征询新的席别选择。",
|
|
1801
|
+
quote_seat_unavailable: "所选席别当前无足够余票;告知用户并由其决定是否换方案。",
|
|
1802
|
+
quote_refresh_expired: "实时报价过程超时;保留原选择,不把旧价当可付款价格。",
|
|
1803
|
+
};
|
|
1706
1804
|
function servicesNextEnvelope(model) {
|
|
1707
1805
|
const execution = model.execution;
|
|
1708
1806
|
const currentDelivery = model.current_delivery ?? model.delivery_bindings.at(-1);
|
|
@@ -1713,7 +1811,7 @@ function servicesNextEnvelope(model) {
|
|
|
1713
1811
|
next: refundState.next ? { command: `itpay refund get ${lockedRefund.refund_request_id} --json`, reason: "读取同一退款权威状态" } : null };
|
|
1714
1812
|
}
|
|
1715
1813
|
const latestRun = model.execution_requests?.filter((request) => request.execution_kind === "service.execution.run").at(-1);
|
|
1716
|
-
if (latestRun && ["pending", "started"].includes(latestRun.status)) {
|
|
1814
|
+
if (latestRun && ["pending", "started"].includes(latestRun.status) && model.workflow?.status !== "failed") {
|
|
1717
1815
|
return {
|
|
1718
1816
|
status: "running",
|
|
1719
1817
|
result: { service_execution_id: execution.service_execution_id, execution_request_id: latestRun.execution_request_id },
|
|
@@ -1725,7 +1823,7 @@ function servicesNextEnvelope(model) {
|
|
|
1725
1823
|
// A terminal execution wins over the rail read model: a failed, cancelled,
|
|
1726
1824
|
// or refunded run must not render as still issuing. Completed executions
|
|
1727
1825
|
// keep the rail view so issued seats stay visible.
|
|
1728
|
-
if (model.rail_booking && isTerminalServiceExecutionStatus(execution.status) && !["completed", "delivery_completed"].includes(execution.status)) {
|
|
1826
|
+
if (model.rail_booking && model.workflow?.human_action?.context?.quote_recovery !== true && isTerminalServiceExecutionStatus(execution.status) && !["completed", "delivery_completed"].includes(execution.status)) {
|
|
1729
1827
|
const terminal = terminalExecutionEnvelope(model);
|
|
1730
1828
|
if (terminal)
|
|
1731
1829
|
return terminal;
|
|
@@ -1748,7 +1846,7 @@ function servicesNextEnvelope(model) {
|
|
|
1748
1846
|
const workflowReason = model.workflow?.failure?.reason_code ?? model.workflow?.error_code;
|
|
1749
1847
|
const paymentVerified = model.payment_bindings.some((binding) => binding.status === "payment_verified") || model.checkout_bindings.some((binding) => binding.status === "payment_verified");
|
|
1750
1848
|
const state = model.workflow?.status === "payment" && paymentVerified ? "running" : model.workflow?.status ?? "input_required";
|
|
1751
|
-
if (["quota_paused", "failed"].includes(state) && workflowReason
|
|
1849
|
+
if (["quota_paused", "failed"].includes(state) && !RAIL_LOCATION_EVIDENCE_REASONS.has(workflowReason ?? "") && (model.workflow?.dependency ||
|
|
1752
1850
|
/^amap_/.test(workflowReason ?? ""))) {
|
|
1753
1851
|
const dependency = model.workflow?.dependency;
|
|
1754
1852
|
const resumable = state === "quota_paused" && dependency?.retryable === true;
|
|
@@ -1762,28 +1860,37 @@ function servicesNextEnvelope(model) {
|
|
|
1762
1860
|
recovery: [],
|
|
1763
1861
|
};
|
|
1764
1862
|
}
|
|
1765
|
-
if (state === "failed" && workflowReason
|
|
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 ?? "")) {
|
|
1863
|
+
if (state === "failed" && RAIL_LOCATION_EVIDENCE_REASONS.has(workflowReason ?? "")) {
|
|
1771
1864
|
const code = workflowReason;
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
} } : {}) },
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1865
|
+
const affected = model.workflow?.failure?.endpoint ?? model.workflow?.dependency?.endpoint;
|
|
1866
|
+
const query = model.workflow?.query_input ?? {};
|
|
1867
|
+
const endpoints = query.endpoints && typeof query.endpoints === "object" ? query.endpoints : {};
|
|
1868
|
+
const stationPair = [endpoints.origin, endpoints.destination].every(endpoint => endpoint?.kind === "station" && typeof endpoint.text === "string" && endpoint.text.trim() && typeof endpoint.station_code === "string" && /^[A-Z]{3}$/.test(endpoint.station_code));
|
|
1869
|
+
if (code === "station_location_evidence_required") {
|
|
1870
|
+
const exactInput = stationPair && typeof query.travel_date === "string" ? { origin: endpoints.origin.text, destination: endpoints.destination.text, travel_date: query.travel_date } : undefined;
|
|
1871
|
+
return { status: "system_evidence_required", result: { service_execution_id: id, service_id: execution.service_id, reason: code, affected_endpoint: affected, failure: model.workflow?.failure, query,
|
|
1872
|
+
...(exactInput ? { new_query_template: { command: "itpay services run itpay-rail-exact --input-json <双锁站直达.json> --json", input: exactInput, executable: false, required_input: ["file"], instruction: "当前明确双锁站可核验直达;Exact只覆盖这一站对,仍按原人数、时限和偏好核对结果。" } } : {}) },
|
|
1873
|
+
instruction: "尚未查票。车站身份已知,但Smart缺车站坐标证据;这是系统证据缺口,不要求用户填技术坐标或换站。" + (exactInput ? "当前双锁站可按完整模板另用Exact核验直达,原需求见query。" : "本路线Smart暂受系统证据限制,当前无可执行恢复;停止并交由维护方补证据。"), next: null, recovery: [] };
|
|
1874
|
+
}
|
|
1875
|
+
if (affected !== "origin" && affected !== "destination")
|
|
1876
|
+
return { status: "input_evidence_required", result: { service_execution_id: id, reason: code, failed_step: model.workflow?.failure?.step_id, failure: model.workflow?.failure, query }, instruction: "地点问题未明确受影响侧,当前无可提交修正模板;保留原需求,停止并核对服务端事实,不猜另一端或坐标。", next: null, recovery: [] };
|
|
1877
|
+
const properties = model.workflow_entry.input_schema.properties;
|
|
1878
|
+
if (!properties?.endpoints && properties?.[affected] === undefined)
|
|
1879
|
+
return { status: "contract_unavailable", result: { service_execution_id: id, reason: code, failed_step: model.workflow?.failure?.step_id, failure: model.workflow?.failure, query }, instruction: "旧查询已终止,当前锁定合同缺少可用地点字段;保留原需求并停止,不猜新旧模板或隐藏字段。", next: null, recovery: [] };
|
|
1880
|
+
const input = { ...Object.fromEntries(Object.entries(query).filter(([key]) => properties?.[key] !== undefined)), ...serviceInputTemplate(model.workflow_entry.input_schema, query) };
|
|
1881
|
+
const identityConflict = ["endpoint_kind_conflict", "endpoint_adcode_name_conflict", "endpoint_poi_name_conflict", "endpoint_station_name_code_conflict", "coordinate_name_mismatch"].includes(code);
|
|
1882
|
+
const replacement = code === "area_scope_too_broad" ? { kind: "area", text: "<受影响端的市县镇完整名称>" } : { ...(endpoints[affected]?.kind ? { kind: endpoints[affected].kind } : {}), text: code === "coordinate_identity_unverified" ? "<已核实与地点身份一致的完整地址或地点名>" : "<已补充行政归属的完整地址或地点名>" };
|
|
1883
|
+
const endpointContract = properties?.endpoints !== undefined && (query.endpoints !== undefined || !properties.origin || !properties.destination);
|
|
1884
|
+
if (endpointContract)
|
|
1885
|
+
input.endpoints = { ...endpoints, [affected]: replacement };
|
|
1886
|
+
else {
|
|
1887
|
+
input[affected] = replacement.text;
|
|
1888
|
+
delete input[`${affected}_location`];
|
|
1889
|
+
}
|
|
1890
|
+
const originalPlace = endpoints[affected]?.text ?? query[affected];
|
|
1891
|
+
return { status: "awaiting_input", result: { service_execution_id: id, service_id: execution.service_id, reason: code, failed_step: model.workflow?.failure?.step_id, classification: "location_evidence_required", affected_endpoint: affected, original_place: originalPlace, ...(model.workflow?.failure?.resolved_scope ? { resolved_scope: model.workflow.failure.resolved_scope } : {}), failure: model.workflow?.failure, query,
|
|
1892
|
+
new_query_template: { command: `itpay services run ${execution.service_id} --input-json <补全原始需求.json> --json`, input, executable: false, required_input: ["file", endpointContract ? `endpoints.${affected}.text` : affected], ...(endpointContract ? { correction_fields: ["text", "kind", "poi_id", "location"].filter(field => { const side = properties?.endpoints?.properties?.[affected]; return !!side?.properties?.[field]; }) } : {}), instruction: "旧执行输入已锁定;补真实地点后将完整input写入JSON文件,按此命令提交新查询,保留另一端、日期、人数和所有原约束。" } },
|
|
1893
|
+
instruction: `旧执行已终止,尚未查票。${identityConflict ? "名称、类别或位置证据存在冲突;先核实返回原因,不能修改kind绕过。" : "完整地点名称合法,但尚缺唯一身份证据;可先用地图或浏览短查,不要求必填门牌或坐标。"}新证据确定类别且原意未变可纠正kind;改变目的地或扩大范围须先取得真实同意。保留原地点及范围;${affected === "origin" ? "出发端" : "到达端"}原文见original_place。有新证据后只补充${affected === "origin" ? "出发端" : "到达端"}${code === "area_scope_too_broad" ? "的市县镇范围" : code === "coordinate_identity_unverified" ? "的已核实地点身份;已移除未核实坐标,不猜坐标" : "的完整地点和行政归属"}。使用当前完整new_query_template;不能在旧执行伪造修改,也不能直接提交占位符。`, next: null, recovery: [] };
|
|
1787
1894
|
}
|
|
1788
1895
|
if (state === "failed" && ["login_required", "rate_limited"].includes(workflowReason ?? "")) {
|
|
1789
1896
|
const login = workflowReason === "login_required";
|
|
@@ -1807,7 +1914,7 @@ function servicesNextEnvelope(model) {
|
|
|
1807
1914
|
recovery: [resume],
|
|
1808
1915
|
};
|
|
1809
1916
|
}
|
|
1810
|
-
if (state === "human_action" && model.workflow?.human_action) {
|
|
1917
|
+
if ((state === "human_action" || (state === "failed" && model.workflow?.human_action?.context?.quote_recovery === true)) && model.workflow?.human_action) {
|
|
1811
1918
|
const action = model.workflow.human_action;
|
|
1812
1919
|
const requiredFields = requiredInputFields(action.input_schema);
|
|
1813
1920
|
const rawPlaces = action.context?.places;
|
|
@@ -1837,9 +1944,10 @@ function servicesNextEnvelope(model) {
|
|
|
1837
1944
|
status: "confirmation_required",
|
|
1838
1945
|
result: { service_execution_id: id, service_id: execution.service_id,
|
|
1839
1946
|
human_action: { action_type: action.action_type, input_schema: action.input_schema },
|
|
1840
|
-
required_fields: requiredFields, sides,
|
|
1947
|
+
required_fields: requiredFields, sides, query: model.workflow?.query_input,
|
|
1841
1948
|
...(sides.some(side => side.status !== "resolved" && !side.candidates?.length && ["ADDRESS_REQUIRED", "ADMINISTRATIVE_AREA_UNRESOLVED"].includes(side.reason ?? "")) ? {
|
|
1842
1949
|
new_query_template: { command: `itpay services run ${execution.service_id} --input-json <补全原始需求.json> --json`, executable: false,
|
|
1950
|
+
input: serviceInputTemplate(model.workflow_entry.input_schema, model.workflow?.query_input ?? {}),
|
|
1843
1951
|
required_input: ["补缺失端点", "保留原始日期人数时限偏好及已确认端点"], instruction: "原执行锁定输入;补全原需求后用新查询,不提交不存在的候选。" },
|
|
1844
1952
|
} : {}) },
|
|
1845
1953
|
instruction: sides.some(side => side.status !== "resolved" && !side.candidates?.length)
|
|
@@ -1874,12 +1982,12 @@ function servicesNextEnvelope(model) {
|
|
|
1874
1982
|
? review.requirements_remaining
|
|
1875
1983
|
: undefined;
|
|
1876
1984
|
return {
|
|
1877
|
-
status: "booking_review_required",
|
|
1878
|
-
result: { service_execution_id: id, service_id: execution.service_id, review,
|
|
1985
|
+
status: state === "failed" ? "quote_recovery_required" : "booking_review_required",
|
|
1986
|
+
result: { service_execution_id: id, service_id: execution.service_id, review, ...(state === "failed" ? { failure: model.workflow?.failure } : {}),
|
|
1879
1987
|
action_type: action.action_type, required_fields: requiredFields,
|
|
1880
1988
|
...(requirementsRemaining ? { requirements_remaining: requirementsRemaining } : {}) },
|
|
1881
1989
|
...(reviewURL ? { handoff: { url: reviewURL, kind: "booking_review" } } : {}),
|
|
1882
|
-
instruction: `${reviewURL ? "官方确认页见 handoff.url,打开不等于确认;沿用已知人数和席别。" : "当前草稿见 review。"}
|
|
1990
|
+
instruction: `${state === "failed" ? `${typeof model.workflow?.failure?.affected_leg === "number" ? `第${model.workflow.failure.affected_leg + 1}段` : ""}报价失败:${RAIL_QUOTE_FAILURE_GUIDANCE[model.workflow?.failure?.reason_code ?? ""] ?? "报价核验未完成。"} 当前无完整报价、无付款入口。按用户意愿有界地重新核验一次同一购买;复制下方当前确认模板,未变条款沿用真实同意,价格/车次/席别/人数/条款变化须确认差异。无票或车次变化时先让用户决定,不能自动换路线。支付尝试后不得修订。 ` : ""}${reviewURL ? "官方确认页见 handoff.url,打开不等于确认;沿用已知人数和席别。" : "当前草稿见 review。"} 对照对话只补缺失选择或真实条款同意;无字母偏好自动分配,不默认问字母。仅代码修正且车次、实际席别、人数、价格及条款不变,沿用已有明确确认;实质变化只核对变化。复制 interaction.input_template.input;自动分配使用该对象数组,不传 null 或字符串。只有用户本人已接受当前条款,才把 accept_non_guaranteed 改为 true;选车或委托选票不等于条款同意,false是未同意保护而非模板错误。缺同意时仅用 review.consent_question 问一次;没有该文案时说明座位/卧铺偏好可能自动分配,并按当前notice补整程条款。已有充分同意且条件不变则不再问。身份信息只在官方 Checkout 页填写。`,
|
|
1883
1991
|
next: null,
|
|
1884
1992
|
interaction: {
|
|
1885
1993
|
schema_version: "itpay.interaction.v1",
|
|
@@ -1887,6 +1995,8 @@ function servicesNextEnvelope(model) {
|
|
|
1887
1995
|
input_template: {
|
|
1888
1996
|
command: `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved --input-json <file> --json`,
|
|
1889
1997
|
required_input: ["file", ...requiredFields],
|
|
1998
|
+
input: review.input_template && typeof review.input_template === "object"
|
|
1999
|
+
? review.input_template : serviceInputTemplate(action.input_schema ?? {}, review),
|
|
1890
2000
|
executable: false,
|
|
1891
2001
|
},
|
|
1892
2002
|
},
|
|
@@ -1916,35 +2026,33 @@ function servicesNextEnvelope(model) {
|
|
|
1916
2026
|
command = `itpay services checkout ${id} --json`;
|
|
1917
2027
|
// input_required: the resume command needs a real file path — expose it as
|
|
1918
2028
|
// a non-executable input_template, not a fake `next.command`.
|
|
2029
|
+
const guidance = railServiceGuidance(execution.service_id, model.workflow_entry?.input_schema);
|
|
1919
2030
|
const inputTemplate = state === "input_required"
|
|
1920
2031
|
? {
|
|
1921
2032
|
command: `itpay services run ${execution.service_id} --execution ${id} --input-json <file> --json`,
|
|
1922
|
-
|
|
2033
|
+
input: serviceInputTemplate(model.workflow_entry?.input_schema ?? {}, Object.keys(model.workflow?.query_input ?? {}).length ? (model.workflow?.query_input ?? {}) : guidance?.input_example ?? {}),
|
|
2034
|
+
required_input: ["file", ...requiredInputFields(model.workflow_entry?.input_schema)],
|
|
1923
2035
|
executable: false,
|
|
1924
2036
|
}
|
|
1925
2037
|
: undefined;
|
|
1926
|
-
|
|
2038
|
+
if (state === "input_required" && execution.service_id === "itpay-rail-smart" && !guidance) {
|
|
2039
|
+
return { status: "contract_unavailable", result: { service_execution_id: id, service_id: execution.service_id }, instruction: "当前锁定合同缺失;读取一次同执行合同后仍缺失就停止,不猜新旧输入结构。", next: { command: `itpay services get ${id} --json`, reason: "获取锁定合同一次" }, recovery: [] };
|
|
2040
|
+
}
|
|
1927
2041
|
const failedStep = recovery ? model.workflow?.failure?.step_id : undefined;
|
|
1928
2042
|
const failedStepGuidance = failedStep ? WORKFLOW_STEP_GUIDANCE[failedStep] : undefined;
|
|
1929
2043
|
const failedInvocation = recovery
|
|
1930
2044
|
? [...model.provider_invocations].reverse().find((item) => typeof item.status === "string" && item.status.startsWith("failed"))
|
|
1931
2045
|
: undefined;
|
|
1932
2046
|
const providerErrorCode = typeof failedInvocation?.error_code === "string" && failedInvocation.error_code ? failedInvocation.error_code : undefined;
|
|
1933
|
-
const quoteFailure = execution.service_id === "itpay-rail-booking" ?
|
|
1934
|
-
|
|
1935
|
-
quote_train_changed: "所选车次的时刻或运行信息已变化;向用户说明并征询是否重新查票。",
|
|
1936
|
-
quote_seat_changed: "所选席别已变化;向用户说明并征询新的席别选择。",
|
|
1937
|
-
quote_seat_unavailable: "所选席别当前无足够余票;告知用户并由其决定是否换方案。",
|
|
1938
|
-
quote_refresh_expired: "实时报价过程超时;保留原选择,不把旧价当可付款价格。",
|
|
1939
|
-
}[providerErrorCode ?? ""] : undefined;
|
|
1940
|
-
const failureCode = model.workflow?.failure?.reason_code ?? providerErrorCode ?? model.workflow?.error_code;
|
|
2047
|
+
const quoteFailure = execution.service_id === "itpay-rail-booking" ? RAIL_QUOTE_FAILURE_GUIDANCE[(model.workflow?.failure?.reason_code === "workflow_failed" ? providerErrorCode : model.workflow?.failure?.reason_code) ?? providerErrorCode ?? ""] : undefined;
|
|
2048
|
+
const failureCode = model.workflow?.failure?.reason_code === "workflow_failed" && providerErrorCode && RAIL_QUOTE_FAILURE_GUIDANCE[providerErrorCode] ? providerErrorCode : model.workflow?.failure?.reason_code ?? providerErrorCode ?? model.workflow?.error_code;
|
|
1941
2049
|
const failedInstruction = recovery
|
|
1942
2050
|
? quoteFailure
|
|
1943
|
-
? quoteFailure
|
|
2051
|
+
? `${typeof model.workflow?.failure?.affected_leg === "number" ? `第${model.workflow.failure.affected_leg + 1}段报价失败。` : ""}${quoteFailure} 当前整程未取得完整有效报价,没有付款入口;保留同一购买和未变化的选择,当前没有可提交恢复动作,停止;需要用户决定或系统处理,不能靠反复 get/next 重新报价,不能收取其余段款项或重复创建购买。`
|
|
1944
2052
|
: failedStepGuidance
|
|
1945
2053
|
? `执行在「${failedStepGuidance.meaning}」步失败${failureCode ? `(错误码:${failureCode})` : ""}:${failedStepGuidance.hint}。先核对原因和当前执行状态,不要盲目重放。`
|
|
1946
2054
|
: state === "failed"
|
|
1947
|
-
? `执行已失败${failureCode ? `(错误码:${failureCode})` : ""}
|
|
2055
|
+
? `执行已失败${failureCode ? `(错误码:${failureCode})` : ""}且不可续用;${execution.service_id === "itpay-rail-booking" || paymentVerified ? "核对服务端原因、订单与付款事实" : "保留原地点和约束,核对服务端原因;当前没有可执行恢复则停止,不重复读取终态"}后再决定下一步。`
|
|
1948
2056
|
: "执行未完成,请按步骤错误处理;不要重建执行或重复调用。"
|
|
1949
2057
|
: undefined;
|
|
1950
2058
|
return {
|
|
@@ -1967,8 +2075,8 @@ function servicesNextEnvelope(model) {
|
|
|
1967
2075
|
? (model.workflow?.human_action?.context?.review
|
|
1968
2076
|
? "服务已到付款步骤,可直接 Checkout 付款;如需调整席别/座位偏好,先用 confirm_booking 动作修订(会重新报价并锁定新价),完成后再付款。"
|
|
1969
2077
|
: "服务已到付款步骤,使用现有 Checkout 完成扫码付款。")
|
|
1970
|
-
: guidance
|
|
1971
|
-
? "按 result.guidance 的字段契约填写输入后继续同一服务执行;不要臆造字段名。"
|
|
2078
|
+
: state === "input_required" && guidance
|
|
2079
|
+
? RAIL_ENDPOINT_BRIEF + "按 result.guidance 的字段契约填写输入后继续同一服务执行;不要臆造字段名。"
|
|
1972
2080
|
: "继续读取同一执行;缺少输入时按服务声明补齐。"),
|
|
1973
2081
|
next: recovery || inputTemplate ? null : { command, reason: "继续当前流程" },
|
|
1974
2082
|
...(inputTemplate || (state === "payment" && model.workflow?.human_action?.context?.review) ? {
|
|
@@ -1985,7 +2093,7 @@ function servicesNextEnvelope(model) {
|
|
|
1985
2093
|
} : {}),
|
|
1986
2094
|
},
|
|
1987
2095
|
} : {}),
|
|
1988
|
-
recovery: recovery && state === "failed"
|
|
2096
|
+
recovery: recovery && state === "failed" && !["itpay-rail-exact", "itpay-rail-smart"].includes(execution.service_id)
|
|
1989
2097
|
? [{ command: `itpay services get ${id} --json`, reason: "核对本执行的错误、结果和付款事实" }]
|
|
1990
2098
|
: [],
|
|
1991
2099
|
};
|
|
@@ -2460,6 +2568,36 @@ export async function runServicesRun(backend, config, serviceID, input, options
|
|
|
2460
2568
|
await runServicesNext(backend, id, options);
|
|
2461
2569
|
}
|
|
2462
2570
|
catch (cause) {
|
|
2571
|
+
if (cause instanceof HttpError && cause.code === "capability_input_invalid") {
|
|
2572
|
+
const schema = cause.payload?.input_schema;
|
|
2573
|
+
const executionID = cause.payload?.service_execution_id ?? id;
|
|
2574
|
+
throw new CommandContractError(cause.code, cause.message, "Agent按当前模板自行修正错误字段和JSON层级,保留已有摘要的端点含义及原地点/日期/人数,不重复研究或事后补写冒充查询前摘要;不要向用户询问技术字段。只有真实业务歧义才问人;使用返回的执行身份恢复,未创建时用原服务。", schema ? [] : [{ command: executionID ? `itpay services get ${executionID} --json` : "itpay catalog list --json", reason: "获取当前锁定合同;仍缺合同则停止,不猜旧写法" }], schema ? { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: {
|
|
2575
|
+
command: `itpay services run ${serviceID}${executionID ? ` --execution ${executionID}` : ""} --input-json <file> --json`,
|
|
2576
|
+
input: serviceInputTemplate(schema, input), required_input: ["file", ...requiredInputFields(schema)], executable: false,
|
|
2577
|
+
} } : undefined, { service_id: serviceID, ...(executionID ? { service_execution_id: executionID } : {}), execution_created: cause.payload?.execution_created ?? Boolean(executionID), ...(typeof cause.payload?.provider_called === "boolean" ? { provider_called: cause.payload.provider_called } : {}), input_errors: cause.payload?.input_errors ?? [cause.message], ...(schema ? { input_schema: schema } : {}) });
|
|
2578
|
+
}
|
|
2579
|
+
if (cause instanceof HttpError && ["invalid_selection", "selection_wrong_buyer"].includes(cause.code ?? "")) {
|
|
2580
|
+
const reason = cause.payload?.reason;
|
|
2581
|
+
const inputInvalid = reason === "input_constraint_invalid";
|
|
2582
|
+
throw new CommandContractError(cause.code, cause.message, inputInvalid ? "输入违反当前完整计划约束。读取原查询的完整购买模板,修正人数、完整段数和顺序;不要追加未声明字段或拆单。"
|
|
2583
|
+
: cause.code === "selection_wrong_buyer" ? "当前身份无权使用这份购买凭据。停止;不要切换身份绕过,也不展示其他买家资料。"
|
|
2584
|
+
: reason === "stored_selection_inconsistent" ? "服务端凭据与已存目录不一致,补参数无法修复。停止购买并提供诊断标识 stored_selection_inconsistent;不猜字段、不重新创建购买。"
|
|
2585
|
+
: "购买凭据未通过校验,原因尚未确定且没有安全自动恢复。停止,不把推测报成原因或追加隐藏字段。", inputInvalid ? [{ command: "itpay services list --json", reason: "定位原查询并读取当前完整购买模板" }, { command: "itpay docs show rail-booking --json", reason: "读取正式输入约束" }] : [], undefined, { service_id: serviceID, ...(id ? { service_execution_id: id } : {}), ...(reason ? { reason } : {}), execution_created: Boolean(id) });
|
|
2586
|
+
}
|
|
2587
|
+
if (cause instanceof HttpError && cause.code === "rail_selection_expired") {
|
|
2588
|
+
const expiry = cause.payload?.selection_expiry;
|
|
2589
|
+
const bookingID = expiry?.booking_execution_id ?? id;
|
|
2590
|
+
const refreshInput = !bookingID && expiry?.safe_new_query && ["itpay-rail-smart", "itpay-rail-exact"].includes(expiry.query_service_id) ? expiry.query_input : undefined;
|
|
2591
|
+
const refresh = refreshInput !== undefined;
|
|
2592
|
+
throw new CommandContractError(cause.code, cause.message, refresh ? "刚才余票信息已过时,按原条件刷新一次真实库存(可能消耗查询额度);不需要重复批准同条件查票。有价格、库存或条款变化再确认;不复用旧库存、不换站或日期。"
|
|
2593
|
+
: bookingID ? "保留原购买执行,先读取订单与付款事实;不要重新查票、创建替代购买或重复付款。"
|
|
2594
|
+
: "旧选择凭据已过期,历史详情不能续期;缺少完整原查询条件或安全购买状态证据,停止自动刷新。", bookingID ? [{ command: `itpay services next ${bookingID} --json`, reason: "核对原购买执行和付款状态" }] : [], refreshInput && expiry ? { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: {
|
|
2595
|
+
command: `itpay services run ${expiry.query_service_id} --input-json <file> --json`, input: refreshInput, required_input: ["file"], executable: false,
|
|
2596
|
+
} } : undefined, { service_id: serviceID, ...(bookingID ? { service_execution_id: bookingID } : {}), ...(expiry ? { selection_expiry: expiry } : {}) });
|
|
2597
|
+
}
|
|
2598
|
+
if (cause instanceof HttpError && cause.code === "rail_journey_selection_required") {
|
|
2599
|
+
throw new CommandContractError(cause.code, cause.message, "整程必须使用原查询当前完整 ticket plan 的 selections 模板;手填 legs 只兼容单腿。缺少整程模板时读取原方案;不拆单、不拼腿、不擅自换站或日期。", [{ command: "itpay services list --json", reason: "找回原查询并读取其完整方案模板" }, { command: "itpay docs show rail-booking --json", reason: "读取正式整程输入合同" }], undefined, { service_id: serviceID, ...(id ? { service_execution_id: id } : {}) });
|
|
2600
|
+
}
|
|
2463
2601
|
if (cause instanceof HttpError && cause.code === "rail_seat_code_invalid" && id) {
|
|
2464
2602
|
throw new CommandContractError(cause.code, cause.message, "席别必须使用原查询 booking_template.seat_choices 中与名称对应的代码,不能填“二等座”等显示名。用原 selection token 与真实人数修正输入,继续同一booking execution;若只是代码写法修正且车次、席别名称、人数等购买条件未变,沿用用户已给的确认,不再重复询问。", [{ command: `itpay services next ${id} --json`, reason: "读取同一booking当前状态后修正输入" }]);
|
|
2465
2603
|
}
|