build(cli): replace bundled binary cli
This commit is contained in:
@@ -7,6 +7,5 @@ __pycache__/
|
||||
docker-compose.yml
|
||||
data/
|
||||
logs/
|
||||
cli/
|
||||
AGENT_HARNESS_REPORT.md
|
||||
HARNESS_INTRODUCTION.md
|
||||
|
||||
+2
-2
@@ -65,7 +65,7 @@ WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=8787
|
||||
ENV TJWATER_CLI_PATH=./cli/tjwater-cli/tjwater-cli
|
||||
ENV TJWATER_CLI_PATH=./cli/tjwater-cli
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY --from=deps /app/.opencode/node_modules ./.opencode/node_modules
|
||||
@@ -76,7 +76,7 @@ COPY .opencode ./.opencode
|
||||
COPY cli ./cli
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh ./cli/tjwater-cli/tjwater-cli
|
||||
RUN chmod +x /entrypoint.sh ./cli/tjwater-cli
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
|
||||
Executable
+656
@@ -0,0 +1,656 @@
|
||||
#!/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)));
|
||||
Reference in New Issue
Block a user