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