@itpay/cli 2.1.17 → 2.1.19
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/checkout.js +109 -2
- package/dist/src/commands/guidance.js +27 -0
- package/dist/src/commands/order.js +8 -1
- package/dist/src/commands/presentation.js +153 -0
- package/dist/src/commands/services.js +314 -61
- package/dist/src/main.js +73 -17
- package/dist/src/render/browser.js +65 -0
- package/dist/src/state/account_auth.js +196 -49
- package/dist/src/state/client_context.js +70 -0
- package/dist/src/state/config.js +13 -2
- package/dist/src/state/device_authority.js +69 -13
- package/dist/src/state/task_journal.js +88 -0
- package/docs/agent/buyer/identity-and-sessions.json +9 -1
- package/docs/agent/buyer/install-and-setup.json +2 -2
- package/docs/agent/buyer/rail-booking.json +23 -10
- package/docs/agent/buyer/rail-fast-checkout.json +68 -0
- package/docs/cli-reference/commands/checkout.md +10 -1
- package/docs/reviews/agent-ux-handoff-2026-09-23/IMPLEMENTATION-EVIDENCE.md +174 -0
- package/package.json +1 -1
- package/skills/itpay/SKILL.md +109 -5
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { HttpError } from "../client/http.js";
|
|
2
2
|
import { HttpTransportError } from "../client/transport.js";
|
|
3
|
-
import {
|
|
3
|
+
import { DeviceLockBusyError, DeviceStateError } from "../state/device_authority.js";
|
|
4
|
+
import { operationID, taskJournalPath } from "../state/config.js";
|
|
5
|
+
import { TaskJournal } from "../state/task_journal.js";
|
|
4
6
|
import { validateContext } from "../state/client_context.js";
|
|
5
7
|
import { dispatchRender } from "../render/index.js";
|
|
6
8
|
import { ensureIdeImageAttach } from "../render/ide.js";
|
|
@@ -16,6 +18,20 @@ import { renderTerminalQR } from "../render/qr.js";
|
|
|
16
18
|
const RAIL_PROGRESSIVE_FEATURES = ["rail.progressive.v2"];
|
|
17
19
|
import { buildCheckoutQRPlan } from "./buy.js";
|
|
18
20
|
import { appendFeedbackPostmortemInstruction, CommandContractError, isTerminalServiceExecutionStatus, writeCommandEnvelope, } from "./guidance.js";
|
|
21
|
+
// AUTH04: record the pause point of each Service Execution so a later
|
|
22
|
+
// `itpay auth login` can resume the SAME task. Ids/stage/resume command only —
|
|
23
|
+
// no chat content, PII or credentials. Best-effort: journaling never breaks
|
|
24
|
+
// a command.
|
|
25
|
+
function journalTaskState(serviceExecutionID, envelope, env) {
|
|
26
|
+
try {
|
|
27
|
+
const journal = new TaskJournal(taskJournalPath(env));
|
|
28
|
+
const serviceID = typeof envelope.result?.service_id === "string" ? envelope.result.service_id : undefined;
|
|
29
|
+
journal.observe(serviceExecutionID, envelope.status, serviceID ? `itpay services run ${serviceID} --execution ${serviceExecutionID} --json` : undefined);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* local journal is advisory; never fail the envelope */
|
|
33
|
+
}
|
|
34
|
+
}
|
|
19
35
|
const serviceActionStatuses = new Set(["pending", "approved", "rejected", "expired", "cancelled"]);
|
|
20
36
|
const RAIL_SERVICE_GUIDANCE = {
|
|
21
37
|
"itpay-rail-exact": {
|
|
@@ -99,13 +115,29 @@ export async function runServicesStart(backend, serviceID, options = {}) {
|
|
|
99
115
|
});
|
|
100
116
|
if (response.workflow_entry) {
|
|
101
117
|
const guidance = railServiceGuidance(serviceID);
|
|
102
|
-
writeCommandEnvelope({
|
|
118
|
+
writeCommandEnvelope({
|
|
119
|
+
status: "input_required",
|
|
120
|
+
result: { service_execution_id: response.execution.service_execution_id, service_id: serviceID, input_schema: response.workflow_entry.input_schema, ...(guidance ? { guidance } : {}) },
|
|
121
|
+
instruction: guidance ? "按 result.guidance 逐项填写输入(when_to_use 说明本服务适用场景、input_fields 是必填契约、input_example 可直接照抄),然后继续同一服务执行。不要臆造字段名。" : "根据服务声明填写输入,然后继续同一服务执行。",
|
|
122
|
+
next: null,
|
|
123
|
+
interaction: {
|
|
124
|
+
schema_version: "itpay.interaction.v1",
|
|
125
|
+
stage: "input_required",
|
|
126
|
+
input_template: {
|
|
127
|
+
command: `itpay services run ${serviceID} --execution ${response.execution.service_execution_id} --input-json <file> --json`,
|
|
128
|
+
required_input: ["file"],
|
|
129
|
+
executable: false,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
recovery: []
|
|
133
|
+
}, { ...options });
|
|
103
134
|
return;
|
|
104
135
|
}
|
|
105
136
|
const capability = response.capabilities.find((item) => item.phase === response.execution.phase && !item.requires_payment);
|
|
106
137
|
const requiredInput = requiredInputFields(capability?.input_schema);
|
|
107
|
-
const
|
|
108
|
-
|
|
138
|
+
const needsInput = requiredInput.length > 0;
|
|
139
|
+
const command = capability && !needsInput
|
|
140
|
+
? `itpay services invoke ${response.execution.service_execution_id} --capability ${capability.capability_id} --json`
|
|
109
141
|
: `itpay services next ${response.execution.service_execution_id} --json`;
|
|
110
142
|
const capabilitySummary = capability ? {
|
|
111
143
|
capability_id: capability.capability_id,
|
|
@@ -126,8 +158,19 @@ export async function runServicesStart(backend, serviceID, options = {}) {
|
|
|
126
158
|
: "当前没有可直接调用的 capability;读取服务端下一步,不要猜测 capability。",
|
|
127
159
|
next: {
|
|
128
160
|
command,
|
|
129
|
-
reason: capability ? "执行当前允许的能力" : "读取服务端计算的下一步",
|
|
161
|
+
reason: capability && !needsInput ? "执行当前允许的能力" : "读取服务端计算的下一步",
|
|
130
162
|
},
|
|
163
|
+
...(capability && needsInput ? {
|
|
164
|
+
interaction: {
|
|
165
|
+
schema_version: "itpay.interaction.v1",
|
|
166
|
+
stage: "input_required",
|
|
167
|
+
input_template: {
|
|
168
|
+
command: `itpay services invoke ${response.execution.service_execution_id} --capability ${capability.capability_id}${requiredInput.map((field) => ` --input ${field}=<value>`).join("")} --json`,
|
|
169
|
+
required_input: requiredInput,
|
|
170
|
+
executable: false,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
} : {}),
|
|
131
174
|
recovery: [],
|
|
132
175
|
}, {
|
|
133
176
|
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
@@ -153,11 +196,19 @@ function locationConfirmationEnvelope(executionID, capabilityID, input, confirma
|
|
|
153
196
|
result: { service_execution_id: executionID, query: input, location_confirmation: confirmation },
|
|
154
197
|
instruction: "尚未查票。仅向用户确认 endpoints 中有歧义的地点,展示真实候选名称、地址和高德链接。不要自行选择候选、把区域改成车站或重复调用原查询。用户选择后保留原输入,带 location_confirmation 回到同一服务。没有候选时请用户补充已知城市或准确地名,再使用修正输入查询。",
|
|
155
198
|
next: null,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
199
|
+
...(confirmation.can_resume ? {
|
|
200
|
+
interaction: {
|
|
201
|
+
schema_version: "itpay.interaction.v1",
|
|
202
|
+
stage: "location_confirmation_required",
|
|
203
|
+
input_template: {
|
|
204
|
+
command: `itpay services invoke ${executionID} --capability ${capabilityID}${formatInputOptions({ ...input,
|
|
205
|
+
location_confirmation: { plan_id: confirmation.plan_id, token: confirmation.token, choices } })} --json`,
|
|
206
|
+
required_input: ["choices"],
|
|
207
|
+
executable: false,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
} : {}),
|
|
211
|
+
recovery: [],
|
|
161
212
|
};
|
|
162
213
|
}
|
|
163
214
|
function requiredInputFields(schema) {
|
|
@@ -314,18 +365,33 @@ function invokedEnvelope(response, requestedCapability, capabilities, input) {
|
|
|
314
365
|
}
|
|
315
366
|
}
|
|
316
367
|
else if (items.length > 0 && requestedCapability.requires_human_action) {
|
|
317
|
-
next =
|
|
318
|
-
command: `itpay services action ${response.execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
|
|
319
|
-
reason: "在当前 Execution 记录用户选择",
|
|
320
|
-
};
|
|
368
|
+
next = null;
|
|
321
369
|
}
|
|
322
370
|
else if (items.length === 0) {
|
|
323
371
|
next = null;
|
|
324
372
|
}
|
|
373
|
+
const interaction = items.length > 0 && requestedCapability.requires_human_action
|
|
374
|
+
? {
|
|
375
|
+
schema_version: "itpay.interaction.v1",
|
|
376
|
+
stage: "query_results_ready",
|
|
377
|
+
by_goal: {
|
|
378
|
+
compare: { action: "present_options", max_options: 3, stop_after_presentation: true },
|
|
379
|
+
prepare_checkout: {
|
|
380
|
+
action: "select_under_user_rules_then_prepare",
|
|
381
|
+
requires: ["current_delegation", "eligible_current_selection", "owner_permits_next_step"],
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
input_template: {
|
|
385
|
+
command: `itpay services action ${response.execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
|
|
386
|
+
required_input: ["rank"],
|
|
387
|
+
executable: false,
|
|
388
|
+
},
|
|
389
|
+
}
|
|
390
|
+
: undefined;
|
|
325
391
|
if (preview?.resolved_locations)
|
|
326
392
|
instruction += " 按 resolved_locations 说明实际解析地点;区域级位置仅是范围代表点,接驳时间不是从用户精确位置计算的。";
|
|
327
393
|
return {
|
|
328
|
-
value: { status, result: baseResult, instruction, next, recovery: [] },
|
|
394
|
+
value: { status, result: baseResult, instruction, next, recovery: [], ...(interaction ? { interaction } : {}) },
|
|
329
395
|
plainResult: serviceResultPlainLines(baseResult),
|
|
330
396
|
};
|
|
331
397
|
}
|
|
@@ -479,7 +545,33 @@ export async function runServicesAction(backend, serviceExecutionID, actionType,
|
|
|
479
545
|
request.result_item_id = resultItemID;
|
|
480
546
|
if (options.requiredBefore)
|
|
481
547
|
request.required_before = options.requiredBefore;
|
|
482
|
-
|
|
548
|
+
let response;
|
|
549
|
+
try {
|
|
550
|
+
response = await backend.recordServiceExecutionAction(serviceExecutionID, request);
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
if (error instanceof HttpError && ["booking_review_invalid", "booking_review_changed", "booking_review_locked"].includes(error.code)) {
|
|
554
|
+
throw new CommandContractError(error.code, error.message, error.code === "booking_review_invalid"
|
|
555
|
+
? "确认输入与当前草稿不符。重新读取同一booking的review模板,席别使用返回的代码,不填显示名或猜新值。若仅修正代码写法,且车次、实际席别、人数、价格及条款等已确认内容未变,可沿用用户已有明确确认后提交;草稿实质变化或缺少真实同意时,先请用户确认变化内容。"
|
|
556
|
+
: error.code === "booking_review_changed"
|
|
557
|
+
? "草稿版本已变化;重新读取同一booking的当前review与revision,经用户核对后再提交。"
|
|
558
|
+
: "当前购买已有付款尝试;读取同一booking及订单状态,不创建新订单或重做付款。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取同一booking的当前状态与review" }]);
|
|
559
|
+
}
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
if ("state" in response) {
|
|
563
|
+
const updated = await backend.getServiceExecution(serviceExecutionID);
|
|
564
|
+
const envelope = servicesNextEnvelope(updated);
|
|
565
|
+
envelope.result = {
|
|
566
|
+
...envelope.result,
|
|
567
|
+
planning_action: { action_id: response.action_id, state: response.state, replayed: response.replayed },
|
|
568
|
+
};
|
|
569
|
+
writeCommandEnvelope(envelope, {
|
|
570
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
571
|
+
...(options.output ? { output: options.output } : {}),
|
|
572
|
+
});
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
483
575
|
if (selection && actionType === "select_candidate" && response.status === "approved") {
|
|
484
576
|
const updated = await backend.getServiceExecution(serviceExecutionID);
|
|
485
577
|
const preferred = updated.allowed_actions?.[0];
|
|
@@ -727,6 +819,7 @@ export async function runServicesGet(backend, serviceExecutionID, options = {})
|
|
|
727
819
|
const lockedRefund = response.refunds.find((refund) => refund.access_locked);
|
|
728
820
|
const nextState = servicesNextEnvelope(response);
|
|
729
821
|
const result = {
|
|
822
|
+
...nextState.result,
|
|
730
823
|
service_execution_id: execution.service_execution_id,
|
|
731
824
|
service_id: execution.service_id,
|
|
732
825
|
status: execution.status,
|
|
@@ -744,13 +837,11 @@ export async function runServicesGet(backend, serviceExecutionID, options = {})
|
|
|
744
837
|
} : {}),
|
|
745
838
|
};
|
|
746
839
|
const envelope = {
|
|
747
|
-
status:
|
|
840
|
+
status: nextState.status,
|
|
748
841
|
result,
|
|
749
|
-
instruction:
|
|
750
|
-
? nextState.instruction
|
|
751
|
-
: "时间线仅用于解释和恢复;按当前首选动作继续,不要重放已完成步骤。",
|
|
842
|
+
instruction: nextState.instruction,
|
|
752
843
|
next: nextState.next ? { command: nextState.next.command, reason: "继续当前首选动作" } : null,
|
|
753
|
-
recovery:
|
|
844
|
+
recovery: nextState.recovery,
|
|
754
845
|
};
|
|
755
846
|
writeCommandEnvelope(envelope, {
|
|
756
847
|
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
@@ -767,6 +858,7 @@ export async function runServicesGet(backend, serviceExecutionID, options = {})
|
|
|
767
858
|
export async function runServicesNext(backend, serviceExecutionID, options = {}) {
|
|
768
859
|
const response = await backend.getServiceExecution(serviceExecutionID, options.sinceSnapshot ? { sinceSnapshot: options.sinceSnapshot } : {});
|
|
769
860
|
const envelope = servicesNextEnvelope(response);
|
|
861
|
+
journalTaskState(serviceExecutionID, envelope, options.env ?? process.env);
|
|
770
862
|
if (response.rail_booking)
|
|
771
863
|
envelope.result = { ...envelope.result, rail_booking: response.rail_booking };
|
|
772
864
|
writeCommandEnvelope(envelope, {
|
|
@@ -777,7 +869,7 @@ export async function runServicesNext(backend, serviceExecutionID, options = {})
|
|
|
777
869
|
}
|
|
778
870
|
export async function runServicesPage(backend, serviceExecutionID, resultItemID, options = {}) {
|
|
779
871
|
const offset = options.offset ?? 0;
|
|
780
|
-
const limit = options.limit ??
|
|
872
|
+
const limit = options.limit ?? 20;
|
|
781
873
|
if (!Number.isInteger(offset) || offset < 0) {
|
|
782
874
|
throw new CommandContractError("offset_invalid", "--offset must be a non-negative integer", "offset 必须是非负整数;本次未读取服务端分页。", [{ command: `itpay services page ${serviceExecutionID} ${resultItemID} --json`, reason: "从第一页重新读取" }]);
|
|
783
875
|
}
|
|
@@ -807,11 +899,11 @@ export async function runServicesPage(backend, serviceExecutionID, resultItemID,
|
|
|
807
899
|
next_offset: nextOffset,
|
|
808
900
|
page,
|
|
809
901
|
},
|
|
810
|
-
instruction: "
|
|
902
|
+
instruction: "读取的是已保存结果的同版本分页,不重新查询、不消耗额度。先读完所需页,再合并回答用户;不要逐页播报。",
|
|
811
903
|
next: nextOffset !== null
|
|
812
904
|
? { command: response.snapshot_id
|
|
813
|
-
? `itpay services page ${serviceExecutionID} ${resultItemID} --cursor rcur_${nextOffset} --json`
|
|
814
|
-
: `itpay services page ${serviceExecutionID} ${resultItemID} --offset ${nextOffset} --json`,
|
|
905
|
+
? `itpay services page ${serviceExecutionID} ${resultItemID} --cursor rcur_${nextOffset} --limit ${limit} --json`
|
|
906
|
+
: `itpay services page ${serviceExecutionID} ${resultItemID} --offset ${nextOffset} --limit ${limit} --json`,
|
|
815
907
|
reason: "读取同版本结果的下一页" }
|
|
816
908
|
: null,
|
|
817
909
|
recovery: offset > 0
|
|
@@ -892,10 +984,8 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
892
984
|
query_revision: committed.query_revision,
|
|
893
985
|
catalog,
|
|
894
986
|
},
|
|
895
|
-
instruction: "catalog
|
|
896
|
-
next:
|
|
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: "返回规划进展" },
|
|
987
|
+
instruction: "catalog 是本次查询的完整已保存目录。按用户问题筛选全部journeys并一次回答;只有需要单程细节时才读取journey。价格和余票供比较,购买前须实时报价。",
|
|
988
|
+
next: null,
|
|
899
989
|
recovery: [],
|
|
900
990
|
}, {
|
|
901
991
|
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
@@ -1046,12 +1136,34 @@ function railJourneySummary(card, serviceExecutionID) {
|
|
|
1046
1136
|
...(card.observed_at ? { observed_at: card.observed_at } : {}),
|
|
1047
1137
|
...(card.risk_notes?.length ? { risk_notes: card.risk_notes } : {}),
|
|
1048
1138
|
...(card.booking_support === "single_leg"
|
|
1049
|
-
? {
|
|
1139
|
+
? {
|
|
1140
|
+
select: `itpay services action ${serviceExecutionID} --action select_journey --actor-type human --status approved --input journey_id=${card.journey_id} --json`,
|
|
1141
|
+
// Delegated selection under explicit user rules is an agent action —
|
|
1142
|
+
// never record it as a human pick.
|
|
1143
|
+
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`,
|
|
1144
|
+
}
|
|
1050
1145
|
: {}),
|
|
1051
1146
|
...(card.booking_offer
|
|
1052
1147
|
? {
|
|
1053
1148
|
booking_offer: card.booking_offer,
|
|
1054
|
-
|
|
1149
|
+
booking_template: {
|
|
1150
|
+
command: `itpay services run ${card.booking_offer.service_id} --input-json <file> --json`,
|
|
1151
|
+
required_input: ["file"],
|
|
1152
|
+
executable: false,
|
|
1153
|
+
seat_choices: (card.representative_offer?.ticket_offers ?? []).map((offer) => ({
|
|
1154
|
+
seat_type: offer.seat_type,
|
|
1155
|
+
seat_name: offer.seat_name,
|
|
1156
|
+
})).filter((seat) => typeof seat.seat_type === "string"),
|
|
1157
|
+
input_example: {
|
|
1158
|
+
selection: {
|
|
1159
|
+
token: card.booking_offer.selection_token,
|
|
1160
|
+
...(typeof card.representative_offer?.ticket_offers?.[0]?.seat_type === "string"
|
|
1161
|
+
? { seat_type: card.representative_offer.ticket_offers[0].seat_type }
|
|
1162
|
+
: {}),
|
|
1163
|
+
},
|
|
1164
|
+
passengers: card.passengers ?? 1,
|
|
1165
|
+
},
|
|
1166
|
+
},
|
|
1055
1167
|
}
|
|
1056
1168
|
: {}),
|
|
1057
1169
|
};
|
|
@@ -1108,12 +1220,61 @@ function railPlanningEnvelope(model) {
|
|
|
1108
1220
|
...(plan.notices?.length ? { notices: plan.notices } : {}),
|
|
1109
1221
|
...(plan.recommendation ? { recommendation: railJourneySummary(plan.recommendation, se) } : {}),
|
|
1110
1222
|
...(cards.length ? { journeys: cards } : {}),
|
|
1223
|
+
...(plan.snapshot_id ? { full_result: { command: `itpay services read-result ${se} --snapshot ${plan.snapshot_id} --json`, meaning: "读取已保存的完整目录;推荐和journeys仅是摘要" } } : {}),
|
|
1111
1224
|
...(plan.available_actions?.length ? { available_actions: plan.available_actions } : {}),
|
|
1112
1225
|
};
|
|
1113
1226
|
const nextPoll = {
|
|
1114
1227
|
command: `itpay services next ${se}${plan.snapshot_id ? ` --since-snapshot ${plan.snapshot_id}` : ""} --json`,
|
|
1115
1228
|
reason: "读取同一规划的增量进展(不触发供应商调用)",
|
|
1116
1229
|
};
|
|
1230
|
+
// Two-goal interaction contract (spec 03 §2): the local Agent picks compare
|
|
1231
|
+
// or prepare_checkout from full context — the envelope teaches both branches
|
|
1232
|
+
// so a delegated purchase keeps moving and a comparison never buys.
|
|
1233
|
+
const bookable = journeys.find((card) => card.booking_support === "single_leg" && card.booking_offer);
|
|
1234
|
+
const queryResultsInteraction = journeys.length > 0
|
|
1235
|
+
? {
|
|
1236
|
+
schema_version: "itpay.interaction.v1",
|
|
1237
|
+
stage: "query_results_ready",
|
|
1238
|
+
by_goal: {
|
|
1239
|
+
compare: { action: "present_options", max_options: 3, stop_after_presentation: true },
|
|
1240
|
+
prepare_checkout: {
|
|
1241
|
+
action: "select_under_user_rules_then_prepare",
|
|
1242
|
+
recipe: "rail.selected-to-checkout.v1",
|
|
1243
|
+
requires: ["current_delegation", "eligible_current_selection", "owner_permits_next_step"],
|
|
1244
|
+
...(bookable
|
|
1245
|
+
? {
|
|
1246
|
+
steps: [
|
|
1247
|
+
"按用户明确规则在 journeys 中选择合格者(卡片 select_delegated 提交 agent 委托选择;人类亲自选择用 select)",
|
|
1248
|
+
`用所选卡片的 booking_template 写 owner-only 临时输入(selection token + 席别 + 人数,不含身份信息),运行 itpay services run itpay-rail-booking --input-json <file> --json`,
|
|
1249
|
+
"owner 要求聊天 review 时一次合并补齐必要字段,真实确认后提交原 workflow:confirm_booking;不得伪造 accept",
|
|
1250
|
+
`确认后恢复同一购买执行:itpay services run itpay-rail-booking --execution <实际ID> --json,沿用有界等待与受保护 Checkout`,
|
|
1251
|
+
],
|
|
1252
|
+
}
|
|
1253
|
+
: { unavailable: "当前结果无可整段购买映射;分别说明各段风险,不得合成原子联程。" }),
|
|
1254
|
+
},
|
|
1255
|
+
},
|
|
1256
|
+
}
|
|
1257
|
+
: undefined;
|
|
1258
|
+
const queryResultsCommunication = journeys.length > 0
|
|
1259
|
+
? {
|
|
1260
|
+
schema_version: "itpay.communication.v1",
|
|
1261
|
+
status_line: expansion === "paused"
|
|
1262
|
+
? "首批结果已就绪,扩展搜索已暂停等待用户决定"
|
|
1263
|
+
: "查询结果就绪",
|
|
1264
|
+
recommended_reason: plan.recommendation?.explanation?.[0],
|
|
1265
|
+
human_steps: [
|
|
1266
|
+
"compare:比较首选与最多两个备选后告知取舍",
|
|
1267
|
+
"prepare_checkout:委托仍在时确认所选,继续官方确认页",
|
|
1268
|
+
"姓名、证件、手机号只在受保护 Checkout 页填写",
|
|
1269
|
+
],
|
|
1270
|
+
next_expectation: "compare:用户选定其一;prepare_checkout:委托仍在时继续到官方确认入口,人完成review与付款",
|
|
1271
|
+
must_convey: [
|
|
1272
|
+
"推荐与备选的时间/费用/便利性取舍",
|
|
1273
|
+
"金额为查询时参考价,下单前须核验",
|
|
1274
|
+
"姓名、证件、手机号只在受保护网页填写",
|
|
1275
|
+
],
|
|
1276
|
+
}
|
|
1277
|
+
: undefined;
|
|
1117
1278
|
switch (expansion) {
|
|
1118
1279
|
case "complete":
|
|
1119
1280
|
return {
|
|
@@ -1121,10 +1282,12 @@ function railPlanningEnvelope(model) {
|
|
|
1121
1282
|
result,
|
|
1122
1283
|
// §7.3: the lead line carries real combination counts bucketed by
|
|
1123
1284
|
// verified transfer count — seat rows never inflate the journey count.
|
|
1124
|
-
instruction: `${journeyMix ? `本次共${journeyMix}(席别不重复计数)。` : ""}${plan.search?.reason === "budget_exhausted" ? "本次查询已达到上限,以下是已查到的方案,搜索范围尚未全部核验。" : "本轮规划已完成。"}
|
|
1285
|
+
instruction: `${journeyMix ? `本次共${journeyMix}(席别不重复计数)。` : ""}${plan.search?.reason === "budget_exhausted" ? "本次查询已达到上限,以下是已查到的方案,搜索范围尚未全部核验。" : "本轮规划已完成。"}推荐和journeys是摘要;追问其他车次先读取 full_result 的已保存完整目录。compare:解释首选及最多两个有意义备选的时间/费用/便利性取舍后等待用户选择。prepare_checkout:用户已明确委托按规则选择时,按 interaction.recipe 选合格者并继续到官方确认页,不再次问是否下单;身份、review和最终付款边界仍然生效。乘车人身份信息不在此收集,后续购买走受保护 Checkout。`,
|
|
1125
1286
|
next: journeys[0]?.booking_support === "single_leg"
|
|
1126
|
-
? { command: railJourneySummary(journeys[0], se).select, reason: "
|
|
1287
|
+
? { command: railJourneySummary(journeys[0], se).select, reason: "用户在 compare 后选定推荐行程时执行;prepare_checkout 且规则命中时用 select_delegated" }
|
|
1127
1288
|
: null,
|
|
1289
|
+
...(queryResultsInteraction ? { interaction: queryResultsInteraction } : {}),
|
|
1290
|
+
...(queryResultsCommunication ? { communication: queryResultsCommunication } : {}),
|
|
1128
1291
|
recovery: [],
|
|
1129
1292
|
};
|
|
1130
1293
|
case "paused": {
|
|
@@ -1168,8 +1331,10 @@ function railPlanningEnvelope(model) {
|
|
|
1168
1331
|
return {
|
|
1169
1332
|
status: "awaiting_input",
|
|
1170
1333
|
result,
|
|
1171
|
-
instruction: `${modelDegradedHint}${failedPairsHint}${directHint}${transferHint}${partialPause}规划已发布首批结果并暂停扩展:展示现有卡片与 available_actions,等待用户意图(${expandHint})。ready 后 next
|
|
1334
|
+
instruction: `${modelDegradedHint}${failedPairsHint}${directHint}${transferHint}${partialPause}规划已发布首批结果并暂停扩展:展示现有卡片与 available_actions,等待用户意图(${expandHint})。ready 后 next 为空表示向用户汇报并等待,不是永远没有更多。compare:解释首选及最多两个备选后等待;prepare_checkout:委托仍在且选项合格时按 interaction.recipe 继续到官方确认入口,不再次问是否下单。`,
|
|
1172
1335
|
next: null,
|
|
1336
|
+
...(queryResultsInteraction ? { interaction: queryResultsInteraction } : {}),
|
|
1337
|
+
...(queryResultsCommunication ? { communication: queryResultsCommunication } : {}),
|
|
1173
1338
|
recovery: [],
|
|
1174
1339
|
};
|
|
1175
1340
|
}
|
|
@@ -1191,9 +1356,11 @@ function railPlanningEnvelope(model) {
|
|
|
1191
1356
|
return {
|
|
1192
1357
|
status: expansion,
|
|
1193
1358
|
result,
|
|
1194
|
-
instruction: `${modelDegradedHint}${failedPairsHint}${savedCatalog}${partial}
|
|
1359
|
+
instruction: `${modelDegradedHint}${failedPairsHint}${savedCatalog}${partial}${expansion === "expired" ? "规划已过期,已保存目录仍可读取并回答历史结果追问;购买须重新核验实时余票与报价。" : "本次规划已结束,展示已有结果;需要新的供应商观测时再明确发起查询。"}`,
|
|
1195
1360
|
next: null,
|
|
1196
|
-
recovery:
|
|
1361
|
+
recovery: plan.snapshot_id
|
|
1362
|
+
? [{ command: `itpay services read-result ${se} --snapshot ${plan.snapshot_id} --json`, reason: "读取同一任务已保存的完整目录" }]
|
|
1363
|
+
: [{ command: `itpay services next ${se} --json`, reason: "读取同一任务当前状态" }],
|
|
1197
1364
|
};
|
|
1198
1365
|
}
|
|
1199
1366
|
default: {
|
|
@@ -1216,6 +1383,17 @@ function railPlanningEnvelope(model) {
|
|
|
1216
1383
|
result,
|
|
1217
1384
|
instruction: `${modelDegradedHint}${manualExpansion}${transferHint}${failedPairsHint}规划进行中:已提交的卡片可先向用户展示比较;稍后按 next 增量轮询同一执行(since-snapshot 命中时负载会被压缩,但阶段与计数仍返回最新值)。不要重新发起查询。`,
|
|
1218
1385
|
next: nextPoll,
|
|
1386
|
+
communication: {
|
|
1387
|
+
schema_version: "itpay.communication.v1",
|
|
1388
|
+
status_line: transferSearching && !manualExpansion
|
|
1389
|
+
? "直达范围暂未找到可用方案,中转组合仍在搜索"
|
|
1390
|
+
: "规划进行中,已确认的方案可先向用户展示",
|
|
1391
|
+
next_expectation: "按 next 增量轮询同一执行;不重新发起查询",
|
|
1392
|
+
must_convey: [
|
|
1393
|
+
"当前是进行中的部分结果,不代表最终答复",
|
|
1394
|
+
...(failedPairsHint ? ["部分站对尚未查明,不能说'没有车'"] : []),
|
|
1395
|
+
],
|
|
1396
|
+
},
|
|
1219
1397
|
recovery: [],
|
|
1220
1398
|
};
|
|
1221
1399
|
}
|
|
@@ -1373,18 +1551,48 @@ function servicesNextEnvelope(model) {
|
|
|
1373
1551
|
result: { service_execution_id: id, service_id: execution.service_id, human_action: action,
|
|
1374
1552
|
required_fields: requiredFields, sides },
|
|
1375
1553
|
instruction: "向用户展示 sides 中 status=needs_confirmation 一端的候选(名称+坐标),请用户选定后按 required_fields 逐项各传一个 --input:名称取候选 name,坐标取候选 location 的 [lng,lat];已 resolved 的一端把其 resolved_place 原样填入。继续同一执行,不重新查询;这一步不是购买确认。",
|
|
1376
|
-
next:
|
|
1554
|
+
next: null,
|
|
1555
|
+
interaction: {
|
|
1556
|
+
schema_version: "itpay.interaction.v1",
|
|
1557
|
+
stage: "location_confirmation_required",
|
|
1558
|
+
input_template: {
|
|
1559
|
+
command: `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved${requiredFields.map((field) => ` --input ${field}=<值>`).join("")} --json`,
|
|
1560
|
+
required_input: requiredFields,
|
|
1561
|
+
executable: false,
|
|
1562
|
+
},
|
|
1563
|
+
},
|
|
1377
1564
|
recovery: [],
|
|
1378
1565
|
};
|
|
1379
1566
|
}
|
|
1380
1567
|
const review = action.context?.review;
|
|
1381
1568
|
if (review) {
|
|
1569
|
+
// W4: when the owner projects an official web review entry, present it
|
|
1570
|
+
// and wait for the human there; otherwise keep the one consolidated
|
|
1571
|
+
// chat review — never fabricate an accepted review.
|
|
1572
|
+
const reviewURL = [review.web_review_url, review.entry_url, review.review_url]
|
|
1573
|
+
.find((value) => typeof value === "string" && /^https:\/\//.test(value));
|
|
1574
|
+
const requirementsRemaining = Array.isArray(review.requirements_remaining)
|
|
1575
|
+
? review.requirements_remaining
|
|
1576
|
+
: undefined;
|
|
1382
1577
|
return {
|
|
1383
1578
|
status: "booking_review_required",
|
|
1384
1579
|
result: { service_execution_id: id, service_id: execution.service_id, human_action: action,
|
|
1385
|
-
required_fields: requiredFields
|
|
1386
|
-
|
|
1387
|
-
|
|
1580
|
+
required_fields: requiredFields,
|
|
1581
|
+
...(requirementsRemaining ? { requirements_remaining: requirementsRemaining } : {}) },
|
|
1582
|
+
...(reviewURL ? { handoff: { url: reviewURL, kind: "booking_review" } } : {}),
|
|
1583
|
+
instruction: reviewURL
|
|
1584
|
+
? "已在 handoff.url 提供官方行程确认页,请用户在该页核对行程、席别与座位偏好并确认;页面打开请求不代表用户已确认。该页失效或用户更愿在对话中确认时,仍可按下述 input_template 一次合并提交。乘客姓名/证件等身份信息不在此提交,仍走受保护 Checkout 页。"
|
|
1585
|
+
: "向用户展示 context.review 中的行程(legs)与可选席别/座位偏好词表(seat_options),并明确告知:座位偏好仅为请求、不保证分配(对应 notice_version 文案须经用户同意)。这是购买前的合并确认:把乘客人数、席别与每位乘客的座位偏好一次问清,然后按 required_fields 组装完整 JSON 对象,经 --input-json <file> 提交——draft_revision 必须等于服务端当前值,被拒绝(booking_review_changed)时重新读取后重试。未得真实确认不得提交,不能暗设 accept_non_guaranteed。乘客姓名/证件等身份信息不在此提交,仍走受保护 Checkout 页。",
|
|
1586
|
+
next: null,
|
|
1587
|
+
interaction: {
|
|
1588
|
+
schema_version: "itpay.interaction.v1",
|
|
1589
|
+
stage: "booking_review_required",
|
|
1590
|
+
input_template: {
|
|
1591
|
+
command: `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved --input-json <file> --json`,
|
|
1592
|
+
required_input: requiredFields.length > 0 ? requiredFields : ["file"],
|
|
1593
|
+
executable: false,
|
|
1594
|
+
},
|
|
1595
|
+
},
|
|
1388
1596
|
recovery: [{ command: `itpay services next ${id} --json`, reason: "重新读取当前 draft_revision 后重试" }],
|
|
1389
1597
|
};
|
|
1390
1598
|
}
|
|
@@ -1392,7 +1600,16 @@ function servicesNextEnvelope(model) {
|
|
|
1392
1600
|
status: "confirmation_required",
|
|
1393
1601
|
result: { service_execution_id: id, service_id: execution.service_id, human_action: action },
|
|
1394
1602
|
instruction: "请展示待确认的内容,请用户明确确认后,按 input_schema 填写 --input 字段。继续同一执行,不重新查询;这一步不是购买确认。",
|
|
1395
|
-
next:
|
|
1603
|
+
next: null,
|
|
1604
|
+
interaction: {
|
|
1605
|
+
schema_version: "itpay.interaction.v1",
|
|
1606
|
+
stage: "confirmation_required",
|
|
1607
|
+
input_template: {
|
|
1608
|
+
command: `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved${requiredFields.map((field) => ` --input ${field}=<值>`).join("")} --json`,
|
|
1609
|
+
required_input: requiredFields,
|
|
1610
|
+
executable: false,
|
|
1611
|
+
},
|
|
1612
|
+
},
|
|
1396
1613
|
recovery: [],
|
|
1397
1614
|
};
|
|
1398
1615
|
}
|
|
@@ -1400,22 +1617,37 @@ function servicesNextEnvelope(model) {
|
|
|
1400
1617
|
let command = `itpay services next ${id} --json`;
|
|
1401
1618
|
if (state === "payment")
|
|
1402
1619
|
command = `itpay services checkout ${id} --json`;
|
|
1403
|
-
|
|
1404
|
-
|
|
1620
|
+
// input_required: the resume command needs a real file path — expose it as
|
|
1621
|
+
// a non-executable input_template, not a fake `next.command`.
|
|
1622
|
+
const inputTemplate = state === "input_required"
|
|
1623
|
+
? {
|
|
1624
|
+
command: `itpay services run ${execution.service_id} --execution ${id} --input-json <file> --json`,
|
|
1625
|
+
required_input: ["file"],
|
|
1626
|
+
executable: false,
|
|
1627
|
+
}
|
|
1628
|
+
: undefined;
|
|
1405
1629
|
const guidance = railServiceGuidance(execution.service_id);
|
|
1406
1630
|
const failedStep = recovery ? failedWorkflowStep(model.workflow?.steps, model.workflow?.error_code) : undefined;
|
|
1407
1631
|
const failedStepGuidance = failedStep ? WORKFLOW_STEP_GUIDANCE[failedStep] : undefined;
|
|
1408
1632
|
const failedInvocation = recovery
|
|
1409
1633
|
? [...model.provider_invocations].reverse().find((item) => typeof item.status === "string" && item.status.startsWith("failed"))
|
|
1410
1634
|
: undefined;
|
|
1411
|
-
const errorDetail = typeof failedInvocation?.error_message === "string" && failedInvocation.error_message ? failedInvocation.error_message : undefined;
|
|
1412
1635
|
const providerErrorCode = typeof failedInvocation?.error_code === "string" && failedInvocation.error_code ? failedInvocation.error_code : undefined;
|
|
1636
|
+
const quoteFailure = execution.service_id === "itpay-rail-booking" ? {
|
|
1637
|
+
quote_source_unavailable: "实时车票数据暂不可用;保留原选择,稍后按用户意愿重新核验,不创建替代订单。",
|
|
1638
|
+
quote_train_changed: "所选车次的时刻或运行信息已变化;向用户说明并征询是否重新查票。",
|
|
1639
|
+
quote_seat_changed: "所选席别已变化;向用户说明并征询新的席别选择。",
|
|
1640
|
+
quote_seat_unavailable: "所选席别当前无足够余票;告知用户并由其决定是否换方案。",
|
|
1641
|
+
quote_refresh_expired: "实时报价过程超时;保留原选择,不把旧价当可付款价格。",
|
|
1642
|
+
}[providerErrorCode ?? ""] : undefined;
|
|
1413
1643
|
const failedInstruction = recovery
|
|
1414
|
-
?
|
|
1415
|
-
?
|
|
1416
|
-
:
|
|
1417
|
-
?
|
|
1418
|
-
: "
|
|
1644
|
+
? quoteFailure
|
|
1645
|
+
? quoteFailure
|
|
1646
|
+
: failedStepGuidance
|
|
1647
|
+
? `执行在「${failedStepGuidance.meaning}」步失败:${failedStepGuidance.hint}。本执行已终止不能续用;先核对原因,不要盲目重放。`
|
|
1648
|
+
: state === "failed"
|
|
1649
|
+
? `执行已失败${failedStep ? `(失败步骤:${failedStep})` : ""}且不可续用;修正输入后用同一服务新建执行重试。`
|
|
1650
|
+
: "执行未完成,请按步骤错误处理;不要重建执行或重复调用。"
|
|
1419
1651
|
: undefined;
|
|
1420
1652
|
return {
|
|
1421
1653
|
status: state,
|
|
@@ -1427,7 +1659,7 @@ function servicesNextEnvelope(model) {
|
|
|
1427
1659
|
...(guidance && state === "input_required" ? { guidance } : {}),
|
|
1428
1660
|
...(failedStep ? { failed_step: failedStep, ...(failedStepGuidance ? { failed_step_meaning: failedStepGuidance.meaning } : {}) } : {}),
|
|
1429
1661
|
...(providerErrorCode ? { provider_error_code: providerErrorCode } : {}),
|
|
1430
|
-
...(
|
|
1662
|
+
...(providerErrorCode && !quoteFailure ? { diagnostic_code: providerErrorCode } : {}),
|
|
1431
1663
|
...(recovery ? { retryable: state === "failed" } : {}),
|
|
1432
1664
|
},
|
|
1433
1665
|
instruction: failedInstruction
|
|
@@ -1439,14 +1671,26 @@ function servicesNextEnvelope(model) {
|
|
|
1439
1671
|
? "按 result.guidance 的字段契约填写输入后继续同一服务执行;不要臆造字段名。"
|
|
1440
1672
|
: "继续读取同一执行;缺少输入时按服务声明补齐。"),
|
|
1441
1673
|
next: recovery ? null : { command, reason: "继续当前流程" },
|
|
1442
|
-
|
|
1674
|
+
...(inputTemplate || (state === "payment" && model.workflow?.human_action?.context?.review) ? {
|
|
1675
|
+
interaction: {
|
|
1676
|
+
schema_version: "itpay.interaction.v1",
|
|
1677
|
+
stage: inputTemplate ? "input_required" : "payment",
|
|
1678
|
+
...(inputTemplate ? { input_template: inputTemplate } : {}),
|
|
1679
|
+
...(state === "payment" && model.workflow?.human_action?.context?.review ? {
|
|
1680
|
+
revise_template: {
|
|
1681
|
+
command: `itpay services action ${id} --action ${model.workflow.human_action.action_type} --actor-type human --status approved --input-json <file> --json`,
|
|
1682
|
+
required_input: ["file"],
|
|
1683
|
+
executable: false,
|
|
1684
|
+
},
|
|
1685
|
+
} : {}),
|
|
1686
|
+
},
|
|
1687
|
+
} : {}),
|
|
1688
|
+
recovery: recovery && state === "failed" && !quoteFailure
|
|
1443
1689
|
? [
|
|
1444
1690
|
{ command: `itpay services start ${execution.service_id} --json`, reason: "修正输入后重新发起(本执行已终止不能续用)" },
|
|
1445
1691
|
...(guidance ? [{ command: "itpay docs show rail-booking --json", reason: "查看本服务输入字段契约与示例" }] : []),
|
|
1446
1692
|
]
|
|
1447
|
-
:
|
|
1448
|
-
? [{ command: `itpay services action ${id} --action ${model.workflow.human_action.action_type} --actor-type human --status approved --input-json <file> --json`, reason: "付款前修订席别/座位偏好(draft_revision 以服务端为准)" }]
|
|
1449
|
-
: [],
|
|
1693
|
+
: [],
|
|
1450
1694
|
};
|
|
1451
1695
|
}
|
|
1452
1696
|
const terminalEnvelope = terminalExecutionEnvelope(model);
|
|
@@ -1801,6 +2045,12 @@ function buildServicesCheckoutEnvelope(response, checkoutURL, plan, agentType, t
|
|
|
1801
2045
|
capability_id: checkoutCapabilityID(response),
|
|
1802
2046
|
locked_input: response.locked_input,
|
|
1803
2047
|
amount,
|
|
2048
|
+
...(checkout.checkout.expires_at ? {
|
|
2049
|
+
// The shared server-owned payable deadline D: refreshing the page or
|
|
2050
|
+
// re-creating a checkout can never extend it; past it the checkout
|
|
2051
|
+
// and pending order are terminally cancelled.
|
|
2052
|
+
payment_deadline_at: checkout.checkout.expires_at,
|
|
2053
|
+
} : {}),
|
|
1804
2054
|
},
|
|
1805
2055
|
handoff: presentationHandoff.handoff,
|
|
1806
2056
|
instruction: presentationHandoff.instruction,
|
|
@@ -1889,12 +2139,12 @@ export async function runServicesRun(backend, config, serviceID, input, options
|
|
|
1889
2139
|
const until = Date.now() + (options.timeoutSeconds ?? 120) * 1000;
|
|
1890
2140
|
const sleep = options.sleep ?? ((milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds)));
|
|
1891
2141
|
const paid = () => model.payment_bindings.some((binding) => binding.status === "payment_verified") || model.checkout_bindings.some((binding) => binding.status === "payment_verified");
|
|
1892
|
-
//
|
|
1893
|
-
//
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
while ((["queued", "running", "delivery"].includes(model.workflow?.status ?? "") || (model.workflow?.status === "payment" && paid())) && !model.current_delivery && !
|
|
2142
|
+
// A planning projection can exist before any usable result or user action.
|
|
2143
|
+
// Keep the existing bounded wait until there is something to hand over.
|
|
2144
|
+
const planningDeliverable = () => Boolean(model.rail_planning && (model.rail_planning.recommendation || model.rail_planning.alternatives?.length ||
|
|
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) {
|
|
1898
2148
|
await sleep(options.pollIntervalMS ?? 1500);
|
|
1899
2149
|
model = await backend.getServiceExecution(id);
|
|
1900
2150
|
}
|
|
@@ -1913,7 +2163,10 @@ export async function runServicesRun(backend, config, serviceID, input, options
|
|
|
1913
2163
|
await runServicesNext(backend, id, options);
|
|
1914
2164
|
}
|
|
1915
2165
|
catch (cause) {
|
|
1916
|
-
if (cause instanceof
|
|
2166
|
+
if (cause instanceof HttpError && cause.code === "rail_seat_code_invalid" && id) {
|
|
2167
|
+
throw new CommandContractError(cause.code, cause.message, "席别必须使用原查询 booking_template.seat_choices 中与名称对应的代码,不能填“二等座”等显示名。用原 selection token 与真实人数修正输入,继续同一booking execution;若只是代码写法修正且车次、席别名称、人数等购买条件未变,沿用用户已给的确认,不再重复询问。", [{ command: `itpay services next ${id} --json`, reason: "读取同一booking当前状态后修正输入" }]);
|
|
2168
|
+
}
|
|
2169
|
+
if (cause instanceof CommandContractError || cause instanceof HttpError || cause instanceof DeviceLockBusyError || cause instanceof DeviceStateError)
|
|
1917
2170
|
throw cause;
|
|
1918
2171
|
if (cause instanceof HttpTransportError && !id) {
|
|
1919
2172
|
throw new CommandContractError("workflow_start_outcome_unknown", cause.message, "创建服务执行时没有收到完整响应。先查询当前身份可见的执行;不要直接重跑并创建替代执行。", [{ command: "itpay services list --json", reason: "查找可能已经创建的服务执行" }]);
|