@itpay/cli 2.0.3 → 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -142
- package/dist/src/client/backend.js +26 -8
- package/dist/src/client/http.js +29 -23
- package/dist/src/commands/buy.js +84 -132
- package/dist/src/commands/cart.js +274 -169
- package/dist/src/commands/catalog.js +64 -38
- package/dist/src/commands/checkout.js +128 -79
- package/dist/src/commands/docs.js +97 -51
- package/dist/src/commands/guidance.js +112 -16
- package/dist/src/commands/install.js +50 -87
- package/dist/src/commands/next.js +45 -0
- package/dist/src/commands/order.js +44 -69
- package/dist/src/commands/orders.js +43 -15
- package/dist/src/commands/pay.js +51 -22
- package/dist/src/commands/readyz.js +8 -4
- package/dist/src/commands/refund.js +132 -11
- package/dist/src/commands/services.js +799 -147
- package/dist/src/commands/skill.js +55 -0
- package/dist/src/main.js +820 -193
- package/dist/src/render/output.js +2 -3
- package/dist/src/state/agent_type.js +19 -0
- package/dist/src/state/cart_session.js +13 -17
- package/dist/src/state/client_context.js +4 -2
- package/dist/src/state/config.js +5 -15
- package/dist/src/state/device_authority.js +175 -57
- package/docs/agent/buyer/cart-checkout.json +27 -83
- package/docs/agent/buyer/catalog-list.json +2 -1
- package/docs/agent/buyer/identity-and-sessions.json +64 -0
- package/docs/agent/buyer/install-and-setup.json +35 -65
- package/docs/agent/buyer/orders-refunds.json +31 -53
- package/docs/agent/buyer/payment-flow.json +28 -57
- package/docs/agent/buyer/quickstart.json +46 -161
- package/docs/agent/buyer/render-hosts.json +43 -57
- package/docs/cli-reference/agent-types.md +51 -0
- package/docs/cli-reference/commands/buy.md +167 -0
- package/docs/cli-reference/commands/cart/add.md +86 -0
- package/docs/cli-reference/commands/cart/clear.md +53 -0
- package/docs/cli-reference/commands/cart/index.md +30 -0
- package/docs/cli-reference/commands/cart/next.md +71 -0
- package/docs/cli-reference/commands/cart/remove.md +53 -0
- package/docs/cli-reference/commands/cart/show.md +65 -0
- package/docs/cli-reference/commands/catalog/index.md +26 -0
- package/docs/cli-reference/commands/catalog/list.md +45 -0
- package/docs/cli-reference/commands/checkout.md +74 -0
- package/docs/cli-reference/commands/device.md +13 -0
- package/docs/cli-reference/commands/docs/index.md +28 -0
- package/docs/cli-reference/commands/docs/list.md +51 -0
- package/docs/cli-reference/commands/docs/search.md +69 -0
- package/docs/cli-reference/commands/docs/show.md +68 -0
- package/docs/cli-reference/commands/install.md +114 -0
- package/docs/cli-reference/commands/next.md +87 -0
- package/docs/cli-reference/commands/order.md +92 -0
- package/docs/cli-reference/commands/orders.md +83 -0
- package/docs/cli-reference/commands/pay.md +103 -0
- package/docs/cli-reference/commands/readyz.md +38 -0
- package/docs/cli-reference/commands/refund/cancel.md +62 -0
- package/docs/cli-reference/commands/refund/create.md +85 -0
- package/docs/cli-reference/commands/refund/get.md +60 -0
- package/docs/cli-reference/commands/refund/index.md +33 -0
- package/docs/cli-reference/commands/refund/list.md +68 -0
- package/docs/cli-reference/commands/refund/watch.md +73 -0
- package/docs/cli-reference/commands/services/action.md +48 -0
- package/docs/cli-reference/commands/services/checkout.md +82 -0
- package/docs/cli-reference/commands/services/events.md +73 -0
- package/docs/cli-reference/commands/services/get.md +66 -0
- package/docs/cli-reference/commands/services/index.md +45 -0
- package/docs/cli-reference/commands/services/invoke.md +67 -0
- package/docs/cli-reference/commands/services/list.md +61 -0
- package/docs/cli-reference/commands/services/next.md +181 -0
- package/docs/cli-reference/commands/services/quote.md +63 -0
- package/docs/cli-reference/commands/services/read-result.md +98 -0
- package/docs/cli-reference/commands/services/start.md +55 -0
- package/docs/cli-reference/commands/skill.md +17 -0
- package/docs/cli-reference/conventions.md +97 -0
- package/docs/cli-reference/index.md +65 -0
- package/package.json +1 -1
- package/skills/itpay-buyer/SKILL.md +71 -110
package/dist/src/main.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// services/backend/internal/httpapi/handlers/*.go. Commands only
|
|
3
3
|
// orchestrate; HTTP and rendering live in src/client and src/render.
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { loadConfig, cartSessionPath, newBackendClient } from "./state/config.js";
|
|
5
|
+
import { CLI_VERSION, loadConfig, cartSessionPath, newBackendClient } from "./state/config.js";
|
|
6
|
+
import { DeviceAuthority, DeviceAuthorizationError, DeviceStateError } from "./state/device_authority.js";
|
|
6
7
|
import { CartSession } from "./state/cart_session.js";
|
|
7
|
-
import { defaultHostForAgentType, normalizeHost } from "./state/client_context.js";
|
|
8
|
+
import { defaultHostForAgentType, normalizeHost, validateContext } from "./state/client_context.js";
|
|
8
9
|
import { HttpError } from "./client/http.js";
|
|
9
10
|
import { runReadyz } from "./commands/readyz.js";
|
|
10
11
|
import { runBuy } from "./commands/buy.js";
|
|
@@ -13,18 +14,20 @@ import { runCheckoutPresentation } from "./commands/checkout.js";
|
|
|
13
14
|
import { runPay } from "./commands/pay.js";
|
|
14
15
|
import { runOrder } from "./commands/order.js";
|
|
15
16
|
import { runListOrders } from "./commands/orders.js";
|
|
16
|
-
import { runRefund } from "./commands/refund.js";
|
|
17
|
-
import { runCartAdd, runCartAddServer, runCartAbandonServer, runCartClear, runCartNext, runCartRemove, runCartRemoveServer, runCartShowServer, } from "./commands/cart.js";
|
|
18
|
-
import { printErrorRecovery } from "./commands/guidance.js";
|
|
17
|
+
import { runCancelRefund, runGetRefund, runListRefunds, runRefund, runWatchRefund } from "./commands/refund.js";
|
|
18
|
+
import { runCartAdd, runCartAddQuoteServer, runCartAddServer, runCartAbandonServer, runCartClear, runCartNext, runCartRemove, runCartRemoveServer, runCartShow, runCartShowServer, } from "./commands/cart.js";
|
|
19
|
+
import { CommandContractError, errorRecoveryActions, printErrorRecovery, writeCommandEnvelope } from "./commands/guidance.js";
|
|
19
20
|
import { runDocsList, runDocsShow, runDocsSearch } from "./commands/docs.js";
|
|
20
21
|
import { runInstall } from "./commands/install.js";
|
|
21
|
-
import {
|
|
22
|
+
import { runSkillShow } from "./commands/skill.js";
|
|
23
|
+
import { runNext } from "./commands/next.js";
|
|
24
|
+
import { collectOption, parseKeyValueList, runServicesAction, runServicesCheckout, runServicesEvents, runServicesGet, runServicesInvoke, runServicesList, runServicesNext, runServicesReadResult, runServicesQuote, runServicesStart, } from "./commands/services.js";
|
|
22
25
|
const program = new Command();
|
|
23
26
|
program
|
|
24
27
|
.name("itpay")
|
|
25
28
|
.description("V3 ItPay CLI — checkout, payment, order, and refund commands")
|
|
26
29
|
.option("--agent-type <type>", "agent runtime type used for device enrollment and client-specific guidance")
|
|
27
|
-
.version(
|
|
30
|
+
.version(CLI_VERSION);
|
|
28
31
|
function withHost(value) {
|
|
29
32
|
const host = normalizeHost(value);
|
|
30
33
|
if (!host) {
|
|
@@ -36,12 +39,24 @@ function parseRequiredContactFields(value) {
|
|
|
36
39
|
if (!value) {
|
|
37
40
|
return undefined;
|
|
38
41
|
}
|
|
39
|
-
const
|
|
42
|
+
const values = value
|
|
40
43
|
.split(",")
|
|
41
44
|
.map((item) => item.trim().toLowerCase())
|
|
42
|
-
.filter(
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
const invalid = values.filter((item) => item !== "email" && item !== "phone");
|
|
47
|
+
if (invalid.length > 0) {
|
|
48
|
+
throw new CommandContractError("contact_field_invalid", `unsupported required contact fields: ${invalid.join(", ")}`, "--require-contact 只接受 email、phone 或二者组合。", [{ command: "itpay buy --help", reason: "查看 contact 参数" }]);
|
|
49
|
+
}
|
|
50
|
+
const parsed = values.filter((item) => item === "email" || item === "phone");
|
|
43
51
|
return parsed.length > 0 ? parsed : undefined;
|
|
44
52
|
}
|
|
53
|
+
function positiveInteger(value, name) {
|
|
54
|
+
const text = String(value ?? "");
|
|
55
|
+
if (!/^[1-9]\d*$/.test(text)) {
|
|
56
|
+
throw new CommandContractError("buy_parameter_invalid", `${name} must be a positive integer`, `${name} 必须是正整数;本次未创建或修改 Cart/Checkout。`, [{ command: "itpay buy --help", reason: "查看参数格式" }]);
|
|
57
|
+
}
|
|
58
|
+
return Number(text);
|
|
59
|
+
}
|
|
45
60
|
function resolveCheckoutPresentationArgs(input) {
|
|
46
61
|
if (input.requestedCheckoutID && input.requestedDisplayToken) {
|
|
47
62
|
return { checkoutID: input.requestedCheckoutID, displayToken: input.requestedDisplayToken };
|
|
@@ -65,7 +80,56 @@ function resolveCheckoutPresentationArgs(input) {
|
|
|
65
80
|
}
|
|
66
81
|
throw new Error("checkout id and display token are required; pass --id/--token or create a checkout first");
|
|
67
82
|
}
|
|
68
|
-
function reportCLIError(error) {
|
|
83
|
+
function reportCLIError(error, contract) {
|
|
84
|
+
const commandError = error instanceof CommandContractError ? error : undefined;
|
|
85
|
+
const deviceError = error instanceof DeviceAuthorizationError ? error : undefined;
|
|
86
|
+
const stateError = error instanceof DeviceStateError ? error : undefined;
|
|
87
|
+
const httpRecovery = errorRecoveryActions(error).map((action) => ({
|
|
88
|
+
command: action.command,
|
|
89
|
+
reason: action.reason ?? action.label,
|
|
90
|
+
}));
|
|
91
|
+
const identityRecovery = error instanceof HttpError &&
|
|
92
|
+
(error.code === "agent_identity_required" || error.code === "agent_device_session_required");
|
|
93
|
+
const incompatible = error instanceof HttpError && (error.code === "client_upgrade_required" ||
|
|
94
|
+
error.code === "client_compatibility_headers_required" ||
|
|
95
|
+
error.code === "platform_release_unavailable" ||
|
|
96
|
+
(error.status === 404 && error.code === "unknown_error"));
|
|
97
|
+
const deviceRecovery = deviceError ? [{
|
|
98
|
+
command: "itpay skill show itpay-buyer --json",
|
|
99
|
+
reason: "读取身份边界;该错误需要用户或运营恢复 Backend 登记,不能通过换类型或删除本地身份绕过",
|
|
100
|
+
}] : [];
|
|
101
|
+
const stateRecovery = stateError ? [{
|
|
102
|
+
command: "itpay skill show itpay-buyer --json",
|
|
103
|
+
reason: "读取 Device 状态边界;修复当前 Host 的持久写权限后重试原命令",
|
|
104
|
+
}] : [];
|
|
105
|
+
const authorizationInstruction = stateError
|
|
106
|
+
? "当前运行环境无法写入 owner-only Device 状态;请保持同一 Node、CLI 和 Agent Type,在允许持久写入 ~/.itpay-v3 的执行环境中重试。不要手工创建 lock、删除 identity 或换运行时碰运气。"
|
|
107
|
+
: error instanceof HttpError && error.code === "agent_device_session_required"
|
|
108
|
+
? "CLI 已自动续期并重试同一请求一次,仍被拒绝;停止重试,不要切换 Agent Type 或旋转身份。"
|
|
109
|
+
: deviceError?.code === "agent_device_revoked"
|
|
110
|
+
? "Backend 已撤销当前 Device 登记;CLI 没有自动创建替代身份。停止重试并请用户或运营恢复登记。"
|
|
111
|
+
: deviceError
|
|
112
|
+
? "Device 身份验证失败;停止重试,不要切换 Agent Type、删除状态或旋转私钥。"
|
|
113
|
+
: undefined;
|
|
114
|
+
if (contract || commandError) {
|
|
115
|
+
writeCommandEnvelope({
|
|
116
|
+
status: "error",
|
|
117
|
+
error: {
|
|
118
|
+
code: incompatible ? "backend_contract_incompatible" : commandError?.code ?? (error instanceof HttpError ? error.code : stateError?.code ?? deviceError?.code ?? contract?.code ?? "command_failed"),
|
|
119
|
+
message: error instanceof Error ? error.message : String(error),
|
|
120
|
+
},
|
|
121
|
+
instruction: incompatible
|
|
122
|
+
? "当前 Backend 不支持本 CLI 所需的交易合同。立即停止;不要尝试 services quote、services checkout、cart、buy 或 pay 作为替代路径。需要先同步 CLI 与 Backend 版本。"
|
|
123
|
+
: commandError?.instruction ?? authorizationInstruction ?? contract?.instruction ?? "检查命令参数后重试。",
|
|
124
|
+
next: null,
|
|
125
|
+
recovery: incompatible ? [] : commandError?.recovery ?? (stateError ? stateRecovery : deviceError ? deviceRecovery : identityRecovery ? httpRecovery : contract?.recovery ?? []),
|
|
126
|
+
}, {
|
|
127
|
+
...(contract?.jsonOutput !== undefined ? { jsonOutput: contract.jsonOutput } : {}),
|
|
128
|
+
output: (text) => { process.stderr.write(text); },
|
|
129
|
+
});
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
69
133
|
if (error instanceof HttpError) {
|
|
70
134
|
process.stderr.write(`[${error.status}] ${error.code}: ${error.message}\n`);
|
|
71
135
|
printErrorRecovery(error, (text) => process.stderr.write(text));
|
|
@@ -79,27 +143,120 @@ function reportCLIError(error) {
|
|
|
79
143
|
}
|
|
80
144
|
throw error;
|
|
81
145
|
}
|
|
146
|
+
function docsErrorFallback(jsonOutput) {
|
|
147
|
+
return {
|
|
148
|
+
jsonOutput,
|
|
149
|
+
code: "docs_unavailable",
|
|
150
|
+
instruction: "内置文档缺失或损坏;重新安装同版本 CLI 后重试。",
|
|
151
|
+
recovery: [{ command: `npm install -g @itpay/cli@${CLI_VERSION}`, reason: "恢复随包发布的文档" }],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
82
154
|
program
|
|
83
155
|
.command("readyz")
|
|
84
156
|
.description("Probe the V3 backend readiness endpoint")
|
|
85
|
-
.action(async () => {
|
|
86
|
-
await withBackend(async (backend) => runReadyz(backend));
|
|
87
|
-
});
|
|
88
|
-
program
|
|
89
|
-
.command("next")
|
|
90
|
-
.description("Show the next recommended agent action from remembered server handles")
|
|
91
157
|
.option("--json", "output JSON instead of terminal text")
|
|
92
158
|
.action(async (options) => {
|
|
93
159
|
const config = loadConfig();
|
|
94
160
|
const backend = newBackendClient(config);
|
|
95
|
-
const session = CartSession.loadFromFile(cartSessionPath(), config.checkoutCurrency);
|
|
96
161
|
try {
|
|
97
|
-
await
|
|
162
|
+
await runReadyz(backend, { jsonOutput: Boolean(options.json), ...(config.agentType ? { agentType: config.agentType } : {}) });
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
reportCLIError(error, {
|
|
166
|
+
jsonOutput: Boolean(options.json),
|
|
167
|
+
code: "backend_unavailable",
|
|
168
|
+
instruction: "检查 ITPAY_BACKEND_URL 后重试;后端恢复前不要继续下单。",
|
|
169
|
+
recovery: [
|
|
170
|
+
{ command: "echo $ITPAY_BACKEND_URL", reason: "确认当前 Backend URL" },
|
|
171
|
+
{ command: "itpay readyz", reason: "重试可用性检查" },
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
// --- device ---------------------------------------------------------------
|
|
177
|
+
const deviceCmd = program.command("device").description("Recover local Device registration state after an operator-confirmed Backend reset");
|
|
178
|
+
deviceCmd
|
|
179
|
+
.command("recover")
|
|
180
|
+
.description("Forget only the selected Backend registration while preserving the local private key")
|
|
181
|
+
.option("--confirm-backend-reset", "confirm that an operator reset the selected Backend registration database")
|
|
182
|
+
.option("--json", "output JSON instead of terminal text")
|
|
183
|
+
.action(async (options) => {
|
|
184
|
+
const config = loadConfig();
|
|
185
|
+
try {
|
|
186
|
+
if (!config.agentType) {
|
|
187
|
+
throw new CommandContractError("agent_type_required", "agent type is required for Backend-scoped Device recovery", "如实声明当前 Agent Type;恢复后必须用同一类型重新登记。", [{ command: "itpay install --json", reason: "选择当前真实 Agent Type" }]);
|
|
188
|
+
}
|
|
189
|
+
if (!options.confirmBackendReset) {
|
|
190
|
+
throw new CommandContractError("backend_reset_confirmation_required", "--confirm-backend-reset is required", "仅在运营已确认当前 Backend 的 Device 登记数据库被重建或清空后执行;普通 session 失效或 revoked 不得使用。", [{ command: "itpay docs show identity-and-sessions --json", reason: "检查适用边界" }]);
|
|
191
|
+
}
|
|
192
|
+
const recovered = await new DeviceAuthority({
|
|
193
|
+
baseURL: config.baseURL,
|
|
194
|
+
requestedAgentType: config.agentType,
|
|
195
|
+
compatibilityHeaders: {},
|
|
196
|
+
}).recoverBackendReset();
|
|
197
|
+
writeCommandEnvelope({
|
|
198
|
+
status: recovered.removed ? "backend_registration_removed" : "backend_registration_absent",
|
|
199
|
+
result: {
|
|
200
|
+
backend: config.baseURL,
|
|
201
|
+
removed_agent_types: recovered.agentTypes,
|
|
202
|
+
private_key_preserved: true,
|
|
203
|
+
other_backend_registrations_preserved: true,
|
|
204
|
+
},
|
|
205
|
+
instruction: "只读列出 Service Executions,以同一私钥和 Agent Type 重新登记当前 Backend;不要删除 ~/.itpay-v3 或切换运行时。",
|
|
206
|
+
next: {
|
|
207
|
+
command: `itpay --agent-type ${config.agentType} services list --limit 1 --json`,
|
|
208
|
+
reason: "用无业务写入的签名请求重新登记当前 Backend",
|
|
209
|
+
},
|
|
210
|
+
recovery: [],
|
|
211
|
+
}, {
|
|
212
|
+
jsonOutput: Boolean(options.json),
|
|
213
|
+
plainResult: [
|
|
214
|
+
`backend: ${config.baseURL}`,
|
|
215
|
+
`registration: ${recovered.removed ? "removed" : "already absent"}`,
|
|
216
|
+
"private_key: preserved",
|
|
217
|
+
"other_backends: preserved",
|
|
218
|
+
],
|
|
219
|
+
});
|
|
98
220
|
}
|
|
99
221
|
catch (error) {
|
|
100
|
-
reportCLIError(error
|
|
222
|
+
reportCLIError(error, {
|
|
223
|
+
jsonOutput: Boolean(options.json),
|
|
224
|
+
code: "device_recovery_failed",
|
|
225
|
+
instruction: "仅恢复运营已确认重建的当前 Backend;不要删除整个 Device identity。",
|
|
226
|
+
recovery: [{ command: "itpay docs show identity-and-sessions --json", reason: "检查 Device 恢复边界" }],
|
|
227
|
+
});
|
|
101
228
|
}
|
|
102
229
|
});
|
|
230
|
+
// --- skill ----------------------------------------------------------------
|
|
231
|
+
const skillCmd = program.command("skill").description("Read complete packaged Agent skills");
|
|
232
|
+
skillCmd
|
|
233
|
+
.command("show")
|
|
234
|
+
.description("Show one complete packaged skill")
|
|
235
|
+
.argument("<name>", "skill name")
|
|
236
|
+
.option("--json", "output JSON instead of terminal text")
|
|
237
|
+
.action((name, options) => {
|
|
238
|
+
const config = loadConfig();
|
|
239
|
+
try {
|
|
240
|
+
runSkillShow(name, { jsonOutput: Boolean(options.json), ...(config.agentType ? { agentType: config.agentType } : {}) });
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
reportCLIError(error, {
|
|
244
|
+
jsonOutput: Boolean(options.json),
|
|
245
|
+
code: "skill_unavailable",
|
|
246
|
+
instruction: "内置 Skill 缺失或损坏;重新安装同版本 CLI 后重试。",
|
|
247
|
+
recovery: [{ command: `npm install -g @itpay/cli@${CLI_VERSION}`, reason: "恢复随包发布的 Skill" }],
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
program
|
|
252
|
+
.command("next")
|
|
253
|
+
.description("Show the next recommended agent action from remembered server handles")
|
|
254
|
+
.option("--json", "output JSON instead of terminal text")
|
|
255
|
+
.action(async (options) => {
|
|
256
|
+
const config = loadConfig();
|
|
257
|
+
const session = CartSession.loadFromFile(cartSessionPath(), config.checkoutCurrency);
|
|
258
|
+
runNext(session, { jsonOutput: Boolean(options.json) });
|
|
259
|
+
});
|
|
103
260
|
// --- catalog --------------------------------------------------------------
|
|
104
261
|
const catalogCmd = program.command("catalog").description("Browse V3 service catalog");
|
|
105
262
|
catalogCmd
|
|
@@ -107,49 +264,93 @@ catalogCmd
|
|
|
107
264
|
.description("List all available services from the published catalog manifest")
|
|
108
265
|
.option("--json", "output JSON instead of terminal text")
|
|
109
266
|
.action(async (options) => {
|
|
110
|
-
|
|
267
|
+
const backend = newBackendClient(loadConfig());
|
|
268
|
+
try {
|
|
269
|
+
await runCatalogList(backend, { jsonOutput: Boolean(options.json) });
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
reportCLIError(error, {
|
|
273
|
+
jsonOutput: Boolean(options.json),
|
|
274
|
+
code: "catalog_unavailable",
|
|
275
|
+
instruction: "确认 ItPay 可用后重试目录读取;不要猜测 service_id。",
|
|
276
|
+
recovery: [
|
|
277
|
+
{ command: "itpay readyz", reason: "确认 Backend 可用" },
|
|
278
|
+
{ command: "itpay catalog list", reason: "重新读取已发布目录" },
|
|
279
|
+
],
|
|
280
|
+
});
|
|
281
|
+
}
|
|
111
282
|
});
|
|
112
283
|
// --- install --------------------------------------------------------------
|
|
113
284
|
const installCmd = program.command("install").description("Show setup instructions for each agent host");
|
|
114
285
|
installCmd
|
|
115
|
-
.argument("[target]", "
|
|
286
|
+
.argument("[target]", "Agent Type, or list")
|
|
287
|
+
.option("--json", "output JSON instead of terminal text")
|
|
116
288
|
.description("Show agent-specific installation and configuration instructions")
|
|
117
|
-
.action((target) => {
|
|
118
|
-
|
|
289
|
+
.action((target, options) => {
|
|
290
|
+
try {
|
|
291
|
+
runInstall(target, { jsonOutput: Boolean(options.json) });
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
reportCLIError(error, {
|
|
295
|
+
jsonOutput: Boolean(options.json),
|
|
296
|
+
code: "install_failed",
|
|
297
|
+
instruction: "选择受支持的真实 Agent Type;本命令不会修改宿主配置。",
|
|
298
|
+
recovery: [{ command: "itpay install --json", reason: "列出支持类型" }],
|
|
299
|
+
});
|
|
300
|
+
}
|
|
119
301
|
});
|
|
120
302
|
// --- docs -----------------------------------------------------------------
|
|
121
303
|
const docsCmd = program.command("docs").description("Browse agent documentation");
|
|
122
304
|
docsCmd
|
|
123
305
|
.command("list")
|
|
124
306
|
.description("List all available agent doc topics")
|
|
125
|
-
.
|
|
126
|
-
|
|
307
|
+
.option("--json", "output JSON instead of terminal text")
|
|
308
|
+
.action((options) => {
|
|
309
|
+
try {
|
|
310
|
+
runDocsList({ jsonOutput: Boolean(options.json) });
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
reportCLIError(error, docsErrorFallback(Boolean(options.json)));
|
|
314
|
+
}
|
|
127
315
|
});
|
|
128
316
|
docsCmd
|
|
129
317
|
.command("show")
|
|
130
318
|
.description("Show a specific doc topic")
|
|
131
319
|
.argument("<topic>", "doc topic name")
|
|
132
|
-
.
|
|
133
|
-
|
|
320
|
+
.option("--json", "output JSON instead of terminal text")
|
|
321
|
+
.action((topic, options) => {
|
|
322
|
+
try {
|
|
323
|
+
runDocsShow(topic, { jsonOutput: Boolean(options.json) });
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
reportCLIError(error, docsErrorFallback(Boolean(options.json)));
|
|
327
|
+
}
|
|
134
328
|
});
|
|
135
329
|
docsCmd
|
|
136
330
|
.command("search")
|
|
137
331
|
.description("Search doc topics by keyword")
|
|
138
332
|
.argument("<query>", "search query")
|
|
139
|
-
.
|
|
140
|
-
|
|
333
|
+
.option("--json", "output JSON instead of terminal text")
|
|
334
|
+
.action((query, options) => {
|
|
335
|
+
try {
|
|
336
|
+
runDocsSearch(query, { jsonOutput: Boolean(options.json) });
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
reportCLIError(error, docsErrorFallback(Boolean(options.json)));
|
|
340
|
+
}
|
|
141
341
|
});
|
|
142
342
|
// --- cart ----------------------------------------------------------------
|
|
143
343
|
const cart = program.command("cart").description("V3 canonical server cart");
|
|
144
344
|
cart
|
|
145
345
|
.command("add")
|
|
146
346
|
.description("Add a variant/offer/quantity to the canonical server cart")
|
|
147
|
-
.
|
|
148
|
-
.
|
|
149
|
-
.
|
|
150
|
-
.option("--
|
|
347
|
+
.option("--item <catalog_item_id>")
|
|
348
|
+
.option("--variant <catalog_variant_id>")
|
|
349
|
+
.option("--offer <offer_id>")
|
|
350
|
+
.option("--quote <service_quote_lock_id>", "add a prepared service quote")
|
|
351
|
+
.option("--quantity <n>", "quantity", Number, 1)
|
|
151
352
|
.option("--input <json>")
|
|
152
|
-
.option("--host <host>", "client host (terminal, codex, telegram, feishu, lark, ...)"
|
|
353
|
+
.option("--host <host>", "client host (terminal, codex, telegram, feishu, lark, ...)")
|
|
153
354
|
.option("--target <target>", "chat id / channel id / open id for IM hosts")
|
|
154
355
|
.option("--json", "output JSON instead of terminal text")
|
|
155
356
|
.option("--local", "only add to the local draft cache; not valid for service-backed flows")
|
|
@@ -157,32 +358,82 @@ cart
|
|
|
157
358
|
const config = loadConfig();
|
|
158
359
|
const sessionPath = cartSessionPath();
|
|
159
360
|
const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
|
|
160
|
-
const
|
|
161
|
-
catalogItemID: options.item,
|
|
162
|
-
catalogVariantID: options.variant,
|
|
163
|
-
offerID: options.offer,
|
|
164
|
-
quantity: options.quantity,
|
|
165
|
-
...(options.input ? { input: JSON.parse(options.input) } : {}),
|
|
166
|
-
};
|
|
361
|
+
const jsonOutput = Boolean(options.json);
|
|
167
362
|
try {
|
|
363
|
+
const quoteMode = Boolean(options.quote);
|
|
364
|
+
if (quoteMode && (options.item || options.variant || options.offer || options.input || options.local)) {
|
|
365
|
+
throw new CommandContractError("cart_item_scope_invalid", "--quote cannot be combined with catalog fields, input, or --local", "服务报价只使用 services quote 返回的 quote ID;不要混入 Catalog 或 input 参数。", [{ command: "itpay cart add --help", reason: "查看两种添加方式" }]);
|
|
366
|
+
}
|
|
367
|
+
if (!quoteMode && (!options.item || !options.variant || !options.offer)) {
|
|
368
|
+
throw new CommandContractError("cart_item_required", "--item, --variant and --offer are required", "使用同一条 Catalog 记录返回的 item、variant 和 offer ID;不要猜测或混用。", [{ command: "itpay catalog list --json", reason: "读取已发布目录 ID" }]);
|
|
369
|
+
}
|
|
370
|
+
if (!quoteMode && (!Number.isInteger(options.quantity) || options.quantity < 1)) {
|
|
371
|
+
throw new CommandContractError("quantity_invalid", "--quantity must be a positive integer", "使用大于 0 的整数 quantity;本次未修改 Cart。", [{ command: "itpay cart show", reason: "确认当前 Cart 未变化" }]);
|
|
372
|
+
}
|
|
373
|
+
let input;
|
|
374
|
+
if (options.input) {
|
|
375
|
+
const parsed = JSON.parse(options.input);
|
|
376
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
377
|
+
throw new CommandContractError("cart_input_invalid", "--input must be a JSON object", "按照服务合同传入 JSON object;本次未修改 Cart。", [{ command: "itpay catalog list --json", reason: "重新确认服务入口" }]);
|
|
378
|
+
}
|
|
379
|
+
input = parsed;
|
|
380
|
+
}
|
|
381
|
+
const addOptions = {
|
|
382
|
+
catalogItemID: options.item,
|
|
383
|
+
catalogVariantID: options.variant,
|
|
384
|
+
offerID: options.offer,
|
|
385
|
+
quantity: options.quantity,
|
|
386
|
+
...(input ? { input } : {}),
|
|
387
|
+
};
|
|
168
388
|
if (options.local) {
|
|
169
|
-
runCartAdd(session, addOptions);
|
|
389
|
+
runCartAdd(session, { ...addOptions, jsonOutput });
|
|
170
390
|
}
|
|
171
391
|
else {
|
|
392
|
+
const host = withHost(options.host ?? defaultHostForAgentType(config.agentType));
|
|
393
|
+
const contextError = validateContext(host, options.target);
|
|
394
|
+
if (contextError) {
|
|
395
|
+
throw new CommandContractError(contextError.code, contextError.message, "补齐当前客户端所需的 Host/target;本次未创建或修改 Cart。", [{ command: "itpay cart add --help", reason: "查看客户端参数" }]);
|
|
396
|
+
}
|
|
172
397
|
const backend = newBackendClient(config);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
398
|
+
if (quoteMode) {
|
|
399
|
+
await runCartAddQuoteServer({
|
|
400
|
+
serviceQuoteLockID: options.quote,
|
|
401
|
+
backend, config, session, host,
|
|
402
|
+
...(options.target ? { target: options.target } : {}),
|
|
403
|
+
jsonOutput,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
await runCartAddServer({
|
|
408
|
+
...addOptions,
|
|
409
|
+
backend,
|
|
410
|
+
config,
|
|
411
|
+
session,
|
|
412
|
+
host,
|
|
413
|
+
...(options.target ? { target: options.target } : {}),
|
|
414
|
+
jsonOutput,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
182
417
|
}
|
|
183
418
|
}
|
|
184
419
|
catch (error) {
|
|
185
|
-
|
|
420
|
+
const quoteMode = Boolean(options.quote);
|
|
421
|
+
reportCLIError(error, {
|
|
422
|
+
jsonOutput,
|
|
423
|
+
code: "cart_add_failed",
|
|
424
|
+
instruction: quoteMode
|
|
425
|
+
? "Quote 是否已加入 Cart 尚未确认;先恢复当前 Cart 或来源 Execution,不要重复准备报价或直接创建 Checkout。"
|
|
426
|
+
: "核对 Catalog ID、输入和 Cart 状态;不要在失败后直接创建 Checkout。",
|
|
427
|
+
recovery: quoteMode
|
|
428
|
+
? [
|
|
429
|
+
{ command: "itpay cart show --json", reason: "检查当前 canonical Cart" },
|
|
430
|
+
{ command: "itpay services list --json", reason: "恢复 Quote 所属的 Service Execution" },
|
|
431
|
+
]
|
|
432
|
+
: [
|
|
433
|
+
{ command: "itpay catalog list --json", reason: "核对已发布项目" },
|
|
434
|
+
{ command: "itpay cart show", reason: "确认 canonical Cart 当前状态" },
|
|
435
|
+
],
|
|
436
|
+
});
|
|
186
437
|
}
|
|
187
438
|
finally {
|
|
188
439
|
session.saveToFile(sessionPath);
|
|
@@ -200,7 +451,15 @@ cart
|
|
|
200
451
|
await runCartNext(backend, session, { jsonOutput: Boolean(options.json) });
|
|
201
452
|
}
|
|
202
453
|
catch (error) {
|
|
203
|
-
reportCLIError(error
|
|
454
|
+
reportCLIError(error, {
|
|
455
|
+
jsonOutput: Boolean(options.json),
|
|
456
|
+
code: "cart_next_failed",
|
|
457
|
+
instruction: "canonical Cart 无法读取;不要猜测 Cart 内容或创建重复订单。",
|
|
458
|
+
recovery: [
|
|
459
|
+
{ command: "itpay services list --json", reason: "恢复当前设备可见的 Service Execution" },
|
|
460
|
+
{ command: "itpay catalog list --json", reason: "在没有可恢复执行时重新选择服务" },
|
|
461
|
+
],
|
|
462
|
+
});
|
|
204
463
|
}
|
|
205
464
|
});
|
|
206
465
|
cart
|
|
@@ -210,27 +469,51 @@ cart
|
|
|
210
469
|
.option("--variant <catalog_variant_id>", "local draft variant id, only with --local")
|
|
211
470
|
.option("--offer <offer_id>", "local draft offer id, only with --local")
|
|
212
471
|
.option("--local", "only remove from the explicit local draft cache")
|
|
472
|
+
.option("--json", "output JSON instead of terminal text")
|
|
213
473
|
.action(async (options) => {
|
|
214
474
|
const config = loadConfig();
|
|
215
475
|
const sessionPath = cartSessionPath();
|
|
216
476
|
const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
|
|
477
|
+
const jsonOutput = Boolean(options.json);
|
|
217
478
|
try {
|
|
218
479
|
if (options.local) {
|
|
219
480
|
if (!options.variant || !options.offer) {
|
|
220
|
-
throw new
|
|
481
|
+
throw new CommandContractError("local_cart_item_required", "--local remove requires --variant and --offer", "从 `cart show --local --json` 使用同一条草稿 line 的 variant 和 offer;不要猜测。", [{ command: "itpay cart show --local --json", reason: "读取本地草稿" }]);
|
|
482
|
+
}
|
|
483
|
+
if (options.line) {
|
|
484
|
+
throw new CommandContractError("cart_remove_scope_invalid", "--line cannot be combined with --local", "canonical line 使用 --line;本地草稿使用 --local --variant --offer,不要混用。", [{ command: "itpay cart remove --help", reason: "查看两种删除范围" }]);
|
|
221
485
|
}
|
|
222
486
|
runCartRemove(session, {
|
|
223
487
|
catalogVariantID: options.variant,
|
|
224
488
|
offerID: options.offer,
|
|
489
|
+
jsonOutput,
|
|
225
490
|
});
|
|
226
491
|
}
|
|
227
492
|
else {
|
|
493
|
+
if (options.variant || options.offer) {
|
|
494
|
+
throw new CommandContractError("cart_remove_scope_invalid", "--variant and --offer require --local", "canonical Cart 使用 cart_item_id;先从 `cart show --json` 读取 line。", [{ command: "itpay cart show --json", reason: "读取 canonical line 句柄" }]);
|
|
495
|
+
}
|
|
496
|
+
if (!session.lastCartID) {
|
|
497
|
+
throw new CommandContractError("cart_handle_missing", "no canonical server cart is remembered", "本地没有 canonical Cart 句柄;不要用 local draft 代替服务端删除。", [{ command: "itpay next --json", reason: "检查其他可恢复句柄" }]);
|
|
498
|
+
}
|
|
499
|
+
const lineID = options.line ?? session.lastCartItemID;
|
|
500
|
+
if (!lineID) {
|
|
501
|
+
throw new CommandContractError("cart_item_required", "cart item id is required", "从 `cart show --json` 选择 cart_item_id;不要使用 variant、offer 或 quote lock ID。", [{ command: "itpay cart show --json", reason: "读取 canonical line 句柄" }]);
|
|
502
|
+
}
|
|
228
503
|
const backend = newBackendClient(config);
|
|
229
|
-
await runCartRemoveServer(backend, session,
|
|
504
|
+
await runCartRemoveServer(backend, session, lineID, { jsonOutput });
|
|
230
505
|
}
|
|
231
506
|
}
|
|
232
507
|
catch (error) {
|
|
233
|
-
|
|
508
|
+
const locked = error instanceof HttpError && error.code === "cart_item_locked";
|
|
509
|
+
reportCLIError(error, {
|
|
510
|
+
jsonOutput,
|
|
511
|
+
code: "cart_remove_failed",
|
|
512
|
+
instruction: locked
|
|
513
|
+
? "该 line 已绑定 quote/Checkout,不能再删除;继续同一 Cart 的现有流程,不要清本地状态伪装成功。"
|
|
514
|
+
: "删除失败;重新读取同一 Cart,不要假设 line 或 Service Execution 已取消。",
|
|
515
|
+
recovery: [{ command: "itpay cart next --json", reason: "读取同一 Cart 的服务端首选动作" }],
|
|
516
|
+
});
|
|
234
517
|
}
|
|
235
518
|
finally {
|
|
236
519
|
session.saveToFile(sessionPath);
|
|
@@ -239,37 +522,60 @@ cart
|
|
|
239
522
|
cart
|
|
240
523
|
.command("show")
|
|
241
524
|
.description("Print the canonical server cart or local draft fallback")
|
|
242
|
-
.
|
|
525
|
+
.option("--json", "output JSON instead of terminal text")
|
|
526
|
+
.option("--local", "show only the explicit local compatibility draft")
|
|
527
|
+
.action(async (options) => {
|
|
243
528
|
const config = loadConfig();
|
|
244
|
-
const backend = newBackendClient(config);
|
|
245
529
|
const sessionPath = cartSessionPath();
|
|
246
530
|
const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
|
|
247
531
|
try {
|
|
248
|
-
|
|
532
|
+
if (options.local) {
|
|
533
|
+
runCartShow(session, { jsonOutput: Boolean(options.json) });
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
await runCartShowServer(newBackendClient(config), session, { jsonOutput: Boolean(options.json) });
|
|
537
|
+
}
|
|
249
538
|
}
|
|
250
539
|
catch (error) {
|
|
251
|
-
reportCLIError(error
|
|
540
|
+
reportCLIError(error, {
|
|
541
|
+
jsonOutput: Boolean(options.json),
|
|
542
|
+
code: "cart_show_failed",
|
|
543
|
+
instruction: "canonical Cart 无法读取;不要根据本地旧句柄猜测服务端状态。",
|
|
544
|
+
recovery: [
|
|
545
|
+
{ command: "itpay services list --json", reason: "恢复当前设备可见的 Service Execution" },
|
|
546
|
+
{ command: "itpay catalog list --json", reason: "在没有可恢复资源时重新选择" },
|
|
547
|
+
],
|
|
548
|
+
});
|
|
252
549
|
}
|
|
253
550
|
});
|
|
254
551
|
cart
|
|
255
552
|
.command("clear")
|
|
256
|
-
.description("Abandon the canonical server cart or clear local
|
|
553
|
+
.description("Abandon the canonical server cart or explicitly clear local state")
|
|
257
554
|
.option("--local", "only clear local handles and explicit local draft")
|
|
555
|
+
.option("--json", "output JSON instead of terminal text")
|
|
258
556
|
.action(async (options) => {
|
|
259
557
|
const config = loadConfig();
|
|
260
558
|
const sessionPath = cartSessionPath();
|
|
261
559
|
const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
|
|
262
560
|
try {
|
|
263
561
|
if (options.local) {
|
|
264
|
-
runCartClear(session);
|
|
562
|
+
runCartClear(session, { jsonOutput: Boolean(options.json) });
|
|
265
563
|
}
|
|
266
564
|
else {
|
|
267
565
|
const backend = newBackendClient(config);
|
|
268
|
-
await runCartAbandonServer(backend, session);
|
|
566
|
+
await runCartAbandonServer(backend, session, { jsonOutput: Boolean(options.json) });
|
|
269
567
|
}
|
|
270
568
|
}
|
|
271
569
|
catch (error) {
|
|
272
|
-
|
|
570
|
+
const locked = error instanceof HttpError && (error.code === "cart_item_locked" || error.status === 409);
|
|
571
|
+
reportCLIError(error, {
|
|
572
|
+
jsonOutput: Boolean(options.json),
|
|
573
|
+
code: "cart_clear_failed",
|
|
574
|
+
instruction: locked
|
|
575
|
+
? "该 Cart 已绑定 quote/Checkout,不能放弃;保留本地句柄并继续同一流程。"
|
|
576
|
+
: "放弃失败;不要清本地句柄或假设 Backend Cart 已改变。",
|
|
577
|
+
recovery: [{ command: "itpay cart next --json", reason: "恢复同一 canonical Cart" }],
|
|
578
|
+
});
|
|
273
579
|
}
|
|
274
580
|
finally {
|
|
275
581
|
session.saveToFile(sessionPath);
|
|
@@ -279,13 +585,13 @@ cart
|
|
|
279
585
|
program
|
|
280
586
|
.command("buy")
|
|
281
587
|
.description("Create a V3 cart and checkout, then render the checkout QR for the host")
|
|
282
|
-
.option("--host <host>", "client host (terminal, telegram, feishu, lark, ...)"
|
|
588
|
+
.option("--host <host>", "client host (terminal, telegram, feishu, lark, ...)")
|
|
283
589
|
.option("--target <target>", "chat id / channel id / open id for IM hosts")
|
|
284
590
|
.option("--item <catalog_item_id>")
|
|
285
591
|
.option("--variant <catalog_variant_id>")
|
|
286
592
|
.option("--offer <offer_id>")
|
|
287
593
|
.option("--cart <cart_id>", "existing canonical server cart id")
|
|
288
|
-
.option("--quantity <n>", "quantity",
|
|
594
|
+
.option("--quantity <n>", "quantity", "1")
|
|
289
595
|
.option("--ref <client_reference_id>")
|
|
290
596
|
.option("--contact-email <email>")
|
|
291
597
|
.option("--contact-phone <phone>")
|
|
@@ -295,48 +601,78 @@ program
|
|
|
295
601
|
.option("--pay", "also create a payment intent and optionally wait for verification")
|
|
296
602
|
.option("--method <alipay|wechatpay>", "payment method for --pay", "alipay")
|
|
297
603
|
.option("--no-wait", "do not wait for payment verification after --pay")
|
|
298
|
-
.option("--timeout <seconds>", "max seconds to wait for payment",
|
|
604
|
+
.option("--timeout <seconds>", "max seconds to wait for payment", "120")
|
|
299
605
|
.option("--json", "output JSON instead of terminal text")
|
|
300
606
|
.action(async (options) => {
|
|
301
607
|
const config = loadConfig();
|
|
302
608
|
const backend = newBackendClient(config);
|
|
303
609
|
const sessionPath = cartSessionPath();
|
|
304
610
|
const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
|
|
305
|
-
const
|
|
306
|
-
if (options.item && options.variant && options.offer) {
|
|
307
|
-
runCartAdd(session, {
|
|
308
|
-
catalogItemID: options.item,
|
|
309
|
-
catalogVariantID: options.variant,
|
|
310
|
-
offerID: options.offer,
|
|
311
|
-
quantity: options.quantity,
|
|
312
|
-
});
|
|
313
|
-
session.saveToFile(sessionPath);
|
|
314
|
-
}
|
|
315
|
-
const contact = {};
|
|
316
|
-
if (options.contactEmail)
|
|
317
|
-
contact.email = options.contactEmail;
|
|
318
|
-
if (options.contactPhone)
|
|
319
|
-
contact.phone = options.contactPhone;
|
|
320
|
-
const requiredContactFields = parseRequiredContactFields(options.requireContact);
|
|
321
|
-
const method = options.method === "wechatpay" ? "wechatpay" : "alipay";
|
|
322
|
-
const buyOptions = {
|
|
323
|
-
cartSession: session,
|
|
324
|
-
host,
|
|
325
|
-
...(options.cart ? { cartID: options.cart } : {}),
|
|
326
|
-
...(options.target ? { target: options.target } : {}),
|
|
327
|
-
...(options.ref ? { clientReferenceID: options.ref } : {}),
|
|
328
|
-
...(Object.keys(contact).length > 0 ? { contact } : {}),
|
|
329
|
-
...(requiredContactFields ? { requiredContactFields } : {}),
|
|
330
|
-
...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
|
|
331
|
-
...(options.qrFile ? { qrFilePath: options.qrFile } : {}),
|
|
332
|
-
...(options.pay ? { pay: true, payMethod: method, noWait: options.wait === false, payTimeoutSec: options.timeout } : {}),
|
|
333
|
-
...(options.json ? { jsonOutput: true } : {}),
|
|
334
|
-
};
|
|
611
|
+
const jsonOutput = Boolean(options.json);
|
|
335
612
|
try {
|
|
613
|
+
const inline = [options.item, options.variant, options.offer].filter(Boolean).length;
|
|
614
|
+
if (inline !== 0 && inline !== 3) {
|
|
615
|
+
throw new CommandContractError("buy_source_invalid", "--item, --variant and --offer must be provided together", "inline 购买必须同时使用 Catalog 返回的 item、variant 和 offer;不要猜测或部分提交。", [{ command: "itpay catalog list --json", reason: "读取已发布项目" }]);
|
|
616
|
+
}
|
|
617
|
+
if (options.cart && inline > 0) {
|
|
618
|
+
throw new CommandContractError("buy_source_invalid", "--cart cannot be combined with --item/--variant/--offer", "已有 canonical Cart 与 inline item 二选一;本次未修改任何资源。", [{ command: "itpay cart show --json", reason: "检查已有 Cart" }]);
|
|
619
|
+
}
|
|
620
|
+
if (options.method !== "alipay" && options.method !== "wechatpay") {
|
|
621
|
+
throw new CommandContractError("payment_method_invalid", `unsupported payment method: ${options.method}`, "--method 只接受 alipay 或 wechatpay。", [{ command: "itpay buy --help", reason: "查看付款参数" }]);
|
|
622
|
+
}
|
|
623
|
+
if (options.wait === false && !options.pay) {
|
|
624
|
+
throw new CommandContractError("buy_parameter_invalid", "--no-wait requires --pay", "普通 buy 不创建 Payment Intent;移除 --no-wait,或使用明确的付款运维流程。", [{ command: "itpay buy --help", reason: "查看参数关系" }]);
|
|
625
|
+
}
|
|
626
|
+
const quantity = positiveInteger(options.quantity, "--quantity");
|
|
627
|
+
const timeout = positiveInteger(options.timeout, "--timeout");
|
|
628
|
+
const host = withHost(options.host ?? defaultHostForAgentType(config.agentType));
|
|
629
|
+
const contact = {};
|
|
630
|
+
if (options.contactEmail)
|
|
631
|
+
contact.email = options.contactEmail;
|
|
632
|
+
if (options.contactPhone)
|
|
633
|
+
contact.phone = options.contactPhone;
|
|
634
|
+
const requiredContactFields = parseRequiredContactFields(options.requireContact);
|
|
635
|
+
const missingContactFields = (requiredContactFields ?? []).filter((field) => {
|
|
636
|
+
const value = contact[field];
|
|
637
|
+
return typeof value !== "string" || value.trim().length === 0;
|
|
638
|
+
});
|
|
639
|
+
if (missingContactFields.length > 0) {
|
|
640
|
+
throw new CommandContractError("missing_contact", `missing required contact fields: ${missingContactFields.join(", ")}`, `向用户询问 ${missingContactFields.join(" 和 ")},再补充对应 contact 参数重跑同一命令;禁止编造。`, [{ command: "itpay buy --help", reason: "查看 contact 参数" }]);
|
|
641
|
+
}
|
|
642
|
+
if (inline === 3) {
|
|
643
|
+
runCartAdd(session, {
|
|
644
|
+
catalogItemID: options.item,
|
|
645
|
+
catalogVariantID: options.variant,
|
|
646
|
+
offerID: options.offer,
|
|
647
|
+
quantity,
|
|
648
|
+
output: () => undefined,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
const buyOptions = {
|
|
652
|
+
cartSession: session,
|
|
653
|
+
host,
|
|
654
|
+
...(options.cart ? { cartID: options.cart } : {}),
|
|
655
|
+
...(options.target ? { target: options.target } : {}),
|
|
656
|
+
...(options.ref ? { clientReferenceID: options.ref } : {}),
|
|
657
|
+
...(Object.keys(contact).length > 0 ? { contact } : {}),
|
|
658
|
+
...(requiredContactFields ? { requiredContactFields } : {}),
|
|
659
|
+
...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
|
|
660
|
+
...(options.qrFile ? { qrFilePath: options.qrFile } : {}),
|
|
661
|
+
...(options.pay ? { pay: true, payMethod: options.method, noWait: options.wait === false, payTimeoutSec: timeout } : {}),
|
|
662
|
+
...(jsonOutput ? { jsonOutput: true } : {}),
|
|
663
|
+
};
|
|
336
664
|
await runBuy(backend, config, buyOptions);
|
|
337
665
|
}
|
|
338
666
|
catch (error) {
|
|
339
|
-
reportCLIError(error
|
|
667
|
+
reportCLIError(error, {
|
|
668
|
+
jsonOutput,
|
|
669
|
+
code: "buy_failed",
|
|
670
|
+
instruction: "Checkout 创建失败;保留当前 Cart/Checkout 句柄并按恢复命令继续,不要重复创建资源。",
|
|
671
|
+
recovery: [
|
|
672
|
+
{ command: "itpay next --json", reason: "恢复最近资源" },
|
|
673
|
+
{ command: "itpay cart next --json", reason: "检查 canonical Cart" },
|
|
674
|
+
],
|
|
675
|
+
});
|
|
340
676
|
}
|
|
341
677
|
finally {
|
|
342
678
|
session.saveToFile(sessionPath);
|
|
@@ -349,36 +685,44 @@ program
|
|
|
349
685
|
.option("--target <target>")
|
|
350
686
|
.option("--id <checkout_id>")
|
|
351
687
|
.option("--token <display_token>")
|
|
688
|
+
.option("--json", "output compact JSON")
|
|
352
689
|
.action(async (options) => {
|
|
353
690
|
const config = loadConfig();
|
|
354
691
|
const host = withHost(options.host ?? defaultHostForAgentType(config.agentType));
|
|
355
692
|
const session = CartSession.loadFromFile(cartSessionPath(), config.checkoutCurrency);
|
|
356
693
|
const snap = session.show();
|
|
357
|
-
const { checkoutID, displayToken } = resolveCheckoutPresentationArgs({
|
|
358
|
-
...(options.id ? { requestedCheckoutID: options.id } : {}),
|
|
359
|
-
...(options.token ? { requestedDisplayToken: options.token } : {}),
|
|
360
|
-
...(snap.lastCheckoutID ? { savedCheckoutID: snap.lastCheckoutID } : {}),
|
|
361
|
-
...(snap.lastDisplayToken ? { savedDisplayToken: snap.lastDisplayToken } : {}),
|
|
362
|
-
});
|
|
363
694
|
const backend = newBackendClient(config);
|
|
364
695
|
try {
|
|
696
|
+
const { checkoutID, displayToken } = resolveCheckoutPresentationArgs({
|
|
697
|
+
...(options.id ? { requestedCheckoutID: options.id } : {}),
|
|
698
|
+
...(options.token ? { requestedDisplayToken: options.token } : {}),
|
|
699
|
+
...(snap.lastCheckoutID ? { savedCheckoutID: snap.lastCheckoutID } : {}),
|
|
700
|
+
...(snap.lastDisplayToken ? { savedDisplayToken: snap.lastDisplayToken } : {}),
|
|
701
|
+
});
|
|
365
702
|
await runCheckoutPresentation(backend, {
|
|
366
703
|
checkoutID,
|
|
367
704
|
displayToken,
|
|
368
705
|
host,
|
|
369
706
|
baseURL: config.baseURL,
|
|
707
|
+
jsonOutput: Boolean(options.json),
|
|
370
708
|
});
|
|
371
709
|
}
|
|
372
710
|
catch (error) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
711
|
+
const canResumeSavedService = Boolean(snap.lastServiceExecutionID && (!options.id || options.id === snap.lastCheckoutID));
|
|
712
|
+
const recovery = canResumeSavedService
|
|
713
|
+
? [{
|
|
714
|
+
command: `itpay services checkout ${snap.lastServiceExecutionID} --resume --json`,
|
|
715
|
+
reason: "为同一个 Service Execution 轮换 Checkout handoff",
|
|
716
|
+
}]
|
|
717
|
+
: [{ command: "itpay services list --json", reason: "查找当前设备可恢复的 Service Execution" }];
|
|
718
|
+
reportCLIError(error, {
|
|
719
|
+
jsonOutput: Boolean(options.json),
|
|
720
|
+
code: "checkout_unavailable",
|
|
721
|
+
instruction: error instanceof HttpError && error.status === 404
|
|
722
|
+
? "当前 Checkout 句柄已失效或不匹配;恢复原 Service Execution,不要创建无关购物车。"
|
|
723
|
+
: "使用同一笔 Checkout 的完整 checkout_id 与 display token;不要拼接不同 Checkout 的句柄。",
|
|
724
|
+
recovery,
|
|
725
|
+
});
|
|
382
726
|
}
|
|
383
727
|
});
|
|
384
728
|
program
|
|
@@ -386,58 +730,216 @@ program
|
|
|
386
730
|
.description("Create a V3 payment intent (CLI escape hatch — usually done by the checkout page)")
|
|
387
731
|
.requiredOption("--checkout <checkout_id>")
|
|
388
732
|
.requiredOption("--method <alipay|wechatpay>")
|
|
389
|
-
.option("--
|
|
390
|
-
.option("--buyer <buyer_id>")
|
|
733
|
+
.option("--token <display_token>", "checkout display token; defaults only from the same saved checkout")
|
|
391
734
|
.option("--refresh", "request a fresh provider payment action for the existing intent")
|
|
735
|
+
.option("--host <host>", "client host")
|
|
736
|
+
.option("--target <target>")
|
|
737
|
+
.option("--json", "output compact JSON")
|
|
392
738
|
.action(async (options) => {
|
|
393
739
|
const config = loadConfig();
|
|
394
740
|
const backend = newBackendClient(config);
|
|
395
|
-
const
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
method
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
741
|
+
const session = CartSession.loadFromFile(cartSessionPath(), config.checkoutCurrency);
|
|
742
|
+
const jsonOutput = Boolean(options.json);
|
|
743
|
+
try {
|
|
744
|
+
if (options.method !== "alipay" && options.method !== "wechatpay") {
|
|
745
|
+
throw new CommandContractError("payment_method_invalid", `unsupported payment method: ${options.method}`, "--method 只接受 alipay 或 wechatpay;本次未创建 Payment Intent。", [{ command: "itpay pay --help", reason: "查看受支持参数" }]);
|
|
746
|
+
}
|
|
747
|
+
const displayToken = options.token ?? (session.lastCheckoutID === options.checkout ? session.lastDisplayToken : undefined);
|
|
748
|
+
if (!displayToken) {
|
|
749
|
+
throw new CommandContractError("checkout_token_required", "display token is required for this checkout", "提供同一 Checkout 的 display token;不要拼接其他 Checkout 的 token。", [{ command: "itpay next --json", reason: "恢复本机保存的同一 Checkout" }]);
|
|
750
|
+
}
|
|
751
|
+
const host = withHost(options.host ?? defaultHostForAgentType(config.agentType));
|
|
752
|
+
const contextError = validateContext(host, options.target);
|
|
753
|
+
if (contextError) {
|
|
754
|
+
throw new CommandContractError(contextError.code, contextError.message, "为当前 Host 提供有效 target;本次未创建 Payment Intent。", [
|
|
755
|
+
{ command: "itpay pay --help", reason: "查看 Host 参数" },
|
|
756
|
+
]);
|
|
757
|
+
}
|
|
758
|
+
await runPay(backend, {
|
|
759
|
+
checkoutID: options.checkout,
|
|
760
|
+
displayToken,
|
|
761
|
+
method: options.method,
|
|
762
|
+
host,
|
|
763
|
+
...(options.refresh ? { refreshAction: true } : {}),
|
|
764
|
+
...(jsonOutput ? { jsonOutput: true } : {}),
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
reportCLIError(error, {
|
|
769
|
+
jsonOutput,
|
|
770
|
+
code: "payment_intent_failed",
|
|
771
|
+
instruction: "不要创建替代 Checkout;恢复同一 Checkout 并由用户在 ItPay 页面继续付款。",
|
|
772
|
+
recovery: [{ command: "itpay next --json", reason: "恢复当前 Checkout" }],
|
|
773
|
+
});
|
|
774
|
+
}
|
|
404
775
|
});
|
|
405
776
|
program
|
|
406
777
|
.command("order")
|
|
407
778
|
.description("Read a V3 order by id")
|
|
408
779
|
.argument("<order_id>")
|
|
409
780
|
.option("--host <host>", "client host")
|
|
781
|
+
.option("--json", "output JSON instead of terminal text")
|
|
410
782
|
.action(async (orderID, options) => {
|
|
411
|
-
|
|
412
|
-
|
|
783
|
+
if (options.host)
|
|
784
|
+
withHost(options.host);
|
|
785
|
+
const config = loadConfig();
|
|
786
|
+
const backend = newBackendClient(config);
|
|
787
|
+
try {
|
|
788
|
+
await runOrder(backend, orderID, { ...(options.host ? { host: options.host } : {}), jsonOutput: Boolean(options.json) });
|
|
789
|
+
}
|
|
790
|
+
catch (error) {
|
|
791
|
+
reportCLIError(error, {
|
|
792
|
+
jsonOutput: Boolean(options.json),
|
|
793
|
+
code: "order_read_failed",
|
|
794
|
+
instruction: "确认订单属于当前账号或已绑定 Agent;不要通过错误差异探测其他账号的订单。",
|
|
795
|
+
recovery: [{ command: "itpay services list --json", reason: "恢复当前身份可见的 Service Execution" }],
|
|
796
|
+
});
|
|
797
|
+
}
|
|
413
798
|
});
|
|
414
799
|
program
|
|
415
800
|
.command("orders")
|
|
416
801
|
.description("List V3 orders for the account-scoped bearer session")
|
|
417
802
|
.option("--limit <n>", "max orders", (value) => Number.parseInt(value, 10), 20)
|
|
418
803
|
.option("--status <status>")
|
|
804
|
+
.option("--json", "output JSON instead of terminal text")
|
|
419
805
|
.action(async (options) => {
|
|
420
806
|
const config = loadConfig();
|
|
421
807
|
const backend = newBackendClient(config);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
808
|
+
try {
|
|
809
|
+
await runListOrders(backend, config, {
|
|
810
|
+
limit: options.limit,
|
|
811
|
+
status: options.status,
|
|
812
|
+
jsonOutput: Boolean(options.json),
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
catch (error) {
|
|
816
|
+
reportCLIError(error, {
|
|
817
|
+
jsonOutput: Boolean(options.json),
|
|
818
|
+
code: "orders_list_failed",
|
|
819
|
+
instruction: "订单历史只对 account-scoped Buyer session 开放;不要通过错误差异探测其他账号。",
|
|
820
|
+
recovery: [{ command: "itpay services list --json", reason: "恢复当前 Agent 设备可见的执行" }],
|
|
821
|
+
});
|
|
822
|
+
}
|
|
426
823
|
});
|
|
427
|
-
program
|
|
824
|
+
const refund = program
|
|
428
825
|
.command("refund")
|
|
826
|
+
.enablePositionalOptions()
|
|
429
827
|
.description("Create a V3 refund request for an order")
|
|
430
|
-
.
|
|
828
|
+
.option("--order <order_id>")
|
|
431
829
|
.option("--reason <reason>")
|
|
830
|
+
.option("--json", "output JSON instead of terminal text")
|
|
432
831
|
.action(async (options) => {
|
|
832
|
+
if (!options.order) {
|
|
833
|
+
process.stdout.write(refund.helpInformation());
|
|
834
|
+
process.stdout.write("\ninstruction: 使用 `itpay refund create --order <order_id>` 提交退款;本次未发送请求。\n");
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
await executeRefundCreate(options.order, options.reason, Boolean(options.json));
|
|
838
|
+
});
|
|
839
|
+
refund.command("create").option("--order <order_id>").option("--reason <reason>").option("--json", "output JSON instead of terminal text").action(async (options) => {
|
|
840
|
+
const inherited = refund.opts();
|
|
841
|
+
const orderID = options.order ?? inherited.order;
|
|
842
|
+
if (!orderID) {
|
|
843
|
+
reportCLIError(new Error("--order is required"), {
|
|
844
|
+
jsonOutput: Boolean(options.json ?? inherited.json),
|
|
845
|
+
code: "order_required",
|
|
846
|
+
instruction: "使用用户订单的 order_id;不要猜测或代填。",
|
|
847
|
+
recovery: [{ command: "itpay services list --json", reason: "恢复当前身份可见的 Service Execution" }],
|
|
848
|
+
});
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const reason = options.reason ?? inherited.reason;
|
|
852
|
+
await executeRefundCreate(orderID, reason, Boolean(options.json ?? inherited.json));
|
|
853
|
+
});
|
|
854
|
+
refund.command("list").option("--order <order_id>").option("--json", "output JSON instead of terminal text").action(async (options) => {
|
|
855
|
+
const inherited = refund.opts();
|
|
856
|
+
const orderID = options.order ?? inherited.order;
|
|
857
|
+
const jsonOutput = Boolean(options.json ?? inherited.json);
|
|
858
|
+
if (!orderID) {
|
|
859
|
+
reportCLIError(new Error("--order is required"), {
|
|
860
|
+
jsonOutput,
|
|
861
|
+
code: "order_required",
|
|
862
|
+
instruction: "使用用户订单的 order_id;不要猜测或代填。",
|
|
863
|
+
recovery: [{ command: "itpay services list --json", reason: "恢复当前身份可见的 Service Execution" }],
|
|
864
|
+
});
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
433
867
|
const config = loadConfig();
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
868
|
+
try {
|
|
869
|
+
await runListRefunds(newBackendClient(config), { orderID, jsonOutput });
|
|
870
|
+
}
|
|
871
|
+
catch (error) {
|
|
872
|
+
reportCLIError(error, {
|
|
873
|
+
jsonOutput,
|
|
874
|
+
code: "refund_list_failed",
|
|
875
|
+
instruction: "确认订单属于当前账号或已绑定 Agent;不要探测其他账号的退款。",
|
|
876
|
+
recovery: [{ command: "itpay services list --json", reason: "恢复当前身份可见的 Service Execution" }],
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
refund.command("get").argument("<refund_request_id>").option("--json", "output JSON instead of terminal text").action(async (id, options) => {
|
|
881
|
+
const config = loadConfig();
|
|
882
|
+
try {
|
|
883
|
+
await runGetRefund(newBackendClient(config), id, { jsonOutput: Boolean(options.json ?? refund.opts().json) });
|
|
884
|
+
}
|
|
885
|
+
catch (error) {
|
|
886
|
+
reportCLIError(error, {
|
|
887
|
+
jsonOutput: Boolean(options.json ?? refund.opts().json),
|
|
888
|
+
code: "refund_read_failed",
|
|
889
|
+
instruction: "确认退款属于当前账号或已绑定 Agent;不要探测其他账号的退款。",
|
|
890
|
+
recovery: [{ command: "itpay services list --json", reason: "恢复当前身份可见的 Service Execution" }],
|
|
891
|
+
});
|
|
892
|
+
}
|
|
440
893
|
});
|
|
894
|
+
refund.command("watch").argument("<refund_request_id>").option("--interval <seconds>", "poll interval", Number, 2).option("--timeout <seconds>", "timeout", Number, 120).option("--json", "output JSON instead of terminal text").action(async (id, options) => {
|
|
895
|
+
const config = loadConfig();
|
|
896
|
+
try {
|
|
897
|
+
await runWatchRefund(newBackendClient(config), id, {
|
|
898
|
+
intervalSeconds: options.interval,
|
|
899
|
+
timeoutSeconds: options.timeout,
|
|
900
|
+
jsonOutput: Boolean(options.json ?? refund.opts().json),
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
catch (error) {
|
|
904
|
+
reportCLIError(error, {
|
|
905
|
+
jsonOutput: Boolean(options.json ?? refund.opts().json),
|
|
906
|
+
code: "refund_watch_failed",
|
|
907
|
+
instruction: "检查退款 ID 和轮询参数后恢复同一退款;不要重复申请。",
|
|
908
|
+
recovery: [{ command: `itpay refund get ${id} --json`, reason: "读取当前权威状态" }],
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
refund.command("cancel").argument("<refund_request_id>").option("--reason <reason>").option("--json", "output JSON instead of terminal text").action(async (id, options) => {
|
|
913
|
+
const config = loadConfig();
|
|
914
|
+
try {
|
|
915
|
+
await runCancelRefund(newBackendClient(config), id, options.reason, { jsonOutput: Boolean(options.json ?? refund.opts().json) });
|
|
916
|
+
}
|
|
917
|
+
catch (error) {
|
|
918
|
+
reportCLIError(error, {
|
|
919
|
+
jsonOutput: Boolean(options.json ?? refund.opts().json),
|
|
920
|
+
code: "refund_cancel_failed",
|
|
921
|
+
instruction: "取消未生效;以 Refund Owner 当前状态为准,不要重复退款或自行解除交付锁。",
|
|
922
|
+
recovery: [{ command: `itpay refund get ${id} --json`, reason: "读取当前权威状态" }],
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
async function executeRefundCreate(orderID, reason, jsonOutput) {
|
|
927
|
+
const config = loadConfig();
|
|
928
|
+
try {
|
|
929
|
+
await runRefund(newBackendClient(config), config, { orderID, ...(reason ? { reason } : {}), jsonOutput });
|
|
930
|
+
}
|
|
931
|
+
catch (error) {
|
|
932
|
+
reportCLIError(error, {
|
|
933
|
+
jsonOutput,
|
|
934
|
+
code: "refund_create_failed",
|
|
935
|
+
instruction: "确认订单属于当前账号且可退款;不要修改金额、支付或消费事实。",
|
|
936
|
+
recovery: [
|
|
937
|
+
{ command: `itpay order ${orderID} --json`, reason: "检查订单和交付锁" },
|
|
938
|
+
{ command: `itpay refund list --order ${orderID} --json`, reason: "检查已有退款" },
|
|
939
|
+
],
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
}
|
|
441
943
|
// --- service execution ----------------------------------------------------
|
|
442
944
|
const services = program.command("services").description("Generic V3 Service Execution commands");
|
|
443
945
|
services
|
|
@@ -446,15 +948,28 @@ services
|
|
|
446
948
|
.argument("<service_id>")
|
|
447
949
|
.option("--host <host>", "client host")
|
|
448
950
|
.option("--target <target>")
|
|
449
|
-
.option("--
|
|
951
|
+
.option("--json", "output JSON instead of terminal text")
|
|
450
952
|
.action(async (serviceID, options) => {
|
|
451
953
|
const config = loadConfig();
|
|
452
954
|
const backend = newBackendClient(config);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
955
|
+
try {
|
|
956
|
+
await runServicesStart(backend, serviceID, {
|
|
957
|
+
host: withHost(options.host ?? defaultHostForAgentType(config.agentType)),
|
|
958
|
+
...(options.target ? { target: options.target } : {}),
|
|
959
|
+
jsonOutput: Boolean(options.json),
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
reportCLIError(error, {
|
|
964
|
+
jsonOutput: Boolean(options.json),
|
|
965
|
+
code: "service_start_failed",
|
|
966
|
+
instruction: "只使用已发布 Catalog 返回的 service_id;设备身份问题应由 CLI 自动恢复。",
|
|
967
|
+
recovery: [
|
|
968
|
+
{ command: "itpay catalog list", reason: "重新取得有效 service_id" },
|
|
969
|
+
{ command: "itpay readyz", reason: "确认 Backend 可用" },
|
|
970
|
+
],
|
|
971
|
+
});
|
|
972
|
+
}
|
|
458
973
|
});
|
|
459
974
|
services
|
|
460
975
|
.command("invoke")
|
|
@@ -466,7 +981,20 @@ services
|
|
|
466
981
|
.action(async (serviceExecutionID, options) => {
|
|
467
982
|
const config = loadConfig();
|
|
468
983
|
const backend = newBackendClient(config);
|
|
469
|
-
|
|
984
|
+
try {
|
|
985
|
+
await runServicesInvoke(backend, config, serviceExecutionID, options.capability, parseKeyValueList(options.input), { jsonOutput: Boolean(options.json) });
|
|
986
|
+
}
|
|
987
|
+
catch (error) {
|
|
988
|
+
reportCLIError(error, {
|
|
989
|
+
jsonOutput: Boolean(options.json),
|
|
990
|
+
code: "service_invoke_failed",
|
|
991
|
+
instruction: "读取当前 Service Execution 的合法下一步后重试;不要复用已结束的 execution。",
|
|
992
|
+
recovery: [
|
|
993
|
+
{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前合法动作" },
|
|
994
|
+
{ command: `itpay services get ${serviceExecutionID} --json`, reason: "检查执行状态" },
|
|
995
|
+
],
|
|
996
|
+
});
|
|
997
|
+
}
|
|
470
998
|
});
|
|
471
999
|
services
|
|
472
1000
|
.command("action")
|
|
@@ -478,27 +1006,63 @@ services
|
|
|
478
1006
|
.option("--status <status>", "pending, approved, rejected, expired, or cancelled")
|
|
479
1007
|
.option("--candidate <rank>", "select a displayed candidate by its rank", Number)
|
|
480
1008
|
.option("--result-item <service_capability_result_item_id>")
|
|
481
|
-
.option("--selected-candidate-hash <hash>")
|
|
482
1009
|
.option("--required-before <step>")
|
|
483
1010
|
.option("--input <key=value>", "action input snapshot", collectOption, [])
|
|
1011
|
+
.option("--json", "output JSON instead of terminal text")
|
|
484
1012
|
.action(async (serviceExecutionID, options) => {
|
|
485
1013
|
const config = loadConfig();
|
|
486
1014
|
const backend = newBackendClient(config);
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
1015
|
+
try {
|
|
1016
|
+
await runServicesAction(backend, serviceExecutionID, options.action, parseKeyValueList(options.input), {
|
|
1017
|
+
...(options.actorType ? { actorType: options.actorType } : {}),
|
|
1018
|
+
...(options.actorId ? { actorID: options.actorId } : {}),
|
|
1019
|
+
...(options.status ? { status: options.status } : {}),
|
|
1020
|
+
...(options.candidate !== undefined ? { candidateRank: options.candidate } : {}),
|
|
1021
|
+
...(options.resultItem ? { resultItemID: options.resultItem } : {}),
|
|
1022
|
+
...(options.requiredBefore ? { requiredBefore: options.requiredBefore } : {}),
|
|
1023
|
+
jsonOutput: Boolean(options.json),
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
catch (error) {
|
|
1027
|
+
reportCLIError(error, {
|
|
1028
|
+
jsonOutput: Boolean(options.json),
|
|
1029
|
+
code: "service_action_failed",
|
|
1030
|
+
instruction: "读取当前 Service Execution 的合法 action 后重试;不要猜测状态或候选 ID。",
|
|
1031
|
+
recovery: [
|
|
1032
|
+
{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前可选动作" },
|
|
1033
|
+
{ command: `itpay services get ${serviceExecutionID} --json`, reason: "检查执行状态" },
|
|
1034
|
+
],
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
services
|
|
1039
|
+
.command("quote")
|
|
1040
|
+
.description("Prepare a paid service quote without creating a Cart or Checkout")
|
|
1041
|
+
.argument("<service_execution_id>")
|
|
1042
|
+
.requiredOption("--capability <capability_id>")
|
|
1043
|
+
.option("--input <key=value>", "input to lock into the paid service quote", collectOption, [])
|
|
1044
|
+
.option("--email <delivery_email>")
|
|
1045
|
+
.option("--json", "output compact JSON")
|
|
1046
|
+
.action(async (serviceExecutionID, options) => {
|
|
1047
|
+
const config = loadConfig();
|
|
1048
|
+
try {
|
|
1049
|
+
await runServicesQuote(newBackendClient(config), serviceExecutionID, options.capability, parseKeyValueList(options.input), { ...(options.email ? { email: options.email } : {}), jsonOutput: Boolean(options.json) });
|
|
1050
|
+
}
|
|
1051
|
+
catch (error) {
|
|
1052
|
+
reportCLIError(error, {
|
|
1053
|
+
jsonOutput: Boolean(options.json),
|
|
1054
|
+
code: "service_quote_failed",
|
|
1055
|
+
instruction: "按当前 Execution 的合法付费 capability 和可信输入重试;本次不要自行创建 Cart 或 Checkout。",
|
|
1056
|
+
recovery: [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前合法动作" }],
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
496
1059
|
});
|
|
497
1060
|
services
|
|
498
1061
|
.command("checkout")
|
|
499
1062
|
.description("Create checkout from a service execution and render the ItPay checkout handoff")
|
|
500
1063
|
.argument("<service_execution_id>")
|
|
501
1064
|
.option("--capability <capability_id>")
|
|
1065
|
+
.option("--input <key=value>", "input to lock into the paid service quote", collectOption, [])
|
|
502
1066
|
.option("--email <delivery_email>")
|
|
503
1067
|
.option("--resume", "reissue the existing checkout handoff without creating another checkout")
|
|
504
1068
|
.option("--host <host>", "client host (terminal, codex, telegram, feishu, lark, ...)")
|
|
@@ -511,35 +1075,61 @@ services
|
|
|
511
1075
|
const backend = newBackendClient(config);
|
|
512
1076
|
const sessionPath = cartSessionPath();
|
|
513
1077
|
const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
1078
|
+
try {
|
|
1079
|
+
await runServicesCheckout(backend, config, serviceExecutionID, options.capability, {
|
|
1080
|
+
...(options.email ? { email: options.email } : {}),
|
|
1081
|
+
lockedInput: parseKeyValueList(options.input),
|
|
1082
|
+
resume: Boolean(options.resume),
|
|
1083
|
+
host: withHost(options.host ?? defaultHostForAgentType(config.agentType)),
|
|
1084
|
+
...(options.target ? { target: options.target } : {}),
|
|
1085
|
+
...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
|
|
1086
|
+
...(options.qrFile ? { qrFilePath: options.qrFile } : {}),
|
|
1087
|
+
jsonOutput: Boolean(options.json),
|
|
1088
|
+
persistHandoff: (handoff) => {
|
|
1089
|
+
session.rememberCheckout({
|
|
1090
|
+
checkoutID: handoff.checkoutID,
|
|
1091
|
+
displayToken: handoff.displayToken,
|
|
1092
|
+
checkoutURL: handoff.checkoutURL,
|
|
1093
|
+
serviceExecutionID: handoff.serviceExecutionID,
|
|
1094
|
+
});
|
|
1095
|
+
session.saveToFile(sessionPath);
|
|
1096
|
+
},
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
catch (error) {
|
|
1100
|
+
reportCLIError(error, {
|
|
1101
|
+
jsonOutput: Boolean(options.json),
|
|
1102
|
+
code: "service_checkout_failed",
|
|
1103
|
+
instruction: "读取当前 Service Execution 后按服务端允许的 capability、输入和交付要求重试;不要创建替代 Checkout。",
|
|
1104
|
+
recovery: [
|
|
1105
|
+
{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前合法下一步" },
|
|
1106
|
+
{ command: `itpay services get ${serviceExecutionID} --json`, reason: "检查 Checkout 与执行状态" },
|
|
1107
|
+
],
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
533
1110
|
});
|
|
534
1111
|
services
|
|
535
1112
|
.command("list")
|
|
536
1113
|
.description("Recover service executions visible to this enrolled device or account")
|
|
537
|
-
.option("--limit <number>", "maximum executions", "
|
|
1114
|
+
.option("--limit <number>", "maximum executions", "10")
|
|
538
1115
|
.option("--json", "output compact JSON")
|
|
539
1116
|
.action(async (options) => {
|
|
540
1117
|
const config = loadConfig();
|
|
541
1118
|
const backend = newBackendClient(config);
|
|
542
|
-
|
|
1119
|
+
try {
|
|
1120
|
+
await runServicesList(backend, { limit: Number.parseInt(options.limit, 10), jsonOutput: Boolean(options.json) });
|
|
1121
|
+
}
|
|
1122
|
+
catch (error) {
|
|
1123
|
+
reportCLIError(error, {
|
|
1124
|
+
jsonOutput: Boolean(options.json),
|
|
1125
|
+
code: "services_list_failed",
|
|
1126
|
+
instruction: "确认当前设备身份和 Backend 后重试;不要猜测 Service Execution ID。",
|
|
1127
|
+
recovery: [
|
|
1128
|
+
{ command: "itpay readyz --json", reason: "确认 Backend 可用" },
|
|
1129
|
+
{ command: "itpay services list --limit 10 --json", reason: "重新读取最近执行" },
|
|
1130
|
+
],
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
543
1133
|
});
|
|
544
1134
|
services
|
|
545
1135
|
.command("get")
|
|
@@ -549,7 +1139,17 @@ services
|
|
|
549
1139
|
.action(async (serviceExecutionID, options) => {
|
|
550
1140
|
const config = loadConfig();
|
|
551
1141
|
const backend = newBackendClient(config);
|
|
552
|
-
|
|
1142
|
+
try {
|
|
1143
|
+
await runServicesGet(backend, serviceExecutionID, { jsonOutput: Boolean(options.json) });
|
|
1144
|
+
}
|
|
1145
|
+
catch (error) {
|
|
1146
|
+
reportCLIError(error, {
|
|
1147
|
+
jsonOutput: Boolean(options.json),
|
|
1148
|
+
code: "service_get_failed",
|
|
1149
|
+
instruction: "确认 execution 属于当前设备或账号;不要通过错误差异探测其他账号。",
|
|
1150
|
+
recovery: [{ command: "itpay services list --json", reason: "读取当前身份可见的执行" }],
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
553
1153
|
});
|
|
554
1154
|
services
|
|
555
1155
|
.command("next")
|
|
@@ -559,40 +1159,67 @@ services
|
|
|
559
1159
|
.action(async (serviceExecutionID, options) => {
|
|
560
1160
|
const config = loadConfig();
|
|
561
1161
|
const backend = newBackendClient(config);
|
|
562
|
-
|
|
1162
|
+
try {
|
|
1163
|
+
await runServicesNext(backend, serviceExecutionID, { jsonOutput: Boolean(options.json) });
|
|
1164
|
+
}
|
|
1165
|
+
catch (error) {
|
|
1166
|
+
reportCLIError(error, {
|
|
1167
|
+
jsonOutput: Boolean(options.json),
|
|
1168
|
+
code: "service_next_failed",
|
|
1169
|
+
instruction: "检查 Service Execution 是否属于当前设备或账号,然后读取完整时间线。",
|
|
1170
|
+
recovery: [{ command: `itpay services get ${serviceExecutionID} --json`, reason: "检查执行状态与归属" }],
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
563
1173
|
});
|
|
564
1174
|
services
|
|
565
1175
|
.command("read-result")
|
|
566
1176
|
.description("Read a human-granted service result for this agent")
|
|
567
1177
|
.argument("<service_execution_id>")
|
|
568
|
-
.
|
|
1178
|
+
.option("--json", "output JSON instead of terminal text")
|
|
1179
|
+
.action(async (serviceExecutionID, options) => {
|
|
569
1180
|
const config = loadConfig();
|
|
570
1181
|
const backend = newBackendClient(config);
|
|
571
|
-
|
|
1182
|
+
try {
|
|
1183
|
+
await runServicesReadResult(backend, serviceExecutionID, { jsonOutput: Boolean(options.json) });
|
|
1184
|
+
}
|
|
1185
|
+
catch (error) {
|
|
1186
|
+
reportCLIError(error, {
|
|
1187
|
+
jsonOutput: Boolean(options.json),
|
|
1188
|
+
code: "agent_access_denied",
|
|
1189
|
+
instruction: "请用户在订单页面重新授权;不要使用开发者权限绕过授权或退款锁。",
|
|
1190
|
+
recovery: [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "检查交付模式和 grant 状态" }],
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
572
1193
|
});
|
|
573
1194
|
services
|
|
574
1195
|
.command("events")
|
|
575
1196
|
.description("List redacted service execution events")
|
|
576
1197
|
.argument("<service_execution_id>")
|
|
577
|
-
.
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
});
|
|
582
|
-
async function withBackend(action) {
|
|
1198
|
+
.option("--after-sequence <number>", "return events after this sequence", "0")
|
|
1199
|
+
.option("--limit <number>", "maximum events (1-100)", "50")
|
|
1200
|
+
.option("--json", "output compact JSON")
|
|
1201
|
+
.action(async (serviceExecutionID, options) => {
|
|
583
1202
|
const config = loadConfig();
|
|
584
1203
|
const backend = newBackendClient(config);
|
|
585
1204
|
try {
|
|
586
|
-
await
|
|
1205
|
+
await runServicesEvents(backend, serviceExecutionID, {
|
|
1206
|
+
afterSequence: Number(options.afterSequence),
|
|
1207
|
+
limit: Number(options.limit),
|
|
1208
|
+
jsonOutput: Boolean(options.json),
|
|
1209
|
+
});
|
|
587
1210
|
}
|
|
588
1211
|
catch (error) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
1212
|
+
reportCLIError(error, {
|
|
1213
|
+
jsonOutput: Boolean(options.json),
|
|
1214
|
+
code: "service_events_failed",
|
|
1215
|
+
instruction: "确认 execution 属于当前身份;事件只用于诊断,不要据此重放业务步骤。",
|
|
1216
|
+
recovery: [
|
|
1217
|
+
{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前业务动作" },
|
|
1218
|
+
{ command: "itpay services list --json", reason: "列出当前身份可见执行" },
|
|
1219
|
+
],
|
|
1220
|
+
});
|
|
594
1221
|
}
|
|
595
|
-
}
|
|
1222
|
+
});
|
|
596
1223
|
program.parseAsync(process.argv).catch((error) => {
|
|
597
1224
|
reportCLIError(error);
|
|
598
1225
|
});
|