@itpay/cli 2.1.19 → 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 +215 -47
- package/dist/src/commands/skill.js +1 -1
- package/dist/src/main.js +7 -3
- 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/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 +5 -4
- 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 -166
- package/docs/agent/buyer/rail-fast-checkout.json +21 -44
- 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 -238
- package/docs/reviews/agent-ux-handoff-2026-09-23/IMPLEMENTATION-EVIDENCE.md +0 -174
- package/docs/skill-bundle-rollout/01-mcp-authentication.md +0 -134
- package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +0 -210
- package/docs/skill-bundle-rollout/03-platform-publishing.md +0 -161
- package/docs/skill-bundle-rollout/04-first-wave-platforms.md +0 -70
- package/docs/skill-bundle-rollout/05-sync-operations.md +0 -62
- package/docs/skill-bundle-rollout/README.md +0 -151
- /package/bin/{itp → itp.mjs} +0 -0
|
@@ -35,7 +35,7 @@ function journalTaskState(serviceExecutionID, envelope, env) {
|
|
|
35
35
|
const serviceActionStatuses = new Set(["pending", "approved", "rejected", "expired", "cancelled"]);
|
|
36
36
|
const RAIL_SERVICE_GUIDANCE = {
|
|
37
37
|
"itpay-rail-exact": {
|
|
38
|
-
when_to_use: "
|
|
38
|
+
when_to_use: "已知可信站对时先用本服务核验直达;城市需求可先形成有依据的站对假设。只覆盖这一站对,不代表全城市最优。完整流程:itpay docs show rail-booking --json。",
|
|
39
39
|
input_fields: [
|
|
40
40
|
{ name: "origin", required: true, description: "出发火车站名(如'古镇'),不能是城市或地址", example: "古镇" },
|
|
41
41
|
{ name: "destination", required: true, description: "到达火车站名(如'广州南')", example: "广州南" },
|
|
@@ -44,11 +44,11 @@ const RAIL_SERVICE_GUIDANCE = {
|
|
|
44
44
|
input_example: { origin: "古镇", destination: "广州南", travel_date: "2026-09-19" },
|
|
45
45
|
notes: [
|
|
46
46
|
"本服务只接受这三个字段,多传字段会被供应商拒绝",
|
|
47
|
-
"
|
|
47
|
+
"查询为空只说明这一已解析站对;如原意仍需更多覆盖,可验证另一可信站对或使用 Smart",
|
|
48
48
|
],
|
|
49
49
|
},
|
|
50
50
|
"itpay-rail-smart": {
|
|
51
|
-
when_to_use: "
|
|
51
|
+
when_to_use: "需要广泛站点比较、中转或具体地址接驳时使用;有可信站对且只需先核验直达可用 Exact。完整流程:itpay docs show rail-booking --json。",
|
|
52
52
|
input_fields: [
|
|
53
53
|
{ name: "origin", required: true, description: "出发地:已知最完整的位置名或区域(城市/区县/地址均可)", example: "中山古镇" },
|
|
54
54
|
{ name: "destination", required: true, description: "目的地:与 origin 同样的位置规则", example: "广州南" },
|
|
@@ -63,8 +63,9 @@ const RAIL_SERVICE_GUIDANCE = {
|
|
|
63
63
|
{ name: "arrive_before", description: "不晚于该时间到达" },
|
|
64
64
|
{ name: "priority", enum: "balanced|fastest|cheapest|safest|flexible", description: "方案偏好" },
|
|
65
65
|
{ name: "max_transfers", description: "允许中转次数,0-2" },
|
|
66
|
+
{ name: "passengers", description: "成人乘客整数 1-5;不填写身份资料" },
|
|
66
67
|
],
|
|
67
|
-
input_example: { origin: "
|
|
68
|
+
input_example: { origin: "上海", destination: "长沙", travel_date: "2026-10-06", arrive_before: "15:00", priority: "fastest", passengers: 1 },
|
|
68
69
|
notes: [
|
|
69
70
|
"位置有歧义时会进入位置确认步骤,请用户选定后继续同一执行",
|
|
70
71
|
"推荐结果含每趟车的席别与余票;购票在后续受保护流程完成",
|
|
@@ -75,16 +76,16 @@ function railServiceGuidance(serviceID) {
|
|
|
75
76
|
return RAIL_SERVICE_GUIDANCE[serviceID];
|
|
76
77
|
}
|
|
77
78
|
const WORKFLOW_STEP_GUIDANCE = {
|
|
78
|
-
input: { meaning: "输入校验", hint: "
|
|
79
|
-
quota: { meaning: "额度检查", hint: "
|
|
80
|
-
geo: { meaning: "位置解析", hint: "
|
|
81
|
-
geo_confirm: { meaning: "确认后位置解析", hint: "
|
|
82
|
-
resolved: { meaning: "位置解析复核", hint: "
|
|
83
|
-
resolved_after_confirm: { meaning: "位置确认复核", hint: "
|
|
84
|
-
search: { meaning: "供应商车次检索", hint: "
|
|
85
|
-
catalog: { meaning: "可行车次计算", hint: "
|
|
86
|
-
recommend: { meaning: "方案推荐", hint: "
|
|
87
|
-
delivery: { meaning: "交付", hint: "
|
|
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: "读取当前执行状态;已有订单或付款状态不明时先核实,不重建" },
|
|
88
89
|
};
|
|
89
90
|
function failedWorkflowStep(steps, errorCode) {
|
|
90
91
|
if (!steps)
|
|
@@ -695,6 +696,26 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
695
696
|
const checkoutURL = tokenizedCheckoutURL(checkout.checkout_url, displayToken, checkout.qr_payload);
|
|
696
697
|
const cardURL = localizeCardURL(absolutePublicURL(config.baseURL, checkout.card_url ?? fallbackCardURL(config.baseURL, checkoutID, displayToken)), locale);
|
|
697
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
|
+
}
|
|
698
719
|
const plan = buildCheckoutQRPlan({
|
|
699
720
|
host,
|
|
700
721
|
checkoutID,
|
|
@@ -705,7 +726,7 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
705
726
|
qrPNGURL: cardPNGURL,
|
|
706
727
|
nextAction: checkout.checkout.next_action,
|
|
707
728
|
orderItems: response.cart.items.map((item) => ({
|
|
708
|
-
title: item.title,
|
|
729
|
+
title: itineraryTitle || item.title,
|
|
709
730
|
quantity: item.quantity,
|
|
710
731
|
amountMinor: item.amount_minor,
|
|
711
732
|
currency: item.currency,
|
|
@@ -840,7 +861,10 @@ export async function runServicesGet(backend, serviceExecutionID, options = {})
|
|
|
840
861
|
status: nextState.status,
|
|
841
862
|
result,
|
|
842
863
|
instruction: nextState.instruction,
|
|
843
|
-
next: nextState.next
|
|
864
|
+
next: nextState.next,
|
|
865
|
+
...(nextState.interaction ? { interaction: nextState.interaction } : {}),
|
|
866
|
+
...(nextState.communication ? { communication: nextState.communication } : {}),
|
|
867
|
+
...(nextState.handoff ? { handoff: nextState.handoff } : {}),
|
|
844
868
|
recovery: nextState.recovery,
|
|
845
869
|
};
|
|
846
870
|
writeCommandEnvelope(envelope, {
|
|
@@ -855,9 +879,31 @@ export async function runServicesGet(backend, serviceExecutionID, options = {})
|
|
|
855
879
|
],
|
|
856
880
|
});
|
|
857
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
|
+
}
|
|
858
893
|
export async function runServicesNext(backend, serviceExecutionID, options = {}) {
|
|
859
|
-
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
|
+
}
|
|
860
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
|
+
}
|
|
861
907
|
journalTaskState(serviceExecutionID, envelope, options.env ?? process.env);
|
|
862
908
|
if (response.rail_booking)
|
|
863
909
|
envelope.result = { ...envelope.result, rail_booking: response.rail_booking };
|
|
@@ -879,6 +925,33 @@ export async function runServicesPage(backend, serviceExecutionID, resultItemID,
|
|
|
879
925
|
const response = await backend.getServiceExecutionResultItemPage(serviceExecutionID, resultItemID, offset, limit);
|
|
880
926
|
const page = response.page;
|
|
881
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
|
+
}
|
|
882
955
|
// rail.progressive.v2 saved pages key journeys/journey_page; legacy result
|
|
883
956
|
// items key candidates/catalog_page. Both are committed, read-only slices.
|
|
884
957
|
const v2Page = (container.journey_page ?? {});
|
|
@@ -956,13 +1029,56 @@ export async function runServicesList(backend, options = {}) {
|
|
|
956
1029
|
});
|
|
957
1030
|
}
|
|
958
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
|
+
}
|
|
959
1076
|
// rail.progressive.v2 planning read: --journey/--snapshot select committed
|
|
960
1077
|
// planning evidence — a free owner-validated read that never touches the
|
|
961
1078
|
// grant/Vault path. Without the selectors the authorized-delivery flow is
|
|
962
1079
|
// unchanged.
|
|
963
|
-
// rail.planning
|
|
964
|
-
// committed rail.catalog.v3
|
|
965
|
-
// 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.
|
|
966
1082
|
if (options.snapshot && !options.journey) {
|
|
967
1083
|
const committed = await backend.getRailPlanningCatalog(serviceExecutionID, options.snapshot);
|
|
968
1084
|
const catalog = committed.catalog;
|
|
@@ -972,7 +1088,10 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
972
1088
|
const decoded = catalog
|
|
973
1089
|
? decodeRailCatalogJourneys(catalog)
|
|
974
1090
|
: { journeys: [], packed: false };
|
|
975
|
-
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
|
+
}));
|
|
976
1095
|
const counts = (catalog?.counts ?? {});
|
|
977
1096
|
const journeyCount = journeys.length || Number(counts?.combinations ?? 0);
|
|
978
1097
|
writeCommandEnvelope({
|
|
@@ -982,9 +1101,12 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
982
1101
|
plan_id: committed.plan_id,
|
|
983
1102
|
snapshot_id: committed.snapshot_id,
|
|
984
1103
|
query_revision: committed.query_revision,
|
|
985
|
-
catalog,
|
|
1104
|
+
coverage: catalog?.coverage,
|
|
1105
|
+
calculation_scope: catalog?.request_summary,
|
|
1106
|
+
total: journeyCount,
|
|
1107
|
+
journeys,
|
|
986
1108
|
},
|
|
987
|
-
instruction: "
|
|
1109
|
+
instruction: "这是同一已保存快照的紧凑行程目录。按用户问题筛选全部 journeys 并一次回答;未计入接驳或未知费用不能当免费。需要购票详情时执行所选行 detail.command;价格和余票仍须购买前实时报价。",
|
|
988
1110
|
next: null,
|
|
989
1111
|
recovery: [],
|
|
990
1112
|
}, {
|
|
@@ -993,7 +1115,7 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
993
1115
|
plainResult: journeys.length > 0
|
|
994
1116
|
? [
|
|
995
1117
|
`${journeyCount} combinations:`,
|
|
996
|
-
...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(" / ") ?? ""}`),
|
|
997
1119
|
]
|
|
998
1120
|
: [`catalog: ${committed.snapshot_id} (no combinations)`],
|
|
999
1121
|
});
|
|
@@ -1010,10 +1132,13 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
1010
1132
|
query_revision: detail.query_revision,
|
|
1011
1133
|
journey: detail.journey,
|
|
1012
1134
|
},
|
|
1013
|
-
instruction: "展示该 journey 的完整明细(车次、分段、席别报价、接驳估计与风险标注)。rail_payable 只是该行程当前可购报价的参考价,不是锁价;下单前须走受保护 Checkout
|
|
1014
|
-
next:
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
+
} } : {}),
|
|
1017
1142
|
recovery: [],
|
|
1018
1143
|
}, {
|
|
1019
1144
|
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
@@ -1038,6 +1163,25 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
1038
1163
|
plainResult: servicesNextPlainResult(envelope.result),
|
|
1039
1164
|
});
|
|
1040
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
|
+
}
|
|
1041
1185
|
function railLegSeatSummary(seat) {
|
|
1042
1186
|
return {
|
|
1043
1187
|
passenger_index: seat.passenger_index,
|
|
@@ -1119,12 +1263,19 @@ function railJourneySummary(card, serviceExecutionID) {
|
|
|
1119
1263
|
const first = rides[0];
|
|
1120
1264
|
const last = rides.at(-1);
|
|
1121
1265
|
const offer = card.representative_offer;
|
|
1266
|
+
const metrics = card.representative_metrics;
|
|
1122
1267
|
return {
|
|
1123
1268
|
journey_id: card.journey_id,
|
|
1124
1269
|
route: (card.route_names?.length ? card.route_names : card.route ?? []).join("→"),
|
|
1125
1270
|
...(trains.length ? { trains } : {}),
|
|
1126
1271
|
...(first?.departure || last?.arrival ? { time: `${first?.departure ?? ""}–${last?.arrival ?? ""}` } : {}),
|
|
1127
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
|
+
} } : {}),
|
|
1128
1279
|
...(card.decision_role ? { decision_role: card.decision_role } : {}),
|
|
1129
1280
|
...(card.explanation?.length ? { explanation: card.explanation } : {}),
|
|
1130
1281
|
...(card.reason_codes?.length ? { reason_codes: card.reason_codes } : {}),
|
|
@@ -1137,7 +1288,7 @@ function railJourneySummary(card, serviceExecutionID) {
|
|
|
1137
1288
|
...(card.risk_notes?.length ? { risk_notes: card.risk_notes } : {}),
|
|
1138
1289
|
...(card.booking_support === "single_leg"
|
|
1139
1290
|
? {
|
|
1140
|
-
|
|
1291
|
+
select_user_chosen: `itpay services action ${serviceExecutionID} --action select_journey --actor-type human --status approved --input journey_id=${card.journey_id} --json`,
|
|
1141
1292
|
// Delegated selection under explicit user rules is an agent action —
|
|
1142
1293
|
// never record it as a human pick.
|
|
1143
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`,
|
|
@@ -1224,8 +1375,8 @@ function railPlanningEnvelope(model) {
|
|
|
1224
1375
|
...(plan.available_actions?.length ? { available_actions: plan.available_actions } : {}),
|
|
1225
1376
|
};
|
|
1226
1377
|
const nextPoll = {
|
|
1227
|
-
command: `itpay services next ${se}${plan.snapshot_id ? ` --since-snapshot ${plan.snapshot_id}` : ""} --json`,
|
|
1228
|
-
reason: "
|
|
1378
|
+
command: `itpay services next ${se} --timeout 120${plan.snapshot_id ? ` --since-snapshot ${plan.snapshot_id}` : ""} --json`,
|
|
1379
|
+
reason: "仍需更多结果时有界等待同一规划(不触发供应商调用)",
|
|
1229
1380
|
};
|
|
1230
1381
|
// Two-goal interaction contract (spec 03 §2): the local Agent picks compare
|
|
1231
1382
|
// or prepare_checkout from full context — the envelope teaches both branches
|
|
@@ -1244,7 +1395,7 @@ function railPlanningEnvelope(model) {
|
|
|
1244
1395
|
...(bookable
|
|
1245
1396
|
? {
|
|
1246
1397
|
steps: [
|
|
1247
|
-
"按用户明确规则在 journeys 中选择合格者(卡片 select_delegated 提交 agent 委托选择;人类亲自选择用
|
|
1398
|
+
"按用户明确规则在 journeys 中选择合格者(卡片 select_delegated 提交 agent 委托选择;人类亲自选择用 select_user_chosen)",
|
|
1248
1399
|
`用所选卡片的 booking_template 写 owner-only 临时输入(selection token + 席别 + 人数,不含身份信息),运行 itpay services run itpay-rail-booking --input-json <file> --json`,
|
|
1249
1400
|
"owner 要求聊天 review 时一次合并补齐必要字段,真实确认后提交原 workflow:confirm_booking;不得伪造 accept",
|
|
1250
1401
|
`确认后恢复同一购买执行:itpay services run itpay-rail-booking --execution <实际ID> --json,沿用有界等待与受保护 Checkout`,
|
|
@@ -1277,15 +1428,20 @@ function railPlanningEnvelope(model) {
|
|
|
1277
1428
|
: undefined;
|
|
1278
1429
|
switch (expansion) {
|
|
1279
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
|
+
}
|
|
1280
1438
|
return {
|
|
1281
1439
|
status: "ready",
|
|
1282
1440
|
result,
|
|
1283
1441
|
// §7.3: the lead line carries real combination counts bucketed by
|
|
1284
1442
|
// verified transfer count — seat rows never inflate the journey count.
|
|
1285
1443
|
instruction: `${journeyMix ? `本次共${journeyMix}(席别不重复计数)。` : ""}${plan.search?.reason === "budget_exhausted" ? "本次查询已达到上限,以下是已查到的方案,搜索范围尚未全部核验。" : "本轮规划已完成。"}推荐和journeys是摘要;追问其他车次先读取 full_result 的已保存完整目录。compare:解释首选及最多两个有意义备选的时间/费用/便利性取舍后等待用户选择。prepare_checkout:用户已明确委托按规则选择时,按 interaction.recipe 选合格者并继续到官方确认页,不再次问是否下单;身份、review和最终付款边界仍然生效。乘车人身份信息不在此收集,后续购买走受保护 Checkout。`,
|
|
1286
|
-
next:
|
|
1287
|
-
? { command: railJourneySummary(journeys[0], se).select, reason: "用户在 compare 后选定推荐行程时执行;prepare_checkout 且规则命中时用 select_delegated" }
|
|
1288
|
-
: null,
|
|
1444
|
+
next: null,
|
|
1289
1445
|
...(queryResultsInteraction ? { interaction: queryResultsInteraction } : {}),
|
|
1290
1446
|
...(queryResultsCommunication ? { communication: queryResultsCommunication } : {}),
|
|
1291
1447
|
recovery: [],
|
|
@@ -1640,13 +1796,14 @@ function servicesNextEnvelope(model) {
|
|
|
1640
1796
|
quote_seat_unavailable: "所选席别当前无足够余票;告知用户并由其决定是否换方案。",
|
|
1641
1797
|
quote_refresh_expired: "实时报价过程超时;保留原选择,不把旧价当可付款价格。",
|
|
1642
1798
|
}[providerErrorCode ?? ""] : undefined;
|
|
1799
|
+
const failureCode = providerErrorCode ?? model.workflow?.error_code;
|
|
1643
1800
|
const failedInstruction = recovery
|
|
1644
1801
|
? quoteFailure
|
|
1645
1802
|
? quoteFailure
|
|
1646
1803
|
: failedStepGuidance
|
|
1647
|
-
? `执行在「${failedStepGuidance.meaning}
|
|
1804
|
+
? `执行在「${failedStepGuidance.meaning}」步失败${failureCode ? `(错误码:${failureCode})` : ""}:${failedStepGuidance.hint}。先核对原因和当前执行状态,不要盲目重放。`
|
|
1648
1805
|
: state === "failed"
|
|
1649
|
-
? `执行已失败${
|
|
1806
|
+
? `执行已失败${failureCode ? `(错误码:${failureCode})` : ""}且不可续用;核对服务端原因、订单与付款事实后再决定下一步。`
|
|
1650
1807
|
: "执行未完成,请按步骤错误处理;不要重建执行或重复调用。"
|
|
1651
1808
|
: undefined;
|
|
1652
1809
|
return {
|
|
@@ -1660,7 +1817,7 @@ function servicesNextEnvelope(model) {
|
|
|
1660
1817
|
...(failedStep ? { failed_step: failedStep, ...(failedStepGuidance ? { failed_step_meaning: failedStepGuidance.meaning } : {}) } : {}),
|
|
1661
1818
|
...(providerErrorCode ? { provider_error_code: providerErrorCode } : {}),
|
|
1662
1819
|
...(providerErrorCode && !quoteFailure ? { diagnostic_code: providerErrorCode } : {}),
|
|
1663
|
-
...(recovery ? { retryable:
|
|
1820
|
+
...(recovery ? { retryable: false } : {}),
|
|
1664
1821
|
},
|
|
1665
1822
|
instruction: failedInstruction
|
|
1666
1823
|
?? (state === "payment"
|
|
@@ -1685,11 +1842,8 @@ function servicesNextEnvelope(model) {
|
|
|
1685
1842
|
} : {}),
|
|
1686
1843
|
},
|
|
1687
1844
|
} : {}),
|
|
1688
|
-
recovery: recovery && state === "failed"
|
|
1689
|
-
? [
|
|
1690
|
-
{ command: `itpay services start ${execution.service_id} --json`, reason: "修正输入后重新发起(本执行已终止不能续用)" },
|
|
1691
|
-
...(guidance ? [{ command: "itpay docs show rail-booking --json", reason: "查看本服务输入字段契约与示例" }] : []),
|
|
1692
|
-
]
|
|
1845
|
+
recovery: recovery && state === "failed"
|
|
1846
|
+
? [{ command: `itpay services get ${id} --json`, reason: "核对本执行的错误、结果和付款事实" }]
|
|
1693
1847
|
: [],
|
|
1694
1848
|
};
|
|
1695
1849
|
}
|
|
@@ -1706,6 +1860,23 @@ function servicesNextEnvelope(model) {
|
|
|
1706
1860
|
}
|
|
1707
1861
|
const delivery = currentDelivery;
|
|
1708
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
|
+
}
|
|
1709
1880
|
const candidateSelection = model.allowed_actions?.find((action) => action.type === "select_candidate");
|
|
1710
1881
|
if (candidateSelection && currentItems.length > 0) {
|
|
1711
1882
|
const paidCapability = delivery?.capability_id
|
|
@@ -2141,10 +2312,7 @@ export async function runServicesRun(backend, config, serviceID, input, options
|
|
|
2141
2312
|
const paid = () => model.payment_bindings.some((binding) => binding.status === "payment_verified") || model.checkout_bindings.some((binding) => binding.status === "payment_verified");
|
|
2142
2313
|
// A planning projection can exist before any usable result or user action.
|
|
2143
2314
|
// Keep the existing bounded wait until there is something to hand over.
|
|
2144
|
-
|
|
2145
|
-
["complete", "paused", "failed", "cancelled", "expired"].includes(model.rail_planning.search?.expansion_status ?? "") ||
|
|
2146
|
-
model.rail_planning.available_actions?.some((action) => ["select_journey", "expand_search"].includes(action.type))));
|
|
2147
|
-
while ((["queued", "running", "delivery"].includes(model.workflow?.status ?? "") || (model.workflow?.status === "payment" && paid())) && !model.current_delivery && !planningDeliverable() && Date.now() < until) {
|
|
2315
|
+
while ((["queued", "running", "delivery"].includes(model.workflow?.status ?? "") || (model.workflow?.status === "payment" && paid())) && shouldWaitForServiceResult(model) && Date.now() < until) {
|
|
2148
2316
|
await sleep(options.pollIntervalMS ?? 1500);
|
|
2149
2317
|
model = await backend.getServiceExecution(id);
|
|
2150
2318
|
}
|
|
@@ -24,7 +24,7 @@ export function runSkillShow(name, options = {}) {
|
|
|
24
24
|
result: { skill: ITPAY_SKILL, content },
|
|
25
25
|
instruction: agentType
|
|
26
26
|
? agentType === "workbuddy"
|
|
27
|
-
? "完整读取并遵守 Skill
|
|
27
|
+
? "完整读取并遵守 Skill;按用户意图选择第一条命令。保持 workbuddy 和同一 CLI launcher,并为 ~/.itpay-v3 申请宿主允许的持久化权限;权限被拒时报告受阻操作,不删除 identity。"
|
|
28
28
|
: `完整读取并遵守 Skill;把自己视为服务用户的代理,先理解用户意图和解释用户权益,再选择购买、已购内容、订单或退款对应的第一条命令。当前 Agent Type 是 ${agentType},后续命令保持不变。`
|
|
29
29
|
: "完整读取并遵守 Skill;先如实选择当前运行环境对应的 Agent Type。",
|
|
30
30
|
next: agentType
|
package/dist/src/main.js
CHANGED
|
@@ -226,7 +226,7 @@ function reportCLIError(error, contract) {
|
|
|
226
226
|
? "临时网络故障;CLI 已仅对可安全重放的操作完成有限自动重试,但仍未获得完整响应。按 recovery 查询同一资源的权威状态;不要创建替代 Checkout、Execution、Payment 或 Refund。"
|
|
227
227
|
: "网络在完整响应前中断;当前写操作没有安全重放合同,因此 CLI 未自动重试。按 recovery 查询权威状态;不要原样重放或创建替代 Checkout、Execution、Payment 或 Refund。"
|
|
228
228
|
: backendOverrideError
|
|
229
|
-
? "移除 ITPAY_BACKEND_URL
|
|
229
|
+
? "移除 ITPAY_BACKEND_URL 使用正式环境,或按当前测试目标准确设置为 https://sandbox.itpay.ai 或 https://dev.itpay.ai。不要通过切换环境规避当前错误。"
|
|
230
230
|
: commandError?.instruction ?? authorizationInstruction ?? contract?.instruction ?? "检查命令参数后重试。",
|
|
231
231
|
next: null,
|
|
232
232
|
recovery: incompatible
|
|
@@ -1413,7 +1413,7 @@ services
|
|
|
1413
1413
|
.argument("<result_item_id>")
|
|
1414
1414
|
.option("--offset <offset>", "zero-based candidate offset", Number, 0)
|
|
1415
1415
|
.option("--cursor <cursor>", "opaque v2 page cursor (rcur_<offset>); takes precedence over --offset")
|
|
1416
|
-
.option("--limit <limit>", "page size (1-20)", Number,
|
|
1416
|
+
.option("--limit <limit>", "page size (1-20)", Number, 20)
|
|
1417
1417
|
.option("--json", "output JSON")
|
|
1418
1418
|
.action(async (serviceExecutionID, resultItemID, options) => {
|
|
1419
1419
|
const config = loadConfig();
|
|
@@ -1665,12 +1665,16 @@ services
|
|
|
1665
1665
|
.description("Show the next recommended agent action for a Service Execution")
|
|
1666
1666
|
.argument("<service_execution_id>")
|
|
1667
1667
|
.option("--since-snapshot <snapshot_id>", "delta read: suppress the journey payload when unchanged")
|
|
1668
|
+
.option("--timeout <seconds>", "wait up to 120 seconds for a useful result on this execution", (value) => Number(value))
|
|
1668
1669
|
.option("--json", "output JSON instead of terminal text")
|
|
1669
1670
|
.action(async (serviceExecutionID, options) => {
|
|
1670
1671
|
const config = loadConfig();
|
|
1671
1672
|
const backend = newBackendClient(config);
|
|
1672
1673
|
try {
|
|
1673
|
-
|
|
1674
|
+
if (options.timeout !== undefined && (!Number.isInteger(options.timeout) || options.timeout < 0 || options.timeout > 120)) {
|
|
1675
|
+
throw new Error("--timeout must be an integer from 0 to 120");
|
|
1676
|
+
}
|
|
1677
|
+
await runServicesNext(backend, serviceExecutionID, { jsonOutput: Boolean(options.json), sinceSnapshot: options.sinceSnapshot, timeoutSeconds: options.timeout });
|
|
1674
1678
|
}
|
|
1675
1679
|
catch (error) {
|
|
1676
1680
|
reportCLIError(error, {
|
|
@@ -24,9 +24,6 @@ export function buildAgentChatHandoff(plan) {
|
|
|
24
24
|
if (plan.orderItems?.length) {
|
|
25
25
|
lines.push(`项目:${plan.orderItems.map((item) => `${item.title} x${item.quantity}`).join("、")}`);
|
|
26
26
|
}
|
|
27
|
-
if (plan.afterActionCommand) {
|
|
28
|
-
lines.push("", `付款后查询:\`${plan.afterActionCommand}\``);
|
|
29
|
-
}
|
|
30
27
|
return {
|
|
31
28
|
type: "send_payment_handoff",
|
|
32
29
|
must_send_to_user: true,
|
|
@@ -200,9 +200,13 @@ async function accountAuth(action, baseURL, env, fetcher, backend) {
|
|
|
200
200
|
}
|
|
201
201
|
catch (error) {
|
|
202
202
|
if (error instanceof AuthRequestError && (error.httpStatus === 404 || error.httpStatus === 410)) {
|
|
203
|
-
|
|
203
|
+
rmSync(sellerAuthPath(baseURL, env, purpose), { force: true });
|
|
204
|
+
// The explicit login call may open one fresh session now. Status
|
|
205
|
+
// remains read-only and still reports the missing old session.
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
return authStatusUnknown(open.sessionID);
|
|
204
209
|
}
|
|
205
|
-
return authStatusUnknown(open.sessionID);
|
|
206
210
|
}
|
|
207
211
|
}
|
|
208
212
|
}
|
|
@@ -55,9 +55,12 @@ export class CartSession {
|
|
|
55
55
|
...(this.state.lastDisplayToken ? { lastDisplayToken: this.state.lastDisplayToken } : {}),
|
|
56
56
|
...(this.state.lastCheckoutURL ? { lastCheckoutURL: this.state.lastCheckoutURL } : {}),
|
|
57
57
|
};
|
|
58
|
+
const contents = JSON.stringify(toSave, null, 2);
|
|
59
|
+
if (existsSync(path) && readFileSync(path, "utf-8") === contents)
|
|
60
|
+
return;
|
|
58
61
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
59
62
|
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
60
|
-
writeFileSync(temporary,
|
|
63
|
+
writeFileSync(temporary, contents, { encoding: "utf-8", mode: 0o600 });
|
|
61
64
|
chmodSync(temporary, 0o600);
|
|
62
65
|
renameSync(temporary, path);
|
|
63
66
|
chmodSync(path, 0o600);
|
package/dist/src/state/config.js
CHANGED
|
@@ -15,7 +15,7 @@ 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.
|
|
18
|
+
export const CLI_VERSION = "2.1.22";
|
|
19
19
|
export const API_CONTRACT_REVISION = "sha256:2f9fbcdc5cd0630910abb52671304cc019e348e9a778017ec92ddc3513b3639f";
|
|
20
20
|
const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
|
|
21
21
|
const CART_SESSION_FILENAME = "cart.json";
|
|
@@ -45,6 +45,8 @@ export class TaskJournal {
|
|
|
45
45
|
observe(serviceExecutionID, stage, resumeCommand) {
|
|
46
46
|
const file = this.readFile();
|
|
47
47
|
const existing = file.tasks[serviceExecutionID];
|
|
48
|
+
if (existing && existing.stage === stage && (resumeCommand === undefined || existing.resume_command === resumeCommand))
|
|
49
|
+
return;
|
|
48
50
|
file.tasks[serviceExecutionID] = {
|
|
49
51
|
service_execution_id: serviceExecutionID,
|
|
50
52
|
...(existing?.service_id ? { service_id: existing.service_id } : {}),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "itp.agent_doc.v1",
|
|
3
3
|
"role": "buyer",
|
|
4
|
-
"product_scope": "itpay is the single public CLI entry point
|
|
4
|
+
"product_scope": "itpay is the single public CLI entry point for Buyer services and the existing itpay sell Seller workflow; $itpay is its user-facing Skill invocation.",
|
|
5
5
|
"topic": "cart-checkout",
|
|
6
6
|
"title": "Canonical Cart And Checkout Routing",
|
|
7
7
|
"purpose": "Use the canonical server Cart and route service-backed lines through Service Execution without hardcoded service logic.",
|
|
@@ -32,11 +32,12 @@
|
|
|
32
32
|
}
|
|
33
33
|
],
|
|
34
34
|
"agent_rules": [
|
|
35
|
-
"The Backend Cart is business truth;
|
|
35
|
+
"The Backend Cart is business truth; the current backend-isolated local cart state file only stores local recovery handles and explicit drafts.",
|
|
36
36
|
"Run the exact next.command returned by the CLI. Do not extract or invent a capability from a raw list.",
|
|
37
|
-
"
|
|
37
|
+
"Railway query and booking use distinct Service Executions for one purchase journey. Start another execution for an independent intent.",
|
|
38
38
|
"Ask for email only when the selected capability requires protected delivery; never invent contact data.",
|
|
39
|
-
"A lost Checkout response is retried with the same Cart and persisted idempotency operation."
|
|
39
|
+
"A lost Checkout response is retried with the same Cart and persisted idempotency operation.",
|
|
40
|
+
"A single railway service checkout normally follows its booking execution; Cart is used only when the returned flow calls for it."
|
|
40
41
|
],
|
|
41
42
|
"forbidden": [
|
|
42
43
|
"Do not call ordinary buy for a service-backed Cart line.",
|