@itpay/cli 2.1.8 → 2.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/services.js +17 -2
- package/dist/src/main.js +97 -11
- package/dist/src/state/config.js +1 -1
- package/dist/src/state/device_authority.js +64 -12
- package/docs/agent/buyer/identity-and-sessions.json +9 -2
- package/docs/agent/buyer/rail-booking.json +60 -8
- package/docs/cli-reference/commands/auth.md +1 -0
- package/docs/cli-reference/commands/device.md +13 -1
- package/docs/cli-reference/commands/services/action.md +9 -1
- package/docs/cli-reference/commands/services/checkout.md +4 -2
- package/docs/cli-reference/commands/services/get.md +2 -0
- package/docs/cli-reference/commands/services/next.md +29 -0
- package/docs/cli-reference/commands/services/run.md +1 -1
- package/package.json +1 -1
|
@@ -1095,6 +1095,17 @@ function servicesNextEnvelope(model) {
|
|
|
1095
1095
|
recovery: [],
|
|
1096
1096
|
};
|
|
1097
1097
|
}
|
|
1098
|
+
const review = action.context?.review;
|
|
1099
|
+
if (review) {
|
|
1100
|
+
return {
|
|
1101
|
+
status: "booking_review_required",
|
|
1102
|
+
result: { service_execution_id: id, service_id: execution.service_id, human_action: action,
|
|
1103
|
+
required_fields: requiredFields },
|
|
1104
|
+
instruction: "向用户展示 context.review 中的行程(legs)与可选席别/座位偏好词表(seat_options),并明确告知:座位偏好仅为请求、不保证分配(对应 notice_version 文案须经用户同意)。收集乘客人数、席别与每位乘客偏好后,按 required_fields 组装完整 JSON 对象,经 --input-json <file> 提交——draft_revision 必须等于服务端当前值,被拒绝(booking_review_changed)时重新读取后重试。乘客姓名/证件等身份信息不在此提交,仍走受保护 Checkout 页。",
|
|
1105
|
+
next: { command: `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved --input-json <file> --json`, reason: "提交用户确认后的行程确认/修订" },
|
|
1106
|
+
recovery: [{ command: `itpay services next ${id} --json`, reason: "重新读取当前 draft_revision 后重试" }],
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1098
1109
|
return {
|
|
1099
1110
|
status: "confirmation_required",
|
|
1100
1111
|
result: { service_execution_id: id, service_id: execution.service_id, human_action: action },
|
|
@@ -1139,7 +1150,9 @@ function servicesNextEnvelope(model) {
|
|
|
1139
1150
|
},
|
|
1140
1151
|
instruction: failedInstruction
|
|
1141
1152
|
?? (state === "payment"
|
|
1142
|
-
?
|
|
1153
|
+
? (model.workflow?.human_action?.context?.review
|
|
1154
|
+
? "服务已到付款步骤,可直接 Checkout 付款;如需调整席别/座位偏好,先用 confirm_booking 动作修订(会重新报价并锁定新价),完成后再付款。"
|
|
1155
|
+
: "服务已到付款步骤,使用现有 Checkout 完成扫码付款。")
|
|
1143
1156
|
: guidance
|
|
1144
1157
|
? "按 result.guidance 的字段契约填写输入后继续同一服务执行;不要臆造字段名。"
|
|
1145
1158
|
: "继续读取同一执行;缺少输入时按服务声明补齐。"),
|
|
@@ -1149,7 +1162,9 @@ function servicesNextEnvelope(model) {
|
|
|
1149
1162
|
{ command: `itpay services start ${execution.service_id} --json`, reason: "修正输入后重新发起(本执行已终止不能续用)" },
|
|
1150
1163
|
...(guidance ? [{ command: "itpay docs show rail-booking --json", reason: "查看本服务输入字段契约与示例" }] : []),
|
|
1151
1164
|
]
|
|
1152
|
-
:
|
|
1165
|
+
: state === "payment" && model.workflow?.human_action?.context?.review
|
|
1166
|
+
? [{ command: `itpay services action ${id} --action ${model.workflow.human_action.action_type} --actor-type human --status approved --input-json <file> --json`, reason: "付款前修订席别/座位偏好(draft_revision 以服务端为准)" }]
|
|
1167
|
+
: [],
|
|
1153
1168
|
};
|
|
1154
1169
|
}
|
|
1155
1170
|
const terminalEnvelope = terminalExecutionEnvelope(model);
|
package/dist/src/main.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agentAuth } from "./state/account_auth.js";
|
|
2
2
|
import { registerSell } from "./sell/commands.js";
|
|
3
|
-
import { readFileSync as readWorkflowInputFile } from "node:fs";
|
|
3
|
+
import { readFileSync as readWorkflowInputFile, statSync as statWorkflowInputFile } from "node:fs";
|
|
4
4
|
import { runServicesRun } from "./commands/services.js";
|
|
5
5
|
// V3 CLI entrypoint. Each command maps 1:1 to a route family in
|
|
6
6
|
// services/backend/internal/httpapi/handlers/*.go. Commands only
|
|
@@ -137,10 +137,19 @@ function reportCLIError(error, contract) {
|
|
|
137
137
|
const providerInputRejected = error instanceof HttpError && error.code === "provider_input_rejected";
|
|
138
138
|
const providerContractMismatch = error instanceof HttpError && error.code === "provider_contract_mismatch";
|
|
139
139
|
const capabilityInputInvalid = error instanceof HttpError && error.code === "capability_input_invalid";
|
|
140
|
-
const
|
|
140
|
+
const deviceKeyResettable = Boolean(deviceError && (deviceError.code === "agent_device_key_rotated" ||
|
|
141
|
+
deviceError.code === "agent_device_key_conflict" ||
|
|
142
|
+
(deviceError.enrollmentFailed && deviceError.status === 500)));
|
|
143
|
+
const deviceRecovery = deviceError ? [
|
|
144
|
+
{
|
|
141
145
|
command: "itpay skill show itpay --json",
|
|
142
146
|
reason: "读取 ItPay 身份边界;该错误需要用户或运营恢复 Backend 登记,不能通过换类型或删除本地身份绕过",
|
|
143
|
-
}
|
|
147
|
+
},
|
|
148
|
+
...(deviceKeyResettable ? [{
|
|
149
|
+
command: "itpay device reset-key --confirm-key-reset --json",
|
|
150
|
+
reason: "服务端拒绝以当前私钥完成设备登记;生成全新 Ed25519 密钥并重新登记(旧设备身份在服务端保留为孤儿,不影响新身份和额度谱系)",
|
|
151
|
+
}] : []),
|
|
152
|
+
] : [];
|
|
144
153
|
const stateRecovery = stateError ? [{
|
|
145
154
|
command: "itpay skill show itpay --json",
|
|
146
155
|
reason: "读取 Device 状态边界;修复当前 Host 的持久写权限后重试原命令",
|
|
@@ -151,9 +160,13 @@ function reportCLIError(error, contract) {
|
|
|
151
160
|
? "CLI 已自动续期并重试同一请求一次,仍被拒绝;停止重试,不要切换 Agent Type 或旋转身份。"
|
|
152
161
|
: deviceError?.code === "agent_device_revoked"
|
|
153
162
|
? "Backend 已撤销当前 Device 登记;CLI 没有自动创建替代身份。停止重试并请用户或运营恢复登记。"
|
|
154
|
-
: deviceError
|
|
155
|
-
? "
|
|
156
|
-
:
|
|
163
|
+
: deviceKeyResettable && deviceError?.status === 500
|
|
164
|
+
? "服务端未能完成设备登记。先原样重试一次原命令:服务端会把已登记的同一公钥幂等挂回原设备。若仍返回 internal_error(Backend 未含该修复),执行 itpay device reset-key --confirm-key-reset 生成全新密钥后重试。"
|
|
165
|
+
: deviceKeyResettable
|
|
166
|
+
? "服务端记录显示当前设备私钥已不再有效(已轮换或与既有登记冲突)。执行 itpay device reset-key --confirm-key-reset 生成全新密钥并重新登记。"
|
|
167
|
+
: deviceError
|
|
168
|
+
? "Device 身份验证失败;停止重试,不要切换 Agent Type、删除状态或旋转私钥。"
|
|
169
|
+
: undefined;
|
|
157
170
|
if (contract || commandError || backendOverrideError) {
|
|
158
171
|
writeCommandEnvelope({
|
|
159
172
|
status: "error",
|
|
@@ -359,6 +372,53 @@ deviceCmd
|
|
|
359
372
|
});
|
|
360
373
|
}
|
|
361
374
|
});
|
|
375
|
+
deviceCmd
|
|
376
|
+
.command("reset-key")
|
|
377
|
+
.description("Discard the local Ed25519 device key and all Backend registrations so the next command enrolls as a new device")
|
|
378
|
+
.option("--confirm-key-reset", "confirm that the current device key must be abandoned and a new identity created")
|
|
379
|
+
.option("--json", "output JSON instead of terminal text")
|
|
380
|
+
.action(async (options) => {
|
|
381
|
+
const config = loadConfig();
|
|
382
|
+
try {
|
|
383
|
+
if (!options.confirmKeyReset) {
|
|
384
|
+
throw new CommandContractError("key_reset_confirmation_required", "--confirm-key-reset is required", "仅当服务端拒绝以当前私钥完成设备登记(internal_error、agent_device_key_rotated 或 agent_device_key_conflict)时使用;会放弃本地设备身份并重新登记,旧设备在服务端保留为孤儿。", [{ command: "itpay docs show identity-and-sessions --json", reason: "检查适用边界" }]);
|
|
385
|
+
}
|
|
386
|
+
const reset = await new DeviceAuthority({
|
|
387
|
+
baseURL: config.baseURL,
|
|
388
|
+
...(config.agentType ? { requestedAgentType: config.agentType } : {}),
|
|
389
|
+
compatibilityHeaders: {},
|
|
390
|
+
}).resetDeviceKey();
|
|
391
|
+
writeCommandEnvelope({
|
|
392
|
+
status: "device_key_reset",
|
|
393
|
+
result: {
|
|
394
|
+
removed_backend_registrations: reset.removedBackends,
|
|
395
|
+
private_key_preserved: false,
|
|
396
|
+
server_side_device: "orphaned_under_previous_key",
|
|
397
|
+
},
|
|
398
|
+
instruction: "本地设备私钥已重置;下一次需要设备身份的命令会以全新 Ed25519 密钥重新登记并获得新的额度谱系。服务端旧设备记录保留为孤儿;如需清理请走运营流程。",
|
|
399
|
+
next: {
|
|
400
|
+
command: `itpay --agent-type ${config.agentType ?? "<agent_type>"} services list --limit 1 --json`,
|
|
401
|
+
reason: "用无业务写入的签名请求完成新密钥的重新登记",
|
|
402
|
+
},
|
|
403
|
+
recovery: [],
|
|
404
|
+
}, {
|
|
405
|
+
jsonOutput: Boolean(options.json),
|
|
406
|
+
plainResult: [
|
|
407
|
+
`removed_backend_registrations: ${reset.removedBackends.join(", ") || "none"}`,
|
|
408
|
+
"private_key: rotated (previous key discarded)",
|
|
409
|
+
"server_side_device: orphaned under previous key",
|
|
410
|
+
],
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
reportCLIError(error, {
|
|
415
|
+
jsonOutput: Boolean(options.json),
|
|
416
|
+
code: "device_key_reset_failed",
|
|
417
|
+
instruction: "仅在确认要放弃当前设备私钥时使用;普通 session 失效或 revoked 不需要本命令。",
|
|
418
|
+
recovery: [{ command: "itpay docs show identity-and-sessions --json", reason: "检查 Device 恢复边界" }],
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
});
|
|
362
422
|
// --- skill ----------------------------------------------------------------
|
|
363
423
|
const skillCmd = program.command("skill").description("Read complete packaged Agent skills");
|
|
364
424
|
skillCmd
|
|
@@ -1231,6 +1291,25 @@ vault
|
|
|
1231
1291
|
}
|
|
1232
1292
|
});
|
|
1233
1293
|
// --- service execution ----------------------------------------------------
|
|
1294
|
+
const WORKFLOW_INPUT_JSON_MAX_BYTES = 256 * 1024;
|
|
1295
|
+
function readInputJsonObject(file, code, recovery) {
|
|
1296
|
+
let parsed;
|
|
1297
|
+
try {
|
|
1298
|
+
if (statWorkflowInputFile(file).size > WORKFLOW_INPUT_JSON_MAX_BYTES) {
|
|
1299
|
+
throw new CommandContractError(code, `--input-json file exceeds ${WORKFLOW_INPUT_JSON_MAX_BYTES} bytes`, "输入文件过大;只提交服务端 input_schema 声明的字段。", recovery);
|
|
1300
|
+
}
|
|
1301
|
+
parsed = JSON.parse(readWorkflowInputFile(file, "utf8"));
|
|
1302
|
+
}
|
|
1303
|
+
catch (error) {
|
|
1304
|
+
if (error instanceof CommandContractError)
|
|
1305
|
+
throw error;
|
|
1306
|
+
throw new CommandContractError(code, `--input-json is not readable valid JSON: ${error instanceof Error ? error.message : String(error)}`, "确认文件存在且为合法 JSON;按服务端 input_schema 字段填写。", recovery);
|
|
1307
|
+
}
|
|
1308
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1309
|
+
throw new CommandContractError(code, "--input-json must contain a JSON object", "确认输入必须是 JSON 对象;按服务端 input_schema 字段填写。", recovery);
|
|
1310
|
+
}
|
|
1311
|
+
return parsed;
|
|
1312
|
+
}
|
|
1234
1313
|
const services = program.command("services").description("Generic V3 Service Execution commands");
|
|
1235
1314
|
services
|
|
1236
1315
|
.command("run")
|
|
@@ -1245,10 +1324,11 @@ services
|
|
|
1245
1324
|
.action(async (serviceID, options) => {
|
|
1246
1325
|
const config = loadConfig();
|
|
1247
1326
|
try {
|
|
1248
|
-
const input = options.inputJson
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1327
|
+
const input = options.inputJson
|
|
1328
|
+
? readInputJsonObject(options.inputJson, "workflow_input_invalid", [
|
|
1329
|
+
{ command: `itpay services run ${serviceID} --json`, reason: "重新读取服务输入声明" },
|
|
1330
|
+
])
|
|
1331
|
+
: undefined;
|
|
1252
1332
|
if (!Number.isFinite(options.timeout) || options.timeout < 0 || options.timeout > 600) {
|
|
1253
1333
|
throw new Error("timeout must be between 0 and 600 seconds");
|
|
1254
1334
|
}
|
|
@@ -1362,12 +1442,18 @@ services
|
|
|
1362
1442
|
.option("--result-item <service_capability_result_item_id>")
|
|
1363
1443
|
.option("--required-before <step>")
|
|
1364
1444
|
.option("--input <key=value>", "action input snapshot", collectOption, [])
|
|
1445
|
+
.option("--input-json <file>", "JSON object containing the full action input snapshot")
|
|
1365
1446
|
.option("--json", "output JSON instead of terminal text")
|
|
1366
1447
|
.action(async (serviceExecutionID, options) => {
|
|
1367
1448
|
const config = loadConfig();
|
|
1368
1449
|
const backend = newBackendClient(config);
|
|
1369
1450
|
try {
|
|
1370
|
-
|
|
1451
|
+
const recovery = [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前动作要求" }];
|
|
1452
|
+
if (options.inputJson && options.input.length > 0) {
|
|
1453
|
+
throw new CommandContractError("service_action_invalid", "--input and --input-json cannot be combined", "--input 与 --input-json 只能二选一:嵌套结构用 --input-json <file>,扁平键值用 --input key=value。", recovery);
|
|
1454
|
+
}
|
|
1455
|
+
const fileInput = options.inputJson ? readInputJsonObject(options.inputJson, "service_action_invalid", recovery) : {};
|
|
1456
|
+
await runServicesAction(backend, serviceExecutionID, options.action, { ...fileInput, ...parseKeyValueList(options.input) }, {
|
|
1371
1457
|
...(options.actorType ? { actorType: options.actorType } : {}),
|
|
1372
1458
|
...(options.actorId ? { actorID: options.actorId } : {}),
|
|
1373
1459
|
...(options.status ? { status: options.status } : {}),
|
package/dist/src/state/config.js
CHANGED
|
@@ -15,7 +15,7 @@ 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.
|
|
18
|
+
export const CLI_VERSION = "2.1.9";
|
|
19
19
|
export const API_CONTRACT_REVISION = "sha256:66a95bbc5ec9d7b35747cf87206c63dd45983f26de21afab582d8dd63bf198f7";
|
|
20
20
|
const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
|
|
21
21
|
const CART_SESSION_FILENAME = "cart.json";
|
|
@@ -73,6 +73,27 @@ export class DeviceAuthority {
|
|
|
73
73
|
return { removed: true, agentTypes };
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
|
+
async resetDeviceKey() {
|
|
77
|
+
return withFileLock(`${this.statePath}.lock`, async () => {
|
|
78
|
+
const state = this.readState();
|
|
79
|
+
const removedBackends = state ? Object.keys(state.registrations).sort() : [];
|
|
80
|
+
// Delete the key first: if the process dies before the state write, the
|
|
81
|
+
// next run treats the missing key as a fresh install and completes the
|
|
82
|
+
// reset itself; it can never pair a new key with stale registrations.
|
|
83
|
+
if (existsSync(this.privateKeyPath)) {
|
|
84
|
+
try {
|
|
85
|
+
unlinkSync(this.privateKeyPath);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error.code !== "ENOENT") {
|
|
89
|
+
throw asDeviceStateError(error, "remove_private_key") ?? error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
this.writeState(emptyDeviceState());
|
|
94
|
+
return { removedBackends };
|
|
95
|
+
});
|
|
96
|
+
}
|
|
76
97
|
async prepareAuthorization() {
|
|
77
98
|
let state = this.readState() ?? emptyDeviceState();
|
|
78
99
|
const agentType = this.requestedAgentType;
|
|
@@ -83,8 +104,14 @@ export class DeviceAuthority {
|
|
|
83
104
|
if (!privateKey) {
|
|
84
105
|
const pair = generateKeyPairSync("ed25519");
|
|
85
106
|
privateKey = pair.privateKey;
|
|
86
|
-
|
|
107
|
+
// Persist an empty state before the new key ever reaches disk so a crash
|
|
108
|
+
// can never pair the new key with registrations bound to the old one.
|
|
109
|
+
// A crash between the two writes leaves either no key (fresh retry) or a
|
|
110
|
+
// key with no registrations (the backend re-attaches to the existing
|
|
111
|
+
// device instead of creating a duplicate).
|
|
87
112
|
state = emptyDeviceState();
|
|
113
|
+
this.writeState(state);
|
|
114
|
+
this.writePrivateKey(pair.privateKey.export({ format: "pem", type: "pkcs8" }).toString());
|
|
88
115
|
}
|
|
89
116
|
let registration = state.registrations[this.backendKey];
|
|
90
117
|
if (!registration && state.legacyRegistration) {
|
|
@@ -102,7 +129,21 @@ export class DeviceAuthority {
|
|
|
102
129
|
registration = await this.enroll(agentType, privateKey);
|
|
103
130
|
}
|
|
104
131
|
state.registrations[this.backendKey] = registration;
|
|
105
|
-
|
|
132
|
+
let session;
|
|
133
|
+
try {
|
|
134
|
+
session = await this.ensureRegistrationAgentType(registration, agentType, privateKey, false);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (!isUnknownDeviceRegistration(error))
|
|
138
|
+
throw error;
|
|
139
|
+
// The backend lost this registration (e.g. its identity store was
|
|
140
|
+
// rebuilt). Drop the stale record and enroll once: the same key either
|
|
141
|
+
// re-attaches to the surviving device or creates a fresh one.
|
|
142
|
+
delete state.registrations[this.backendKey];
|
|
143
|
+
registration = await this.enroll(agentType, privateKey);
|
|
144
|
+
state.registrations[this.backendKey] = registration;
|
|
145
|
+
session = await this.ensureRegistrationAgentType(registration, agentType, privateKey, false);
|
|
146
|
+
}
|
|
106
147
|
this.writeState(state);
|
|
107
148
|
return { state: registration, agentType, session, privateKey };
|
|
108
149
|
}
|
|
@@ -136,16 +177,23 @@ export class DeviceAuthority {
|
|
|
136
177
|
if (!publicJWK.x)
|
|
137
178
|
throw new Error("unable to export Ed25519 public key");
|
|
138
179
|
const publicKey = Buffer.from(publicJWK.x, "base64url").toString("base64");
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
180
|
+
try {
|
|
181
|
+
const started = await this.publicJSON("/v1/agent-device-enrollments", { public_key: publicKey, agent_type: agentType });
|
|
182
|
+
const proof = enrollmentProofMessage(started.agent_device_enrollment_id, started.challenge);
|
|
183
|
+
const verified = await this.publicJSON(`/v1/agent-device-enrollments/${encodeURIComponent(started.agent_device_enrollment_id)}/verify`, { challenge: started.challenge, signature: sign(null, Buffer.from(proof), privateKey).toString("base64") });
|
|
184
|
+
return {
|
|
185
|
+
deviceID: verified.agent_device_id,
|
|
186
|
+
deviceKeyID: verified.agent_device_key_id,
|
|
187
|
+
quotaLineageID: verified.quota_lineage_id,
|
|
188
|
+
agentInstances: { [verified.agent_type]: verified.agent_instance_id },
|
|
189
|
+
sessions: {},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (error instanceof DeviceAuthorizationError)
|
|
194
|
+
error.enrollmentFailed = true;
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
149
197
|
}
|
|
150
198
|
async ensureSession(state, agentType, privateKey, force = false) {
|
|
151
199
|
const existing = state.sessions[agentType];
|
|
@@ -233,6 +281,7 @@ export class DeviceAuthority {
|
|
|
233
281
|
export class DeviceAuthorizationError extends Error {
|
|
234
282
|
status;
|
|
235
283
|
code;
|
|
284
|
+
enrollmentFailed = false;
|
|
236
285
|
constructor(status, code, message) {
|
|
237
286
|
super(message);
|
|
238
287
|
this.status = status;
|
|
@@ -257,6 +306,9 @@ function emptyDeviceState() {
|
|
|
257
306
|
function canMovePastLegacyRegistration(error) {
|
|
258
307
|
return error instanceof DeviceAuthorizationError && (error.code === "agent_device_revoked" || error.status === 404);
|
|
259
308
|
}
|
|
309
|
+
function isUnknownDeviceRegistration(error) {
|
|
310
|
+
return error instanceof DeviceAuthorizationError && (error.code === "agent_device_not_found" || error.status === 404);
|
|
311
|
+
}
|
|
260
312
|
function normalizeBackendKey(value) {
|
|
261
313
|
const url = new URL(value);
|
|
262
314
|
url.search = "";
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"purpose": "Explain stable local identity, official Backend-scoped registrations, Agent Type instances, and bounded automatic session recovery.",
|
|
8
8
|
"when_to_use": [
|
|
9
9
|
"A different Agent window or runtime starts using an existing CLI installation.",
|
|
10
|
-
"The CLI reports device_state_unwritable, or the Backend reports agent_identity_required, agent_device_session_required, or agent_device_revoked.",
|
|
10
|
+
"The CLI reports device_state_unwritable, or the Backend reports agent_identity_required, agent_device_session_required, agent_device_not_found, or agent_device_revoked.",
|
|
11
11
|
"An operator confirms that the currently selected official Backend registration database was reset."
|
|
12
12
|
],
|
|
13
13
|
"identity_model": {
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
"On one HTTP 401 with code agent_device_session_required, it invalidates only the current Backend and Agent Type session, renews it, and retries the exact request once.",
|
|
22
22
|
"A second failure is returned to the Agent. The CLI does not loop, switch Agent Type, rotate the key, or create a replacement v2 device.",
|
|
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
|
+
"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. If enrollment verification keeps failing with internal_error, or returns agent_device_key_rotated or agent_device_key_conflict, `itpay device reset-key --confirm-key-reset` is the supported way to start over with a fresh key."
|
|
25
27
|
],
|
|
26
28
|
"local_state_recovery": [
|
|
27
29
|
"Device state updates use an atomic directory lock and owner-only atomic file replacement; no lock file is opened with fs.open or fs.openSync.",
|
|
@@ -44,6 +46,11 @@
|
|
|
44
46
|
"intent": "recover after an operator-confirmed reset of the selected official Backend registration database",
|
|
45
47
|
"command": "itpay --agent-type <agent_type> device recover --confirm-backend-reset --json",
|
|
46
48
|
"success_signal": "only the selected Backend registration is removed and the private key and other Backend registration remain"
|
|
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"
|
|
47
54
|
}
|
|
48
55
|
],
|
|
49
56
|
"agent_rules": [
|
|
@@ -26,7 +26,11 @@
|
|
|
26
26
|
"travel_date": "Travel date in YYYY-MM-DD. The field name must be exactly travel_date; aliases like date are rejected."
|
|
27
27
|
},
|
|
28
28
|
"optional": "None. additionalProperties is false — extra fields are rejected by the provider.",
|
|
29
|
-
"example": {
|
|
29
|
+
"example": {
|
|
30
|
+
"origin": "古镇",
|
|
31
|
+
"destination": "广州南",
|
|
32
|
+
"travel_date": "2026-09-19"
|
|
33
|
+
}
|
|
30
34
|
},
|
|
31
35
|
"itpay-rail-smart": {
|
|
32
36
|
"required": {
|
|
@@ -44,7 +48,12 @@
|
|
|
44
48
|
"priority": "One of balanced | fastest | cheapest | safest | flexible.",
|
|
45
49
|
"max_transfers": "Integer 0-2, allowed transfer count."
|
|
46
50
|
},
|
|
47
|
-
"example": {
|
|
51
|
+
"example": {
|
|
52
|
+
"origin": "中山古镇",
|
|
53
|
+
"destination": "广州南",
|
|
54
|
+
"travel_date": "2026-09-19",
|
|
55
|
+
"priority": "fastest"
|
|
56
|
+
}
|
|
48
57
|
},
|
|
49
58
|
"itpay-rail-booking": {
|
|
50
59
|
"preferred": {
|
|
@@ -78,7 +87,8 @@
|
|
|
78
87
|
"services start <service> --json returns input_required plus the guidance block for rail services.",
|
|
79
88
|
"Submit input with services run <service> --execution <id> --input-json <file> --json on the SAME execution.",
|
|
80
89
|
"itpay-rail-smart may stop at a location confirmation step; show the candidate places to the buyer, then continue the same execution after the human confirms.",
|
|
81
|
-
"After the buyer explicitly picks a train/seat, run itpay-rail-booking with the selection token (preferred) or explicit legs.",
|
|
90
|
+
"After the buyer explicitly picks a train/seat, run itpay-rail-booking with the selection token (preferred) or explicit legs. The execution then pauses at the booking review (workflow:confirm_booking): services next returns booking_review_required with human_action.context.review carrying draft_revision, legs, seat_options and notice_version.",
|
|
91
|
+
"Collect the review input and submit it with services action <id> --action workflow:confirm_booking --actor-type human --status approved --input-json <file> --json. Required fields: draft_revision (exact server value), passengers, seat_type, seat_preferences (one entry per passenger_index 0..n-1, values limited to that leg's seat_options), party_preference, fallback=automatic_assignment, notice_version, accept_non_guaranteed=true. Only after an accepted review does the server quote and offer checkout.",
|
|
82
92
|
"The booking execution reaches a protected checkout: passenger identity data is entered by the human on that page, never through the CLI. Account authentication may happen inside checkout — do not force a separate auth login first (Path B).",
|
|
83
93
|
"Payment success does not mean the ticket is issued. Issuance is asynchronous; keep reading the same execution with services next.",
|
|
84
94
|
"If seat details are still syncing after issuance, advise the buyer to verify the itinerary on 12306 instead of repeating the query."
|
|
@@ -89,25 +99,67 @@
|
|
|
89
99
|
"search": "Supplier query failed: most commonly a wrong field name (must be travel_date) or a nonexistent station name; recheck input against this doc.",
|
|
90
100
|
"catalog_or_recommend": "Location resolved but no feasible trains: change the date or stations.",
|
|
91
101
|
"login_required_is_not_failure": "login_required / rate_limited pause the execution; they are NOT failures and never justify a replacement execution.",
|
|
92
|
-
"general_rule": "Never replay a failed execution and never guess different field names; correct the input and start a new execution of the same service."
|
|
102
|
+
"general_rule": "Never replay a failed execution and never guess different field names; correct the input and start a new execution of the same service.",
|
|
103
|
+
"booking_review_changed": "The submitted draft_revision is stale: re-read services next for the current review projection and resubmit the full input; do not bump the revision yourself."
|
|
93
104
|
},
|
|
94
105
|
"seat_preferences": {
|
|
95
106
|
"meaning": "A buyer's seat-letter preference may be planned per leg before payment. The plan is submitted to the supplier when encodable; unsupported combinations fall back to automatic assignment.",
|
|
96
107
|
"letters": "Evidence-backed letters only: O=ABCDF (second class), M=ACDF (first class), 9=ACF (business class). Other layouts auto-assign.",
|
|
97
|
-
"boundary": "Never claim a preference was fulfilled before supplier confirmation; check the issued seat in the order/status output."
|
|
108
|
+
"boundary": "Never claim a preference was fulfilled before supplier confirmation; check the issued seat in the order/status output.",
|
|
109
|
+
"review_contract": "Submitted inside the confirm_booking action via --input-json (nested JSON; --input key=value cannot express it and must not be combined with it). One preference per passenger slot, passenger_index 0-based. draft_revision must equal the server projection; a stale revision returns booking_review_changed — re-read services next and resubmit, never increment locally.",
|
|
110
|
+
"revision": "While payment has not started, the same execution accepts a revised review: resubmit confirm_booking with the new draft_revision; the old unpaid quote/checkout is retired and re-quoted. After a payment attempt the review is closed — never create a replacement purchase."
|
|
98
111
|
},
|
|
99
112
|
"agent_rules": [
|
|
100
113
|
"Pick the service by the buyer's input precision: exact station names → itpay-rail-exact; anything fuzzier → itpay-rail-smart; explicit purchase intent after a selection → itpay-rail-booking.",
|
|
101
114
|
"Never invent input field names. If the guidance block or this doc does not list a field, do not send it.",
|
|
102
115
|
"Never ask the buyer for passenger names, ID numbers, or phone numbers in chat; those are collected only on the protected checkout page.",
|
|
103
116
|
"Never reconstruct provider-shaped legs JSON when a booking_offer.selection_token is available; submit the token plus passengers.",
|
|
117
|
+
"Seat preferences are requests, not guarantees: always repeat the notice that automatic assignment remains possible, and never claim a specific seat before the issued order shows it.",
|
|
118
|
+
"Take draft_revision only from the current services next review projection; on booking_review_changed re-read instead of retrying the stale value.",
|
|
104
119
|
"login_required and rate_limited are pauses, not failures: resume the same execution, never spawn a replacement.",
|
|
105
120
|
"Do not repeat or replay a failed execution; correct the input and start a new one.",
|
|
106
121
|
"Do not claim payment equals ticket issuance; issuance is asynchronous and confirmed only by the issued seat status."
|
|
107
122
|
],
|
|
108
123
|
"human_language": {
|
|
109
|
-
"prefer": [
|
|
110
|
-
|
|
124
|
+
"prefer": [
|
|
125
|
+
"车次",
|
|
126
|
+
"出发站",
|
|
127
|
+
"到达站",
|
|
128
|
+
"出行日期",
|
|
129
|
+
"座位",
|
|
130
|
+
"余票",
|
|
131
|
+
"出票中",
|
|
132
|
+
"已出票",
|
|
133
|
+
"登录后继续免费查询"
|
|
134
|
+
],
|
|
135
|
+
"avoid": [
|
|
136
|
+
"workflow",
|
|
137
|
+
"execution",
|
|
138
|
+
"provider",
|
|
139
|
+
"seat_source",
|
|
140
|
+
"candidate_id",
|
|
141
|
+
"token",
|
|
142
|
+
"quota"
|
|
143
|
+
]
|
|
111
144
|
},
|
|
112
|
-
"search_terms": [
|
|
145
|
+
"search_terms": [
|
|
146
|
+
"rail",
|
|
147
|
+
"railway",
|
|
148
|
+
"train",
|
|
149
|
+
"ticket",
|
|
150
|
+
"火车",
|
|
151
|
+
"火车票",
|
|
152
|
+
"高铁",
|
|
153
|
+
"车票",
|
|
154
|
+
"车次",
|
|
155
|
+
"seat",
|
|
156
|
+
"booking",
|
|
157
|
+
"rail-booking"
|
|
158
|
+
],
|
|
159
|
+
"notifications": {
|
|
160
|
+
"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.",
|
|
161
|
+
"status_read": "The order read exposes masked per-leg notification entries (keys: issued, details_pending, seat_ready) with channel, status and masked recipient — never raw numbers.",
|
|
162
|
+
"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.",
|
|
163
|
+
"unavailable": "If SMS is not configured or a recipient is unverified, email still satisfies the contact policy — never present an unverified phone as enabled."
|
|
164
|
+
}
|
|
113
165
|
}
|
|
@@ -87,6 +87,7 @@ Terminal and error states:
|
|
|
87
87
|
## Rules
|
|
88
88
|
|
|
89
89
|
- The human's phone number and SMS code are entered only on the official page — never through chat or CLI input. A `buyer_id`, a typed phone string, or a passenger phone is **not** proof of phone verification.
|
|
90
|
+
- Verified email OR verified mainland phone both establish a valid login; the user picks on the page. Phone verification is not a prerequisite for registered free search — a verified email alone is sufficient.
|
|
90
91
|
- Login never grants Vault content, passenger history, or another Agent's authority. Other devices/platforms enroll and authorize separately.
|
|
91
92
|
- After `authenticated`, server-side identity is re-read; an anonymous execution blocked at the quota step resumes with `services run <service> --execution <id> --json` — no new execution, no re-asked itinerary.
|
|
92
93
|
- Refusing or cancelling login deletes nothing: saved results and purchase options remain usable.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## 范围
|
|
6
6
|
|
|
7
|
-
`itpay device` 只显示该命令组的帮助并退出,不访问 Backend
|
|
7
|
+
`itpay device` 只显示该命令组的帮助并退出,不访问 Backend、不读取或修改身份。当前子命令是 `recover`(运营确认 Backend 重置后删除本地登记)和 `reset-key`(服务端拒绝当前私钥登记时放弃本地密钥)。
|
|
8
8
|
|
|
9
9
|
仅在运营明确确认当前 Backend 的 Device 登记数据库已重建或清空后,删除本地该 Backend 的 v2 registration:
|
|
10
10
|
|
|
@@ -24,6 +24,18 @@ Type 或执行 `device recover`;应保留身份并重试原命令一次,持
|
|
|
24
24
|
|
|
25
25
|
缺少确认参数返回 `backend_reset_confirmation_required`。普通 session 失效由 CLI 自动续期;revoked、quota、权限或未知 Backend 故障不得使用本命令。所有 Agent Type 使用相同输入和输出合同。
|
|
26
26
|
|
|
27
|
+
## `itpay device reset-key`
|
|
28
|
+
|
|
29
|
+
仅在服务端拒绝以当前私钥完成设备登记时使用(登记阶段持续返回 `internal_error`,或返回 `agent_device_key_rotated` / `agent_device_key_conflict`)。这是本地操作,不访问 Backend:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
itpay device reset-key --confirm-key-reset --json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
命令删除本地 Ed25519 私钥并清空所有 Backend 的本地登记记录;下一次需要设备身份的命令会以全新密钥重新登记为新设备。服务端旧设备记录保留为孤儿,不会被删除或复用;原设备的额度谱系不迁移。先尝试普通重试——Backend 会把已验证私钥的重复登记幂等挂回原设备,只有在该修复不可用或私钥已被服务端轮换/冲突时才需要本命令。
|
|
36
|
+
|
|
37
|
+
缺少确认参数返回 `key_reset_confirmation_required`;本地文件操作失败返回 `device_key_reset_failed`。本命令不得删除 Cart、operation journal 或 `~/.itpay-v3` 的其他内容。
|
|
38
|
+
|
|
27
39
|
## 参数
|
|
28
40
|
|
|
29
41
|
| 参数 | 必填 | 说明 |
|
|
@@ -16,9 +16,15 @@ itpay services action <service_execution_id> --action <action_type>
|
|
|
16
16
|
[--actor-type <actor_type>] [--actor-id <actor_id>]
|
|
17
17
|
[--status <pending|approved|rejected|expired|cancelled>]
|
|
18
18
|
[--candidate <rank> | --result-item <result_item_id>]
|
|
19
|
-
[--required-before <step>] [--input <key=value> ...]
|
|
19
|
+
[--required-before <step>] [--input <key=value> ...]
|
|
20
|
+
[--input-json <file>] [--json]
|
|
20
21
|
```
|
|
21
22
|
|
|
23
|
+
| 选项 | 说明 |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `--input <key=value>` | 逐项提交 action input;值可为 JSON 字面量(对象/数组/布尔/数字),可重复。 |
|
|
26
|
+
| `--input-json <file>` | UTF-8 JSON 文件;顶层必须是 object,整体作为 action input;文件不超过 256 KiB。适合嵌套结构(如订票确认的 `seat_preferences`)。**与 `--input` 互斥**:同时给出时在发起请求前返回 `service_action_invalid`。文件不存在、非合法 JSON 或顶层非 object 同样在本地报结构化错误,不发生网络写。 |
|
|
27
|
+
|
|
22
28
|
普通 Agent 优先使用 `--candidate <rank>`。CLI 只从当前 Execution 的 `current_result_items` 解析 Result Item ID;Backend 再读取权威 Invocation 和 Stable Hash。Agent 不提交 Hash,也不能使用其他 Execution 或外部来源的候选。`--result-item` 只用于已持有当前 Execution 内部句柄的受控恢复,不应要求用户提供。
|
|
23
29
|
|
|
24
30
|
## 标准输出
|
|
@@ -45,6 +51,8 @@ itpay services action <service_execution_id> --action <action_type>
|
|
|
45
51
|
|
|
46
52
|
rank 不存在、属于旧结果集或其他 Execution、action 不允许、status 非法时均不写 action;返回结构化错误并且只引导同一 Execution 的 `services next`。不得新建 Execution、重新 invoke 或构造候选 ID。相同候选重试幂等;同一结果集改选另一个候选返回冲突,不覆盖已批准事实。
|
|
47
53
|
|
|
54
|
+
订票类 `workflow:confirm_booking` 动作要求嵌套输入(`passengers`、`seat_type`、逐乘客 `seat_preferences`、`draft_revision`、`notice_version` 等),一律经 `--input-json <file>` 提交完整 JSON 对象。`draft_revision` 必须等于 `services next` 投影的 `context.review.draft_revision`;服务端拒绝 `booking_review_changed` 时重新读取后重试,不得自行递增。乘客姓名、证件号等身份信息不在此提交,仍由受保护 Checkout 页收集。
|
|
55
|
+
|
|
48
56
|
## Agent Type / Host
|
|
49
57
|
|
|
50
58
|
所有正式支持的 Local Agent Type 行为相同。需要人确认时 instruction 必须明确“先询问用户”,不能因 Desktop Host 自动代替用户选择。
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
为单个 Service Execution 快速创建或恢复 Checkout,并按 Host 向人交接付款入口。它是 `services quote -> cart add --quote -> buy --cart` 的单项快捷方式,必须复用相同的 Quote、Cart 和 Checkout Use Case。
|
|
8
8
|
|
|
9
|
-
未注册/未登录用户不要求先执行 `itpay auth login`(Path B):官方 checkout
|
|
9
|
+
未注册/未登录用户不要求先执行 `itpay auth login`(Path B):官方 checkout 页内完成必要的钱包认证、创建或复用账号并取得页面权限;人在页面上确认旅客、通知联系方式和最终报价后付款。支付确认沿用既有设备绑定机制,不再独立绑定。checkout 内完成账号认证不等于账号已通过手机号验证;钱包显示名、账号手机号与乘车人身份互相独立。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**通知联系方式(铁路单):** 订单级联系策略要求至少一个可用通道——邮箱,或在 checkout 页内经验证的大陆手机号(order-contact 验证码只校验收件人,不创建/合并登录身份)。有效邮箱即可独立完成 checkout;SMS 不是必填项,SMS 能力未配置时不得阻塞邮箱通道。该策略仅适用于铁路订单,企知道等既有服务的联系策略不变。乘客证件手机号、账号登录手机号与通知收件人是三个独立事实,互不替代。
|
|
12
|
+
|
|
13
|
+
**上游:** `services next` 返回的 `prepare_quote` capability 和已验证输入。铁路购票执行必须先通过 `workflow:confirm_booking` review(见 `services next` 的 `booking_review_required`),后端才生成可支付报价;review 开放期间不创建 Checkout。
|
|
12
14
|
**下游:** 人完成 Checkout,随后 `checkout` 或 `services next`。
|
|
13
15
|
|
|
14
16
|
## 语法与参数
|
|
@@ -57,6 +57,8 @@ itpay services get <service_execution_id> [--json]
|
|
|
57
57
|
|
|
58
58
|
`timeline` 只保留最近 20 个公开节点;超过时增加 `timeline_truncated: true`。它不返回 event ID 或 `redacted_summary`。交付存在时可增加 `delivery_mode`;退款锁存在时增加 `access_locked` 和退款 ID/status,并优先返回锁定 instruction。
|
|
59
59
|
|
|
60
|
+
订票类 Execution 的 `workflow:confirm_booking` 待确认/待修订状态不在 timeline 中展开细节——行程、`seat_options` 与当前 `draft_revision` 一律通过 `services next` 的 `booking_review_required` 投影读取;`get` 只用于确认执行仍处于可继续状态。
|
|
61
|
+
|
|
60
62
|
## 异常处理
|
|
61
63
|
|
|
62
64
|
默认不返回原始 events、Provider metadata、内部 bindings、Arazzo workflow、capability schema、candidate hash、client context 或完整 payload。需要完整但仍经服务端脱敏的事件时才运行 `services events`。
|
|
@@ -249,6 +249,35 @@ itpay services run itpay-rail-booking --input-json booking.json --json
|
|
|
249
249
|
|
|
250
250
|
`booking_offer` 只是受条件约束的可选承接:查询完成本身已满足查询目标,不自动购买;过期 token 返回明确错误,恢复方式是按 `services run` 输入合同重新查询或显式提供 `legs`。
|
|
251
251
|
|
|
252
|
+
## 订票确认(booking review)
|
|
253
|
+
|
|
254
|
+
`itpay-rail-booking` 在报价前停在 `workflow:confirm_booking` 人工确认节点。`services next` 返回 `booking_review_required`,`human_action.context.review` 携带服务端投影:`draft_revision`、`legs`(含可编码的 `seat_options`)、`notice_version` 等。乘客身份信息永不在此投影,也不经 CLI 提交。
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{
|
|
258
|
+
"status": "booking_review_required",
|
|
259
|
+
"result": {
|
|
260
|
+
"service_execution_id": "<id>",
|
|
261
|
+
"service_id": "itpay-rail-booking",
|
|
262
|
+
"human_action": {
|
|
263
|
+
"action_type": "workflow:confirm_booking",
|
|
264
|
+
"context": { "review": { "draft_revision": 1, "legs": [], "seat_options": {} } }
|
|
265
|
+
},
|
|
266
|
+
"required_fields": ["draft_revision", "passengers", "seat_type", "seat_preferences", "party_preference", "fallback", "notice_version", "accept_non_guaranteed"]
|
|
267
|
+
},
|
|
268
|
+
"instruction": "向用户展示行程与可选席别/座位偏好词表,并明确告知:座位偏好仅为请求、不保证分配。收集后经 --input-json 提交完整 JSON;draft_revision 以服务端当前值为准。",
|
|
269
|
+
"next": { "command": "itpay services action <id> --action workflow:confirm_booking --actor-type human --status approved --input-json <file> --json", "reason": "提交用户确认后的行程确认/修订" },
|
|
270
|
+
"recovery": [{ "command": "itpay services next <id> --json", "reason": "重新读取当前 draft_revision 后重试" }]
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
规则:
|
|
275
|
+
|
|
276
|
+
- `seat_preferences` 逐乘客一条,`passenger_index` 从 0 起、人数 1–5、取值限该席别 `seat_options`;`party_preference`、`fallback=automatic_assignment`、`notice_version`、`accept_non_guaranteed=true` 全部必填。
|
|
277
|
+
- `draft_revision` 必须等于投影值;服务端返回 `booking_review_changed` 时重新 `services next` 读取后重试,不得自行递增或重放旧 revision。
|
|
278
|
+
- 付款暂停且 review 仍开放时,`next` 指向 Checkout 恢复,`recovery` 附带修订命令(`services action … confirm_booking --input-json`)——修订回到同一执行重新报价,不产生第二笔订单或付款。
|
|
279
|
+
- 支付已在途/完成后 review 关闭,修订被拒绝;按返回的同一订单状态继续,不得新建执行。
|
|
280
|
+
|
|
252
281
|
## 异常处理
|
|
253
282
|
|
|
254
283
|
execution 不存在或不属于当前设备/账号时返回错误信封,并仅建议:
|
|
@@ -21,7 +21,7 @@ itpay services run <service_id>
|
|
|
21
21
|
| 参数 | 说明 |
|
|
22
22
|
| --- | --- |
|
|
23
23
|
| `service_id` | Catalog 返回的服务 ID。恢复时必须与 Execution 所属服务一致。 |
|
|
24
|
-
| `--input-json` | UTF-8 JSON 文件;顶层必须是 object,并符合服务发布的 input schema
|
|
24
|
+
| `--input-json` | UTF-8 JSON 文件;顶层必须是 object,并符合服务发布的 input schema;文件不超过 256 KiB。文件不可读、非合法 JSON 或顶层非 object 时在本地报 `workflow_input_invalid`,不发生网络写。 |
|
|
25
25
|
| `--execution` | 恢复同一 Execution。不得用它切换服务或绕过原状态。 |
|
|
26
26
|
| `--timeout` | 本次等待 workflow 状态变化的秒数,默认 120,范围 0–600;超时只返回当前状态。 |
|
|
27
27
|
| `--host` / `--target` | Host 展示上下文,不是业务输入或 Buyer 身份。 |
|