@itpay/cli 2.1.22 → 2.1.33
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 +2 -2
- package/dist/src/client/backend.js +4 -1
- package/dist/src/client/http.js +2 -0
- package/dist/src/commands/buy.js +6 -4
- package/dist/src/commands/cart.js +15 -15
- package/dist/src/commands/catalog.js +10 -8
- package/dist/src/commands/checkout.js +9 -11
- package/dist/src/commands/checkout_handoff.js +7 -4
- package/dist/src/commands/docs.js +3 -14
- package/dist/src/commands/guidance.js +43 -25
- package/dist/src/commands/install.js +2 -2
- package/dist/src/commands/order.js +4 -3
- package/dist/src/commands/orders.js +4 -9
- package/dist/src/commands/pay.js +1 -1
- package/dist/src/commands/refund.js +5 -5
- package/dist/src/commands/services.js +628 -390
- package/dist/src/commands/vault.js +27 -20
- package/dist/src/main.js +35 -24
- package/dist/src/render/qr.js +1 -1
- package/dist/src/state/account_auth.js +9 -6
- package/dist/src/state/config.js +4 -4
- package/dist/src/state/device_authority.js +2 -2
- package/docs/agent/buyer/cart-checkout.json +1 -1
- package/docs/agent/buyer/catalog-list.json +1 -1
- package/docs/agent/buyer/identity-and-sessions.json +3 -7
- package/docs/agent/buyer/orders-refunds.json +8 -1
- package/docs/agent/buyer/payment-flow.json +1 -0
- package/docs/agent/buyer/purchased-content.json +1 -1
- package/docs/agent/buyer/quickstart.json +6 -5
- package/docs/agent/buyer/rail-booking.json +96 -51
- package/docs/agent/buyer/rail-fast-checkout.json +6 -3
- package/docs/agent/buyer/render-hosts.json +1 -1
- package/docs/cli-reference/commands/device.md +4 -5
- package/docs/cli-reference/commands/services/invoke.md +2 -2
- package/docs/cli-reference/commands/services/read-result.md +12 -5
- package/docs/cli-reference/commands/services/run.md +2 -0
- package/package.json +5 -1
- package/skills/itpay/SKILL.md +25 -16
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HttpError } from "../client/http.js";
|
|
2
|
-
import { requiresTarget } from "../state/client_context.js";
|
|
2
|
+
import { requiresTarget, validateContext } from "../state/client_context.js";
|
|
3
3
|
import { formatMoney } from "../render/output.js";
|
|
4
|
-
import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
|
|
4
|
+
import { CommandContractError, shellArgument, writeCommandEnvelope } from "./guidance.js";
|
|
5
5
|
import { buildVaultHandoff } from "./vault_handoff.js";
|
|
6
6
|
function outputOptions(options, plainResult) {
|
|
7
7
|
return {
|
|
@@ -19,21 +19,21 @@ export async function runVaultList(backend, input) {
|
|
|
19
19
|
const value = await backend.listBuyerVaultArtifacts(input);
|
|
20
20
|
writeCommandEnvelope({
|
|
21
21
|
status: value.items.length ? "vault_listed" : "no_vault_artifacts",
|
|
22
|
-
result: { items: value.items, next_cursor: value.next_cursor || null },
|
|
22
|
+
result: { items: value.items.map(item => ({ ...item, amount: formatMoney(item.amount_minor, item.currency), reader: { command: vaultReadCommand(item.artifact_ref, [], input), reason: "读取这一已购内容" } })), next_cursor: value.next_cursor || null },
|
|
23
23
|
instruction: value.items.length
|
|
24
|
-
? "
|
|
24
|
+
? "用编号、服务名称、内容主体、购买时间、金额和订单号说明匹配结果,不要向用户显示内部内容标识。一个精确匹配可按用户原始查看意图继续读取;已有任务可明确匹配时读取对应行;只有真实歧义才让用户选择。"
|
|
25
25
|
: "当前账号没有匹配的已购内容。向用户说明没有找到,不要猜测内容标识、自动购买或发起新的服务查询。",
|
|
26
|
-
next: null,
|
|
26
|
+
next: value.next_cursor ? { command: `itpay vault list --limit ${input.limit}${input.query ? ` --query ${shellArgument(input.query)}` : ""} --cursor ${shellArgument(value.next_cursor)}${contextFlags(input)} --json`, reason: "读取同一筛选的下一页" } : null,
|
|
27
27
|
recovery: [],
|
|
28
|
-
}, outputOptions(input
|
|
28
|
+
}, outputOptions(input));
|
|
29
29
|
}
|
|
30
30
|
catch (error) {
|
|
31
31
|
if (error instanceof HttpError && error.code === "vault_authorization_required") {
|
|
32
32
|
writeCommandEnvelope({
|
|
33
33
|
status: "human_authorization_required",
|
|
34
34
|
result: { intent: "list_purchased_content", query: input.query ?? "" },
|
|
35
|
-
instruction:
|
|
36
|
-
|
|
35
|
+
instruction: `需要用户确认一次身份和只读权限。按当前动作或待填模板生成官方入口,不要声称链接已经创建;用户完成后重新运行原始 vault list 命令。${accessContextInstruction(input)}`,
|
|
36
|
+
...vaultAccessGuidance(undefined, input), recovery: [],
|
|
37
37
|
}, outputOptions(input));
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
@@ -41,6 +41,9 @@ export async function runVaultList(backend, input) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
export async function runVaultAccess(backend, artifactRef, options) {
|
|
44
|
+
const issue = validateContext(options.host, options.target);
|
|
45
|
+
if (issue)
|
|
46
|
+
throw new CommandContractError(issue.code, issue.message, "先补齐当前可信会话的真实宿主和目标;尚未创建授权请求。", [], { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command: vaultAccessCommand(artifactRef, options), required_input: issue.code === "target_required" ? ["target"] : ["host"], executable: false } });
|
|
44
47
|
const value = await backend.createVaultAccessRequest(artifactRef
|
|
45
48
|
? { purpose: "artifact_reveal", artifact_ref: artifactRef }
|
|
46
49
|
: { purpose: "account_window" });
|
|
@@ -90,23 +93,23 @@ export async function runVaultRead(backend, artifactRef, sections, options) {
|
|
|
90
93
|
: value.status === "result_preparing"
|
|
91
94
|
? "这份已购内容仍在准备。稍后只重试同一 read,不要重新授权、购买或调用 Provider。"
|
|
92
95
|
: "这份已购内容当前不可用。停止,不要重试、重新购买或绕过退款锁。",
|
|
93
|
-
next: null, recovery: [],
|
|
96
|
+
next: value.status === "result_preparing" ? { command: vaultReadCommand(artifactRef, normalized, options), reason: "稍后读取同一内容及原sections;不重新授权或购买" } : null, recovery: [],
|
|
94
97
|
}, outputOptions(options));
|
|
95
98
|
}
|
|
96
99
|
catch (error) {
|
|
97
100
|
if (error instanceof HttpError && error.code === "artifact_authorization_required") {
|
|
98
101
|
writeCommandEnvelope({
|
|
99
102
|
status: "human_authorization_required", result: { artifact_ref: artifactRef },
|
|
100
|
-
instruction:
|
|
101
|
-
|
|
103
|
+
instruction: `这份内容需要用户单独确认读取权限。按当前动作或待填模板生成一次官方入口;用户完成后重新运行原始 read,不要重复创建授权请求。${accessContextInstruction(options)}`,
|
|
104
|
+
...vaultAccessGuidance(artifactRef, options), recovery: [],
|
|
102
105
|
}, outputOptions(options));
|
|
103
106
|
return;
|
|
104
107
|
}
|
|
105
108
|
if (error instanceof HttpError && error.code === "vault_authorization_required") {
|
|
106
109
|
writeCommandEnvelope({
|
|
107
110
|
status: "human_authorization_required", result: { artifact_ref: artifactRef },
|
|
108
|
-
instruction:
|
|
109
|
-
|
|
111
|
+
instruction: `账号读取授权已缺失或过期。按当前动作或待填模板生成一次官方入口;用户完成后重新运行原始 read。${accessContextInstruction(options)}`,
|
|
112
|
+
...vaultAccessGuidance(undefined, options), recovery: [],
|
|
110
113
|
}, outputOptions(options));
|
|
111
114
|
return;
|
|
112
115
|
}
|
|
@@ -121,7 +124,7 @@ export function vaultAccessCommand(artifactRef, options) {
|
|
|
121
124
|
const host = options.host ?? (openClaw ? "<host>" : undefined);
|
|
122
125
|
if (host)
|
|
123
126
|
parts.push("--host", host);
|
|
124
|
-
const target = options.target ?? (openClaw &&
|
|
127
|
+
const target = options.target ?? (((openClaw && !options.host) || (options.host && requiresTarget(options.host))) ? "<target>" : undefined);
|
|
125
128
|
if (target)
|
|
126
129
|
parts.push("--target", shellArgument(target));
|
|
127
130
|
parts.push("--json");
|
|
@@ -132,10 +135,14 @@ export function accessContextInstruction(options) {
|
|
|
132
135
|
? " 将 <host> 和 <target> 替换为当前可信 OpenClaw 会话的真实值,不要照抄占位符或猜测目标。"
|
|
133
136
|
: "";
|
|
134
137
|
}
|
|
135
|
-
function
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
function contextFlags(options) {
|
|
139
|
+
return `${options.host ? ` --host ${options.host}` : ""}${options.target ? ` --target ${shellArgument(options.target)}` : ""}`;
|
|
140
|
+
}
|
|
141
|
+
function vaultReadCommand(artifact, sections, options) {
|
|
142
|
+
return `itpay vault read --artifact ${shellArgument(artifact)}${sections.map(section => ` --section ${shellArgument(section)}`).join("")}${contextFlags(options)} --json`;
|
|
143
|
+
}
|
|
144
|
+
export function vaultAccessGuidance(artifact, options) {
|
|
145
|
+
const command = vaultAccessCommand(artifact, options);
|
|
146
|
+
const missing = [...(options.agentType === "openclaw" && !options.host ? ["host"] : []), ...(options.host && requiresTarget(options.host) && !options.target || options.agentType === "openclaw" && !options.host && !options.target ? ["target"] : [])];
|
|
147
|
+
return missing.length ? { next: null, interaction: { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command, required_input: missing, executable: false } } } : { next: { command, reason: "创建一次官方只读授权入口;用户完成后恢复原reader" } };
|
|
141
148
|
}
|
package/dist/src/main.js
CHANGED
|
@@ -25,7 +25,7 @@ import { runCancelRefund, runGetRefund, runListRefunds, runRefund, runWatchRefun
|
|
|
25
25
|
import { runCartAdd, runCartAddQuoteServer, runCartAddServer, runCartAbandonServer, runCartClear, runCartNext, runCartRemove, runCartRemoveServer, runCartShow, runCartShowServer, } from "./commands/cart.js";
|
|
26
26
|
import { CommandContractError, errorRecoveryActions, printErrorRecovery, writeCommandEnvelope } from "./commands/guidance.js";
|
|
27
27
|
import { runDocsList, runDocsShow, runDocsSearch } from "./commands/docs.js";
|
|
28
|
-
import { runInstall } from "./commands/install.js";
|
|
28
|
+
import { INSTALL_AGENT_TYPES, runInstall } from "./commands/install.js";
|
|
29
29
|
import { runSkillShow } from "./commands/skill.js";
|
|
30
30
|
import { runNext } from "./commands/next.js";
|
|
31
31
|
import { runVaultAccess, runVaultList, runVaultRead } from "./commands/vault.js";
|
|
@@ -139,17 +139,12 @@ function reportCLIError(error, contract) {
|
|
|
139
139
|
const providerContractMismatch = error instanceof HttpError && error.code === "provider_contract_mismatch";
|
|
140
140
|
const capabilityInputInvalid = error instanceof HttpError && error.code === "capability_input_invalid";
|
|
141
141
|
const deviceKeyResettable = Boolean(deviceError && (deviceError.code === "agent_device_key_rotated" ||
|
|
142
|
-
deviceError.code === "agent_device_key_conflict"
|
|
143
|
-
(deviceError.enrollmentFailed && deviceError.status === 500)));
|
|
142
|
+
deviceError.code === "agent_device_key_conflict"));
|
|
144
143
|
const deviceRecovery = deviceError ? [
|
|
145
144
|
{
|
|
146
145
|
command: "itpay skill show itpay --json",
|
|
147
146
|
reason: "读取 ItPay 身份边界;该错误需要用户或运营恢复 Backend 登记,不能通过换类型或删除本地身份绕过",
|
|
148
147
|
},
|
|
149
|
-
...(deviceKeyResettable ? [{
|
|
150
|
-
command: "itpay device reset-key --confirm-key-reset --json",
|
|
151
|
-
reason: "服务端拒绝以当前私钥完成设备登记;生成全新 Ed25519 密钥并重新登记(旧设备身份在服务端保留为孤儿,不影响新身份和额度谱系)",
|
|
152
|
-
}] : []),
|
|
153
148
|
] : [];
|
|
154
149
|
const stateRecovery = stateError ? [{
|
|
155
150
|
command: "itpay skill show itpay --json",
|
|
@@ -167,10 +162,10 @@ function reportCLIError(error, contract) {
|
|
|
167
162
|
? "CLI 已自动续期并重试同一请求一次,仍被拒绝;停止重试,不要切换 Agent Type 或旋转身份。"
|
|
168
163
|
: deviceError?.code === "agent_device_revoked"
|
|
169
164
|
? "Backend 已撤销当前 Device 登记;CLI 没有自动创建替代身份。停止重试并请用户或运营恢复登记。"
|
|
170
|
-
:
|
|
171
|
-
? "
|
|
165
|
+
: deviceError?.enrollmentFailed && deviceError.status === 500
|
|
166
|
+
? "服务端未能完成设备登记;保持原密钥和Agent Type,说明系统故障,待服务端恢复后继续原任务。不要重置身份。"
|
|
172
167
|
: deviceKeyResettable
|
|
173
|
-
? "
|
|
168
|
+
? "服务端拒绝当前设备私钥登记(轮换或冲突)。停止自动恢复;换钥会放弃旧身份,旧执行访问不会迁移。仅用户或运营明确决定放弃旧身份后,按 device reset-key 帮助执行。"
|
|
174
169
|
: deviceError
|
|
175
170
|
? "Device 身份验证失败;停止重试,不要切换 Agent Type、删除状态或旋转私钥。"
|
|
176
171
|
: undefined;
|
|
@@ -181,7 +176,7 @@ function reportCLIError(error, contract) {
|
|
|
181
176
|
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"),
|
|
182
177
|
message: error instanceof Error ? error.message : String(error),
|
|
183
178
|
},
|
|
184
|
-
...(requiredCLIVersion ? {
|
|
179
|
+
...(commandError?.result ? { result: commandError.result } : requiredCLIVersion ? {
|
|
185
180
|
result: {
|
|
186
181
|
current_cli_version: CLI_VERSION,
|
|
187
182
|
required_cli_version: requiredCLIVersion,
|
|
@@ -220,7 +215,7 @@ function reportCLIError(error, contract) {
|
|
|
220
215
|
: providerRejected
|
|
221
216
|
? "告诉用户数据来源没有接受本次请求,但没有说明是输入错误,并按 result.quota 说明额度状态,然后停止。不要转述内部错误、修改输入、重试或创建新查询。"
|
|
222
217
|
: capabilityInputInvalid
|
|
223
|
-
? "
|
|
218
|
+
? "Agent按当前合同模板修正字段和JSON层级,保留用户原始需求,继续同一次服务。不要让用户修技术输入,只有真实业务歧义才询问。"
|
|
224
219
|
: transportError
|
|
225
220
|
? transportError.attempts > 1
|
|
226
221
|
? "临时网络故障;CLI 已仅对可安全重放的操作完成有限自动重试,但仍未获得完整响应。按 recovery 查询同一资源的权威状态;不要创建替代 Checkout、Execution、Payment 或 Refund。"
|
|
@@ -229,6 +224,7 @@ function reportCLIError(error, contract) {
|
|
|
229
224
|
? "移除 ITPAY_BACKEND_URL 使用正式环境,或按当前测试目标准确设置为 https://sandbox.itpay.ai 或 https://dev.itpay.ai。不要通过切换环境规避当前错误。"
|
|
230
225
|
: commandError?.instruction ?? authorizationInstruction ?? contract?.instruction ?? "检查命令参数后重试。",
|
|
231
226
|
next: null,
|
|
227
|
+
...(commandError?.interaction ? { interaction: commandError.interaction } : {}),
|
|
232
228
|
recovery: incompatible
|
|
233
229
|
? requiredCLIVersion
|
|
234
230
|
? [distributionUpgradeAction(requiredCLIVersion)]
|
|
@@ -238,7 +234,7 @@ function reportCLIError(error, contract) {
|
|
|
238
234
|
: backendOverrideError ? [] : commandError?.recovery ?? (stateError ? stateRecovery : lockError ? lockRecovery : deviceError ? deviceRecovery : identityRecovery ? httpRecovery : contract?.recovery ?? []),
|
|
239
235
|
}, {
|
|
240
236
|
...(contract?.jsonOutput !== undefined ? { jsonOutput: contract.jsonOutput } : backendOverrideError || lockError || stateError ? { jsonOutput: process.argv.includes("--json") } : {}),
|
|
241
|
-
output: (text) => { process.
|
|
237
|
+
output: (text) => { process.stdout.write(text); },
|
|
242
238
|
});
|
|
243
239
|
process.exitCode = 1;
|
|
244
240
|
return;
|
|
@@ -323,7 +319,7 @@ for (const action of ["login", "status"]) {
|
|
|
323
319
|
// and non-JSON output is human-readable instead of a raw JSON dump.
|
|
324
320
|
const envelope = {
|
|
325
321
|
status: String(result.status ?? "error"),
|
|
326
|
-
result: (result.result ?? result),
|
|
322
|
+
result: (result.result ?? Object.fromEntries(Object.entries(result).filter(([key]) => !["status", "instruction", "next", "recovery", "handoff"].includes(key)))),
|
|
327
323
|
...(result.handoff ? { handoff: result.handoff } : {}),
|
|
328
324
|
instruction: typeof result.instruction === "string" ? result.instruction : "按返回状态继续。",
|
|
329
325
|
next: (result.next ?? null),
|
|
@@ -422,8 +418,11 @@ deviceCmd
|
|
|
422
418
|
.action(async (options) => {
|
|
423
419
|
const config = loadConfig();
|
|
424
420
|
try {
|
|
421
|
+
if (!config.agentType || !INSTALL_AGENT_TYPES.some(type => type === config.agentType)) {
|
|
422
|
+
throw new CommandContractError("agent_type_required", "supported Agent Type required before key reset", "先按真实宿主配置正式支持的Agent Type;尚未修改本地密钥。", [{ command: "itpay install --json", reason: "读取当前宿主配置" }]);
|
|
423
|
+
}
|
|
425
424
|
if (!options.confirmKeyReset) {
|
|
426
|
-
throw new CommandContractError("key_reset_confirmation_required", "--confirm-key-reset is required", "
|
|
425
|
+
throw new CommandContractError("key_reset_confirmation_required", "--confirm-key-reset is required", "仅在用户或运营明确决定放弃旧身份后使用;会丢弃本地私钥,旧执行访问不会迁移。internal_error 需服务端恢复,不能换钥。", [{ command: "itpay docs show identity-and-sessions --json", reason: "检查适用边界" }]);
|
|
427
426
|
}
|
|
428
427
|
const reset = await new DeviceAuthority({
|
|
429
428
|
baseURL: config.baseURL,
|
|
@@ -437,9 +436,9 @@ deviceCmd
|
|
|
437
436
|
private_key_preserved: false,
|
|
438
437
|
server_side_device: "orphaned_under_previous_key",
|
|
439
438
|
},
|
|
440
|
-
instruction: "本地设备私钥已重置;下一次需要设备身份的命令会以全新 Ed25519
|
|
439
|
+
instruction: "本地设备私钥已重置;下一次需要设备身份的命令会以全新 Ed25519 密钥重新登记;这不能恢复旧身份的订单或用于刷新额度。服务端旧设备记录保留为孤儿;如需清理请走运营流程。",
|
|
441
440
|
next: {
|
|
442
|
-
command: `itpay --agent-type ${config.agentType
|
|
441
|
+
command: `itpay --agent-type ${config.agentType} services list --limit 1 --json`,
|
|
443
442
|
reason: "用无业务写入的签名请求完成新密钥的重新登记",
|
|
444
443
|
},
|
|
445
444
|
recovery: [],
|
|
@@ -1402,7 +1401,7 @@ services
|
|
|
1402
1401
|
catch (error) {
|
|
1403
1402
|
reportCLIError(error, {
|
|
1404
1403
|
jsonOutput: Boolean(options.json), code: "workflow_run_failed",
|
|
1405
|
-
instruction: "
|
|
1404
|
+
instruction: "本次操作失败;按已返回的具体原因和恢复动作处理。没有恢复动作时停止,不猜字段或重复创建购买。", recovery: [],
|
|
1406
1405
|
});
|
|
1407
1406
|
}
|
|
1408
1407
|
});
|
|
@@ -1477,12 +1476,18 @@ services
|
|
|
1477
1476
|
.argument("<service_execution_id>")
|
|
1478
1477
|
.requiredOption("--capability <capability_id>")
|
|
1479
1478
|
.option("--input <key=value>", "redacted input summary", collectOption, [])
|
|
1479
|
+
.option("--input-json <file>", "JSON object containing the complete capability input")
|
|
1480
1480
|
.option("--json", "output JSON")
|
|
1481
1481
|
.action(async (serviceExecutionID, options) => {
|
|
1482
1482
|
const config = loadConfig();
|
|
1483
1483
|
const backend = newBackendClient(config);
|
|
1484
1484
|
try {
|
|
1485
|
-
|
|
1485
|
+
const recovery = [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前动作要求" }];
|
|
1486
|
+
if (options.inputJson && options.input.length > 0) {
|
|
1487
|
+
throw new CommandContractError("capability_input_invalid", "--input and --input-json cannot be combined", "嵌套输入用 --input-json <file>,扁平输入用 --input key=value,二选一;本次未调用供应商。", recovery);
|
|
1488
|
+
}
|
|
1489
|
+
const input = options.inputJson ? readInputJsonObject(options.inputJson, "capability_input_invalid", recovery) : parseKeyValueList(options.input);
|
|
1490
|
+
await runServicesInvoke(backend, config, serviceExecutionID, options.capability, input, { jsonOutput: Boolean(options.json) });
|
|
1486
1491
|
}
|
|
1487
1492
|
catch (error) {
|
|
1488
1493
|
reportCLIError(error, {
|
|
@@ -1687,22 +1692,28 @@ services
|
|
|
1687
1692
|
});
|
|
1688
1693
|
services
|
|
1689
1694
|
.command("read-result")
|
|
1690
|
-
.description("Read
|
|
1695
|
+
.description("Read saved railway results or human-granted service content")
|
|
1691
1696
|
.argument("<service_execution_id>")
|
|
1692
|
-
.option("--snapshot <snapshot_id>", "
|
|
1697
|
+
.option("--snapshot <snapshot_id>", "Smart: saved snapshot catalog; add --journey for a journey detail. Exact: saved station-pair rows without --snapshot")
|
|
1693
1698
|
.option("--journey <journey_id>", "rail.progressive.v2: journey id to read")
|
|
1699
|
+
.option("--offset <offset>", "saved journey offset", Number, 0)
|
|
1700
|
+
.option("--limit <limit>", "rows per page (1-20): Exact 20, Smart 3", Number)
|
|
1701
|
+
.option("--all", "read every saved journey page")
|
|
1694
1702
|
.option("--json", "output JSON instead of terminal text")
|
|
1695
1703
|
.action(async (serviceExecutionID, options) => {
|
|
1696
1704
|
const config = loadConfig();
|
|
1697
1705
|
const backend = newBackendClient(config);
|
|
1698
1706
|
try {
|
|
1699
|
-
await runServicesReadResult(backend, serviceExecutionID, { jsonOutput: Boolean(options.json), snapshot: options.snapshot, journey: options.journey
|
|
1707
|
+
await runServicesReadResult(backend, serviceExecutionID, { jsonOutput: Boolean(options.json), snapshot: options.snapshot, journey: options.journey,
|
|
1708
|
+
offset: options.offset, limit: options.limit, all: Boolean(options.all) });
|
|
1700
1709
|
}
|
|
1701
1710
|
catch (error) {
|
|
1702
1711
|
reportCLIError(error, {
|
|
1703
1712
|
jsonOutput: Boolean(options.json),
|
|
1704
|
-
code: "agent_access_denied",
|
|
1705
|
-
instruction: "
|
|
1713
|
+
code: error instanceof HttpError && error.code === "agent_access_denied" ? "agent_access_denied" : "service_result_read_failed",
|
|
1714
|
+
instruction: error instanceof HttpError && error.code === "agent_access_denied"
|
|
1715
|
+
? "请用户在订单页面重新授权;不要使用开发者权限绕过授权或退款锁。"
|
|
1716
|
+
: "读取同一服务的已保存结果;若快照已不可读,先查看该服务当前状态和有效动作。",
|
|
1706
1717
|
recovery: [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "检查交付模式和 grant 状态" }],
|
|
1707
1718
|
});
|
|
1708
1719
|
}
|
package/dist/src/render/qr.js
CHANGED
|
@@ -20,7 +20,7 @@ export async function renderTerminalQR(url, format) {
|
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
22
|
export async function writeLocalPNG(url) {
|
|
23
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "itpay-v3-"));
|
|
23
|
+
const dir = await fs.mkdtemp(path.join(process.env.ITPAY_STATE_DIR || os.tmpdir(), "itpay-v3-"));
|
|
24
24
|
const filePath = path.join(dir, `itpay-${randomUUID()}.png`);
|
|
25
25
|
await QRCode.toFile(filePath, url, {
|
|
26
26
|
type: "png",
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
2
|
import { dirname, resolve } from 'node:path';
|
|
4
3
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, rmSync } from 'node:fs';
|
|
5
4
|
import { writeLocalPNG } from '../render/qr.js';
|
|
6
5
|
import { TaskJournal } from './task_journal.js';
|
|
7
6
|
import { taskJournalPath } from './config.js';
|
|
8
7
|
export function sellerAuthPath(baseURL, env = process.env, purpose = "seller") {
|
|
9
|
-
return resolve(env
|
|
8
|
+
return resolve(stateDir(env), `${purpose}-${createHash('sha256').update(baseURL).digest('hex').slice(0, 16)}.json`);
|
|
10
9
|
}
|
|
11
10
|
function read(baseURL, env = process.env, purpose = "seller") {
|
|
12
11
|
const path = sellerAuthPath(baseURL, env, purpose);
|
|
@@ -59,11 +58,11 @@ const TERMINAL_SESSION_STATES = {
|
|
|
59
58
|
},
|
|
60
59
|
cancelled: {
|
|
61
60
|
status: 'auth_cancelled',
|
|
62
|
-
instruction: '
|
|
61
|
+
instruction: '用户在官方页取消了授权;停止登录。只有用户明确重新要求授权时才发起新的登录。',
|
|
63
62
|
},
|
|
64
63
|
denied: {
|
|
65
64
|
status: 'auth_denied',
|
|
66
|
-
instruction: '
|
|
65
|
+
instruction: '官方授权被拒绝;停止登录,不自动创建新请求。只有用户明确重新要求授权时才重新登录。',
|
|
67
66
|
},
|
|
68
67
|
};
|
|
69
68
|
class AuthRequestError extends Error {
|
|
@@ -215,7 +214,10 @@ async function accountAuth(action, baseURL, env, fetcher, backend) {
|
|
|
215
214
|
const url = new URL(result.start_url, baseURL);
|
|
216
215
|
const sameOrigin = url.origin === new URL(baseURL).origin;
|
|
217
216
|
const alipay = url.origin === 'https://openauth.alipay.com' && url.pathname === '/oauth2/publicAppAuthorize.htm' && !url.username && !url.password;
|
|
218
|
-
|
|
217
|
+
const local = response.headers.get('X-ItPay-Environment') === 'local_simulation'
|
|
218
|
+
&& new URL(baseURL).protocol === 'http:' && new URL(baseURL).hostname === '127.0.0.1'
|
|
219
|
+
&& url.protocol === 'http:' && url.hostname === '127.0.0.1' && !url.username && !url.password;
|
|
220
|
+
if (!sameOrigin && !alipay && !local)
|
|
219
221
|
throw new Error('Unexpected authorization origin');
|
|
220
222
|
const fragment = new URLSearchParams(url.hash.replace(/^#dashboard-auth\?/, ''));
|
|
221
223
|
const state = url.searchParams.get('state')?.split('.');
|
|
@@ -289,7 +291,7 @@ async function accountAuth(action, baseURL, env, fetcher, backend) {
|
|
|
289
291
|
status: terminal.status,
|
|
290
292
|
result: { dashboard_auth_session_id: state.sessionID, stage: progress.status },
|
|
291
293
|
instruction: terminal.instruction,
|
|
292
|
-
next: { command: 'itpay auth login --json', reason: '
|
|
294
|
+
next: ['denied', 'cancelled'].includes(progress.status) ? null : { command: 'itpay auth login --json', reason: '需要登录时重新发起官方授权' },
|
|
293
295
|
recovery: [],
|
|
294
296
|
};
|
|
295
297
|
}
|
|
@@ -327,3 +329,4 @@ async function accountAuth(action, baseURL, env, fetcher, backend) {
|
|
|
327
329
|
save({ baseURL, sessionToken: token, expiresAt: session.expires_at }, env);
|
|
328
330
|
return { status: 'authenticated', base_url: baseURL, expires_at: session.expires_at };
|
|
329
331
|
}
|
|
332
|
+
import { stateDir } from "./config.js";
|
package/dist/src/state/config.js
CHANGED
|
@@ -15,8 +15,8 @@ import { OperationJournal } from "./operation_journal.js";
|
|
|
15
15
|
export const DEFAULT_BASE_URL = "https://app.itpay.ai";
|
|
16
16
|
export const DEV_BASE_URL = "https://dev.itpay.ai";
|
|
17
17
|
export const SANDBOX_BASE_URL = "https://sandbox.itpay.ai";
|
|
18
|
-
export const CLI_VERSION = "2.1.
|
|
19
|
-
export const API_CONTRACT_REVISION = "sha256:
|
|
18
|
+
export const CLI_VERSION = "2.1.33";
|
|
19
|
+
export const API_CONTRACT_REVISION = "sha256:80d69dbad36debbe4bdfbe468127a934c87b657e7795f369bb04de54863da476";
|
|
20
20
|
const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
|
|
21
21
|
const CART_SESSION_FILENAME = "cart.json";
|
|
22
22
|
const OPERATION_JOURNAL_FILENAME = "operations.json";
|
|
@@ -65,8 +65,8 @@ function stateFilename(filename, baseURL) {
|
|
|
65
65
|
const dot = filename.lastIndexOf(".");
|
|
66
66
|
return dot < 0 ? `${filename}.${suffix}` : `${filename.slice(0, dot)}.${suffix}${filename.slice(dot)}`;
|
|
67
67
|
}
|
|
68
|
-
function stateDir(env) {
|
|
69
|
-
return resolve(env.HOME || homedir(), CART_SESSION_DEFAULT_DIR);
|
|
68
|
+
export function stateDir(env = process.env) {
|
|
69
|
+
return env.ITPAY_STATE_DIR ? resolve(env.ITPAY_STATE_DIR) : resolve(env.HOME || homedir(), CART_SESSION_DEFAULT_DIR);
|
|
70
70
|
}
|
|
71
71
|
export function cartSessionPath(env = process.env) {
|
|
72
72
|
if (env.ITPAY_CART_SESSION_PATH) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, sign, } from "node:crypto";
|
|
2
2
|
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import { dirname, resolve } from "node:path";
|
|
5
4
|
const PROTECTED_PATHS = ["/v1/agent-device-account-bindings", "/v1/carts", "/v1/service-executions", "/v1/agent-instances", "/v1/orders", "/v1/refunds", "/v1/me", "/v1/vault", "/v1/rail/phone-links"];
|
|
6
5
|
export class DeviceAuthority {
|
|
@@ -17,7 +16,7 @@ export class DeviceAuthority {
|
|
|
17
16
|
this.backendKey = normalizeBackendKey(options.baseURL);
|
|
18
17
|
this.requestedAgentType = options.requestedAgentType;
|
|
19
18
|
this.compatibilityHeaders = options.compatibilityHeaders;
|
|
20
|
-
const root = resolve(
|
|
19
|
+
const root = resolve(stateDir(), "device");
|
|
21
20
|
this.statePath = options.statePath ?? resolve(root, "identity.json");
|
|
22
21
|
this.privateKeyPath = options.privateKeyPath ?? resolve(root, "device-private.pem");
|
|
23
22
|
this.fetchImpl = (options.fetchImpl ?? globalThis.fetch).bind(globalThis);
|
|
@@ -470,3 +469,4 @@ function asDeviceStatePathError(error, operation) {
|
|
|
470
469
|
const code = error.code;
|
|
471
470
|
return code === "EEXIST" ? new DeviceStateError(operation, code) : asDeviceStateError(error, operation);
|
|
472
471
|
}
|
|
472
|
+
import { stateDir } from "./config.js";
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"The Backend Cart is business truth; the current backend-isolated local cart state file only stores local recovery handles and explicit drafts.",
|
|
36
36
|
"Run the exact next.command returned by the CLI. Do not extract or invent a capability from a raw list.",
|
|
37
37
|
"Railway query and booking use distinct Service Executions for one purchase journey. Start another execution for an independent intent.",
|
|
38
|
-
"
|
|
38
|
+
"Only non-rail capabilities explicitly requiring delivery email may ask its purpose and value. Railway notification contact belongs to the protected checkout page with verified account email reuse; the Agent must not guess or collect it in chat.",
|
|
39
39
|
"A lost Checkout response is retried with the same Cart and persisted idempotency operation.",
|
|
40
40
|
"A single railway service checkout normally follows its booking execution; Cart is used only when the returned flow calls for it."
|
|
41
41
|
],
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"service_flow": "Explain discovery as an auxiliary identity-confirmation step, including free quota and paid continuation. Present primary_service as the actual paid outcome. Read all values from the manifest.",
|
|
22
22
|
"variant_selection": "Internal IDs are for the Agent's next command only and are never shown or requested from the human.",
|
|
23
23
|
"price_note": "amount_minor is in minor units (e.g., 10000 = 100.00 CNY). The CLI formats prices as XX.YY currency automatically.",
|
|
24
|
-
"railway": "Railway entries point to itpay docs show rail-booking
|
|
24
|
+
"railway": "Railway entries point to itpay docs show rail-booking; use its query and booking contracts instead of a generic start→invoke→cart sequence."
|
|
25
25
|
},
|
|
26
26
|
"agent_rules": [
|
|
27
27
|
"Use catalog list only when the human asks for a new service, not when they want an earlier purchase.",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"When a previously unseen Agent Type is registered under an existing Device, the CLI skips locally known Agent Instances that the Backend reports as revoked and uses the first remaining valid Instance. It never reactivates a revoked Instance or replaces the Device; if none is valid, registration stops.",
|
|
24
24
|
"When the Backend reports agent_device_not_found (HTTP 404) for a locally known registration — for example after the Backend registration database was rebuilt — the CLI drops only that Backend's registration and re-enrolls once with the same private key, re-attaching to the same device and quota lineage where the Backend still holds the key.",
|
|
25
25
|
"A revoked v2 registration requires explicit operator recovery; it is never silently replaced.",
|
|
26
|
-
"Re-enrolling a public key the Backend already holds re-attaches to the existing device and quota lineage after proof-of-possession; it never creates a duplicate device.
|
|
26
|
+
"Re-enrolling a public key the Backend already holds re-attaches to the existing device and quota lineage after proof-of-possession; it never creates a duplicate device. An internal_error is a server failure: preserve the key and wait for service recovery. If enrollment returns agent_device_key_rotated or agent_device_key_conflict, stop automatic recovery. Only an explicit human/operator decision to abandon the old identity permits reset-key; old execution access does not migrate."
|
|
27
27
|
],
|
|
28
28
|
"local_state_recovery": [
|
|
29
29
|
"Device state uses an owner-only atomic file replacement and a lock that the CLI can inspect. On a stale lock, run the returned device repair-lock command; it only reclaims a proven stale owner.",
|
|
@@ -47,11 +47,6 @@
|
|
|
47
47
|
"command": "itpay --agent-type <agent_type> device recover --confirm-backend-reset --json",
|
|
48
48
|
"success_signal": "only the selected Backend registration is removed and the private key and other Backend registration remain"
|
|
49
49
|
},
|
|
50
|
-
{
|
|
51
|
-
"intent": "abandon the local device key after the Backend refuses to complete enrollment with it (persistent internal_error, agent_device_key_rotated, or agent_device_key_conflict)",
|
|
52
|
-
"command": "itpay device reset-key --confirm-key-reset --json",
|
|
53
|
-
"success_signal": "the local private key is discarded and the next command enrolls as a brand-new device"
|
|
54
|
-
},
|
|
55
50
|
{
|
|
56
51
|
"intent": "repair only a proven stale local device lock",
|
|
57
52
|
"command": "itpay device repair-lock --json",
|
|
@@ -103,5 +98,6 @@
|
|
|
103
98
|
"missing": "auth_session_missing means the cached session is gone. An explicit auth login opens one fresh official session in that call; auth status stays read-only.",
|
|
104
99
|
"completed_resume": "After login completes, execute the returned command to resume the same paused service execution. Login does not silently run that command.",
|
|
105
100
|
"path_b": "A buyer who reaches checkout without a login completes identity inside the protected checkout page; never force a separate itpay auth login first."
|
|
106
|
-
}
|
|
101
|
+
},
|
|
102
|
+
"key_reset_decision": "Only after explicit user/operator acceptance of abandoning the old identity, consult device reset-key help; this is not automatic error recovery, and old execution access does not migrate."
|
|
107
103
|
}
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"Act as the human's ItPay service representative: explain payment, delivery, access, and refund facts in plain language before giving the next action.",
|
|
42
42
|
"After verified payment, say the Order is recorded and the human must not pay again. Recover that same Order or Service Execution if delivery is delayed or fails.",
|
|
43
43
|
"ItPay derives payment, amount, currency, account ownership, and refund policy from the owned order; explain only their user-facing meaning.",
|
|
44
|
+
"A railway order can contain 1–3 legs paid together and issued separately. Report all-issued only when every leg is issued; retain issued tickets on a partial failure or unknown result. Railway return and platform refund follow per-leg supplier facts; do not promise a full refund or create replacement tickets.",
|
|
44
45
|
"Read this order’s current refund policy. Railway return depends on supplier ticket facts and may require manual review; platform refund status alone does not prove a train ticket was returned.",
|
|
45
46
|
"A refund request freezes all delivery paths and revokes existing grants immediately.",
|
|
46
47
|
"Cancellation or rejection follows the returned order policy and never reactivates an old grant automatically.",
|
|
@@ -92,5 +93,11 @@
|
|
|
92
93
|
"交付失败",
|
|
93
94
|
"没有收到结果",
|
|
94
95
|
"能不能退"
|
|
95
|
-
]
|
|
96
|
+
],
|
|
97
|
+
"rail_notifications": {
|
|
98
|
+
"channels": "Delivery notices reach the checkout-confirmed contact: email always works; a verified mainland SMS recipient is optional and checkout-scoped. The passenger phone, the login phone and the notification recipient are independent facts.",
|
|
99
|
+
"status_read": "Use the current returned reader for ticket state; order summaries do not expose per-leg notification delivery records. Do not infer a sent or delivered notification.",
|
|
100
|
+
"truthfulness": "Provider acceptance is not delivery; an uncertain send outcome is reconciled, not silently retried. A notification failure never changes ticket or refund state; report it as a delivery detail, not an order failure.",
|
|
101
|
+
"unavailable": "If SMS is not configured or a recipient is unverified, email still satisfies the contact policy — never present an unverified phone as enabled."
|
|
102
|
+
}
|
|
96
103
|
}
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"Normal buyer flow opens the ItPay Checkout page; itpay pay and buy --pay are operator escape hatches.",
|
|
31
31
|
"Use only handoff fields returned for the current Host and make them actually visible to the human.",
|
|
32
32
|
"Obtain only missing required agreement or material price/term changes. Reuse a valid explicit confirmation for unchanged terms; never invent it.",
|
|
33
|
+
"For a railway journey, show one official checkout for all legs: one total/payment, separate issuance. The page shares its authoritative payment_deadline_at/server_now/payment_remaining_seconds; reaching zero requires reading current status, not declaring cancellation.",
|
|
33
34
|
"After presenting a Checkout, stop. Run the returned next.command only after the human completes the payment action or asks to query the same Checkout.",
|
|
34
35
|
"Use only the handoff fields returned for the current Host. Load render-hosts if the official handoff is not visible.",
|
|
35
36
|
"Payment is verified only by Backend Checkout or Order state, never by QR rendering, redirect, or user claim.",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"authorization_flow": [
|
|
36
36
|
"Run the original list, orders, or read command.",
|
|
37
37
|
"OpenClaw must pass its current trusted host and required target on the original command so the authorization continuation preserves the real presentation destination.",
|
|
38
|
-
"If it returns human_authorization_required, run
|
|
38
|
+
"If it returns human_authorization_required and context is complete, run its executable next.command once. If next is null or context/consent is missing, fill the returned interaction/input_template first; never invent host, target or consent.",
|
|
39
39
|
"Make the returned handoff genuinely visible and stop.",
|
|
40
40
|
"After the human says authorization is complete, rerun the original read command unchanged.",
|
|
41
41
|
"Never rerun vault access as a status check."
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"refund_or_paid_problem": "Resume the known Order or Refund before creating anything new.",
|
|
28
28
|
"service_feedback": "Finish the original order outcome first, then submit one safe Agent postmortem; include a rating or comment only when the human explicitly gives it.",
|
|
29
29
|
"ambiguous": "Ask whether the human wants an earlier purchase or a new query before calling ItPay.",
|
|
30
|
-
"railway": "Known railway intent: itpay docs show rail-booking
|
|
30
|
+
"railway": "Known railway intent: itpay docs show rail-booking. Read once, then follow its Exact/Smart route."
|
|
31
31
|
},
|
|
32
32
|
"commands": [
|
|
33
33
|
{
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
"agent_rules": [
|
|
40
40
|
"Use the CLI as the control surface; infer the human's intent before the first command and never hardcode a service workflow.",
|
|
41
41
|
"Treat result as current facts, instruction as how to use those facts, next as the preferred executable action, and recovery as exceptional paths.",
|
|
42
|
-
"Run one state-changing command at a time. Treat next.command as the preferred continuation: execute it unchanged
|
|
42
|
+
"Run one state-changing command at a time. Treat next.command as the preferred continuation: execute it unchanged only when its required values are already present; fill a non-executable input_template separately only when the current result has not already satisfied the user's goal.",
|
|
43
43
|
"Keep the same explicit Agent Type through next and recovery commands; same-type windows reuse one Backend Agent Instance.",
|
|
44
44
|
"When testing sandbox, execute every returned command unchanged so its ITPAY_BACKEND_URL=https://sandbox.itpay.ai prefix is preserved.",
|
|
45
|
-
"Use --target only for a Host presentation destination.
|
|
46
|
-
"
|
|
45
|
+
"Use --target only for a Host presentation destination. Use --input key=value only for simple scalars; nested objects use a small JSON file with the existing --input-json command.",
|
|
46
|
+
"For non-rail delivery that explicitly requires email, ask only that missing value and explain its purpose. Railway identity and notification contact are handled on the protected official checkout page; never guess them.",
|
|
47
47
|
"Explain useful service facts in ordinary language; never ask the human to run commands or learn internal IDs.",
|
|
48
48
|
"Normal payment uses the returned ItPay Checkout handoff.",
|
|
49
49
|
"A railway query execution and its separate booking execution may serve one purchase intent; keep both references."
|
|
@@ -106,5 +106,6 @@
|
|
|
106
106
|
"rating",
|
|
107
107
|
"target",
|
|
108
108
|
"input"
|
|
109
|
-
]
|
|
109
|
+
],
|
|
110
|
+
"reading_results": "Read normal CLI text directly, or parse the complete --json envelope with the host file reader. If saving stdout to a file, first check the command exit code, then read the whole file with cat or the existing file tool. Keep stderr separate; do not use head, tail, regex or a custom script to recover truncated results. Write complex input as a small JSON file and use --input-json."
|
|
110
111
|
}
|