@itpay/cli 2.1.41 → 2.1.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -5
- package/assets/sell-preview/index.html +6 -0
- package/assets/sell-preview/preview.js +9 -0
- package/bin/darwin-amd64/itpay-sell +0 -0
- package/bin/darwin-arm64/itpay-sell +0 -0
- package/bin/linux-amd64/itpay-sell +0 -0
- package/bin/linux-arm64/itpay-sell +0 -0
- package/bin/windows-amd64/itpay-sell.exe +0 -0
- package/dist/src/client/backend.js +38 -2
- package/dist/src/client/http.js +2 -0
- package/dist/src/commands/aftercare.js +12 -0
- package/dist/src/commands/buy.js +17 -58
- package/dist/src/commands/cart.js +17 -15
- package/dist/src/commands/catalog.js +39 -14
- package/dist/src/commands/checkout.js +146 -5
- package/dist/src/commands/checkout_handoff.js +11 -61
- package/dist/src/commands/docs.js +7 -18
- package/dist/src/commands/guidance.js +1 -134
- package/dist/src/commands/install.js +6 -3
- package/dist/src/commands/order.js +34 -3
- package/dist/src/commands/orders.js +4 -9
- package/dist/src/commands/pay.js +1 -1
- package/dist/src/commands/presentation.js +153 -0
- package/dist/src/commands/rail_catalog.js +1 -0
- package/dist/src/commands/readyz.js +1 -1
- package/dist/src/commands/refund.js +13 -57
- package/dist/src/commands/services/actions.js +133 -0
- package/dist/src/commands/services/commerce.js +197 -0
- package/dist/src/commands/services/lifecycle.js +418 -0
- package/dist/src/commands/services/results.js +130 -0
- package/dist/src/commands/services.js +14 -1068
- package/dist/src/commands/skill.js +8 -5
- package/dist/src/commands/vault.js +27 -20
- package/dist/src/commands/vault_handoff.js +18 -0
- package/dist/src/main.js +332 -46
- package/dist/src/render/browser.js +65 -0
- package/dist/src/render/checkout_handoff.js +101 -0
- package/dist/src/render/checkout_plan.js +47 -0
- package/dist/src/render/markdown.js +0 -3
- package/dist/src/render/qr.js +1 -1
- package/dist/src/sell/commands.js +80 -0
- package/dist/src/sell/contract.js +86 -0
- package/dist/src/sell/local.js +260 -0
- package/dist/src/sell/mcp.js +49 -0
- package/dist/src/sell/preview.js +70 -0
- package/dist/src/sell/sync.js +145 -0
- package/dist/src/services/adapter.js +1 -0
- package/dist/src/services/presentation.js +198 -0
- package/dist/src/services/rail/actions.js +51 -0
- package/dist/src/services/rail/adapter.js +36 -0
- package/dist/src/services/rail/aftercare.js +45 -0
- package/dist/src/services/rail/catalog.js +261 -0
- package/dist/src/services/rail/checkout.js +19 -0
- package/dist/src/services/rail/fulfillment.js +76 -0
- package/dist/src/services/rail/guide.js +110 -0
- package/dist/src/services/rail/invocation.js +54 -0
- package/dist/src/services/rail/page.js +76 -0
- package/dist/src/services/rail/pagination.js +6 -0
- package/dist/src/services/rail/query.js +71 -0
- package/dist/src/services/rail/readers.js +107 -0
- package/dist/src/services/rail/reading.js +125 -0
- package/dist/src/services/rail/recovery.js +51 -0
- package/dist/src/services/rail/results.js +378 -0
- package/dist/src/services/rail/review.js +268 -0
- package/dist/src/services/rail/saved.js +49 -0
- package/dist/src/services/rail/wait.js +11 -0
- package/dist/src/services/registry.js +31 -0
- package/dist/src/services/schema.js +21 -0
- package/dist/src/services/shared/envelope.js +184 -0
- package/dist/src/services/shared/input.js +39 -0
- package/dist/src/services/shared/pagination.js +28 -0
- package/dist/src/services/shared/platform.js +365 -0
- package/dist/src/services/shared/refund.js +40 -0
- package/dist/src/services/shared/workflow.js +28 -0
- package/dist/src/state/account_auth.js +354 -0
- package/dist/src/state/cart_session.js +4 -1
- package/dist/src/state/client_context.js +72 -0
- package/dist/src/state/config.js +44 -14
- package/dist/src/state/device_authority.js +402 -72
- package/dist/src/state/task_journal.js +90 -0
- package/docs/agent/buyer/cart-checkout.json +8 -6
- package/docs/agent/buyer/catalog-list.json +29 -9
- package/docs/agent/buyer/identity-and-sessions.json +26 -9
- package/docs/agent/buyer/install-and-setup.json +17 -15
- package/docs/agent/buyer/orders-refunds.json +25 -9
- package/docs/agent/buyer/payment-flow.json +6 -4
- package/docs/agent/buyer/purchased-content.json +18 -4
- package/docs/agent/buyer/quickstart.json +26 -17
- package/docs/agent/buyer/rail-booking.json +176 -0
- package/docs/agent/buyer/rail-fast-checkout.json +48 -0
- package/docs/agent/buyer/render-hosts.json +23 -12
- package/docs/agent/buyer/service-feedback.json +4 -3
- package/docs/cli-reference/agent-types.md +7 -2
- package/docs/cli-reference/commands/aftercare.md +15 -0
- package/docs/cli-reference/commands/auth.md +96 -0
- package/docs/cli-reference/commands/buy.md +2 -1
- package/docs/cli-reference/commands/cart/add.md +5 -1
- package/docs/cli-reference/commands/cart/clear.md +1 -1
- package/docs/cli-reference/commands/cart/index.md +1 -1
- package/docs/cli-reference/commands/cart/next.md +5 -1
- package/docs/cli-reference/commands/cart/remove.md +1 -1
- package/docs/cli-reference/commands/cart/show.md +1 -1
- package/docs/cli-reference/commands/catalog/index.md +1 -1
- package/docs/cli-reference/commands/catalog/list.md +2 -2
- package/docs/cli-reference/commands/checkout.md +12 -2
- package/docs/cli-reference/commands/device.md +18 -11
- package/docs/cli-reference/commands/docs/index.md +1 -1
- package/docs/cli-reference/commands/docs/list.md +1 -1
- package/docs/cli-reference/commands/docs/search.md +1 -1
- package/docs/cli-reference/commands/docs/show.md +1 -1
- package/docs/cli-reference/commands/install.md +8 -6
- package/docs/cli-reference/commands/next.md +1 -1
- package/docs/cli-reference/commands/order.md +1 -1
- package/docs/cli-reference/commands/pay.md +1 -1
- package/docs/cli-reference/commands/readyz.md +11 -11
- package/docs/cli-reference/commands/refund/cancel.md +1 -1
- package/docs/cli-reference/commands/refund/create.md +1 -1
- package/docs/cli-reference/commands/refund/get.md +2 -2
- package/docs/cli-reference/commands/refund/index.md +1 -1
- package/docs/cli-reference/commands/refund/list.md +1 -1
- package/docs/cli-reference/commands/refund/watch.md +1 -1
- package/docs/cli-reference/commands/sell.md +1364 -0
- package/docs/cli-reference/commands/services/action.md +24 -3
- package/docs/cli-reference/commands/services/checkout.md +17 -2
- package/docs/cli-reference/commands/services/events.md +1 -1
- package/docs/cli-reference/commands/services/get.md +4 -2
- package/docs/cli-reference/commands/services/index.md +4 -3
- package/docs/cli-reference/commands/services/invoke.md +6 -4
- package/docs/cli-reference/commands/services/list.md +1 -1
- package/docs/cli-reference/commands/services/next.md +78 -12
- package/docs/cli-reference/commands/services/page.md +71 -0
- package/docs/cli-reference/commands/services/quote.md +4 -4
- package/docs/cli-reference/commands/services/read-result.md +17 -6
- package/docs/cli-reference/commands/services/run.md +152 -0
- package/docs/cli-reference/commands/services/start.md +5 -3
- package/docs/cli-reference/commands/vault/access.md +5 -1
- package/docs/cli-reference/conventions.md +4 -6
- package/docs/cli-reference/index.md +10 -1
- package/docs/sell.md +84 -0
- package/package.json +20 -12
- package/skills/itpay/SKILL.md +112 -102
- package/docs/skill-bundle-rollout/01-mcp-authentication.md +0 -134
- package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +0 -210
- package/docs/skill-bundle-rollout/03-platform-publishing.md +0 -161
- package/docs/skill-bundle-rollout/04-first-wave-platforms.md +0 -69
- package/docs/skill-bundle-rollout/05-sync-operations.md +0 -62
- package/docs/skill-bundle-rollout/README.md +0 -151
- /package/bin/{itp → itp.mjs} +0 -0
package/dist/src/main.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { runAftercareList, runAftercareCreate, runAftercareGet, runAftercareRespond } from "./commands/aftercare.js";
|
|
2
|
+
import { agentAuth } from "./state/account_auth.js";
|
|
3
|
+
import { registerSell } from "./sell/commands.js";
|
|
4
|
+
import { readFileSync as readWorkflowInputFile, statSync as statWorkflowInputFile } from "node:fs";
|
|
5
|
+
import { runServicesRun } from "./commands/services.js";
|
|
1
6
|
// V3 CLI entrypoint. Each command maps 1:1 to a route family in
|
|
2
7
|
// services/backend/internal/httpapi/handlers/*.go. Commands only
|
|
3
8
|
// orchestrate; HTTP and rendering live in src/client and src/render.
|
|
4
9
|
import { Command } from "commander";
|
|
5
10
|
import { BackendOverrideError, API_CONTRACT_REVISION, CLI_VERSION, cliDistribution, loadConfig, cartSessionPath, newBackendClient } from "./state/config.js";
|
|
6
|
-
import { DeviceAuthority, DeviceAuthorizationError, DeviceStateError } from "./state/device_authority.js";
|
|
11
|
+
import { DeviceAuthority, DeviceAuthorizationError, DeviceLockBusyError, DeviceStateError } from "./state/device_authority.js";
|
|
7
12
|
import { CartSession } from "./state/cart_session.js";
|
|
8
13
|
import { defaultHostForAgentType, normalizeHost, validateContext } from "./state/client_context.js";
|
|
9
14
|
import { HttpError } from "./client/http.js";
|
|
@@ -21,11 +26,11 @@ import { runCancelRefund, runGetRefund, runListRefunds, runRefund, runWatchRefun
|
|
|
21
26
|
import { runCartAdd, runCartAddQuoteServer, runCartAddServer, runCartAbandonServer, runCartClear, runCartNext, runCartRemove, runCartRemoveServer, runCartShow, runCartShowServer, } from "./commands/cart.js";
|
|
22
27
|
import { CommandContractError, errorRecoveryActions, printErrorRecovery, writeCommandEnvelope } from "./commands/guidance.js";
|
|
23
28
|
import { runDocsList, runDocsShow, runDocsSearch } from "./commands/docs.js";
|
|
24
|
-
import { runInstall } from "./commands/install.js";
|
|
29
|
+
import { INSTALL_AGENT_TYPES, runInstall } from "./commands/install.js";
|
|
25
30
|
import { runSkillShow } from "./commands/skill.js";
|
|
26
31
|
import { runNext } from "./commands/next.js";
|
|
27
32
|
import { runVaultAccess, runVaultList, runVaultRead } from "./commands/vault.js";
|
|
28
|
-
import { collectOption, parseKeyValueList, runServicesAction, runServicesCheckout, runServicesEvents, runServicesGet, runServicesInvoke, runServicesList, runServicesNext, runServicesReadResult, runServicesQuote, runServicesStart, } from "./commands/services.js";
|
|
33
|
+
import { collectOption, parseKeyValueList, runServicesAction, runServicesCheckout, runServicesEvents, runServicesGet, runServicesInvoke, runServicesList, runServicesNext, runServicesPage, runServicesReadResult, runServicesQuote, runServicesStart, } from "./commands/services.js";
|
|
29
34
|
const program = new Command();
|
|
30
35
|
program
|
|
31
36
|
.name("itpay")
|
|
@@ -112,6 +117,7 @@ function reportCLIError(error, contract) {
|
|
|
112
117
|
const backendOverrideError = error instanceof BackendOverrideError ? error : undefined;
|
|
113
118
|
const deviceError = error instanceof DeviceAuthorizationError ? error : undefined;
|
|
114
119
|
const stateError = error instanceof DeviceStateError ? error : undefined;
|
|
120
|
+
const lockError = error instanceof DeviceLockBusyError ? error : undefined;
|
|
115
121
|
const transportError = error instanceof HttpTransportError ? error : undefined;
|
|
116
122
|
const httpRecovery = errorRecoveryActions(error).map((action) => ({
|
|
117
123
|
command: action.command,
|
|
@@ -130,31 +136,42 @@ function reportCLIError(error, contract) {
|
|
|
130
136
|
const providerInputRejected = error instanceof HttpError && error.code === "provider_input_rejected";
|
|
131
137
|
const providerContractMismatch = error instanceof HttpError && error.code === "provider_contract_mismatch";
|
|
132
138
|
const capabilityInputInvalid = error instanceof HttpError && error.code === "capability_input_invalid";
|
|
133
|
-
const
|
|
139
|
+
const deviceKeyResettable = Boolean(deviceError && (deviceError.code === "agent_device_key_rotated" ||
|
|
140
|
+
deviceError.code === "agent_device_key_conflict"));
|
|
141
|
+
const deviceRecovery = deviceError ? [
|
|
142
|
+
{
|
|
134
143
|
command: "itpay skill show itpay --json",
|
|
135
144
|
reason: "读取 ItPay 身份边界;该错误需要用户或运营恢复 Backend 登记,不能通过换类型或删除本地身份绕过",
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
145
|
+
},
|
|
146
|
+
] : [];
|
|
147
|
+
const stateRecovery = [];
|
|
148
|
+
const lockRecovery = lockError ? [{
|
|
149
|
+
command: "itpay device repair-lock --json",
|
|
150
|
+
reason: "检查锁持有者;只回收已退出进程留下的锁,不改变设备身份",
|
|
140
151
|
}] : [];
|
|
141
152
|
const authorizationInstruction = stateError
|
|
142
|
-
?
|
|
143
|
-
:
|
|
144
|
-
? "
|
|
145
|
-
:
|
|
146
|
-
? "
|
|
147
|
-
: deviceError
|
|
148
|
-
? "Device
|
|
149
|
-
:
|
|
150
|
-
|
|
153
|
+
? `本地设备状态操作未完成:${stateError.operation}(${stateError.causeCode},来源 ${stateError.source})。保持原身份和已有授权;由环境负责人核对该操作、锁持有者和宿主返回。权限错误才检查读写权限;未知操作结果先核对现场,不盲目重试,不删除身份或绕过宿主保护。`
|
|
154
|
+
: lockError
|
|
155
|
+
? "本地身份锁占用或安全转换未完成。执行 recovery 核对;活跃持有者需等待,遗留转换需环境负责人检查。不能仅凭等待时间删锁或重建身份。"
|
|
156
|
+
: error instanceof HttpError && error.code === "agent_device_session_required"
|
|
157
|
+
? "CLI 已自动续期并重试同一请求一次,仍被拒绝;停止重试,不要切换 Agent Type 或旋转身份。"
|
|
158
|
+
: deviceError?.code === "agent_device_revoked"
|
|
159
|
+
? "Backend 已撤销当前 Device 登记;CLI 没有自动创建替代身份。停止重试并请用户或运营恢复登记。"
|
|
160
|
+
: deviceError?.enrollmentFailed && deviceError.status === 500
|
|
161
|
+
? "服务端未能完成设备登记;保持原密钥和Agent Type,说明系统故障,待服务端恢复后继续原任务。不要重置身份。"
|
|
162
|
+
: deviceKeyResettable
|
|
163
|
+
? "服务端拒绝当前设备私钥登记(轮换或冲突)。停止自动恢复;换钥会放弃旧身份,旧执行访问不会迁移。仅用户或运营明确决定放弃旧身份后,按 device reset-key 帮助执行。"
|
|
164
|
+
: deviceError
|
|
165
|
+
? "Device 身份验证失败;停止重试,不要切换 Agent Type、删除状态或旋转私钥。"
|
|
166
|
+
: undefined;
|
|
167
|
+
if (contract || commandError || backendOverrideError || lockError || stateError) {
|
|
151
168
|
writeCommandEnvelope({
|
|
152
169
|
status: "error",
|
|
153
170
|
error: {
|
|
154
|
-
code: incompatible ? "backend_contract_incompatible" : backendOverrideError?.code ?? commandError?.code ?? (error instanceof HttpError ? error.code : transportError?.code ?? stateError?.code ?? deviceError?.code ?? contract?.code ?? "command_failed"),
|
|
171
|
+
code: incompatible ? "backend_contract_incompatible" : backendOverrideError?.code ?? commandError?.code ?? (error instanceof HttpError ? error.code : transportError?.code ?? stateError?.code ?? lockError?.code ?? deviceError?.code ?? contract?.code ?? "command_failed"),
|
|
155
172
|
message: error instanceof Error ? error.message : String(error),
|
|
156
173
|
},
|
|
157
|
-
...(incompatible && error instanceof HttpError ? {
|
|
174
|
+
...(stateError ? { result: { failure_stage: "local_device_state", operation: stateError.operation, cause_code: stateError.causeCode, source: stateError.source } } : commandError?.result ? { result: commandError.result } : incompatible && error instanceof HttpError ? {
|
|
158
175
|
result: {
|
|
159
176
|
current_cli_version: CLI_VERSION,
|
|
160
177
|
current_api_contract_revision: API_CONTRACT_REVISION,
|
|
@@ -194,7 +211,7 @@ function reportCLIError(error, contract) {
|
|
|
194
211
|
: providerRejected
|
|
195
212
|
? "告诉用户数据来源没有接受本次请求,但没有说明是输入错误,并按 result.quota 说明额度状态,然后停止。不要转述内部错误、修改输入、重试或创建新查询。"
|
|
196
213
|
: capabilityInputInvalid
|
|
197
|
-
? "
|
|
214
|
+
? "Agent按当前合同模板修正字段和JSON层级,保留用户原始需求,继续同一次服务。不要让用户修技术输入,只有真实业务歧义才询问。"
|
|
198
215
|
: transportError
|
|
199
216
|
? contract?.initialization
|
|
200
217
|
? "本轮初始化检查未完成;CLI 已结束有限的网络尝试,尚未确认后端或设备就绪。结束本轮,网络恢复后可重新检查;不要继续登录或业务操作。"
|
|
@@ -202,17 +219,18 @@ function reportCLIError(error, contract) {
|
|
|
202
219
|
? "临时网络故障;CLI 已仅对可安全重放的操作完成有限自动重试,但仍未获得完整响应。按 recovery 查询同一资源的权威状态;不要创建替代 Checkout、Execution、Payment 或 Refund。"
|
|
203
220
|
: "网络在完整响应前中断;当前写操作没有安全重放合同,因此 CLI 未自动重试。按 recovery 查询权威状态;不要原样重放或创建替代 Checkout、Execution、Payment 或 Refund。"
|
|
204
221
|
: backendOverrideError
|
|
205
|
-
? "移除 ITPAY_BACKEND_URL
|
|
222
|
+
? "移除 ITPAY_BACKEND_URL 使用正式环境,或按当前测试目标准确设置为 https://sandbox.itpay.ai 或 https://dev.itpay.ai。不要通过切换环境规避当前错误。"
|
|
206
223
|
: commandError?.instruction ?? authorizationInstruction ?? contract?.instruction ?? "检查命令参数后重试。",
|
|
207
224
|
next: null,
|
|
225
|
+
...(commandError?.interaction ? { interaction: commandError.interaction } : {}),
|
|
208
226
|
recovery: incompatible || (transportError && contract?.initialization)
|
|
209
227
|
? []
|
|
210
228
|
: backendInternal || providerConnectionUnavailable || providerTemporary || providerInputRejected || providerContractMismatch || providerRejected || capabilityInputInvalid
|
|
211
229
|
? []
|
|
212
|
-
: backendOverrideError ? [] : commandError?.recovery ?? (stateError ? stateRecovery : deviceError ? deviceRecovery : identityRecovery ? httpRecovery : contract?.recovery ?? []),
|
|
230
|
+
: backendOverrideError ? [] : commandError?.recovery ?? (stateError ? stateRecovery : lockError ? lockRecovery : deviceError ? deviceRecovery : identityRecovery ? httpRecovery : contract?.recovery ?? []),
|
|
213
231
|
}, {
|
|
214
|
-
...(contract?.jsonOutput !== undefined ? { jsonOutput: contract.jsonOutput } : backendOverrideError ? { jsonOutput: process.argv.includes("--json") } : {}),
|
|
215
|
-
output: (text) => { process.
|
|
232
|
+
...(contract?.jsonOutput !== undefined ? { jsonOutput: contract.jsonOutput } : backendOverrideError || lockError || stateError ? { jsonOutput: process.argv.includes("--json") } : {}),
|
|
233
|
+
output: (text) => { process.stdout.write(text); },
|
|
216
234
|
});
|
|
217
235
|
process.exitCode = 1;
|
|
218
236
|
return;
|
|
@@ -279,31 +297,56 @@ program
|
|
|
279
297
|
});
|
|
280
298
|
}
|
|
281
299
|
});
|
|
300
|
+
const authCmd = program.command("auth").description("Log in and bind this enrolled Agent to your ItPay account");
|
|
301
|
+
for (const action of ["login", "status"]) {
|
|
302
|
+
authCmd.command(action).option("--json", "output JSON").action(async (options) => {
|
|
303
|
+
const config = loadConfig();
|
|
304
|
+
try {
|
|
305
|
+
const result = await agentAuth(action, config.baseURL, newBackendClient(config));
|
|
306
|
+
// AUTH06: auth output goes through the same envelope renderer as every
|
|
307
|
+
// other command — next/recovery keep Agent Type + Backend qualification
|
|
308
|
+
// and non-JSON output is human-readable instead of a raw JSON dump.
|
|
309
|
+
const envelope = {
|
|
310
|
+
status: String(result.status ?? "error"),
|
|
311
|
+
result: (result.result ?? Object.fromEntries(Object.entries(result).filter(([key]) => !["status", "instruction", "next", "recovery", "handoff"].includes(key)))),
|
|
312
|
+
...(result.handoff ? { handoff: result.handoff } : {}),
|
|
313
|
+
instruction: typeof result.instruction === "string" ? result.instruction : "按返回状态继续。",
|
|
314
|
+
next: (result.next ?? null),
|
|
315
|
+
recovery: (result.recovery ?? []),
|
|
316
|
+
};
|
|
317
|
+
writeCommandEnvelope(envelope, { jsonOutput: Boolean(options.json) });
|
|
318
|
+
if (envelope.status === "auth_status_unknown")
|
|
319
|
+
process.exitCode = 1;
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
reportCLIError(error, { jsonOutput: Boolean(options.json), code: "account_login_failed", initialization: true, instruction: "本轮授权检查未完成;保留设备及已有授权状态,按明确错误原因恢复。不要自动发起新的登录。" });
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
282
326
|
// --- device ---------------------------------------------------------------
|
|
283
327
|
const deviceCmd = program.command("device").description("Recover the current official Backend registration after an operator-confirmed reset");
|
|
284
|
-
deviceCmd
|
|
285
|
-
.
|
|
328
|
+
deviceCmd
|
|
329
|
+
.command("repair-lock")
|
|
330
|
+
.description("Safely inspect and recover a local Device identity lock left by an exited process")
|
|
286
331
|
.option("--json", "output JSON instead of terminal text")
|
|
287
|
-
.action(
|
|
288
|
-
const config = loadConfig();
|
|
332
|
+
.action((options) => {
|
|
289
333
|
try {
|
|
290
|
-
|
|
291
|
-
const
|
|
334
|
+
const config = loadConfig();
|
|
335
|
+
const status = new DeviceAuthority({
|
|
292
336
|
baseURL: config.baseURL,
|
|
293
337
|
...(config.agentType ? { requestedAgentType: config.agentType } : {}),
|
|
294
|
-
compatibilityHeaders: {
|
|
295
|
-
});
|
|
296
|
-
const verified = await authority.status();
|
|
338
|
+
compatibilityHeaders: {},
|
|
339
|
+
}).repairLock();
|
|
297
340
|
writeCommandEnvelope({
|
|
298
|
-
status: "
|
|
299
|
-
result: {
|
|
300
|
-
instruction: "
|
|
341
|
+
status: status === "recovered" ? "device_lock_recovered" : status === "active" ? "device_lock_active" : "device_lock_absent",
|
|
342
|
+
result: { lock_status: status, device_identity_preserved: true },
|
|
343
|
+
instruction: status === "active" ? "本地锁占用或安全转换未完成;保留原身份,核对持有者及宿主现场后继续原任务。" : "本地锁无需进一步处理;继续原任务。",
|
|
301
344
|
next: null,
|
|
302
345
|
recovery: [],
|
|
303
|
-
}, { jsonOutput: Boolean(options.json) });
|
|
346
|
+
}, { jsonOutput: Boolean(options.json), plainResult: [`lock: ${status}`, "device_identity: preserved"] });
|
|
304
347
|
}
|
|
305
348
|
catch (error) {
|
|
306
|
-
reportCLIError(error, { jsonOutput: Boolean(options.json), code: "
|
|
349
|
+
reportCLIError(error, { jsonOutput: Boolean(options.json), code: "device_lock_repair_failed", instruction: "本地锁无法安全检查;保留设备身份并联系维护者。" });
|
|
307
350
|
}
|
|
308
351
|
});
|
|
309
352
|
deviceCmd
|
|
@@ -358,6 +401,56 @@ deviceCmd
|
|
|
358
401
|
});
|
|
359
402
|
}
|
|
360
403
|
});
|
|
404
|
+
deviceCmd
|
|
405
|
+
.command("reset-key")
|
|
406
|
+
.description("Discard the local Ed25519 device key and all Backend registrations so the next command enrolls as a new device")
|
|
407
|
+
.option("--confirm-key-reset", "confirm that the current device key must be abandoned and a new identity created")
|
|
408
|
+
.option("--json", "output JSON instead of terminal text")
|
|
409
|
+
.action(async (options) => {
|
|
410
|
+
const config = loadConfig();
|
|
411
|
+
try {
|
|
412
|
+
if (!config.agentType || !INSTALL_AGENT_TYPES.some(type => type === config.agentType)) {
|
|
413
|
+
throw new CommandContractError("agent_type_required", "supported Agent Type required before key reset", "先按真实宿主配置正式支持的Agent Type;尚未修改本地密钥。", [{ command: "itpay install --json", reason: "读取当前宿主配置" }]);
|
|
414
|
+
}
|
|
415
|
+
if (!options.confirmKeyReset) {
|
|
416
|
+
throw new CommandContractError("key_reset_confirmation_required", "--confirm-key-reset is required", "仅在用户或运营明确决定放弃旧身份后使用;会丢弃本地私钥,旧执行访问不会迁移。internal_error 需服务端恢复,不能换钥。", [{ command: "itpay docs show identity-and-sessions --json", reason: "检查适用边界" }]);
|
|
417
|
+
}
|
|
418
|
+
const reset = await new DeviceAuthority({
|
|
419
|
+
baseURL: config.baseURL,
|
|
420
|
+
...(config.agentType ? { requestedAgentType: config.agentType } : {}),
|
|
421
|
+
compatibilityHeaders: {},
|
|
422
|
+
}).resetDeviceKey();
|
|
423
|
+
writeCommandEnvelope({
|
|
424
|
+
status: "device_key_reset",
|
|
425
|
+
result: {
|
|
426
|
+
removed_backend_registrations: reset.removedBackends,
|
|
427
|
+
private_key_preserved: false,
|
|
428
|
+
server_side_device: "orphaned_under_previous_key",
|
|
429
|
+
},
|
|
430
|
+
instruction: "本地设备私钥已重置;下一次需要设备身份的命令会以全新 Ed25519 密钥重新登记;这不能恢复旧身份的订单或用于刷新额度。服务端旧设备记录保留为孤儿;如需清理请走运营流程。",
|
|
431
|
+
next: {
|
|
432
|
+
command: `itpay --agent-type ${config.agentType} services list --limit 1 --json`,
|
|
433
|
+
reason: "用无业务写入的签名请求完成新密钥的重新登记",
|
|
434
|
+
},
|
|
435
|
+
recovery: [],
|
|
436
|
+
}, {
|
|
437
|
+
jsonOutput: Boolean(options.json),
|
|
438
|
+
plainResult: [
|
|
439
|
+
`removed_backend_registrations: ${reset.removedBackends.join(", ") || "none"}`,
|
|
440
|
+
"private_key: rotated (previous key discarded)",
|
|
441
|
+
"server_side_device: orphaned under previous key",
|
|
442
|
+
],
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
reportCLIError(error, {
|
|
447
|
+
jsonOutput: Boolean(options.json),
|
|
448
|
+
code: "device_key_reset_failed",
|
|
449
|
+
instruction: "仅在确认要放弃当前设备私钥时使用;普通 session 失效或 revoked 不需要本命令。",
|
|
450
|
+
recovery: [{ command: "itpay docs show identity-and-sessions --json", reason: "检查 Device 恢复边界" }],
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
});
|
|
361
454
|
// --- skill ----------------------------------------------------------------
|
|
362
455
|
const skillCmd = program.command("skill").description("Read complete packaged Agent skills");
|
|
363
456
|
skillCmd
|
|
@@ -832,6 +925,13 @@ program
|
|
|
832
925
|
.option("--id <checkout_id>")
|
|
833
926
|
.option("--token <display_token>")
|
|
834
927
|
.option("--locale <locale>", "payment card language: zh-CN|en", "zh-CN")
|
|
928
|
+
.option("--present <method>", "display once: auto|browser|image|link|none (JSON default plans only)")
|
|
929
|
+
.option("--no-open", "never open a browser window")
|
|
930
|
+
.option("--viewer <viewer>", "device the human is looking at: desktop|mobile|unknown")
|
|
931
|
+
.option("--relay-option <ref>", "server-issued contact option reference for message relay")
|
|
932
|
+
.option("--confirm-relay", "confirm the human explicitly consented to this relay send")
|
|
933
|
+
.option("--request-key <key>", "stable idempotency key for a relay request")
|
|
934
|
+
.option("--relay-status <relay_id>", "read a prior relay result; never resends")
|
|
835
935
|
.option("--json", "output compact JSON")
|
|
836
936
|
.action(async (options) => {
|
|
837
937
|
const config = loadConfig();
|
|
@@ -849,12 +949,22 @@ program
|
|
|
849
949
|
await runCheckoutPresentation(backend, {
|
|
850
950
|
checkoutID,
|
|
851
951
|
displayToken,
|
|
952
|
+
...(snap.lastCheckoutID === checkoutID && snap.lastCheckoutURL
|
|
953
|
+
? { savedCheckoutURL: snap.lastCheckoutURL }
|
|
954
|
+
: {}),
|
|
852
955
|
host,
|
|
853
956
|
...(options.target ? { target: options.target } : {}),
|
|
854
957
|
...(config.agentType ? { agentType: config.agentType } : {}),
|
|
855
958
|
baseURL: config.baseURL,
|
|
856
959
|
locale: options.locale,
|
|
857
960
|
jsonOutput: Boolean(options.json),
|
|
961
|
+
...(options.present ? { present: options.present } : {}),
|
|
962
|
+
...(options.noOpen ? { noOpen: true } : {}),
|
|
963
|
+
...(options.viewer ? { viewer: options.viewer } : {}),
|
|
964
|
+
...(options.relayOption ? { relayOption: options.relayOption } : {}),
|
|
965
|
+
...(options.confirmRelay ? { confirmRelay: true } : {}),
|
|
966
|
+
...(options.requestKey ? { requestKey: options.requestKey } : {}),
|
|
967
|
+
...(options.relayStatus ? { relayStatus: options.relayStatus } : {}),
|
|
858
968
|
});
|
|
859
969
|
}
|
|
860
970
|
catch (error) {
|
|
@@ -1022,6 +1132,49 @@ feedback
|
|
|
1022
1132
|
});
|
|
1023
1133
|
}
|
|
1024
1134
|
});
|
|
1135
|
+
const aftercare = program.command("aftercare").description("Railway manual refund/change requests; never automatic supplier operations");
|
|
1136
|
+
aftercare.command("list").requiredOption("--order <order_id>").option("--json").action(async (options) => {
|
|
1137
|
+
try {
|
|
1138
|
+
const config = loadConfig();
|
|
1139
|
+
writeCommandEnvelope(await runAftercareList(newBackendClient(config), options.order), { jsonOutput: Boolean(options.json) });
|
|
1140
|
+
}
|
|
1141
|
+
catch (error) {
|
|
1142
|
+
reportCLIError(error, { jsonOutput: Boolean(options.json), code: "aftercare_read_failed", instruction: "核对原订单与当前身份;铁路人工售后不改变平台内容授权,不换身份绕过。", recovery: [{ command: "itpay services list --json", reason: "找回当前身份原订单" }] });
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
aftercare.command("create").requiredOption("--order <order_id>").requiredOption("--input-json <file>").option("--json").action(async (options) => {
|
|
1146
|
+
const recovery = [{ command: `itpay aftercare list --order ${options.order} --json`, reason: "读取原订单当前行程与完整申请模板" }];
|
|
1147
|
+
try {
|
|
1148
|
+
const config = loadConfig();
|
|
1149
|
+
const input = readInputJsonObject(options.inputJson, "rail_aftercare_input_invalid", recovery);
|
|
1150
|
+
writeCommandEnvelope(await runAftercareCreate(newBackendClient(config), config, options.order, input), { jsonOutput: Boolean(options.json) });
|
|
1151
|
+
}
|
|
1152
|
+
catch (error) {
|
|
1153
|
+
reportCLIError(error, { jsonOutput: Boolean(options.json), code: "aftercare_create_failed", instruction: "保留原申请与用户范围,按实际错误读取当前模板或办理状态;不猜金额/身份,不重复未知操作。", recovery });
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
aftercare.command("get").argument("<case_id>").option("--json").action(async (id, options) => {
|
|
1157
|
+
try {
|
|
1158
|
+
const config = loadConfig();
|
|
1159
|
+
writeCommandEnvelope(await runAftercareGet(newBackendClient(config), id), { jsonOutput: Boolean(options.json) });
|
|
1160
|
+
}
|
|
1161
|
+
catch (error) {
|
|
1162
|
+
reportCLIError(error, { jsonOutput: Boolean(options.json), code: "aftercare_read_failed", instruction: "核对当前身份的原申请,不换身份或探测其他申请。", recovery: [] });
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
for (const action of ["respond", "cancel"]) {
|
|
1166
|
+
aftercare.command(action).argument("<case_id>").requiredOption("--input-json <file>").option("--json").action(async (id, options) => {
|
|
1167
|
+
const recovery = [{ command: `itpay aftercare get ${id} --json`, reason: "读取原申请当前版本与明确下一步" }];
|
|
1168
|
+
try {
|
|
1169
|
+
const config = loadConfig();
|
|
1170
|
+
const input = readInputJsonObject(options.inputJson, "rail_aftercare_input_invalid", recovery);
|
|
1171
|
+
writeCommandEnvelope(await runAftercareRespond(newBackendClient(config), id, input, action === "cancel"), { jsonOutput: Boolean(options.json) });
|
|
1172
|
+
}
|
|
1173
|
+
catch (error) {
|
|
1174
|
+
reportCLIError(error, { jsonOutput: Boolean(options.json), code: "aftercare_response_failed", instruction: "按原申请当前版本处理;取消不撤回供应商操作,未知资金不重试。", recovery });
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1025
1178
|
const refund = program
|
|
1026
1179
|
.command("refund")
|
|
1027
1180
|
.enablePositionalOptions()
|
|
@@ -1092,7 +1245,7 @@ refund.command("get").argument("<refund_request_id>").option("--json", "output J
|
|
|
1092
1245
|
});
|
|
1093
1246
|
}
|
|
1094
1247
|
});
|
|
1095
|
-
refund.command("watch").argument("<refund_request_id>").option("--interval <seconds>", "poll interval", Number, 2).option("--timeout <seconds>", "
|
|
1248
|
+
refund.command("watch").argument("<refund_request_id>").option("--interval <seconds>", "poll interval", Number, 2).option("--timeout <seconds>", "bounded wait; returns on change/manual attention", Number, 20).option("--json", "output JSON instead of terminal text").action(async (id, options) => {
|
|
1096
1249
|
const config = loadConfig();
|
|
1097
1250
|
try {
|
|
1098
1251
|
await runWatchRefund(newBackendClient(config), id, {
|
|
@@ -1230,7 +1383,114 @@ vault
|
|
|
1230
1383
|
}
|
|
1231
1384
|
});
|
|
1232
1385
|
// --- service execution ----------------------------------------------------
|
|
1386
|
+
const WORKFLOW_INPUT_JSON_MAX_BYTES = 256 * 1024;
|
|
1387
|
+
function readInputJsonObject(file, code, recovery) {
|
|
1388
|
+
let parsed;
|
|
1389
|
+
try {
|
|
1390
|
+
if (statWorkflowInputFile(file).size > WORKFLOW_INPUT_JSON_MAX_BYTES) {
|
|
1391
|
+
throw new CommandContractError(code, `--input-json file exceeds ${WORKFLOW_INPUT_JSON_MAX_BYTES} bytes`, "输入文件过大;只提交服务端 input_schema 声明的字段。", recovery);
|
|
1392
|
+
}
|
|
1393
|
+
parsed = JSON.parse(readWorkflowInputFile(file, "utf8"));
|
|
1394
|
+
}
|
|
1395
|
+
catch (error) {
|
|
1396
|
+
if (error instanceof CommandContractError)
|
|
1397
|
+
throw error;
|
|
1398
|
+
throw new CommandContractError(code, `--input-json is not readable valid JSON: ${error instanceof Error ? error.message : String(error)}`, "确认文件存在且为合法 JSON;按服务端 input_schema 字段填写。", recovery);
|
|
1399
|
+
}
|
|
1400
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1401
|
+
throw new CommandContractError(code, "--input-json must contain a JSON object", "确认输入必须是 JSON 对象;按服务端 input_schema 字段填写。", recovery);
|
|
1402
|
+
}
|
|
1403
|
+
return parsed;
|
|
1404
|
+
}
|
|
1233
1405
|
const services = program.command("services").description("Generic V3 Service Execution commands");
|
|
1406
|
+
services
|
|
1407
|
+
.command("run")
|
|
1408
|
+
.description("Run a declared service operation; preparation approval is required before Checkout")
|
|
1409
|
+
.option("--operation <operation_id>", "operation declared by the public service definition")
|
|
1410
|
+
.argument("<service_id>")
|
|
1411
|
+
.option("--input-json <file>", "JSON object containing the service input")
|
|
1412
|
+
.option("--execution <execution_id>", "resume the same execution")
|
|
1413
|
+
.option("--timeout <seconds>", "maximum wait before returning current progress", Number, 120)
|
|
1414
|
+
.option("--host <host>")
|
|
1415
|
+
.option("--target <target>")
|
|
1416
|
+
.option("--json", "output JSON")
|
|
1417
|
+
.action(async (serviceID, options) => {
|
|
1418
|
+
const config = loadConfig();
|
|
1419
|
+
try {
|
|
1420
|
+
let input = options.inputJson
|
|
1421
|
+
? readInputJsonObject(options.inputJson, "workflow_input_invalid", [
|
|
1422
|
+
{ command: `itpay services run ${serviceID} --json`, reason: "重新读取服务输入声明" },
|
|
1423
|
+
])
|
|
1424
|
+
: undefined;
|
|
1425
|
+
let continuation;
|
|
1426
|
+
if (options.operation && input !== undefined) {
|
|
1427
|
+
const parameters = input.input;
|
|
1428
|
+
if (typeof parameters !== "object" || parameters === null || Array.isArray(parameters))
|
|
1429
|
+
throw new CommandContractError("operation_input_invalid", "operation JSON must include an input object", "使用公开操作返回的完整JSON模板,顶层input是业务输入;保留服务端continuation。", []);
|
|
1430
|
+
const reference = input.continuation;
|
|
1431
|
+
if (reference !== undefined) {
|
|
1432
|
+
if (typeof reference !== "object" || reference === null || Array.isArray(reference) || !("service_execution_id" in reference) || !("snapshot_id" in reference) || typeof reference.service_execution_id !== "string" || typeof reference.snapshot_id !== "string")
|
|
1433
|
+
throw new CommandContractError("operation_input_invalid", "invalid continuation reference", "保留结果详情返回的continuation,不猜执行或snapshot。", []);
|
|
1434
|
+
continuation = { service_execution_id: reference.service_execution_id, snapshot_id: reference.snapshot_id };
|
|
1435
|
+
}
|
|
1436
|
+
input = Object.fromEntries(Object.entries(parameters));
|
|
1437
|
+
}
|
|
1438
|
+
if (!Number.isFinite(options.timeout) || options.timeout < 0 || options.timeout > 600) {
|
|
1439
|
+
throw new Error("timeout must be between 0 and 600 seconds");
|
|
1440
|
+
}
|
|
1441
|
+
await runServicesRun(newBackendClient(config), config, serviceID, input, {
|
|
1442
|
+
...(options.execution ? { executionID: options.execution } : {}),
|
|
1443
|
+
...(options.operation ? { operationID: options.operation } : {}),
|
|
1444
|
+
...(continuation ? { continuation } : {}),
|
|
1445
|
+
jsonOutput: Boolean(options.json),
|
|
1446
|
+
host: withHost(options.host, config.agentType, options.target),
|
|
1447
|
+
...(options.target ? { target: options.target } : {}),
|
|
1448
|
+
timeoutSeconds: options.timeout,
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
catch (error) {
|
|
1452
|
+
reportCLIError(error, {
|
|
1453
|
+
jsonOutput: Boolean(options.json), code: "workflow_run_failed",
|
|
1454
|
+
instruction: "本次操作失败;按已返回的具体原因和恢复动作处理。没有恢复动作时停止,不猜字段或重复创建购买。", recovery: [],
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
services
|
|
1459
|
+
.command("page")
|
|
1460
|
+
.description("Read one page of a saved service result set (same stored version, no re-query, no quota)")
|
|
1461
|
+
.argument("<service_execution_id>")
|
|
1462
|
+
.argument("<result_item_id>")
|
|
1463
|
+
.option("--offset <offset>", "zero-based candidate offset", Number, 0)
|
|
1464
|
+
.option("--cursor <cursor>", "opaque v2 page cursor (rcur_<offset>); takes precedence over --offset")
|
|
1465
|
+
.option("--limit <limit>", "page size (1-20)", Number, 20)
|
|
1466
|
+
.option("--json", "output JSON")
|
|
1467
|
+
.action(async (serviceExecutionID, resultItemID, options) => {
|
|
1468
|
+
const config = loadConfig();
|
|
1469
|
+
const backend = newBackendClient(config);
|
|
1470
|
+
try {
|
|
1471
|
+
const cursorOffset = typeof options.cursor === "string" && /^rcur_\d+$/.test(options.cursor)
|
|
1472
|
+
? Number(options.cursor.slice(5))
|
|
1473
|
+
: undefined;
|
|
1474
|
+
if (options.cursor !== undefined && cursorOffset === undefined) {
|
|
1475
|
+
throw new CommandContractError("cursor_invalid", "--cursor must be an opaque rcur_<offset> token from a previous page response", "--cursor 需为上一页返回的 rcur_<offset> 不透明游标。", []);
|
|
1476
|
+
}
|
|
1477
|
+
await runServicesPage(backend, serviceExecutionID, resultItemID, {
|
|
1478
|
+
offset: cursorOffset ?? options.offset,
|
|
1479
|
+
limit: options.limit,
|
|
1480
|
+
jsonOutput: Boolean(options.json),
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
catch (error) {
|
|
1484
|
+
reportCLIError(error, {
|
|
1485
|
+
jsonOutput: Boolean(options.json),
|
|
1486
|
+
code: "service_page_failed",
|
|
1487
|
+
instruction: "分页读取的是已保存的同版本结果;不要重新发起查询或新建 execution。",
|
|
1488
|
+
recovery: [
|
|
1489
|
+
{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前结果页与合法动作" },
|
|
1490
|
+
],
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
});
|
|
1234
1494
|
services
|
|
1235
1495
|
.command("start")
|
|
1236
1496
|
.description("Start a contract-backed service execution")
|
|
@@ -1266,12 +1526,18 @@ services
|
|
|
1266
1526
|
.argument("<service_execution_id>")
|
|
1267
1527
|
.requiredOption("--capability <capability_id>")
|
|
1268
1528
|
.option("--input <key=value>", "redacted input summary", collectOption, [])
|
|
1529
|
+
.option("--input-json <file>", "JSON object containing the complete capability input")
|
|
1269
1530
|
.option("--json", "output JSON")
|
|
1270
1531
|
.action(async (serviceExecutionID, options) => {
|
|
1271
1532
|
const config = loadConfig();
|
|
1272
1533
|
const backend = newBackendClient(config);
|
|
1273
1534
|
try {
|
|
1274
|
-
|
|
1535
|
+
const recovery = [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前动作要求" }];
|
|
1536
|
+
if (options.inputJson && options.input.length > 0) {
|
|
1537
|
+
throw new CommandContractError("capability_input_invalid", "--input and --input-json cannot be combined", "嵌套输入用 --input-json <file>,扁平输入用 --input key=value,二选一;本次未调用供应商。", recovery);
|
|
1538
|
+
}
|
|
1539
|
+
const input = options.inputJson ? readInputJsonObject(options.inputJson, "capability_input_invalid", recovery) : parseKeyValueList(options.input);
|
|
1540
|
+
await runServicesInvoke(backend, config, serviceExecutionID, options.capability, input, { jsonOutput: Boolean(options.json) });
|
|
1275
1541
|
}
|
|
1276
1542
|
catch (error) {
|
|
1277
1543
|
reportCLIError(error, {
|
|
@@ -1297,12 +1563,18 @@ services
|
|
|
1297
1563
|
.option("--result-item <service_capability_result_item_id>")
|
|
1298
1564
|
.option("--required-before <step>")
|
|
1299
1565
|
.option("--input <key=value>", "action input snapshot", collectOption, [])
|
|
1566
|
+
.option("--input-json <file>", "JSON object containing the full action input snapshot")
|
|
1300
1567
|
.option("--json", "output JSON instead of terminal text")
|
|
1301
1568
|
.action(async (serviceExecutionID, options) => {
|
|
1302
1569
|
const config = loadConfig();
|
|
1303
1570
|
const backend = newBackendClient(config);
|
|
1304
1571
|
try {
|
|
1305
|
-
|
|
1572
|
+
const recovery = [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前动作要求" }];
|
|
1573
|
+
if (options.inputJson && options.input.length > 0) {
|
|
1574
|
+
throw new CommandContractError("service_action_invalid", "--input and --input-json cannot be combined", "--input 与 --input-json 只能二选一:嵌套结构用 --input-json <file>,扁平键值用 --input key=value。", recovery);
|
|
1575
|
+
}
|
|
1576
|
+
const fileInput = options.inputJson ? readInputJsonObject(options.inputJson, "service_action_invalid", recovery) : {};
|
|
1577
|
+
await runServicesAction(backend, serviceExecutionID, options.action, { ...fileInput, ...parseKeyValueList(options.input) }, {
|
|
1306
1578
|
...(options.actorType ? { actorType: options.actorType } : {}),
|
|
1307
1579
|
...(options.actorId ? { actorID: options.actorId } : {}),
|
|
1308
1580
|
...(options.status ? { status: options.status } : {}),
|
|
@@ -1447,12 +1719,17 @@ services
|
|
|
1447
1719
|
.command("next")
|
|
1448
1720
|
.description("Show the next recommended agent action for a Service Execution")
|
|
1449
1721
|
.argument("<service_execution_id>")
|
|
1722
|
+
.option("--since-snapshot <snapshot_id>", "delta read: suppress the journey payload when unchanged")
|
|
1723
|
+
.option("--timeout <seconds>", "wait up to 120 seconds for a useful result on this execution", (value) => Number(value))
|
|
1450
1724
|
.option("--json", "output JSON instead of terminal text")
|
|
1451
1725
|
.action(async (serviceExecutionID, options) => {
|
|
1452
1726
|
const config = loadConfig();
|
|
1453
1727
|
const backend = newBackendClient(config);
|
|
1454
1728
|
try {
|
|
1455
|
-
|
|
1729
|
+
if (options.timeout !== undefined && (!Number.isInteger(options.timeout) || options.timeout < 0 || options.timeout > 120)) {
|
|
1730
|
+
throw new Error("--timeout must be an integer from 0 to 120");
|
|
1731
|
+
}
|
|
1732
|
+
await runServicesNext(backend, serviceExecutionID, { jsonOutput: Boolean(options.json), sinceSnapshot: options.sinceSnapshot, timeoutSeconds: options.timeout });
|
|
1456
1733
|
}
|
|
1457
1734
|
catch (error) {
|
|
1458
1735
|
reportCLIError(error, {
|
|
@@ -1465,20 +1742,28 @@ services
|
|
|
1465
1742
|
});
|
|
1466
1743
|
services
|
|
1467
1744
|
.command("read-result")
|
|
1468
|
-
.description("Read
|
|
1745
|
+
.description("Read saved railway results or human-granted service content")
|
|
1469
1746
|
.argument("<service_execution_id>")
|
|
1747
|
+
.option("--snapshot <snapshot_id>", "Smart: saved snapshot catalog; add --journey for a journey detail. Exact: saved station-pair rows without --snapshot")
|
|
1748
|
+
.option("--journey <journey_id>", "rail.progressive.v2: journey id to read")
|
|
1749
|
+
.option("--offset <offset>", "saved journey offset", Number, 0)
|
|
1750
|
+
.option("--limit <limit>", "rows per page (1-20): Exact 20, Smart 3", Number)
|
|
1751
|
+
.option("--all", "read every saved journey page")
|
|
1470
1752
|
.option("--json", "output JSON instead of terminal text")
|
|
1471
1753
|
.action(async (serviceExecutionID, options) => {
|
|
1472
1754
|
const config = loadConfig();
|
|
1473
1755
|
const backend = newBackendClient(config);
|
|
1474
1756
|
try {
|
|
1475
|
-
await runServicesReadResult(backend, serviceExecutionID, { jsonOutput: Boolean(options.json)
|
|
1757
|
+
await runServicesReadResult(backend, serviceExecutionID, { jsonOutput: Boolean(options.json), snapshot: options.snapshot, journey: options.journey,
|
|
1758
|
+
offset: options.offset, limit: options.limit, all: Boolean(options.all) });
|
|
1476
1759
|
}
|
|
1477
1760
|
catch (error) {
|
|
1478
1761
|
reportCLIError(error, {
|
|
1479
1762
|
jsonOutput: Boolean(options.json),
|
|
1480
|
-
code: "agent_access_denied",
|
|
1481
|
-
instruction: "
|
|
1763
|
+
code: error instanceof HttpError && error.code === "agent_access_denied" ? "agent_access_denied" : "service_result_read_failed",
|
|
1764
|
+
instruction: error instanceof HttpError && error.code === "agent_access_denied"
|
|
1765
|
+
? "请用户在订单页面重新授权;不要使用开发者权限绕过授权或退款锁。"
|
|
1766
|
+
: "读取同一服务的已保存结果;若快照已不可读,先查看该服务当前状态和有效动作。",
|
|
1482
1767
|
recovery: [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "检查交付模式和 grant 状态" }],
|
|
1483
1768
|
});
|
|
1484
1769
|
}
|
|
@@ -1512,6 +1797,7 @@ services
|
|
|
1512
1797
|
});
|
|
1513
1798
|
}
|
|
1514
1799
|
});
|
|
1800
|
+
registerSell(program);
|
|
1515
1801
|
program.parseAsync(process.argv).catch((error) => {
|
|
1516
1802
|
reportCLIError(error);
|
|
1517
1803
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Local browser dispatch for `--present browser`. Opens the URL via the OS
|
|
2
|
+
// opener with an argv array — never a model-generated shell string. Dispatch
|
|
3
|
+
// proves only that the request was accepted by the OS; it never means the
|
|
4
|
+
// page became visible (that would be `page_loaded`, which needs evidence).
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { platform } from "node:os";
|
|
7
|
+
// Official HTTPS origins (or the loopback dev origin behind ITPAY_CLI_DEV).
|
|
8
|
+
// No userinfo, no non-http(s) schemes, no redirect chains — the opener gets
|
|
9
|
+
// exactly this URL once.
|
|
10
|
+
export function isPresentableURL(raw, baseURL, env = process.env) {
|
|
11
|
+
let url;
|
|
12
|
+
try {
|
|
13
|
+
url = new URL(raw);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (url.username || url.password)
|
|
19
|
+
return false;
|
|
20
|
+
if (env.ITPAY_CLI_DEV === "1" && /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(url.origin))
|
|
21
|
+
return true;
|
|
22
|
+
if (url.protocol !== "https:")
|
|
23
|
+
return false;
|
|
24
|
+
const allowedOrigins = new Set(["https://app.itpay.ai", "https://sandbox.itpay.ai", "https://dev.itpay.ai"]);
|
|
25
|
+
if (baseURL) {
|
|
26
|
+
try {
|
|
27
|
+
allowedOrigins.add(new URL(baseURL).origin);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* ignore malformed base URL */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return allowedOrigins.has(url.origin);
|
|
34
|
+
}
|
|
35
|
+
export function openInSystemBrowser(raw, baseURL, env = process.env) {
|
|
36
|
+
if (!isPresentableURL(raw, baseURL, env)) {
|
|
37
|
+
return Promise.resolve({ status: "failed", reason: "url_rejected" });
|
|
38
|
+
}
|
|
39
|
+
const [command, args] = openerArgv(raw);
|
|
40
|
+
if (!command) {
|
|
41
|
+
return Promise.resolve({ status: "unavailable", reason: "no system opener on this platform" });
|
|
42
|
+
}
|
|
43
|
+
return new Promise((resolvePromise) => {
|
|
44
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
45
|
+
child.once("error", (error) => resolvePromise({ status: "failed", reason: error.message }));
|
|
46
|
+
child.once("spawn", () => {
|
|
47
|
+
child.unref();
|
|
48
|
+
resolvePromise({ status: "dispatch_accepted", argv: [command, ...args] });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function openerArgv(url) {
|
|
53
|
+
switch (platform()) {
|
|
54
|
+
case "darwin":
|
|
55
|
+
return ["open", [url]];
|
|
56
|
+
case "win32":
|
|
57
|
+
return ["cmd.exe", ["/c", "start", "", url]];
|
|
58
|
+
case "linux":
|
|
59
|
+
case "freebsd":
|
|
60
|
+
case "openbsd":
|
|
61
|
+
return ["xdg-open", [url]];
|
|
62
|
+
default:
|
|
63
|
+
return [undefined, []];
|
|
64
|
+
}
|
|
65
|
+
}
|