@itpay/cli 2.1.33 → 2.1.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/client/backend.js +5 -0
- package/dist/src/commands/aftercare.js +12 -0
- package/dist/src/commands/buy.js +12 -56
- package/dist/src/commands/cart.js +2 -0
- package/dist/src/commands/catalog.js +24 -9
- package/dist/src/commands/checkout_handoff.js +11 -99
- package/dist/src/commands/guidance.js +1 -184
- package/dist/src/commands/install.js +2 -1
- package/dist/src/commands/order.js +20 -2
- package/dist/src/commands/rail_catalog.js +1 -261
- package/dist/src/commands/readyz.js +2 -2
- package/dist/src/commands/refund.js +10 -54
- package/dist/src/commands/services/actions.js +133 -0
- package/dist/src/commands/services/commerce.js +197 -0
- package/dist/src/commands/services/lifecycle.js +418 -0
- package/dist/src/commands/services/results.js +130 -0
- package/dist/src/commands/services.js +14 -2584
- package/dist/src/commands/skill.js +3 -0
- package/dist/src/main.js +82 -31
- package/dist/src/render/checkout_handoff.js +101 -0
- package/dist/src/render/checkout_plan.js +47 -0
- package/dist/src/services/adapter.js +1 -0
- package/dist/src/services/presentation.js +198 -0
- package/dist/src/services/rail/actions.js +51 -0
- package/dist/src/services/rail/adapter.js +27 -0
- package/dist/src/services/rail/aftercare.js +45 -0
- package/dist/src/services/rail/catalog.js +261 -0
- package/dist/src/services/rail/checkout.js +19 -0
- package/dist/src/services/rail/fulfillment.js +76 -0
- package/dist/src/services/rail/guide.js +110 -0
- package/dist/src/services/rail/invocation.js +54 -0
- package/dist/src/services/rail/page.js +76 -0
- package/dist/src/services/rail/pagination.js +6 -0
- package/dist/src/services/rail/query.js +71 -0
- package/dist/src/services/rail/readers.js +107 -0
- package/dist/src/services/rail/reading.js +125 -0
- package/dist/src/services/rail/recovery.js +51 -0
- package/dist/src/services/rail/results.js +378 -0
- package/dist/src/services/rail/review.js +268 -0
- package/dist/src/services/rail/saved.js +49 -0
- package/dist/src/services/rail/wait.js +11 -0
- package/dist/src/services/registry.js +31 -0
- package/dist/src/services/schema.js +21 -0
- package/dist/src/services/shared/envelope.js +184 -0
- package/dist/src/services/shared/input.js +39 -0
- package/dist/src/services/shared/pagination.js +28 -0
- package/dist/src/services/shared/platform.js +365 -0
- package/dist/src/services/shared/refund.js +40 -0
- package/dist/src/services/shared/workflow.js +28 -0
- package/dist/src/state/account_auth.js +25 -10
- package/dist/src/state/config.js +2 -2
- package/dist/src/state/device_authority.js +5 -3
- package/docs/agent/buyer/cart-checkout.json +2 -1
- package/docs/agent/buyer/catalog-list.json +6 -4
- package/docs/agent/buyer/identity-and-sessions.json +3 -2
- package/docs/agent/buyer/install-and-setup.json +8 -3
- package/docs/agent/buyer/orders-refunds.json +14 -5
- package/docs/agent/buyer/payment-flow.json +2 -1
- package/docs/agent/buyer/quickstart.json +3 -2
- package/docs/agent/buyer/rail-booking.json +41 -27
- package/docs/agent/buyer/render-hosts.json +1 -1
- package/docs/agent/buyer/service-feedback.json +2 -2
- package/docs/cli-reference/commands/aftercare.md +15 -0
- package/docs/cli-reference/commands/cart/add.md +4 -0
- package/docs/cli-reference/commands/cart/next.md +4 -0
- package/docs/cli-reference/commands/readyz.md +2 -50
- package/docs/cli-reference/commands/services/checkout.md +4 -0
- package/docs/cli-reference/commands/services/next.md +8 -35
- package/docs/cli-reference/commands/services/quote.md +1 -1
- package/docs/cli-reference/commands/services/run.md +8 -12
- package/docs/cli-reference/commands/services/start.md +3 -1
- package/docs/cli-reference/conventions.md +1 -1
- package/docs/cli-reference/index.md +2 -0
- package/package.json +4 -3
- package/skills/itpay/SKILL.md +53 -19
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { assertCheckoutPayable } from "../checkout_handoff.js";
|
|
2
|
+
import { serviceAdapter } from "../../services/registry.js";
|
|
3
|
+
import { HttpError } from "../../client/http.js";
|
|
4
|
+
import { validateContext } from "../../state/client_context.js";
|
|
5
|
+
import { dispatchRender } from "../../render/index.js";
|
|
6
|
+
import { ensureIdeImageAttach } from "../../render/ide.js";
|
|
7
|
+
import { shouldPrepareLocalCheckoutImage } from "../../render/checkout_handoff.js";
|
|
8
|
+
import { localizeCardURL, normalizeCardLocale } from "../../render/locale.js";
|
|
9
|
+
import { platformKeyForHost } from "../../render/plan.js";
|
|
10
|
+
import { renderTerminalQR } from "../../render/qr.js";
|
|
11
|
+
import { buildCheckoutQRPlan } from "../../render/checkout_plan.js";
|
|
12
|
+
import { CommandContractError, writeCommandEnvelope, plainValueLines } from "../../services/shared/envelope.js";
|
|
13
|
+
import { missingRequiredInput, checkoutCommand, formatInputOptions, tokenizedCheckoutURL, absolutePublicURL, fallbackCardURL, fallbackCardPNGURL, buildServicesCheckoutEnvelope, quoteCommand, formatMoney } from "../../services/shared/platform.js";
|
|
14
|
+
export async function runServicesCheckout(backend, config, serviceExecutionID, capabilityID, options = {}) {
|
|
15
|
+
const host = options.host ?? "terminal";
|
|
16
|
+
const contextError = validateContext(host, options.target);
|
|
17
|
+
if (contextError) {
|
|
18
|
+
throw new CommandContractError(contextError.code, contextError.message, "从当前可信会话上下文补齐 Host/target;本次未创建 Checkout。", []);
|
|
19
|
+
}
|
|
20
|
+
const deliveryContact = {
|
|
21
|
+
...(options.deliveryContact ?? {}),
|
|
22
|
+
...(options.email ? { email: options.email } : {}),
|
|
23
|
+
};
|
|
24
|
+
if (!options.resume && !capabilityID) {
|
|
25
|
+
const model = await backend.getServiceExecution(serviceExecutionID);
|
|
26
|
+
capabilityID = model.workflow_entry?.capability_id;
|
|
27
|
+
}
|
|
28
|
+
if (!options.resume && !capabilityID) {
|
|
29
|
+
throw new CommandContractError("capability_required", "--capability is required when creating a service checkout", "使用当前 Service Execution 返回的付费 capability;恢复已有 Checkout 时改用 --resume。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前允许的付费 capability" }]);
|
|
30
|
+
}
|
|
31
|
+
if (!options.resume) {
|
|
32
|
+
const readModel = await backend.getServiceExecution(serviceExecutionID);
|
|
33
|
+
const capability = readModel.capabilities.find((item) => item.capability_id === capabilityID);
|
|
34
|
+
if (!capability || !capability.requires_payment) {
|
|
35
|
+
throw new CommandContractError("capability_not_checkoutable", `capability ${capabilityID} is not available for checkout on service execution ${serviceExecutionID}`, "只为当前 Service Execution 返回的 requires_payment capability 创建 Checkout。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前允许的下一步" }]);
|
|
36
|
+
}
|
|
37
|
+
if (readModel.checkout_bindings.length === 0 && !readModel.actions.some(action => action.action_type === "approve_purchase" && action.status === "approved")) {
|
|
38
|
+
await runServicesQuote(backend, serviceExecutionID, capability.capability_id, options.lockedInput ?? {}, options);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const lockedInput = options.lockedInput ?? {};
|
|
42
|
+
const missingInput = missingRequiredInput(capability.input_schema, lockedInput);
|
|
43
|
+
if (missingInput.length > 0 && !readModel.workflow_entry && readModel.execution.next_action !== "create_checkout") {
|
|
44
|
+
throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐付费 capability 的 required_input;本次没有创建 quote、Checkout 或订单。", [], { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command: checkoutCommand(serviceExecutionID, capability, lockedInput), required_input: missingInput, executable: false } });
|
|
45
|
+
}
|
|
46
|
+
if (capability.delivery_email_required && readModel.workflow_entry?.capability_id !== capability.capability_id && String(deliveryContact.email ?? "").trim() === "") {
|
|
47
|
+
throw new CommandContractError("delivery_email_required", "delivery email is required before creating this service checkout", "该 capability 的交付链接会发送到用户邮箱;先向用户说明用途并询问邮箱,不要代填。", [], { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command: `itpay services checkout ${serviceExecutionID} --capability ${capability.capability_id}${formatInputOptions(lockedInput)} --email <email> --json`, required_input: ["email"], executable: false } });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let response;
|
|
51
|
+
try {
|
|
52
|
+
response = await backend.createServiceExecutionCheckout(serviceExecutionID, {
|
|
53
|
+
...(capabilityID ? { capability_id: capabilityID } : {}),
|
|
54
|
+
...(Object.keys(deliveryContact).length > 0 ? { delivery_contact: deliveryContact } : {}),
|
|
55
|
+
...(options.lockedInput && Object.keys(options.lockedInput).length > 0 ? { locked_input: options.lockedInput } : {}),
|
|
56
|
+
...(options.resume ? { resume: true } : {}),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof HttpError && error.code === "purchase_approval_required") {
|
|
61
|
+
throw new CommandContractError(error.code, error.message, "先读当前报价及完整范围模板。沿用覆盖同条件的真实批准;缺少哪部分才补哪部分,不把候选选择当作价格同意,也不代付。", [{ command: capabilityID ? `itpay services quote ${serviceExecutionID} --capability ${capabilityID}${formatInputOptions(options.lockedInput ?? {})} --json` : `itpay services next ${serviceExecutionID} --json`, reason: "读取同一执行的当前批准范围,不新建购买" }]);
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
const checkout = response.checkout;
|
|
66
|
+
const authoritative = checkout.display_token ? await backend.getCheckoutPresentation(checkout.checkout.checkout_id, checkout.display_token) : undefined;
|
|
67
|
+
assertCheckoutPayable(checkout, authoritative, response.cart.cart_id, serviceExecutionID);
|
|
68
|
+
if (authoritative)
|
|
69
|
+
response.cart.items = authoritative.items;
|
|
70
|
+
const checkoutID = checkout.checkout.checkout_id;
|
|
71
|
+
const displayToken = checkout.display_token;
|
|
72
|
+
const locale = normalizeCardLocale(options.locale);
|
|
73
|
+
const checkoutURL = tokenizedCheckoutURL(checkout.checkout_url, displayToken, checkout.qr_payload);
|
|
74
|
+
const cardURL = localizeCardURL(absolutePublicURL(config.baseURL, checkout.card_url ?? fallbackCardURL(config.baseURL, checkoutID, displayToken)), locale);
|
|
75
|
+
const cardPNGURL = localizeCardURL(absolutePublicURL(config.baseURL, checkout.card_png_url ?? checkout.qr_png_url ?? fallbackCardPNGURL(config.baseURL, checkoutID, displayToken)), locale);
|
|
76
|
+
let itineraryTitle;
|
|
77
|
+
let itinerary;
|
|
78
|
+
try {
|
|
79
|
+
const booking = await backend.getServiceExecution(serviceExecutionID);
|
|
80
|
+
({ itinerary, itineraryTitle } = await serviceAdapter(booking.execution).checkout(backend, booking, checkoutID, displayToken));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Checkout already exists; a display-only title lookup cannot hide its official handoff.
|
|
84
|
+
}
|
|
85
|
+
const plan = buildCheckoutQRPlan({
|
|
86
|
+
localSimulation: backend.localSimulation,
|
|
87
|
+
host,
|
|
88
|
+
checkoutID,
|
|
89
|
+
checkoutURL,
|
|
90
|
+
cardURL,
|
|
91
|
+
displayToken,
|
|
92
|
+
qrPayload: checkout.qr_payload,
|
|
93
|
+
qrPNGURL: cardPNGURL,
|
|
94
|
+
nextAction: checkout.checkout.next_action,
|
|
95
|
+
orderItems: response.cart.items.map((item) => ({
|
|
96
|
+
title: itineraryTitle || item.title,
|
|
97
|
+
quantity: item.quantity,
|
|
98
|
+
amountMinor: item.amount_minor,
|
|
99
|
+
currency: item.currency,
|
|
100
|
+
})),
|
|
101
|
+
orderCurrency: checkout.checkout.currency,
|
|
102
|
+
...(options.agentType ? { agentType: options.agentType } : {}),
|
|
103
|
+
locale,
|
|
104
|
+
});
|
|
105
|
+
options.persistHandoff?.({
|
|
106
|
+
serviceExecutionID,
|
|
107
|
+
cartID: response.cart.cart_id,
|
|
108
|
+
checkoutID,
|
|
109
|
+
displayToken,
|
|
110
|
+
checkoutURL,
|
|
111
|
+
});
|
|
112
|
+
const platform = platformKeyForHost(plan.host);
|
|
113
|
+
if (!options.jsonOutput && (platform === "telegram" || platform === "feishu" || platform === "lark")) {
|
|
114
|
+
await dispatchRender(plan, {
|
|
115
|
+
host,
|
|
116
|
+
...(options.target ? { target: options.target } : {}),
|
|
117
|
+
...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
|
|
118
|
+
...(options.qrFilePath ? { qrFilePath: options.qrFilePath } : {}),
|
|
119
|
+
...(options.output ? { output: options.output } : {}),
|
|
120
|
+
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
121
|
+
baseURL: config.baseURL,
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (shouldPrepareLocalCheckoutImage(platform)) {
|
|
126
|
+
await ensureIdeImageAttach(plan, {
|
|
127
|
+
enabled: config.ideImageAttach,
|
|
128
|
+
...(config.baseURL ? { baseURL: config.baseURL } : {}),
|
|
129
|
+
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const envelope = buildServicesCheckoutEnvelope(response, checkoutURL, plan, options.agentType, options.target);
|
|
133
|
+
if (itinerary) {
|
|
134
|
+
envelope.result = { ...envelope.result, itinerary };
|
|
135
|
+
envelope.instruction = "整程一次付款,各段分别出票;展示入口不等于付款,付款成功不等于全部出票。" + envelope.instruction;
|
|
136
|
+
}
|
|
137
|
+
const plainResult = plainValueLines(envelope.result);
|
|
138
|
+
if (!options.jsonOutput && platform === "terminal") {
|
|
139
|
+
plainResult.push("qr:", await renderTerminalQR(checkoutURL, options.qrFormat ?? "terminal"));
|
|
140
|
+
}
|
|
141
|
+
writeCommandEnvelope(envelope, {
|
|
142
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
143
|
+
...(options.output ? { output: options.output } : {}),
|
|
144
|
+
plainResult,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
export async function runServicesQuote(backend, serviceExecutionID, capabilityID, input, options = {}) {
|
|
148
|
+
const model = await backend.getServiceExecution(serviceExecutionID);
|
|
149
|
+
const capability = model.capabilities.find((item) => item.capability_id === capabilityID);
|
|
150
|
+
if (!capability || !capability.requires_payment) {
|
|
151
|
+
throw new CommandContractError("capability_not_quoteable", `capability ${capabilityID} is not available for quote on service execution ${serviceExecutionID}`, "只为当前 Service Execution 返回的付费 capability 创建报价。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前合法动作" }]);
|
|
152
|
+
}
|
|
153
|
+
const selectionBacked = model.execution.status === "human_action_approved" &&
|
|
154
|
+
model.allowed_actions?.some((action) => action.type === "prepare_quote" && action.capability_id === capabilityID);
|
|
155
|
+
const missingInput = missingRequiredInput(capability.input_schema, input);
|
|
156
|
+
if (missingInput.length > 0 && !selectionBacked && model.workflow_entry?.capability_id !== capability.capability_id) {
|
|
157
|
+
throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐付费 capability 输入;本次没有创建 Quote、Cart 或 Checkout。", [], { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command: quoteCommand(serviceExecutionID, capability, input), required_input: missingInput, executable: false } });
|
|
158
|
+
}
|
|
159
|
+
const deliveryContact = {
|
|
160
|
+
...(options.deliveryContact ?? {}),
|
|
161
|
+
...(options.email ? { email: options.email } : {}),
|
|
162
|
+
};
|
|
163
|
+
if (capability.delivery_email_required && model.workflow_entry?.capability_id !== capability.capability_id && String(deliveryContact.email ?? "").trim() === "") {
|
|
164
|
+
throw new CommandContractError("delivery_email_required", "delivery email is required before preparing this service quote", "交付链接会发送到用户邮箱;说明用途并询问邮箱,不要代填。", [], { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command: quoteCommand(serviceExecutionID, capability, input), required_input: ["email"], executable: false } });
|
|
165
|
+
}
|
|
166
|
+
const quote = await backend.prepareServiceQuote(serviceExecutionID, {
|
|
167
|
+
capability_id: capabilityID,
|
|
168
|
+
...(Object.keys(deliveryContact).length > 0 ? { delivery_contact: deliveryContact } : {}),
|
|
169
|
+
...(Object.keys(input).length > 0 ? { locked_input: input } : {}),
|
|
170
|
+
});
|
|
171
|
+
const result = {
|
|
172
|
+
service_quote_lock_id: quote.service_quote_lock_id,
|
|
173
|
+
service_execution_id: quote.service_execution_id,
|
|
174
|
+
capability_id: quote.capability_id,
|
|
175
|
+
price: formatMoney(quote.amount_minor, quote.currency),
|
|
176
|
+
...(quote.purchase_summary ? { purchase_summary: quote.purchase_summary } : {}),
|
|
177
|
+
expires_at: quote.expires_at,
|
|
178
|
+
...(quote.approval_template ? { approval_template: quote.approval_template } : {}),
|
|
179
|
+
};
|
|
180
|
+
writeCommandEnvelope({
|
|
181
|
+
status: "quote_ready",
|
|
182
|
+
result,
|
|
183
|
+
instruction: quote.approval_template
|
|
184
|
+
? "报价已锁定。先展示服务端商品名称、对象、范围、数量和金额;与用户请求不符时让用户决定,不自称基础报告或换商品。选择候选不是价格同意;模板中的user_approved=false表示尚未记录批准。仅在用户已有真实批准覆盖本范围时改为true并提交,不重复询问同条件已明确同意。用户授权预算时用capped及真实maximum_minor;仅委托准备入口、最终金额在官方页确认时用confirm_on_official_page。不要伪造接受未知金额或条款。"
|
|
185
|
+
: "报价已锁定当前 Execution 的可信输入和价格;可单独付款,也可与其他独立 Execution 的报价合并。",
|
|
186
|
+
...(quote.approval_template ? { interaction: { schema_version: "itpay.interaction.v1", stage: "purchase_approval_required", input_template: { command: `itpay services action ${serviceExecutionID} --action approve_purchase --actor-type agent --status approved --input-json <file> --json`, input: quote.approval_template, required_input: ["file"], executable: false } } } : {}),
|
|
187
|
+
next: quote.approval_template ? null : {
|
|
188
|
+
command: `itpay cart add --quote ${quote.service_quote_lock_id} --json`,
|
|
189
|
+
reason: "加入 canonical Cart",
|
|
190
|
+
},
|
|
191
|
+
recovery: [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "重新读取当前 Execution 状态" }],
|
|
192
|
+
}, {
|
|
193
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
194
|
+
...(options.output ? { output: options.output } : {}),
|
|
195
|
+
plainResult: plainValueLines(result),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { serviceAdapter, serviceClientFeatures } from "../../services/registry.js";
|
|
2
|
+
import { HttpError } from "../../client/http.js";
|
|
3
|
+
import { HttpTransportError } from "../../client/transport.js";
|
|
4
|
+
import { DeviceLockBusyError, DeviceStateError } from "../../state/device_authority.js";
|
|
5
|
+
import { operationID } from "../../state/config.js";
|
|
6
|
+
import { CommandContractError, isTerminalServiceExecutionStatus, writeCommandEnvelope, plainValueLines } from "../../services/shared/envelope.js";
|
|
7
|
+
import { requiredInputFields, missingRequiredInput, stableInput, formatInputOptions, queryText, capabilityPrice, purchaseConfirmationInstruction, checkoutCommand, serviceResultPlainLines, serviceDeliveryMode, journalTaskState, servicesNextPlainResult } from "../../services/shared/platform.js";
|
|
8
|
+
import { servicesNextEnvelope } from "../../services/presentation.js";
|
|
9
|
+
import { executionInputContext, operationInputTemplate } from "../../services/shared/input.js";
|
|
10
|
+
import { runServicesCheckout } from "./commerce.js";
|
|
11
|
+
export async function runServicesStart(backend, serviceID, options = {}) {
|
|
12
|
+
const host = options.host ?? "terminal";
|
|
13
|
+
const response = await backend.startServiceExecution({
|
|
14
|
+
service_id: serviceID,
|
|
15
|
+
client_context: {
|
|
16
|
+
host,
|
|
17
|
+
features: serviceClientFeatures(),
|
|
18
|
+
...(options.target ? { target: options.target } : {}),
|
|
19
|
+
...(options.clientContext ?? {}),
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
if ("overview_only" in response) {
|
|
23
|
+
writeServiceOverview(response.service_definition, options);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!serviceAdapter(response.execution).supported) {
|
|
27
|
+
writeCommandEnvelope({ status: "presentation_contract_unavailable", result: { service_execution_id: response.execution.service_execution_id, service_id: response.execution.service_id, service_contract_version_id: response.execution.service_contract_version_id }, instruction: "当前锁定合同不受支持;保留原执行与已有结果并停止,使用兼容CLI,不猜模式或输入。", next: null, recovery: [] }, options);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (response.workflow_entry) {
|
|
31
|
+
const guidance = serviceAdapter(response.execution).guidance(response.execution.service_id, response.workflow_entry.input_schema);
|
|
32
|
+
if (serviceAdapter(response.execution).requiresGuidance(serviceID) && !guidance) {
|
|
33
|
+
writeCommandEnvelope({ status: "contract_unavailable", result: { service_execution_id: response.execution.service_execution_id, service_id: serviceID }, instruction: "当前执行缺少可用输入合同;只读取一次同执行合同,仍缺失则报告系统缺口并停止,不猜缺失字段或旧输入写法。", next: { command: `itpay services get ${response.execution.service_execution_id} --json`, reason: "读取当前执行锁定的输入合同一次" }, recovery: [] }, options);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
writeCommandEnvelope({
|
|
37
|
+
status: "input_required",
|
|
38
|
+
result: { service_execution_id: response.execution.service_execution_id, service_id: serviceID, input_schema: response.workflow_entry.input_schema, ...(guidance ? { guidance } : {}) },
|
|
39
|
+
instruction: serviceAdapter(response.execution).entryInstruction,
|
|
40
|
+
next: null,
|
|
41
|
+
interaction: {
|
|
42
|
+
schema_version: "itpay.interaction.v1",
|
|
43
|
+
stage: "input_required",
|
|
44
|
+
input_template: operationInputTemplate(executionInputContext(response.execution), guidance?.input_example ?? serviceAdapter(response.execution).inputTemplate(response.workflow_entry.input_schema), requiredInputFields(response.workflow_entry.input_schema)),
|
|
45
|
+
},
|
|
46
|
+
recovery: []
|
|
47
|
+
}, { ...options });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const capability = response.capabilities.find((item) => item.phase === response.execution.phase && !item.requires_payment);
|
|
51
|
+
const requiredInput = requiredInputFields(capability?.input_schema);
|
|
52
|
+
const needsInput = requiredInput.length > 0;
|
|
53
|
+
const command = capability && !needsInput
|
|
54
|
+
? `itpay services invoke ${response.execution.service_execution_id} --capability ${capability.capability_id} --json`
|
|
55
|
+
: `itpay services next ${response.execution.service_execution_id} --json`;
|
|
56
|
+
const capabilitySummary = capability ? {
|
|
57
|
+
capability_id: capability.capability_id,
|
|
58
|
+
required_input: requiredInput,
|
|
59
|
+
input_schema: capability.input_schema,
|
|
60
|
+
...(capability.free_quota_limit !== undefined ? { free_quota_limit: capability.free_quota_limit } : {}),
|
|
61
|
+
} : null;
|
|
62
|
+
writeCommandEnvelope({
|
|
63
|
+
status: "ready",
|
|
64
|
+
result: {
|
|
65
|
+
service_execution_id: response.execution.service_execution_id,
|
|
66
|
+
service_id: response.execution.service_id,
|
|
67
|
+
phase: response.execution.phase,
|
|
68
|
+
capability: capabilitySummary,
|
|
69
|
+
},
|
|
70
|
+
instruction: capability
|
|
71
|
+
? "填写首选 capability 的 required_input;一次只提交当前 execution 所代表的服务意图。" + serviceAdapter(response.execution).locationInstruction(capability.input_schema)
|
|
72
|
+
: "当前没有可直接调用的 capability;读取服务端下一步,不要猜测 capability。",
|
|
73
|
+
next: {
|
|
74
|
+
command,
|
|
75
|
+
reason: capability && !needsInput ? "执行当前允许的能力" : "读取服务端计算的下一步",
|
|
76
|
+
},
|
|
77
|
+
...(capability && needsInput ? {
|
|
78
|
+
interaction: {
|
|
79
|
+
schema_version: "itpay.interaction.v1",
|
|
80
|
+
stage: "input_required",
|
|
81
|
+
input_template: {
|
|
82
|
+
command: `itpay services invoke ${response.execution.service_execution_id} --capability ${capability.capability_id}${requiredInput.map((field) => ` --input ${field}=<value>`).join("")} --json`,
|
|
83
|
+
required_input: requiredInput,
|
|
84
|
+
executable: false,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
} : {}),
|
|
88
|
+
recovery: [],
|
|
89
|
+
}, {
|
|
90
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
91
|
+
...(options.output ? { output: options.output } : {}),
|
|
92
|
+
plainResult: [
|
|
93
|
+
`service_execution_id: ${response.execution.service_execution_id}`,
|
|
94
|
+
`service_id: ${response.execution.service_id}`,
|
|
95
|
+
`phase: ${response.execution.phase}`,
|
|
96
|
+
...(capability ? [
|
|
97
|
+
`capability: ${capability.capability_id}`,
|
|
98
|
+
`required_input: ${requiredInput.length > 0 ? requiredInput.join(",") : "none"}`,
|
|
99
|
+
...(capability.free_quota_limit !== undefined ? [`free_quota_limit: ${capability.free_quota_limit}`] : []),
|
|
100
|
+
] : []),
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export async function runServicesInvoke(backend, config, serviceExecutionID, capabilityID, input, options = {}) {
|
|
105
|
+
const readModel = await backend.getServiceExecution(serviceExecutionID);
|
|
106
|
+
const requestedCapability = readModel.capabilities.find((capability) => capability.capability_id === capabilityID);
|
|
107
|
+
if (!requestedCapability) {
|
|
108
|
+
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" }]);
|
|
109
|
+
}
|
|
110
|
+
if (requestedCapability.requires_payment) {
|
|
111
|
+
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 的当前合法动作" }]);
|
|
112
|
+
}
|
|
113
|
+
const missingInput = missingRequiredInput(requestedCapability.input_schema, input);
|
|
114
|
+
if (missingInput.length > 0) {
|
|
115
|
+
throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "只补缺失字段到小JSON文件,再提交同一执行;本次尚未调用供应商。", [], { schema_version: "itpay.interaction.v1", stage: "input_required",
|
|
116
|
+
input_template: { command: `itpay services invoke ${serviceExecutionID} --capability ${capabilityID} --input-json <file> --json`,
|
|
117
|
+
required_input: ["file", ...missingInput], executable: false } });
|
|
118
|
+
}
|
|
119
|
+
const idempotencyKey = await operationID(config, `service.invoke:${serviceExecutionID}:${capabilityID}:${stableInput(input)}`);
|
|
120
|
+
let response;
|
|
121
|
+
try {
|
|
122
|
+
response = await backend.invokeServiceCapability(serviceExecutionID, capabilityID, {
|
|
123
|
+
idempotency_key: idempotencyKey,
|
|
124
|
+
redacted_summary: input,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (!(error instanceof HttpError) || error.code !== "verified_phone_required")
|
|
129
|
+
throw error;
|
|
130
|
+
writeCommandEnvelope({
|
|
131
|
+
status: "human_action_required",
|
|
132
|
+
result: { service_execution_id: serviceExecutionID, error_code: "verified_phone_required" },
|
|
133
|
+
instruction: "需要在官方页面完成手机号验证后当前执行才能继续。运行 itpay auth login 打开官方登录页,完成手机号验证并绑定本设备后重试原命令;CLI 不接收手机号或验证码。",
|
|
134
|
+
next: { command: "itpay auth login --json", reason: "完成官方手机号验证并绑定当前设备" },
|
|
135
|
+
recovery: [{ command: `itpay services invoke ${serviceExecutionID} --capability ${capabilityID}${formatInputOptions(input)} --json`, reason: "仅在完成手机号验证与设备绑定后重试" }],
|
|
136
|
+
}, { ...options, plainResult: ["手机号验证:itpay auth login"] });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const envelope = invokedEnvelope(response, requestedCapability, readModel.capabilities, input);
|
|
140
|
+
writeCommandEnvelope(envelope.value, {
|
|
141
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
142
|
+
...(options.output ? { output: options.output } : {}),
|
|
143
|
+
plainResult: envelope.plainResult,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
export function invokedEnvelope(response, requestedCapability, capabilities, input) {
|
|
147
|
+
if (response.execution_request_id) {
|
|
148
|
+
const id = response.execution.service_execution_id;
|
|
149
|
+
return { value: { status: "running", result: { service_execution_id: id, execution_request_id: response.execution_request_id },
|
|
150
|
+
instruction: "查询已排队或正在运行。告知用户稍候,只读取同一任务的状态;不要重新发起查询,也不要把暂未返回结果说成没有车。",
|
|
151
|
+
next: { command: `itpay services next ${id} --json`, reason: "稍后读取同一查询结果" }, recovery: [] },
|
|
152
|
+
plainResult: ["status: running", `service_execution_id: ${id}`] };
|
|
153
|
+
}
|
|
154
|
+
const items = response.result_items.map((item) => ({
|
|
155
|
+
rank: item.rank,
|
|
156
|
+
title: item.display_title,
|
|
157
|
+
safe_payload: item.safe_payload,
|
|
158
|
+
}));
|
|
159
|
+
const quota = response.effective_quota
|
|
160
|
+
? { remaining: response.effective_quota.remaining, limit: response.effective_quota.limit }
|
|
161
|
+
: undefined;
|
|
162
|
+
const baseResult = {
|
|
163
|
+
service_execution_id: response.execution.service_execution_id,
|
|
164
|
+
capability_id: requestedCapability.capability_id,
|
|
165
|
+
query: input,
|
|
166
|
+
items,
|
|
167
|
+
...(quota ? { quota } : {}),
|
|
168
|
+
};
|
|
169
|
+
const businessProjection = serviceAdapter(response.execution).invocation(response, requestedCapability, input, items.length);
|
|
170
|
+
if (businessProjection?.early)
|
|
171
|
+
return businessProjection.early;
|
|
172
|
+
if (businessProjection?.catalog)
|
|
173
|
+
baseResult.catalog = businessProjection.catalog;
|
|
174
|
+
let status = items.length > 0 ? "result_ready" : "no_result";
|
|
175
|
+
let instruction = items.length > 0
|
|
176
|
+
? "用编号、名称和可公开字段向用户说明候选;若候选列表已满足目标就停止。只有用户明确选择并希望继续时,才提交对应编号;不要向用户提及 safe_payload、Execution 或内部 ID。"
|
|
177
|
+
: `没有找到与“${queryText(input)}”匹配的结果。向用户展示本次为 0 个结果并停止。不要修改、缩短或猜测其他输入;只有用户明确提供新输入后,才能启动新的查询。`;
|
|
178
|
+
if (businessProjection?.instruction)
|
|
179
|
+
instruction = businessProjection.instruction;
|
|
180
|
+
let next = null;
|
|
181
|
+
if (response.effective_quota?.exhausted) {
|
|
182
|
+
status = "quota_exhausted";
|
|
183
|
+
instruction = "免费额度已用完且本次没有调用 Provider。当前没有可购买的 continuation;只读取同一 Execution 的服务端恢复方向。";
|
|
184
|
+
const checkoutAction = response.next_actions?.find((action) => action.kind === "create_checkout");
|
|
185
|
+
const checkoutCapability = capabilities.find((capability) => capability.capability_id === checkoutAction?.capability_id);
|
|
186
|
+
if (checkoutCapability) {
|
|
187
|
+
baseResult.checkout = {
|
|
188
|
+
capability_id: checkoutCapability.capability_id,
|
|
189
|
+
...(checkoutCapability.price_amount_minor !== undefined && checkoutCapability.price_currency ? {
|
|
190
|
+
price: { amount_minor: checkoutCapability.price_amount_minor, currency: checkoutCapability.price_currency },
|
|
191
|
+
} : {}),
|
|
192
|
+
delivery_email_required: checkoutCapability.delivery_email_required,
|
|
193
|
+
};
|
|
194
|
+
const price = capabilityPrice(checkoutCapability);
|
|
195
|
+
instruction = purchaseConfirmationInstruction("quota_exhausted", price, checkoutCapability.delivery_email_required, checkoutCapability.delivery_email_purpose);
|
|
196
|
+
next = {
|
|
197
|
+
command: checkoutCommand(response.execution.service_execution_id, checkoutCapability, input),
|
|
198
|
+
reason: `仅在用户明确同意支付 ${price} 后执行;否则停止`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
next = {
|
|
203
|
+
command: `itpay services next ${response.execution.service_execution_id} --json`,
|
|
204
|
+
reason: "读取服务端提供的付费恢复入口",
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else if (items.length > 0 && requestedCapability.requires_human_action) {
|
|
209
|
+
next = null;
|
|
210
|
+
}
|
|
211
|
+
else if (items.length === 0) {
|
|
212
|
+
next = null;
|
|
213
|
+
}
|
|
214
|
+
const interaction = items.length > 0 && requestedCapability.requires_human_action
|
|
215
|
+
? {
|
|
216
|
+
schema_version: "itpay.interaction.v1",
|
|
217
|
+
stage: "query_results_ready",
|
|
218
|
+
input_template: {
|
|
219
|
+
command: `itpay services action ${response.execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
|
|
220
|
+
required_input: ["rank"],
|
|
221
|
+
executable: false,
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
: undefined;
|
|
225
|
+
if (businessProjection?.suffix)
|
|
226
|
+
instruction += businessProjection.suffix;
|
|
227
|
+
return {
|
|
228
|
+
value: { status, result: baseResult, instruction, next, recovery: [], ...(interaction ? { interaction } : {}) },
|
|
229
|
+
plainResult: serviceResultPlainLines(baseResult),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
export async function runServicesGet(backend, serviceExecutionID, options = {}) {
|
|
233
|
+
const response = await backend.getServiceExecution(serviceExecutionID);
|
|
234
|
+
const execution = response.execution;
|
|
235
|
+
const timeline = response.events.slice(-20).map((event) => ({
|
|
236
|
+
sequence: event.sequence,
|
|
237
|
+
step: event.type,
|
|
238
|
+
status: event.status,
|
|
239
|
+
phase: event.phase,
|
|
240
|
+
...(event.capability_id ? { capability_id: event.capability_id } : {}),
|
|
241
|
+
occurred_at: event.occurred_at,
|
|
242
|
+
}));
|
|
243
|
+
const deliveryMode = serviceDeliveryMode(response);
|
|
244
|
+
const lockedRefund = response.refunds.find((refund) => refund.access_locked);
|
|
245
|
+
const nextState = servicesNextEnvelope(response);
|
|
246
|
+
const result = {
|
|
247
|
+
...nextState.result,
|
|
248
|
+
service_execution_id: execution.service_execution_id,
|
|
249
|
+
service_id: execution.service_id,
|
|
250
|
+
status: execution.status,
|
|
251
|
+
phase: execution.phase,
|
|
252
|
+
...(execution.current_capability_id ? { current_capability_id: execution.current_capability_id } : {}),
|
|
253
|
+
updated_at: execution.updated_at,
|
|
254
|
+
timeline,
|
|
255
|
+
...(response.workflow ? { workflow: response.workflow } : {}),
|
|
256
|
+
...serviceAdapter(response.execution).extraFacts(response),
|
|
257
|
+
...(response.events.length > timeline.length ? { timeline_truncated: true } : {}),
|
|
258
|
+
...(deliveryMode ? { delivery_mode: deliveryMode } : {}),
|
|
259
|
+
...(lockedRefund ? {
|
|
260
|
+
access_locked: true,
|
|
261
|
+
refund: { refund_request_id: lockedRefund.refund_request_id, status: lockedRefund.status },
|
|
262
|
+
} : {}),
|
|
263
|
+
};
|
|
264
|
+
const envelope = {
|
|
265
|
+
status: nextState.status,
|
|
266
|
+
result,
|
|
267
|
+
instruction: nextState.instruction,
|
|
268
|
+
next: nextState.next,
|
|
269
|
+
...(nextState.interaction ? { interaction: nextState.interaction } : {}),
|
|
270
|
+
...(nextState.communication ? { communication: nextState.communication } : {}),
|
|
271
|
+
...(nextState.handoff ? { handoff: nextState.handoff } : {}),
|
|
272
|
+
recovery: nextState.recovery.filter(action => action.command !== `itpay services get ${serviceExecutionID} --json`),
|
|
273
|
+
};
|
|
274
|
+
writeCommandEnvelope(envelope, {
|
|
275
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
276
|
+
...(options.output ? { output: options.output } : {}),
|
|
277
|
+
plainResult: plainValueLines(envelope.result),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
export function shouldWaitForServiceResult(model) {
|
|
281
|
+
if (model.current_delivery || isTerminalServiceExecutionStatus(model.execution.status))
|
|
282
|
+
return false;
|
|
283
|
+
const adapter = serviceAdapter(model.execution);
|
|
284
|
+
if (!adapter.supported)
|
|
285
|
+
return false;
|
|
286
|
+
const wait = adapter.shouldWait(model);
|
|
287
|
+
if (wait !== undefined)
|
|
288
|
+
return wait;
|
|
289
|
+
return ["queued", "running", "delivery"].includes(model.workflow?.status ?? "");
|
|
290
|
+
}
|
|
291
|
+
export async function runServicesNext(backend, serviceExecutionID, options = {}) {
|
|
292
|
+
const started = Date.now();
|
|
293
|
+
const until = started + (options.timeoutSeconds ?? 0) * 1000;
|
|
294
|
+
const sleep = options.sleep ?? ((milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds)));
|
|
295
|
+
let response = await backend.getServiceExecution(serviceExecutionID, options.sinceSnapshot ? { sinceSnapshot: options.sinceSnapshot } : {});
|
|
296
|
+
while (shouldWaitForServiceResult(response) && Date.now() < until) {
|
|
297
|
+
await sleep(options.pollIntervalMS ?? Math.max(1500, serviceAdapter(response.execution).pollAfter(response)));
|
|
298
|
+
response = await backend.getServiceExecution(serviceExecutionID, options.sinceSnapshot ? { sinceSnapshot: options.sinceSnapshot } : {});
|
|
299
|
+
}
|
|
300
|
+
const envelope = servicesNextEnvelope(response);
|
|
301
|
+
if (shouldWaitForServiceResult(response) && options.timeoutSeconds) {
|
|
302
|
+
envelope.result = { ...envelope.result, waited_seconds: Math.round((Date.now() - started) / 1000), wait_timed_out: true };
|
|
303
|
+
envelope.next = { command: `itpay services next ${serviceExecutionID} --timeout 120 --json`, reason: "稍后继续等待同一任务;不重新提交查询" };
|
|
304
|
+
}
|
|
305
|
+
journalTaskState(serviceExecutionID, envelope, options.env ?? process.env);
|
|
306
|
+
writeCommandEnvelope(envelope, {
|
|
307
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
308
|
+
...(options.output ? { output: options.output } : {}),
|
|
309
|
+
plainResult: servicesNextPlainResult(envelope.result),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
export async function runServicesRun(backend, config, serviceID, input, options = {}) {
|
|
313
|
+
let id = options.executionID;
|
|
314
|
+
let adapter = serviceAdapter({ service_id: serviceID, service_contract_version_id: "" });
|
|
315
|
+
let inputContext = { serviceID, operationID: options.operationID, executionID: options.executionID, continuation: options.continuation };
|
|
316
|
+
try {
|
|
317
|
+
if (!id) {
|
|
318
|
+
const started = await backend.startServiceExecution({
|
|
319
|
+
service_id: serviceID,
|
|
320
|
+
...(options.operationID ? { operation_id: options.operationID } : {}),
|
|
321
|
+
...(options.continuation ? { continuation: options.continuation } : {}),
|
|
322
|
+
client_context: { host: options.host ?? "terminal", features: serviceClientFeatures(), ...(options.target ? { target: options.target } : {}) },
|
|
323
|
+
...(input !== undefined ? { input } : {}),
|
|
324
|
+
});
|
|
325
|
+
if ("overview_only" in started) {
|
|
326
|
+
writeServiceOverview(started.service_definition, options);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
id = started.execution.service_execution_id;
|
|
330
|
+
if (!started.workflow_entry) {
|
|
331
|
+
await runServicesNext(backend, id, options);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
let model = await backend.getServiceExecution(id);
|
|
336
|
+
adapter = serviceAdapter(model.execution);
|
|
337
|
+
if (model.execution.public_binding)
|
|
338
|
+
inputContext = { ...inputContext, ...executionInputContext(model.execution), ...(options.continuation ? { continuation: options.continuation } : {}) };
|
|
339
|
+
if (model.execution.service_id !== serviceID && model.execution.public_binding?.public_service_id !== serviceID)
|
|
340
|
+
throw new Error("execution belongs to another service");
|
|
341
|
+
if (options.operationID && model.execution.public_binding?.operation_id !== options.operationID)
|
|
342
|
+
throw new Error("execution belongs to another operation");
|
|
343
|
+
if (!model.workflow_entry || (input === undefined && !model.workflow)) {
|
|
344
|
+
await runServicesNext(backend, id, options);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (!serviceAdapter(model.execution).supported) {
|
|
348
|
+
writeCommandEnvelope(servicesNextEnvelope(model), options);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (isTerminalServiceExecutionStatus(model.execution.status)) {
|
|
352
|
+
// A restored terminal purchase cannot advance or obtain a new checkout.
|
|
353
|
+
writeCommandEnvelope(servicesNextEnvelope(model), options);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (input !== undefined) {
|
|
357
|
+
model = await backend.advanceServiceExecution(id, input, `workflow-input:${id}`);
|
|
358
|
+
}
|
|
359
|
+
else if (model.workflow?.status === "quota_paused") {
|
|
360
|
+
model = await backend.advanceServiceExecution(id, undefined, `workflow-resume:${id}`);
|
|
361
|
+
}
|
|
362
|
+
const until = Date.now() + (options.timeoutSeconds ?? 120) * 1000;
|
|
363
|
+
const sleep = options.sleep ?? ((milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds)));
|
|
364
|
+
const paid = () => model.payment_bindings.some((binding) => binding.status === "payment_verified") || model.checkout_bindings.some((binding) => binding.status === "payment_verified");
|
|
365
|
+
// A planning projection can exist before any usable result or user action.
|
|
366
|
+
// Keep the existing bounded wait until there is something to hand over.
|
|
367
|
+
while ((["queued", "running", "delivery"].includes(model.workflow?.status ?? "") || (model.workflow?.status === "payment" && paid())) && shouldWaitForServiceResult(model) && Date.now() < until) {
|
|
368
|
+
await sleep(options.pollIntervalMS ?? 1500);
|
|
369
|
+
model = await backend.getServiceExecution(id);
|
|
370
|
+
}
|
|
371
|
+
if (model.refunds.some(refund => refund.access_locked)) {
|
|
372
|
+
await runServicesNext(backend, id, options);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (model.workflow?.status === "payment" && !paid()) {
|
|
376
|
+
await runServicesCheckout(backend, config, id, model.workflow_entry?.capability_id, {
|
|
377
|
+
...options,
|
|
378
|
+
...(config.agentType ? { agentType: config.agentType } : {}),
|
|
379
|
+
resume: model.checkout_bindings.length > 0,
|
|
380
|
+
});
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
await runServicesNext(backend, id, options);
|
|
384
|
+
}
|
|
385
|
+
catch (cause) {
|
|
386
|
+
if (cause instanceof HttpError && cause.payload?.service_id && cause.payload.service_contract_version_id)
|
|
387
|
+
adapter = serviceAdapter({ service_id: cause.payload.service_id, service_contract_version_id: cause.payload.service_contract_version_id });
|
|
388
|
+
if (cause instanceof HttpError && cause.code === "public_operation_invalid")
|
|
389
|
+
throw new CommandContractError(cause.code, cause.message, "按返回的固定service_definition核对声明操作;购买仍使用原结果详情的完整continuation模板,不改owner/snapshot、不猜operation、不重建原购买。", cause.payload?.service_definition ? [] : [{ command: `itpay services start ${serviceID} --json`, reason: "只读当前公开定义;不创建执行" }], undefined, { service_id: serviceID, ...(cause.payload?.service_definition ? { service_definition: cause.payload.service_definition } : {}), execution_created: false, provider_called: false });
|
|
390
|
+
if (cause instanceof HttpError && cause.code === "capability_input_invalid") {
|
|
391
|
+
const schema = cause.payload?.input_schema;
|
|
392
|
+
const executionID = cause.payload?.service_execution_id ?? id;
|
|
393
|
+
const errorAdapter = cause.payload?.service_id && cause.payload?.service_contract_version_id ? serviceAdapter({ service_id: cause.payload.service_id, service_contract_version_id: cause.payload.service_contract_version_id }) : adapter;
|
|
394
|
+
if (!errorAdapter.supported)
|
|
395
|
+
throw new CommandContractError("presentation_contract_unavailable", cause.message, "当前锁定合同不受此CLI支持;保留原输入和执行并停止,使用兼容CLI,不猜业务或提交修正模板。", [], undefined, { service_id: serviceID, ...(executionID ? { service_execution_id: executionID } : {}), original_error_code: cause.code });
|
|
396
|
+
throw new CommandContractError(cause.code, cause.message, errorAdapter.inputInstruction, schema ? [] : [{ command: executionID ? `itpay services get ${executionID} --json` : "itpay catalog list --json", reason: "获取当前锁定合同;仍缺合同则停止,不猜旧写法" }], schema ? { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: operationInputTemplate({ ...inputContext, executionID }, errorAdapter.inputTemplate(schema, input), requiredInputFields(schema)) } : undefined, { service_id: serviceID, ...(executionID ? { service_execution_id: executionID } : {}), execution_created: cause.payload?.execution_created ?? Boolean(executionID), ...(typeof cause.payload?.provider_called === "boolean" ? { provider_called: cause.payload.provider_called } : {}), input_errors: cause.payload?.input_errors ?? [cause.message], ...(schema ? { input_schema: schema } : {}) });
|
|
397
|
+
}
|
|
398
|
+
adapter.runError(cause, serviceID, id);
|
|
399
|
+
if (cause instanceof CommandContractError || cause instanceof HttpError || cause instanceof DeviceLockBusyError || cause instanceof DeviceStateError)
|
|
400
|
+
throw cause;
|
|
401
|
+
if (!id) {
|
|
402
|
+
throw new CommandContractError("workflow_start_outcome_unknown", cause instanceof Error ? cause.message : "service creation outcome unknown", "创建服务执行时没有收到完整响应。先查询当前身份可见的执行;不要直接重跑并创建替代执行。", [{ command: "itpay services list --json", reason: "查找可能已经创建的服务执行" }]);
|
|
403
|
+
}
|
|
404
|
+
if (cause instanceof HttpTransportError)
|
|
405
|
+
throw new CommandContractError("workflow_outcome_unknown", cause.message, "未收到完整响应;只读同一执行的权威事实,不重放写操作或创建替代执行。", [{ command: `itpay services next ${id} --json`, reason: "核对同一执行" }]);
|
|
406
|
+
throw new CommandContractError("workflow_run_failed", cause instanceof Error ? cause.message : "workflow run failed", "保留当前执行并按错误处理,不要重复创建服务执行。", [{ command: `itpay services next ${id} --json`, reason: "读取同一执行的当前事实与恢复动作" }]);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
export function writeServiceOverview(definition, options) {
|
|
410
|
+
const adapter = serviceAdapter({ service_id: definition.service_id, service_contract_version_id: definition.definition_version, public_binding: { public_service_id: definition.service_id, service_definition_version: definition.definition_version, operation_id: "overview", adapter_key: definition.adapter_key } });
|
|
411
|
+
if (!adapter.supported) {
|
|
412
|
+
writeCommandEnvelope({ status: "presentation_contract_unavailable", result: { service_id: definition.service_id, definition_version: definition.definition_version }, instruction: "当前公开定义不受此CLI支持;保留原任务并停止,使用兼容CLI,不猜操作或输入合同。", next: null, recovery: [] }, options);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const operations = definition.operations.map(op => ({ operation_id: op.id, title: op.display_names["zh-CN"], role: op.role, billing_mode: op.billing_mode, requires_continuation: op.requires_continuation ?? false, ...(!op.requires_continuation ? { input_schema: op.input_schema, input_template: operationInputTemplate({ serviceID: definition.service_id, operationID: op.id }, adapter.inputTemplate(op.input_schema), requiredInputFields(op.input_schema)) } : { instruction: "使用已有结果详情返回的完整准备购买模板、输入合同和continuation;不自行拼凭据或重新发现购买服务。" }) }));
|
|
416
|
+
const result = { service_id: definition.service_id, title: definition.display_names["zh-CN"], definition_version: definition.definition_version, ...(definition.guide ? { guide: `itpay docs show ${definition.guide.topic}` } : {}), operations };
|
|
417
|
+
writeCommandEnvelope({ status: "service_overview", result, instruction: adapter.overviewInstruction(definition.guide?.topic ?? ""), next: definition.guide ? { command: `itpay docs show ${definition.guide.topic}`, reason: "首次操作前读取服务的完整指导" } : null, recovery: [] }, { ...options, plainResult: plainValueLines(result) });
|
|
418
|
+
}
|