@itpay/cli 2.0.3 → 2.0.7
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 +96 -142
- package/dist/src/client/backend.js +26 -8
- package/dist/src/client/http.js +29 -23
- package/dist/src/commands/buy.js +84 -132
- package/dist/src/commands/cart.js +274 -169
- package/dist/src/commands/catalog.js +64 -38
- package/dist/src/commands/checkout.js +128 -79
- package/dist/src/commands/docs.js +97 -51
- package/dist/src/commands/guidance.js +112 -16
- package/dist/src/commands/install.js +50 -87
- package/dist/src/commands/next.js +45 -0
- package/dist/src/commands/order.js +44 -69
- package/dist/src/commands/orders.js +43 -15
- package/dist/src/commands/pay.js +51 -22
- package/dist/src/commands/readyz.js +8 -4
- package/dist/src/commands/refund.js +132 -11
- package/dist/src/commands/services.js +799 -147
- package/dist/src/commands/skill.js +55 -0
- package/dist/src/main.js +820 -193
- package/dist/src/render/output.js +2 -3
- package/dist/src/state/agent_type.js +19 -0
- package/dist/src/state/cart_session.js +13 -17
- package/dist/src/state/client_context.js +4 -2
- package/dist/src/state/config.js +5 -15
- package/dist/src/state/device_authority.js +175 -57
- package/docs/agent/buyer/cart-checkout.json +27 -83
- package/docs/agent/buyer/catalog-list.json +2 -1
- package/docs/agent/buyer/identity-and-sessions.json +64 -0
- package/docs/agent/buyer/install-and-setup.json +35 -65
- package/docs/agent/buyer/orders-refunds.json +31 -53
- package/docs/agent/buyer/payment-flow.json +28 -57
- package/docs/agent/buyer/quickstart.json +46 -161
- package/docs/agent/buyer/render-hosts.json +43 -57
- package/docs/cli-reference/agent-types.md +51 -0
- package/docs/cli-reference/commands/buy.md +167 -0
- package/docs/cli-reference/commands/cart/add.md +86 -0
- package/docs/cli-reference/commands/cart/clear.md +53 -0
- package/docs/cli-reference/commands/cart/index.md +30 -0
- package/docs/cli-reference/commands/cart/next.md +71 -0
- package/docs/cli-reference/commands/cart/remove.md +53 -0
- package/docs/cli-reference/commands/cart/show.md +65 -0
- package/docs/cli-reference/commands/catalog/index.md +26 -0
- package/docs/cli-reference/commands/catalog/list.md +45 -0
- package/docs/cli-reference/commands/checkout.md +74 -0
- package/docs/cli-reference/commands/device.md +13 -0
- package/docs/cli-reference/commands/docs/index.md +28 -0
- package/docs/cli-reference/commands/docs/list.md +51 -0
- package/docs/cli-reference/commands/docs/search.md +69 -0
- package/docs/cli-reference/commands/docs/show.md +68 -0
- package/docs/cli-reference/commands/install.md +114 -0
- package/docs/cli-reference/commands/next.md +87 -0
- package/docs/cli-reference/commands/order.md +92 -0
- package/docs/cli-reference/commands/orders.md +83 -0
- package/docs/cli-reference/commands/pay.md +103 -0
- package/docs/cli-reference/commands/readyz.md +38 -0
- package/docs/cli-reference/commands/refund/cancel.md +62 -0
- package/docs/cli-reference/commands/refund/create.md +85 -0
- package/docs/cli-reference/commands/refund/get.md +60 -0
- package/docs/cli-reference/commands/refund/index.md +33 -0
- package/docs/cli-reference/commands/refund/list.md +68 -0
- package/docs/cli-reference/commands/refund/watch.md +73 -0
- package/docs/cli-reference/commands/services/action.md +48 -0
- package/docs/cli-reference/commands/services/checkout.md +82 -0
- package/docs/cli-reference/commands/services/events.md +73 -0
- package/docs/cli-reference/commands/services/get.md +66 -0
- package/docs/cli-reference/commands/services/index.md +45 -0
- package/docs/cli-reference/commands/services/invoke.md +67 -0
- package/docs/cli-reference/commands/services/list.md +61 -0
- package/docs/cli-reference/commands/services/next.md +181 -0
- package/docs/cli-reference/commands/services/quote.md +63 -0
- package/docs/cli-reference/commands/services/read-result.md +98 -0
- package/docs/cli-reference/commands/services/start.md +55 -0
- package/docs/cli-reference/commands/skill.md +17 -0
- package/docs/cli-reference/conventions.md +97 -0
- package/docs/cli-reference/index.md +65 -0
- package/package.json +1 -1
- package/skills/itpay-buyer/SKILL.md +71 -110
|
@@ -1,52 +1,259 @@
|
|
|
1
1
|
import { operationID } from "../state/config.js";
|
|
2
|
-
import { resolveOutput } from "../render/sink.js";
|
|
3
2
|
import { dispatchRender } from "../render/index.js";
|
|
4
3
|
import { ensureIdeImageAttach } from "../render/ide.js";
|
|
5
4
|
import { buildAgentChatHandoff } from "../render/markdown.js";
|
|
5
|
+
import { platformKeyForHost } from "../render/plan.js";
|
|
6
|
+
import { renderTerminalQR } from "../render/qr.js";
|
|
6
7
|
import { buildCheckoutQRPlan } from "./buy.js";
|
|
7
|
-
import {
|
|
8
|
+
import { CommandContractError, isTerminalServiceExecutionStatus, writeCommandEnvelope, } from "./guidance.js";
|
|
8
9
|
const serviceActionStatuses = new Set(["pending", "approved", "rejected", "expired", "cancelled"]);
|
|
9
|
-
export async function runServicesStart(backend,
|
|
10
|
+
export async function runServicesStart(backend, serviceID, options = {}) {
|
|
10
11
|
const host = options.host ?? "terminal";
|
|
11
12
|
const response = await backend.startServiceExecution({
|
|
12
13
|
service_id: serviceID,
|
|
13
|
-
agent_device_id: config.agentDeviceID,
|
|
14
|
-
...(options.buyerID ? { buyer_id: options.buyerID } : {}),
|
|
15
14
|
client_context: {
|
|
16
|
-
agent_device_id: config.agentDeviceID,
|
|
17
15
|
host,
|
|
18
16
|
...(options.target ? { target: options.target } : {}),
|
|
19
17
|
...(options.clientContext ?? {}),
|
|
20
18
|
},
|
|
21
19
|
});
|
|
22
|
-
|
|
20
|
+
const capability = response.capabilities.find((item) => item.phase === response.execution.phase && !item.requires_payment);
|
|
21
|
+
const requiredInput = requiredInputFields(capability?.input_schema);
|
|
22
|
+
const command = capability
|
|
23
|
+
? `itpay services invoke ${response.execution.service_execution_id} --capability ${capability.capability_id}${requiredInput.map((field) => ` --input ${field}=<value>`).join("")} --json`
|
|
24
|
+
: `itpay services next ${response.execution.service_execution_id} --json`;
|
|
25
|
+
const capabilitySummary = capability ? {
|
|
26
|
+
capability_id: capability.capability_id,
|
|
27
|
+
required_input: requiredInput,
|
|
28
|
+
...(capability.free_quota_limit !== undefined ? { free_quota_limit: capability.free_quota_limit } : {}),
|
|
29
|
+
} : null;
|
|
30
|
+
writeCommandEnvelope({
|
|
31
|
+
status: "ready",
|
|
32
|
+
result: {
|
|
33
|
+
service_execution_id: response.execution.service_execution_id,
|
|
34
|
+
service_id: response.execution.service_id,
|
|
35
|
+
phase: response.execution.phase,
|
|
36
|
+
capability: capabilitySummary,
|
|
37
|
+
},
|
|
38
|
+
instruction: capability
|
|
39
|
+
? "填写首选 capability 的 required_input;一次只提交当前 execution 所代表的服务意图。"
|
|
40
|
+
: "当前没有可直接调用的 capability;读取服务端下一步,不要猜测 capability。",
|
|
41
|
+
next: {
|
|
42
|
+
command,
|
|
43
|
+
reason: capability ? "执行当前允许的能力" : "读取服务端计算的下一步",
|
|
44
|
+
},
|
|
45
|
+
recovery: [],
|
|
46
|
+
}, {
|
|
47
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
48
|
+
...(options.output ? { output: options.output } : {}),
|
|
49
|
+
plainResult: [
|
|
50
|
+
`service_execution_id: ${response.execution.service_execution_id}`,
|
|
51
|
+
`service_id: ${response.execution.service_id}`,
|
|
52
|
+
`phase: ${response.execution.phase}`,
|
|
53
|
+
...(capability ? [
|
|
54
|
+
`capability: ${capability.capability_id}`,
|
|
55
|
+
`required_input: ${requiredInput.length > 0 ? requiredInput.join(",") : "none"}`,
|
|
56
|
+
...(capability.free_quota_limit !== undefined ? [`free_quota_limit: ${capability.free_quota_limit}`] : []),
|
|
57
|
+
] : []),
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function requiredInputFields(schema) {
|
|
62
|
+
const required = schema?.required;
|
|
63
|
+
return Array.isArray(required) ? required.filter((field) => typeof field === "string") : [];
|
|
23
64
|
}
|
|
24
65
|
export async function runServicesInvoke(backend, config, serviceExecutionID, capabilityID, input, options = {}) {
|
|
66
|
+
const readModel = await backend.getServiceExecution(serviceExecutionID);
|
|
67
|
+
const requestedCapability = readModel.capabilities.find((capability) => capability.capability_id === capabilityID);
|
|
68
|
+
if (!requestedCapability) {
|
|
69
|
+
throw new CommandContractError("capability_not_found", `capability ${capabilityID} is not available on service execution ${serviceExecutionID}`, "使用 Service Execution 当前返回的 capability_id,不要猜测名称。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前可用 capability" }]);
|
|
70
|
+
}
|
|
71
|
+
if (requestedCapability.requires_payment) {
|
|
72
|
+
throw new CommandContractError("checkout_required", `capability ${capabilityID} requires checkout and cannot be invoked directly`, "付费 capability 不能直接 invoke。不要尝试 quote、cart、buy、checkout 或 pay 作为旁路;只恢复同一 Execution 的当前合法动作。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取同一 Execution 的当前合法动作" }]);
|
|
73
|
+
}
|
|
74
|
+
const missingInput = missingRequiredInput(requestedCapability.input_schema, input);
|
|
75
|
+
if (missingInput.length > 0) {
|
|
76
|
+
const correctedInput = { ...input };
|
|
77
|
+
for (const field of missingInput)
|
|
78
|
+
correctedInput[field] = "<value>";
|
|
79
|
+
throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐 required_input 后重试同一个 execution;本次没有调用 Provider。", [{
|
|
80
|
+
command: `itpay services invoke ${serviceExecutionID} --capability ${capabilityID}${formatInputOptions(correctedInput)} --json`,
|
|
81
|
+
reason: "提交完整 capability 输入",
|
|
82
|
+
}]);
|
|
83
|
+
}
|
|
25
84
|
const idempotencyKey = await operationID(config, `service.invoke:${serviceExecutionID}:${capabilityID}:${stableInput(input)}`);
|
|
26
85
|
const response = await backend.invokeServiceCapability(serviceExecutionID, capabilityID, {
|
|
27
86
|
idempotency_key: idempotencyKey,
|
|
28
87
|
redacted_summary: input,
|
|
29
88
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
89
|
+
const envelope = invokedEnvelope(response, requestedCapability, readModel.capabilities, input);
|
|
90
|
+
writeCommandEnvelope(envelope.value, {
|
|
91
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
92
|
+
...(options.output ? { output: options.output } : {}),
|
|
93
|
+
plainResult: envelope.plainResult,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function invokedEnvelope(response, requestedCapability, capabilities, input) {
|
|
97
|
+
const items = response.result_items.map((item) => ({
|
|
98
|
+
rank: item.rank,
|
|
99
|
+
title: item.display_title,
|
|
100
|
+
safe_payload: item.safe_payload,
|
|
101
|
+
}));
|
|
102
|
+
const quota = response.effective_quota
|
|
103
|
+
? { remaining: response.effective_quota.remaining, limit: response.effective_quota.limit }
|
|
104
|
+
: undefined;
|
|
105
|
+
const baseResult = {
|
|
106
|
+
service_execution_id: response.execution.service_execution_id,
|
|
107
|
+
capability_id: requestedCapability.capability_id,
|
|
108
|
+
items,
|
|
109
|
+
...(quota ? { quota } : {}),
|
|
110
|
+
};
|
|
111
|
+
let status = items.length > 0 ? "result_ready" : "no_result";
|
|
112
|
+
let instruction = items.length > 0
|
|
113
|
+
? "向用户展示编号和 safe_payload;若候选列表已满足用户目标,在此停止。仅在用户明确选择并希望继续时,才在当前 Execution 提交对应 rank。"
|
|
114
|
+
: "Provider 已返回空结果;不要重放当前 execution,按下一步恢复。";
|
|
115
|
+
let next = null;
|
|
116
|
+
if (response.effective_quota?.exhausted) {
|
|
117
|
+
status = "quota_exhausted";
|
|
118
|
+
instruction = "免费额度已用完且本次没有调用 Provider。当前没有可购买的 continuation;只读取同一 Execution 的服务端恢复方向。";
|
|
119
|
+
const checkoutAction = response.next_actions?.find((action) => action.kind === "create_checkout");
|
|
120
|
+
const checkoutCapability = capabilities.find((capability) => capability.capability_id === checkoutAction?.capability_id);
|
|
121
|
+
if (checkoutCapability) {
|
|
122
|
+
baseResult.checkout = {
|
|
123
|
+
capability_id: checkoutCapability.capability_id,
|
|
124
|
+
...(checkoutCapability.price_amount_minor !== undefined && checkoutCapability.price_currency ? {
|
|
125
|
+
price: { amount_minor: checkoutCapability.price_amount_minor, currency: checkoutCapability.price_currency },
|
|
126
|
+
} : {}),
|
|
127
|
+
delivery_email_required: checkoutCapability.delivery_email_required,
|
|
128
|
+
};
|
|
129
|
+
const price = capabilityPrice(checkoutCapability);
|
|
130
|
+
instruction = purchaseConfirmationInstruction("quota_exhausted", price, checkoutCapability.delivery_email_required);
|
|
131
|
+
next = {
|
|
132
|
+
command: checkoutCommand(response.execution.service_execution_id, checkoutCapability, input),
|
|
133
|
+
reason: `仅在用户明确同意支付 ${price} 后执行;否则停止`,
|
|
134
|
+
};
|
|
34
135
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
136
|
+
else {
|
|
137
|
+
next = {
|
|
138
|
+
command: `itpay services next ${response.execution.service_execution_id} --json`,
|
|
139
|
+
reason: "读取服务端提供的付费恢复入口",
|
|
140
|
+
};
|
|
38
141
|
}
|
|
39
142
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
143
|
+
else if (items.length > 0 && requestedCapability.requires_human_action) {
|
|
144
|
+
next = {
|
|
145
|
+
command: `itpay services action ${response.execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
|
|
146
|
+
reason: "在当前 Execution 记录用户选择",
|
|
147
|
+
};
|
|
44
148
|
}
|
|
45
|
-
|
|
149
|
+
else if (items.length === 0) {
|
|
150
|
+
next = response.provider_called
|
|
151
|
+
? { command: `itpay services start ${response.execution.service_id}`, reason: "为新的服务输入启动新 execution" }
|
|
152
|
+
: { command: `itpay services next ${response.execution.service_execution_id} --json`, reason: "读取服务端恢复动作" };
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
value: { status, result: baseResult, instruction, next, recovery: [] },
|
|
156
|
+
plainResult: serviceResultPlainLines(baseResult),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function serviceResultPlainLines(result) {
|
|
160
|
+
const lines = [
|
|
161
|
+
`service_execution_id: ${String(result.service_execution_id)}`,
|
|
162
|
+
`capability_id: ${String(result.capability_id)}`,
|
|
163
|
+
];
|
|
164
|
+
if (result.quota)
|
|
165
|
+
lines.push(`quota: ${JSON.stringify(result.quota)}`);
|
|
166
|
+
if (result.checkout)
|
|
167
|
+
lines.push(`checkout: ${JSON.stringify(result.checkout)}`);
|
|
168
|
+
const items = result.items;
|
|
169
|
+
if (items.length > 0) {
|
|
170
|
+
lines.push("items:");
|
|
171
|
+
for (const item of items) {
|
|
172
|
+
lines.push(` ${item.rank}. ${item.title}`);
|
|
173
|
+
for (const [key, value] of Object.entries(item.safe_payload)) {
|
|
174
|
+
lines.push(` ${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return lines;
|
|
179
|
+
}
|
|
180
|
+
function missingRequiredInput(schema, input) {
|
|
181
|
+
return requiredInputFields(schema).filter((field) => {
|
|
182
|
+
if (!(field in input) || input[field] === null || input[field] === undefined)
|
|
183
|
+
return true;
|
|
184
|
+
return typeof input[field] === "string" && String(input[field]).trim() === "";
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function checkoutCommand(serviceExecutionID, capability, input, fillMissing = true) {
|
|
188
|
+
const lockedInput = { ...input };
|
|
189
|
+
if (fillMissing) {
|
|
190
|
+
for (const field of missingRequiredInput(capability.input_schema, lockedInput))
|
|
191
|
+
lockedInput[field] = "<value>";
|
|
192
|
+
}
|
|
193
|
+
return `itpay services checkout ${serviceExecutionID} --capability ${capability.capability_id}${formatInputOptions(lockedInput)}${capability.delivery_email_required ? " --email <email>" : ""} --json`;
|
|
194
|
+
}
|
|
195
|
+
function capabilityPrice(capability) {
|
|
196
|
+
return capability.price_amount_minor !== undefined && capability.price_currency
|
|
197
|
+
? formatMoney(capability.price_amount_minor, capability.price_currency)
|
|
198
|
+
: "当前发布价格";
|
|
199
|
+
}
|
|
200
|
+
function purchaseConfirmationInstruction(context, price, deliveryEmailRequired, candidateTitle = "") {
|
|
201
|
+
if (context === "quota_exhausted") {
|
|
202
|
+
return deliveryEmailRequired
|
|
203
|
+
? `免费额度已用完,本次没有调用 Provider,也尚未创建 Quote 或 Checkout。现在只向用户说明:继续当前请求需要支付 ${price},交付还需要用户邮箱;请确认是否购买并提供邮箱。然后停止并等待。用户明确同意并提供真实邮箱前,不要执行 next.command,不要新建 Execution,不要尝试其他 capability、quote、cart、buy、checkout 或 pay 命令。`
|
|
204
|
+
: `免费额度已用完,本次没有调用 Provider,也尚未创建 Quote 或 Checkout。现在只向用户说明:“继续当前请求需要支付 ${price},是否购买?”然后停止并等待用户明确回复。用户明确同意前,不要执行 next.command,不要新建 Execution,不要尝试其他 capability、quote、cart、buy、checkout 或 pay 命令。`;
|
|
205
|
+
}
|
|
206
|
+
const selected = candidateTitle ? `已选择 ${candidateTitle}。` : "当前候选已经确认。";
|
|
207
|
+
return deliveryEmailRequired
|
|
208
|
+
? `${selected}候选已绑定到当前 Execution,但尚未购买后续服务。现在只向用户说明:继续购买后续服务需要支付 ${price},并提供用于发送交付认领链接的邮箱;请确认是否购买并提供邮箱。然后停止。用户明确同意并提供真实邮箱前,不要执行 next.command,不要创建新 Execution 或 Checkout。`
|
|
209
|
+
: `${selected}候选已绑定到当前 Execution,但尚未购买后续服务。现在只向用户说明:“继续购买后续服务需要支付 ${price},是否购买?”然后停止。用户明确同意前,不要执行 next.command,不要创建新 Execution 或 Checkout。`;
|
|
210
|
+
}
|
|
211
|
+
function paidContinuation(model, action, input) {
|
|
212
|
+
if (!action.capability_id)
|
|
213
|
+
return null;
|
|
214
|
+
const capability = model.capabilities.find((item) => item.capability_id === action.capability_id && item.requires_payment);
|
|
215
|
+
if (!capability)
|
|
216
|
+
return null;
|
|
217
|
+
const price = capabilityPrice(capability);
|
|
218
|
+
const stateBacked = model.execution.status === "quota_exhausted" || model.execution.status === "human_action_approved";
|
|
219
|
+
return {
|
|
220
|
+
capability,
|
|
221
|
+
price,
|
|
222
|
+
checkout: {
|
|
223
|
+
capability_id: capability.capability_id,
|
|
224
|
+
...(capability.price_amount_minor !== undefined && capability.price_currency ? {
|
|
225
|
+
price: { amount_minor: capability.price_amount_minor, currency: capability.price_currency },
|
|
226
|
+
} : {}),
|
|
227
|
+
delivery_email_required: capability.delivery_email_required,
|
|
228
|
+
},
|
|
229
|
+
next: {
|
|
230
|
+
command: checkoutCommand(model.execution.service_execution_id, capability, input, !stateBacked),
|
|
231
|
+
reason: `仅在用户明确同意支付 ${price}${capability.delivery_email_required ? " 并提供真实邮箱" : ""}后执行;否则停止`,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function quoteCommand(serviceExecutionID, capability, input) {
|
|
236
|
+
const lockedInput = { ...input };
|
|
237
|
+
for (const field of missingRequiredInput(capability.input_schema, lockedInput))
|
|
238
|
+
lockedInput[field] = "<value>";
|
|
239
|
+
return `itpay services quote ${serviceExecutionID} --capability ${capability.capability_id}${formatInputOptions(lockedInput)}${capability.delivery_email_required ? " --email <email>" : ""} --json`;
|
|
46
240
|
}
|
|
47
241
|
function stableInput(input) {
|
|
48
242
|
return JSON.stringify(Object.fromEntries(Object.entries(input).sort(([left], [right]) => left.localeCompare(right))));
|
|
49
243
|
}
|
|
244
|
+
function formatInputOptions(input) {
|
|
245
|
+
return Object.entries(input)
|
|
246
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
247
|
+
.map(([key, value]) => String(value) === "<value>"
|
|
248
|
+
? ` --input ${key}=<value>`
|
|
249
|
+
: ` --input ${shellArgument(`${key}=${String(value)}`)}`)
|
|
250
|
+
.join("");
|
|
251
|
+
}
|
|
252
|
+
function shellArgument(value) {
|
|
253
|
+
if (/^[\p{L}\p{N}._:=/-]+$/u.test(value))
|
|
254
|
+
return value;
|
|
255
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
256
|
+
}
|
|
50
257
|
export async function runServicesAction(backend, serviceExecutionID, actionType, input, options = {}) {
|
|
51
258
|
const selection = await resolveCandidateSelection(backend, serviceExecutionID, actionType, options);
|
|
52
259
|
const request = {
|
|
@@ -58,61 +265,118 @@ export async function runServicesAction(backend, serviceExecutionID, actionType,
|
|
|
58
265
|
if (options.actorID)
|
|
59
266
|
request.actor_id = options.actorID;
|
|
60
267
|
if (options.status)
|
|
61
|
-
request.status = normalizeServiceActionStatus(options.status);
|
|
268
|
+
request.status = normalizeServiceActionStatus(options.status, serviceExecutionID);
|
|
62
269
|
const resultItemID = selection?.resultItemID ?? options.resultItemID;
|
|
63
|
-
const selectedCandidateHash = selection?.stableHash ?? options.selectedCandidateHash;
|
|
64
270
|
if (resultItemID)
|
|
65
271
|
request.result_item_id = resultItemID;
|
|
66
|
-
if (selectedCandidateHash)
|
|
67
|
-
request.selected_candidate_hash = selectedCandidateHash;
|
|
68
272
|
if (options.requiredBefore)
|
|
69
273
|
request.required_before = options.requiredBefore;
|
|
70
274
|
const response = await backend.recordServiceExecutionAction(serviceExecutionID, request);
|
|
71
|
-
|
|
275
|
+
if (selection && actionType === "select_candidate" && response.status === "approved") {
|
|
276
|
+
const updated = await backend.getServiceExecution(serviceExecutionID);
|
|
277
|
+
const preferred = updated.allowed_actions?.[0];
|
|
278
|
+
const continuation = preferred?.type === "prepare_quote"
|
|
279
|
+
? paidContinuation(updated, preferred, {})
|
|
280
|
+
: null;
|
|
281
|
+
const next = continuation?.next ?? (preferred ? serviceAllowedActionCommand(updated, preferred) : null);
|
|
282
|
+
writeCommandEnvelope({
|
|
283
|
+
status: "candidate_selected",
|
|
284
|
+
result: {
|
|
285
|
+
service_execution_id: response.service_execution_id,
|
|
286
|
+
candidate: { rank: selection.rank, title: selection.title },
|
|
287
|
+
...(continuation ? { checkout: continuation.checkout } : {}),
|
|
288
|
+
},
|
|
289
|
+
instruction: continuation
|
|
290
|
+
? purchaseConfirmationInstruction("candidate_selected", continuation.price, continuation.capability.delivery_email_required, selection.title)
|
|
291
|
+
: "候选已绑定到来源 Execution;后续动作必须继续使用该 Execution。",
|
|
292
|
+
next,
|
|
293
|
+
recovery: [{
|
|
294
|
+
command: `itpay services next ${response.service_execution_id} --json`,
|
|
295
|
+
reason: "重新读取服务端允许的动作",
|
|
296
|
+
}],
|
|
297
|
+
}, {
|
|
298
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
299
|
+
...(options.output ? { output: options.output } : {}),
|
|
300
|
+
});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
writeCommandEnvelope({
|
|
304
|
+
status: "action_recorded",
|
|
305
|
+
result: {
|
|
306
|
+
service_execution_id: response.service_execution_id,
|
|
307
|
+
action_type: response.action_type,
|
|
308
|
+
action_status: response.status,
|
|
309
|
+
},
|
|
310
|
+
instruction: "动作已记录,读取服务端计算的新状态;不要自行假设下一 capability。",
|
|
311
|
+
next: {
|
|
312
|
+
command: `itpay services next ${response.service_execution_id} --json`,
|
|
313
|
+
reason: "取得更新后的首选动作",
|
|
314
|
+
},
|
|
315
|
+
recovery: [],
|
|
316
|
+
}, {
|
|
317
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
318
|
+
...(options.output ? { output: options.output } : {}),
|
|
319
|
+
});
|
|
72
320
|
}
|
|
73
321
|
async function resolveCandidateSelection(backend, serviceExecutionID, actionType, options) {
|
|
74
322
|
if (options.candidateRank === undefined)
|
|
75
323
|
return undefined;
|
|
76
324
|
if (actionType !== "select_candidate") {
|
|
77
|
-
throw
|
|
325
|
+
throw actionInputError(serviceExecutionID, "--candidate is only valid with --action select_candidate");
|
|
78
326
|
}
|
|
79
|
-
if (options.resultItemID
|
|
80
|
-
throw
|
|
327
|
+
if (options.resultItemID) {
|
|
328
|
+
throw actionInputError(serviceExecutionID, "--candidate cannot be combined with --result-item");
|
|
81
329
|
}
|
|
82
330
|
if (!Number.isInteger(options.candidateRank) || options.candidateRank < 1) {
|
|
83
|
-
throw
|
|
331
|
+
throw actionInputError(serviceExecutionID, "--candidate must be a positive integer result rank");
|
|
84
332
|
}
|
|
85
333
|
const execution = await backend.getServiceExecution(serviceExecutionID);
|
|
86
|
-
const
|
|
334
|
+
const currentItems = execution.current_result_items ?? [];
|
|
335
|
+
const result = currentItems.find((item) => item.rank === options.candidateRank);
|
|
87
336
|
if (!result) {
|
|
88
|
-
throw
|
|
337
|
+
throw actionInputError(serviceExecutionID, `candidate ${options.candidateRank} is not available on service execution ${serviceExecutionID}`, "candidate_not_found");
|
|
89
338
|
}
|
|
90
339
|
return {
|
|
91
340
|
resultItemID: result.service_capability_result_item_id,
|
|
92
|
-
|
|
341
|
+
rank: result.rank,
|
|
342
|
+
title: result.display_title,
|
|
93
343
|
};
|
|
94
344
|
}
|
|
345
|
+
function actionInputError(serviceExecutionID, message, code = "service_action_invalid") {
|
|
346
|
+
return new CommandContractError(code, message, code === "candidate_not_found"
|
|
347
|
+
? "当前 rank 不存在或当前候选集不可用。不要新建 Execution,不要重新 invoke,不要构造候选 ID;只恢复同一 Execution 当前仍然有效的候选。"
|
|
348
|
+
: "使用当前 safe result 中的合法 action 和 candidate rank;需要人确认时先询问用户。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "重新读取同一 Execution 的当前可选动作" }]);
|
|
349
|
+
}
|
|
95
350
|
export async function runServicesCheckout(backend, config, serviceExecutionID, capabilityID, options = {}) {
|
|
96
351
|
const deliveryContact = {
|
|
97
352
|
...(options.deliveryContact ?? {}),
|
|
98
353
|
...(options.email ? { email: options.email } : {}),
|
|
99
354
|
};
|
|
100
355
|
if (!options.resume && !capabilityID) {
|
|
101
|
-
throw new
|
|
356
|
+
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" }]);
|
|
102
357
|
}
|
|
103
|
-
if (!options.resume
|
|
104
|
-
const
|
|
105
|
-
const capability =
|
|
106
|
-
if (!capability) {
|
|
107
|
-
throw new
|
|
358
|
+
if (!options.resume) {
|
|
359
|
+
const readModel = await backend.getServiceExecution(serviceExecutionID);
|
|
360
|
+
const capability = readModel.capabilities.find((item) => item.capability_id === capabilityID);
|
|
361
|
+
if (!capability || !capability.requires_payment) {
|
|
362
|
+
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: "读取当前允许的下一步" }]);
|
|
363
|
+
}
|
|
364
|
+
const lockedInput = options.lockedInput ?? {};
|
|
365
|
+
const missingInput = missingRequiredInput(capability.input_schema, lockedInput);
|
|
366
|
+
if (missingInput.length > 0 && readModel.execution.next_action !== "create_checkout") {
|
|
367
|
+
throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐付费 capability 的 required_input;本次没有创建 quote、Checkout 或订单。", [{ command: checkoutCommand(serviceExecutionID, capability, lockedInput), reason: "提交完整且会被锁定的服务输入" }]);
|
|
108
368
|
}
|
|
109
|
-
if (capability.delivery_email_required) {
|
|
110
|
-
throw new
|
|
369
|
+
if (capability.delivery_email_required && String(deliveryContact.email ?? "").trim() === "") {
|
|
370
|
+
throw new CommandContractError("delivery_email_required", "delivery email is required before creating this service checkout", "该 capability 的交付链接会发送到用户邮箱;先向用户说明用途并询问邮箱,不要代填。", [{
|
|
371
|
+
command: `itpay services checkout ${serviceExecutionID} --capability ${capability.capability_id}${formatInputOptions(lockedInput)} --email <email> --json`,
|
|
372
|
+
reason: "使用用户提供的邮箱创建 Checkout",
|
|
373
|
+
}]);
|
|
111
374
|
}
|
|
112
375
|
}
|
|
113
376
|
const response = await backend.createServiceExecutionCheckout(serviceExecutionID, {
|
|
114
377
|
...(capabilityID ? { capability_id: capabilityID } : {}),
|
|
115
378
|
...(Object.keys(deliveryContact).length > 0 ? { delivery_contact: deliveryContact } : {}),
|
|
379
|
+
...(options.lockedInput && Object.keys(options.lockedInput).length > 0 ? { locked_input: options.lockedInput } : {}),
|
|
116
380
|
...(options.resume ? { resume: true } : {}),
|
|
117
381
|
});
|
|
118
382
|
const checkout = response.checkout;
|
|
@@ -142,67 +406,473 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
142
406
|
displayToken,
|
|
143
407
|
checkoutURL,
|
|
144
408
|
});
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
409
|
+
const platform = platformKeyForHost(plan.host);
|
|
410
|
+
if (platform === "telegram" || platform === "feishu" || platform === "lark") {
|
|
411
|
+
await dispatchRender(plan, {
|
|
412
|
+
host: options.host ?? "terminal",
|
|
413
|
+
...(options.target ? { target: options.target } : {}),
|
|
414
|
+
...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
|
|
415
|
+
...(options.qrFilePath ? { qrFilePath: options.qrFilePath } : {}),
|
|
416
|
+
...(options.output ? { output: options.output } : {}),
|
|
149
417
|
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
418
|
+
baseURL: config.baseURL,
|
|
150
419
|
});
|
|
151
|
-
writeJSON(options.output, buildServicesCheckoutJSON(response, checkoutURL, plan));
|
|
152
420
|
return;
|
|
153
421
|
}
|
|
154
|
-
await
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
...(options.target ? { target: options.target } : {}),
|
|
158
|
-
...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
|
|
159
|
-
...(options.qrFilePath ? { qrFilePath: options.qrFilePath } : {}),
|
|
160
|
-
...(options.output ? { output: options.output } : {}),
|
|
422
|
+
await ensureIdeImageAttach(plan, {
|
|
423
|
+
enabled: config.ideImageAttach,
|
|
424
|
+
...(config.baseURL ? { baseURL: config.baseURL } : {}),
|
|
161
425
|
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
162
|
-
|
|
426
|
+
});
|
|
427
|
+
const envelope = buildServicesCheckoutEnvelope(response, checkoutURL, plan, config.baseURL);
|
|
428
|
+
const plainResult = [
|
|
429
|
+
`service_execution_id: ${response.binding.service_execution_id}`,
|
|
430
|
+
`checkout_id: ${checkoutID}`,
|
|
431
|
+
`capability_id: ${checkoutCapabilityID(response, capabilityID)}`,
|
|
432
|
+
`locked_input: ${JSON.stringify(response.locked_input)}`,
|
|
433
|
+
`amount: ${formatMoney(checkout.checkout.amount_minor, checkout.checkout.currency)}`,
|
|
434
|
+
];
|
|
435
|
+
if (!options.jsonOutput && platform === "terminal") {
|
|
436
|
+
plainResult.push("qr:", await renderTerminalQR(checkoutURL, options.qrFormat ?? "terminal"));
|
|
437
|
+
}
|
|
438
|
+
writeCommandEnvelope(envelope, {
|
|
439
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
440
|
+
...(options.output ? { output: options.output } : {}),
|
|
441
|
+
plainResult,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
export async function runServicesQuote(backend, serviceExecutionID, capabilityID, input, options = {}) {
|
|
445
|
+
const model = await backend.getServiceExecution(serviceExecutionID);
|
|
446
|
+
const capability = model.capabilities.find((item) => item.capability_id === capabilityID);
|
|
447
|
+
if (!capability || !capability.requires_payment) {
|
|
448
|
+
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: "读取当前合法动作" }]);
|
|
449
|
+
}
|
|
450
|
+
const selectionBacked = model.execution.status === "human_action_approved" &&
|
|
451
|
+
model.allowed_actions?.some((action) => action.type === "prepare_quote" && action.capability_id === capabilityID);
|
|
452
|
+
const missingInput = missingRequiredInput(capability.input_schema, input);
|
|
453
|
+
if (missingInput.length > 0 && !selectionBacked) {
|
|
454
|
+
throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐付费 capability 输入;本次没有创建 Quote、Cart 或 Checkout。", [{ command: quoteCommand(serviceExecutionID, capability, input), reason: "提交完整且会被锁定的输入" }]);
|
|
455
|
+
}
|
|
456
|
+
const deliveryContact = {
|
|
457
|
+
...(options.deliveryContact ?? {}),
|
|
458
|
+
...(options.email ? { email: options.email } : {}),
|
|
459
|
+
};
|
|
460
|
+
if (capability.delivery_email_required && String(deliveryContact.email ?? "").trim() === "") {
|
|
461
|
+
throw new CommandContractError("delivery_email_required", "delivery email is required before preparing this service quote", "交付链接会发送到用户邮箱;说明用途并询问邮箱,不要代填。", [{ command: quoteCommand(serviceExecutionID, capability, input), reason: "使用用户提供的邮箱创建报价" }]);
|
|
462
|
+
}
|
|
463
|
+
const quote = await backend.prepareServiceQuote(serviceExecutionID, {
|
|
464
|
+
capability_id: capabilityID,
|
|
465
|
+
...(Object.keys(deliveryContact).length > 0 ? { delivery_contact: deliveryContact } : {}),
|
|
466
|
+
...(Object.keys(input).length > 0 ? { locked_input: input } : {}),
|
|
467
|
+
});
|
|
468
|
+
const result = {
|
|
469
|
+
service_quote_lock_id: quote.service_quote_lock_id,
|
|
470
|
+
service_execution_id: quote.service_execution_id,
|
|
471
|
+
capability_id: quote.capability_id,
|
|
472
|
+
price: formatMoney(quote.amount_minor, quote.currency),
|
|
473
|
+
expires_at: quote.expires_at,
|
|
474
|
+
};
|
|
475
|
+
writeCommandEnvelope({
|
|
476
|
+
status: "quote_ready",
|
|
477
|
+
result,
|
|
478
|
+
instruction: "报价已锁定当前 Execution 的可信输入和价格;可单独付款,也可与其他独立 Execution 的报价合并。",
|
|
479
|
+
next: {
|
|
480
|
+
command: `itpay cart add --quote ${quote.service_quote_lock_id} --json`,
|
|
481
|
+
reason: "加入 canonical Cart",
|
|
482
|
+
},
|
|
483
|
+
recovery: [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "重新读取当前 Execution 状态" }],
|
|
484
|
+
}, {
|
|
485
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
486
|
+
...(options.output ? { output: options.output } : {}),
|
|
487
|
+
plainResult: Object.entries(result).map(([key, value]) => `${key}: ${String(value)}`),
|
|
163
488
|
});
|
|
164
489
|
}
|
|
165
490
|
export async function runServicesGet(backend, serviceExecutionID, options = {}) {
|
|
166
491
|
const response = await backend.getServiceExecution(serviceExecutionID);
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
492
|
+
const execution = response.execution;
|
|
493
|
+
const timeline = response.events.slice(-20).map((event) => ({
|
|
494
|
+
sequence: event.sequence,
|
|
495
|
+
step: event.type,
|
|
496
|
+
status: event.status,
|
|
497
|
+
phase: event.phase,
|
|
498
|
+
...(event.capability_id ? { capability_id: event.capability_id } : {}),
|
|
499
|
+
occurred_at: event.occurred_at,
|
|
500
|
+
}));
|
|
501
|
+
const deliveryMode = serviceDeliveryMode(response);
|
|
502
|
+
const lockedRefund = response.refunds.find((refund) => refund.access_locked);
|
|
503
|
+
const nextState = servicesNextEnvelope(response);
|
|
504
|
+
const result = {
|
|
505
|
+
service_execution_id: execution.service_execution_id,
|
|
506
|
+
service_id: execution.service_id,
|
|
507
|
+
status: execution.status,
|
|
508
|
+
phase: execution.phase,
|
|
509
|
+
...(execution.current_capability_id ? { current_capability_id: execution.current_capability_id } : {}),
|
|
510
|
+
updated_at: execution.updated_at,
|
|
511
|
+
timeline,
|
|
512
|
+
...(response.events.length > timeline.length ? { timeline_truncated: true } : {}),
|
|
513
|
+
...(deliveryMode ? { delivery_mode: deliveryMode } : {}),
|
|
514
|
+
...(lockedRefund ? {
|
|
515
|
+
access_locked: true,
|
|
516
|
+
refund: { refund_request_id: lockedRefund.refund_request_id, status: lockedRefund.status },
|
|
517
|
+
} : {}),
|
|
518
|
+
};
|
|
519
|
+
const envelope = {
|
|
520
|
+
status: "shown",
|
|
521
|
+
result,
|
|
522
|
+
instruction: lockedRefund || isTerminalServiceExecutionStatus(execution.status)
|
|
523
|
+
? nextState.instruction
|
|
524
|
+
: "时间线仅用于解释和恢复;按当前首选动作继续,不要重放已完成步骤。",
|
|
525
|
+
next: nextState.next ? { command: nextState.next.command, reason: "继续当前首选动作" } : null,
|
|
526
|
+
recovery: [{ command: `itpay services events ${serviceExecutionID} --json`, reason: "仅在需要完整诊断事件时使用" }],
|
|
527
|
+
};
|
|
528
|
+
writeCommandEnvelope(envelope, {
|
|
529
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
530
|
+
...(options.output ? { output: options.output } : {}),
|
|
531
|
+
plainResult: [
|
|
532
|
+
`service_execution_id: ${execution.service_execution_id}`,
|
|
533
|
+
`service_id: ${execution.service_id}`,
|
|
534
|
+
`state: ${execution.status}/${execution.phase}`,
|
|
535
|
+
...(execution.current_capability_id ? [`current_capability_id: ${execution.current_capability_id}`] : []),
|
|
536
|
+
...timeline.map((event) => `${event.sequence}. ${event.step} ${event.status}/${event.phase} ${event.occurred_at}`),
|
|
537
|
+
],
|
|
538
|
+
});
|
|
173
539
|
}
|
|
174
540
|
export async function runServicesNext(backend, serviceExecutionID, options = {}) {
|
|
175
541
|
const response = await backend.getServiceExecution(serviceExecutionID);
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
542
|
+
const envelope = servicesNextEnvelope(response);
|
|
543
|
+
writeCommandEnvelope(envelope, {
|
|
544
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
545
|
+
...(options.output ? { output: options.output } : {}),
|
|
546
|
+
plainResult: servicesNextPlainResult(envelope.result),
|
|
547
|
+
});
|
|
182
548
|
}
|
|
183
549
|
export async function runServicesList(backend, options = {}) {
|
|
184
|
-
const
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
execution: model.execution,
|
|
188
|
-
agent_guidance: buildServiceReadModelGuidance(model),
|
|
189
|
-
})));
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const out = resolveOutput(options.output);
|
|
193
|
-
for (const model of response.executions) {
|
|
194
|
-
const guidance = buildServiceReadModelGuidance(model);
|
|
195
|
-
out(`${guidance.summary}\n`);
|
|
196
|
-
const next = guidance.next_actions[0];
|
|
197
|
-
if (next)
|
|
198
|
-
out(`next: ${next.command}\n`);
|
|
550
|
+
const limit = options.limit ?? 10;
|
|
551
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
552
|
+
throw new CommandContractError("limit_invalid", "--limit must be an integer from 1 to 100", "使用 1 到 100 的整数 limit;本次未读取服务端列表。", [{ command: "itpay services list --limit 10 --json", reason: "使用默认上限重试" }]);
|
|
199
553
|
}
|
|
554
|
+
const response = await backend.listServiceExecutions(limit);
|
|
555
|
+
const executions = response.executions.map(({ execution }) => ({
|
|
556
|
+
service_execution_id: execution.service_execution_id,
|
|
557
|
+
service_id: execution.service_id,
|
|
558
|
+
status: execution.status,
|
|
559
|
+
phase: execution.phase,
|
|
560
|
+
updated_at: execution.updated_at,
|
|
561
|
+
}));
|
|
562
|
+
const latest = executions[0];
|
|
563
|
+
const envelope = {
|
|
564
|
+
status: latest ? "listed" : "no_executions",
|
|
565
|
+
result: { executions },
|
|
566
|
+
instruction: latest
|
|
567
|
+
? "结果按最新到最旧排列,默认只列最近 10 条;找不到目标时再扩大 limit。"
|
|
568
|
+
: "当前设备没有可恢复的 Service Execution;先读取已发布目录,不要猜测 ID。",
|
|
569
|
+
next: latest
|
|
570
|
+
? { command: `itpay services next ${latest.service_execution_id} --json`, reason: "默认恢复最新执行" }
|
|
571
|
+
: { command: "itpay catalog list --json", reason: "选择已发布服务" },
|
|
572
|
+
recovery: [],
|
|
573
|
+
};
|
|
574
|
+
writeCommandEnvelope(envelope, {
|
|
575
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
576
|
+
...(options.output ? { output: options.output } : {}),
|
|
577
|
+
plainResult: executions.map((execution) => `${execution.service_execution_id}: ${execution.service_id} ${execution.status}/${execution.phase} updated=${execution.updated_at}`),
|
|
578
|
+
});
|
|
200
579
|
}
|
|
201
580
|
export async function runServicesReadResult(backend, serviceExecutionID, options = {}) {
|
|
202
|
-
|
|
581
|
+
const envelope = grantedResultEnvelope(await backend.getGrantedServiceResult(serviceExecutionID));
|
|
582
|
+
writeCommandEnvelope(envelope, {
|
|
583
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
584
|
+
...(options.output ? { output: options.output } : {}),
|
|
585
|
+
plainResult: servicesNextPlainResult(envelope.result),
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
function servicesNextEnvelope(model) {
|
|
589
|
+
const execution = model.execution;
|
|
590
|
+
const lockedRefund = model.refunds.find((refund) => refund.access_locked);
|
|
591
|
+
if (lockedRefund) {
|
|
592
|
+
const terminal = lockedRefund.status === "succeeded";
|
|
593
|
+
return {
|
|
594
|
+
status: "delivery_locked",
|
|
595
|
+
result: {
|
|
596
|
+
service_execution_id: execution.service_execution_id,
|
|
597
|
+
access_locked: true,
|
|
598
|
+
refund: {
|
|
599
|
+
refund_request_id: lockedRefund.refund_request_id,
|
|
600
|
+
status: lockedRefund.status,
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
instruction: terminal
|
|
604
|
+
? "退款已成功,交付永久关闭;不要 reveal、创建 grant 或读取结果。"
|
|
605
|
+
: "退款处理中,交付已冻结;不要 reveal、创建 grant 或读取结果。",
|
|
606
|
+
next: terminal ? null : {
|
|
607
|
+
command: `itpay refund get ${lockedRefund.refund_request_id} --json`,
|
|
608
|
+
reason: "读取退款权威状态",
|
|
609
|
+
},
|
|
610
|
+
recovery: [],
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
if (isTerminalServiceExecutionStatus(execution.status)) {
|
|
614
|
+
return {
|
|
615
|
+
status: execution.status,
|
|
616
|
+
result: {
|
|
617
|
+
service_execution_id: execution.service_execution_id,
|
|
618
|
+
service_id: execution.service_id,
|
|
619
|
+
phase: execution.phase,
|
|
620
|
+
},
|
|
621
|
+
instruction: execution.status === "refunded"
|
|
622
|
+
? "该服务执行已退款并永久结束;不要重放 capability 或创建 Checkout。"
|
|
623
|
+
: "该服务执行已结束;不要重放 capability 或创建 Checkout。",
|
|
624
|
+
next: null,
|
|
625
|
+
recovery: [{
|
|
626
|
+
command: `itpay services events ${execution.service_execution_id} --json`,
|
|
627
|
+
reason: "仅在需要诊断终止原因时读取事件",
|
|
628
|
+
}],
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
const currentItems = model.current_result_items ?? [];
|
|
632
|
+
const delivery = model.current_delivery ?? model.delivery_bindings.at(-1);
|
|
633
|
+
const deliveryMode = serviceDeliveryMode(model);
|
|
634
|
+
const candidateSelection = model.allowed_actions?.find((action) => action.type === "select_candidate");
|
|
635
|
+
if (candidateSelection && currentItems.length > 0) {
|
|
636
|
+
const paidCapability = delivery?.capability_id
|
|
637
|
+
? model.capabilities.find((capability) => capability.capability_id === delivery.capability_id && capability.requires_payment)
|
|
638
|
+
: undefined;
|
|
639
|
+
return {
|
|
640
|
+
status: "candidate_selection_available",
|
|
641
|
+
result: {
|
|
642
|
+
service_execution_id: execution.service_execution_id,
|
|
643
|
+
...(delivery?.capability_id ? { capability_id: delivery.capability_id } : {}),
|
|
644
|
+
...(deliveryMode ? { delivery_mode: deliveryMode } : {}),
|
|
645
|
+
items: currentItems.map((item) => ({
|
|
646
|
+
rank: item.rank,
|
|
647
|
+
title: item.display_title,
|
|
648
|
+
safe_payload: item.safe_payload,
|
|
649
|
+
})),
|
|
650
|
+
},
|
|
651
|
+
instruction: paidCapability
|
|
652
|
+
? "付费 Agent-visible 搜索已完成。现在把 items 中的编号、title 和 safe_payload 展示给用户,然后停止;不要调用 read-result。若用户目标只是候选搜索,任务已经完成。只有用户之后明确选择某个候选并要求继续时,才执行 next.command;不要自动购买后续报告。"
|
|
653
|
+
: "向用户展示编号和 safe_payload;若候选列表已满足用户目标,在此停止。仅在用户明确选择并希望继续时,才在当前 Execution 提交对应 rank。",
|
|
654
|
+
next: {
|
|
655
|
+
command: `itpay services action ${execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
|
|
656
|
+
reason: paidCapability ? "仅在用户明确选择候选并要求继续时执行" : "仅在用户明确选择后锁定来源候选",
|
|
657
|
+
},
|
|
658
|
+
recovery: [],
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (deliveryMode === "agent_visible_result") {
|
|
662
|
+
const items = currentItems.map((item) => ({
|
|
663
|
+
rank: item.rank,
|
|
664
|
+
title: item.display_title,
|
|
665
|
+
safe_payload: item.safe_payload,
|
|
666
|
+
}));
|
|
667
|
+
const selection = model.allowed_actions?.find((action) => action.type === "select_candidate");
|
|
668
|
+
return {
|
|
669
|
+
status: items.length > 0 ? "result_ready" : "no_result",
|
|
670
|
+
result: {
|
|
671
|
+
service_execution_id: execution.service_execution_id,
|
|
672
|
+
...(delivery?.capability_id ? { capability_id: delivery.capability_id } : {}),
|
|
673
|
+
delivery_mode: deliveryMode,
|
|
674
|
+
items,
|
|
675
|
+
},
|
|
676
|
+
instruction: items.length > 0
|
|
677
|
+
? selection
|
|
678
|
+
? "Agent-visible 搜索已完成。向用户展示 items 中的编号、title 和 safe_payload,然后停止;不要调用 read-result。只有用户明确选择候选并要求继续时,才执行 next.command。"
|
|
679
|
+
: "这是当前 Graph 步骤对应的交付;结果已可供 Agent 使用,只使用 safe_payload。"
|
|
680
|
+
: "Agent-visible 交付已完成但没有结果项;不要调用 read-result 或重放当前 execution。",
|
|
681
|
+
next: selection ? {
|
|
682
|
+
command: `itpay services action ${execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
|
|
683
|
+
reason: "仅在用户明确选择后锁定来源候选",
|
|
684
|
+
} : null,
|
|
685
|
+
recovery: items.length > 0 ? [] : [{ command: `itpay services get ${execution.service_execution_id} --json`, reason: "检查交付时间线" }],
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
if (deliveryMode === "vault_artifact") {
|
|
689
|
+
const grantStatus = normalizeGrantStatus(delivery?.grant_status);
|
|
690
|
+
const grantActive = grantStatus === "active";
|
|
691
|
+
return {
|
|
692
|
+
status: grantActive ? "grant_active" : "human_authorization_required",
|
|
693
|
+
result: {
|
|
694
|
+
service_execution_id: execution.service_execution_id,
|
|
695
|
+
...(delivery?.capability_id ? { capability_id: delivery.capability_id } : {}),
|
|
696
|
+
delivery_mode: deliveryMode,
|
|
697
|
+
grant_status: grantStatus,
|
|
698
|
+
...(grantActive && delivery?.grant_expires_at ? { grant_expires_at: delivery.grant_expires_at } : {}),
|
|
699
|
+
},
|
|
700
|
+
instruction: grantActive
|
|
701
|
+
? "这是当前 Graph 步骤对应的交付;用户授权有效,立即读取并遵守字段范围与到期时间。"
|
|
702
|
+
: "这是当前 Graph 步骤对应的交付;请用户在订单页面授权,未授权前不要读取或猜测内容。",
|
|
703
|
+
next: {
|
|
704
|
+
command: `itpay services read-result ${execution.service_execution_id} --json`,
|
|
705
|
+
reason: grantActive ? "读取当前有效 grant 的结果" : "仅在用户确认授权后执行",
|
|
706
|
+
},
|
|
707
|
+
recovery: [],
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
const allowedActions = model.allowed_actions ?? [];
|
|
711
|
+
const preferred = allowedActions[0];
|
|
712
|
+
if (preferred?.type === "prepare_quote") {
|
|
713
|
+
const continuation = paidContinuation(model, preferred, {});
|
|
714
|
+
if (continuation) {
|
|
715
|
+
return {
|
|
716
|
+
status: execution.status,
|
|
717
|
+
result: {
|
|
718
|
+
service_execution_id: execution.service_execution_id,
|
|
719
|
+
service_id: execution.service_id,
|
|
720
|
+
phase: execution.phase,
|
|
721
|
+
checkout: continuation.checkout,
|
|
722
|
+
},
|
|
723
|
+
instruction: purchaseConfirmationInstruction(execution.status === "quota_exhausted" ? "quota_exhausted" : "candidate_selected", continuation.price, continuation.capability.delivery_email_required),
|
|
724
|
+
next: continuation.next,
|
|
725
|
+
recovery: [],
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
const next = preferred ? serviceAllowedActionCommand(model, preferred) : null;
|
|
730
|
+
return {
|
|
731
|
+
status: execution.status,
|
|
732
|
+
result: {
|
|
733
|
+
service_execution_id: execution.service_execution_id,
|
|
734
|
+
service_id: execution.service_id,
|
|
735
|
+
phase: execution.phase,
|
|
736
|
+
allowed_actions: allowedActions.map((action) => ({
|
|
737
|
+
type: action.type,
|
|
738
|
+
...(action.capability_id ? { capability_id: action.capability_id } : {}),
|
|
739
|
+
requires_human: action.requires_human,
|
|
740
|
+
})),
|
|
741
|
+
},
|
|
742
|
+
instruction: preferred?.type === "resume_checkout"
|
|
743
|
+
? "当前 Execution 已经有一笔 Checkout。不要创建新的 Quote、Cart、Checkout 或 Execution。现在只执行 next.command,恢复并展示同一 Checkout 的付款入口。"
|
|
744
|
+
: preferred?.type === "wait"
|
|
745
|
+
? "付款已确认,Provider 正在处理当前 Execution。不要新建 Execution、Checkout 或再次付款;稍后只执行 next.command 查询同一 Execution。"
|
|
746
|
+
: preferred?.requires_human
|
|
747
|
+
? "当前下一步需要用户明确选择;先展示必要信息并等待确认。"
|
|
748
|
+
: preferred ? "执行服务端返回的唯一首选动作;不要猜测其他 capability。" : "当前没有后续动作。",
|
|
749
|
+
next,
|
|
750
|
+
recovery: [{ command: `itpay services get ${execution.service_execution_id} --json`, reason: "仅在当前动作异常时检查时间线" }],
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function serviceAllowedActionCommand(model, action) {
|
|
754
|
+
const executionID = model.execution.service_execution_id;
|
|
755
|
+
const capability = action.capability_id
|
|
756
|
+
? model.capabilities.find((item) => item.capability_id === action.capability_id)
|
|
757
|
+
: undefined;
|
|
758
|
+
switch (action.type) {
|
|
759
|
+
case "invoke_capability": {
|
|
760
|
+
if (!capability)
|
|
761
|
+
return null;
|
|
762
|
+
const input = Object.fromEntries(requiredInputFields(capability.input_schema).map((field) => [field, "<value>"]));
|
|
763
|
+
return {
|
|
764
|
+
command: `itpay services invoke ${executionID} --capability ${capability.capability_id}${formatInputOptions(input)} --json`,
|
|
765
|
+
reason: "执行当前允许的 Agent-visible capability",
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
case "select_candidate":
|
|
769
|
+
return {
|
|
770
|
+
command: `itpay services action ${executionID} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
|
|
771
|
+
reason: "仅在用户明确选择后提交当前候选 rank",
|
|
772
|
+
};
|
|
773
|
+
case "prepare_quote": {
|
|
774
|
+
return paidContinuation(model, action, {})?.next ?? null;
|
|
775
|
+
}
|
|
776
|
+
case "resume_checkout":
|
|
777
|
+
return { command: `itpay services checkout ${executionID} --resume --json`, reason: "恢复同一 Checkout,不创建第二笔" };
|
|
778
|
+
case "wait":
|
|
779
|
+
return { command: `itpay services next ${executionID} --json`, reason: "等待 durable execution 推进" };
|
|
780
|
+
case "view_delivery":
|
|
781
|
+
return { command: `itpay services next ${executionID} --json`, reason: "读取当前交付模式" };
|
|
782
|
+
default:
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function serviceDeliveryMode(model) {
|
|
787
|
+
const delivery = model.current_delivery ?? model.delivery_bindings.at(-1);
|
|
788
|
+
const explicit = String(delivery?.redacted_summary?.delivery_mode ?? "");
|
|
789
|
+
if (explicit)
|
|
790
|
+
return explicit;
|
|
791
|
+
return delivery?.vault_artifact_id ? "vault_artifact" : "";
|
|
792
|
+
}
|
|
793
|
+
function normalizeGrantStatus(status) {
|
|
794
|
+
return !status || status === "missing" ? "none" : status;
|
|
795
|
+
}
|
|
796
|
+
function grantedResultEnvelope(response) {
|
|
797
|
+
return {
|
|
798
|
+
status: "granted_result_ready",
|
|
799
|
+
result: {
|
|
800
|
+
service_execution_id: response.service_execution_id,
|
|
801
|
+
...(response.expires_at ? { grant_expires_at: response.expires_at } : {}),
|
|
802
|
+
granted_fields: Object.keys(response.result),
|
|
803
|
+
payload: response.result,
|
|
804
|
+
},
|
|
805
|
+
instruction: "结果来自当前有效 Vault Grant;只使用本次授权字段,过期后停止读取并重新请求用户同意。",
|
|
806
|
+
next: null,
|
|
807
|
+
recovery: [],
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
function servicesNextPlainResult(result) {
|
|
811
|
+
const lines = [];
|
|
812
|
+
for (const [key, value] of Object.entries(result)) {
|
|
813
|
+
if (key === "items" && Array.isArray(value)) {
|
|
814
|
+
lines.push("items:");
|
|
815
|
+
for (const item of value) {
|
|
816
|
+
lines.push(` ${item.rank}. ${item.title}`);
|
|
817
|
+
for (const [field, fieldValue] of Object.entries(item.safe_payload)) {
|
|
818
|
+
lines.push(` ${field}: ${typeof fieldValue === "string" ? fieldValue : JSON.stringify(fieldValue)}`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
lines.push(`${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
|
|
824
|
+
}
|
|
825
|
+
return lines;
|
|
203
826
|
}
|
|
204
827
|
export async function runServicesEvents(backend, serviceExecutionID, options = {}) {
|
|
205
|
-
|
|
828
|
+
const afterSequence = options.afterSequence ?? 0;
|
|
829
|
+
const limit = options.limit ?? 50;
|
|
830
|
+
if (!serviceExecutionID.trim()) {
|
|
831
|
+
throw new CommandContractError("service_execution_id_required", "service execution id is required", "使用 services list 返回的 execution ID;不要猜测。", [{ command: "itpay services list --json", reason: "列出当前身份可见执行" }]);
|
|
832
|
+
}
|
|
833
|
+
if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) {
|
|
834
|
+
throw new CommandContractError("events_parameter_invalid", "after_sequence must be a non-negative integer", "--after-sequence 必须是非负整数;本次未读取事件。", [{ command: `itpay services events ${serviceExecutionID} --help`, reason: "查看诊断参数" }]);
|
|
835
|
+
}
|
|
836
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
837
|
+
throw new CommandContractError("events_parameter_invalid", "limit must be an integer between 1 and 100", "--limit 必须是 1 到 100 的整数;本次未读取事件。", [{ command: `itpay services events ${serviceExecutionID} --help`, reason: "查看诊断参数" }]);
|
|
838
|
+
}
|
|
839
|
+
const response = await backend.listServiceExecutionEvents(serviceExecutionID, afterSequence, limit);
|
|
840
|
+
const events = response.events.map((event) => ({
|
|
841
|
+
sequence: event.sequence,
|
|
842
|
+
type: event.type,
|
|
843
|
+
status: event.status,
|
|
844
|
+
phase: event.phase,
|
|
845
|
+
...(event.capability_id ? { capability_id: event.capability_id } : {}),
|
|
846
|
+
occurred_at: event.occurred_at,
|
|
847
|
+
}));
|
|
848
|
+
writeCommandEnvelope({
|
|
849
|
+
status: "listed",
|
|
850
|
+
result: {
|
|
851
|
+
service_execution_id: serviceExecutionID,
|
|
852
|
+
after_sequence: afterSequence,
|
|
853
|
+
returned_count: events.length,
|
|
854
|
+
events,
|
|
855
|
+
},
|
|
856
|
+
instruction: "事件仅用于诊断;不要从事件重放业务步骤,回到 services next 获取当前动作。",
|
|
857
|
+
next: {
|
|
858
|
+
command: `itpay services next ${serviceExecutionID} --json`,
|
|
859
|
+
reason: "恢复正常服务流程",
|
|
860
|
+
},
|
|
861
|
+
recovery: events.length === limit && events.length > 0
|
|
862
|
+
? [{
|
|
863
|
+
command: `itpay services events ${serviceExecutionID} --after-sequence ${events.at(-1).sequence} --limit ${limit} --json`,
|
|
864
|
+
reason: "继续读取下一页诊断事件",
|
|
865
|
+
}]
|
|
866
|
+
: [],
|
|
867
|
+
}, {
|
|
868
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
869
|
+
...(options.output ? { output: options.output } : {}),
|
|
870
|
+
plainResult: [
|
|
871
|
+
`service_execution_id: ${serviceExecutionID}`,
|
|
872
|
+
`returned_count: ${events.length}`,
|
|
873
|
+
...events.map((event) => `${event.sequence} ${event.occurred_at} ${event.type} ${event.status}/${event.phase}`),
|
|
874
|
+
],
|
|
875
|
+
});
|
|
206
876
|
}
|
|
207
877
|
export function parseKeyValueList(values) {
|
|
208
878
|
const result = {};
|
|
@@ -228,81 +898,63 @@ function parseValue(value) {
|
|
|
228
898
|
return Number(value);
|
|
229
899
|
return value;
|
|
230
900
|
}
|
|
231
|
-
function
|
|
232
|
-
resolveOutput(output)(JSON.stringify(value, null, 2) + "\n");
|
|
233
|
-
}
|
|
234
|
-
function buildServicesCheckoutJSON(response, checkoutURL, plan) {
|
|
901
|
+
function buildServicesCheckoutEnvelope(response, checkoutURL, plan, baseURL) {
|
|
235
902
|
const checkout = response.checkout;
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
};
|
|
252
|
-
const nextActions = [{
|
|
253
|
-
id: "open_human_checkout",
|
|
254
|
-
label: "Show the ItPay checkout URL or branded QR to the human",
|
|
255
|
-
command: `itpay checkout --id ${checkout.checkout.checkout_id} --token ${checkout.display_token}`,
|
|
256
|
-
requires_human: true,
|
|
257
|
-
reason: "The human must review and pay on the ItPay checkout page.",
|
|
258
|
-
}];
|
|
259
|
-
output.next_actions = nextActions;
|
|
260
|
-
output.agent_guidance = {
|
|
261
|
-
kind: "checkout_handoff",
|
|
262
|
-
summary: `checkout ${checkout.checkout.checkout_id}: show the ItPay checkout handoff to the human`,
|
|
263
|
-
state: {
|
|
903
|
+
const platform = platformKeyForHost(plan.host);
|
|
904
|
+
const handoff = { url: checkoutURL };
|
|
905
|
+
if (plan.ideImageAttach?.status === "downloaded" && plan.ideImageAttach.localPath) {
|
|
906
|
+
handoff.qr_local_path = plan.ideImageAttach.localPath;
|
|
907
|
+
}
|
|
908
|
+
if (platform === "markdown") {
|
|
909
|
+
handoff.markdown = buildAgentChatHandoff(plan).markdown;
|
|
910
|
+
}
|
|
911
|
+
else if (platform === "plain_chat" && checkout.qr_png_url) {
|
|
912
|
+
handoff.qr_image_url = absolutePublicURL(baseURL, checkout.qr_png_url);
|
|
913
|
+
}
|
|
914
|
+
const amount = formatMoney(checkout.checkout.amount_minor, checkout.checkout.currency);
|
|
915
|
+
return {
|
|
916
|
+
status: "human_checkout_required",
|
|
917
|
+
result: {
|
|
264
918
|
service_execution_id: response.binding.service_execution_id,
|
|
265
919
|
checkout_id: checkout.checkout.checkout_id,
|
|
266
|
-
|
|
920
|
+
capability_id: checkoutCapabilityID(response),
|
|
921
|
+
locked_input: response.locked_input,
|
|
922
|
+
amount,
|
|
267
923
|
},
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
924
|
+
handoff,
|
|
925
|
+
instruction: checkoutInstruction(platform, amount),
|
|
926
|
+
next: {
|
|
927
|
+
command: `itpay checkout --id ${checkout.checkout.checkout_id} --token ${checkout.display_token} --json`,
|
|
928
|
+
reason: "仅在用户完成付款操作或要求查询后,读取同一 Checkout 的权威状态",
|
|
929
|
+
},
|
|
930
|
+
recovery: [],
|
|
274
931
|
};
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (plan.ideImageAttach.caption) {
|
|
293
|
-
output.brand_qr_caption = plan.ideImageAttach.caption;
|
|
294
|
-
}
|
|
295
|
-
if (plan.ideImageAttach.error) {
|
|
296
|
-
output.brand_qr_error = plan.ideImageAttach.error;
|
|
297
|
-
}
|
|
932
|
+
}
|
|
933
|
+
function checkoutCapabilityID(response, fallback = "") {
|
|
934
|
+
return response.capability_id || fallback;
|
|
935
|
+
}
|
|
936
|
+
function checkoutInstruction(platform, amount) {
|
|
937
|
+
if (platform === "markdown")
|
|
938
|
+
return `把 handoff.markdown 原样发送到当前桌面对话,确认二维码、付款链接和金额 ${amount} 都已实际对用户可见,然后停止等待。不要立即执行 next.command,不要创建第二个 Checkout、Execution 或调用 pay;用户完成付款操作或要求查询后,只执行 next.command。`;
|
|
939
|
+
if (platform === "terminal")
|
|
940
|
+
return `在用户可见终端展示二维码、付款链接和金额 ${amount},然后停止等待。不要立即执行 next.command,不要创建第二个 Checkout、Execution 或调用 pay;用户完成付款操作或要求查询后,只执行 next.command。`;
|
|
941
|
+
return `现在只做以下动作:1)把 handoff.url 作为可点击付款链接发送给用户;2)优先把 handoff.qr_local_path 作为图片附件发送,不能发送本地附件时使用 handoff.qr_image_url;3)明确告诉用户本次金额是 ${amount};4)发送完成后停止并等待用户操作。不要立即执行 next.command,不要创建第二个 Checkout,不要新建 Execution,不要调用 pay。用户表示已经完成付款或要求查询状态后,只执行 next.command;用户的话本身不是付款成功证明。`;
|
|
942
|
+
}
|
|
943
|
+
function absolutePublicURL(baseURL, value) {
|
|
944
|
+
try {
|
|
945
|
+
return new URL(value, baseURL.endsWith("/") ? baseURL : `${baseURL}/`).toString();
|
|
946
|
+
}
|
|
947
|
+
catch {
|
|
948
|
+
return value;
|
|
298
949
|
}
|
|
299
|
-
output.agent_action = buildAgentChatHandoff(plan);
|
|
300
|
-
return output;
|
|
301
950
|
}
|
|
302
|
-
function
|
|
951
|
+
function formatMoney(amountMinor, currency) {
|
|
952
|
+
return `${(amountMinor / 100).toFixed(2)} ${currency}`;
|
|
953
|
+
}
|
|
954
|
+
function normalizeServiceActionStatus(status, serviceExecutionID) {
|
|
303
955
|
const normalized = status.trim().toLowerCase();
|
|
304
956
|
if (!serviceActionStatuses.has(normalized)) {
|
|
305
|
-
throw
|
|
957
|
+
throw actionInputError(serviceExecutionID, `invalid --status "${status}". Supported: pending, approved, rejected, expired, cancelled`);
|
|
306
958
|
}
|
|
307
959
|
return normalized;
|
|
308
960
|
}
|