@itpay/cli 2.1.34 → 2.1.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/client/backend.js +5 -0
- package/dist/src/commands/aftercare.js +12 -0
- package/dist/src/commands/buy.js +12 -56
- package/dist/src/commands/cart.js +2 -0
- package/dist/src/commands/catalog.js +24 -9
- package/dist/src/commands/checkout_handoff.js +11 -99
- package/dist/src/commands/guidance.js +1 -184
- package/dist/src/commands/order.js +20 -2
- package/dist/src/commands/rail_catalog.js +1 -261
- package/dist/src/commands/readyz.js +2 -2
- package/dist/src/commands/refund.js +10 -54
- package/dist/src/commands/services/actions.js +133 -0
- package/dist/src/commands/services/commerce.js +197 -0
- package/dist/src/commands/services/lifecycle.js +418 -0
- package/dist/src/commands/services/results.js +130 -0
- package/dist/src/commands/services.js +14 -2613
- package/dist/src/main.js +81 -30
- package/dist/src/render/checkout_handoff.js +101 -0
- package/dist/src/render/checkout_plan.js +47 -0
- package/dist/src/services/adapter.js +1 -0
- package/dist/src/services/presentation.js +198 -0
- package/dist/src/services/rail/actions.js +51 -0
- package/dist/src/services/rail/adapter.js +27 -0
- package/dist/src/services/rail/aftercare.js +45 -0
- package/dist/src/services/rail/catalog.js +261 -0
- package/dist/src/services/rail/checkout.js +19 -0
- package/dist/src/services/rail/fulfillment.js +76 -0
- package/dist/src/services/rail/guide.js +110 -0
- package/dist/src/services/rail/invocation.js +54 -0
- package/dist/src/services/rail/page.js +76 -0
- package/dist/src/services/rail/pagination.js +6 -0
- package/dist/src/services/rail/query.js +71 -0
- package/dist/src/services/rail/readers.js +107 -0
- package/dist/src/services/rail/reading.js +125 -0
- package/dist/src/services/rail/recovery.js +51 -0
- package/dist/src/services/rail/results.js +378 -0
- package/dist/src/services/rail/review.js +268 -0
- package/dist/src/services/rail/saved.js +49 -0
- package/dist/src/services/rail/wait.js +11 -0
- package/dist/src/services/registry.js +31 -0
- package/dist/src/services/schema.js +21 -0
- package/dist/src/services/shared/envelope.js +184 -0
- package/dist/src/services/shared/input.js +39 -0
- package/dist/src/services/shared/pagination.js +28 -0
- package/dist/src/services/shared/platform.js +365 -0
- package/dist/src/services/shared/refund.js +40 -0
- package/dist/src/services/shared/workflow.js +28 -0
- package/dist/src/state/account_auth.js +25 -10
- package/dist/src/state/config.js +2 -2
- package/docs/agent/buyer/cart-checkout.json +2 -1
- package/docs/agent/buyer/catalog-list.json +6 -4
- package/docs/agent/buyer/identity-and-sessions.json +3 -2
- package/docs/agent/buyer/install-and-setup.json +8 -3
- package/docs/agent/buyer/orders-refunds.json +14 -5
- package/docs/agent/buyer/payment-flow.json +2 -1
- package/docs/agent/buyer/quickstart.json +3 -2
- package/docs/agent/buyer/rail-booking.json +38 -24
- package/docs/agent/buyer/service-feedback.json +2 -2
- package/docs/cli-reference/commands/aftercare.md +15 -0
- package/docs/cli-reference/commands/cart/add.md +4 -0
- package/docs/cli-reference/commands/cart/next.md +4 -0
- package/docs/cli-reference/commands/readyz.md +2 -50
- package/docs/cli-reference/commands/services/checkout.md +4 -0
- package/docs/cli-reference/commands/services/next.md +8 -35
- package/docs/cli-reference/commands/services/quote.md +1 -1
- package/docs/cli-reference/commands/services/run.md +8 -12
- package/docs/cli-reference/commands/services/start.md +3 -1
- package/docs/cli-reference/conventions.md +1 -1
- package/docs/cli-reference/index.md +2 -0
- package/package.json +4 -3
- package/skills/itpay/SKILL.md +37 -25
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { railAftercareOrder, railAftercareList, railAftercareEnvelope, railAftercareCreate, railAftercareRespond } from "./aftercare.js";
|
|
2
|
+
import { railInputTemplate, railServiceGuidance, RAIL_ENDPOINT_BRIEF, locationInputInstruction, railOverviewInstruction } from "./guide.js";
|
|
3
|
+
import { railShouldWait, railPollAfter } from "./wait.js";
|
|
4
|
+
import { railInvocationProjection } from "./invocation.js";
|
|
5
|
+
import { railInputRecoveryInstruction, railRunError, railActionError } from "./recovery.js";
|
|
6
|
+
import { railWorkflowEnvelope } from "./review.js";
|
|
7
|
+
import { railPlanningEnvelope } from "./results.js";
|
|
8
|
+
import { railBookingEnvelope } from "./fulfillment.js";
|
|
9
|
+
import { railSavedResultEnvelope, railSavedDeliveryGuidance } from "./saved.js";
|
|
10
|
+
import { railCheckoutItinerary } from "./checkout.js";
|
|
11
|
+
import { renderRailResultPage } from "./page.js";
|
|
12
|
+
import { readSavedResult } from "./reading.js";
|
|
13
|
+
export const railAdapter = {
|
|
14
|
+
aftercare: { order: railAftercareOrder, directory: railAftercareList, present: railAftercareEnvelope, create: railAftercareCreate, respond: railAftercareRespond },
|
|
15
|
+
entryInstruction: RAIL_ENDPOINT_BRIEF + "按 result.guidance 逐项填写输入(when_to_use 说明本服务适用场景、input_fields 是必填契约、input_example 是待填示意,必须替换用户真实值),然后继续同一服务执行。不要臆造字段名。",
|
|
16
|
+
supported: true, inputTemplate: railInputTemplate, inputInstruction: railInputRecoveryInstruction,
|
|
17
|
+
guidance: railServiceGuidance, requiresGuidance: id => id === "itpay-rail-smart",
|
|
18
|
+
overviewInstruction: railOverviewInstruction, locationInstruction: locationInputInstruction,
|
|
19
|
+
invocation: railInvocationProjection, shouldWait: railShouldWait, pollAfter: railPollAfter,
|
|
20
|
+
preservesTerminalRecovery: model => model.workflow?.human_action?.context?.quote_recovery === true,
|
|
21
|
+
projectState: model => model.rail_booking ? railBookingEnvelope(model) : model.rail_planning ? railPlanningEnvelope(model) : null,
|
|
22
|
+
workflow: railWorkflowEnvelope, savedResult: railSavedResultEnvelope, deliveryGuidance: railSavedDeliveryGuidance,
|
|
23
|
+
extraFacts: model => model.rail_booking ? { rail_booking: model.rail_booking } : {},
|
|
24
|
+
runError: railRunError, actionError: railActionError, readResult: readSavedResult,
|
|
25
|
+
renderPage: (...args) => { renderRailResultPage(...args); return true; },
|
|
26
|
+
checkout: async (backend, model, id, token) => railCheckoutItinerary(model, await backend.getCheckoutPresentation(id, token)),
|
|
27
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { operationID } from "../../state/config.js";
|
|
3
|
+
import { CommandContractError } from "../shared/envelope.js";
|
|
4
|
+
const target = z.object({ leg_index: z.number().int().nonnegative(), passenger_index: z.number().int().nonnegative() }).strict();
|
|
5
|
+
const inputSchema = z.object({ operation: z.enum(["refund", "change"]), targets: z.array(target).min(1).max(15), reason: z.string().trim().min(1).max(1000), scope_revision: z.literal(1), user_confirmed_scope: z.literal(true), change: z.object({ travel_date: z.string().optional(), departure_after: z.string().optional(), arrival_before: z.string().optional(), seat_type: z.string().optional(), train_code: z.string().optional(), constraints: z.string().min(1).max(1000) }).strict().optional() }).strict();
|
|
6
|
+
const responseSchema = z.object({ expected_revision: z.number().int().positive(), proposal_revision: z.number().int().positive().optional(), user_approved: z.boolean().optional(), additional_information: z.string().max(1000).optional(), reason: z.string().max(1000).optional() }).strict();
|
|
7
|
+
export function railAftercareList(directory, orderID) {
|
|
8
|
+
return { status: "aftercare_directory", result: { ...directory }, instruction: directory.instruction + "段/乘客索引从0起,按返回事实选目标;明确‘第二段’用leg_index:1。整程模板列全部可用目标,指定范围则仅保留对应目标,不手填票价/供应商单号。范围已明确只说明后提交;不重新询问整程选择。", next: null, recovery: [], interaction: { schema_version: "itpay.interaction.v1", stage: "input_required", input_templates: Object.fromEntries(Object.entries(directory.input_templates).map(([operation, input]) => [operation, { command: `itpay aftercare create --order ${orderID} --input-json <file> --json`, input, executable: false, required_input: ["file", "targets", "reason", "user_confirmed_scope"] }])) } };
|
|
9
|
+
}
|
|
10
|
+
export function railAftercareEnvelope(view) {
|
|
11
|
+
const c = view.case;
|
|
12
|
+
const instruction = c.status === "awaiting_confirmation" ? "展示当前具体改签方案、旧新对应、差价和条件;旧购买委托不是新方案/费用批准。没有真实回答保持false并等待;本轮费用由人工协调,没有自动支付链接。"
|
|
13
|
+
: c.status === "needs_input" ? "展示管理员要求补充的内容,只补缺失事实;保留原申请范围。"
|
|
14
|
+
: c.status === "completed" ? "人工受理已完成;分别说明供应商核验结果与平台退款金额/状态。未退段仅报告权威已知事实,不保证仍可乘;不恢复平台内容访问锁。"
|
|
15
|
+
: c.status === "processing" ? "原申请正在人工办理或财务处理中;没有最终事实不说退票、改签或到账成功,不重试未知资金、不取消已发生操作。"
|
|
16
|
+
: ["cancelled", "rejected"].includes(c.status) ? "说明原申请已取消或被拒绝及原因;没有撤销已发生供应商操作,也不自动创建替代购买。"
|
|
17
|
+
: "已提交人工受理,尚未确认供应商退票/改签或钱款到账。先交付申请状态入口,再按用户意愿查询原申请,不循环轮询。";
|
|
18
|
+
const cancellation = view.can_cancel ? { cancel_template: { command: `itpay aftercare cancel ${c.case_id} --input-json <file> --json`, input: { expected_revision: c.revision, reason: "" }, executable: false, required_input: ["file"] } } : {};
|
|
19
|
+
const needsReply = ["awaiting_confirmation", "needs_input"].includes(c.status);
|
|
20
|
+
return { status: c.status, result: { ...view }, instruction, next: needsReply || ["completed", "cancelled", "rejected"].includes(c.status) ? null : { command: `itpay aftercare get ${c.case_id} --json`, reason: "按用户要求读取同一人工申请" }, recovery: [], ...((needsReply || view.can_cancel) ? { interaction: { schema_version: "itpay.interaction.v1", stage: c.status === "awaiting_confirmation" ? "consent_required" : "input_required", ...cancellation, ...(needsReply ? { input_template: { command: `itpay aftercare respond ${c.case_id} --input-json <file> --json`, input: { expected_revision: c.revision, ...(c.status === "awaiting_confirmation" ? { proposal_revision: c.proposal_revision, user_approved: false } : { additional_information: "" }) }, executable: false, required_input: ["file"] } } : {}) } } : {}) };
|
|
21
|
+
}
|
|
22
|
+
export async function railAftercareCreate(backend, config, orderID, raw) {
|
|
23
|
+
const parsed = inputSchema.safeParse(raw);
|
|
24
|
+
if (!parsed.success)
|
|
25
|
+
throw new CommandContractError("rail_aftercare_input_invalid", "Aftercare input fields or genuine scope confirmation are missing", "Agent读取当前完整模板修正技术字段;只问缺失的真实范围/改签意向,不让用户填技术JSON。", [{ command: `itpay aftercare list --order ${orderID} --json`, reason: "读取当前行程、目标和完整模板" }]);
|
|
26
|
+
const value = parsed.data;
|
|
27
|
+
const change = value.change;
|
|
28
|
+
const input = { operation: value.operation, targets: value.targets, reason: value.reason, scope_revision: value.scope_revision, user_confirmed_scope: value.user_confirmed_scope, ...(change ? { change: { constraints: change.constraints, ...(change.travel_date !== undefined ? { travel_date: change.travel_date } : {}), ...(change.departure_after !== undefined ? { departure_after: change.departure_after } : {}), ...(change.arrival_before !== undefined ? { arrival_before: change.arrival_before } : {}), ...(change.seat_type !== undefined ? { seat_type: change.seat_type } : {}), ...(change.train_code !== undefined ? { train_code: change.train_code } : {}) } } : {}) };
|
|
29
|
+
return railAftercareEnvelope(await backend.createRailAftercare(orderID, input, await operationID(config, `aftercare.create:${orderID}:${JSON.stringify(input)}`)));
|
|
30
|
+
}
|
|
31
|
+
export async function railAftercareGet(backend, id) { return railAftercareEnvelope(await backend.getRailAftercare(id)); }
|
|
32
|
+
export async function railAftercareRespond(backend, id, raw, cancel = false) {
|
|
33
|
+
const parsed = responseSchema.safeParse(raw);
|
|
34
|
+
if (!parsed.success)
|
|
35
|
+
throw new CommandContractError("rail_aftercare_input_invalid", "Current revision or response fields are missing", "使用原申请当前返回模板;实质方案变化须重新取得明确同意。", [{ command: `itpay aftercare get ${id} --json`, reason: "读取同一申请当前方案和版本" }]);
|
|
36
|
+
const value = parsed.data;
|
|
37
|
+
const input = { expected_revision: value.expected_revision, ...(value.proposal_revision !== undefined ? { proposal_revision: value.proposal_revision } : {}), ...(value.user_approved !== undefined ? { user_approved: value.user_approved } : {}), ...(value.additional_information !== undefined ? { additional_information: value.additional_information } : {}), ...(value.reason !== undefined ? { reason: value.reason } : {}) };
|
|
38
|
+
return railAftercareEnvelope(await (cancel ? backend.cancelRailAftercare(id, input) : backend.respondRailAftercare(id, input)));
|
|
39
|
+
}
|
|
40
|
+
export async function railAftercareOrder(backend, order, envelope) {
|
|
41
|
+
if (!["paid", "delivery_pending", "delivered", "partially_refunded", "refunded"].includes(order.status))
|
|
42
|
+
return envelope;
|
|
43
|
+
const directory = await backend.listRailAftercare(order.order_id);
|
|
44
|
+
return { ...envelope, result: { ...envelope.result, journey: directory.journey, aftercare_cases: directory.cases }, instruction: (order.status === "partially_refunded" ? "订单为部分退款;仅本次选中票的供应商/金额事实已变化,不能说全程已退。" : "按原订单读取票段与人工售后事实。") + " 平台内容锁保持;脱敏剩余票段状态是合法只读事实,不保证历史已出票仍可乘坐。", next: { command: `itpay aftercare list --order ${order.order_id} --json`, reason: "读取原行程、剩余票段、申请目标及完整售后模板" } };
|
|
45
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { CLI_VERSION, cliDistribution } from "../../state/config.js";
|
|
2
|
+
export class RailCatalogEncodingError extends Error {
|
|
3
|
+
constructor(encoding) {
|
|
4
|
+
super(`catalog encoding ${JSON.stringify(encoding)} is not supported by CLI ${CLI_VERSION} ` +
|
|
5
|
+
`(${cliDistribution()}); obtain a compatible exact version through this installation channel. ` +
|
|
6
|
+
`The committed catalog is intact`);
|
|
7
|
+
this.name = "rail_catalog_encoding";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function scopeFromProfiles(value) {
|
|
11
|
+
const profiles = Array.isArray(value) ? value : [];
|
|
12
|
+
const selected = profiles.find((p) => Array.isArray(p) && Array.isArray(p[1]) && p[1].includes("balanced")) ?? profiles[0];
|
|
13
|
+
if (!Array.isArray(selected))
|
|
14
|
+
return { origin: "legacy_unknown", destination: "legacy_unknown",
|
|
15
|
+
duration_basis: "legacy_unknown", arrival_basis: "legacy_unknown", cost_basis: "legacy_unknown",
|
|
16
|
+
duration_minutes: null, estimated_total_cost: null, cost_incomplete: true };
|
|
17
|
+
const extras = selected[10] && typeof selected[10] === "object" ? selected[10] : {};
|
|
18
|
+
const total = typeof selected[9] === "number" && extras.ci !== true ? selected[9] / 100 : null;
|
|
19
|
+
return {
|
|
20
|
+
origin: extras.os ?? "legacy_unknown", destination: extras.ds ?? "legacy_unknown",
|
|
21
|
+
duration_basis: extras.db ?? "legacy_unknown", arrival_basis: extras.ab ?? "legacy_unknown",
|
|
22
|
+
cost_basis: extras.cb ?? "legacy_unknown", duration_minutes: selected[8] ?? null,
|
|
23
|
+
estimated_total_cost: total, cost_incomplete: extras.ci === true,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function asString(v) {
|
|
27
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
28
|
+
}
|
|
29
|
+
function asIndex(v, bound) {
|
|
30
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 0 || v >= bound) {
|
|
31
|
+
throw new Error("shared_rows: index out of bounds");
|
|
32
|
+
}
|
|
33
|
+
return v;
|
|
34
|
+
}
|
|
35
|
+
function asObject(value) {
|
|
36
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
37
|
+
}
|
|
38
|
+
function datedTime(date, value) {
|
|
39
|
+
const raw = asString(value);
|
|
40
|
+
if (!raw)
|
|
41
|
+
return null;
|
|
42
|
+
if (/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(raw))
|
|
43
|
+
return raw.replace("T", " ").slice(0, 16);
|
|
44
|
+
const short = raw.match(/^(\d{2}:\d{2})(?:\+(\d+))?$/);
|
|
45
|
+
const day = asString(date);
|
|
46
|
+
if (!short || !day)
|
|
47
|
+
return null;
|
|
48
|
+
if (!short[2])
|
|
49
|
+
return `${day} ${short[1]}`;
|
|
50
|
+
const shifted = new Date(`${day}T00:00:00Z`);
|
|
51
|
+
if (Number.isNaN(shifted.getTime()))
|
|
52
|
+
return null;
|
|
53
|
+
shifted.setUTCDate(shifted.getUTCDate() + Number(short[2]));
|
|
54
|
+
return `${shifted.toISOString().slice(0, 10)} ${short[1]}`;
|
|
55
|
+
}
|
|
56
|
+
function elapsedMinutes(departure, arrival) {
|
|
57
|
+
if (!departure || !arrival)
|
|
58
|
+
return null;
|
|
59
|
+
const start = Date.parse(departure.replace(" ", "T") + "Z");
|
|
60
|
+
const end = Date.parse(arrival.replace(" ", "T") + "Z");
|
|
61
|
+
return Number.isFinite(start) && Number.isFinite(end) && end >= start ? (end - start) / 60_000 : null;
|
|
62
|
+
}
|
|
63
|
+
function seatOffer(row, names) {
|
|
64
|
+
if (!Array.isArray(row))
|
|
65
|
+
return null;
|
|
66
|
+
const extras = asObject(row[7]);
|
|
67
|
+
const code = asString(row[1]);
|
|
68
|
+
return {
|
|
69
|
+
offer_ref: row[0], seat_type: code ?? null, seat_name: code ? names[code] ?? code : null,
|
|
70
|
+
inventory_status: row[2] ?? null, quantity: row[3] ?? null, unit_price_fen: row[4] ?? null,
|
|
71
|
+
eligible: row[5] === true, requires_quote: row[6] === true,
|
|
72
|
+
price_source: extras.s ?? "sale_price", quote_price_unverified: extras.u === true,
|
|
73
|
+
conflicted: extras.c === true,
|
|
74
|
+
...(Array.isArray(extras.o) ? { price_observations_fen: extras.o } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function decodeRide(row, wait, stations, services) {
|
|
78
|
+
if (!Array.isArray(row)) {
|
|
79
|
+
const ride = asObject(row);
|
|
80
|
+
if (!Object.keys(ride).length)
|
|
81
|
+
throw new Error("rail catalog: malformed ride row");
|
|
82
|
+
const refs = Array.isArray(ride.service_refs) ? ride.service_refs : [];
|
|
83
|
+
const codes = [...new Set(refs.map((ref) => asObject(services[asString(ref) ?? ""]).tc)
|
|
84
|
+
.filter((code) => typeof code === "string" && code.length > 0))];
|
|
85
|
+
const departure = datedTime(ride.boarding_date, ride.departure_at ?? ride.departure);
|
|
86
|
+
const arrival = datedTime(ride.boarding_date, ride.arrival_at ?? ride.arrival);
|
|
87
|
+
return { train_codes: codes, train_code: codes.join("/"),
|
|
88
|
+
from_station: stations[asString(ride.from_station_code) ?? ""] ?? ride.from_station ?? null,
|
|
89
|
+
to_station: stations[asString(ride.to_station_code) ?? ""] ?? ride.to_station ?? null,
|
|
90
|
+
boarding_date: ride.boarding_date ?? null,
|
|
91
|
+
departure, arrival, duration_minutes: elapsedMinutes(departure, arrival),
|
|
92
|
+
wait_minutes: typeof wait === "number" ? wait : ride.wait_minutes ?? null,
|
|
93
|
+
same_run: ride.same_run === true, onboard_stops: Array.isArray(ride.onboard_stops) ? ride.onboard_stops : [],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const extras = asObject(row[8]);
|
|
97
|
+
const refs = Array.isArray(row[4]) ? row[4] : [];
|
|
98
|
+
const codes = [...new Set(refs.map((ref) => asObject(services[asString(ref) ?? ""]).tc)
|
|
99
|
+
.filter((code) => typeof code === "string" && code.length > 0))];
|
|
100
|
+
const base = row[0];
|
|
101
|
+
const departure = datedTime(base, extras.dep);
|
|
102
|
+
const arrival = datedTime(base, extras.arr);
|
|
103
|
+
return {
|
|
104
|
+
train_codes: codes, train_code: codes.join("/"),
|
|
105
|
+
from_station: stations[asString(row[2]) ?? ""] ?? row[2] ?? null,
|
|
106
|
+
to_station: stations[asString(row[3]) ?? ""] ?? row[3] ?? null,
|
|
107
|
+
boarding_date: base ?? null,
|
|
108
|
+
departure, arrival, duration_minutes: elapsedMinutes(departure, arrival),
|
|
109
|
+
wait_minutes: typeof wait === "number" ? wait : typeof row[5] === "number" ? row[5] : null,
|
|
110
|
+
same_run: row[6] === true, onboard_stops: Array.isArray(row[7]) ? row[7] : [],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function decodePlans(rawPlans, rawProfiles, packed, catalog, services, names, stations) {
|
|
114
|
+
const plans = Array.isArray(rawPlans) ? rawPlans : [];
|
|
115
|
+
const profiles = Array.isArray(rawProfiles) ? rawProfiles : [];
|
|
116
|
+
const preferred = profiles.find((profile) => Array.isArray(profile) && Array.isArray(profile[1]) && profile[1].includes("balanced")) ?? profiles[0];
|
|
117
|
+
const profile = Array.isArray(preferred) ? preferred : [];
|
|
118
|
+
const serviceIndex = Array.isArray(catalog.service_index) ? catalog.service_index : [];
|
|
119
|
+
const selectedPlan = packed && typeof profile[2] === "number" ? plans[asIndex(profile[2], plans.length)] :
|
|
120
|
+
plans.find((plan) => Array.isArray(plan) && plan[0] === profile[2]);
|
|
121
|
+
const selectedRef = Array.isArray(selectedPlan) ? selectedPlan[0] : null;
|
|
122
|
+
const decoded = plans.map((plan) => {
|
|
123
|
+
if (!Array.isArray(plan))
|
|
124
|
+
throw new Error("rail catalog: malformed ticket plan");
|
|
125
|
+
const refs = (Array.isArray(plan[1]) ? plan[1] : []).map((ref) => packed && typeof ref === "number" ? serviceIndex[asIndex(ref, serviceIndex.length)] : ref);
|
|
126
|
+
const extras = asObject(plan[7]);
|
|
127
|
+
const legs = refs.map((ref, index) => {
|
|
128
|
+
const service = asObject(services[asString(ref) ?? ""]);
|
|
129
|
+
const offers = Array.isArray(service.of) ? service.of : [];
|
|
130
|
+
const selectedOffer = plan[0] === selectedRef && Array.isArray(profile[3]) ? profile[3][index] : undefined;
|
|
131
|
+
const chosen = packed && typeof selectedOffer === "number" ? offers[asIndex(selectedOffer, offers.length)] :
|
|
132
|
+
offers.find((offer) => Array.isArray(offer) && offer[0] === selectedOffer);
|
|
133
|
+
return {
|
|
134
|
+
service_ref: ref ?? null, train_code: service.tc ?? null,
|
|
135
|
+
from_station: stations[asString(service.f) ?? ""] ?? service.f ?? null,
|
|
136
|
+
to_station: stations[asString(service.t) ?? ""] ?? service.t ?? null,
|
|
137
|
+
departure: datedTime(service.d, service.dep), arrival: datedTime(service.d, service.arr),
|
|
138
|
+
seat_options: offers.map((offer) => seatOffer(offer, names)).filter((offer) => offer !== null),
|
|
139
|
+
...(chosen ? { selected_seat: seatOffer(chosen, names) } : {}),
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
const amount = asObject(plan[3]);
|
|
143
|
+
return {
|
|
144
|
+
ticket_plan_ref: plan[0], passenger_count: plan[2] ?? null,
|
|
145
|
+
purchase_support: plan[6] ?? "none", rail_amount_verified: plan[4] === true,
|
|
146
|
+
rail_amount_fen: { default: amount.def ?? null, minimum: amount.min ?? null, maximum: amount.max ?? null },
|
|
147
|
+
service_fee_fen: plan[5] ?? null, quote_price_unverified: extras.qu === true,
|
|
148
|
+
legs,
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
const selected = decoded.find((plan) => plan.ticket_plan_ref === selectedRef);
|
|
152
|
+
return { plans: decoded, ...(selected ? { selected } : {}) };
|
|
153
|
+
}
|
|
154
|
+
// _cn_route_text port: "南头 C7608 → 广州南换乘71分 → G2944 重庆西" — kept
|
|
155
|
+
// byte-identical with the planner so a packed journey renders the same text
|
|
156
|
+
// its object form carried.
|
|
157
|
+
function regenerateRoute(ridePairs, rideTable, stations, services) {
|
|
158
|
+
const name = (code, fallback) => asString(stations[asString(code) ?? ""]) ?? asString(fallback) ?? asString(code) ?? "?";
|
|
159
|
+
const rides = ridePairs.map((pair) => {
|
|
160
|
+
if (!Array.isArray(pair) || pair.length !== 2)
|
|
161
|
+
throw new Error("shared_rows: malformed ride pair");
|
|
162
|
+
const src = rideTable[asIndex(pair[0], rideTable.length)];
|
|
163
|
+
if (!Array.isArray(src))
|
|
164
|
+
throw new Error("shared_rows: malformed ride row");
|
|
165
|
+
return { row: src, wait: pair[1] };
|
|
166
|
+
});
|
|
167
|
+
const first = rides[0];
|
|
168
|
+
if (first === undefined)
|
|
169
|
+
return "";
|
|
170
|
+
const trainCodes = (rideRow) => (Array.isArray(rideRow[4]) ? rideRow[4] : [])
|
|
171
|
+
.map((ref) => {
|
|
172
|
+
const svc = services[asString(ref) ?? ""];
|
|
173
|
+
return asString(svc?.tc);
|
|
174
|
+
})
|
|
175
|
+
.filter((c) => Boolean(c));
|
|
176
|
+
const waitOf = (ride) => typeof ride.wait === "number" ? ride.wait
|
|
177
|
+
: typeof ride.row[5] === "number" ? ride.row[5]
|
|
178
|
+
: undefined;
|
|
179
|
+
const sameRun = (rideRow) => rideRow[6] === true;
|
|
180
|
+
const parts = [`${name(first.row[2], undefined)} ${trainCodes(first.row).join("/")}`];
|
|
181
|
+
for (let i = 1; i < rides.length; i += 1) {
|
|
182
|
+
const prev = rides[i - 1].row;
|
|
183
|
+
const ride = rides[i];
|
|
184
|
+
const via = name(prev[3], undefined);
|
|
185
|
+
const wait = waitOf(ride);
|
|
186
|
+
parts.push(sameRun(ride.row)
|
|
187
|
+
? wait !== undefined ? `${via}同车停${wait}分` : `${via}同车接续`
|
|
188
|
+
: wait !== undefined ? `${via}换乘${wait}分` : `${via}换乘`);
|
|
189
|
+
parts.push(`${trainCodes(ride.row).join("/")} ${name(ride.row[3], undefined)}`);
|
|
190
|
+
}
|
|
191
|
+
if (rides.length === 1) {
|
|
192
|
+
parts.push(name(first.row[3], undefined));
|
|
193
|
+
}
|
|
194
|
+
return parts.filter((s) => s.trim().length > 0).join(" → ");
|
|
195
|
+
}
|
|
196
|
+
// decodeRailCatalogJourneys returns one summary per committed combination in
|
|
197
|
+
// catalog order. Legacy object-form catalogs (no `encoding`) pass through;
|
|
198
|
+
// shared_rows.v1 rows are positional under journey_columns
|
|
199
|
+
// [ref, route, rides, plans, gc, pr, profiles, tc, ev, rep, tw, risk].
|
|
200
|
+
// Any other encoding throws RailCatalogEncodingError — an explicit upgrade
|
|
201
|
+
// hint, never an empty catalog.
|
|
202
|
+
export function decodeRailCatalogJourneys(catalog) {
|
|
203
|
+
const encoding = catalog["encoding"];
|
|
204
|
+
const layerMap = catalog["choice_layers"]?.["journey_layer"] ?? {};
|
|
205
|
+
const layerOf = (ref) => layerMap[ref] === "backup" ? "backup" : "main";
|
|
206
|
+
const stations = asObject(catalog.stations);
|
|
207
|
+
const services = asObject(catalog.services);
|
|
208
|
+
const seatNames = asObject(catalog.seat_names);
|
|
209
|
+
if (encoding === undefined || encoding === null) {
|
|
210
|
+
const rows = Array.isArray(catalog["journeys"]) ? catalog["journeys"] : [];
|
|
211
|
+
return {
|
|
212
|
+
packed: false,
|
|
213
|
+
journeys: rows.map((j) => {
|
|
214
|
+
const rec = (j ?? {});
|
|
215
|
+
const ref = asString(rec["ref"]) ?? asString(rec["journey_id"]) ?? "?";
|
|
216
|
+
const calculationScope = scopeFromProfiles(rec["profiles"]);
|
|
217
|
+
const rides = (Array.isArray(rec.rides) ? rec.rides : []).map((ride) => decodeRide(ride, null, stations, services));
|
|
218
|
+
const ticketPlans = decodePlans(rec.plans, rec.profiles, false, catalog, services, seatNames, stations);
|
|
219
|
+
return {
|
|
220
|
+
ref,
|
|
221
|
+
route: asString(rec["route"]) ?? asString(rec["route_text"]) ?? asString(rec["summary"]) ?? "",
|
|
222
|
+
defaultLayer: layerOf(ref),
|
|
223
|
+
rides,
|
|
224
|
+
...(calculationScope ? { calculation_scope: calculationScope } : {}),
|
|
225
|
+
ticket_plans: ticketPlans.plans,
|
|
226
|
+
...(ticketPlans.selected ? { selected_plan: ticketPlans.selected } : {}),
|
|
227
|
+
};
|
|
228
|
+
}),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (encoding !== "shared_rows.v1") {
|
|
232
|
+
throw new RailCatalogEncodingError(encoding);
|
|
233
|
+
}
|
|
234
|
+
const columns = catalog["journey_columns"];
|
|
235
|
+
if (!Array.isArray(columns) || columns[0] !== "ref" || columns[1] !== "route" || columns[2] !== "rides") {
|
|
236
|
+
throw new RailCatalogEncodingError(`${String(encoding)} (unexpected journey_columns)`);
|
|
237
|
+
}
|
|
238
|
+
const rideTable = (Array.isArray(catalog["ride_table"]) ? catalog["ride_table"] : []);
|
|
239
|
+
const journeys = (Array.isArray(catalog["journeys"]) ? catalog["journeys"] : []);
|
|
240
|
+
return {
|
|
241
|
+
packed: true,
|
|
242
|
+
journeys: journeys.map((row) => {
|
|
243
|
+
if (!Array.isArray(row))
|
|
244
|
+
throw new Error("shared_rows: journey row is not an array");
|
|
245
|
+
const ref = asString(row[0]) ?? "?";
|
|
246
|
+
const route = asString(row[1]) ??
|
|
247
|
+
(Array.isArray(row[2]) ? regenerateRoute(row[2], rideTable, stations, services) : "");
|
|
248
|
+
const rides = (Array.isArray(row[2]) ? row[2] : []).map((pair) => {
|
|
249
|
+
if (!Array.isArray(pair) || pair.length !== 2)
|
|
250
|
+
throw new Error("shared_rows: malformed ride pair");
|
|
251
|
+
return decodeRide(rideTable[asIndex(pair[0], rideTable.length)], pair[1], stations, services);
|
|
252
|
+
});
|
|
253
|
+
const calculationScope = scopeFromProfiles(row[6]);
|
|
254
|
+
const ticketPlans = decodePlans(row[3], row[6], true, catalog, services, seatNames, stations);
|
|
255
|
+
return { ref, route, defaultLayer: layerOf(ref), rides,
|
|
256
|
+
...(calculationScope ? { calculation_scope: calculationScope } : {}),
|
|
257
|
+
ticket_plans: ticketPlans.plans,
|
|
258
|
+
...(ticketPlans.selected ? { selected_plan: ticketPlans.selected } : {}) };
|
|
259
|
+
}),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function railCheckoutItinerary(booking, presentation) {
|
|
2
|
+
const review = booking.workflow?.human_action?.context?.review;
|
|
3
|
+
const quote = presentation.rail_quote;
|
|
4
|
+
const facts = quote?.legs ?? booking.rail_booking?.legs ?? (Array.isArray(review?.legs) ? review.legs : []);
|
|
5
|
+
const legs = facts.filter(value => value && typeof value === "object").map(value => {
|
|
6
|
+
const leg = value;
|
|
7
|
+
return { travel_date: leg.travel_date, train_code: leg.train_code,
|
|
8
|
+
from_station: leg.from || leg.from_station || `站点代码 ${String(leg.from_station_code ?? "未知")}(名称暂缺)`,
|
|
9
|
+
to_station: leg.to || leg.to_station || `站点代码 ${String(leg.to_station_code ?? "未知")}(名称暂缺)`,
|
|
10
|
+
departure: leg.departure ?? leg.departure_time, arrival: leg.arrival ?? leg.arrival_time,
|
|
11
|
+
arrival_days: leg.arrival_days, seat_name: leg.seat_name || leg.seat_type_name || `席别代码 ${String(leg.seat_type ?? "未知")}(名称暂缺)` };
|
|
12
|
+
});
|
|
13
|
+
if (legs.length) {
|
|
14
|
+
const itinerary = { legs, passengers: quote?.passengers ?? review?.passengers ?? null, purchase_unit: "journey" };
|
|
15
|
+
const itineraryTitle = legs.map(leg => [leg.train_code, leg.from_station, "->", leg.to_station].filter(value => typeof value === "string" && value.length).join(" ")).join(" / ");
|
|
16
|
+
return { itinerary, itineraryTitle };
|
|
17
|
+
}
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export function railLegSeatSummary(seat) {
|
|
2
|
+
return {
|
|
3
|
+
passenger_index: seat.passenger_index,
|
|
4
|
+
...(seat.seat_type_name ? { seat_type_name: seat.seat_type_name } : {}),
|
|
5
|
+
...(seat.seat_label || seat.coach_no || seat.seat_no
|
|
6
|
+
? { seat: seat.seat_label ?? [seat.coach_no ? `${seat.coach_no}车` : "", seat.seat_no ?? ""].join("") }
|
|
7
|
+
: {}),
|
|
8
|
+
confirmed: seat.confirmed,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function railBookingEnvelope(model) {
|
|
12
|
+
const rail = model.rail_booking;
|
|
13
|
+
const execution = model.execution;
|
|
14
|
+
const orderID = (model.current_delivery ?? model.delivery_bindings.at(-1))?.order_id;
|
|
15
|
+
const legs = rail.legs.map((leg) => ({
|
|
16
|
+
leg_index: leg.leg_index,
|
|
17
|
+
state: leg.state,
|
|
18
|
+
issued: leg.issued,
|
|
19
|
+
...(leg.error_code ? { error_code: leg.error_code } : {}),
|
|
20
|
+
arrival_days: leg.arrival_days ?? 0,
|
|
21
|
+
...(leg.details_pending ? { details_pending: true } : {}),
|
|
22
|
+
...(leg.supplier_state ? { supplier_state: leg.supplier_state } : {}),
|
|
23
|
+
...(leg.train_code ? { train_code: leg.train_code } : {}),
|
|
24
|
+
...(leg.travel_date ? { travel_date: leg.travel_date } : {}),
|
|
25
|
+
...(leg.from || leg.to ? { route: `${leg.from ?? ""} → ${leg.to ?? ""}` } : {}),
|
|
26
|
+
...(leg.departure || leg.arrival ? { time: `${leg.departure ?? ""}–${leg.arrival ?? ""}` } : {}),
|
|
27
|
+
...(leg.seat_name ? { seat_name: leg.seat_name } : {}),
|
|
28
|
+
...(leg.seat_request ? { seat_request: leg.seat_request } : {}),
|
|
29
|
+
...(leg.seat_preferences?.length ? { seat_preferences: leg.seat_preferences } : {}),
|
|
30
|
+
...(leg.seats?.length ? { seats: leg.seats.map(railLegSeatSummary) } : {}),
|
|
31
|
+
}));
|
|
32
|
+
const result = {
|
|
33
|
+
service_execution_id: execution.service_execution_id,
|
|
34
|
+
...(orderID ? { order_id: orderID } : {}),
|
|
35
|
+
rail: { state: rail.state, issued_legs: rail.issued_legs, total_legs: rail.legs.length, message: rail.message, legs },
|
|
36
|
+
};
|
|
37
|
+
if (rail.state === "issued" && rail.issued_legs === rail.legs.length && rail.legs.length > 0) {
|
|
38
|
+
const detailsPending = rail.legs.some((leg) => leg.details_pending);
|
|
39
|
+
return {
|
|
40
|
+
status: "issued",
|
|
41
|
+
result: { ...result, ...(orderID ? { aftercare: { command: `itpay order ${orderID} --json`, when: "仅用户请求核对订单或售后时" } } : {}) },
|
|
42
|
+
instruction: `告诉用户:车票已出票,座位以实际出票为准,平台不提供 12306 票号,可在订单页核对行程。不要在对话中索要乘车人身份信息。${detailsPending ? "部分席位信息未能同步,请前往 12306 核对行程;不要重复购买。" : ""}`,
|
|
43
|
+
next: null,
|
|
44
|
+
recovery: [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (rail.state === "manual_review") {
|
|
48
|
+
return {
|
|
49
|
+
status: "manual_review",
|
|
50
|
+
result,
|
|
51
|
+
instruction: `告诉用户:付款已确认,已出票 ${rail.issued_legs}/${rail.legs.length},其余需要人工核对;请勿重复付款或重新下单,已出票的车票会保留。未知结果先对账,退款按对应段处理,不承诺整笔退款或时效。`,
|
|
52
|
+
next: orderID ? { command: `itpay order ${orderID} --json`, reason: "查看同一订单当前状态" } : null,
|
|
53
|
+
recovery: [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// A rail booking run only exists after a verified payment, so payment is
|
|
57
|
+
// confirmed here — but only `pending` actually means supplier issuance is in
|
|
58
|
+
// flight. Any state added later falls back to a conservative re-read instead
|
|
59
|
+
// of claiming issuance.
|
|
60
|
+
if (rail.state === "pending") {
|
|
61
|
+
return {
|
|
62
|
+
status: "issuing",
|
|
63
|
+
result,
|
|
64
|
+
instruction: `告诉用户:付款已确认,已出票 ${rail.issued_legs}/${rail.legs.length},后台继续处理;付款成功不代表整程出票,请勿重复购买或再次付款。稍后只读取同一任务。`,
|
|
65
|
+
next: { command: `itpay services next ${execution.service_execution_id} --json`, reason: "稍后读取同一出票任务" },
|
|
66
|
+
recovery: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
status: "processing",
|
|
71
|
+
result,
|
|
72
|
+
instruction: `告诉用户:付款已确认,订单处理状态待确认(${rail.state});付款成功不代表已出票,请勿重复购买或再次付款。稍后只读取同一任务。`,
|
|
73
|
+
next: { command: `itpay services next ${execution.service_execution_id} --json`, reason: "稍后读取同一出票任务" },
|
|
74
|
+
recovery: [],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { serviceInputTemplate } from "../shared/input.js";
|
|
2
|
+
// Declared at execution creation: the client understands the rail.progressive.v2
|
|
3
|
+
// response format (rail_planning projection, snapshot paging, rsel_ handles).
|
|
4
|
+
// The server alone decides service version, quota and provider credentials.
|
|
5
|
+
export const RAIL_PROGRESSIVE_FEATURES = ["rail.progressive.v2"];
|
|
6
|
+
export const RAIL_ENDPOINT_BRIEF = "查询前先留下2–4行端点理解摘要:两端各指哪里、是车站/行政范围/具体地点、依据及未确定事实,再说明日期/人数/时限与Exact/Smart选择。对话或现有route-brief.txt二选一;已有摘要沿用,只更新变化项。含义不清先澄清,定位待核验不等于含义不清;勿凭字尾猜类型。";
|
|
7
|
+
export const RAIL_SERVICE_GUIDANCE = {
|
|
8
|
+
"itpay-rail-exact": {
|
|
9
|
+
when_to_use: "已知可信站对时先用本服务核验直达;城市需求可先形成有依据的站对假设。只覆盖这一站对,不代表全城市最优。只有明确可靠路线知识可直接用;大概率判断先短查,受阻且清晰双站可Exact核验。查询前记录两端含义、类别、依据、未知事实和Exact/Smart选择;格式见已读rail-booking。",
|
|
10
|
+
input_fields: [
|
|
11
|
+
{ name: "origin", required: true, description: "已核实出发铁路站完整名称,不能以同名站代替区域", example: "<已核实出发站>" },
|
|
12
|
+
{ name: "destination", required: true, description: "已核实到达铁路站完整名称", example: "<已核实到达站>" },
|
|
13
|
+
{ name: "travel_date", required: true, description: "出行日期 YYYY-MM-DD;字段名必须是 travel_date,'date' 等别名无效", example: "<用户日期 YYYY-MM-DD>" },
|
|
14
|
+
],
|
|
15
|
+
input_example: { origin: "<已核实出发站名>", destination: "<已核实到达站名>", travel_date: "<用户出行日期 YYYY-MM-DD>" },
|
|
16
|
+
notes: [
|
|
17
|
+
"本服务只接受这三个字段,多传字段会被供应商拒绝",
|
|
18
|
+
"查询为空只说明这一已解析站对;允许中转可用Smart保持双站;改变用户指定站须先取得同意",
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
"itpay-rail-smart": {
|
|
22
|
+
when_to_use: "需要广泛站点比较、中转或具体地址接驳时使用;有可信站对且只需先核验直达可用 Exact。只有明确可靠路线知识可直接用;大概率判断先短查,受阻且清晰双站可Exact核验。查询前记录两端含义、类别、依据、未知事实和Exact/Smart选择;格式见已读rail-booking。",
|
|
23
|
+
input_fields: [
|
|
24
|
+
{ name: "origin", required: true, description: "出发地:已知最完整的位置名或区域(城市/区县/地址均可)", example: "<原始完整地点>" },
|
|
25
|
+
{ name: "destination", required: true, description: "目的地:与 origin 同样的位置规则", example: "<已核实到达站>" },
|
|
26
|
+
{ name: "travel_date", required: true, description: "出行日期 YYYY-MM-DD;字段名必须是 travel_date", example: "<用户日期 YYYY-MM-DD>" },
|
|
27
|
+
],
|
|
28
|
+
optional_fields: [
|
|
29
|
+
{ name: "origin_city", description: "出发地城市提示,辅助位置解析" },
|
|
30
|
+
{ name: "destination_city", description: "目的地城市提示" },
|
|
31
|
+
{ name: "origin_location", description: "已知坐标对象(高德 GCJ-02)" },
|
|
32
|
+
{ name: "destination_location", description: "目的地坐标对象" },
|
|
33
|
+
{ name: "depart_after", description: "不早于该时间出发" },
|
|
34
|
+
{ name: "depart_before", description: "不晚于该时间出发" },
|
|
35
|
+
{ name: "arrive_before", description: "不晚于该时间到达" },
|
|
36
|
+
{ name: "user_query", description: "旅行需求原话,最多4096 UTF-8字节;时间等明确条件仍填写对应结构化字段" },
|
|
37
|
+
{ name: "seat_preferences", description: "已确认的席别代码数组,不填写旅客身份" },
|
|
38
|
+
{ name: "priority", enum: "balanced|fastest|cheapest|safest|flexible", description: "方案偏好" },
|
|
39
|
+
{ name: "duration_basis", enum: "rail|door_to_door", description: "最快默认按铁路全程含换乘;明确要求且接驳证据完整时可按门到门" },
|
|
40
|
+
{ name: "max_transfers", description: "允许中转次数,0-2" },
|
|
41
|
+
{ name: "passengers", description: "成人乘客整数 1-5;不填写身份资料" },
|
|
42
|
+
],
|
|
43
|
+
input_example: { origin: "<用户出发地点>", destination: "<用户目的地>", travel_date: "<用户出行日期 YYYY-MM-DD>", priority: "fastest", passengers: 1 },
|
|
44
|
+
notes: [
|
|
45
|
+
"位置有歧义时会进入位置确认步骤,请用户选定后继续同一执行",
|
|
46
|
+
"推荐结果含每趟车的席别与余票;购票在后续受保护流程完成",
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
export function railServiceGuidance(serviceID, schema) {
|
|
51
|
+
const guidance = RAIL_SERVICE_GUIDANCE[serviceID];
|
|
52
|
+
if (serviceID !== "itpay-rail-smart" || !guidance)
|
|
53
|
+
return guidance;
|
|
54
|
+
const properties = schema?.properties;
|
|
55
|
+
if (!properties?.endpoints)
|
|
56
|
+
return properties?.origin && properties?.destination ? guidance : undefined;
|
|
57
|
+
return {
|
|
58
|
+
...guidance,
|
|
59
|
+
input_fields: [
|
|
60
|
+
{ name: "endpoints", required: true, description: "origin/destination 各填完整text;类别有依据时填kind(area/place/station)。仅对象含义明确、类别确实待核验时可省略kind;完整名称合法,坐标可选,不靠省略kind猜城市" },
|
|
61
|
+
{ name: "travel_date", required: true, description: "用户出行日期 YYYY-MM-DD" },
|
|
62
|
+
],
|
|
63
|
+
optional_fields: (guidance.optional_fields ?? []).filter((field) => !["origin_city", "destination_city", "origin_location", "destination_location"].includes(field.name)).concat([
|
|
64
|
+
{ name: "reuse_from", description: "同 owner 的已保存 Exact result_item_id 或 Smart snapshot_id;端点/日期新查询可复用有效事实" },
|
|
65
|
+
]).filter(field => properties[field.name] !== undefined),
|
|
66
|
+
input_example: { endpoints: { origin: { kind: "place", text: "<用户原始完整具体地点>" }, destination: { kind: "area", text: "<用户原始完整行政范围>" } }, travel_date: "<用户出行日期 YYYY-MM-DD>" },
|
|
67
|
+
notes: ["仅用当前 input_schema 支持的端点写法;不要与旧 origin/destination 混用", "唯一地点直接解析;真实候选只确认有歧义一端;地图等待按可续用状态恢复,终态有新证据才用返回新查询模板,无动作则停止"],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export const WORKFLOW_STEP_GUIDANCE = {
|
|
71
|
+
input: { meaning: "输入校验", hint: "核对本服务的输入契约和服务端错误码;本执行的输入不能凭猜测修复" },
|
|
72
|
+
quota: { meaning: "额度检查", hint: "查看实际额度及授权状态;只按返回的登录或等待动作处理" },
|
|
73
|
+
geo: { meaning: "位置解析", hint: "查看实际地点解析错误;有歧义时让用户选择返回的候选" },
|
|
74
|
+
geo_confirm: { meaning: "确认后位置解析", hint: "核对服务端记录的候选与用户实际选择" },
|
|
75
|
+
resolved: { meaning: "位置解析复核", hint: "查看未满足的实际位置条件" },
|
|
76
|
+
resolved_after_confirm: { meaning: "位置确认复核", hint: "核对已确认地点与服务端复核结果" },
|
|
77
|
+
search: { meaning: "供应商车次检索", hint: "查看供应商返回的错误码与当前站对;不要推断是字段或站名错误" },
|
|
78
|
+
catalog: { meaning: "可行车次计算", hint: "查看已保存结果和实际过滤原因;不要无原因改日期或站点" },
|
|
79
|
+
recommend: { meaning: "方案推荐", hint: "查看已保存候选和实际推荐失败原因;不要无原因放宽条件" },
|
|
80
|
+
delivery: { meaning: "交付", hint: "读取当前执行状态;已有订单或付款状态不明时先核实,不重建" },
|
|
81
|
+
};
|
|
82
|
+
export function locationInputInstruction(schema) {
|
|
83
|
+
const properties = schema?.properties;
|
|
84
|
+
if (properties?.endpoints)
|
|
85
|
+
return " " + RAIL_ENDPOINT_BRIEF + " 两端分别填写 endpoints.origin/destination 的完整text;有类别依据就填kind,只有对象含义明确而类别待核验时可省略。区域保留范围、精确车站仅在目录验证后锁定。不要混用旧 origin/destination、手填锁站或伪造坐标。唯一地点会自动解析;只有真实多候选才让用户选返回的候选 id,地图失败应按当前执行恢复。";
|
|
86
|
+
if (!properties?.origin_location || !properties?.destination_location)
|
|
87
|
+
return "";
|
|
88
|
+
if (!properties.location_confirmation)
|
|
89
|
+
return " " + RAIL_ENDPOINT_BRIEF + " 地点保留用户已知最完整名称或城市范围;不要编造坐标或把城市改成同名车站。坐标及其他可选字段严格按当前 input_schema 提交。";
|
|
90
|
+
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 才展示候选名称、地址和高德链接,等待用户选择,禁止自行选第一项。";
|
|
91
|
+
}
|
|
92
|
+
// Compatibility normalization belongs only to the declared rail adapter.
|
|
93
|
+
export function railInputTemplate(schema, known = {}) {
|
|
94
|
+
const properties = schema.properties;
|
|
95
|
+
if (properties?.endpoints && !properties.origin && !properties.destination && !known.endpoints && (typeof known.origin === "string" || typeof known.destination === "string"))
|
|
96
|
+
known = { ...known, endpoints: { origin: { text: known.origin }, destination: { text: known.destination } } };
|
|
97
|
+
if (properties?.travel_date && !known.travel_date && typeof known.date === "string")
|
|
98
|
+
known = { ...known, travel_date: known.date };
|
|
99
|
+
const result = serviceInputTemplate(schema, known);
|
|
100
|
+
// Current Smart exposes endpoints through oneOf rather than top-level required.
|
|
101
|
+
// Preserve a supplied legacy branch; otherwise use the complete declared pair.
|
|
102
|
+
if (properties?.endpoints && !known.origin && !known.destination && !result.endpoints)
|
|
103
|
+
result.endpoints = serviceInputTemplate(properties.endpoints);
|
|
104
|
+
if (properties?.accept_non_guaranteed)
|
|
105
|
+
result.accept_non_guaranteed = false;
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
export function railOverviewInstruction(topic) {
|
|
109
|
+
return `尚未创建执行或查询。先读 ${topic} 服务指南、理解用户完整地点与约束,形成简短路线摘要后选择操作;查询结果不是价格同意。`;
|
|
110
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { plainValueLines } from "../shared/envelope.js";
|
|
2
|
+
import { locationConfirmationEnvelope } from "./query.js";
|
|
3
|
+
export function railInvocationProjection(response, requestedCapability, input, itemCount) {
|
|
4
|
+
const baseResult = {};
|
|
5
|
+
let instruction;
|
|
6
|
+
const preview = response.invocation?.safe_result_preview;
|
|
7
|
+
const notices = Array.isArray(preview?.notices) ? preview.notices.map((notice) => {
|
|
8
|
+
if (!notice || typeof notice !== "object" || !("code" in notice) || !("message" in notice) || typeof notice.message !== "string")
|
|
9
|
+
return notice;
|
|
10
|
+
if (!["RAIL_TRANSFER_SCOPE_LIMIT", "RAIL_TRANSFER_SEARCH_INCOMPLETE"].includes(String(notice.code)))
|
|
11
|
+
return notice;
|
|
12
|
+
return { ...notice, message: notice.message.replace(/可按主要枢纽分段查询[。;]?/g, "") };
|
|
13
|
+
}) : preview?.notices;
|
|
14
|
+
if (!response.effective_quota?.exhausted && preview?.search_status === "LOCATION_CONFIRMATION_REQUIRED" && preview.location_confirmation) {
|
|
15
|
+
return { early: { value: locationConfirmationEnvelope(response.execution.service_execution_id, requestedCapability.capability_id, input, preview.location_confirmation),
|
|
16
|
+
plainResult: plainValueLines(preview.location_confirmation) } };
|
|
17
|
+
}
|
|
18
|
+
if (typeof preview?.catalog_total === "number") {
|
|
19
|
+
baseResult.catalog = {
|
|
20
|
+
total: preview.catalog_total,
|
|
21
|
+
recommendation: preview.recommendation,
|
|
22
|
+
decision_source: preview.decision_source,
|
|
23
|
+
coverage: preview.coverage,
|
|
24
|
+
notices,
|
|
25
|
+
search_status: preview.search_status,
|
|
26
|
+
searched_scope: preview.searched_scope,
|
|
27
|
+
effective_policy_hash: preview.effective_policy_hash,
|
|
28
|
+
api_cost: preview.api_cost,
|
|
29
|
+
stage_timestamps: preview.stage_timestamps,
|
|
30
|
+
page: preview.catalog_page,
|
|
31
|
+
journey_summary: preview.journey_summary,
|
|
32
|
+
journeys: preview.journeys,
|
|
33
|
+
train_services: preview.train_services,
|
|
34
|
+
resolved_locations: preview.resolved_locations,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (itemCount === 0 && Array.isArray(notices)) {
|
|
38
|
+
const railNotice = notices.find((notice) => {
|
|
39
|
+
if (!notice || typeof notice !== "object")
|
|
40
|
+
return false;
|
|
41
|
+
const value = notice;
|
|
42
|
+
return ["RAIL_TRANSFER_SCOPE_LIMIT", "RAIL_TRANSFER_SEARCH_INCOMPLETE"].includes(String(value.code)) && typeof value.message === "string";
|
|
43
|
+
});
|
|
44
|
+
if (railNotice)
|
|
45
|
+
instruction = `向用户展示官方提示:${railNotice.message} 说明本次已查范围和未完成原因,按服务端提供的当前动作恢复;不能据此断言没有车,不默认扩大范围或让用户另选分段站点。`;
|
|
46
|
+
}
|
|
47
|
+
if (itemCount > 0 && baseResult.catalog) {
|
|
48
|
+
instruction = "先向用户说明排在首位的推荐方案和其他方案的时间、费用与便利性取舍。items 包含本次返回的合格候选,用户不满意时继续从该列表比较,不必重复查票。搜索是否完成、目录是否截断、覆盖范围和模型降级以 catalog 为准;不能把部分结果说成完整搜索。费用尚需购票前核验。姓名、身份证和手机号仅在 ItPay 网页填写。";
|
|
49
|
+
}
|
|
50
|
+
if (itemCount > 0 && preview?.journey_summary) {
|
|
51
|
+
instruction = "先按 journey_summary 报告本次已查询范围内的可用车次、换乘走法和实际乘车组合数量,不能用 catalog.total 或 items 数量冒充车次或组合数。按 journeys 展示组合,推荐置顶,保留全部组合和 train_services 车次列表供用户查看。每组先说明乘坐哪些车、在哪里真正换车以及等待多久;rides.onboard_stops 是同车接续停站,无需下车,可能需车内换座,不计入换乘次数。席别、余票、价格及接驳是组合下的选择,通过 candidate_ids 查对应 items;确认具体选择后才使用该 item 的编号,不猜席别或自动付款。30 分钟只是在已确认便捷换乘站点的筛选下限,不是接续保证。不得把多段票称为已可购买的套票;以 purchase_supported 为准。覆盖不完整、截断和模型仅看短名单时必须说明。姓名、身份证和手机号只在 ItPay 网页填写。";
|
|
52
|
+
}
|
|
53
|
+
return { ...(baseResult.catalog ? { catalog: baseResult.catalog } : {}), ...(instruction ? { instruction } : {}), ...(preview?.resolved_locations ? { suffix: " 按 resolved_locations 说明实际解析地点;区域级位置仅是范围代表点,接驳时间不是从用户精确位置计算的。" } : {}) };
|
|
54
|
+
}
|