657 lines
22 KiB
TypeScript
Executable File
657 lines
22 KiB
TypeScript
Executable File
#!/usr/bin/env bun
|
|
|
|
const SCHEMA_VERSION = "tjwater-cli/v1";
|
|
const DEFAULT_SERVER = "http://192.168.1.114:8000";
|
|
const DEFAULT_TIMEOUT = 180;
|
|
|
|
const GROUPS = {
|
|
network: "管网节点、管线等基础属性查询命令。",
|
|
component: "组件选项与配置读取命令。",
|
|
simulation: "模拟运行与调度相关命令。",
|
|
analysis: "分析计算与诊断相关命令。",
|
|
data: "时序、SCADA 和方案数据查询命令。",
|
|
};
|
|
|
|
const NETWORK_COMMANDS = {
|
|
"get-junction-properties": {
|
|
summary: "读取节点属性成功",
|
|
path: "/getjunctionproperties/",
|
|
options: { junction: { required: true } },
|
|
params: ({ args, ctx }) => ({ network: requireNetwork(ctx), junction: args.junction }),
|
|
},
|
|
"get-pipe-properties": {
|
|
summary: "读取管道属性成功",
|
|
path: "/getpipeproperties/",
|
|
options: { pipe: { required: true } },
|
|
params: ({ args, ctx }) => ({ network: requireNetwork(ctx), pipe: args.pipe }),
|
|
},
|
|
"get-all-pipes-properties": {
|
|
summary: "读取全部管道属性成功",
|
|
path: "/getallpipeproperties/",
|
|
params: ({ ctx }) => ({ network: requireNetwork(ctx) }),
|
|
},
|
|
"get-reservoir-properties": {
|
|
summary: "读取水库属性成功",
|
|
path: "/getreservoirproperties/",
|
|
options: { reservoir: { required: true } },
|
|
params: ({ args, ctx }) => ({ network: requireNetwork(ctx), reservoir: args.reservoir }),
|
|
},
|
|
"get-all-reservoirs-properties": {
|
|
summary: "读取全部水库属性成功",
|
|
path: "/getallreservoirproperties/",
|
|
params: ({ ctx }) => ({ network: requireNetwork(ctx) }),
|
|
},
|
|
"get-tank-properties": {
|
|
summary: "读取水箱属性成功",
|
|
path: "/gettankproperties/",
|
|
options: { tank: { required: true } },
|
|
params: ({ args, ctx }) => ({ network: requireNetwork(ctx), tank: args.tank }),
|
|
},
|
|
"get-all-tanks-properties": {
|
|
summary: "读取全部水箱属性成功",
|
|
path: "/getalltankproperties/",
|
|
params: ({ ctx }) => ({ network: requireNetwork(ctx) }),
|
|
},
|
|
"get-pump-properties": {
|
|
summary: "读取水泵属性成功",
|
|
path: "/getpumpproperties/",
|
|
options: { pump: { required: true } },
|
|
params: ({ args, ctx }) => ({ network: requireNetwork(ctx), pump: args.pump }),
|
|
},
|
|
"get-all-pumps-properties": {
|
|
summary: "读取全部水泵属性成功",
|
|
path: "/getallpumpproperties/",
|
|
params: ({ ctx }) => ({ network: requireNetwork(ctx) }),
|
|
},
|
|
"get-valve-properties": {
|
|
summary: "读取阀门属性成功",
|
|
path: "/getvalveproperties/",
|
|
options: { valve: { required: true } },
|
|
params: ({ args, ctx }) => ({ network: requireNetwork(ctx), valve: args.valve }),
|
|
},
|
|
"get-all-valves-properties": {
|
|
summary: "读取全部阀门属性成功",
|
|
path: "/getallvalveproperties/",
|
|
params: ({ ctx }) => ({ network: requireNetwork(ctx) }),
|
|
},
|
|
};
|
|
|
|
const DATA_REALTIME_COMMANDS = {
|
|
links: {
|
|
summary: "读取实时管道数据成功",
|
|
path: "/realtime/links",
|
|
options: { "start-time": { required: true }, "end-time": { required: true } },
|
|
requireProject: true,
|
|
params: ({ args }) => ({
|
|
start_time: parseTime(args["start-time"], "--start-time"),
|
|
end_time: parseTime(args["end-time"], "--end-time"),
|
|
}),
|
|
},
|
|
nodes: {
|
|
summary: "读取实时节点数据成功",
|
|
path: "/realtime/nodes",
|
|
options: { "start-time": { required: true }, "end-time": { required: true } },
|
|
requireProject: true,
|
|
params: ({ args }) => ({
|
|
start_time: parseTime(args["start-time"], "--start-time"),
|
|
end_time: parseTime(args["end-time"], "--end-time"),
|
|
}),
|
|
},
|
|
"simulation-by-id-time": {
|
|
summary: "读取实时模拟数据成功",
|
|
path: "/realtime/query/by-id-time",
|
|
options: { id: { required: true }, type: { required: true }, time: { required: true } },
|
|
requireProject: true,
|
|
params: ({ args }) => ({
|
|
id: args.id,
|
|
type: args.type,
|
|
query_time: parseTime(args.time, "--time"),
|
|
}),
|
|
},
|
|
"simulation-by-time-property": {
|
|
summary: "读取实时属性聚合数据成功",
|
|
path: "/realtime/query/by-time-property",
|
|
options: { type: { required: true }, time: { required: true }, property: { required: true } },
|
|
requireProject: true,
|
|
params: ({ args }) => ({
|
|
type: args.type,
|
|
query_time: parseTime(args.time, "--time"),
|
|
property: args.property,
|
|
}),
|
|
},
|
|
};
|
|
|
|
const DATA_SCHEME_COMMANDS = {
|
|
list: {
|
|
summary: "读取方案列表成功",
|
|
path: "/schemes",
|
|
requireProject: true,
|
|
params: ({ args }) => maybe({ scheme_type: args["scheme-type"] }),
|
|
},
|
|
get: {
|
|
summary: "读取方案详情成功",
|
|
path: ({ positionals }) => `/schemes/${encodeURIComponent(requirePos(positionals, 0, "scheme name"))}`,
|
|
requireProject: true,
|
|
params: ({ args }) => maybe({ scheme_type: args["scheme-type"] }),
|
|
},
|
|
links: {
|
|
summary: "读取方案管道数据成功",
|
|
path: "/scheme/links",
|
|
options: { "start-time": { required: true }, "end-time": { required: true } },
|
|
requireProject: true,
|
|
params: ({ args, ctx }) => ({
|
|
scheme_name: resolveScheme(ctx, args.scheme, true),
|
|
scheme_type: args["scheme-type"] ?? "simulation",
|
|
start_time: parseTime(args["start-time"], "--start-time"),
|
|
end_time: parseTime(args["end-time"], "--end-time"),
|
|
}),
|
|
},
|
|
};
|
|
|
|
const DATA_SCADA_COMMANDS = {
|
|
list: {
|
|
summary: "读取 SCADA 元数据成功",
|
|
path: "/scada/devices",
|
|
requireProject: true,
|
|
params: ({ args }) => maybe({ kind: args.kind }),
|
|
},
|
|
query: {
|
|
summary: "读取 SCADA 时序成功",
|
|
path: ({ args }) => (args.field ? "/scada/by-ids-field-time-range" : "/scada/by-ids-time-range"),
|
|
options: { "device-id": { required: true, repeated: true }, "start-time": { required: true }, "end-time": { required: true } },
|
|
requireProject: true,
|
|
params: ({ args }) => ({
|
|
device_ids: toArray(args["device-id"]).join(","),
|
|
start_time: parseTime(args["start-time"], "--start-time"),
|
|
end_time: parseTime(args["end-time"], "--end-time"),
|
|
...maybe({ field: args.field }),
|
|
}),
|
|
},
|
|
};
|
|
|
|
async function main() {
|
|
const parsed = parseArgs(Bun.argv.slice(2));
|
|
if (parsed.help || parsed.command.length === 0) {
|
|
emitHelp(parsed.command);
|
|
return 0;
|
|
}
|
|
if (parsed.command[0] === "help") {
|
|
emitHelp(parsed.command.slice(1));
|
|
return 0;
|
|
}
|
|
|
|
const ctx = await buildContext(parsed.global);
|
|
const startedAt = Date.now();
|
|
const result = await dispatch(parsed, ctx);
|
|
const data = await requestJson(ctx, result);
|
|
emitSuccess({
|
|
summary: result.summary,
|
|
data,
|
|
ctx,
|
|
durationMs: Date.now() - startedAt,
|
|
nextCommands: result.nextCommands,
|
|
});
|
|
return 0;
|
|
}
|
|
|
|
async function dispatch(parsed, ctx) {
|
|
const [group, ...rest] = parsed.command;
|
|
if (group === "network") {
|
|
return commandFromMap(NETWORK_COMMANDS, rest, parsed, ctx);
|
|
}
|
|
if (group === "component" && rest[0] === "option") {
|
|
return componentOption(rest.slice(1), parsed, ctx);
|
|
}
|
|
if (group === "simulation" && rest[0] === "run") {
|
|
requireOptions(parsed.args, { "start-time": { required: true }, duration: { required: true } });
|
|
const start = parseTime(parsed.args["start-time"], "--start-time");
|
|
const duration = Number(parsed.args.duration);
|
|
const endTime = new Date(Date.parse(start) + duration * 60_000).toISOString();
|
|
return {
|
|
summary: "触发模拟成功",
|
|
method: "POST",
|
|
path: "/runsimulationmanuallybydate/",
|
|
body: { name: requireNetwork(ctx), start_time: start, duration },
|
|
nextCommands: [
|
|
`tjwater-cli data timeseries realtime links --start-time ${start} --end-time ${endTime}`,
|
|
`tjwater-cli data timeseries realtime nodes --start-time ${start} --end-time ${endTime}`,
|
|
],
|
|
};
|
|
}
|
|
if (group === "analysis") {
|
|
return analysis(rest, parsed, ctx);
|
|
}
|
|
if (group === "data") {
|
|
return dataCommand(rest, parsed, ctx);
|
|
}
|
|
throw cliError("未找到命令", "COMMAND_NOT_FOUND", `unknown command: ${parsed.command.join(" ")}`, 2, {
|
|
next_commands: ["tjwater-cli help"],
|
|
});
|
|
}
|
|
|
|
function commandFromMap(map, commandPath, parsed, ctx) {
|
|
const name = commandPath[0];
|
|
const spec = map[name];
|
|
if (!spec) {
|
|
throw cliError("未找到命令", "COMMAND_NOT_FOUND", `unknown command: ${parsed.command.join(" ")}`, 2, {
|
|
next_commands: ["tjwater-cli help"],
|
|
});
|
|
}
|
|
requireOptions(parsed.args, spec.options ?? {});
|
|
const positionals = [...commandPath.slice(1), ...parsed.positionals];
|
|
return {
|
|
summary: spec.summary,
|
|
method: spec.method ?? "GET",
|
|
path: valueOf(spec.path, { args: parsed.args, positionals, ctx }),
|
|
params: spec.params?.({ args: parsed.args, positionals, ctx }) ?? {},
|
|
body: spec.body?.({ args: parsed.args, positionals, ctx }),
|
|
requireProject: spec.requireProject,
|
|
nextCommands: spec.nextCommands,
|
|
};
|
|
}
|
|
|
|
function componentOption(rest, parsed, ctx) {
|
|
const action = rest[0];
|
|
if (action !== "schema" && action !== "get") {
|
|
throw cliError("未找到命令", "COMMAND_NOT_FOUND", "unknown component option command", 2);
|
|
}
|
|
requireOptions(parsed.args, { kind: { required: true } });
|
|
const kind = parsed.args.kind;
|
|
const routes = {
|
|
"time:schema": "/gettimeschema",
|
|
"time:get": "/gettimeproperties/",
|
|
"energy:schema": "/getenergyschema/",
|
|
"energy:get": "/getenergyproperties/",
|
|
"pump-energy:schema": "/getpumpenergyschema/",
|
|
"pump-energy:get": "/getpumpenergyproperties//",
|
|
"network:schema": "/getoptionschema/",
|
|
"network:get": "/getoptionproperties/",
|
|
};
|
|
const path = routes[`${kind}:${action}`];
|
|
if (!path) {
|
|
throw cliError("CLI 参数错误", "INVALID_KIND", "--kind must be one of time, energy, pump-energy, network", 2);
|
|
}
|
|
if (kind === "pump-energy" && action === "get" && !parsed.args.pump) {
|
|
throw cliError("CLI 参数错误", "PUMP_REQUIRED", "--pump is required when --kind pump-energy", 2);
|
|
}
|
|
return {
|
|
summary: action === "schema" ? "读取选项 schema 成功" : "读取选项属性成功",
|
|
method: "GET",
|
|
path,
|
|
params: { network: requireNetwork(ctx), ...maybe({ pump: parsed.args.pump }) },
|
|
};
|
|
}
|
|
|
|
function analysis(rest, parsed, ctx) {
|
|
const [domain, sub, leaf] = rest;
|
|
if (domain === "age") {
|
|
requireOptions(parsed.args, { "start-time": { required: true }, duration: { required: true } });
|
|
return {
|
|
summary: "水龄分析执行成功",
|
|
method: "GET",
|
|
path: "/age_analysis/",
|
|
params: {
|
|
network: requireNetwork(ctx),
|
|
start_time: parseTime(parsed.args["start-time"], "--start-time"),
|
|
duration: Number(parsed.args.duration),
|
|
},
|
|
};
|
|
}
|
|
if (domain === "leakage" && sub === "schemes") {
|
|
return schemesCommand("漏损", "/leakage/schemes", leaf, rest[3], parsed, ctx);
|
|
}
|
|
if (domain === "burst-detection" && sub === "schemes") {
|
|
return schemesCommand("爆管检测", "/burst-detection/schemes", leaf, rest[3], parsed, ctx);
|
|
}
|
|
if (domain === "leakage" && sub === "identify") {
|
|
return detectionCommand("漏损识别执行成功", "/leakage/identify/", parsed, ctx);
|
|
}
|
|
if (domain === "burst-detection" && sub === "detect") {
|
|
return detectionCommand("爆管检测执行成功", "/burst-detection/detect/", parsed, ctx);
|
|
}
|
|
if (domain === "sensor-placement" && sub === "kmeans") {
|
|
requireOptions(parsed.args, { count: { required: true } });
|
|
return {
|
|
summary: "传感器选址执行成功",
|
|
method: "POST",
|
|
path: "/pressure_sensor_placement_kmeans/",
|
|
body: {
|
|
name: requireNetwork(ctx),
|
|
scheme_name: resolveScheme(ctx, parsed.args.scheme, true),
|
|
sensor_number: Number(parsed.args.count),
|
|
min_diameter: Number(parsed.args["min-diameter"] ?? 0),
|
|
username: requireUsername(ctx),
|
|
},
|
|
};
|
|
}
|
|
throw cliError("未找到命令", "COMMAND_NOT_FOUND", `unknown analysis command: ${rest.join(" ")}`, 2);
|
|
}
|
|
|
|
function schemesCommand(label, basePath, action, inlineName, parsed, ctx) {
|
|
if (action === "list") {
|
|
return {
|
|
summary: `读取${label}方案列表成功`,
|
|
method: "GET",
|
|
path: `${basePath}/`,
|
|
params: { network: requireNetwork(ctx) },
|
|
};
|
|
}
|
|
if (action === "get") {
|
|
const name = inlineName ?? requirePos(parsed.positionals, 0, "scheme name");
|
|
return {
|
|
summary: `读取${label}方案详情成功`,
|
|
method: "GET",
|
|
path: `${basePath}/${encodeURIComponent(name)}`,
|
|
params: { network: requireNetwork(ctx) },
|
|
};
|
|
}
|
|
throw cliError("未找到命令", "COMMAND_NOT_FOUND", "unknown schemes command", 2);
|
|
}
|
|
|
|
function detectionCommand(summary, path, parsed, ctx) {
|
|
requireOptions(parsed.args, { "start-time": { required: true }, "end-time": { required: true } });
|
|
return {
|
|
summary,
|
|
method: "POST",
|
|
path,
|
|
body: {
|
|
network: requireNetwork(ctx),
|
|
scada_start: parseTime(parsed.args["start-time"], "--start-time"),
|
|
scada_end: parseTime(parsed.args["end-time"], "--end-time"),
|
|
scheme_name: resolveScheme(ctx, parsed.args.scheme, true),
|
|
},
|
|
};
|
|
}
|
|
|
|
function dataCommand(rest, parsed, ctx) {
|
|
if (rest[0] === "timeseries" && rest[1] === "realtime") {
|
|
return commandFromMap(DATA_REALTIME_COMMANDS, rest.slice(2), parsed, ctx);
|
|
}
|
|
if (rest[0] === "timeseries" && rest[1] === "scheme") {
|
|
return commandFromMap(DATA_SCHEME_COMMANDS, rest.slice(2), parsed, ctx);
|
|
}
|
|
if (rest[0] === "timeseries" && rest[1] === "scada") {
|
|
return commandFromMap(DATA_SCADA_COMMANDS, rest.slice(2), parsed, ctx);
|
|
}
|
|
if (rest[0] === "scada") {
|
|
return commandFromMap(DATA_SCADA_COMMANDS, rest.slice(1), parsed, ctx);
|
|
}
|
|
if (rest[0] === "scheme") {
|
|
return commandFromMap(DATA_SCHEME_COMMANDS, rest.slice(1), parsed, ctx);
|
|
}
|
|
throw cliError("未找到命令", "COMMAND_NOT_FOUND", `unknown data command: ${rest.join(" ")}`, 2);
|
|
}
|
|
|
|
async function requestJson(ctx, request) {
|
|
if (request.requireProject) {
|
|
requireProject(ctx);
|
|
}
|
|
const url = new URL(valueOf(request.path, { args: {}, positionals: [], ctx }), ctx.server);
|
|
for (const [key, value] of Object.entries(request.params ?? {})) {
|
|
if (value === undefined || value === null || value === "") continue;
|
|
if (Array.isArray(value)) {
|
|
for (const item of value) url.searchParams.append(key, String(item));
|
|
continue;
|
|
}
|
|
url.searchParams.set(key, String(value));
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), ctx.timeout * 1000);
|
|
const headers = {
|
|
Accept: "application/json",
|
|
"X-Request-Id": ctx.requestId,
|
|
...ctx.auth.headers,
|
|
};
|
|
if (ctx.auth.accessToken) headers.Authorization = `Bearer ${ctx.auth.accessToken}`;
|
|
if (ctx.auth.projectId) headers["X-Project-Id"] = ctx.auth.projectId;
|
|
if (ctx.auth.userId) headers["X-User-Id"] = ctx.auth.userId;
|
|
if (request.body !== undefined) headers["Content-Type"] = "application/json";
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: request.method ?? "GET",
|
|
headers,
|
|
body: request.body === undefined ? undefined : JSON.stringify(request.body),
|
|
signal: controller.signal,
|
|
});
|
|
const text = await response.text();
|
|
let data = text;
|
|
try {
|
|
data = text ? JSON.parse(text) : null;
|
|
} catch {}
|
|
if (!response.ok) {
|
|
throw cliError("服务端请求失败", "HTTP_ERROR", `server returned HTTP ${response.status}`, response.status >= 500 ? 4 : 2, {
|
|
retryable: response.status >= 500,
|
|
data: { status: response.status, body: data },
|
|
});
|
|
}
|
|
return data;
|
|
} catch (error) {
|
|
if (error?.name === "AbortError") {
|
|
throw cliError("命令超时", "TIMEOUT", `request timed out after ${ctx.timeout}s`, 4, { retryable: true });
|
|
}
|
|
throw error;
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
}
|
|
|
|
async function buildContext(global) {
|
|
const auth = await loadAuth(global.authStdin);
|
|
return {
|
|
server: (global.server ?? auth.server ?? DEFAULT_SERVER).replace(/\/+$/, ""),
|
|
auth,
|
|
scheme: global.scheme,
|
|
timeout: Number(global.timeout ?? DEFAULT_TIMEOUT),
|
|
requestId: global.requestId ?? crypto.randomUUID(),
|
|
};
|
|
}
|
|
|
|
async function loadAuth(authStdin) {
|
|
if (authStdin) {
|
|
const raw = await new Response(Bun.stdin.stream()).text();
|
|
return normalizeAuth(JSON.parse(raw || "{}"));
|
|
}
|
|
const extraHeaders = process.env.TJWATER_EXTRA_HEADERS
|
|
? JSON.parse(process.env.TJWATER_EXTRA_HEADERS)
|
|
: {};
|
|
return normalizeAuth({
|
|
server: process.env.TJWATER_SERVER,
|
|
access_token: process.env.TJWATER_ACCESS_TOKEN,
|
|
project_id: process.env.TJWATER_PROJECT_ID,
|
|
user_id: process.env.TJWATER_USER_ID,
|
|
username: process.env.TJWATER_USERNAME,
|
|
network: process.env.TJWATER_NETWORK,
|
|
headers: extraHeaders,
|
|
});
|
|
}
|
|
|
|
function normalizeAuth(raw) {
|
|
return {
|
|
server: pick(raw, "server", "base_url"),
|
|
accessToken: pick(raw, "access_token", "token", "accessToken"),
|
|
projectId: pick(raw, "project_id", "projectId", "x_project_id"),
|
|
userId: pick(raw, "user_id", "userId", "x_user_id"),
|
|
username: pick(raw, "username", "preferred_username"),
|
|
network: pick(raw, "network", "project_code", "projectCode", "project"),
|
|
headers: typeof raw.headers === "object" && raw.headers !== null ? raw.headers : {},
|
|
};
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const global = {};
|
|
const command = [];
|
|
const args = {};
|
|
const positionals = [];
|
|
let help = false;
|
|
let commandStarted = false;
|
|
for (let i = 0; i < argv.length; i += 1) {
|
|
const token = argv[i];
|
|
if (token === "--help" || token === "-h") {
|
|
help = true;
|
|
continue;
|
|
}
|
|
if (!commandStarted && token.startsWith("--")) {
|
|
const key = camel(token.slice(2));
|
|
if (["authStdin"].includes(key)) {
|
|
global[key] = true;
|
|
} else {
|
|
global[key] = argv[++i];
|
|
}
|
|
continue;
|
|
}
|
|
commandStarted = true;
|
|
if (token.startsWith("--")) {
|
|
const key = token.slice(2);
|
|
const next = argv[i + 1];
|
|
const value = next && !next.startsWith("--") ? argv[++i] : true;
|
|
if (args[key] === undefined) {
|
|
args[key] = value;
|
|
} else if (Array.isArray(args[key])) {
|
|
args[key].push(value);
|
|
} else {
|
|
args[key] = [args[key], value];
|
|
}
|
|
continue;
|
|
}
|
|
if (Object.keys(args).length === 0 && positionals.length === 0) {
|
|
command.push(token);
|
|
} else {
|
|
positionals.push(token);
|
|
}
|
|
}
|
|
return { global, command, args, positionals, help };
|
|
}
|
|
|
|
function requireOptions(args, spec) {
|
|
for (const [name, option] of Object.entries(spec)) {
|
|
if (option.required && (args[name] === undefined || args[name] === "")) {
|
|
throw cliError("CLI 参数错误", "MISSING_OPTION", `missing required option --${name}`, 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
function emitHelp(command = []) {
|
|
const payload = {
|
|
ok: true,
|
|
schema_version: SCHEMA_VERSION,
|
|
summary: command.length ? `命令帮助:${command.join(" ")}` : "TJWater agent CLI",
|
|
data: {
|
|
command: command.join(" "),
|
|
groups: GROUPS,
|
|
examples: [
|
|
"tjwater-cli help",
|
|
"tjwater-cli network get-all-pipes-properties",
|
|
"tjwater-cli data timeseries realtime links --start-time 2025-01-01T00:00:00+08:00 --end-time 2025-01-01T01:00:00+08:00",
|
|
"tjwater-cli analysis leakage schemes list",
|
|
],
|
|
},
|
|
};
|
|
console.log(JSON.stringify(payload, null, 2));
|
|
}
|
|
|
|
function emitSuccess({ summary, data, ctx, durationMs, nextCommands }) {
|
|
console.log(JSON.stringify({
|
|
ok: true,
|
|
schema_version: SCHEMA_VERSION,
|
|
summary,
|
|
data,
|
|
metadata: {
|
|
server: ctx.server,
|
|
request_id: ctx.requestId,
|
|
duration_ms: durationMs,
|
|
},
|
|
next_commands: nextCommands,
|
|
}));
|
|
}
|
|
|
|
function emitFailure(error) {
|
|
const payload = {
|
|
ok: false,
|
|
schema_version: SCHEMA_VERSION,
|
|
summary: error.summary ?? "命令失败",
|
|
error: {
|
|
code: error.code ?? "UNKNOWN",
|
|
message: error.message ?? String(error),
|
|
retryable: Boolean(error.retryable),
|
|
},
|
|
data: error.data,
|
|
next_commands: error.next_commands,
|
|
};
|
|
console.error(JSON.stringify(payload));
|
|
return error.exitCode ?? 1;
|
|
}
|
|
|
|
function cliError(summary, code, message, exitCode, extra = {}) {
|
|
const error = new Error(message);
|
|
error.summary = summary;
|
|
error.code = code;
|
|
error.exitCode = exitCode;
|
|
Object.assign(error, extra);
|
|
return error;
|
|
}
|
|
|
|
function requireNetwork(ctx) {
|
|
if (ctx.auth.network) return ctx.auth.network;
|
|
throw cliError("认证失败", "NETWORK_CONTEXT_REQUIRED", "missing network in auth context", 3);
|
|
}
|
|
|
|
function requireProject(ctx) {
|
|
if (ctx.auth.projectId) return ctx.auth.projectId;
|
|
throw cliError("认证失败", "PROJECT_CONTEXT_REQUIRED", "missing project_id for agent context", 3);
|
|
}
|
|
|
|
function requireUsername(ctx) {
|
|
if (ctx.auth.username) return ctx.auth.username;
|
|
throw cliError("认证失败", "USERNAME_CONTEXT_REQUIRED", "missing username in auth context", 3);
|
|
}
|
|
|
|
function resolveScheme(ctx, explicit, required) {
|
|
const scheme = explicit ?? ctx.scheme;
|
|
if (required && !scheme) {
|
|
throw cliError("CLI 参数错误", "SCHEME_REQUIRED", "missing scheme; use --scheme", 2);
|
|
}
|
|
return scheme;
|
|
}
|
|
|
|
function parseTime(value, optionName) {
|
|
if (!value || Number.isNaN(Date.parse(value)) || !/[zZ]|[+-]\d\d:\d\d$/.test(value)) {
|
|
throw cliError("CLI 参数错误", "INVALID_TIME", `${optionName} must be RFC3339 with timezone`, 2);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function requirePos(values, index, name) {
|
|
const value = values[index];
|
|
if (!value) throw cliError("CLI 参数错误", "MISSING_ARGUMENT", `missing ${name}`, 2);
|
|
return value;
|
|
}
|
|
|
|
function pick(raw, ...keys) {
|
|
for (const key of keys) {
|
|
if (raw?.[key] !== undefined && raw[key] !== "") return raw[key];
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function valueOf(value, args) {
|
|
return typeof value === "function" ? value(args) : value;
|
|
}
|
|
|
|
function maybe(values) {
|
|
return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined && value !== ""));
|
|
}
|
|
|
|
function toArray(value) {
|
|
return Array.isArray(value) ? value : value === undefined ? [] : [value];
|
|
}
|
|
|
|
function camel(value) {
|
|
return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
}
|
|
|
|
main()
|
|
.then((code) => process.exit(code))
|
|
.catch((error) => process.exit(emitFailure(error)));
|