@itpay/cli 2.1.18 → 2.1.22
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/commands/catalog.js +1 -0
- package/dist/src/commands/rail_catalog.js +160 -11
- package/dist/src/commands/services.js +289 -71
- package/dist/src/commands/skill.js +1 -1
- package/dist/src/main.js +54 -19
- package/dist/src/render/markdown.js +0 -3
- package/dist/src/state/account_auth.js +6 -2
- package/dist/src/state/cart_session.js +4 -1
- package/dist/src/state/config.js +1 -1
- package/dist/src/state/device_authority.js +69 -13
- package/dist/src/state/task_journal.js +2 -0
- package/docs/agent/buyer/cart-checkout.json +5 -4
- package/docs/agent/buyer/catalog-list.json +24 -6
- package/docs/agent/buyer/identity-and-sessions.json +15 -10
- package/docs/agent/buyer/install-and-setup.json +7 -6
- package/docs/agent/buyer/orders-refunds.json +4 -4
- package/docs/agent/buyer/payment-flow.json +3 -3
- package/docs/agent/buyer/purchased-content.json +17 -3
- package/docs/agent/buyer/quickstart.json +18 -11
- package/docs/agent/buyer/rail-booking.json +79 -167
- package/docs/agent/buyer/rail-fast-checkout.json +21 -43
- package/docs/agent/buyer/render-hosts.json +8 -5
- package/docs/agent/buyer/service-feedback.json +2 -1
- package/docs/cli-reference/agent-types.md +1 -1
- package/docs/cli-reference/commands/auth.md +2 -0
- package/docs/cli-reference/commands/buy.md +1 -1
- package/docs/cli-reference/commands/cart/add.md +1 -1
- package/docs/cli-reference/commands/cart/clear.md +1 -1
- package/docs/cli-reference/commands/cart/index.md +1 -1
- package/docs/cli-reference/commands/cart/next.md +1 -1
- package/docs/cli-reference/commands/cart/remove.md +1 -1
- package/docs/cli-reference/commands/cart/show.md +1 -1
- package/docs/cli-reference/commands/catalog/index.md +1 -1
- package/docs/cli-reference/commands/catalog/list.md +1 -1
- package/docs/cli-reference/commands/checkout.md +1 -1
- package/docs/cli-reference/commands/device.md +4 -2
- package/docs/cli-reference/commands/docs/index.md +1 -1
- package/docs/cli-reference/commands/docs/list.md +1 -1
- package/docs/cli-reference/commands/docs/search.md +1 -1
- package/docs/cli-reference/commands/docs/show.md +1 -1
- package/docs/cli-reference/commands/install.md +2 -2
- package/docs/cli-reference/commands/next.md +1 -1
- package/docs/cli-reference/commands/order.md +1 -1
- package/docs/cli-reference/commands/pay.md +1 -1
- package/docs/cli-reference/commands/readyz.md +1 -1
- package/docs/cli-reference/commands/refund/cancel.md +1 -1
- package/docs/cli-reference/commands/refund/create.md +1 -1
- package/docs/cli-reference/commands/refund/get.md +2 -2
- package/docs/cli-reference/commands/refund/index.md +1 -1
- package/docs/cli-reference/commands/refund/list.md +1 -1
- package/docs/cli-reference/commands/refund/watch.md +1 -1
- package/docs/cli-reference/commands/services/action.md +3 -3
- package/docs/cli-reference/commands/services/checkout.md +1 -1
- package/docs/cli-reference/commands/services/events.md +1 -1
- package/docs/cli-reference/commands/services/get.md +1 -1
- package/docs/cli-reference/commands/services/index.md +1 -1
- package/docs/cli-reference/commands/services/invoke.md +3 -11
- package/docs/cli-reference/commands/services/list.md +1 -1
- package/docs/cli-reference/commands/services/next.md +12 -10
- package/docs/cli-reference/commands/services/page.md +9 -7
- package/docs/cli-reference/commands/services/quote.md +1 -1
- package/docs/cli-reference/commands/services/read-result.md +8 -8
- package/docs/cli-reference/commands/services/run.md +2 -2
- package/docs/cli-reference/commands/services/start.md +1 -1
- package/docs/cli-reference/conventions.md +2 -2
- package/package.json +7 -5
- package/skills/itpay/SKILL.md +70 -186
- package/docs/reviews/agent-ux-handoff-2026-09-23/IMPLEMENTATION-EVIDENCE.md +0 -174
- package/docs/skill-bundle-rollout/01-mcp-authentication.md +0 -134
- package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +0 -210
- package/docs/skill-bundle-rollout/03-platform-publishing.md +0 -161
- package/docs/skill-bundle-rollout/04-first-wave-platforms.md +0 -70
- package/docs/skill-bundle-rollout/05-sync-operations.md +0 -62
- package/docs/skill-bundle-rollout/README.md +0 -151
- /package/bin/{itp → itp.mjs} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HttpError } from "../client/http.js";
|
|
2
2
|
import { HttpTransportError } from "../client/transport.js";
|
|
3
|
+
import { DeviceLockBusyError, DeviceStateError } from "../state/device_authority.js";
|
|
3
4
|
import { operationID, taskJournalPath } from "../state/config.js";
|
|
4
5
|
import { TaskJournal } from "../state/task_journal.js";
|
|
5
6
|
import { validateContext } from "../state/client_context.js";
|
|
@@ -34,7 +35,7 @@ function journalTaskState(serviceExecutionID, envelope, env) {
|
|
|
34
35
|
const serviceActionStatuses = new Set(["pending", "approved", "rejected", "expired", "cancelled"]);
|
|
35
36
|
const RAIL_SERVICE_GUIDANCE = {
|
|
36
37
|
"itpay-rail-exact": {
|
|
37
|
-
when_to_use: "
|
|
38
|
+
when_to_use: "已知可信站对时先用本服务核验直达;城市需求可先形成有依据的站对假设。只覆盖这一站对,不代表全城市最优。完整流程:itpay docs show rail-booking --json。",
|
|
38
39
|
input_fields: [
|
|
39
40
|
{ name: "origin", required: true, description: "出发火车站名(如'古镇'),不能是城市或地址", example: "古镇" },
|
|
40
41
|
{ name: "destination", required: true, description: "到达火车站名(如'广州南')", example: "广州南" },
|
|
@@ -43,11 +44,11 @@ const RAIL_SERVICE_GUIDANCE = {
|
|
|
43
44
|
input_example: { origin: "古镇", destination: "广州南", travel_date: "2026-09-19" },
|
|
44
45
|
notes: [
|
|
45
46
|
"本服务只接受这三个字段,多传字段会被供应商拒绝",
|
|
46
|
-
"
|
|
47
|
+
"查询为空只说明这一已解析站对;如原意仍需更多覆盖,可验证另一可信站对或使用 Smart",
|
|
47
48
|
],
|
|
48
49
|
},
|
|
49
50
|
"itpay-rail-smart": {
|
|
50
|
-
when_to_use: "
|
|
51
|
+
when_to_use: "需要广泛站点比较、中转或具体地址接驳时使用;有可信站对且只需先核验直达可用 Exact。完整流程:itpay docs show rail-booking --json。",
|
|
51
52
|
input_fields: [
|
|
52
53
|
{ name: "origin", required: true, description: "出发地:已知最完整的位置名或区域(城市/区县/地址均可)", example: "中山古镇" },
|
|
53
54
|
{ name: "destination", required: true, description: "目的地:与 origin 同样的位置规则", example: "广州南" },
|
|
@@ -62,8 +63,9 @@ const RAIL_SERVICE_GUIDANCE = {
|
|
|
62
63
|
{ name: "arrive_before", description: "不晚于该时间到达" },
|
|
63
64
|
{ name: "priority", enum: "balanced|fastest|cheapest|safest|flexible", description: "方案偏好" },
|
|
64
65
|
{ name: "max_transfers", description: "允许中转次数,0-2" },
|
|
66
|
+
{ name: "passengers", description: "成人乘客整数 1-5;不填写身份资料" },
|
|
65
67
|
],
|
|
66
|
-
input_example: { origin: "
|
|
68
|
+
input_example: { origin: "上海", destination: "长沙", travel_date: "2026-10-06", arrive_before: "15:00", priority: "fastest", passengers: 1 },
|
|
67
69
|
notes: [
|
|
68
70
|
"位置有歧义时会进入位置确认步骤,请用户选定后继续同一执行",
|
|
69
71
|
"推荐结果含每趟车的席别与余票;购票在后续受保护流程完成",
|
|
@@ -74,16 +76,16 @@ function railServiceGuidance(serviceID) {
|
|
|
74
76
|
return RAIL_SERVICE_GUIDANCE[serviceID];
|
|
75
77
|
}
|
|
76
78
|
const WORKFLOW_STEP_GUIDANCE = {
|
|
77
|
-
input: { meaning: "输入校验", hint: "
|
|
78
|
-
quota: { meaning: "额度检查", hint: "
|
|
79
|
-
geo: { meaning: "位置解析", hint: "
|
|
80
|
-
geo_confirm: { meaning: "确认后位置解析", hint: "
|
|
81
|
-
resolved: { meaning: "位置解析复核", hint: "
|
|
82
|
-
resolved_after_confirm: { meaning: "位置确认复核", hint: "
|
|
83
|
-
search: { meaning: "供应商车次检索", hint: "
|
|
84
|
-
catalog: { meaning: "可行车次计算", hint: "
|
|
85
|
-
recommend: { meaning: "方案推荐", hint: "
|
|
86
|
-
delivery: { meaning: "交付", hint: "
|
|
79
|
+
input: { meaning: "输入校验", hint: "核对本服务的输入契约和服务端错误码;本执行的输入不能凭猜测修复" },
|
|
80
|
+
quota: { meaning: "额度检查", hint: "查看实际额度及授权状态;只按返回的登录或等待动作处理" },
|
|
81
|
+
geo: { meaning: "位置解析", hint: "查看实际地点解析错误;有歧义时让用户选择返回的候选" },
|
|
82
|
+
geo_confirm: { meaning: "确认后位置解析", hint: "核对服务端记录的候选与用户实际选择" },
|
|
83
|
+
resolved: { meaning: "位置解析复核", hint: "查看未满足的实际位置条件" },
|
|
84
|
+
resolved_after_confirm: { meaning: "位置确认复核", hint: "核对已确认地点与服务端复核结果" },
|
|
85
|
+
search: { meaning: "供应商车次检索", hint: "查看供应商返回的错误码与当前站对;不要推断是字段或站名错误" },
|
|
86
|
+
catalog: { meaning: "可行车次计算", hint: "查看已保存结果和实际过滤原因;不要无原因改日期或站点" },
|
|
87
|
+
recommend: { meaning: "方案推荐", hint: "查看已保存候选和实际推荐失败原因;不要无原因放宽条件" },
|
|
88
|
+
delivery: { meaning: "交付", hint: "读取当前执行状态;已有订单或付款状态不明时先核实,不重建" },
|
|
87
89
|
};
|
|
88
90
|
function failedWorkflowStep(steps, errorCode) {
|
|
89
91
|
if (!steps)
|
|
@@ -544,7 +546,33 @@ export async function runServicesAction(backend, serviceExecutionID, actionType,
|
|
|
544
546
|
request.result_item_id = resultItemID;
|
|
545
547
|
if (options.requiredBefore)
|
|
546
548
|
request.required_before = options.requiredBefore;
|
|
547
|
-
|
|
549
|
+
let response;
|
|
550
|
+
try {
|
|
551
|
+
response = await backend.recordServiceExecutionAction(serviceExecutionID, request);
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
if (error instanceof HttpError && ["booking_review_invalid", "booking_review_changed", "booking_review_locked"].includes(error.code)) {
|
|
555
|
+
throw new CommandContractError(error.code, error.message, error.code === "booking_review_invalid"
|
|
556
|
+
? "确认输入与当前草稿不符。重新读取同一booking的review模板,席别使用返回的代码,不填显示名或猜新值。若仅修正代码写法,且车次、实际席别、人数、价格及条款等已确认内容未变,可沿用用户已有明确确认后提交;草稿实质变化或缺少真实同意时,先请用户确认变化内容。"
|
|
557
|
+
: error.code === "booking_review_changed"
|
|
558
|
+
? "草稿版本已变化;重新读取同一booking的当前review与revision,经用户核对后再提交。"
|
|
559
|
+
: "当前购买已有付款尝试;读取同一booking及订单状态,不创建新订单或重做付款。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取同一booking的当前状态与review" }]);
|
|
560
|
+
}
|
|
561
|
+
throw error;
|
|
562
|
+
}
|
|
563
|
+
if ("state" in response) {
|
|
564
|
+
const updated = await backend.getServiceExecution(serviceExecutionID);
|
|
565
|
+
const envelope = servicesNextEnvelope(updated);
|
|
566
|
+
envelope.result = {
|
|
567
|
+
...envelope.result,
|
|
568
|
+
planning_action: { action_id: response.action_id, state: response.state, replayed: response.replayed },
|
|
569
|
+
};
|
|
570
|
+
writeCommandEnvelope(envelope, {
|
|
571
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
572
|
+
...(options.output ? { output: options.output } : {}),
|
|
573
|
+
});
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
548
576
|
if (selection && actionType === "select_candidate" && response.status === "approved") {
|
|
549
577
|
const updated = await backend.getServiceExecution(serviceExecutionID);
|
|
550
578
|
const preferred = updated.allowed_actions?.[0];
|
|
@@ -668,6 +696,26 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
668
696
|
const checkoutURL = tokenizedCheckoutURL(checkout.checkout_url, displayToken, checkout.qr_payload);
|
|
669
697
|
const cardURL = localizeCardURL(absolutePublicURL(config.baseURL, checkout.card_url ?? fallbackCardURL(config.baseURL, checkoutID, displayToken)), locale);
|
|
670
698
|
const cardPNGURL = localizeCardURL(absolutePublicURL(config.baseURL, checkout.card_png_url ?? checkout.qr_png_url ?? fallbackCardPNGURL(config.baseURL, checkoutID, displayToken)), locale);
|
|
699
|
+
let itineraryTitle;
|
|
700
|
+
try {
|
|
701
|
+
const booking = await backend.getServiceExecution(serviceExecutionID);
|
|
702
|
+
if (booking.execution.service_id === "itpay-rail-booking") {
|
|
703
|
+
const leg = booking.rail_booking?.legs?.[0];
|
|
704
|
+
const review = booking.workflow?.human_action?.context?.review;
|
|
705
|
+
const reviewLeg = Array.isArray(review?.legs) ? review.legs[0] : undefined;
|
|
706
|
+
const facts = leg ?? reviewLeg;
|
|
707
|
+
if (facts) {
|
|
708
|
+
itineraryTitle = [facts.travel_date, facts.train_code,
|
|
709
|
+
facts.from ?? facts.from_station,
|
|
710
|
+
"→", facts.to ?? facts.to_station,
|
|
711
|
+
facts.seat_name ?? facts.seat_type_name]
|
|
712
|
+
.filter((value) => typeof value === "string" && value.length > 0).join(" ");
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
// Checkout already exists; a display-only title lookup cannot hide its official handoff.
|
|
718
|
+
}
|
|
671
719
|
const plan = buildCheckoutQRPlan({
|
|
672
720
|
host,
|
|
673
721
|
checkoutID,
|
|
@@ -678,7 +726,7 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
678
726
|
qrPNGURL: cardPNGURL,
|
|
679
727
|
nextAction: checkout.checkout.next_action,
|
|
680
728
|
orderItems: response.cart.items.map((item) => ({
|
|
681
|
-
title: item.title,
|
|
729
|
+
title: itineraryTitle || item.title,
|
|
682
730
|
quantity: item.quantity,
|
|
683
731
|
amountMinor: item.amount_minor,
|
|
684
732
|
currency: item.currency,
|
|
@@ -792,6 +840,7 @@ export async function runServicesGet(backend, serviceExecutionID, options = {})
|
|
|
792
840
|
const lockedRefund = response.refunds.find((refund) => refund.access_locked);
|
|
793
841
|
const nextState = servicesNextEnvelope(response);
|
|
794
842
|
const result = {
|
|
843
|
+
...nextState.result,
|
|
795
844
|
service_execution_id: execution.service_execution_id,
|
|
796
845
|
service_id: execution.service_id,
|
|
797
846
|
status: execution.status,
|
|
@@ -809,13 +858,14 @@ export async function runServicesGet(backend, serviceExecutionID, options = {})
|
|
|
809
858
|
} : {}),
|
|
810
859
|
};
|
|
811
860
|
const envelope = {
|
|
812
|
-
status:
|
|
861
|
+
status: nextState.status,
|
|
813
862
|
result,
|
|
814
|
-
instruction:
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
863
|
+
instruction: nextState.instruction,
|
|
864
|
+
next: nextState.next,
|
|
865
|
+
...(nextState.interaction ? { interaction: nextState.interaction } : {}),
|
|
866
|
+
...(nextState.communication ? { communication: nextState.communication } : {}),
|
|
867
|
+
...(nextState.handoff ? { handoff: nextState.handoff } : {}),
|
|
868
|
+
recovery: nextState.recovery,
|
|
819
869
|
};
|
|
820
870
|
writeCommandEnvelope(envelope, {
|
|
821
871
|
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
@@ -829,9 +879,31 @@ export async function runServicesGet(backend, serviceExecutionID, options = {})
|
|
|
829
879
|
],
|
|
830
880
|
});
|
|
831
881
|
}
|
|
882
|
+
function shouldWaitForServiceResult(model) {
|
|
883
|
+
if (model.current_delivery || model.rail_booking || isTerminalServiceExecutionStatus(model.execution.status))
|
|
884
|
+
return false;
|
|
885
|
+
if (model.rail_planning) {
|
|
886
|
+
const plan = model.rail_planning;
|
|
887
|
+
return !plan.recommendation && !(plan.alternatives?.length) &&
|
|
888
|
+
!["complete", "paused", "failed", "cancelled", "expired"].includes(plan.search?.expansion_status ?? "") &&
|
|
889
|
+
!plan.available_actions?.some((action) => ["select_journey", "confirm_location", "expand_search"].includes(action.type));
|
|
890
|
+
}
|
|
891
|
+
return ["queued", "running", "delivery"].includes(model.workflow?.status ?? "");
|
|
892
|
+
}
|
|
832
893
|
export async function runServicesNext(backend, serviceExecutionID, options = {}) {
|
|
833
|
-
const
|
|
894
|
+
const started = Date.now();
|
|
895
|
+
const until = started + (options.timeoutSeconds ?? 0) * 1000;
|
|
896
|
+
const sleep = options.sleep ?? ((milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds)));
|
|
897
|
+
let response = await backend.getServiceExecution(serviceExecutionID, options.sinceSnapshot ? { sinceSnapshot: options.sinceSnapshot } : {});
|
|
898
|
+
while (shouldWaitForServiceResult(response) && Date.now() < until) {
|
|
899
|
+
await sleep(options.pollIntervalMS ?? Math.max(1500, response.rail_planning?.search?.poll_after_ms ?? 0));
|
|
900
|
+
response = await backend.getServiceExecution(serviceExecutionID, options.sinceSnapshot ? { sinceSnapshot: options.sinceSnapshot } : {});
|
|
901
|
+
}
|
|
834
902
|
const envelope = servicesNextEnvelope(response);
|
|
903
|
+
if (shouldWaitForServiceResult(response) && options.timeoutSeconds) {
|
|
904
|
+
envelope.result = { ...envelope.result, waited_seconds: Math.round((Date.now() - started) / 1000), wait_timed_out: true };
|
|
905
|
+
envelope.next = { command: `itpay services next ${serviceExecutionID} --timeout 120 --json`, reason: "稍后继续等待同一任务;不重新提交查询" };
|
|
906
|
+
}
|
|
835
907
|
journalTaskState(serviceExecutionID, envelope, options.env ?? process.env);
|
|
836
908
|
if (response.rail_booking)
|
|
837
909
|
envelope.result = { ...envelope.result, rail_booking: response.rail_booking };
|
|
@@ -843,7 +915,7 @@ export async function runServicesNext(backend, serviceExecutionID, options = {})
|
|
|
843
915
|
}
|
|
844
916
|
export async function runServicesPage(backend, serviceExecutionID, resultItemID, options = {}) {
|
|
845
917
|
const offset = options.offset ?? 0;
|
|
846
|
-
const limit = options.limit ??
|
|
918
|
+
const limit = options.limit ?? 20;
|
|
847
919
|
if (!Number.isInteger(offset) || offset < 0) {
|
|
848
920
|
throw new CommandContractError("offset_invalid", "--offset must be a non-negative integer", "offset 必须是非负整数;本次未读取服务端分页。", [{ command: `itpay services page ${serviceExecutionID} ${resultItemID} --json`, reason: "从第一页重新读取" }]);
|
|
849
921
|
}
|
|
@@ -853,6 +925,33 @@ export async function runServicesPage(backend, serviceExecutionID, resultItemID,
|
|
|
853
925
|
const response = await backend.getServiceExecutionResultItemPage(serviceExecutionID, resultItemID, offset, limit);
|
|
854
926
|
const page = response.page;
|
|
855
927
|
const container = (page.result ?? page);
|
|
928
|
+
const exactCandidate = limit === 1 && Array.isArray(container.candidates) && container.candidates.length === 1
|
|
929
|
+
? container.candidates[0] : undefined;
|
|
930
|
+
if (exactCandidate && typeof exactCandidate.train_code === "string" && Array.isArray(exactCandidate.seats)) {
|
|
931
|
+
const offer = exactCandidate.booking_offer;
|
|
932
|
+
const seats = exactCandidate.seats.filter((seat) => seat.purchase_supported === true);
|
|
933
|
+
const seat = seats[0];
|
|
934
|
+
writeCommandEnvelope({
|
|
935
|
+
status: "exact_train_detail",
|
|
936
|
+
result: {
|
|
937
|
+
service_execution_id: serviceExecutionID, result_item_id: resultItemID, result_offset: offset,
|
|
938
|
+
candidate: exactCandidate,
|
|
939
|
+
...(offer?.service_id && offer.selection_token ? { booking_template: {
|
|
940
|
+
command: `itpay services run ${offer.service_id} --input-json <file> --json`,
|
|
941
|
+
executable: false, required_input: ["file", "passengers", "seat_type"],
|
|
942
|
+
seat_choices: seats.map((choice) => ({ seat_type: choice.seat_type, seat_name: choice.seat_name })),
|
|
943
|
+
input_example: { selection: { token: offer.selection_token, ...(typeof seat?.seat_type === "string" ? { seat_type: seat.seat_type } : {}) }, passengers: 1 },
|
|
944
|
+
} } : {}),
|
|
945
|
+
},
|
|
946
|
+
instruction: offer?.selection_token
|
|
947
|
+
? "这是已保存车次的当前详情;只按 purchase_supported 席别代码、库存与报价状态选择。购票模板的人数须按用户真实需求填写;查询价需在后续报价确认。若用户仅要求比较,答复后停止。"
|
|
948
|
+
: "这条车次当前没有可用的购买凭据;说明席别与余票事实,不要猜测代码或构造订单。",
|
|
949
|
+
next: null, recovery: [],
|
|
950
|
+
}, { ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
951
|
+
...(options.output ? { output: options.output } : {}),
|
|
952
|
+
plainResult: [`${exactCandidate.train_code} ${exactCandidate.from_station ?? ""} → ${exactCandidate.to_station ?? ""}`] });
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
856
955
|
// rail.progressive.v2 saved pages key journeys/journey_page; legacy result
|
|
857
956
|
// items key candidates/catalog_page. Both are committed, read-only slices.
|
|
858
957
|
const v2Page = (container.journey_page ?? {});
|
|
@@ -873,11 +972,11 @@ export async function runServicesPage(backend, serviceExecutionID, resultItemID,
|
|
|
873
972
|
next_offset: nextOffset,
|
|
874
973
|
page,
|
|
875
974
|
},
|
|
876
|
-
instruction: "
|
|
975
|
+
instruction: "读取的是已保存结果的同版本分页,不重新查询、不消耗额度。先读完所需页,再合并回答用户;不要逐页播报。",
|
|
877
976
|
next: nextOffset !== null
|
|
878
977
|
? { command: response.snapshot_id
|
|
879
|
-
? `itpay services page ${serviceExecutionID} ${resultItemID} --cursor rcur_${nextOffset} --json`
|
|
880
|
-
: `itpay services page ${serviceExecutionID} ${resultItemID} --offset ${nextOffset} --json`,
|
|
978
|
+
? `itpay services page ${serviceExecutionID} ${resultItemID} --cursor rcur_${nextOffset} --limit ${limit} --json`
|
|
979
|
+
: `itpay services page ${serviceExecutionID} ${resultItemID} --offset ${nextOffset} --limit ${limit} --json`,
|
|
881
980
|
reason: "读取同版本结果的下一页" }
|
|
882
981
|
: null,
|
|
883
982
|
recovery: offset > 0
|
|
@@ -930,13 +1029,56 @@ export async function runServicesList(backend, options = {}) {
|
|
|
930
1029
|
});
|
|
931
1030
|
}
|
|
932
1031
|
export async function runServicesReadResult(backend, serviceExecutionID, options = {}) {
|
|
1032
|
+
if (!options.snapshot && !options.journey) {
|
|
1033
|
+
const model = await backend.getServiceExecution(serviceExecutionID);
|
|
1034
|
+
if (model.execution.service_id === "itpay-rail-exact") {
|
|
1035
|
+
const item = (model.current_result_items?.length ? model.current_result_items : model.result_items)?.[0];
|
|
1036
|
+
if (!item) {
|
|
1037
|
+
await runServicesNext(backend, serviceExecutionID, options);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
const rows = [];
|
|
1041
|
+
let offset = 0;
|
|
1042
|
+
let total = 0;
|
|
1043
|
+
let resolvedStationPair;
|
|
1044
|
+
let nextOffset = 0;
|
|
1045
|
+
while (nextOffset !== null && rows.length < 100) {
|
|
1046
|
+
const response = await backend.getServiceExecutionResultItemPage(serviceExecutionID, item.service_capability_result_item_id, offset, 20);
|
|
1047
|
+
const container = (response.page.result ?? response.page);
|
|
1048
|
+
const candidates = Array.isArray(container.candidates) ? container.candidates : [];
|
|
1049
|
+
resolvedStationPair ??= container.resolved_station_pair;
|
|
1050
|
+
rows.push(...candidates.map((value, index) => ({
|
|
1051
|
+
...compactExactTrain(value),
|
|
1052
|
+
result_item_id: item.service_capability_result_item_id,
|
|
1053
|
+
result_offset: offset + index,
|
|
1054
|
+
detail: { command: `itpay services page ${serviceExecutionID} ${item.service_capability_result_item_id} --offset ${offset + index} --limit 1 --json`,
|
|
1055
|
+
reason: "读取此车次的当前席别与服务端购买凭据" },
|
|
1056
|
+
})));
|
|
1057
|
+
const page = (container.catalog_page ?? {});
|
|
1058
|
+
total = typeof container.catalog_total === "number" ? container.catalog_total : Math.max(total, rows.length);
|
|
1059
|
+
nextOffset = typeof page.next_offset === "number" && page.next_offset > offset ? page.next_offset : null;
|
|
1060
|
+
offset = nextOffset ?? offset;
|
|
1061
|
+
}
|
|
1062
|
+
writeCommandEnvelope({
|
|
1063
|
+
status: "ready",
|
|
1064
|
+
result: { service_execution_id: serviceExecutionID, scope: "station_pair", resolved_station_pair: resolvedStationPair,
|
|
1065
|
+
total, read_count: rows.length,
|
|
1066
|
+
trains: rows, ...(nextOffset !== null ? { next_offset: nextOffset } : {}) },
|
|
1067
|
+
instruction: "这是同一已保存站对的车次行。按用户时限、席别和人数筛选;零条仅代表这一站对。纯比较答复后结束;已有购票委托则执行所选行 detail.command 读取当前席别、库存和购买凭据。",
|
|
1068
|
+
next: nextOffset !== null ? { command: `itpay services page ${serviceExecutionID} ${item.service_capability_result_item_id} --offset ${nextOffset} --limit 20 --json`, reason: "继续读取同一结果的剩余车次" } : null,
|
|
1069
|
+
recovery: [],
|
|
1070
|
+
}, { ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
1071
|
+
...(options.output ? { output: options.output } : {}),
|
|
1072
|
+
plainResult: rows.map((row) => `${row.train_code} ${row.from_station} ${row.departure} → ${row.to_station} ${row.arrival}`) });
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
933
1076
|
// rail.progressive.v2 planning read: --journey/--snapshot select committed
|
|
934
1077
|
// planning evidence — a free owner-validated read that never touches the
|
|
935
1078
|
// grant/Vault path. Without the selectors the authorized-delivery flow is
|
|
936
1079
|
// unchanged.
|
|
937
|
-
// rail.planning
|
|
938
|
-
// committed rail.catalog.v3
|
|
939
|
-
// preserved verbatim; one catalog, never split into candidates+journeys.
|
|
1080
|
+
// rail.planning catalog read: --snapshot returns compact rows for the
|
|
1081
|
+
// committed rail.catalog.v3; journey detail remains available separately.
|
|
940
1082
|
if (options.snapshot && !options.journey) {
|
|
941
1083
|
const committed = await backend.getRailPlanningCatalog(serviceExecutionID, options.snapshot);
|
|
942
1084
|
const catalog = committed.catalog;
|
|
@@ -946,7 +1088,10 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
946
1088
|
const decoded = catalog
|
|
947
1089
|
? decodeRailCatalogJourneys(catalog)
|
|
948
1090
|
: { journeys: [], packed: false };
|
|
949
|
-
const journeys = decoded.journeys
|
|
1091
|
+
const journeys = decoded.journeys.map((journey) => ({ ...journey,
|
|
1092
|
+
detail: { command: `itpay services read-result ${serviceExecutionID} --snapshot ${committed.snapshot_id} --journey ${journey.ref} --json`,
|
|
1093
|
+
reason: "读取同一已保存快照的此行程完整购票条件" },
|
|
1094
|
+
}));
|
|
950
1095
|
const counts = (catalog?.counts ?? {});
|
|
951
1096
|
const journeyCount = journeys.length || Number(counts?.combinations ?? 0);
|
|
952
1097
|
writeCommandEnvelope({
|
|
@@ -956,12 +1101,13 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
956
1101
|
plan_id: committed.plan_id,
|
|
957
1102
|
snapshot_id: committed.snapshot_id,
|
|
958
1103
|
query_revision: committed.query_revision,
|
|
959
|
-
catalog,
|
|
1104
|
+
coverage: catalog?.coverage,
|
|
1105
|
+
calculation_scope: catalog?.request_summary,
|
|
1106
|
+
total: journeyCount,
|
|
1107
|
+
journeys,
|
|
960
1108
|
},
|
|
961
|
-
instruction: "
|
|
962
|
-
next:
|
|
963
|
-
? { command: `itpay services read-result ${serviceExecutionID} --snapshot ${committed.snapshot_id} --journey ${journeys[0].ref} --json`, reason: "查看单个行程明细" }
|
|
964
|
-
: { command: `itpay services next ${serviceExecutionID} --since-snapshot ${committed.snapshot_id} --json`, reason: "返回规划进展" },
|
|
1109
|
+
instruction: "这是同一已保存快照的紧凑行程目录。按用户问题筛选全部 journeys 并一次回答;未计入接驳或未知费用不能当免费。需要购票详情时执行所选行 detail.command;价格和余票仍须购买前实时报价。",
|
|
1110
|
+
next: null,
|
|
965
1111
|
recovery: [],
|
|
966
1112
|
}, {
|
|
967
1113
|
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
@@ -969,7 +1115,7 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
969
1115
|
plainResult: journeys.length > 0
|
|
970
1116
|
? [
|
|
971
1117
|
`${journeyCount} combinations:`,
|
|
972
|
-
...journeys.map((j) => `${j.defaultLayer === "backup" ? "[备选] " : "[主选择] "}${j.ref} ${j.route}`),
|
|
1118
|
+
...journeys.map((j) => `${j.defaultLayer === "backup" ? "[备选] " : "[主选择] "}${j.ref} ${j.route} ${j.rides?.map((r) => `${r.train_code} ${r.departure}–${r.arrival}`).join(" / ") ?? ""}`),
|
|
973
1119
|
]
|
|
974
1120
|
: [`catalog: ${committed.snapshot_id} (no combinations)`],
|
|
975
1121
|
});
|
|
@@ -986,10 +1132,13 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
986
1132
|
query_revision: detail.query_revision,
|
|
987
1133
|
journey: detail.journey,
|
|
988
1134
|
},
|
|
989
|
-
instruction: "展示该 journey 的完整明细(车次、分段、席别报价、接驳估计与风险标注)。rail_payable 只是该行程当前可购报价的参考价,不是锁价;下单前须走受保护 Checkout
|
|
990
|
-
next:
|
|
991
|
-
|
|
992
|
-
|
|
1135
|
+
instruction: "展示该 journey 的完整明细(车次、分段、席别报价、接驳估计与风险标注)。rail_payable 只是该行程当前可购报价的参考价,不是锁价;下单前须走受保护 Checkout 收集乘车人。用户已明确委托按规则代选时用 select_delegated;用户亲自选定该行程时用 select_user_chosen,不能把 Agent 选择记作人类动作。",
|
|
1136
|
+
next: null,
|
|
1137
|
+
...(detail.journey?.booking_support === "single_leg" ? { interaction: {
|
|
1138
|
+
schema_version: "itpay.interaction.v1", stage: "query_results_ready",
|
|
1139
|
+
select_user_chosen: `itpay services action ${serviceExecutionID} --action select_journey --actor-type human --status approved --input journey_id=${detail.journey.journey_id} --json`,
|
|
1140
|
+
select_delegated: `itpay services action ${serviceExecutionID} --action select_journey --actor-type agent --status approved --input journey_id=${detail.journey.journey_id} --input selection_mode=delegated --json`,
|
|
1141
|
+
} } : {}),
|
|
993
1142
|
recovery: [],
|
|
994
1143
|
}, {
|
|
995
1144
|
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
@@ -1014,6 +1163,25 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
1014
1163
|
plainResult: servicesNextPlainResult(envelope.result),
|
|
1015
1164
|
});
|
|
1016
1165
|
}
|
|
1166
|
+
function compactExactTrain(candidate) {
|
|
1167
|
+
const seats = Array.isArray(candidate.seats) ? candidate.seats : [];
|
|
1168
|
+
return {
|
|
1169
|
+
candidate_id: candidate.candidate_id,
|
|
1170
|
+
train_code: candidate.train_code,
|
|
1171
|
+
from_station: candidate.from_station,
|
|
1172
|
+
from_station_code: candidate.from_station_code,
|
|
1173
|
+
to_station: candidate.to_station,
|
|
1174
|
+
to_station_code: candidate.to_station_code,
|
|
1175
|
+
travel_date: candidate.travel_date,
|
|
1176
|
+
departure: candidate.departure,
|
|
1177
|
+
arrival: candidate.arrival,
|
|
1178
|
+
purchase_supported: candidate.purchase_supported ?? Boolean(candidate.booking_offer),
|
|
1179
|
+
seats: seats.map((seat) => ({ seat_type: seat.seat_type, seat_name: seat.seat_name,
|
|
1180
|
+
availability_text: seat.availability_text, remaining: seat.remaining,
|
|
1181
|
+
fare_minor: seat.fare_minor, quoted_total_minor: seat.quoted_total_minor,
|
|
1182
|
+
purchase_supported: seat.purchase_supported, requires_price_refresh: seat.requires_price_refresh })),
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1017
1185
|
function railLegSeatSummary(seat) {
|
|
1018
1186
|
return {
|
|
1019
1187
|
passenger_index: seat.passenger_index,
|
|
@@ -1095,12 +1263,19 @@ function railJourneySummary(card, serviceExecutionID) {
|
|
|
1095
1263
|
const first = rides[0];
|
|
1096
1264
|
const last = rides.at(-1);
|
|
1097
1265
|
const offer = card.representative_offer;
|
|
1266
|
+
const metrics = card.representative_metrics;
|
|
1098
1267
|
return {
|
|
1099
1268
|
journey_id: card.journey_id,
|
|
1100
1269
|
route: (card.route_names?.length ? card.route_names : card.route ?? []).join("→"),
|
|
1101
1270
|
...(trains.length ? { trains } : {}),
|
|
1102
1271
|
...(first?.departure || last?.arrival ? { time: `${first?.departure ?? ""}–${last?.arrival ?? ""}` } : {}),
|
|
1103
1272
|
...(offer ? { price: formatMoney(offer.rail_payable_minor, offer.currency) } : { price: "unknown" }),
|
|
1273
|
+
...(metrics ? { calculation_scope: {
|
|
1274
|
+
origin: metrics.origin_scope ?? "legacy_unknown", destination: metrics.destination_scope ?? "legacy_unknown",
|
|
1275
|
+
duration_basis: metrics.duration_basis ?? "legacy_unknown", arrival_basis: metrics.arrival_basis ?? "legacy_unknown",
|
|
1276
|
+
cost_basis: metrics.cost_basis ?? "legacy_unknown",
|
|
1277
|
+
duration_minutes: metrics.door_to_door_min ?? null,
|
|
1278
|
+
} } : {}),
|
|
1104
1279
|
...(card.decision_role ? { decision_role: card.decision_role } : {}),
|
|
1105
1280
|
...(card.explanation?.length ? { explanation: card.explanation } : {}),
|
|
1106
1281
|
...(card.reason_codes?.length ? { reason_codes: card.reason_codes } : {}),
|
|
@@ -1113,7 +1288,7 @@ function railJourneySummary(card, serviceExecutionID) {
|
|
|
1113
1288
|
...(card.risk_notes?.length ? { risk_notes: card.risk_notes } : {}),
|
|
1114
1289
|
...(card.booking_support === "single_leg"
|
|
1115
1290
|
? {
|
|
1116
|
-
|
|
1291
|
+
select_user_chosen: `itpay services action ${serviceExecutionID} --action select_journey --actor-type human --status approved --input journey_id=${card.journey_id} --json`,
|
|
1117
1292
|
// Delegated selection under explicit user rules is an agent action —
|
|
1118
1293
|
// never record it as a human pick.
|
|
1119
1294
|
select_delegated: `itpay services action ${serviceExecutionID} --action select_journey --actor-type agent --status approved --input journey_id=${card.journey_id} --input selection_mode=delegated --json`,
|
|
@@ -1126,7 +1301,19 @@ function railJourneySummary(card, serviceExecutionID) {
|
|
|
1126
1301
|
command: `itpay services run ${card.booking_offer.service_id} --input-json <file> --json`,
|
|
1127
1302
|
required_input: ["file"],
|
|
1128
1303
|
executable: false,
|
|
1129
|
-
|
|
1304
|
+
seat_choices: (card.representative_offer?.ticket_offers ?? []).map((offer) => ({
|
|
1305
|
+
seat_type: offer.seat_type,
|
|
1306
|
+
seat_name: offer.seat_name,
|
|
1307
|
+
})).filter((seat) => typeof seat.seat_type === "string"),
|
|
1308
|
+
input_example: {
|
|
1309
|
+
selection: {
|
|
1310
|
+
token: card.booking_offer.selection_token,
|
|
1311
|
+
...(typeof card.representative_offer?.ticket_offers?.[0]?.seat_type === "string"
|
|
1312
|
+
? { seat_type: card.representative_offer.ticket_offers[0].seat_type }
|
|
1313
|
+
: {}),
|
|
1314
|
+
},
|
|
1315
|
+
passengers: card.passengers ?? 1,
|
|
1316
|
+
},
|
|
1130
1317
|
},
|
|
1131
1318
|
}
|
|
1132
1319
|
: {}),
|
|
@@ -1184,11 +1371,12 @@ function railPlanningEnvelope(model) {
|
|
|
1184
1371
|
...(plan.notices?.length ? { notices: plan.notices } : {}),
|
|
1185
1372
|
...(plan.recommendation ? { recommendation: railJourneySummary(plan.recommendation, se) } : {}),
|
|
1186
1373
|
...(cards.length ? { journeys: cards } : {}),
|
|
1374
|
+
...(plan.snapshot_id ? { full_result: { command: `itpay services read-result ${se} --snapshot ${plan.snapshot_id} --json`, meaning: "读取已保存的完整目录;推荐和journeys仅是摘要" } } : {}),
|
|
1187
1375
|
...(plan.available_actions?.length ? { available_actions: plan.available_actions } : {}),
|
|
1188
1376
|
};
|
|
1189
1377
|
const nextPoll = {
|
|
1190
|
-
command: `itpay services next ${se}${plan.snapshot_id ? ` --since-snapshot ${plan.snapshot_id}` : ""} --json`,
|
|
1191
|
-
reason: "
|
|
1378
|
+
command: `itpay services next ${se} --timeout 120${plan.snapshot_id ? ` --since-snapshot ${plan.snapshot_id}` : ""} --json`,
|
|
1379
|
+
reason: "仍需更多结果时有界等待同一规划(不触发供应商调用)",
|
|
1192
1380
|
};
|
|
1193
1381
|
// Two-goal interaction contract (spec 03 §2): the local Agent picks compare
|
|
1194
1382
|
// or prepare_checkout from full context — the envelope teaches both branches
|
|
@@ -1207,7 +1395,7 @@ function railPlanningEnvelope(model) {
|
|
|
1207
1395
|
...(bookable
|
|
1208
1396
|
? {
|
|
1209
1397
|
steps: [
|
|
1210
|
-
"按用户明确规则在 journeys 中选择合格者(卡片 select_delegated 提交 agent 委托选择;人类亲自选择用
|
|
1398
|
+
"按用户明确规则在 journeys 中选择合格者(卡片 select_delegated 提交 agent 委托选择;人类亲自选择用 select_user_chosen)",
|
|
1211
1399
|
`用所选卡片的 booking_template 写 owner-only 临时输入(selection token + 席别 + 人数,不含身份信息),运行 itpay services run itpay-rail-booking --input-json <file> --json`,
|
|
1212
1400
|
"owner 要求聊天 review 时一次合并补齐必要字段,真实确认后提交原 workflow:confirm_booking;不得伪造 accept",
|
|
1213
1401
|
`确认后恢复同一购买执行:itpay services run itpay-rail-booking --execution <实际ID> --json,沿用有界等待与受保护 Checkout`,
|
|
@@ -1240,15 +1428,20 @@ function railPlanningEnvelope(model) {
|
|
|
1240
1428
|
: undefined;
|
|
1241
1429
|
switch (expansion) {
|
|
1242
1430
|
case "complete":
|
|
1431
|
+
if (plan.search?.reason === "ground_evidence_required") {
|
|
1432
|
+
return {
|
|
1433
|
+
status: "awaiting_input", result,
|
|
1434
|
+
instruction: "已查到铁路班次,但具体地址的接驳或出站证据不足,无法验证到达时限;剩余站对未继续查询,不能说没有车。向用户说明已查范围,并请其补充准确起终点地址或可验证的接驳/出站信息;取得新事实后再按其要求重新规划,不要默认扩大铁路搜索。",
|
|
1435
|
+
next: null, recovery: [],
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1243
1438
|
return {
|
|
1244
1439
|
status: "ready",
|
|
1245
1440
|
result,
|
|
1246
1441
|
// §7.3: the lead line carries real combination counts bucketed by
|
|
1247
1442
|
// verified transfer count — seat rows never inflate the journey count.
|
|
1248
|
-
instruction: `${journeyMix ? `本次共${journeyMix}(席别不重复计数)。` : ""}${plan.search?.reason === "budget_exhausted" ? "本次查询已达到上限,以下是已查到的方案,搜索范围尚未全部核验。" : "本轮规划已完成。"}compare:解释首选及最多两个有意义备选的时间/费用/便利性取舍后等待用户选择。prepare_checkout:用户已明确委托按规则选择时,按 interaction.recipe 选合格者并继续到官方确认页,不再次问是否下单;身份、review和最终付款边界仍然生效。乘车人身份信息不在此收集,后续购买走受保护 Checkout。`,
|
|
1249
|
-
next:
|
|
1250
|
-
? { command: railJourneySummary(journeys[0], se).select, reason: "用户在 compare 后选定推荐行程时执行;prepare_checkout 且规则命中时用 select_delegated" }
|
|
1251
|
-
: null,
|
|
1443
|
+
instruction: `${journeyMix ? `本次共${journeyMix}(席别不重复计数)。` : ""}${plan.search?.reason === "budget_exhausted" ? "本次查询已达到上限,以下是已查到的方案,搜索范围尚未全部核验。" : "本轮规划已完成。"}推荐和journeys是摘要;追问其他车次先读取 full_result 的已保存完整目录。compare:解释首选及最多两个有意义备选的时间/费用/便利性取舍后等待用户选择。prepare_checkout:用户已明确委托按规则选择时,按 interaction.recipe 选合格者并继续到官方确认页,不再次问是否下单;身份、review和最终付款边界仍然生效。乘车人身份信息不在此收集,后续购买走受保护 Checkout。`,
|
|
1444
|
+
next: null,
|
|
1252
1445
|
...(queryResultsInteraction ? { interaction: queryResultsInteraction } : {}),
|
|
1253
1446
|
...(queryResultsCommunication ? { communication: queryResultsCommunication } : {}),
|
|
1254
1447
|
recovery: [],
|
|
@@ -1319,9 +1512,11 @@ function railPlanningEnvelope(model) {
|
|
|
1319
1512
|
return {
|
|
1320
1513
|
status: expansion,
|
|
1321
1514
|
result,
|
|
1322
|
-
instruction: `${modelDegradedHint}${failedPairsHint}${savedCatalog}${partial}
|
|
1515
|
+
instruction: `${modelDegradedHint}${failedPairsHint}${savedCatalog}${partial}${expansion === "expired" ? "规划已过期,已保存目录仍可读取并回答历史结果追问;购买须重新核验实时余票与报价。" : "本次规划已结束,展示已有结果;需要新的供应商观测时再明确发起查询。"}`,
|
|
1323
1516
|
next: null,
|
|
1324
|
-
recovery:
|
|
1517
|
+
recovery: plan.snapshot_id
|
|
1518
|
+
? [{ command: `itpay services read-result ${se} --snapshot ${plan.snapshot_id} --json`, reason: "读取同一任务已保存的完整目录" }]
|
|
1519
|
+
: [{ command: `itpay services next ${se} --json`, reason: "读取同一任务当前状态" }],
|
|
1325
1520
|
};
|
|
1326
1521
|
}
|
|
1327
1522
|
default: {
|
|
@@ -1593,14 +1788,23 @@ function servicesNextEnvelope(model) {
|
|
|
1593
1788
|
const failedInvocation = recovery
|
|
1594
1789
|
? [...model.provider_invocations].reverse().find((item) => typeof item.status === "string" && item.status.startsWith("failed"))
|
|
1595
1790
|
: undefined;
|
|
1596
|
-
const errorDetail = typeof failedInvocation?.error_message === "string" && failedInvocation.error_message ? failedInvocation.error_message : undefined;
|
|
1597
1791
|
const providerErrorCode = typeof failedInvocation?.error_code === "string" && failedInvocation.error_code ? failedInvocation.error_code : undefined;
|
|
1792
|
+
const quoteFailure = execution.service_id === "itpay-rail-booking" ? {
|
|
1793
|
+
quote_source_unavailable: "实时车票数据暂不可用;保留原选择,稍后按用户意愿重新核验,不创建替代订单。",
|
|
1794
|
+
quote_train_changed: "所选车次的时刻或运行信息已变化;向用户说明并征询是否重新查票。",
|
|
1795
|
+
quote_seat_changed: "所选席别已变化;向用户说明并征询新的席别选择。",
|
|
1796
|
+
quote_seat_unavailable: "所选席别当前无足够余票;告知用户并由其决定是否换方案。",
|
|
1797
|
+
quote_refresh_expired: "实时报价过程超时;保留原选择,不把旧价当可付款价格。",
|
|
1798
|
+
}[providerErrorCode ?? ""] : undefined;
|
|
1799
|
+
const failureCode = providerErrorCode ?? model.workflow?.error_code;
|
|
1598
1800
|
const failedInstruction = recovery
|
|
1599
|
-
?
|
|
1600
|
-
?
|
|
1601
|
-
:
|
|
1602
|
-
?
|
|
1603
|
-
: "
|
|
1801
|
+
? quoteFailure
|
|
1802
|
+
? quoteFailure
|
|
1803
|
+
: failedStepGuidance
|
|
1804
|
+
? `执行在「${failedStepGuidance.meaning}」步失败${failureCode ? `(错误码:${failureCode})` : ""}:${failedStepGuidance.hint}。先核对原因和当前执行状态,不要盲目重放。`
|
|
1805
|
+
: state === "failed"
|
|
1806
|
+
? `执行已失败${failureCode ? `(错误码:${failureCode})` : ""}且不可续用;核对服务端原因、订单与付款事实后再决定下一步。`
|
|
1807
|
+
: "执行未完成,请按步骤错误处理;不要重建执行或重复调用。"
|
|
1604
1808
|
: undefined;
|
|
1605
1809
|
return {
|
|
1606
1810
|
status: state,
|
|
@@ -1612,8 +1816,8 @@ function servicesNextEnvelope(model) {
|
|
|
1612
1816
|
...(guidance && state === "input_required" ? { guidance } : {}),
|
|
1613
1817
|
...(failedStep ? { failed_step: failedStep, ...(failedStepGuidance ? { failed_step_meaning: failedStepGuidance.meaning } : {}) } : {}),
|
|
1614
1818
|
...(providerErrorCode ? { provider_error_code: providerErrorCode } : {}),
|
|
1615
|
-
...(
|
|
1616
|
-
...(recovery ? { retryable:
|
|
1819
|
+
...(providerErrorCode && !quoteFailure ? { diagnostic_code: providerErrorCode } : {}),
|
|
1820
|
+
...(recovery ? { retryable: false } : {}),
|
|
1617
1821
|
},
|
|
1618
1822
|
instruction: failedInstruction
|
|
1619
1823
|
?? (state === "payment"
|
|
@@ -1639,10 +1843,7 @@ function servicesNextEnvelope(model) {
|
|
|
1639
1843
|
},
|
|
1640
1844
|
} : {}),
|
|
1641
1845
|
recovery: recovery && state === "failed"
|
|
1642
|
-
? [
|
|
1643
|
-
{ command: `itpay services start ${execution.service_id} --json`, reason: "修正输入后重新发起(本执行已终止不能续用)" },
|
|
1644
|
-
...(guidance ? [{ command: "itpay docs show rail-booking --json", reason: "查看本服务输入字段契约与示例" }] : []),
|
|
1645
|
-
]
|
|
1846
|
+
? [{ command: `itpay services get ${id} --json`, reason: "核对本执行的错误、结果和付款事实" }]
|
|
1646
1847
|
: [],
|
|
1647
1848
|
};
|
|
1648
1849
|
}
|
|
@@ -1659,6 +1860,23 @@ function servicesNextEnvelope(model) {
|
|
|
1659
1860
|
}
|
|
1660
1861
|
const delivery = currentDelivery;
|
|
1661
1862
|
const deliveryMode = serviceDeliveryMode(model);
|
|
1863
|
+
if (execution.service_id === "itpay-rail-exact" && currentItems.length > 0) {
|
|
1864
|
+
const item = currentItems[0];
|
|
1865
|
+
const container = (item.safe_payload?.result ?? item.safe_payload);
|
|
1866
|
+
const total = typeof container?.catalog_total === "number" ? container.catalog_total :
|
|
1867
|
+
Array.isArray(container?.candidates) ? container.candidates.length : 0;
|
|
1868
|
+
return {
|
|
1869
|
+
status: total > 0 ? "result_ready" : "no_result",
|
|
1870
|
+
result: { service_execution_id: execution.service_execution_id, scope: "station_pair",
|
|
1871
|
+
resolved_station_pair: container?.resolved_station_pair, total,
|
|
1872
|
+
result_item_id: item.service_capability_result_item_id },
|
|
1873
|
+
instruction: total > 0
|
|
1874
|
+
? "直达站对结果已保存。执行 next.command 一次读取紧凑车次行,按用户条件筛选;若只需比较,答复后结束;已有购票委托则读取所选行详情。"
|
|
1875
|
+
: "这一已解析站对返回 0 条;不代表全城无车。若原需求需要更广覆盖,可核验另一可信站对或 Smart。",
|
|
1876
|
+
next: { command: `itpay services read-result ${execution.service_execution_id} --json`, reason: "读取这一已保存站对的完整紧凑结果" },
|
|
1877
|
+
recovery: [],
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1662
1880
|
const candidateSelection = model.allowed_actions?.find((action) => action.type === "select_candidate");
|
|
1663
1881
|
if (candidateSelection && currentItems.length > 0) {
|
|
1664
1882
|
const paidCapability = delivery?.capability_id
|
|
@@ -2092,12 +2310,9 @@ export async function runServicesRun(backend, config, serviceID, input, options
|
|
|
2092
2310
|
const until = Date.now() + (options.timeoutSeconds ?? 120) * 1000;
|
|
2093
2311
|
const sleep = options.sleep ?? ((milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds)));
|
|
2094
2312
|
const paid = () => model.payment_bindings.some((binding) => binding.status === "payment_verified") || model.checkout_bindings.some((binding) => binding.status === "payment_verified");
|
|
2095
|
-
//
|
|
2096
|
-
//
|
|
2097
|
-
|
|
2098
|
-
// owner keeps advancing in the background; the CLI returning early never
|
|
2099
|
-
// cancels it.
|
|
2100
|
-
while ((["queued", "running", "delivery"].includes(model.workflow?.status ?? "") || (model.workflow?.status === "payment" && paid())) && !model.current_delivery && !model.rail_planning && Date.now() < until) {
|
|
2313
|
+
// A planning projection can exist before any usable result or user action.
|
|
2314
|
+
// Keep the existing bounded wait until there is something to hand over.
|
|
2315
|
+
while ((["queued", "running", "delivery"].includes(model.workflow?.status ?? "") || (model.workflow?.status === "payment" && paid())) && shouldWaitForServiceResult(model) && Date.now() < until) {
|
|
2101
2316
|
await sleep(options.pollIntervalMS ?? 1500);
|
|
2102
2317
|
model = await backend.getServiceExecution(id);
|
|
2103
2318
|
}
|
|
@@ -2116,7 +2331,10 @@ export async function runServicesRun(backend, config, serviceID, input, options
|
|
|
2116
2331
|
await runServicesNext(backend, id, options);
|
|
2117
2332
|
}
|
|
2118
2333
|
catch (cause) {
|
|
2119
|
-
if (cause instanceof
|
|
2334
|
+
if (cause instanceof HttpError && cause.code === "rail_seat_code_invalid" && id) {
|
|
2335
|
+
throw new CommandContractError(cause.code, cause.message, "席别必须使用原查询 booking_template.seat_choices 中与名称对应的代码,不能填“二等座”等显示名。用原 selection token 与真实人数修正输入,继续同一booking execution;若只是代码写法修正且车次、席别名称、人数等购买条件未变,沿用用户已给的确认,不再重复询问。", [{ command: `itpay services next ${id} --json`, reason: "读取同一booking当前状态后修正输入" }]);
|
|
2336
|
+
}
|
|
2337
|
+
if (cause instanceof CommandContractError || cause instanceof HttpError || cause instanceof DeviceLockBusyError || cause instanceof DeviceStateError)
|
|
2120
2338
|
throw cause;
|
|
2121
2339
|
if (cause instanceof HttpTransportError && !id) {
|
|
2122
2340
|
throw new CommandContractError("workflow_start_outcome_unknown", cause.message, "创建服务执行时没有收到完整响应。先查询当前身份可见的执行;不要直接重跑并创建替代执行。", [{ command: "itpay services list --json", reason: "查找可能已经创建的服务执行" }]);
|