@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,153 @@
|
|
|
1
|
+
// Deterministic PresentationResolver (spec 03 §5): business state first, then
|
|
2
|
+
// declared viewer/executor capabilities, then the user's explicit choice and
|
|
3
|
+
// failure history. Output is one recommended route plus at most two feasible
|
|
4
|
+
// alternatives — never a parallel burst of open + send.
|
|
5
|
+
//
|
|
6
|
+
// Routes are abstract kinds; the caller maps them to concrete handoff fields
|
|
7
|
+
// (url, card image, qr). "unknown" capability answers are never treated as
|
|
8
|
+
// "yes", and a route that already failed is not retried without a new reason.
|
|
9
|
+
const yes = (value) => value === "yes";
|
|
10
|
+
export function resolvePresentation(input) {
|
|
11
|
+
const blocker = businessBlocker(input.business);
|
|
12
|
+
if (blocker) {
|
|
13
|
+
return {
|
|
14
|
+
blocker,
|
|
15
|
+
recommended: "none",
|
|
16
|
+
alternatives: [],
|
|
17
|
+
routes: [],
|
|
18
|
+
communication: communicationForBlocker(blocker),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const caps = input.capabilities;
|
|
22
|
+
const local = input.executor_locality === "same_device";
|
|
23
|
+
const routes = [];
|
|
24
|
+
if (input.viewer === "desktop") {
|
|
25
|
+
if (yes(caps.user_visible_browser))
|
|
26
|
+
routes.push("open_embedded_browser");
|
|
27
|
+
if (local && yes(caps.system_browser))
|
|
28
|
+
routes.push("open_system_browser");
|
|
29
|
+
if (yes(caps.clickable_https))
|
|
30
|
+
routes.push("show_link");
|
|
31
|
+
if (yes(caps.image_visible))
|
|
32
|
+
routes.push("show_official_image");
|
|
33
|
+
if (yes(caps.user_visible_terminal) && yes(caps.other_device_scan))
|
|
34
|
+
routes.push("show_terminal_qr");
|
|
35
|
+
}
|
|
36
|
+
else if (input.viewer === "mobile") {
|
|
37
|
+
if (yes(caps.native_url_button))
|
|
38
|
+
routes.push("show_mobile_button");
|
|
39
|
+
if (yes(caps.clickable_https))
|
|
40
|
+
routes.push("show_mobile_link");
|
|
41
|
+
if (yes(caps.user_visible_browser))
|
|
42
|
+
routes.push("open_user_browser");
|
|
43
|
+
if (local && yes(caps.system_browser))
|
|
44
|
+
routes.push("open_system_browser");
|
|
45
|
+
// QR never targets the same phone the human is holding.
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
if (yes(caps.clickable_https))
|
|
49
|
+
routes.push("show_link");
|
|
50
|
+
if (routes.length === 0 && yes(caps.image_visible))
|
|
51
|
+
routes.push("ask_viewer_device");
|
|
52
|
+
}
|
|
53
|
+
// Relay is a consented fallback, not a presentation route — it only appears
|
|
54
|
+
// when an issued, verified contact option exists.
|
|
55
|
+
if (input.verified_phone_option === true)
|
|
56
|
+
routes.push("request_sms_consent");
|
|
57
|
+
if (input.verified_email_option === true)
|
|
58
|
+
routes.push("request_email_consent");
|
|
59
|
+
routes.push("show_copyable_entry");
|
|
60
|
+
const failed = new Set(input.failed_routes ?? []);
|
|
61
|
+
const feasible = routes.filter((route) => !failed.has(route));
|
|
62
|
+
const chosen = input.explicit_choice;
|
|
63
|
+
if (chosen && feasible.includes(chosen)) {
|
|
64
|
+
feasible.splice(feasible.indexOf(chosen), 1);
|
|
65
|
+
feasible.unshift(chosen);
|
|
66
|
+
}
|
|
67
|
+
const ranked = feasible.length > 0 ? feasible : ["show_copyable_entry"];
|
|
68
|
+
const [recommended, ...alternatives] = ranked;
|
|
69
|
+
return {
|
|
70
|
+
recommended: recommended,
|
|
71
|
+
alternatives: alternatives.slice(0, 2),
|
|
72
|
+
routes: ranked.slice(0, 3),
|
|
73
|
+
communication: {
|
|
74
|
+
tell: tellForRoute(recommended),
|
|
75
|
+
wait_for: "人完成官方页面上的确认/付款;dispatch 成功不等于已付款",
|
|
76
|
+
must_convey: [
|
|
77
|
+
"真实金额与订单归属以 owner 返回为准",
|
|
78
|
+
"已发起打开不代表已显示或已付款",
|
|
79
|
+
...(input.business.amount_minor === undefined ? ["金额尚未确认,不称锁价或最终价"] : []),
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function businessBlocker(business) {
|
|
85
|
+
if (business.commerce_policy === "blocked")
|
|
86
|
+
return "policy_blocked";
|
|
87
|
+
if (business.commerce_policy === "review")
|
|
88
|
+
return "policy_review_required";
|
|
89
|
+
if (business.payment_state !== "unpaid")
|
|
90
|
+
return "read_same_order";
|
|
91
|
+
if (business.quote_valid !== true)
|
|
92
|
+
return "recover_expired_quote";
|
|
93
|
+
if (typeof business.amount_minor !== "number" || business.amount_minor <= 0)
|
|
94
|
+
return "read_same_order";
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
function communicationForBlocker(blocker) {
|
|
98
|
+
switch (blocker) {
|
|
99
|
+
case "policy_blocked":
|
|
100
|
+
return { tell: "该服务在当前宿主/准入策略下不可用,不绕外链或消息逃避。", wait_for: "用户更换获准渠道", must_convey: ["技术与准入分开说明"] };
|
|
101
|
+
case "policy_review_required":
|
|
102
|
+
return { tell: "当前准入策略要求复核,不发起展示动作。", wait_for: "准入复核结果", must_convey: ["不是业务失败"] };
|
|
103
|
+
case "read_same_order":
|
|
104
|
+
return { tell: "该订单不处于待付款状态;先读取原订单,不展示付款入口、不新建支付动作。", wait_for: "原订单状态", must_convey: ["用户声称已付仍须核对原单"] };
|
|
105
|
+
case "recover_expired_quote":
|
|
106
|
+
return { tell: "报价已失效或过期;读取原执行由 owner 重新核价,不向人收取旧金额。", wait_for: "新的 owner 报价", must_convey: ["旧二维码/入口不再有效"] };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function tellForRoute(route) {
|
|
110
|
+
switch (route) {
|
|
111
|
+
case "open_embedded_browser":
|
|
112
|
+
case "open_system_browser":
|
|
113
|
+
case "open_user_browser":
|
|
114
|
+
return "已发起打开官方页面;若用户未看到,换一种方式,仍是同一订单。";
|
|
115
|
+
case "show_link":
|
|
116
|
+
case "show_mobile_link":
|
|
117
|
+
return "把官方链接作为可点击入口交给用户。";
|
|
118
|
+
case "show_mobile_button":
|
|
119
|
+
return "展示原生 URL 按钮直达官方页。";
|
|
120
|
+
case "show_official_image":
|
|
121
|
+
return "展示官方图片入口(另一台设备用)。";
|
|
122
|
+
case "show_terminal_qr":
|
|
123
|
+
return "展示二维码供另一台设备扫码。";
|
|
124
|
+
case "request_sms_consent":
|
|
125
|
+
return "建议把订单入口发到已验证的手机尾号;征得明确同意后发送一条。";
|
|
126
|
+
case "request_email_consent":
|
|
127
|
+
return "建议把订单入口发到已验证的邮箱;征得明确同意后发送一封。";
|
|
128
|
+
case "ask_viewer_device":
|
|
129
|
+
return "无法判断用户在看哪种设备;最多澄清一次设备类型。";
|
|
130
|
+
case "show_copyable_entry":
|
|
131
|
+
return "给出可复制的官方入口并说明如何打开。";
|
|
132
|
+
default:
|
|
133
|
+
return "当前无可行展示方式;只说明状态,等待用户给出可用渠道。";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export function resolveRelay(input) {
|
|
137
|
+
if (!input.backendCapability) {
|
|
138
|
+
return {
|
|
139
|
+
kind: "unavailable",
|
|
140
|
+
reason: "当前 Backend 未提供消息转发(relay)能力;改用官方链接或二维码呈现,不得伪造发送结果。",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (!input.relayOption) {
|
|
144
|
+
return { kind: "invalid", reason: "--relay-option 必须是服务端已签发的脱敏联系人引用。" };
|
|
145
|
+
}
|
|
146
|
+
if (!input.requestKey || !input.requestKey.trim()) {
|
|
147
|
+
return { kind: "invalid", reason: "--request-key 是必填的稳定幂等键。" };
|
|
148
|
+
}
|
|
149
|
+
if (!input.confirmRelay) {
|
|
150
|
+
return { kind: "needs_consent" };
|
|
151
|
+
}
|
|
152
|
+
return { kind: "unavailable", reason: "relay 后端未启用。" };
|
|
153
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../services/rail/catalog.js";
|
|
@@ -8,7 +8,7 @@ export async function runReadyz(backend, options = {}) {
|
|
|
8
8
|
status: response.status,
|
|
9
9
|
result: { backend: "available", backend_url: backendURL, environment, ...(options.agentType ? { agent_type: options.agentType } : {}) },
|
|
10
10
|
instruction: environment === "development"
|
|
11
|
-
?
|
|
11
|
+
? `ItPay 开发环境 ${backendURL} 可用。只证明后端存活。尚未读此次包 Skill 时完整读取;已读同版本则无需重复,继续设备状态检查;后续必须执行返回的完整命令并保持同一 Backend。`
|
|
12
12
|
: "ItPay 可用。只证明后端存活。尚未读此次包 Skill 时完整读取;已读同版本则无需重复,继续设备状态检查;不自动登录或开始业务。",
|
|
13
13
|
next: { command: "itpay skill show itpay --json", reason: "加载完整操作与安全规则" },
|
|
14
14
|
recovery: [],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { refundStateEnvelope } from "../services/shared/refund.js";
|
|
1
2
|
import { operationID } from "../state/config.js";
|
|
2
3
|
import { formatMoney } from "../render/output.js";
|
|
3
4
|
import { resolveOutput } from "../render/sink.js";
|
|
@@ -12,46 +13,6 @@ export async function runRefund(backend, config, options) {
|
|
|
12
13
|
plainResult: Object.entries(envelope.result).map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`),
|
|
13
14
|
});
|
|
14
15
|
}
|
|
15
|
-
function refundStateEnvelope(refund, status) {
|
|
16
|
-
const terminal = ["succeeded", "failed", "cancelled", "rejected"].includes(refund.status);
|
|
17
|
-
let instruction = refund.decision_mode === "manual"
|
|
18
|
-
? "先告诉用户退款已进入人工审核,原交付保持冻结;人工审核不等于拒绝,等待服务器决定,不要重复申请或承诺结果。"
|
|
19
|
-
: "先告诉用户退款申请已经记录,原交付已冻结;自动路径表示系统会继续处理,但只有最终 succeeded 才能确认退款成功。然后只跟踪同一退款,不要重复申请、reveal、授权或读取结果。";
|
|
20
|
-
if (!refund.access_locked)
|
|
21
|
-
instruction = "先告诉用户退款当前没有锁定交付;按服务器事实解释当前状态,不要自行推断退款结果、到账时间或交付资格。";
|
|
22
|
-
if (refund.status === "succeeded")
|
|
23
|
-
instruction = "先告诉用户退款已由 ItPay 确认成功,原交付永久关闭;不需要继续跟踪或重复申请。";
|
|
24
|
-
if (refund.status === "cancelled" || refund.status === "rejected")
|
|
25
|
-
instruction = "先告诉用户退款没有执行,交付资格可以恢复;旧读取授权不会复活,需要用户重新授权。不要把取消或拒绝说成退款成功。";
|
|
26
|
-
if (refund.status === "failed") {
|
|
27
|
-
if (refund.failure_class === "known_no_effect")
|
|
28
|
-
instruction = "先告诉用户本次退款请求确认未发送,不能说退款已成功;Agent 不自行重试,请用户等待平台管理员决定是否重新执行。";
|
|
29
|
-
else if (refund.failure_class === "retryable")
|
|
30
|
-
instruction = "先告诉用户渠道明确返回可重试失败,但 Agent 不会自行重试或重复申请;请用户等待平台管理员处理。";
|
|
31
|
-
else if (refund.failure_class === "outcome_unknown")
|
|
32
|
-
instruction = "先告诉用户退款渠道结果未知,原交付继续锁定且必须先由平台对账;禁止重试、重复申请或承诺退款结果。";
|
|
33
|
-
else if (refund.failure_class === "permanent")
|
|
34
|
-
instruction = "先告诉用户渠道明确拒绝本次退款,当前不能承诺退款成功;不要重试,请用户联系平台支持。";
|
|
35
|
-
else
|
|
36
|
-
instruction = "先告诉用户退款没有正常完成,当前不能承诺退款成功;不要重试或重复申请,请用户联系平台支持。";
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
39
|
-
status,
|
|
40
|
-
result: {
|
|
41
|
-
refund_request_id: refund.refund_request_id,
|
|
42
|
-
order_id: refund.order_id,
|
|
43
|
-
decision_mode: refund.decision_mode,
|
|
44
|
-
refund_status: refund.status,
|
|
45
|
-
consumption_state: refund.consumption_state,
|
|
46
|
-
...(refund.failure_class ? { failure_class: refund.failure_class } : {}),
|
|
47
|
-
access_locked: refund.access_locked,
|
|
48
|
-
can_cancel: refund.can_cancel,
|
|
49
|
-
},
|
|
50
|
-
instruction,
|
|
51
|
-
next: terminal ? null : { command: `itpay refund watch ${refund.refund_request_id} --json`, reason: "跟踪同一退款" },
|
|
52
|
-
recovery: [],
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
16
|
export async function runListRefunds(backend, options) {
|
|
56
17
|
const out = resolveOutput(options.output);
|
|
57
18
|
const response = await backend.listOrderRefunds(options.orderID);
|
|
@@ -73,7 +34,7 @@ export async function runListRefunds(backend, options) {
|
|
|
73
34
|
: "该订单没有退款记录;确认用户确实要求退款后再创建。",
|
|
74
35
|
next: selected
|
|
75
36
|
? { command: `itpay refund get ${selected.refund_request_id} --json`, reason: active ? "读取活跃退款" : "读取最新退款" }
|
|
76
|
-
:
|
|
37
|
+
: null,
|
|
77
38
|
recovery: [],
|
|
78
39
|
};
|
|
79
40
|
writeCommandEnvelope(envelope, {
|
|
@@ -96,13 +57,13 @@ export async function runGetRefund(backend, refundID, options = {}) {
|
|
|
96
57
|
export async function runCancelRefund(backend, refundID, reason, options = {}) {
|
|
97
58
|
const refund = await backend.cancelRefund(refundID, reason?.trim() || "buyer_cancelled");
|
|
98
59
|
const envelope = {
|
|
99
|
-
status:
|
|
60
|
+
status: refund.status,
|
|
100
61
|
result: {
|
|
101
62
|
refund_request_id: refund.refund_request_id,
|
|
102
63
|
order_id: refund.order_id,
|
|
103
64
|
access_locked: refund.access_locked,
|
|
104
65
|
},
|
|
105
|
-
instruction: "
|
|
66
|
+
instruction: refund.status === "cancelled" ? "Backend确认退款已取消;如需交付,重新进入原订单核对当前权限,旧grant不自动复活;后端要求时取得新的授权。" : "Backend未确认取消;按返回的退款权威状态核对原订单,不重复提交取消。",
|
|
106
67
|
next: { command: `itpay order ${refund.order_id} --json`, reason: "确认订单访问状态" },
|
|
107
68
|
recovery: [],
|
|
108
69
|
};
|
|
@@ -110,36 +71,30 @@ export async function runCancelRefund(backend, refundID, reason, options = {}) {
|
|
|
110
71
|
}
|
|
111
72
|
export async function runWatchRefund(backend, refundID, options = {}) {
|
|
112
73
|
const intervalSeconds = options.intervalSeconds ?? 2;
|
|
113
|
-
const timeoutSeconds = options.timeoutSeconds ??
|
|
74
|
+
const timeoutSeconds = options.timeoutSeconds ?? 20;
|
|
114
75
|
if (!Number.isFinite(intervalSeconds) || intervalSeconds < 1)
|
|
115
76
|
throw new Error("--interval must be at least 1 second");
|
|
116
77
|
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)
|
|
117
78
|
throw new Error("--timeout must be a positive number");
|
|
118
79
|
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
119
80
|
let refund;
|
|
81
|
+
let initialState;
|
|
120
82
|
for (;;) {
|
|
121
83
|
refund = await backend.getRefund(refundID);
|
|
122
|
-
|
|
84
|
+
const state = JSON.stringify([refund.status, refund.decision_mode, refund.failure_class, refund.access_locked, refund.can_cancel]);
|
|
85
|
+
if (["succeeded", "failed", "cancelled", "rejected"].includes(refund.status) || refund.decision_mode === "manual" || refund.failure_class === "outcome_unknown" || (initialState !== undefined && state !== initialState)) {
|
|
123
86
|
writeRefundEnvelope(refundStateEnvelope(refund, "watch_complete"), options);
|
|
124
87
|
return;
|
|
125
88
|
}
|
|
89
|
+
initialState ??= state;
|
|
126
90
|
const remaining = deadline - Date.now();
|
|
127
91
|
if (remaining <= 0)
|
|
128
92
|
break;
|
|
129
93
|
await new Promise((resolve) => setTimeout(resolve, Math.min(intervalSeconds * 1000, remaining)));
|
|
130
94
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
refund_request_id: refund.refund_request_id,
|
|
135
|
-
last_status: refund.status,
|
|
136
|
-
access_locked: refund.access_locked,
|
|
137
|
-
can_cancel: refund.can_cancel,
|
|
138
|
-
},
|
|
139
|
-
instruction: "先告诉用户退款仍在处理,Timeout 只表示本次等待结束,并不表示退款失败;稍后继续跟踪同一退款,不要重复申请或承诺结果。",
|
|
140
|
-
next: { command: `itpay refund watch ${refund.refund_request_id} --json`, reason: "恢复轮询" },
|
|
141
|
-
recovery: [],
|
|
142
|
-
}, options);
|
|
95
|
+
const latest = refundStateEnvelope(refund, "watch_timeout");
|
|
96
|
+
writeRefundEnvelope({ ...latest, result: { ...latest.result, last_status: refund.status, wait_ended: true },
|
|
97
|
+
instruction: `本次等待结束,并不表示退款失败。${latest.instruction} 稍后按需读取同一退款,不自动重启等待,不要重复申请。` }, options);
|
|
143
98
|
}
|
|
144
99
|
function writeRefundEnvelope(envelope, options) {
|
|
145
100
|
writeCommandEnvelope(envelope, {
|
|
@@ -148,3 +103,4 @@ function writeRefundEnvelope(envelope, options) {
|
|
|
148
103
|
plainResult: Object.entries(envelope.result).map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`),
|
|
149
104
|
});
|
|
150
105
|
}
|
|
106
|
+
export { refundStateEnvelope } from "../services/shared/refund.js";
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { serviceAdapter } from "../../services/registry.js";
|
|
2
|
+
import { CommandContractError, writeCommandEnvelope } from "../../services/shared/envelope.js";
|
|
3
|
+
import { normalizeServiceActionStatus, paidContinuation, serviceAllowedActionCommand, purchaseConfirmationInstruction, actionInputError } from "../../services/shared/platform.js";
|
|
4
|
+
import { servicesNextEnvelope } from "../../services/presentation.js";
|
|
5
|
+
export async function runServicesAction(backend, serviceExecutionID, actionType, input, options = {}) {
|
|
6
|
+
const selection = await resolveCandidateSelection(backend, serviceExecutionID, actionType, options);
|
|
7
|
+
const request = {
|
|
8
|
+
action_type: actionType,
|
|
9
|
+
input_snapshot: input,
|
|
10
|
+
};
|
|
11
|
+
if (options.actorType)
|
|
12
|
+
request.actor_type = options.actorType;
|
|
13
|
+
if (options.actorID)
|
|
14
|
+
request.actor_id = options.actorID;
|
|
15
|
+
if (options.status)
|
|
16
|
+
request.status = normalizeServiceActionStatus(options.status, serviceExecutionID);
|
|
17
|
+
const resultItemID = selection?.resultItemID ?? options.resultItemID;
|
|
18
|
+
if (resultItemID)
|
|
19
|
+
request.result_item_id = resultItemID;
|
|
20
|
+
if (options.requiredBefore)
|
|
21
|
+
request.required_before = options.requiredBefore;
|
|
22
|
+
let response;
|
|
23
|
+
try {
|
|
24
|
+
response = await backend.recordServiceExecutionAction(serviceExecutionID, request);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
// A failed write is never replayed. Only the locked execution identifies its adapter.
|
|
28
|
+
try {
|
|
29
|
+
const model = await backend.getServiceExecution(serviceExecutionID);
|
|
30
|
+
serviceAdapter(model.execution).actionError(error, serviceExecutionID, actionType);
|
|
31
|
+
}
|
|
32
|
+
catch (recoveryError) {
|
|
33
|
+
if (recoveryError instanceof CommandContractError)
|
|
34
|
+
throw recoveryError;
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
if (actionType === "approve_purchase" && "status" in response && response.status === "approved") {
|
|
39
|
+
const quoteID = typeof input.service_quote_lock_id === "string" ? input.service_quote_lock_id : "";
|
|
40
|
+
writeCommandEnvelope({
|
|
41
|
+
status: "purchase_preparation_approved",
|
|
42
|
+
result: { service_execution_id: serviceExecutionID, evidence_source: "agent_reported", scope: "prepare_checkout", service_quote_lock_id: quoteID },
|
|
43
|
+
instruction: "已记录同范围的准备付款入口批准;这是Agent报告的真实会话同意,不是已付款或出票。继续使用本报价;条件未变不重复询问,官方页实际支付仍由用户完成。",
|
|
44
|
+
next: quoteID ? { command: `itpay cart add --quote ${quoteID} --json`, reason: "加入本次已批准报价,准备同一个付款入口" } : null,
|
|
45
|
+
recovery: [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取原执行与付款状态,不重新购买" }],
|
|
46
|
+
}, { ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}), ...(options.output ? { output: options.output } : {}) });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if ("state" in response) {
|
|
50
|
+
const updated = await backend.getServiceExecution(serviceExecutionID);
|
|
51
|
+
const envelope = servicesNextEnvelope(updated);
|
|
52
|
+
envelope.result = {
|
|
53
|
+
...envelope.result,
|
|
54
|
+
planning_action: { action_id: response.action_id, state: response.state, replayed: response.replayed },
|
|
55
|
+
};
|
|
56
|
+
writeCommandEnvelope(envelope, {
|
|
57
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
58
|
+
...(options.output ? { output: options.output } : {}),
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (selection && actionType === "select_candidate" && response.status === "approved") {
|
|
63
|
+
const updated = await backend.getServiceExecution(serviceExecutionID);
|
|
64
|
+
const preferred = updated.allowed_actions?.[0];
|
|
65
|
+
const continuation = preferred?.type === "prepare_quote"
|
|
66
|
+
? paidContinuation(updated, preferred, {})
|
|
67
|
+
: null;
|
|
68
|
+
const next = continuation?.next ?? (preferred ? serviceAllowedActionCommand(updated, preferred) : null);
|
|
69
|
+
writeCommandEnvelope({
|
|
70
|
+
status: "candidate_selected",
|
|
71
|
+
result: {
|
|
72
|
+
service_execution_id: response.service_execution_id,
|
|
73
|
+
candidate: { rank: selection.rank, title: selection.title },
|
|
74
|
+
...(continuation ? { checkout: continuation.checkout } : {}),
|
|
75
|
+
},
|
|
76
|
+
instruction: continuation
|
|
77
|
+
? purchaseConfirmationInstruction("candidate_selected", continuation.price, continuation.capability.delivery_email_required, continuation.capability.delivery_email_purpose, selection.title)
|
|
78
|
+
: "候选已绑定到来源 Execution;后续动作必须继续使用该 Execution。",
|
|
79
|
+
next,
|
|
80
|
+
recovery: [{
|
|
81
|
+
command: `itpay services next ${response.service_execution_id} --json`,
|
|
82
|
+
reason: "重新读取服务端允许的动作",
|
|
83
|
+
}],
|
|
84
|
+
}, {
|
|
85
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
86
|
+
...(options.output ? { output: options.output } : {}),
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
writeCommandEnvelope({
|
|
91
|
+
status: "action_recorded",
|
|
92
|
+
result: {
|
|
93
|
+
service_execution_id: response.service_execution_id,
|
|
94
|
+
action_type: response.action_type,
|
|
95
|
+
action_status: response.status,
|
|
96
|
+
evidence_source: "agent_reported",
|
|
97
|
+
reported_actor_type: response.actor_type,
|
|
98
|
+
},
|
|
99
|
+
instruction: "动作已记录(Agent转述;actor_type并非官方页面真人确认的证明)。读取服务端计算的新状态;不要自行假设下一 capability。",
|
|
100
|
+
next: {
|
|
101
|
+
command: `itpay services next ${response.service_execution_id} --json`,
|
|
102
|
+
reason: "取得更新后的首选动作",
|
|
103
|
+
},
|
|
104
|
+
recovery: [],
|
|
105
|
+
}, {
|
|
106
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
107
|
+
...(options.output ? { output: options.output } : {}),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
export async function resolveCandidateSelection(backend, serviceExecutionID, actionType, options) {
|
|
111
|
+
if (options.candidateRank === undefined)
|
|
112
|
+
return undefined;
|
|
113
|
+
if (actionType !== "select_candidate") {
|
|
114
|
+
throw actionInputError(serviceExecutionID, "--candidate is only valid with --action select_candidate");
|
|
115
|
+
}
|
|
116
|
+
if (options.resultItemID) {
|
|
117
|
+
throw actionInputError(serviceExecutionID, "--candidate cannot be combined with --result-item");
|
|
118
|
+
}
|
|
119
|
+
if (!Number.isInteger(options.candidateRank) || options.candidateRank < 1) {
|
|
120
|
+
throw actionInputError(serviceExecutionID, "--candidate must be a positive integer result rank");
|
|
121
|
+
}
|
|
122
|
+
const execution = await backend.getServiceExecution(serviceExecutionID);
|
|
123
|
+
const currentItems = execution.current_result_items ?? [];
|
|
124
|
+
const result = currentItems.find((item) => item.rank === options.candidateRank);
|
|
125
|
+
if (!result) {
|
|
126
|
+
throw actionInputError(serviceExecutionID, `candidate ${options.candidateRank} is not available on service execution ${serviceExecutionID}`, "candidate_not_found");
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
resultItemID: result.service_capability_result_item_id,
|
|
130
|
+
rank: result.rank,
|
|
131
|
+
title: result.display_title,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { assertCheckoutPayable } from "../checkout_handoff.js";
|
|
2
|
+
import { serviceAdapter } from "../../services/registry.js";
|
|
3
|
+
import { HttpError } from "../../client/http.js";
|
|
4
|
+
import { validateContext } from "../../state/client_context.js";
|
|
5
|
+
import { dispatchRender } from "../../render/index.js";
|
|
6
|
+
import { ensureIdeImageAttach } from "../../render/ide.js";
|
|
7
|
+
import { shouldPrepareLocalCheckoutImage } from "../../render/checkout_handoff.js";
|
|
8
|
+
import { localizeCardURL, normalizeCardLocale } from "../../render/locale.js";
|
|
9
|
+
import { platformKeyForHost } from "../../render/plan.js";
|
|
10
|
+
import { renderTerminalQR } from "../../render/qr.js";
|
|
11
|
+
import { buildCheckoutQRPlan } from "../../render/checkout_plan.js";
|
|
12
|
+
import { CommandContractError, writeCommandEnvelope, plainValueLines } from "../../services/shared/envelope.js";
|
|
13
|
+
import { missingRequiredInput, checkoutCommand, formatInputOptions, tokenizedCheckoutURL, absolutePublicURL, fallbackCardURL, fallbackCardPNGURL, buildServicesCheckoutEnvelope, quoteCommand, formatMoney } from "../../services/shared/platform.js";
|
|
14
|
+
export async function runServicesCheckout(backend, config, serviceExecutionID, capabilityID, options = {}) {
|
|
15
|
+
const host = options.host ?? "terminal";
|
|
16
|
+
const contextError = validateContext(host, options.target);
|
|
17
|
+
if (contextError) {
|
|
18
|
+
throw new CommandContractError(contextError.code, contextError.message, "从当前可信会话上下文补齐 Host/target;本次未创建 Checkout。", []);
|
|
19
|
+
}
|
|
20
|
+
const deliveryContact = {
|
|
21
|
+
...(options.deliveryContact ?? {}),
|
|
22
|
+
...(options.email ? { email: options.email } : {}),
|
|
23
|
+
};
|
|
24
|
+
if (!options.resume && !capabilityID) {
|
|
25
|
+
const model = await backend.getServiceExecution(serviceExecutionID);
|
|
26
|
+
capabilityID = model.workflow_entry?.capability_id;
|
|
27
|
+
}
|
|
28
|
+
if (!options.resume && !capabilityID) {
|
|
29
|
+
throw new CommandContractError("capability_required", "--capability is required when creating a service checkout", "使用当前 Service Execution 返回的付费 capability;恢复已有 Checkout 时改用 --resume。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前允许的付费 capability" }]);
|
|
30
|
+
}
|
|
31
|
+
if (!options.resume) {
|
|
32
|
+
const readModel = await backend.getServiceExecution(serviceExecutionID);
|
|
33
|
+
const capability = readModel.capabilities.find((item) => item.capability_id === capabilityID);
|
|
34
|
+
if (!capability || !capability.requires_payment) {
|
|
35
|
+
throw new CommandContractError("capability_not_checkoutable", `capability ${capabilityID} is not available for checkout on service execution ${serviceExecutionID}`, "只为当前 Service Execution 返回的 requires_payment capability 创建 Checkout。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前允许的下一步" }]);
|
|
36
|
+
}
|
|
37
|
+
if (readModel.checkout_bindings.length === 0 && !readModel.actions.some(action => action.action_type === "approve_purchase" && action.status === "approved")) {
|
|
38
|
+
await runServicesQuote(backend, serviceExecutionID, capability.capability_id, options.lockedInput ?? {}, options);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const lockedInput = options.lockedInput ?? {};
|
|
42
|
+
const missingInput = missingRequiredInput(capability.input_schema, lockedInput);
|
|
43
|
+
if (missingInput.length > 0 && !readModel.workflow_entry && readModel.execution.next_action !== "create_checkout") {
|
|
44
|
+
throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐付费 capability 的 required_input;本次没有创建 quote、Checkout 或订单。", [], { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command: checkoutCommand(serviceExecutionID, capability, lockedInput), required_input: missingInput, executable: false } });
|
|
45
|
+
}
|
|
46
|
+
if (capability.delivery_email_required && readModel.workflow_entry?.capability_id !== capability.capability_id && String(deliveryContact.email ?? "").trim() === "") {
|
|
47
|
+
throw new CommandContractError("delivery_email_required", "delivery email is required before creating this service checkout", "该 capability 的交付链接会发送到用户邮箱;先向用户说明用途并询问邮箱,不要代填。", [], { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command: `itpay services checkout ${serviceExecutionID} --capability ${capability.capability_id}${formatInputOptions(lockedInput)} --email <email> --json`, required_input: ["email"], executable: false } });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let response;
|
|
51
|
+
try {
|
|
52
|
+
response = await backend.createServiceExecutionCheckout(serviceExecutionID, {
|
|
53
|
+
...(capabilityID ? { capability_id: capabilityID } : {}),
|
|
54
|
+
...(Object.keys(deliveryContact).length > 0 ? { delivery_contact: deliveryContact } : {}),
|
|
55
|
+
...(options.lockedInput && Object.keys(options.lockedInput).length > 0 ? { locked_input: options.lockedInput } : {}),
|
|
56
|
+
...(options.resume ? { resume: true } : {}),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof HttpError && error.code === "purchase_approval_required") {
|
|
61
|
+
throw new CommandContractError(error.code, error.message, "先读当前报价及完整范围模板。沿用覆盖同条件的真实批准;缺少哪部分才补哪部分,不把候选选择当作价格同意,也不代付。", [{ command: capabilityID ? `itpay services quote ${serviceExecutionID} --capability ${capabilityID}${formatInputOptions(options.lockedInput ?? {})} --json` : `itpay services next ${serviceExecutionID} --json`, reason: "读取同一执行的当前批准范围,不新建购买" }]);
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
const checkout = response.checkout;
|
|
66
|
+
const authoritative = checkout.display_token ? await backend.getCheckoutPresentation(checkout.checkout.checkout_id, checkout.display_token) : undefined;
|
|
67
|
+
assertCheckoutPayable(checkout, authoritative, response.cart.cart_id, serviceExecutionID);
|
|
68
|
+
if (authoritative)
|
|
69
|
+
response.cart.items = authoritative.items;
|
|
70
|
+
const checkoutID = checkout.checkout.checkout_id;
|
|
71
|
+
const displayToken = checkout.display_token;
|
|
72
|
+
const locale = normalizeCardLocale(options.locale);
|
|
73
|
+
const checkoutURL = tokenizedCheckoutURL(checkout.checkout_url, displayToken, checkout.qr_payload);
|
|
74
|
+
const cardURL = localizeCardURL(absolutePublicURL(config.baseURL, checkout.card_url ?? fallbackCardURL(config.baseURL, checkoutID, displayToken)), locale);
|
|
75
|
+
const cardPNGURL = localizeCardURL(absolutePublicURL(config.baseURL, checkout.card_png_url ?? checkout.qr_png_url ?? fallbackCardPNGURL(config.baseURL, checkoutID, displayToken)), locale);
|
|
76
|
+
let itineraryTitle;
|
|
77
|
+
let itinerary;
|
|
78
|
+
try {
|
|
79
|
+
const booking = await backend.getServiceExecution(serviceExecutionID);
|
|
80
|
+
({ itinerary, itineraryTitle } = await serviceAdapter(booking.execution).checkout(backend, booking, checkoutID, displayToken));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Checkout already exists; a display-only title lookup cannot hide its official handoff.
|
|
84
|
+
}
|
|
85
|
+
const plan = buildCheckoutQRPlan({
|
|
86
|
+
localSimulation: backend.localSimulation,
|
|
87
|
+
host,
|
|
88
|
+
checkoutID,
|
|
89
|
+
checkoutURL,
|
|
90
|
+
cardURL,
|
|
91
|
+
displayToken,
|
|
92
|
+
qrPayload: checkout.qr_payload,
|
|
93
|
+
qrPNGURL: cardPNGURL,
|
|
94
|
+
nextAction: checkout.checkout.next_action,
|
|
95
|
+
orderItems: response.cart.items.map((item) => ({
|
|
96
|
+
title: itineraryTitle || item.title,
|
|
97
|
+
quantity: item.quantity,
|
|
98
|
+
amountMinor: item.amount_minor,
|
|
99
|
+
currency: item.currency,
|
|
100
|
+
})),
|
|
101
|
+
orderCurrency: checkout.checkout.currency,
|
|
102
|
+
...(options.agentType ? { agentType: options.agentType } : {}),
|
|
103
|
+
locale,
|
|
104
|
+
});
|
|
105
|
+
options.persistHandoff?.({
|
|
106
|
+
serviceExecutionID,
|
|
107
|
+
cartID: response.cart.cart_id,
|
|
108
|
+
checkoutID,
|
|
109
|
+
displayToken,
|
|
110
|
+
checkoutURL,
|
|
111
|
+
});
|
|
112
|
+
const platform = platformKeyForHost(plan.host);
|
|
113
|
+
if (!options.jsonOutput && (platform === "telegram" || platform === "feishu" || platform === "lark")) {
|
|
114
|
+
await dispatchRender(plan, {
|
|
115
|
+
host,
|
|
116
|
+
...(options.target ? { target: options.target } : {}),
|
|
117
|
+
...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
|
|
118
|
+
...(options.qrFilePath ? { qrFilePath: options.qrFilePath } : {}),
|
|
119
|
+
...(options.output ? { output: options.output } : {}),
|
|
120
|
+
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
121
|
+
baseURL: config.baseURL,
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (shouldPrepareLocalCheckoutImage(platform)) {
|
|
126
|
+
await ensureIdeImageAttach(plan, {
|
|
127
|
+
enabled: config.ideImageAttach,
|
|
128
|
+
...(config.baseURL ? { baseURL: config.baseURL } : {}),
|
|
129
|
+
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const envelope = buildServicesCheckoutEnvelope(response, checkoutURL, plan, options.agentType, options.target);
|
|
133
|
+
if (itinerary) {
|
|
134
|
+
envelope.result = { ...envelope.result, itinerary };
|
|
135
|
+
envelope.instruction = "整程一次付款,各段分别出票;展示入口不等于付款,付款成功不等于全部出票。" + envelope.instruction;
|
|
136
|
+
}
|
|
137
|
+
const plainResult = plainValueLines(envelope.result);
|
|
138
|
+
if (!options.jsonOutput && platform === "terminal") {
|
|
139
|
+
plainResult.push("qr:", await renderTerminalQR(checkoutURL, options.qrFormat ?? "terminal"));
|
|
140
|
+
}
|
|
141
|
+
writeCommandEnvelope(envelope, {
|
|
142
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
143
|
+
...(options.output ? { output: options.output } : {}),
|
|
144
|
+
plainResult,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
export async function runServicesQuote(backend, serviceExecutionID, capabilityID, input, options = {}) {
|
|
148
|
+
const model = await backend.getServiceExecution(serviceExecutionID);
|
|
149
|
+
const capability = model.capabilities.find((item) => item.capability_id === capabilityID);
|
|
150
|
+
if (!capability || !capability.requires_payment) {
|
|
151
|
+
throw new CommandContractError("capability_not_quoteable", `capability ${capabilityID} is not available for quote on service execution ${serviceExecutionID}`, "只为当前 Service Execution 返回的付费 capability 创建报价。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前合法动作" }]);
|
|
152
|
+
}
|
|
153
|
+
const selectionBacked = model.execution.status === "human_action_approved" &&
|
|
154
|
+
model.allowed_actions?.some((action) => action.type === "prepare_quote" && action.capability_id === capabilityID);
|
|
155
|
+
const missingInput = missingRequiredInput(capability.input_schema, input);
|
|
156
|
+
if (missingInput.length > 0 && !selectionBacked && model.workflow_entry?.capability_id !== capability.capability_id) {
|
|
157
|
+
throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐付费 capability 输入;本次没有创建 Quote、Cart 或 Checkout。", [], { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command: quoteCommand(serviceExecutionID, capability, input), required_input: missingInput, executable: false } });
|
|
158
|
+
}
|
|
159
|
+
const deliveryContact = {
|
|
160
|
+
...(options.deliveryContact ?? {}),
|
|
161
|
+
...(options.email ? { email: options.email } : {}),
|
|
162
|
+
};
|
|
163
|
+
if (capability.delivery_email_required && model.workflow_entry?.capability_id !== capability.capability_id && String(deliveryContact.email ?? "").trim() === "") {
|
|
164
|
+
throw new CommandContractError("delivery_email_required", "delivery email is required before preparing this service quote", "交付链接会发送到用户邮箱;说明用途并询问邮箱,不要代填。", [], { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command: quoteCommand(serviceExecutionID, capability, input), required_input: ["email"], executable: false } });
|
|
165
|
+
}
|
|
166
|
+
const quote = await backend.prepareServiceQuote(serviceExecutionID, {
|
|
167
|
+
capability_id: capabilityID,
|
|
168
|
+
...(Object.keys(deliveryContact).length > 0 ? { delivery_contact: deliveryContact } : {}),
|
|
169
|
+
...(Object.keys(input).length > 0 ? { locked_input: input } : {}),
|
|
170
|
+
});
|
|
171
|
+
const result = {
|
|
172
|
+
service_quote_lock_id: quote.service_quote_lock_id,
|
|
173
|
+
service_execution_id: quote.service_execution_id,
|
|
174
|
+
capability_id: quote.capability_id,
|
|
175
|
+
price: formatMoney(quote.amount_minor, quote.currency),
|
|
176
|
+
...(quote.purchase_summary ? { purchase_summary: quote.purchase_summary } : {}),
|
|
177
|
+
expires_at: quote.expires_at,
|
|
178
|
+
...(quote.approval_template ? { approval_template: quote.approval_template } : {}),
|
|
179
|
+
};
|
|
180
|
+
writeCommandEnvelope({
|
|
181
|
+
status: "quote_ready",
|
|
182
|
+
result,
|
|
183
|
+
instruction: quote.approval_template
|
|
184
|
+
? "报价已锁定。先展示服务端商品名称、对象、范围、数量和金额;与用户请求不符时让用户决定,不自称基础报告或换商品。选择候选不是价格同意;模板中的user_approved=false表示尚未记录批准。仅在用户已有真实批准覆盖本范围时改为true并提交,不重复询问同条件已明确同意。用户授权预算时用capped及真实maximum_minor;仅委托准备入口、最终金额在官方页确认时用confirm_on_official_page。不要伪造接受未知金额或条款。"
|
|
185
|
+
: "报价已锁定当前 Execution 的可信输入和价格;可单独付款,也可与其他独立 Execution 的报价合并。",
|
|
186
|
+
...(quote.approval_template ? { interaction: { schema_version: "itpay.interaction.v1", stage: "purchase_approval_required", input_template: { command: `itpay services action ${serviceExecutionID} --action approve_purchase --actor-type agent --status approved --input-json <file> --json`, input: quote.approval_template, required_input: ["file"], executable: false } } } : {}),
|
|
187
|
+
next: quote.approval_template ? null : {
|
|
188
|
+
command: `itpay cart add --quote ${quote.service_quote_lock_id} --json`,
|
|
189
|
+
reason: "加入 canonical Cart",
|
|
190
|
+
},
|
|
191
|
+
recovery: [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "重新读取当前 Execution 状态" }],
|
|
192
|
+
}, {
|
|
193
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
194
|
+
...(options.output ? { output: options.output } : {}),
|
|
195
|
+
plainResult: plainValueLines(result),
|
|
196
|
+
});
|
|
197
|
+
}
|