mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
186 lines
7.5 KiB
TypeScript
186 lines
7.5 KiB
TypeScript
import type { Command } from "commander";
|
|
import { defaultRuntime } from "../../runtime.js";
|
|
import { formatAge, formatPermissions, parseNodeList, parsePairingList } from "./format.js";
|
|
import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
|
|
import type { NodesRpcOpts } from "./types.js";
|
|
|
|
function formatVersionLabel(raw: string) {
|
|
const trimmed = raw.trim();
|
|
if (!trimmed) return raw;
|
|
if (trimmed.toLowerCase().startsWith("v")) return trimmed;
|
|
return /^\d/.test(trimmed) ? `v${trimmed}` : trimmed;
|
|
}
|
|
|
|
function resolveNodeVersions(node: {
|
|
platform?: string;
|
|
version?: string;
|
|
coreVersion?: string;
|
|
uiVersion?: string;
|
|
}) {
|
|
const core = node.coreVersion?.trim() || undefined;
|
|
const ui = node.uiVersion?.trim() || undefined;
|
|
if (core || ui) return { core, ui };
|
|
const legacy = node.version?.trim();
|
|
if (!legacy) return { core: undefined, ui: undefined };
|
|
const platform = node.platform?.trim().toLowerCase() ?? "";
|
|
const headless =
|
|
platform === "darwin" || platform === "linux" || platform === "win32" || platform === "windows";
|
|
return headless ? { core: legacy, ui: undefined } : { core: undefined, ui: legacy };
|
|
}
|
|
|
|
function formatNodeVersions(node: {
|
|
platform?: string;
|
|
version?: string;
|
|
coreVersion?: string;
|
|
uiVersion?: string;
|
|
}) {
|
|
const { core, ui } = resolveNodeVersions(node);
|
|
const parts: string[] = [];
|
|
if (core) parts.push(`core ${formatVersionLabel(core)}`);
|
|
if (ui) parts.push(`ui ${formatVersionLabel(ui)}`);
|
|
return parts.length > 0 ? parts.join(" · ") : null;
|
|
}
|
|
|
|
export function registerNodesStatusCommands(nodes: Command) {
|
|
nodesCallOpts(
|
|
nodes
|
|
.command("status")
|
|
.description("List known nodes with connection status and capabilities")
|
|
.action(async (opts: NodesRpcOpts) => {
|
|
try {
|
|
const result = (await callGatewayCli("node.list", opts, {})) as unknown;
|
|
if (opts.json) {
|
|
defaultRuntime.log(JSON.stringify(result, null, 2));
|
|
return;
|
|
}
|
|
const nodes = parseNodeList(result);
|
|
const pairedCount = nodes.filter((n) => Boolean(n.paired)).length;
|
|
const connectedCount = nodes.filter((n) => Boolean(n.connected)).length;
|
|
defaultRuntime.log(
|
|
`Known: ${nodes.length} · Paired: ${pairedCount} · Connected: ${connectedCount}`,
|
|
);
|
|
for (const n of nodes) {
|
|
const name = n.displayName || n.nodeId;
|
|
const ip = n.remoteIp ? ` · ${n.remoteIp}` : "";
|
|
const device = n.deviceFamily ? ` · device: ${n.deviceFamily}` : "";
|
|
const hw = n.modelIdentifier ? ` · hw: ${n.modelIdentifier}` : "";
|
|
const perms = formatPermissions(n.permissions);
|
|
const permsText = perms ? ` · perms: ${perms}` : "";
|
|
const versions = formatNodeVersions(n);
|
|
const versionText = versions ? ` · ${versions}` : "";
|
|
const caps =
|
|
Array.isArray(n.caps) && n.caps.length > 0
|
|
? `[${n.caps.map(String).filter(Boolean).sort().join(",")}]`
|
|
: Array.isArray(n.caps)
|
|
? "[]"
|
|
: "?";
|
|
const pairing = n.paired ? "paired" : "unpaired";
|
|
defaultRuntime.log(
|
|
`- ${name} · ${n.nodeId}${ip}${device}${hw}${permsText}${versionText} · ${pairing} · ${n.connected ? "connected" : "disconnected"} · caps: ${caps}`,
|
|
);
|
|
}
|
|
} catch (err) {
|
|
defaultRuntime.error(`nodes status failed: ${String(err)}`);
|
|
defaultRuntime.exit(1);
|
|
}
|
|
}),
|
|
);
|
|
|
|
nodesCallOpts(
|
|
nodes
|
|
.command("describe")
|
|
.description("Describe a node (capabilities + supported invoke commands)")
|
|
.requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP")
|
|
.action(async (opts: NodesRpcOpts) => {
|
|
try {
|
|
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
|
|
const result = (await callGatewayCli("node.describe", opts, {
|
|
nodeId,
|
|
})) as unknown;
|
|
if (opts.json) {
|
|
defaultRuntime.log(JSON.stringify(result, null, 2));
|
|
return;
|
|
}
|
|
|
|
const obj =
|
|
typeof result === "object" && result !== null
|
|
? (result as Record<string, unknown>)
|
|
: {};
|
|
const displayName = typeof obj.displayName === "string" ? obj.displayName : nodeId;
|
|
const connected = Boolean(obj.connected);
|
|
const caps = Array.isArray(obj.caps) ? obj.caps.map(String).filter(Boolean).sort() : null;
|
|
const commands = Array.isArray(obj.commands)
|
|
? obj.commands.map(String).filter(Boolean).sort()
|
|
: [];
|
|
const perms = formatPermissions(obj.permissions);
|
|
const family = typeof obj.deviceFamily === "string" ? obj.deviceFamily : null;
|
|
const model = typeof obj.modelIdentifier === "string" ? obj.modelIdentifier : null;
|
|
const ip = typeof obj.remoteIp === "string" ? obj.remoteIp : null;
|
|
const versions = formatNodeVersions(obj as {
|
|
platform?: string;
|
|
version?: string;
|
|
coreVersion?: string;
|
|
uiVersion?: string;
|
|
});
|
|
|
|
const parts: string[] = ["Node:", displayName, nodeId];
|
|
if (ip) parts.push(ip);
|
|
if (family) parts.push(`device: ${family}`);
|
|
if (model) parts.push(`hw: ${model}`);
|
|
if (perms) parts.push(`perms: ${perms}`);
|
|
if (versions) parts.push(versions);
|
|
parts.push(connected ? "connected" : "disconnected");
|
|
parts.push(`caps: ${caps ? `[${caps.join(",")}]` : "?"}`);
|
|
defaultRuntime.log(parts.join(" · "));
|
|
defaultRuntime.log("Commands:");
|
|
if (commands.length === 0) {
|
|
defaultRuntime.log("- (none reported)");
|
|
return;
|
|
}
|
|
for (const c of commands) defaultRuntime.log(`- ${c}`);
|
|
} catch (err) {
|
|
defaultRuntime.error(`nodes describe failed: ${String(err)}`);
|
|
defaultRuntime.exit(1);
|
|
}
|
|
}),
|
|
);
|
|
|
|
nodesCallOpts(
|
|
nodes
|
|
.command("list")
|
|
.description("List pending and paired nodes")
|
|
.action(async (opts: NodesRpcOpts) => {
|
|
try {
|
|
const result = (await callGatewayCli("node.pair.list", opts, {})) as unknown;
|
|
if (opts.json) {
|
|
defaultRuntime.log(JSON.stringify(result, null, 2));
|
|
return;
|
|
}
|
|
const { pending, paired } = parsePairingList(result);
|
|
defaultRuntime.log(`Pending: ${pending.length} · Paired: ${paired.length}`);
|
|
if (pending.length > 0) {
|
|
defaultRuntime.log("\nPending:");
|
|
for (const r of pending) {
|
|
const name = r.displayName || r.nodeId;
|
|
const repair = r.isRepair ? " (repair)" : "";
|
|
const ip = r.remoteIp ? ` · ${r.remoteIp}` : "";
|
|
const age = typeof r.ts === "number" ? ` · ${formatAge(Date.now() - r.ts)} ago` : "";
|
|
defaultRuntime.log(`- ${r.requestId}: ${name}${repair}${ip}${age}`);
|
|
}
|
|
}
|
|
if (paired.length > 0) {
|
|
defaultRuntime.log("\nPaired:");
|
|
for (const n of paired) {
|
|
const name = n.displayName || n.nodeId;
|
|
const ip = n.remoteIp ? ` · ${n.remoteIp}` : "";
|
|
defaultRuntime.log(`- ${n.nodeId}: ${name}${ip}`);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
defaultRuntime.error(`nodes list failed: ${String(err)}`);
|
|
defaultRuntime.exit(1);
|
|
}
|
|
}),
|
|
);
|
|
}
|