@itpay/cli 2.1.12 → 2.1.17
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.
|
@@ -156,6 +156,10 @@ export class BackendClient {
|
|
|
156
156
|
getGrantedServiceResult(serviceExecutionID) {
|
|
157
157
|
return this.http.get(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/granted-result`);
|
|
158
158
|
}
|
|
159
|
+
getRailPlanningCatalog(serviceExecutionID, snapshotID) {
|
|
160
|
+
const query = snapshotID ? `?snapshot=${encodeURIComponent(snapshotID)}` : "";
|
|
161
|
+
return this.http.get(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/rail-planning/catalog${query}`);
|
|
162
|
+
}
|
|
159
163
|
getRailJourneyDetail(serviceExecutionID, journeyID, snapshotID) {
|
|
160
164
|
const query = snapshotID ? `?snapshot=${encodeURIComponent(snapshotID)}` : "";
|
|
161
165
|
return this.http.get(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/rail-planning/journeys/${encodeURIComponent(journeyID)}${query}`);
|
|
@@ -23,12 +23,17 @@ export class CommandContractError extends Error {
|
|
|
23
23
|
this.name = "CommandContractError";
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
// stdoutEnvelopeLimit is the hard byte budget for one emitted JSON envelope:
|
|
27
|
+
// public rail pages/projections must fit 32 KiB end-to-end, so oversized
|
|
28
|
+
// pretty output falls back to a compact line rather than spilling.
|
|
29
|
+
const stdoutEnvelopeLimit = 32 * 1024;
|
|
26
30
|
export function writeCommandEnvelope(value, options = {}) {
|
|
27
31
|
const out = resolveOutput(options.output);
|
|
28
32
|
const agentType = options.agentType ?? declaredAgentType();
|
|
29
33
|
const qualified = qualifyEnvelope(value, agentType);
|
|
30
34
|
if (options.jsonOutput) {
|
|
31
|
-
|
|
35
|
+
const pretty = JSON.stringify(qualified, null, 2) + "\n";
|
|
36
|
+
out(pretty.length <= stdoutEnvelopeLimit ? pretty : JSON.stringify(qualified) + "\n");
|
|
32
37
|
return;
|
|
33
38
|
}
|
|
34
39
|
out(`${qualified.status}\n`);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// shared_rows.v1 catalog decode for the rail planning full-catalog read.
|
|
2
|
+
// The committed rail.catalog.v3 may ship positional rows (journeys, plans,
|
|
3
|
+
// profiles keyed by journey_columns plus shared ride_table / *_index tables)
|
|
4
|
+
// instead of per-journey objects. The summary path needs only ref + route
|
|
5
|
+
// text + default layer; the full packed document is passed through verbatim
|
|
6
|
+
// for JSON output — decoding is for the human-readable summary and selectors.
|
|
7
|
+
export class RailCatalogEncodingError extends Error {
|
|
8
|
+
constructor(encoding) {
|
|
9
|
+
super(`catalog encoding ${JSON.stringify(encoding)} is not supported by this CLI — ` +
|
|
10
|
+
`run npm install -g @itpay/cli to upgrade; the committed catalog is intact`);
|
|
11
|
+
this.name = "rail_catalog_encoding";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function asString(v) {
|
|
15
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
16
|
+
}
|
|
17
|
+
function asIndex(v, bound) {
|
|
18
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 0 || v >= bound) {
|
|
19
|
+
throw new Error("shared_rows: index out of bounds");
|
|
20
|
+
}
|
|
21
|
+
return v;
|
|
22
|
+
}
|
|
23
|
+
// _cn_route_text port: "南头 C7608 → 广州南换乘71分 → G2944 重庆西" — kept
|
|
24
|
+
// byte-identical with the planner so a packed journey renders the same text
|
|
25
|
+
// its object form carried.
|
|
26
|
+
function regenerateRoute(ridePairs, rideTable, stations, services) {
|
|
27
|
+
const name = (code, fallback) => asString(stations[asString(code) ?? ""]) ?? asString(fallback) ?? asString(code) ?? "?";
|
|
28
|
+
const rides = ridePairs.map((pair) => {
|
|
29
|
+
if (!Array.isArray(pair) || pair.length !== 2)
|
|
30
|
+
throw new Error("shared_rows: malformed ride pair");
|
|
31
|
+
const src = rideTable[asIndex(pair[0], rideTable.length)];
|
|
32
|
+
if (!Array.isArray(src))
|
|
33
|
+
throw new Error("shared_rows: malformed ride row");
|
|
34
|
+
return { row: src, wait: pair[1] };
|
|
35
|
+
});
|
|
36
|
+
const first = rides[0];
|
|
37
|
+
if (first === undefined)
|
|
38
|
+
return "";
|
|
39
|
+
const trainCodes = (rideRow) => (Array.isArray(rideRow[4]) ? rideRow[4] : [])
|
|
40
|
+
.map((ref) => {
|
|
41
|
+
const svc = services[asString(ref) ?? ""];
|
|
42
|
+
return asString(svc?.tc);
|
|
43
|
+
})
|
|
44
|
+
.filter((c) => Boolean(c));
|
|
45
|
+
const waitOf = (ride) => typeof ride.wait === "number" ? ride.wait
|
|
46
|
+
: typeof ride.row[5] === "number" ? ride.row[5]
|
|
47
|
+
: undefined;
|
|
48
|
+
const sameRun = (rideRow) => rideRow[6] === true;
|
|
49
|
+
const parts = [`${name(first.row[2], undefined)} ${trainCodes(first.row).join("/")}`];
|
|
50
|
+
for (let i = 1; i < rides.length; i += 1) {
|
|
51
|
+
const prev = rides[i - 1].row;
|
|
52
|
+
const ride = rides[i];
|
|
53
|
+
const via = name(prev[3], undefined);
|
|
54
|
+
const wait = waitOf(ride);
|
|
55
|
+
parts.push(sameRun(ride.row)
|
|
56
|
+
? wait !== undefined ? `${via}同车停${wait}分` : `${via}同车接续`
|
|
57
|
+
: wait !== undefined ? `${via}换乘${wait}分` : `${via}换乘`);
|
|
58
|
+
parts.push(`${trainCodes(ride.row).join("/")} ${name(ride.row[3], undefined)}`);
|
|
59
|
+
}
|
|
60
|
+
if (rides.length === 1) {
|
|
61
|
+
parts.push(name(first.row[3], undefined));
|
|
62
|
+
}
|
|
63
|
+
return parts.filter((s) => s.trim().length > 0).join(" → ");
|
|
64
|
+
}
|
|
65
|
+
// decodeRailCatalogJourneys returns one summary per committed combination in
|
|
66
|
+
// catalog order. Legacy object-form catalogs (no `encoding`) pass through;
|
|
67
|
+
// shared_rows.v1 rows are positional under journey_columns
|
|
68
|
+
// [ref, route, rides, plans, gc, pr, profiles, tc, ev, rep, tw, risk].
|
|
69
|
+
// Any other encoding throws RailCatalogEncodingError — an explicit upgrade
|
|
70
|
+
// hint, never an empty catalog.
|
|
71
|
+
export function decodeRailCatalogJourneys(catalog) {
|
|
72
|
+
const encoding = catalog["encoding"];
|
|
73
|
+
const layerMap = catalog["choice_layers"]?.["journey_layer"] ?? {};
|
|
74
|
+
const layerOf = (ref) => layerMap[ref] === "backup" ? "backup" : "main";
|
|
75
|
+
if (encoding === undefined || encoding === null) {
|
|
76
|
+
const rows = Array.isArray(catalog["journeys"]) ? catalog["journeys"] : [];
|
|
77
|
+
return {
|
|
78
|
+
packed: false,
|
|
79
|
+
journeys: rows.map((j) => {
|
|
80
|
+
const rec = (j ?? {});
|
|
81
|
+
const ref = asString(rec["ref"]) ?? asString(rec["journey_id"]) ?? "?";
|
|
82
|
+
return {
|
|
83
|
+
ref,
|
|
84
|
+
route: asString(rec["route"]) ?? asString(rec["route_text"]) ?? asString(rec["summary"]) ?? "",
|
|
85
|
+
defaultLayer: layerOf(ref),
|
|
86
|
+
};
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (encoding !== "shared_rows.v1") {
|
|
91
|
+
throw new RailCatalogEncodingError(encoding);
|
|
92
|
+
}
|
|
93
|
+
const columns = catalog["journey_columns"];
|
|
94
|
+
if (!Array.isArray(columns) || columns[0] !== "ref" || columns[1] !== "route" || columns[2] !== "rides") {
|
|
95
|
+
throw new RailCatalogEncodingError(`${String(encoding)} (unexpected journey_columns)`);
|
|
96
|
+
}
|
|
97
|
+
const rideTable = (Array.isArray(catalog["ride_table"]) ? catalog["ride_table"] : []);
|
|
98
|
+
const stations = (catalog["stations"] ?? {});
|
|
99
|
+
const services = (catalog["services"] ?? {});
|
|
100
|
+
const journeys = (Array.isArray(catalog["journeys"]) ? catalog["journeys"] : []);
|
|
101
|
+
return {
|
|
102
|
+
packed: true,
|
|
103
|
+
journeys: journeys.map((row) => {
|
|
104
|
+
if (!Array.isArray(row))
|
|
105
|
+
throw new Error("shared_rows: journey row is not an array");
|
|
106
|
+
const ref = asString(row[0]) ?? "?";
|
|
107
|
+
const route = asString(row[1]) ??
|
|
108
|
+
(Array.isArray(row[2]) ? regenerateRoute(row[2], rideTable, stations, services) : "");
|
|
109
|
+
return { ref, route, defaultLayer: layerOf(ref) };
|
|
110
|
+
}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -7,6 +7,7 @@ import { ensureIdeImageAttach } from "../render/ide.js";
|
|
|
7
7
|
import { buildCheckoutHandoff, shouldPrepareLocalCheckoutImage } from "./checkout_handoff.js";
|
|
8
8
|
import { localizeCardURL, normalizeCardLocale } from "../render/locale.js";
|
|
9
9
|
import { buildAgentChatHandoff } from "../render/markdown.js";
|
|
10
|
+
import { decodeRailCatalogJourneys } from "./rail_catalog.js";
|
|
10
11
|
import { platformKeyForHost } from "../render/plan.js";
|
|
11
12
|
import { renderTerminalQR } from "../render/qr.js";
|
|
12
13
|
// Declared at execution creation: the client understands the rail.progressive.v2
|
|
@@ -867,6 +868,47 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
867
868
|
// planning evidence — a free owner-validated read that never touches the
|
|
868
869
|
// grant/Vault path. Without the selectors the authorized-delivery flow is
|
|
869
870
|
// unchanged.
|
|
871
|
+
// rail.planning full catalog read: --snapshot alone returns the complete
|
|
872
|
+
// committed rail.catalog.v3 — shared dictionaries and field_legend are
|
|
873
|
+
// preserved verbatim; one catalog, never split into candidates+journeys.
|
|
874
|
+
if (options.snapshot && !options.journey) {
|
|
875
|
+
const committed = await backend.getRailPlanningCatalog(serviceExecutionID, options.snapshot);
|
|
876
|
+
const catalog = committed.catalog;
|
|
877
|
+
// The committed catalog may ship shared_rows.v1 positional rows — decode
|
|
878
|
+
// the summary through the column legend; an unknown encoding is an
|
|
879
|
+
// explicit upgrade error, never an empty catalog.
|
|
880
|
+
const decoded = catalog
|
|
881
|
+
? decodeRailCatalogJourneys(catalog)
|
|
882
|
+
: { journeys: [], packed: false };
|
|
883
|
+
const journeys = decoded.journeys;
|
|
884
|
+
const counts = (catalog?.counts ?? {});
|
|
885
|
+
const journeyCount = journeys.length || Number(counts?.combinations ?? 0);
|
|
886
|
+
writeCommandEnvelope({
|
|
887
|
+
status: "ready",
|
|
888
|
+
result: {
|
|
889
|
+
service_execution_id: serviceExecutionID,
|
|
890
|
+
plan_id: committed.plan_id,
|
|
891
|
+
snapshot_id: committed.snapshot_id,
|
|
892
|
+
query_revision: committed.query_revision,
|
|
893
|
+
catalog,
|
|
894
|
+
},
|
|
895
|
+
instruction: "catalog 是本次查询的完整无损目录:journeys 为全部可行组合(含超出预览分页的组合),plans 是每个组合下的购票方案,stations/services/offers/ground_options 为共享字典,field_legend 解释紧凑字段。逐组合向用户说明车次、换乘与接驳取舍;席别与价格以 plans 内报价为准、下单前仍需核验;姓名身份证手机号只在 ItPay 网页填写。",
|
|
896
|
+
next: journeys.length > 0
|
|
897
|
+
? { command: `itpay services read-result ${serviceExecutionID} --snapshot ${committed.snapshot_id} --journey ${journeys[0]?.ref ?? '?'} --json`, reason: "查看单个行程明细" }
|
|
898
|
+
: { command: `itpay services next ${serviceExecutionID} --since-snapshot ${committed.snapshot_id} --json`, reason: "返回规划进展" },
|
|
899
|
+
recovery: [],
|
|
900
|
+
}, {
|
|
901
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
902
|
+
...(options.output ? { output: options.output } : {}),
|
|
903
|
+
plainResult: journeys.length > 0
|
|
904
|
+
? [
|
|
905
|
+
`${journeyCount} combinations:`,
|
|
906
|
+
...journeys.map((j) => `${j.defaultLayer === "backup" ? "[备选] " : "[主选择] "}${j.ref} ${j.route}`),
|
|
907
|
+
]
|
|
908
|
+
: [`catalog: ${committed.snapshot_id} (no combinations)`],
|
|
909
|
+
});
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
870
912
|
if (options.journey) {
|
|
871
913
|
const detail = await backend.getRailJourneyDetail(serviceExecutionID, options.journey, options.snapshot);
|
|
872
914
|
writeCommandEnvelope({
|
|
@@ -993,6 +1035,12 @@ function railJourneySummary(card, serviceExecutionID) {
|
|
|
993
1035
|
...(trains.length ? { trains } : {}),
|
|
994
1036
|
...(first?.departure || last?.arrival ? { time: `${first?.departure ?? ""}–${last?.arrival ?? ""}` } : {}),
|
|
995
1037
|
...(offer ? { price: formatMoney(offer.rail_payable_minor, offer.currency) } : { price: "unknown" }),
|
|
1038
|
+
...(card.decision_role ? { decision_role: card.decision_role } : {}),
|
|
1039
|
+
...(card.explanation?.length ? { explanation: card.explanation } : {}),
|
|
1040
|
+
...(card.reason_codes?.length ? { reason_codes: card.reason_codes } : {}),
|
|
1041
|
+
...(card.tradeoff_codes?.length ? { tradeoff_codes: card.tradeoff_codes } : {}),
|
|
1042
|
+
...(card.recommended_profile ? { recommended_profile: card.recommended_profile,
|
|
1043
|
+
price_basis: "price为默认购票方案参考价;推荐对应的席别、接驳及总价以recommended_profile为准,按其中ticket_plan_ref/offer_refs查看并确认后下单。" } : {}),
|
|
996
1044
|
availability: card.availability,
|
|
997
1045
|
booking_support: card.booking_support,
|
|
998
1046
|
...(card.observed_at ? { observed_at: card.observed_at } : {}),
|
|
@@ -1015,8 +1063,27 @@ function railPlanningEnvelope(model) {
|
|
|
1015
1063
|
const plan = model.rail_planning;
|
|
1016
1064
|
const se = model.execution.service_execution_id;
|
|
1017
1065
|
const expansion = plan.search?.expansion_status ?? "running";
|
|
1066
|
+
const counts = plan.search?.counts;
|
|
1018
1067
|
const journeys = [plan.recommendation, ...(plan.alternatives ?? [])].filter(Boolean);
|
|
1019
1068
|
const cards = journeys.map((card) => railJourneySummary(card, se));
|
|
1069
|
+
// Journey counts come from the committed catalog — real combination counts,
|
|
1070
|
+
// never seat-row counts. Transfer buckets follow the verified transfer_count.
|
|
1071
|
+
const journeyMix = counts && (counts.journeys_total ?? 0) > 0
|
|
1072
|
+
? `${counts.journeys_total}个铁路组合(直达${counts.journeys_direct ?? 0}/1中转${counts.journeys_one_transfer ?? 0}/2中转${counts.journeys_multi_transfer ?? 0})`
|
|
1073
|
+
: "";
|
|
1074
|
+
const pairProgress = counts && (counts.pairs_total ?? 0) > 0
|
|
1075
|
+
? `已检查${counts.pairs_checked ?? 0}/${counts.pairs_total}个附近站对`
|
|
1076
|
+
: "";
|
|
1077
|
+
// §S5: failed pairs are named honestly — never implied by "checked" and
|
|
1078
|
+
// never rephrased as "no direct exists".
|
|
1079
|
+
// §S5: a rules-only recommendation after a failed model attempt is honest —
|
|
1080
|
+
// "rules finished, AI didn't", never silently dressed as a model pick.
|
|
1081
|
+
const modelDegradedHint = plan.search?.model_outcome === "fallback"
|
|
1082
|
+
? "规则推荐已完成,AI模型未完成;推荐有效,但置信度说明以规则结果为准。"
|
|
1083
|
+
: "";
|
|
1084
|
+
const failedPairsHint = counts && (counts.pairs_failed ?? 0) > 0
|
|
1085
|
+
? `有${counts.pairs_failed}个站对暂未查明,不能确认没有直达;已有结果仍可查看。`
|
|
1086
|
+
: "";
|
|
1020
1087
|
const result = {
|
|
1021
1088
|
service_execution_id: se,
|
|
1022
1089
|
rail_planning: {
|
|
@@ -1024,12 +1091,21 @@ function railPlanningEnvelope(model) {
|
|
|
1024
1091
|
query_revision: plan.query_revision,
|
|
1025
1092
|
snapshot_id: plan.snapshot_id,
|
|
1026
1093
|
expansion_status: expansion,
|
|
1094
|
+
...(plan.search?.phase ? { phase: plan.search.phase } : {}),
|
|
1095
|
+
...(plan.search?.transfer_status ? { transfer_status: plan.search.transfer_status } : {}),
|
|
1096
|
+
...(plan.search?.transition_reason ? { transition_reason: plan.search.transition_reason } : {}),
|
|
1097
|
+
...(plan.search?.authorization ? { authorization: plan.search.authorization } : {}),
|
|
1098
|
+
...(plan.search?.expansion_target ? { expansion_target: plan.search.expansion_target } : {}),
|
|
1099
|
+
...(plan.search?.decision_source ? { decision_source: plan.search.decision_source } : {}),
|
|
1100
|
+
...(plan.search?.model_outcome ? { model_outcome: plan.search.model_outcome } : {}),
|
|
1027
1101
|
...(plan.search?.reason ? { reason: plan.search.reason } : {}),
|
|
1028
1102
|
...(plan.search?.poll_after_ms ? { poll_after_ms: plan.search.poll_after_ms } : {}),
|
|
1029
1103
|
...(plan.result_not_updated ? { result_not_updated: true } : {}),
|
|
1104
|
+
...(counts ? { counts } : {}),
|
|
1030
1105
|
...(plan.budgets ? { budgets: plan.budgets } : {}),
|
|
1031
1106
|
...(plan.coverage ? { coverage: plan.coverage } : {}),
|
|
1032
1107
|
},
|
|
1108
|
+
...(plan.notices?.length ? { notices: plan.notices } : {}),
|
|
1033
1109
|
...(plan.recommendation ? { recommendation: railJourneySummary(plan.recommendation, se) } : {}),
|
|
1034
1110
|
...(cards.length ? { journeys: cards } : {}),
|
|
1035
1111
|
...(plan.available_actions?.length ? { available_actions: plan.available_actions } : {}),
|
|
@@ -1043,38 +1119,106 @@ function railPlanningEnvelope(model) {
|
|
|
1043
1119
|
return {
|
|
1044
1120
|
status: "ready",
|
|
1045
1121
|
result,
|
|
1046
|
-
|
|
1122
|
+
// §7.3: the lead line carries real combination counts bucketed by
|
|
1123
|
+
// verified transfer count — seat rows never inflate the journey count.
|
|
1124
|
+
instruction: `${journeyMix ? `本次共${journeyMix}(席别不重复计数)。` : ""}${plan.search?.reason === "budget_exhausted" ? "本次查询已达到上限,以下是已查到的方案,搜索范围尚未全部核验。" : "本轮规划已完成。"}向用户展示 recommendation 与 journeys 卡片(车次/时间/价格/余票状态),请用户选定 journey 后用其 select 命令提交;乘车人身份信息不在此收集,后续购买走受保护 Checkout。`,
|
|
1047
1125
|
next: journeys[0]?.booking_support === "single_leg"
|
|
1048
1126
|
? { command: railJourneySummary(journeys[0], se).select, reason: "选定推荐行程" }
|
|
1049
1127
|
: null,
|
|
1050
1128
|
recovery: [],
|
|
1051
1129
|
};
|
|
1052
|
-
case "paused":
|
|
1130
|
+
case "paused": {
|
|
1131
|
+
// §7.1 case 1: usable directs committed while ranked scope remains. The
|
|
1132
|
+
// message must carry real counts, and expansion is a user choice — the
|
|
1133
|
+
// Agent must not treat "expandable" as consent already given.
|
|
1134
|
+
const directHint = (plan.search?.reason === "direct_options_ready" || plan.search?.phase === "direct_ready") && journeyMix
|
|
1135
|
+
? `本次找到${counts?.journeys_direct ?? 0}趟可选直达(${journeyMix}),${pairProgress || "部分站对已核验"},中转尚未展开。可以直接选一趟;时间或价格不合适时可继续搜索其它直达及中转,等待会更久。`
|
|
1136
|
+
: "";
|
|
1137
|
+
// §2.1: a delivered-pause at the authorized transfer depth names what a
|
|
1138
|
+
// consented expand buys next — verified-empty is "nothing at this
|
|
1139
|
+
// depth", never "no routes exist".
|
|
1140
|
+
const transferHint = (plan.search?.reason === "transfer_options_ready" || plan.search?.phase === "transfer_ready") && journeyMix
|
|
1141
|
+
? `一次中转范围已核验完毕并交付(${journeyMix})。可以直接选用;也可以继续比较两次中转方案,查询会明显更久。`
|
|
1142
|
+
: plan.search?.reason === "no_options_verified"
|
|
1143
|
+
? `当前授权深度内的范围已核验完、未找到可用方案——这不等于没有其它路线。可授权继续更深的两次中转搜索(耗时显著增加),或停止。`
|
|
1144
|
+
: "";
|
|
1145
|
+
const expandHint = plan.search?.expansion_target === "two_transfer"
|
|
1146
|
+
? "expand_search 将授权一次更深的两次中转搜索(等待更久)"
|
|
1147
|
+
: plan.search?.expansion_target === "more_direct_and_one_transfer"
|
|
1148
|
+
? "expand_search 将补齐剩余直达并展开一次中转"
|
|
1149
|
+
: "expand_search 是唯一会再消耗供应商配额的命令,其余均为本地读取";
|
|
1150
|
+
// §7.1 case 3 on a recoverable pause: unverified scope is named with real
|
|
1151
|
+
// counts, and the phrasing never implies the whole nearby range ran.
|
|
1152
|
+
const partialPause = !directHint && counts && (counts.pairs_total ?? 0) > (counts.pairs_checked ?? 0)
|
|
1153
|
+
? `目前找到${counts.journeys_total ?? 0}个可选组合;还有${(counts.pairs_total ?? 0) - (counts.pairs_checked ?? 0)}个站对/部分中转路径未核验。以下是已确认结果,不能据此断定没有其它走法。`
|
|
1154
|
+
: "";
|
|
1155
|
+
// §S5: a dispatch whose outcome is unknown pauses for reconciliation —
|
|
1156
|
+
// the message names the state, never rephrases it as a normal pause or
|
|
1157
|
+
// a no-directs result, and never offers a blind retry (expand_search is
|
|
1158
|
+
// withheld by the projection too).
|
|
1159
|
+
if (plan.search?.reason === "dispatch_outcome_unknown") {
|
|
1160
|
+
return {
|
|
1161
|
+
status: "awaiting_input",
|
|
1162
|
+
result,
|
|
1163
|
+
instruction: `${counts?.journeys_total ? `已保存${counts.journeys_total}个已核验组合仍可查看。` : ""}上一次查询发送结果未确认(可能已部分查询),系统已暂停并等待对账,不会自动重复发送,也不能据此断定没有车次。展示已有卡片,稍后可重新读取最新状态。`,
|
|
1164
|
+
next: null,
|
|
1165
|
+
recovery: [{ command: `itpay services next ${se} --json`, reason: "稍后读取对账后的最新状态" }],
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1053
1168
|
return {
|
|
1054
1169
|
status: "awaiting_input",
|
|
1055
1170
|
result,
|
|
1056
|
-
instruction:
|
|
1171
|
+
instruction: `${modelDegradedHint}${failedPairsHint}${directHint}${transferHint}${partialPause}规划已发布首批结果并暂停扩展:展示现有卡片与 available_actions,等待用户意图(${expandHint})。ready 后 next 为空表示向用户汇报并等待,不是永远没有更多。`,
|
|
1057
1172
|
next: null,
|
|
1058
1173
|
recovery: [],
|
|
1059
1174
|
};
|
|
1175
|
+
}
|
|
1060
1176
|
case "failed":
|
|
1061
1177
|
case "cancelled":
|
|
1062
|
-
case "expired":
|
|
1178
|
+
case "expired": {
|
|
1179
|
+
// §7.1 case 3: when scope was only partially verified the honest wording
|
|
1180
|
+
// is "N confirmed + X pairs unverified" — never the case-2 phrasing that
|
|
1181
|
+
// implies the whole nearby range was checked.
|
|
1182
|
+
const partial = counts && (counts.pairs_total ?? 0) > (counts.pairs_checked ?? 0)
|
|
1183
|
+
? `目前找到${counts.journeys_total ?? 0}个可选组合;还有${(counts.pairs_total ?? 0) - (counts.pairs_checked ?? 0)}个站对/部分中转路径未核验。以下是已确认结果,不能据此断定没有其它走法。`
|
|
1184
|
+
: "";
|
|
1185
|
+
// §S5: a processing failure with a committed catalog is "saved N
|
|
1186
|
+
// verified combinations, later expansion/recommend incomplete" — the
|
|
1187
|
+
// existing catalog stays readable and nothing implies no other routes.
|
|
1188
|
+
const savedCatalog = expansion === "failed" && (counts?.journeys_total ?? 0) > 0
|
|
1189
|
+
? `已保存${counts?.journeys_total}个已核验组合;后续扩展/推荐未完成,原因是本次处理异常。已有方案仍可查看,不代表没有其它路线,也不需要重复提交订单或付款。`
|
|
1190
|
+
: "";
|
|
1063
1191
|
return {
|
|
1064
1192
|
status: expansion,
|
|
1065
1193
|
result,
|
|
1066
|
-
instruction:
|
|
1194
|
+
instruction: `${modelDegradedHint}${failedPairsHint}${savedCatalog}${partial}本次规划已结束且不可续用;展示已有卡片后如需重查请用户确认后用同一服务新建执行。不要自动重新发起。`,
|
|
1067
1195
|
next: null,
|
|
1068
1196
|
recovery: [{ command: `itpay services events ${se} --json`, reason: "读取同一任务的处理记录" }],
|
|
1069
1197
|
};
|
|
1070
|
-
|
|
1198
|
+
}
|
|
1199
|
+
default: {
|
|
1200
|
+
// §7.1 case 2: the automatic phase is still running. When the verified
|
|
1201
|
+
// stage is transfer the user must hear "direct scope found nothing
|
|
1202
|
+
// usable, transfer search continues" — never re-submit.
|
|
1203
|
+
const transferSearching = plan.search?.phase === "transfer"
|
|
1204
|
+
|| plan.search?.transition_reason === "no_usable_direct"
|
|
1205
|
+
|| plan.search?.transfer_status === "in_progress" || plan.search?.transfer_status === "pending";
|
|
1206
|
+
// §S5: an expansion the user authorized keeps earlier directs and says
|
|
1207
|
+
// so — it must not reuse the automatic "no usable direct" wording.
|
|
1208
|
+
const manualExpansion = plan.search?.authorization === "manual"
|
|
1209
|
+
? `已保留之前的直达结果,正在按你的要求比较更多直达/中转及分段购票方案,需要多一点时间。`
|
|
1210
|
+
: "";
|
|
1211
|
+
const transferHint = !manualExpansion && transferSearching
|
|
1212
|
+
? `已检查本次附近站范围,暂未找到符合条件且有票的直达,正在继续搜索中转组合,需要多一点时间;不需要重新提交。`
|
|
1213
|
+
: "";
|
|
1071
1214
|
return {
|
|
1072
1215
|
status: "planning",
|
|
1073
1216
|
result,
|
|
1074
|
-
instruction:
|
|
1217
|
+
instruction: `${modelDegradedHint}${manualExpansion}${transferHint}${failedPairsHint}规划进行中:已提交的卡片可先向用户展示比较;稍后按 next 增量轮询同一执行(since-snapshot 命中时负载会被压缩,但阶段与计数仍返回最新值)。不要重新发起查询。`,
|
|
1075
1218
|
next: nextPoll,
|
|
1076
1219
|
recovery: [],
|
|
1077
1220
|
};
|
|
1221
|
+
}
|
|
1078
1222
|
}
|
|
1079
1223
|
}
|
|
1080
1224
|
function terminalExecutionEnvelope(model) {
|
|
@@ -1745,7 +1889,12 @@ export async function runServicesRun(backend, config, serviceID, input, options
|
|
|
1745
1889
|
const until = Date.now() + (options.timeoutSeconds ?? 120) * 1000;
|
|
1746
1890
|
const sleep = options.sleep ?? ((milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds)));
|
|
1747
1891
|
const paid = () => model.payment_bindings.some((binding) => binding.status === "payment_verified") || model.checkout_bindings.some((binding) => binding.status === "payment_verified");
|
|
1748
|
-
|
|
1892
|
+
// §7.2: for progressive services the committed rail_planning projection is
|
|
1893
|
+
// itself a deliverable — return the same railPlanningEnvelope as soon as it
|
|
1894
|
+
// exists instead of hiding every stage behind the 120s workflow poll. The
|
|
1895
|
+
// owner keeps advancing in the background; the CLI returning early never
|
|
1896
|
+
// cancels it.
|
|
1897
|
+
while ((["queued", "running", "delivery"].includes(model.workflow?.status ?? "") || (model.workflow?.status === "payment" && paid())) && !model.current_delivery && !model.rail_planning && Date.now() < until) {
|
|
1749
1898
|
await sleep(options.pollIntervalMS ?? 1500);
|
|
1750
1899
|
model = await backend.getServiceExecution(id);
|
|
1751
1900
|
}
|
package/dist/src/state/config.js
CHANGED
|
@@ -15,8 +15,8 @@ import { OperationJournal } from "./operation_journal.js";
|
|
|
15
15
|
export const DEFAULT_BASE_URL = "https://app.itpay.ai";
|
|
16
16
|
export const DEV_BASE_URL = "https://dev.itpay.ai";
|
|
17
17
|
export const SANDBOX_BASE_URL = "https://sandbox.itpay.ai";
|
|
18
|
-
export const CLI_VERSION = "2.1.
|
|
19
|
-
export const API_CONTRACT_REVISION = "sha256:
|
|
18
|
+
export const CLI_VERSION = "2.1.17";
|
|
19
|
+
export const API_CONTRACT_REVISION = "sha256:c6125896053540d1b56f9d106df499ac90162fb8fa4cce619c8029e90f771f85";
|
|
20
20
|
const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
|
|
21
21
|
const CART_SESSION_FILENAME = "cart.json";
|
|
22
22
|
const OPERATION_JOURNAL_FILENAME = "operations.json";
|