@itpay/cli 2.1.17 → 2.1.18
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 +235 -32
- package/dist/src/main.js +26 -1
- 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/task_journal.js +88 -0
- package/docs/agent/buyer/identity-and-sessions.json +9 -1
- package/docs/agent/buyer/rail-booking.json +16 -2
- package/docs/agent/buyer/rail-fast-checkout.json +67 -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 +55 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { HttpError } from "../client/http.js";
|
|
2
2
|
import { HttpTransportError } from "../client/transport.js";
|
|
3
|
-
import { operationID } from "../state/config.js";
|
|
3
|
+
import { operationID, taskJournalPath } from "../state/config.js";
|
|
4
|
+
import { TaskJournal } from "../state/task_journal.js";
|
|
4
5
|
import { validateContext } from "../state/client_context.js";
|
|
5
6
|
import { dispatchRender } from "../render/index.js";
|
|
6
7
|
import { ensureIdeImageAttach } from "../render/ide.js";
|
|
@@ -16,6 +17,20 @@ import { renderTerminalQR } from "../render/qr.js";
|
|
|
16
17
|
const RAIL_PROGRESSIVE_FEATURES = ["rail.progressive.v2"];
|
|
17
18
|
import { buildCheckoutQRPlan } from "./buy.js";
|
|
18
19
|
import { appendFeedbackPostmortemInstruction, CommandContractError, isTerminalServiceExecutionStatus, writeCommandEnvelope, } from "./guidance.js";
|
|
20
|
+
// AUTH04: record the pause point of each Service Execution so a later
|
|
21
|
+
// `itpay auth login` can resume the SAME task. Ids/stage/resume command only —
|
|
22
|
+
// no chat content, PII or credentials. Best-effort: journaling never breaks
|
|
23
|
+
// a command.
|
|
24
|
+
function journalTaskState(serviceExecutionID, envelope, env) {
|
|
25
|
+
try {
|
|
26
|
+
const journal = new TaskJournal(taskJournalPath(env));
|
|
27
|
+
const serviceID = typeof envelope.result?.service_id === "string" ? envelope.result.service_id : undefined;
|
|
28
|
+
journal.observe(serviceExecutionID, envelope.status, serviceID ? `itpay services run ${serviceID} --execution ${serviceExecutionID} --json` : undefined);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* local journal is advisory; never fail the envelope */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
19
34
|
const serviceActionStatuses = new Set(["pending", "approved", "rejected", "expired", "cancelled"]);
|
|
20
35
|
const RAIL_SERVICE_GUIDANCE = {
|
|
21
36
|
"itpay-rail-exact": {
|
|
@@ -99,13 +114,29 @@ export async function runServicesStart(backend, serviceID, options = {}) {
|
|
|
99
114
|
});
|
|
100
115
|
if (response.workflow_entry) {
|
|
101
116
|
const guidance = railServiceGuidance(serviceID);
|
|
102
|
-
writeCommandEnvelope({
|
|
117
|
+
writeCommandEnvelope({
|
|
118
|
+
status: "input_required",
|
|
119
|
+
result: { service_execution_id: response.execution.service_execution_id, service_id: serviceID, input_schema: response.workflow_entry.input_schema, ...(guidance ? { guidance } : {}) },
|
|
120
|
+
instruction: guidance ? "按 result.guidance 逐项填写输入(when_to_use 说明本服务适用场景、input_fields 是必填契约、input_example 可直接照抄),然后继续同一服务执行。不要臆造字段名。" : "根据服务声明填写输入,然后继续同一服务执行。",
|
|
121
|
+
next: null,
|
|
122
|
+
interaction: {
|
|
123
|
+
schema_version: "itpay.interaction.v1",
|
|
124
|
+
stage: "input_required",
|
|
125
|
+
input_template: {
|
|
126
|
+
command: `itpay services run ${serviceID} --execution ${response.execution.service_execution_id} --input-json <file> --json`,
|
|
127
|
+
required_input: ["file"],
|
|
128
|
+
executable: false,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
recovery: []
|
|
132
|
+
}, { ...options });
|
|
103
133
|
return;
|
|
104
134
|
}
|
|
105
135
|
const capability = response.capabilities.find((item) => item.phase === response.execution.phase && !item.requires_payment);
|
|
106
136
|
const requiredInput = requiredInputFields(capability?.input_schema);
|
|
107
|
-
const
|
|
108
|
-
|
|
137
|
+
const needsInput = requiredInput.length > 0;
|
|
138
|
+
const command = capability && !needsInput
|
|
139
|
+
? `itpay services invoke ${response.execution.service_execution_id} --capability ${capability.capability_id} --json`
|
|
109
140
|
: `itpay services next ${response.execution.service_execution_id} --json`;
|
|
110
141
|
const capabilitySummary = capability ? {
|
|
111
142
|
capability_id: capability.capability_id,
|
|
@@ -126,8 +157,19 @@ export async function runServicesStart(backend, serviceID, options = {}) {
|
|
|
126
157
|
: "当前没有可直接调用的 capability;读取服务端下一步,不要猜测 capability。",
|
|
127
158
|
next: {
|
|
128
159
|
command,
|
|
129
|
-
reason: capability ? "执行当前允许的能力" : "读取服务端计算的下一步",
|
|
160
|
+
reason: capability && !needsInput ? "执行当前允许的能力" : "读取服务端计算的下一步",
|
|
130
161
|
},
|
|
162
|
+
...(capability && needsInput ? {
|
|
163
|
+
interaction: {
|
|
164
|
+
schema_version: "itpay.interaction.v1",
|
|
165
|
+
stage: "input_required",
|
|
166
|
+
input_template: {
|
|
167
|
+
command: `itpay services invoke ${response.execution.service_execution_id} --capability ${capability.capability_id}${requiredInput.map((field) => ` --input ${field}=<value>`).join("")} --json`,
|
|
168
|
+
required_input: requiredInput,
|
|
169
|
+
executable: false,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
} : {}),
|
|
131
173
|
recovery: [],
|
|
132
174
|
}, {
|
|
133
175
|
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
@@ -153,11 +195,19 @@ function locationConfirmationEnvelope(executionID, capabilityID, input, confirma
|
|
|
153
195
|
result: { service_execution_id: executionID, query: input, location_confirmation: confirmation },
|
|
154
196
|
instruction: "尚未查票。仅向用户确认 endpoints 中有歧义的地点,展示真实候选名称、地址和高德链接。不要自行选择候选、把区域改成车站或重复调用原查询。用户选择后保留原输入,带 location_confirmation 回到同一服务。没有候选时请用户补充已知城市或准确地名,再使用修正输入查询。",
|
|
155
197
|
next: null,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
198
|
+
...(confirmation.can_resume ? {
|
|
199
|
+
interaction: {
|
|
200
|
+
schema_version: "itpay.interaction.v1",
|
|
201
|
+
stage: "location_confirmation_required",
|
|
202
|
+
input_template: {
|
|
203
|
+
command: `itpay services invoke ${executionID} --capability ${capabilityID}${formatInputOptions({ ...input,
|
|
204
|
+
location_confirmation: { plan_id: confirmation.plan_id, token: confirmation.token, choices } })} --json`,
|
|
205
|
+
required_input: ["choices"],
|
|
206
|
+
executable: false,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
} : {}),
|
|
210
|
+
recovery: [],
|
|
161
211
|
};
|
|
162
212
|
}
|
|
163
213
|
function requiredInputFields(schema) {
|
|
@@ -314,18 +364,33 @@ function invokedEnvelope(response, requestedCapability, capabilities, input) {
|
|
|
314
364
|
}
|
|
315
365
|
}
|
|
316
366
|
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
|
-
};
|
|
367
|
+
next = null;
|
|
321
368
|
}
|
|
322
369
|
else if (items.length === 0) {
|
|
323
370
|
next = null;
|
|
324
371
|
}
|
|
372
|
+
const interaction = items.length > 0 && requestedCapability.requires_human_action
|
|
373
|
+
? {
|
|
374
|
+
schema_version: "itpay.interaction.v1",
|
|
375
|
+
stage: "query_results_ready",
|
|
376
|
+
by_goal: {
|
|
377
|
+
compare: { action: "present_options", max_options: 3, stop_after_presentation: true },
|
|
378
|
+
prepare_checkout: {
|
|
379
|
+
action: "select_under_user_rules_then_prepare",
|
|
380
|
+
requires: ["current_delegation", "eligible_current_selection", "owner_permits_next_step"],
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
input_template: {
|
|
384
|
+
command: `itpay services action ${response.execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
|
|
385
|
+
required_input: ["rank"],
|
|
386
|
+
executable: false,
|
|
387
|
+
},
|
|
388
|
+
}
|
|
389
|
+
: undefined;
|
|
325
390
|
if (preview?.resolved_locations)
|
|
326
391
|
instruction += " 按 resolved_locations 说明实际解析地点;区域级位置仅是范围代表点,接驳时间不是从用户精确位置计算的。";
|
|
327
392
|
return {
|
|
328
|
-
value: { status, result: baseResult, instruction, next, recovery: [] },
|
|
393
|
+
value: { status, result: baseResult, instruction, next, recovery: [], ...(interaction ? { interaction } : {}) },
|
|
329
394
|
plainResult: serviceResultPlainLines(baseResult),
|
|
330
395
|
};
|
|
331
396
|
}
|
|
@@ -767,6 +832,7 @@ export async function runServicesGet(backend, serviceExecutionID, options = {})
|
|
|
767
832
|
export async function runServicesNext(backend, serviceExecutionID, options = {}) {
|
|
768
833
|
const response = await backend.getServiceExecution(serviceExecutionID, options.sinceSnapshot ? { sinceSnapshot: options.sinceSnapshot } : {});
|
|
769
834
|
const envelope = servicesNextEnvelope(response);
|
|
835
|
+
journalTaskState(serviceExecutionID, envelope, options.env ?? process.env);
|
|
770
836
|
if (response.rail_booking)
|
|
771
837
|
envelope.result = { ...envelope.result, rail_booking: response.rail_booking };
|
|
772
838
|
writeCommandEnvelope(envelope, {
|
|
@@ -893,8 +959,8 @@ export async function runServicesReadResult(backend, serviceExecutionID, options
|
|
|
893
959
|
catalog,
|
|
894
960
|
},
|
|
895
961
|
instruction: "catalog 是本次查询的完整无损目录:journeys 为全部可行组合(含超出预览分页的组合),plans 是每个组合下的购票方案,stations/services/offers/ground_options 为共享字典,field_legend 解释紧凑字段。逐组合向用户说明车次、换乘与接驳取舍;席别与价格以 plans 内报价为准、下单前仍需核验;姓名身份证手机号只在 ItPay 网页填写。",
|
|
896
|
-
next: journeys.length > 0
|
|
897
|
-
? { command: `itpay services read-result ${serviceExecutionID} --snapshot ${committed.snapshot_id} --journey ${journeys[0]
|
|
962
|
+
next: journeys.length > 0 && journeys[0]?.ref && journeys[0].ref !== "?"
|
|
963
|
+
? { command: `itpay services read-result ${serviceExecutionID} --snapshot ${committed.snapshot_id} --journey ${journeys[0].ref} --json`, reason: "查看单个行程明细" }
|
|
898
964
|
: { command: `itpay services next ${serviceExecutionID} --since-snapshot ${committed.snapshot_id} --json`, reason: "返回规划进展" },
|
|
899
965
|
recovery: [],
|
|
900
966
|
}, {
|
|
@@ -1046,12 +1112,22 @@ function railJourneySummary(card, serviceExecutionID) {
|
|
|
1046
1112
|
...(card.observed_at ? { observed_at: card.observed_at } : {}),
|
|
1047
1113
|
...(card.risk_notes?.length ? { risk_notes: card.risk_notes } : {}),
|
|
1048
1114
|
...(card.booking_support === "single_leg"
|
|
1049
|
-
? {
|
|
1115
|
+
? {
|
|
1116
|
+
select: `itpay services action ${serviceExecutionID} --action select_journey --actor-type human --status approved --input journey_id=${card.journey_id} --json`,
|
|
1117
|
+
// Delegated selection under explicit user rules is an agent action —
|
|
1118
|
+
// never record it as a human pick.
|
|
1119
|
+
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`,
|
|
1120
|
+
}
|
|
1050
1121
|
: {}),
|
|
1051
1122
|
...(card.booking_offer
|
|
1052
1123
|
? {
|
|
1053
1124
|
booking_offer: card.booking_offer,
|
|
1054
|
-
|
|
1125
|
+
booking_template: {
|
|
1126
|
+
command: `itpay services run ${card.booking_offer.service_id} --input-json <file> --json`,
|
|
1127
|
+
required_input: ["file"],
|
|
1128
|
+
executable: false,
|
|
1129
|
+
input_example: { selection: { token: card.booking_offer.selection_token, seat_type: "<席别>" }, passengers: "<n>" },
|
|
1130
|
+
},
|
|
1055
1131
|
}
|
|
1056
1132
|
: {}),
|
|
1057
1133
|
};
|
|
@@ -1114,6 +1190,54 @@ function railPlanningEnvelope(model) {
|
|
|
1114
1190
|
command: `itpay services next ${se}${plan.snapshot_id ? ` --since-snapshot ${plan.snapshot_id}` : ""} --json`,
|
|
1115
1191
|
reason: "读取同一规划的增量进展(不触发供应商调用)",
|
|
1116
1192
|
};
|
|
1193
|
+
// Two-goal interaction contract (spec 03 §2): the local Agent picks compare
|
|
1194
|
+
// or prepare_checkout from full context — the envelope teaches both branches
|
|
1195
|
+
// so a delegated purchase keeps moving and a comparison never buys.
|
|
1196
|
+
const bookable = journeys.find((card) => card.booking_support === "single_leg" && card.booking_offer);
|
|
1197
|
+
const queryResultsInteraction = journeys.length > 0
|
|
1198
|
+
? {
|
|
1199
|
+
schema_version: "itpay.interaction.v1",
|
|
1200
|
+
stage: "query_results_ready",
|
|
1201
|
+
by_goal: {
|
|
1202
|
+
compare: { action: "present_options", max_options: 3, stop_after_presentation: true },
|
|
1203
|
+
prepare_checkout: {
|
|
1204
|
+
action: "select_under_user_rules_then_prepare",
|
|
1205
|
+
recipe: "rail.selected-to-checkout.v1",
|
|
1206
|
+
requires: ["current_delegation", "eligible_current_selection", "owner_permits_next_step"],
|
|
1207
|
+
...(bookable
|
|
1208
|
+
? {
|
|
1209
|
+
steps: [
|
|
1210
|
+
"按用户明确规则在 journeys 中选择合格者(卡片 select_delegated 提交 agent 委托选择;人类亲自选择用 select)",
|
|
1211
|
+
`用所选卡片的 booking_template 写 owner-only 临时输入(selection token + 席别 + 人数,不含身份信息),运行 itpay services run itpay-rail-booking --input-json <file> --json`,
|
|
1212
|
+
"owner 要求聊天 review 时一次合并补齐必要字段,真实确认后提交原 workflow:confirm_booking;不得伪造 accept",
|
|
1213
|
+
`确认后恢复同一购买执行:itpay services run itpay-rail-booking --execution <实际ID> --json,沿用有界等待与受保护 Checkout`,
|
|
1214
|
+
],
|
|
1215
|
+
}
|
|
1216
|
+
: { unavailable: "当前结果无可整段购买映射;分别说明各段风险,不得合成原子联程。" }),
|
|
1217
|
+
},
|
|
1218
|
+
},
|
|
1219
|
+
}
|
|
1220
|
+
: undefined;
|
|
1221
|
+
const queryResultsCommunication = journeys.length > 0
|
|
1222
|
+
? {
|
|
1223
|
+
schema_version: "itpay.communication.v1",
|
|
1224
|
+
status_line: expansion === "paused"
|
|
1225
|
+
? "首批结果已就绪,扩展搜索已暂停等待用户决定"
|
|
1226
|
+
: "查询结果就绪",
|
|
1227
|
+
recommended_reason: plan.recommendation?.explanation?.[0],
|
|
1228
|
+
human_steps: [
|
|
1229
|
+
"compare:比较首选与最多两个备选后告知取舍",
|
|
1230
|
+
"prepare_checkout:委托仍在时确认所选,继续官方确认页",
|
|
1231
|
+
"姓名、证件、手机号只在受保护 Checkout 页填写",
|
|
1232
|
+
],
|
|
1233
|
+
next_expectation: "compare:用户选定其一;prepare_checkout:委托仍在时继续到官方确认入口,人完成review与付款",
|
|
1234
|
+
must_convey: [
|
|
1235
|
+
"推荐与备选的时间/费用/便利性取舍",
|
|
1236
|
+
"金额为查询时参考价,下单前须核验",
|
|
1237
|
+
"姓名、证件、手机号只在受保护网页填写",
|
|
1238
|
+
],
|
|
1239
|
+
}
|
|
1240
|
+
: undefined;
|
|
1117
1241
|
switch (expansion) {
|
|
1118
1242
|
case "complete":
|
|
1119
1243
|
return {
|
|
@@ -1121,10 +1245,12 @@ function railPlanningEnvelope(model) {
|
|
|
1121
1245
|
result,
|
|
1122
1246
|
// §7.3: the lead line carries real combination counts bucketed by
|
|
1123
1247
|
// verified transfer count — seat rows never inflate the journey count.
|
|
1124
|
-
instruction: `${journeyMix ? `本次共${journeyMix}(席别不重复计数)。` : ""}${plan.search?.reason === "budget_exhausted" ? "本次查询已达到上限,以下是已查到的方案,搜索范围尚未全部核验。" : "本轮规划已完成。"}
|
|
1248
|
+
instruction: `${journeyMix ? `本次共${journeyMix}(席别不重复计数)。` : ""}${plan.search?.reason === "budget_exhausted" ? "本次查询已达到上限,以下是已查到的方案,搜索范围尚未全部核验。" : "本轮规划已完成。"}compare:解释首选及最多两个有意义备选的时间/费用/便利性取舍后等待用户选择。prepare_checkout:用户已明确委托按规则选择时,按 interaction.recipe 选合格者并继续到官方确认页,不再次问是否下单;身份、review和最终付款边界仍然生效。乘车人身份信息不在此收集,后续购买走受保护 Checkout。`,
|
|
1125
1249
|
next: journeys[0]?.booking_support === "single_leg"
|
|
1126
|
-
? { command: railJourneySummary(journeys[0], se).select, reason: "
|
|
1250
|
+
? { command: railJourneySummary(journeys[0], se).select, reason: "用户在 compare 后选定推荐行程时执行;prepare_checkout 且规则命中时用 select_delegated" }
|
|
1127
1251
|
: null,
|
|
1252
|
+
...(queryResultsInteraction ? { interaction: queryResultsInteraction } : {}),
|
|
1253
|
+
...(queryResultsCommunication ? { communication: queryResultsCommunication } : {}),
|
|
1128
1254
|
recovery: [],
|
|
1129
1255
|
};
|
|
1130
1256
|
case "paused": {
|
|
@@ -1168,8 +1294,10 @@ function railPlanningEnvelope(model) {
|
|
|
1168
1294
|
return {
|
|
1169
1295
|
status: "awaiting_input",
|
|
1170
1296
|
result,
|
|
1171
|
-
instruction: `${modelDegradedHint}${failedPairsHint}${directHint}${transferHint}${partialPause}规划已发布首批结果并暂停扩展:展示现有卡片与 available_actions,等待用户意图(${expandHint})。ready 后 next
|
|
1297
|
+
instruction: `${modelDegradedHint}${failedPairsHint}${directHint}${transferHint}${partialPause}规划已发布首批结果并暂停扩展:展示现有卡片与 available_actions,等待用户意图(${expandHint})。ready 后 next 为空表示向用户汇报并等待,不是永远没有更多。compare:解释首选及最多两个备选后等待;prepare_checkout:委托仍在且选项合格时按 interaction.recipe 继续到官方确认入口,不再次问是否下单。`,
|
|
1172
1298
|
next: null,
|
|
1299
|
+
...(queryResultsInteraction ? { interaction: queryResultsInteraction } : {}),
|
|
1300
|
+
...(queryResultsCommunication ? { communication: queryResultsCommunication } : {}),
|
|
1173
1301
|
recovery: [],
|
|
1174
1302
|
};
|
|
1175
1303
|
}
|
|
@@ -1216,6 +1344,17 @@ function railPlanningEnvelope(model) {
|
|
|
1216
1344
|
result,
|
|
1217
1345
|
instruction: `${modelDegradedHint}${manualExpansion}${transferHint}${failedPairsHint}规划进行中:已提交的卡片可先向用户展示比较;稍后按 next 增量轮询同一执行(since-snapshot 命中时负载会被压缩,但阶段与计数仍返回最新值)。不要重新发起查询。`,
|
|
1218
1346
|
next: nextPoll,
|
|
1347
|
+
communication: {
|
|
1348
|
+
schema_version: "itpay.communication.v1",
|
|
1349
|
+
status_line: transferSearching && !manualExpansion
|
|
1350
|
+
? "直达范围暂未找到可用方案,中转组合仍在搜索"
|
|
1351
|
+
: "规划进行中,已确认的方案可先向用户展示",
|
|
1352
|
+
next_expectation: "按 next 增量轮询同一执行;不重新发起查询",
|
|
1353
|
+
must_convey: [
|
|
1354
|
+
"当前是进行中的部分结果,不代表最终答复",
|
|
1355
|
+
...(failedPairsHint ? ["部分站对尚未查明,不能说'没有车'"] : []),
|
|
1356
|
+
],
|
|
1357
|
+
},
|
|
1219
1358
|
recovery: [],
|
|
1220
1359
|
};
|
|
1221
1360
|
}
|
|
@@ -1373,18 +1512,48 @@ function servicesNextEnvelope(model) {
|
|
|
1373
1512
|
result: { service_execution_id: id, service_id: execution.service_id, human_action: action,
|
|
1374
1513
|
required_fields: requiredFields, sides },
|
|
1375
1514
|
instruction: "向用户展示 sides 中 status=needs_confirmation 一端的候选(名称+坐标),请用户选定后按 required_fields 逐项各传一个 --input:名称取候选 name,坐标取候选 location 的 [lng,lat];已 resolved 的一端把其 resolved_place 原样填入。继续同一执行,不重新查询;这一步不是购买确认。",
|
|
1376
|
-
next:
|
|
1515
|
+
next: null,
|
|
1516
|
+
interaction: {
|
|
1517
|
+
schema_version: "itpay.interaction.v1",
|
|
1518
|
+
stage: "location_confirmation_required",
|
|
1519
|
+
input_template: {
|
|
1520
|
+
command: `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved${requiredFields.map((field) => ` --input ${field}=<值>`).join("")} --json`,
|
|
1521
|
+
required_input: requiredFields,
|
|
1522
|
+
executable: false,
|
|
1523
|
+
},
|
|
1524
|
+
},
|
|
1377
1525
|
recovery: [],
|
|
1378
1526
|
};
|
|
1379
1527
|
}
|
|
1380
1528
|
const review = action.context?.review;
|
|
1381
1529
|
if (review) {
|
|
1530
|
+
// W4: when the owner projects an official web review entry, present it
|
|
1531
|
+
// and wait for the human there; otherwise keep the one consolidated
|
|
1532
|
+
// chat review — never fabricate an accepted review.
|
|
1533
|
+
const reviewURL = [review.web_review_url, review.entry_url, review.review_url]
|
|
1534
|
+
.find((value) => typeof value === "string" && /^https:\/\//.test(value));
|
|
1535
|
+
const requirementsRemaining = Array.isArray(review.requirements_remaining)
|
|
1536
|
+
? review.requirements_remaining
|
|
1537
|
+
: undefined;
|
|
1382
1538
|
return {
|
|
1383
1539
|
status: "booking_review_required",
|
|
1384
1540
|
result: { service_execution_id: id, service_id: execution.service_id, human_action: action,
|
|
1385
|
-
required_fields: requiredFields
|
|
1386
|
-
|
|
1387
|
-
|
|
1541
|
+
required_fields: requiredFields,
|
|
1542
|
+
...(requirementsRemaining ? { requirements_remaining: requirementsRemaining } : {}) },
|
|
1543
|
+
...(reviewURL ? { handoff: { url: reviewURL, kind: "booking_review" } } : {}),
|
|
1544
|
+
instruction: reviewURL
|
|
1545
|
+
? "已在 handoff.url 提供官方行程确认页,请用户在该页核对行程、席别与座位偏好并确认;页面打开请求不代表用户已确认。该页失效或用户更愿在对话中确认时,仍可按下述 input_template 一次合并提交。乘客姓名/证件等身份信息不在此提交,仍走受保护 Checkout 页。"
|
|
1546
|
+
: "向用户展示 context.review 中的行程(legs)与可选席别/座位偏好词表(seat_options),并明确告知:座位偏好仅为请求、不保证分配(对应 notice_version 文案须经用户同意)。这是购买前的合并确认:把乘客人数、席别与每位乘客的座位偏好一次问清,然后按 required_fields 组装完整 JSON 对象,经 --input-json <file> 提交——draft_revision 必须等于服务端当前值,被拒绝(booking_review_changed)时重新读取后重试。未得真实确认不得提交,不能暗设 accept_non_guaranteed。乘客姓名/证件等身份信息不在此提交,仍走受保护 Checkout 页。",
|
|
1547
|
+
next: null,
|
|
1548
|
+
interaction: {
|
|
1549
|
+
schema_version: "itpay.interaction.v1",
|
|
1550
|
+
stage: "booking_review_required",
|
|
1551
|
+
input_template: {
|
|
1552
|
+
command: `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved --input-json <file> --json`,
|
|
1553
|
+
required_input: requiredFields.length > 0 ? requiredFields : ["file"],
|
|
1554
|
+
executable: false,
|
|
1555
|
+
},
|
|
1556
|
+
},
|
|
1388
1557
|
recovery: [{ command: `itpay services next ${id} --json`, reason: "重新读取当前 draft_revision 后重试" }],
|
|
1389
1558
|
};
|
|
1390
1559
|
}
|
|
@@ -1392,7 +1561,16 @@ function servicesNextEnvelope(model) {
|
|
|
1392
1561
|
status: "confirmation_required",
|
|
1393
1562
|
result: { service_execution_id: id, service_id: execution.service_id, human_action: action },
|
|
1394
1563
|
instruction: "请展示待确认的内容,请用户明确确认后,按 input_schema 填写 --input 字段。继续同一执行,不重新查询;这一步不是购买确认。",
|
|
1395
|
-
next:
|
|
1564
|
+
next: null,
|
|
1565
|
+
interaction: {
|
|
1566
|
+
schema_version: "itpay.interaction.v1",
|
|
1567
|
+
stage: "confirmation_required",
|
|
1568
|
+
input_template: {
|
|
1569
|
+
command: `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved${requiredFields.map((field) => ` --input ${field}=<值>`).join("")} --json`,
|
|
1570
|
+
required_input: requiredFields,
|
|
1571
|
+
executable: false,
|
|
1572
|
+
},
|
|
1573
|
+
},
|
|
1396
1574
|
recovery: [],
|
|
1397
1575
|
};
|
|
1398
1576
|
}
|
|
@@ -1400,8 +1578,15 @@ function servicesNextEnvelope(model) {
|
|
|
1400
1578
|
let command = `itpay services next ${id} --json`;
|
|
1401
1579
|
if (state === "payment")
|
|
1402
1580
|
command = `itpay services checkout ${id} --json`;
|
|
1403
|
-
|
|
1404
|
-
|
|
1581
|
+
// input_required: the resume command needs a real file path — expose it as
|
|
1582
|
+
// a non-executable input_template, not a fake `next.command`.
|
|
1583
|
+
const inputTemplate = state === "input_required"
|
|
1584
|
+
? {
|
|
1585
|
+
command: `itpay services run ${execution.service_id} --execution ${id} --input-json <file> --json`,
|
|
1586
|
+
required_input: ["file"],
|
|
1587
|
+
executable: false,
|
|
1588
|
+
}
|
|
1589
|
+
: undefined;
|
|
1405
1590
|
const guidance = railServiceGuidance(execution.service_id);
|
|
1406
1591
|
const failedStep = recovery ? failedWorkflowStep(model.workflow?.steps, model.workflow?.error_code) : undefined;
|
|
1407
1592
|
const failedStepGuidance = failedStep ? WORKFLOW_STEP_GUIDANCE[failedStep] : undefined;
|
|
@@ -1439,14 +1624,26 @@ function servicesNextEnvelope(model) {
|
|
|
1439
1624
|
? "按 result.guidance 的字段契约填写输入后继续同一服务执行;不要臆造字段名。"
|
|
1440
1625
|
: "继续读取同一执行;缺少输入时按服务声明补齐。"),
|
|
1441
1626
|
next: recovery ? null : { command, reason: "继续当前流程" },
|
|
1627
|
+
...(inputTemplate || (state === "payment" && model.workflow?.human_action?.context?.review) ? {
|
|
1628
|
+
interaction: {
|
|
1629
|
+
schema_version: "itpay.interaction.v1",
|
|
1630
|
+
stage: inputTemplate ? "input_required" : "payment",
|
|
1631
|
+
...(inputTemplate ? { input_template: inputTemplate } : {}),
|
|
1632
|
+
...(state === "payment" && model.workflow?.human_action?.context?.review ? {
|
|
1633
|
+
revise_template: {
|
|
1634
|
+
command: `itpay services action ${id} --action ${model.workflow.human_action.action_type} --actor-type human --status approved --input-json <file> --json`,
|
|
1635
|
+
required_input: ["file"],
|
|
1636
|
+
executable: false,
|
|
1637
|
+
},
|
|
1638
|
+
} : {}),
|
|
1639
|
+
},
|
|
1640
|
+
} : {}),
|
|
1442
1641
|
recovery: recovery && state === "failed"
|
|
1443
1642
|
? [
|
|
1444
1643
|
{ command: `itpay services start ${execution.service_id} --json`, reason: "修正输入后重新发起(本执行已终止不能续用)" },
|
|
1445
1644
|
...(guidance ? [{ command: "itpay docs show rail-booking --json", reason: "查看本服务输入字段契约与示例" }] : []),
|
|
1446
1645
|
]
|
|
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
|
-
: [],
|
|
1646
|
+
: [],
|
|
1450
1647
|
};
|
|
1451
1648
|
}
|
|
1452
1649
|
const terminalEnvelope = terminalExecutionEnvelope(model);
|
|
@@ -1801,6 +1998,12 @@ function buildServicesCheckoutEnvelope(response, checkoutURL, plan, agentType, t
|
|
|
1801
1998
|
capability_id: checkoutCapabilityID(response),
|
|
1802
1999
|
locked_input: response.locked_input,
|
|
1803
2000
|
amount,
|
|
2001
|
+
...(checkout.checkout.expires_at ? {
|
|
2002
|
+
// The shared server-owned payable deadline D: refreshing the page or
|
|
2003
|
+
// re-creating a checkout can never extend it; past it the checkout
|
|
2004
|
+
// and pending order are terminally cancelled.
|
|
2005
|
+
payment_deadline_at: checkout.checkout.expires_at,
|
|
2006
|
+
} : {}),
|
|
1804
2007
|
},
|
|
1805
2008
|
handoff: presentationHandoff.handoff,
|
|
1806
2009
|
instruction: presentationHandoff.instruction,
|
package/dist/src/main.js
CHANGED
|
@@ -311,7 +311,18 @@ for (const action of ["login", "status"]) {
|
|
|
311
311
|
const config = loadConfig();
|
|
312
312
|
try {
|
|
313
313
|
const result = await agentAuth(action, config.baseURL, newBackendClient(config));
|
|
314
|
-
|
|
314
|
+
// AUTH06: auth output goes through the same envelope renderer as every
|
|
315
|
+
// other command — next/recovery keep Agent Type + Backend qualification
|
|
316
|
+
// and non-JSON output is human-readable instead of a raw JSON dump.
|
|
317
|
+
const envelope = {
|
|
318
|
+
status: String(result.status ?? "error"),
|
|
319
|
+
result: (result.result ?? result),
|
|
320
|
+
...(result.handoff ? { handoff: result.handoff } : {}),
|
|
321
|
+
instruction: typeof result.instruction === "string" ? result.instruction : "按返回状态继续。",
|
|
322
|
+
next: (result.next ?? null),
|
|
323
|
+
recovery: (result.recovery ?? []),
|
|
324
|
+
};
|
|
325
|
+
writeCommandEnvelope(envelope, { jsonOutput: Boolean(options.json) });
|
|
315
326
|
}
|
|
316
327
|
catch (error) {
|
|
317
328
|
reportCLIError(error, { jsonOutput: Boolean(options.json), code: "account_login_failed", instruction: "完成官方网页登录后重试 itpay auth status;不要清除设备登记。" });
|
|
@@ -893,6 +904,13 @@ program
|
|
|
893
904
|
.option("--id <checkout_id>")
|
|
894
905
|
.option("--token <display_token>")
|
|
895
906
|
.option("--locale <locale>", "payment card language: zh-CN|en", "zh-CN")
|
|
907
|
+
.option("--present <method>", "display once: auto|browser|image|link|none (JSON default plans only)")
|
|
908
|
+
.option("--no-open", "never open a browser window")
|
|
909
|
+
.option("--viewer <viewer>", "device the human is looking at: desktop|mobile|unknown")
|
|
910
|
+
.option("--relay-option <ref>", "server-issued contact option reference for message relay")
|
|
911
|
+
.option("--confirm-relay", "confirm the human explicitly consented to this relay send")
|
|
912
|
+
.option("--request-key <key>", "stable idempotency key for a relay request")
|
|
913
|
+
.option("--relay-status <relay_id>", "read a prior relay result; never resends")
|
|
896
914
|
.option("--json", "output compact JSON")
|
|
897
915
|
.action(async (options) => {
|
|
898
916
|
const config = loadConfig();
|
|
@@ -919,6 +937,13 @@ program
|
|
|
919
937
|
baseURL: config.baseURL,
|
|
920
938
|
locale: options.locale,
|
|
921
939
|
jsonOutput: Boolean(options.json),
|
|
940
|
+
...(options.present ? { present: options.present } : {}),
|
|
941
|
+
...(options.noOpen ? { noOpen: true } : {}),
|
|
942
|
+
...(options.viewer ? { viewer: options.viewer } : {}),
|
|
943
|
+
...(options.relayOption ? { relayOption: options.relayOption } : {}),
|
|
944
|
+
...(options.confirmRelay ? { confirmRelay: true } : {}),
|
|
945
|
+
...(options.requestKey ? { requestKey: options.requestKey } : {}),
|
|
946
|
+
...(options.relayStatus ? { relayStatus: options.relayStatus } : {}),
|
|
922
947
|
});
|
|
923
948
|
}
|
|
924
949
|
catch (error) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Local browser dispatch for `--present browser`. Opens the URL via the OS
|
|
2
|
+
// opener with an argv array — never a model-generated shell string. Dispatch
|
|
3
|
+
// proves only that the request was accepted by the OS; it never means the
|
|
4
|
+
// page became visible (that would be `page_loaded`, which needs evidence).
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { platform } from "node:os";
|
|
7
|
+
// Official HTTPS origins (or the loopback dev origin behind ITPAY_CLI_DEV).
|
|
8
|
+
// No userinfo, no non-http(s) schemes, no redirect chains — the opener gets
|
|
9
|
+
// exactly this URL once.
|
|
10
|
+
export function isPresentableURL(raw, baseURL, env = process.env) {
|
|
11
|
+
let url;
|
|
12
|
+
try {
|
|
13
|
+
url = new URL(raw);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (url.username || url.password)
|
|
19
|
+
return false;
|
|
20
|
+
if (env.ITPAY_CLI_DEV === "1" && /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(url.origin))
|
|
21
|
+
return true;
|
|
22
|
+
if (url.protocol !== "https:")
|
|
23
|
+
return false;
|
|
24
|
+
const allowedOrigins = new Set(["https://app.itpay.ai", "https://sandbox.itpay.ai", "https://dev.itpay.ai"]);
|
|
25
|
+
if (baseURL) {
|
|
26
|
+
try {
|
|
27
|
+
allowedOrigins.add(new URL(baseURL).origin);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* ignore malformed base URL */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return allowedOrigins.has(url.origin);
|
|
34
|
+
}
|
|
35
|
+
export function openInSystemBrowser(raw, baseURL, env = process.env) {
|
|
36
|
+
if (!isPresentableURL(raw, baseURL, env)) {
|
|
37
|
+
return Promise.resolve({ status: "failed", reason: "url_rejected" });
|
|
38
|
+
}
|
|
39
|
+
const [command, args] = openerArgv(raw);
|
|
40
|
+
if (!command) {
|
|
41
|
+
return Promise.resolve({ status: "unavailable", reason: "no system opener on this platform" });
|
|
42
|
+
}
|
|
43
|
+
return new Promise((resolvePromise) => {
|
|
44
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
45
|
+
child.once("error", (error) => resolvePromise({ status: "failed", reason: error.message }));
|
|
46
|
+
child.once("spawn", () => {
|
|
47
|
+
child.unref();
|
|
48
|
+
resolvePromise({ status: "dispatch_accepted", argv: [command, ...args] });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function openerArgv(url) {
|
|
53
|
+
switch (platform()) {
|
|
54
|
+
case "darwin":
|
|
55
|
+
return ["open", [url]];
|
|
56
|
+
case "win32":
|
|
57
|
+
return ["cmd.exe", ["/c", "start", "", url]];
|
|
58
|
+
case "linux":
|
|
59
|
+
case "freebsd":
|
|
60
|
+
case "openbsd":
|
|
61
|
+
return ["xdg-open", [url]];
|
|
62
|
+
default:
|
|
63
|
+
return [undefined, []];
|
|
64
|
+
}
|
|
65
|
+
}
|