@itpay/cli 2.1.41 → 2.1.44
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/README.md +12 -5
- package/assets/sell-preview/index.html +6 -0
- package/assets/sell-preview/preview.js +9 -0
- package/bin/darwin-amd64/itpay-sell +0 -0
- package/bin/darwin-arm64/itpay-sell +0 -0
- package/bin/linux-amd64/itpay-sell +0 -0
- package/bin/linux-arm64/itpay-sell +0 -0
- package/bin/windows-amd64/itpay-sell.exe +0 -0
- package/dist/src/client/backend.js +38 -2
- package/dist/src/client/http.js +2 -0
- package/dist/src/commands/aftercare.js +12 -0
- package/dist/src/commands/buy.js +17 -58
- package/dist/src/commands/cart.js +17 -15
- package/dist/src/commands/catalog.js +39 -14
- package/dist/src/commands/checkout.js +146 -5
- package/dist/src/commands/checkout_handoff.js +11 -61
- package/dist/src/commands/docs.js +7 -18
- package/dist/src/commands/guidance.js +1 -134
- package/dist/src/commands/install.js +6 -3
- package/dist/src/commands/order.js +34 -3
- package/dist/src/commands/orders.js +4 -9
- package/dist/src/commands/pay.js +1 -1
- package/dist/src/commands/presentation.js +153 -0
- package/dist/src/commands/rail_catalog.js +1 -0
- package/dist/src/commands/readyz.js +1 -1
- package/dist/src/commands/refund.js +13 -57
- package/dist/src/commands/services/actions.js +133 -0
- package/dist/src/commands/services/commerce.js +197 -0
- package/dist/src/commands/services/lifecycle.js +418 -0
- package/dist/src/commands/services/results.js +130 -0
- package/dist/src/commands/services.js +14 -1068
- package/dist/src/commands/skill.js +8 -5
- package/dist/src/commands/vault.js +27 -20
- package/dist/src/commands/vault_handoff.js +18 -0
- package/dist/src/main.js +332 -46
- package/dist/src/render/browser.js +65 -0
- package/dist/src/render/checkout_handoff.js +101 -0
- package/dist/src/render/checkout_plan.js +47 -0
- package/dist/src/render/markdown.js +0 -3
- package/dist/src/render/qr.js +1 -1
- package/dist/src/sell/commands.js +80 -0
- package/dist/src/sell/contract.js +86 -0
- package/dist/src/sell/local.js +260 -0
- package/dist/src/sell/mcp.js +49 -0
- package/dist/src/sell/preview.js +70 -0
- package/dist/src/sell/sync.js +145 -0
- package/dist/src/services/adapter.js +1 -0
- package/dist/src/services/presentation.js +198 -0
- package/dist/src/services/rail/actions.js +51 -0
- package/dist/src/services/rail/adapter.js +36 -0
- package/dist/src/services/rail/aftercare.js +45 -0
- package/dist/src/services/rail/catalog.js +261 -0
- package/dist/src/services/rail/checkout.js +19 -0
- package/dist/src/services/rail/fulfillment.js +76 -0
- package/dist/src/services/rail/guide.js +110 -0
- package/dist/src/services/rail/invocation.js +54 -0
- package/dist/src/services/rail/page.js +76 -0
- package/dist/src/services/rail/pagination.js +6 -0
- package/dist/src/services/rail/query.js +71 -0
- package/dist/src/services/rail/readers.js +107 -0
- package/dist/src/services/rail/reading.js +125 -0
- package/dist/src/services/rail/recovery.js +51 -0
- package/dist/src/services/rail/results.js +378 -0
- package/dist/src/services/rail/review.js +268 -0
- package/dist/src/services/rail/saved.js +49 -0
- package/dist/src/services/rail/wait.js +11 -0
- package/dist/src/services/registry.js +31 -0
- package/dist/src/services/schema.js +21 -0
- package/dist/src/services/shared/envelope.js +184 -0
- package/dist/src/services/shared/input.js +39 -0
- package/dist/src/services/shared/pagination.js +28 -0
- package/dist/src/services/shared/platform.js +365 -0
- package/dist/src/services/shared/refund.js +40 -0
- package/dist/src/services/shared/workflow.js +28 -0
- package/dist/src/state/account_auth.js +354 -0
- package/dist/src/state/cart_session.js +4 -1
- package/dist/src/state/client_context.js +72 -0
- package/dist/src/state/config.js +44 -14
- package/dist/src/state/device_authority.js +402 -72
- package/dist/src/state/task_journal.js +90 -0
- package/docs/agent/buyer/cart-checkout.json +8 -6
- package/docs/agent/buyer/catalog-list.json +29 -9
- package/docs/agent/buyer/identity-and-sessions.json +26 -9
- package/docs/agent/buyer/install-and-setup.json +17 -15
- package/docs/agent/buyer/orders-refunds.json +25 -9
- package/docs/agent/buyer/payment-flow.json +6 -4
- package/docs/agent/buyer/purchased-content.json +18 -4
- package/docs/agent/buyer/quickstart.json +26 -17
- package/docs/agent/buyer/rail-booking.json +176 -0
- package/docs/agent/buyer/rail-fast-checkout.json +48 -0
- package/docs/agent/buyer/render-hosts.json +23 -12
- package/docs/agent/buyer/service-feedback.json +4 -3
- package/docs/cli-reference/agent-types.md +7 -2
- package/docs/cli-reference/commands/aftercare.md +15 -0
- package/docs/cli-reference/commands/auth.md +96 -0
- package/docs/cli-reference/commands/buy.md +2 -1
- package/docs/cli-reference/commands/cart/add.md +5 -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 +5 -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 +2 -2
- package/docs/cli-reference/commands/checkout.md +12 -2
- package/docs/cli-reference/commands/device.md +18 -11
- 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 +8 -6
- 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 +11 -11
- 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/sell.md +1364 -0
- package/docs/cli-reference/commands/services/action.md +24 -3
- package/docs/cli-reference/commands/services/checkout.md +17 -2
- package/docs/cli-reference/commands/services/events.md +1 -1
- package/docs/cli-reference/commands/services/get.md +4 -2
- package/docs/cli-reference/commands/services/index.md +4 -3
- package/docs/cli-reference/commands/services/invoke.md +6 -4
- package/docs/cli-reference/commands/services/list.md +1 -1
- package/docs/cli-reference/commands/services/next.md +78 -12
- package/docs/cli-reference/commands/services/page.md +71 -0
- package/docs/cli-reference/commands/services/quote.md +4 -4
- package/docs/cli-reference/commands/services/read-result.md +17 -6
- package/docs/cli-reference/commands/services/run.md +152 -0
- package/docs/cli-reference/commands/services/start.md +5 -3
- package/docs/cli-reference/commands/vault/access.md +5 -1
- package/docs/cli-reference/conventions.md +4 -6
- package/docs/cli-reference/index.md +10 -1
- package/docs/sell.md +84 -0
- package/package.json +20 -12
- package/skills/itpay/SKILL.md +112 -102
- 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 -69
- 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
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { executionInputContext, operationInputTemplate } from "../shared/input.js";
|
|
2
|
+
import { requiredInputFields } from "../shared/platform.js";
|
|
3
|
+
import { locationConfirmationEnvelope } from "./query.js";
|
|
4
|
+
import { RAIL_LOCATION_EVIDENCE_REASONS, RAIL_QUOTE_FAILURE_GUIDANCE } from "./recovery.js";
|
|
5
|
+
import { railServiceGuidance, WORKFLOW_STEP_GUIDANCE, RAIL_ENDPOINT_BRIEF } from "./guide.js";
|
|
6
|
+
import { railInputTemplate as serviceInputTemplate } from "./guide.js";
|
|
7
|
+
export function railWorkflowEnvelope(model) {
|
|
8
|
+
const execution = model.execution;
|
|
9
|
+
if (model.workflow_entry && !["completed", "delivery"].includes(model.workflow?.status ?? "")) {
|
|
10
|
+
const id = execution.service_execution_id;
|
|
11
|
+
const workflowReason = model.workflow?.failure?.reason_code ?? model.workflow?.error_code;
|
|
12
|
+
const paymentVerified = model.payment_bindings.some((binding) => binding.status === "payment_verified") || model.checkout_bindings.some((binding) => binding.status === "payment_verified");
|
|
13
|
+
const state = model.workflow?.status === "payment" && paymentVerified ? "running" : model.workflow?.status ?? "input_required";
|
|
14
|
+
if (["quota_paused", "failed"].includes(state) && !RAIL_LOCATION_EVIDENCE_REASONS.has(workflowReason ?? "") && (model.workflow?.dependency ||
|
|
15
|
+
/^amap_/.test(workflowReason ?? ""))) {
|
|
16
|
+
const dependency = model.workflow?.dependency;
|
|
17
|
+
const resumable = state === "quota_paused" && dependency?.retryable === true;
|
|
18
|
+
return {
|
|
19
|
+
status: "dependency_unavailable",
|
|
20
|
+
result: { service_execution_id: id, service_id: execution.service_id, reason: workflowReason, failure: model.workflow?.failure, affected_endpoint: model.workflow?.failure?.endpoint ?? dependency?.endpoint, ...(dependency ? { dependency } : {}) },
|
|
21
|
+
instruction: resumable
|
|
22
|
+
? "地图服务暂时限流。保留原条件及已解析端点;到 retry_at 后执行下一步,继续同一任务,不重新提交地点。"
|
|
23
|
+
: "地图服务本轮未完成解析,这是外部依赖故障,不要求用户换地址或自行找坐标。有界尝试已结束;保留原条件及已有结果,交由服务维护方处理该错误。",
|
|
24
|
+
next: resumable ? { command: `itpay services run ${execution.service_id} --execution ${id} --json`, reason: "等待所示恢复时间后继续同一执行" } : null,
|
|
25
|
+
recovery: [],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (state === "failed" && RAIL_LOCATION_EVIDENCE_REASONS.has(workflowReason ?? "")) {
|
|
29
|
+
const code = workflowReason;
|
|
30
|
+
const affected = model.workflow?.failure?.endpoint ?? model.workflow?.dependency?.endpoint;
|
|
31
|
+
const query = model.workflow?.query_input ?? {};
|
|
32
|
+
const endpoints = query.endpoints && typeof query.endpoints === "object" ? query.endpoints : {};
|
|
33
|
+
const stationPair = [endpoints.origin, endpoints.destination].every(endpoint => endpoint?.kind === "station" && typeof endpoint.text === "string" && endpoint.text.trim() && typeof endpoint.station_code === "string" && /^[A-Z]{3}$/.test(endpoint.station_code));
|
|
34
|
+
if (code === "station_location_evidence_required") {
|
|
35
|
+
const exactInput = stationPair && typeof query.travel_date === "string" ? { origin: endpoints.origin.text, destination: endpoints.destination.text, travel_date: query.travel_date } : undefined;
|
|
36
|
+
return { status: "system_evidence_required", result: { service_execution_id: id, service_id: execution.service_id, reason: code, affected_endpoint: affected, failure: model.workflow?.failure, query,
|
|
37
|
+
...(exactInput ? { new_query_template: { command: "itpay services run itpay-rail-exact --input-json <双锁站直达.json> --json", input: exactInput, executable: false, required_input: ["file"], instruction: "当前明确双锁站可核验直达;Exact只覆盖这一站对,仍按原人数、时限和偏好核对结果。" } } : {}) },
|
|
38
|
+
instruction: "尚未查票。车站身份已知,但Smart缺车站坐标证据;这是系统证据缺口,不要求用户填技术坐标或换站。" + (exactInput ? "当前双锁站可按完整模板另用Exact核验直达,原需求见query。" : "本路线Smart暂受系统证据限制,当前无可执行恢复;停止并交由维护方补证据。"), next: null, recovery: [] };
|
|
39
|
+
}
|
|
40
|
+
if (affected !== "origin" && affected !== "destination")
|
|
41
|
+
return { status: "input_evidence_required", result: { service_execution_id: id, reason: code, failed_step: model.workflow?.failure?.step_id, failure: model.workflow?.failure, query }, instruction: "地点问题未明确受影响侧,当前无可提交修正模板;保留原需求,停止并核对服务端事实,不猜另一端或坐标。", next: null, recovery: [] };
|
|
42
|
+
const properties = model.workflow_entry.input_schema.properties;
|
|
43
|
+
if (!properties?.endpoints && properties?.[affected] === undefined)
|
|
44
|
+
return { status: "contract_unavailable", result: { service_execution_id: id, reason: code, failed_step: model.workflow?.failure?.step_id, failure: model.workflow?.failure, query }, instruction: "旧查询已终止,当前锁定合同缺少可用地点字段;保留原需求并停止,不猜新旧模板或隐藏字段。", next: null, recovery: [] };
|
|
45
|
+
const input = { ...Object.fromEntries(Object.entries(query).filter(([key]) => properties?.[key] !== undefined)), ...serviceInputTemplate(model.workflow_entry.input_schema, query) };
|
|
46
|
+
const identityConflict = ["endpoint_kind_conflict", "endpoint_adcode_name_conflict", "endpoint_poi_name_conflict", "endpoint_station_name_code_conflict", "coordinate_name_mismatch"].includes(code);
|
|
47
|
+
const replacement = code === "area_scope_too_broad" ? { kind: "area", text: "<受影响端的市县镇完整名称>" } : { ...(endpoints[affected]?.kind ? { kind: endpoints[affected].kind } : {}), text: code === "coordinate_identity_unverified" ? "<已核实与地点身份一致的完整地址或地点名>" : "<已补充行政归属的完整地址或地点名>" };
|
|
48
|
+
const endpointContract = properties?.endpoints !== undefined && (query.endpoints !== undefined || !properties.origin || !properties.destination);
|
|
49
|
+
if (endpointContract)
|
|
50
|
+
input.endpoints = { ...endpoints, [affected]: replacement };
|
|
51
|
+
else {
|
|
52
|
+
input[affected] = replacement.text;
|
|
53
|
+
delete input[`${affected}_location`];
|
|
54
|
+
}
|
|
55
|
+
const originalPlace = endpoints[affected]?.text ?? query[affected];
|
|
56
|
+
return { status: "awaiting_input", result: { service_execution_id: id, service_id: execution.service_id, reason: code, failed_step: model.workflow?.failure?.step_id, classification: "location_evidence_required", affected_endpoint: affected, original_place: originalPlace, ...(model.workflow?.failure?.resolved_scope ? { resolved_scope: model.workflow.failure.resolved_scope } : {}), failure: model.workflow?.failure, query,
|
|
57
|
+
new_query_template: { command: `itpay services run ${execution.service_id} --input-json <补全原始需求.json> --json`, input, executable: false, required_input: ["file", endpointContract ? `endpoints.${affected}.text` : affected], ...(endpointContract ? { correction_fields: ["text", "kind", "poi_id", "location"].filter(field => { const side = properties?.endpoints?.properties?.[affected]; return !!side?.properties?.[field]; }) } : {}), instruction: "旧执行输入已锁定;补真实地点后将完整input写入JSON文件,按此命令提交新查询,保留另一端、日期、人数和所有原约束。" } },
|
|
58
|
+
instruction: `旧执行已终止,尚未查票。${identityConflict ? "名称、类别或位置证据存在冲突;先核实返回原因,不能修改kind绕过。" : "完整地点名称合法,但尚缺唯一身份证据;可先用地图或浏览短查,不要求必填门牌或坐标。"}新证据确定类别且原意未变可纠正kind;改变目的地或扩大范围须先取得真实同意。保留原地点及范围;${affected === "origin" ? "出发端" : "到达端"}原文见original_place。有新证据后只补充${affected === "origin" ? "出发端" : "到达端"}${code === "area_scope_too_broad" ? "的市县镇范围" : code === "coordinate_identity_unverified" ? "的已核实地点身份;已移除未核实坐标,不猜坐标" : "的完整地点和行政归属"}。使用当前完整new_query_template;不能在旧执行伪造修改,也不能直接提交占位符。`, next: null, recovery: [] };
|
|
59
|
+
}
|
|
60
|
+
if (state === "failed" && ["login_required", "rate_limited"].includes(workflowReason ?? "")) {
|
|
61
|
+
const login = workflowReason === "login_required";
|
|
62
|
+
return {
|
|
63
|
+
status: login ? "login_required" : "rate_limited",
|
|
64
|
+
result: { service_execution_id: id, service_id: execution.service_id },
|
|
65
|
+
instruction: login ? "匿名免费额度已用完。使用官方网页登录并绑定当前 Agent,完成后重新发起查询;不需要付款。" : "已达到登录账号每分钟查询上限。请等到下一分钟再发起查询,不要连续重试。",
|
|
66
|
+
next: login ? { command: "itpay auth login --json", reason: "登录继续免费查询" } : null,
|
|
67
|
+
recovery: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (state === "quota_paused") {
|
|
71
|
+
const login = model.workflow?.error_code !== "rate_limited";
|
|
72
|
+
const resume = { command: `itpay services run ${execution.service_id} --execution ${id} --json`, reason: "登录或限流窗口后继续同一执行" };
|
|
73
|
+
const quota = model.quota ? { bucket: model.quota.bucket, subject_type: model.quota.subject_type, limit: model.quota.limit, remaining: model.quota.remaining } : undefined;
|
|
74
|
+
return {
|
|
75
|
+
status: login ? "login_required" : "rate_limited",
|
|
76
|
+
result: { service_execution_id: id, service_id: execution.service_id, ...(quota ? { quota } : {}) },
|
|
77
|
+
instruction: login ? "匿名免费额度已用完。使用 itpay auth login 完成官方登录并绑定当前 Agent,然后继续同一执行;不要重新发起查询,不需要付款。" : "已达到登录账号每分钟查询上限。请等到下一分钟后继续同一执行,不要连续重试。",
|
|
78
|
+
next: login ? { command: "itpay auth login --json", reason: "登录后继续同一查询执行" } : resume,
|
|
79
|
+
recovery: [resume],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if ((state === "human_action" || (state === "failed" && model.workflow?.human_action?.context?.quote_recovery === true)) && model.workflow?.human_action) {
|
|
83
|
+
const action = model.workflow.human_action;
|
|
84
|
+
const requiredFields = requiredInputFields(action.input_schema);
|
|
85
|
+
const rawPlaces = action.context?.places;
|
|
86
|
+
// Only the location-confirmation action carries origin/destination places;
|
|
87
|
+
// every other human action keeps the generic envelope.
|
|
88
|
+
const isLocationConfirmation = !!rawPlaces && typeof rawPlaces === "object" &&
|
|
89
|
+
("origin" in rawPlaces || "destination" in rawPlaces);
|
|
90
|
+
if (isLocationConfirmation) {
|
|
91
|
+
const places = rawPlaces;
|
|
92
|
+
const sides = ["origin", "destination"].map((side) => {
|
|
93
|
+
const place = places[side];
|
|
94
|
+
const candidates = Array.isArray(place?.resolution_candidates) ? place.resolution_candidates : [];
|
|
95
|
+
const name = (item) => item.poi_name ?? item.query ?? item.formatted_address;
|
|
96
|
+
return {
|
|
97
|
+
side,
|
|
98
|
+
status: place?.resolution_status,
|
|
99
|
+
...(place?.resolution_reason ? { reason: place.resolution_reason } : {}),
|
|
100
|
+
...(place?.resolution_status === "resolved"
|
|
101
|
+
? { resolved_place: { name: name(place), address: place.formatted_address, location: place.location } }
|
|
102
|
+
: {}),
|
|
103
|
+
...(candidates.length
|
|
104
|
+
? { candidates: candidates.slice(0, 3).map((item, index) => ({ id: item.id ?? `${side}_${index + 1}`, name: name(item), address: item.formatted_address, type: item.poi_type ?? item.type, location: item.location })) }
|
|
105
|
+
: {}),
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
status: "confirmation_required",
|
|
110
|
+
result: { service_execution_id: id, service_id: execution.service_id,
|
|
111
|
+
human_action: { action_type: action.action_type, input_schema: action.input_schema },
|
|
112
|
+
required_fields: requiredFields, sides, query: model.workflow?.query_input,
|
|
113
|
+
...(sides.some(side => side.status !== "resolved" && !side.candidates?.length && ["ADDRESS_REQUIRED", "ADMINISTRATIVE_AREA_UNRESOLVED"].includes(side.reason ?? "")) ? {
|
|
114
|
+
new_query_template: { command: `itpay services run ${execution.service_id} --input-json <补全原始需求.json> --json`, executable: false,
|
|
115
|
+
input: serviceInputTemplate(model.workflow_entry.input_schema, model.workflow?.query_input ?? {}),
|
|
116
|
+
required_input: ["补缺失端点", "保留原始日期人数时限偏好及已确认端点"], instruction: "原执行锁定输入;补全原需求后用新查询,不提交不存在的候选。" },
|
|
117
|
+
} : {}) },
|
|
118
|
+
instruction: sides.some(side => side.status !== "resolved" && !side.candidates?.length)
|
|
119
|
+
? locationConfirmationEnvelope(id, "", {}, { endpoints: sides.filter(side => side.status !== "resolved") }).instruction
|
|
120
|
+
: requiredFields.includes("choices")
|
|
121
|
+
? "尚未查票。仅向用户展示有真实歧义端点的候选编号、名称、行政地址与类型(内部 id 仅用于回填,不展示给人);用户选定后,把各未解析端点的候选 id 放入 choices 对象,继续同一执行。已解析端点无需再选;不得替用户决定,也不是购买确认。"
|
|
122
|
+
: "向用户展示有歧义一端的候选;按 required_fields 填写真实选择,已解析一端保留原值。继续同一执行;这不是购买确认。",
|
|
123
|
+
next: null,
|
|
124
|
+
...(sides.some(side => side.status !== "resolved" && !side.candidates?.length) ? {} : { interaction: {
|
|
125
|
+
schema_version: "itpay.interaction.v1",
|
|
126
|
+
stage: "location_confirmation_required",
|
|
127
|
+
input_template: {
|
|
128
|
+
command: requiredFields.includes("choices")
|
|
129
|
+
? `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved --input-json <地点确认.json> --json`
|
|
130
|
+
: `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved${requiredFields.map((field) => ` --input ${field}=<值>`).join("")} --json`,
|
|
131
|
+
input: { choices: Object.fromEntries(sides.filter(side => side.candidates?.length).map(side => [side.side, "<所选候选id>"])) },
|
|
132
|
+
required_input: requiredFields,
|
|
133
|
+
executable: false,
|
|
134
|
+
},
|
|
135
|
+
} }),
|
|
136
|
+
recovery: [],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const review = action.context?.review;
|
|
140
|
+
if (review) {
|
|
141
|
+
// W4: when the owner projects an official web review entry, present it
|
|
142
|
+
// and wait for the human there; otherwise keep the one consolidated
|
|
143
|
+
// chat review — never fabricate an accepted review.
|
|
144
|
+
const reviewURL = [review.web_review_url, review.entry_url, review.review_url]
|
|
145
|
+
.find((value) => typeof value === "string" && /^https:\/\//.test(value));
|
|
146
|
+
const requirementsRemaining = Array.isArray(review.requirements_remaining)
|
|
147
|
+
? review.requirements_remaining
|
|
148
|
+
: undefined;
|
|
149
|
+
return {
|
|
150
|
+
status: state === "failed" ? "quote_recovery_required" : "consent_required",
|
|
151
|
+
result: { service_execution_id: id, service_id: execution.service_id, review, ...(state === "failed" ? { failure: model.workflow?.failure } : {}),
|
|
152
|
+
consent_question: review.consent_question, notice_version: review.notice_version, draft_revision: review.draft_revision,
|
|
153
|
+
acceptance: "Agent须核对当前聊天明确回答或已存等价同意;委托选票不是条款同意",
|
|
154
|
+
recorded_acceptance: model.actions.filter(record => record.action_type === action.action_type && record.status === "approved" && record.input_snapshot?.accept_non_guaranteed === true).map(record => ({ notice_version: record.input_snapshot?.notice_version, draft_revision: record.input_snapshot?.draft_revision, accepted: true, recorded_actor_type: record.actor_type, evidence_source: "stored_caller_reported" })),
|
|
155
|
+
action_type: action.action_type, required_fields: requiredFields,
|
|
156
|
+
...(requirementsRemaining ? { requirements_remaining: requirementsRemaining } : {}) },
|
|
157
|
+
...(reviewURL ? { handoff: { url: reviewURL, kind: "booking_review" } } : {}),
|
|
158
|
+
instruction: `${state === "failed" ? `${typeof model.workflow?.failure?.affected_leg === "number" ? `第${model.workflow.failure.affected_leg + 1}段` : ""}报价失败:${RAIL_QUOTE_FAILURE_GUIDANCE[model.workflow?.failure?.reason_code ?? ""] ?? "报价核验未完成。"} 当前无完整报价、无付款入口。按用户意愿有界地重新核验一次同一购买;复制下方当前确认模板,未变条款沿用真实同意,价格/车次/席别/人数/条款变化须确认差异。无票或车次变化时先让用户决定,不能自动换路线。支付尝试后不得修订。 ` : ""}${reviewURL ? "官方确认页见 handoff.url,打开不等于确认;沿用已知人数和席别。" : "当前草稿见 review。"} 先展示 consent_question,只补缺失同意;代码修正且车次、实际席别、人数、价格及条款未变时沿用已有明确确认(真实同意),实质变化只确认差异。复制模板对象,不额外套 input;身份信息只在官方 Checkout 页填写。`,
|
|
159
|
+
next: null,
|
|
160
|
+
interaction: {
|
|
161
|
+
schema_version: "itpay.interaction.v1",
|
|
162
|
+
stage: "consent_required",
|
|
163
|
+
consent_rule: "只有用户本人明确接受当前条款才将 false 改为 true。没有回答时问 consent_question 一次后等待;不要执行模板。Agent 提交只代表转述,不是官方页确认。",
|
|
164
|
+
input_template: {
|
|
165
|
+
command: `itpay services action ${id} --action ${action.action_type} --actor-type agent --status approved --input-json <file> --json`,
|
|
166
|
+
required_input: ["file", ...requiredFields],
|
|
167
|
+
input: review.input_template && typeof review.input_template === "object"
|
|
168
|
+
? review.input_template : serviceInputTemplate(action.input_schema ?? {}, review),
|
|
169
|
+
executable: false,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
recovery: [],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
status: "confirmation_required",
|
|
177
|
+
result: { service_execution_id: id, service_id: execution.service_id, human_action: action },
|
|
178
|
+
instruction: "请展示待确认的内容,请用户明确确认后,按 input_schema 填写 --input 字段。继续同一执行,不重新查询;这一步不是购买确认。",
|
|
179
|
+
next: null,
|
|
180
|
+
interaction: {
|
|
181
|
+
schema_version: "itpay.interaction.v1",
|
|
182
|
+
stage: "confirmation_required",
|
|
183
|
+
input_template: {
|
|
184
|
+
command: `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved${requiredFields.map((field) => ` --input ${field}=<值>`).join("")} --json`,
|
|
185
|
+
required_input: requiredFields,
|
|
186
|
+
executable: false,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
recovery: [],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const recovery = state === "recovery_required" || state === "failed";
|
|
193
|
+
let command = `itpay services next ${id} --json`;
|
|
194
|
+
if (state === "payment")
|
|
195
|
+
command = `itpay services checkout ${id} --json`;
|
|
196
|
+
// input_required: the resume command needs a real file path — expose it as
|
|
197
|
+
// a non-executable input_template, not a fake `next.command`.
|
|
198
|
+
const guidance = railServiceGuidance(execution.service_id, model.workflow_entry?.input_schema);
|
|
199
|
+
const inputTemplate = state === "input_required"
|
|
200
|
+
? operationInputTemplate(executionInputContext(execution), serviceInputTemplate(model.workflow_entry?.input_schema ?? {}, Object.keys(model.workflow?.query_input ?? {}).length ? (model.workflow?.query_input ?? {}) : guidance?.input_example ?? {}), requiredInputFields(model.workflow_entry?.input_schema))
|
|
201
|
+
: undefined;
|
|
202
|
+
if (state === "input_required" && execution.service_id === "itpay-rail-smart" && !guidance) {
|
|
203
|
+
return { status: "contract_unavailable", result: { service_execution_id: id, service_id: execution.service_id }, instruction: "当前锁定合同缺失;读取一次同执行合同后仍缺失就停止,不猜新旧输入结构。", next: { command: `itpay services get ${id} --json`, reason: "获取锁定合同一次" }, recovery: [] };
|
|
204
|
+
}
|
|
205
|
+
const failedStep = recovery ? model.workflow?.failure?.step_id : undefined;
|
|
206
|
+
const failedStepGuidance = failedStep ? WORKFLOW_STEP_GUIDANCE[failedStep] : undefined;
|
|
207
|
+
const failedInvocation = recovery
|
|
208
|
+
? [...model.provider_invocations].reverse().find((item) => typeof item.status === "string" && item.status.startsWith("failed"))
|
|
209
|
+
: undefined;
|
|
210
|
+
const providerErrorCode = typeof failedInvocation?.error_code === "string" && failedInvocation.error_code ? failedInvocation.error_code : undefined;
|
|
211
|
+
const quoteFailure = execution.service_id === "itpay-rail-booking" ? RAIL_QUOTE_FAILURE_GUIDANCE[(model.workflow?.failure?.reason_code === "workflow_failed" ? providerErrorCode : model.workflow?.failure?.reason_code) ?? providerErrorCode ?? ""] : undefined;
|
|
212
|
+
const failureCode = model.workflow?.failure?.reason_code === "workflow_failed" && providerErrorCode && RAIL_QUOTE_FAILURE_GUIDANCE[providerErrorCode] ? providerErrorCode : model.workflow?.failure?.reason_code ?? providerErrorCode ?? model.workflow?.error_code;
|
|
213
|
+
const failedInstruction = recovery
|
|
214
|
+
? failureCode === "http_503"
|
|
215
|
+
? "上游服务返回 HTTP 503,本次查询未完成;这不代表没有班次,也不能认定地点输入有误。保留原地点与约束,当前没有可执行恢复动作,停止读取失败终态;待服务恢复后再按原需求查询。具体基础设施原因未确认。"
|
|
216
|
+
: quoteFailure
|
|
217
|
+
? `${typeof model.workflow?.failure?.affected_leg === "number" ? `第${model.workflow.failure.affected_leg + 1}段报价失败。` : ""}${quoteFailure} 当前整程未取得完整有效报价,没有付款入口;保留同一购买和未变化的选择,当前没有可提交恢复动作,停止;需要用户决定或系统处理,不能靠反复 get/next 重新报价,不能收取其余段款项或重复创建购买。`
|
|
218
|
+
: failedStepGuidance
|
|
219
|
+
? `执行在「${failedStepGuidance.meaning}」步失败${failureCode ? `(错误码:${failureCode})` : ""}:${failedStepGuidance.hint}。先核对原因和当前执行状态,不要盲目重放。`
|
|
220
|
+
: state === "failed"
|
|
221
|
+
? `执行已失败${failureCode ? `(错误码:${failureCode})` : ""}且不可续用;${execution.service_id === "itpay-rail-booking" || paymentVerified ? "核对服务端原因、订单与付款事实" : "保留原地点和约束,核对服务端原因;当前没有可执行恢复则停止,不重复读取终态"}后再决定下一步。`
|
|
222
|
+
: "执行未完成,请按步骤错误处理;不要重建执行或重复调用。"
|
|
223
|
+
: undefined;
|
|
224
|
+
return {
|
|
225
|
+
status: state,
|
|
226
|
+
result: {
|
|
227
|
+
service_execution_id: id,
|
|
228
|
+
service_id: execution.service_id,
|
|
229
|
+
workflow_state: state,
|
|
230
|
+
...(model.workflow ? { current_step: model.workflow.current_step, workflow_revision: model.workflow.revision } : {}),
|
|
231
|
+
...(model.workflow?.failure ? { failure: model.workflow.failure } : {}),
|
|
232
|
+
...(state === "input_required" ? { input_schema: model.workflow_entry.input_schema } : {}),
|
|
233
|
+
...(guidance && state === "input_required" ? { guidance } : {}),
|
|
234
|
+
...(failedStep ? { failed_step: failedStep, ...(failedStepGuidance ? { failed_step_meaning: failedStepGuidance.meaning } : {}) } : {}),
|
|
235
|
+
...(providerErrorCode ? { provider_error_code: providerErrorCode } : {}),
|
|
236
|
+
...(providerErrorCode && !quoteFailure ? { diagnostic_code: providerErrorCode } : {}),
|
|
237
|
+
...(recovery ? { retryable: false } : {}),
|
|
238
|
+
},
|
|
239
|
+
instruction: failedInstruction
|
|
240
|
+
?? (state === "payment"
|
|
241
|
+
? (model.workflow?.human_action?.context?.review
|
|
242
|
+
? "服务已到付款步骤,可直接 Checkout 付款;如需调整席别/座位偏好,先用 confirm_booking 动作修订(会重新报价并锁定新价),完成后再付款。"
|
|
243
|
+
: "服务已到付款步骤,使用现有 Checkout 完成扫码付款。")
|
|
244
|
+
: state === "input_required" && guidance
|
|
245
|
+
? RAIL_ENDPOINT_BRIEF + "按 result.guidance 的字段契约填写输入后继续同一服务执行;不要臆造字段名。"
|
|
246
|
+
: "继续读取同一执行;缺少输入时按服务声明补齐。"),
|
|
247
|
+
next: recovery || inputTemplate ? null : { command, reason: "继续当前流程" },
|
|
248
|
+
...(inputTemplate || (state === "payment" && model.workflow?.human_action?.context?.review) ? {
|
|
249
|
+
interaction: {
|
|
250
|
+
schema_version: "itpay.interaction.v1",
|
|
251
|
+
stage: inputTemplate ? "input_required" : "payment",
|
|
252
|
+
...(inputTemplate ? { input_template: inputTemplate } : {}),
|
|
253
|
+
...(state === "payment" && model.workflow?.human_action?.context?.review ? {
|
|
254
|
+
revise_template: {
|
|
255
|
+
command: `itpay services action ${id} --action ${model.workflow.human_action.action_type} --actor-type human --status approved --input-json <file> --json`,
|
|
256
|
+
required_input: ["file"],
|
|
257
|
+
executable: false,
|
|
258
|
+
},
|
|
259
|
+
} : {}),
|
|
260
|
+
},
|
|
261
|
+
} : {}),
|
|
262
|
+
recovery: recovery && state === "failed" && !["itpay-rail-exact", "itpay-rail-smart"].includes(execution.service_id)
|
|
263
|
+
? [{ command: `itpay services get ${id} --json`, reason: "核对本执行的错误、结果和付款事实" }]
|
|
264
|
+
: [],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { exactTrainRow, exactSmartTemplate, locationConfirmationEnvelope } from "./query.js";
|
|
2
|
+
export function railSavedResultEnvelope(model) {
|
|
3
|
+
const execution = model.execution;
|
|
4
|
+
const currentItems = model.current_result_items ?? [];
|
|
5
|
+
const latestInvocation = model.provider_invocations.at(-1);
|
|
6
|
+
const latestPreview = latestInvocation?.safe_result_preview;
|
|
7
|
+
if (currentItems.length === 0 && latestPreview?.search_status === "LOCATION_CONFIRMATION_REQUIRED" && latestPreview.location_confirmation) {
|
|
8
|
+
const capability = model.capabilities.find((item) => item.capability_id === latestInvocation?.capability_id && item.phase === execution.phase && !item.requires_payment);
|
|
9
|
+
if (capability)
|
|
10
|
+
return locationConfirmationEnvelope(execution.service_execution_id, capability.capability_id, (latestInvocation?.request_summary ?? {}), latestPreview.location_confirmation);
|
|
11
|
+
}
|
|
12
|
+
if (execution.service_id === "itpay-rail-exact" && currentItems.length > 0) {
|
|
13
|
+
const item = currentItems[0];
|
|
14
|
+
const container = (item.safe_payload?.result ?? item.safe_payload);
|
|
15
|
+
const total = typeof container?.catalog_total === "number" ? container.catalog_total :
|
|
16
|
+
Array.isArray(container?.candidates) ? container.candidates.length : 0;
|
|
17
|
+
return {
|
|
18
|
+
status: total > 0 ? "result_ready" : "no_result",
|
|
19
|
+
result: { service_execution_id: execution.service_execution_id, scope: "station_pair",
|
|
20
|
+
resolved_station_pair: container?.resolved_station_pair, total,
|
|
21
|
+
result_item_id: item.service_capability_result_item_id,
|
|
22
|
+
trains: Array.isArray(container?.candidates) ? container.candidates.slice(0, 3).map((value, index) => exactTrainRow(value, execution.service_execution_id, item.service_capability_result_item_id, index)) : [],
|
|
23
|
+
...(exactSmartTemplate(container?.resolved_station_pair, execution.service_execution_id, item.service_capability_result_item_id, execution.public_binding)
|
|
24
|
+
? { smart_template: exactSmartTemplate(container?.resolved_station_pair, execution.service_execution_id, item.service_capability_result_item_id, execution.public_binding) } : {}) },
|
|
25
|
+
instruction: total > 0
|
|
26
|
+
? "直达站对结果已保存。执行 next.command 一次读取紧凑车次行,按用户条件筛选;若只需比较,答复后结束;已有购票委托则读取所选行详情。"
|
|
27
|
+
: "这一已解析站对返回 0 条;不代表全城无车。若原需求需要更广覆盖,可核验另一可信站对或 Smart。",
|
|
28
|
+
next: { command: `itpay services read-result ${execution.service_execution_id} --json`, reason: "读取这一保存站对的紧凑分页;按返回的续页覆盖所需范围" },
|
|
29
|
+
recovery: [],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
export function railSavedDeliveryGuidance(model) {
|
|
35
|
+
const railCatalog = (model.current_result_items ?? []).some((item) => {
|
|
36
|
+
const container = (item.safe_payload?.result ?? item.safe_payload);
|
|
37
|
+
return container?.catalog_page !== undefined || container?.cost_semantics !== undefined;
|
|
38
|
+
});
|
|
39
|
+
const pageRecovery = (model.current_result_items ?? []).flatMap((item) => {
|
|
40
|
+
const container = (item.safe_payload?.result ?? item.safe_payload);
|
|
41
|
+
const catalogPage = container?.catalog_page;
|
|
42
|
+
const nextOffset = catalogPage?.next_offset;
|
|
43
|
+
return typeof nextOffset === "number"
|
|
44
|
+
? [{ command: `itpay services page ${model.execution.service_execution_id} ${item.service_capability_result_item_id} --offset ${nextOffset} --json`, reason: "读取已保存结果的同版本下一页,不重新查询、不消耗额度" }]
|
|
45
|
+
: [];
|
|
46
|
+
});
|
|
47
|
+
const railCostGuidance = "铁路应付只含票价加服务费(quoted_total_minor);地面接驳是单独估算(estimated_ground_minor),两者相加是已知估算(estimated_door_to_door_minor),不是收款额。";
|
|
48
|
+
return { instruction: railCatalog ? railCostGuidance : "", recovery: pageRecovery };
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function railShouldWait(model) {
|
|
2
|
+
if (model.rail_booking)
|
|
3
|
+
return false;
|
|
4
|
+
const plan = model.rail_planning;
|
|
5
|
+
if (!plan)
|
|
6
|
+
return undefined;
|
|
7
|
+
return !plan.recommendation && !(plan.alternatives?.length) &&
|
|
8
|
+
!["complete", "paused", "failed", "cancelled", "expired"].includes(plan.search?.expansion_status ?? "") &&
|
|
9
|
+
!plan.available_actions?.some(action => ["select_journey", "confirm_location", "expand_search"].includes(action.type));
|
|
10
|
+
}
|
|
11
|
+
export function railPollAfter(model) { return model.rail_planning?.search?.poll_after_ms ?? 0; }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { railAdapterFor } from "./rail/adapter.js";
|
|
2
|
+
import { schemaAdapter, unsupportedAdapter } from "./schema.js";
|
|
3
|
+
// Published identities are frozen compatibility contracts. Fields named
|
|
4
|
+
// endpoints/review/search, titles and ID prefixes never choose a business adapter.
|
|
5
|
+
const legacyRailContracts = new Map([
|
|
6
|
+
["itpay-rail-exact", new Set(["scv_itpay_rail_exact_v1"])],
|
|
7
|
+
["itpay-rail-smart", new Set(["scv_itpay_rail_smart_v1", "scv_itpay_rail_smart_v2", "scv_itpay_rail_smart_v3", "scv_itpay_rail_smart_v4", "scv_itpay_rail_smart_v5", "scv_itpay_rail_smart_v6"])],
|
|
8
|
+
["itpay-rail-booking", new Set(["scv_itpay_rail_booking_v1", "scv_itpay_rail_booking_v2", "scv_itpay_rail_booking_v3", "scv_itpay_rail_booking_v4"])],
|
|
9
|
+
]);
|
|
10
|
+
export function serviceAdapterKey(execution) {
|
|
11
|
+
const binding = execution.public_binding;
|
|
12
|
+
if (binding) {
|
|
13
|
+
if (binding.adapter_key === "schema.v1")
|
|
14
|
+
return "schema.v1";
|
|
15
|
+
if (binding.adapter_key === "rail.v1" && binding.service_definition_version === "scv_itpay_rail_public_v1")
|
|
16
|
+
return "rail.v1";
|
|
17
|
+
return "unsupported";
|
|
18
|
+
}
|
|
19
|
+
const versions = legacyRailContracts.get(execution.service_id);
|
|
20
|
+
if (versions)
|
|
21
|
+
return versions.has(execution.service_contract_version_id) ? "rail.v1" : "unsupported";
|
|
22
|
+
return "schema.v1";
|
|
23
|
+
}
|
|
24
|
+
export function serviceClientFeatures() {
|
|
25
|
+
// Negotiation advertises supported wire formats, not a service identity.
|
|
26
|
+
return ["rail.progressive.v2"];
|
|
27
|
+
}
|
|
28
|
+
export function serviceAdapter(execution) {
|
|
29
|
+
const key = serviceAdapterKey(execution);
|
|
30
|
+
return key === "rail.v1" ? railAdapterFor(execution) : key === "schema.v1" ? schemaAdapter : unsupportedAdapter;
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { serviceInputTemplate } from "./shared/input.js";
|
|
2
|
+
import { workflowEnvelope } from "./shared/workflow.js";
|
|
3
|
+
import { CommandContractError } from "./shared/envelope.js";
|
|
4
|
+
export const schemaAdapter = {
|
|
5
|
+
entryInstruction: "根据服务声明填写输入,然后继续同一服务执行。",
|
|
6
|
+
supported: true, inputTemplate: serviceInputTemplate,
|
|
7
|
+
inputInstruction: "Agent按当前锁定模板自行修正错误字段和JSON层级,保留用户原需求和真实选择;不要向用户询问技术字段。只有真实业务歧义才问人;使用返回的执行身份恢复,未创建时用原服务。",
|
|
8
|
+
guidance: () => undefined, requiresGuidance: () => false,
|
|
9
|
+
overviewInstruction: () => "尚未创建执行。按用户请求选择返回的操作并填写完整输入;不猜未声明的操作。",
|
|
10
|
+
locationInstruction: () => "", invocation: () => undefined,
|
|
11
|
+
shouldWait: () => undefined, pollAfter: () => 0,
|
|
12
|
+
preservesTerminalRecovery: () => false, projectState: () => null,
|
|
13
|
+
workflow: workflowEnvelope, savedResult: () => null, deliveryGuidance: () => null, extraFacts: () => ({}),
|
|
14
|
+
runError: () => { }, actionError: () => { },
|
|
15
|
+
readResult: async (_backend, _model, id, options) => {
|
|
16
|
+
if (options.snapshot || options.journey)
|
|
17
|
+
throw new CommandContractError("result_selector_unsupported", "locked service does not support these result selectors", "保留同一执行;使用当前服务返回的结果读取动作,不猜其他业务选择器。", [{ command: `itpay services next ${id} --json`, reason: "读取当前服务声明的动作" }]);
|
|
18
|
+
return undefined;
|
|
19
|
+
}, renderPage: () => false, checkout: async () => ({}),
|
|
20
|
+
};
|
|
21
|
+
export const unsupportedAdapter = { ...schemaAdapter, supported: false, workflow: () => null, readResult: async () => { throw new CommandContractError("presentation_contract_unavailable", "locked contract unsupported", "保留原执行与已有结果并停止,使用兼容CLI读取;不猜模式。", []); } };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { HttpError } from "../../client/http.js";
|
|
2
|
+
import { resolveOutput } from "../../render/sink.js";
|
|
3
|
+
import { declaredAgentType, qualifyItPayCommand } from "../../state/agent_type.js";
|
|
4
|
+
import { qualifyBackendCommand } from "../../state/config.js";
|
|
5
|
+
export function isTerminalServiceExecutionStatus(status) {
|
|
6
|
+
return status === "failed" || status === "refunded" || status === "cancelled";
|
|
7
|
+
}
|
|
8
|
+
export function appendFeedbackPostmortemInstruction(instruction, outcome) {
|
|
9
|
+
const invitation = outcome === "delivered"
|
|
10
|
+
? "结果解释完毕后,可按需为同一订单提交一次安全服务复盘;用户评分和评论均可选,只在用户明确给出时忠实附上,不得猜分或上传对话、交付正文、联系方式和原始日志。"
|
|
11
|
+
: "先完成同一订单的恢复和退款权利解释;处理清楚后,可按需为同一订单提交一次安全服务复盘。只附上用户明确表达的评分或卡壳点,不得猜测。";
|
|
12
|
+
return `${instruction} ${invitation}`;
|
|
13
|
+
}
|
|
14
|
+
export class CommandContractError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
instruction;
|
|
17
|
+
recovery;
|
|
18
|
+
interaction;
|
|
19
|
+
result;
|
|
20
|
+
constructor(code, message, instruction, recovery, interaction, result) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.instruction = instruction;
|
|
24
|
+
this.recovery = recovery;
|
|
25
|
+
this.interaction = interaction;
|
|
26
|
+
this.result = result;
|
|
27
|
+
this.name = "CommandContractError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Domain projections and pages own byte limits; serialization never truncates facts.
|
|
31
|
+
export function plainValueLines(value, label = "", depth = 0) {
|
|
32
|
+
const indent = " ".repeat(depth);
|
|
33
|
+
if (value === null || typeof value !== "object")
|
|
34
|
+
return [`${indent}${label ? `${label}: ` : ""}${String(value ?? "unknown")}`];
|
|
35
|
+
const lines = label ? [`${indent}${label}:`] : [];
|
|
36
|
+
const entries = Array.isArray(value) ? value.map((v, i) => [String(i + 1), v]) : Object.entries(value);
|
|
37
|
+
for (const [key, child] of entries)
|
|
38
|
+
lines.push(...plainValueLines(child, key, depth + (label ? 1 : 0)));
|
|
39
|
+
if (!entries.length)
|
|
40
|
+
lines.push(`${indent} (无记录)`);
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
export function shellArgument(value) {
|
|
44
|
+
if (/^[\p{L}\p{N}._:=/-]+$/u.test(value))
|
|
45
|
+
return value;
|
|
46
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
47
|
+
}
|
|
48
|
+
export function writeCommandEnvelope(value, options = {}) {
|
|
49
|
+
const out = resolveOutput(options.output);
|
|
50
|
+
const agentType = options.agentType ?? declaredAgentType();
|
|
51
|
+
const qualified = qualifyEnvelope(value, agentType);
|
|
52
|
+
if (options.jsonOutput) {
|
|
53
|
+
const pretty = JSON.stringify(qualified, null, 2) + "\n";
|
|
54
|
+
out(pretty);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
out(`${qualified.status}\n`);
|
|
58
|
+
const facts = "error" in qualified ? qualified.error : qualified.result ?? {};
|
|
59
|
+
if (options.plainResult) {
|
|
60
|
+
for (const line of options.plainResult) {
|
|
61
|
+
const command = line.match(/^(\s*(?:command|reader|entry): )(itpay .*)$/);
|
|
62
|
+
out(`${command ? command[1] + qualifyBackendCommand(qualifyItPayCommand(command[2], agentType)) : line}\n`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
for (const [key, fact] of Object.entries(facts)) {
|
|
67
|
+
for (const line of plainValueLines(fact, key))
|
|
68
|
+
out(`${line}\n`);
|
|
69
|
+
}
|
|
70
|
+
if ("error" in qualified && qualified.result) {
|
|
71
|
+
for (const [key, fact] of Object.entries(qualified.result)) {
|
|
72
|
+
for (const line of plainValueLines(fact, key))
|
|
73
|
+
out(`${line}\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if ("handoff" in qualified && qualified.handoff) {
|
|
78
|
+
for (const [key, fact] of Object.entries(qualified.handoff)) {
|
|
79
|
+
for (const line of plainValueLines(fact, `handoff.${key}`))
|
|
80
|
+
out(`${line}\n`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
out(`instruction: ${qualified.instruction}\n`);
|
|
84
|
+
if (qualified.next)
|
|
85
|
+
out(`next: ${qualified.next.command}\n when: ${qualified.next.reason}\n`);
|
|
86
|
+
if ("interaction" in qualified && qualified.interaction) {
|
|
87
|
+
for (const line of plainValueLines(qualified.interaction, "interaction"))
|
|
88
|
+
out(`${line}\n`);
|
|
89
|
+
}
|
|
90
|
+
if ("communication" in qualified && qualified.communication) {
|
|
91
|
+
for (const line of plainValueLines(qualified.communication, "communication"))
|
|
92
|
+
out(`${line}\n`);
|
|
93
|
+
}
|
|
94
|
+
if (qualified.recovery.length > 0) {
|
|
95
|
+
out("recovery:\n");
|
|
96
|
+
for (const action of qualified.recovery) {
|
|
97
|
+
out(` - ${action.command}\n`);
|
|
98
|
+
out(` reason: ${action.reason}\n`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function qualifyEnvelope(value, agentType) {
|
|
103
|
+
return {
|
|
104
|
+
...value,
|
|
105
|
+
result: qualifyCommandsDeep(value.result, agentType),
|
|
106
|
+
...("handoff" in value && value.handoff ? { handoff: qualifyCommandsDeep(value.handoff, agentType) } : {}),
|
|
107
|
+
...("interaction" in value && value.interaction ? { interaction: qualifyCommandsDeep(value.interaction, agentType) } : {}),
|
|
108
|
+
...("communication" in value && value.communication ? { communication: qualifyCommandsDeep(value.communication, agentType) } : {}),
|
|
109
|
+
next: value.next ? { ...value.next, command: qualifyBackendCommand(qualifyItPayCommand(value.next.command, agentType)) } : null,
|
|
110
|
+
recovery: value.recovery.map((action) => ({
|
|
111
|
+
...action,
|
|
112
|
+
command: qualifyBackendCommand(qualifyItPayCommand(action.command, agentType)),
|
|
113
|
+
})),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Every `itpay ` command string anywhere in the envelope — result fields,
|
|
117
|
+
// interaction recipes/templates, available_actions — keeps the same Agent
|
|
118
|
+
// Type / Backend qualification as top-level next/recovery. Instruction prose
|
|
119
|
+
// never starts with "itpay " so prose stays untouched.
|
|
120
|
+
function qualifyCommandsDeep(value, agentType) {
|
|
121
|
+
if (typeof value === "string") {
|
|
122
|
+
return value.startsWith("itpay ")
|
|
123
|
+
? qualifyBackendCommand(qualifyItPayCommand(value, agentType))
|
|
124
|
+
: value;
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(value))
|
|
127
|
+
return value.map((item) => qualifyCommandsDeep(item, agentType));
|
|
128
|
+
if (value && typeof value === "object") {
|
|
129
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, qualifyCommandsDeep(item, agentType)]));
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
export function errorRecoveryActions(error) {
|
|
134
|
+
if (!(error instanceof HttpError))
|
|
135
|
+
return [];
|
|
136
|
+
if (error.code === "agent_identity_required") {
|
|
137
|
+
return [{ id: "inspect_agent_setup", label: "Inspect supported Agent Type setup", command: "itpay install --json" }];
|
|
138
|
+
}
|
|
139
|
+
if (error.code === "agent_device_session_required") {
|
|
140
|
+
return [{
|
|
141
|
+
id: "read_agent_session_rules",
|
|
142
|
+
label: "Read ItPay identity and session recovery rules",
|
|
143
|
+
command: "itpay skill show itpay --json",
|
|
144
|
+
reason: "The CLI already attempted one automatic session renewal; do not rotate identity or loop retries.",
|
|
145
|
+
}];
|
|
146
|
+
}
|
|
147
|
+
if (error.code === "quota_exhausted" || error.code === "checkout_required") {
|
|
148
|
+
return [{
|
|
149
|
+
id: "inspect_service_execution",
|
|
150
|
+
label: "Inspect Service Execution before checkout",
|
|
151
|
+
command: "itpay services list --json",
|
|
152
|
+
}];
|
|
153
|
+
}
|
|
154
|
+
if (error.code === "cart_item_locked") {
|
|
155
|
+
return [{ id: "show_cart", label: "Inspect the locked cart", command: "itpay cart show --json",
|
|
156
|
+
reason: "读取已有购物内容和订单锁,不创建替代 checkout。" }];
|
|
157
|
+
}
|
|
158
|
+
if (error.status === 404) {
|
|
159
|
+
return [{
|
|
160
|
+
id: "recover_service_executions",
|
|
161
|
+
label: "List visible Service Executions and follow their next instruction",
|
|
162
|
+
command: "itpay services list",
|
|
163
|
+
}];
|
|
164
|
+
}
|
|
165
|
+
if (error.status === 502 || error.status === 503 || error.status === 504) {
|
|
166
|
+
return [{
|
|
167
|
+
id: "retry_after_backend_recovers",
|
|
168
|
+
label: "Retry after the selected official Backend is reachable",
|
|
169
|
+
command: "itpay readyz",
|
|
170
|
+
}];
|
|
171
|
+
}
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
export function printErrorRecovery(error, output) {
|
|
175
|
+
const recovery = errorRecoveryActions(error);
|
|
176
|
+
if (recovery.length === 0)
|
|
177
|
+
return;
|
|
178
|
+
const out = resolveOutput(output);
|
|
179
|
+
out("recovery:\n");
|
|
180
|
+
for (const action of recovery) {
|
|
181
|
+
out(` - ${action.label}\n`);
|
|
182
|
+
out(` ${action.command}\n`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { shellArgument } from "./envelope.js";
|
|
2
|
+
import { requiredInputFields } from "./platform.js";
|
|
3
|
+
// Generate only declared required fields and preserve safe known query facts.
|
|
4
|
+
// Authorization and passenger identity are never synthesized into templates.
|
|
5
|
+
export function serviceInputTemplate(schema, known = {}) {
|
|
6
|
+
const properties = schema.properties;
|
|
7
|
+
const result = {};
|
|
8
|
+
for (const field of new Set([...requiredInputFields(schema), ...Object.keys(known).filter(field => properties?.[field] !== undefined)])) {
|
|
9
|
+
const rule = properties?.[field] ?? {};
|
|
10
|
+
if (rule.type === "object") {
|
|
11
|
+
const value = known[field];
|
|
12
|
+
result[field] = serviceInputTemplate(rule, value && typeof value === "object" && !Array.isArray(value) ? value : {});
|
|
13
|
+
}
|
|
14
|
+
else if (known[field] !== undefined && (rule.type === undefined || rule.type === "array" && Array.isArray(known[field]) || rule.type === "string" && typeof known[field] === "string" || rule.type === "boolean" && typeof known[field] === "boolean" || ["integer", "number"].includes(String(rule.type)) && typeof known[field] === "number"))
|
|
15
|
+
result[field] = known[field];
|
|
16
|
+
else if (rule.type === "boolean")
|
|
17
|
+
result[field] = false;
|
|
18
|
+
else if (rule.const !== undefined)
|
|
19
|
+
result[field] = rule.const;
|
|
20
|
+
else if (rule.type === "array")
|
|
21
|
+
result[field] = [];
|
|
22
|
+
else
|
|
23
|
+
result[field] = `<${field}>`;
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
export function operationInputTemplate(context, input, required = []) {
|
|
28
|
+
return {
|
|
29
|
+
command: `itpay services run ${shellArgument(context.serviceID)}${context.operationID ? ` --operation ${shellArgument(context.operationID)}` : ""}${context.executionID ? ` --execution ${shellArgument(context.executionID)}` : ""} --input-json <file> --json`,
|
|
30
|
+
input: context.operationID ? { input, ...(context.continuation ? { continuation: context.continuation } : {}) } : input,
|
|
31
|
+
required_input: ["file", ...required], executable: false,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function executionInputContext(execution) {
|
|
35
|
+
const binding = execution.public_binding;
|
|
36
|
+
return { serviceID: binding?.public_service_id ?? execution.service_id, executionID: execution.service_execution_id,
|
|
37
|
+
...(binding ? { operationID: binding.operation_id } : {}),
|
|
38
|
+
...(binding?.source_execution_id && binding.source_snapshot_id ? { continuation: { service_execution_id: binding.source_execution_id, snapshot_id: binding.source_snapshot_id } } : {}) };
|
|
39
|
+
}
|