






















@@ -11,14 +11,23 @@ import { formatErrorMessage } from "../../infra/errors.js";
1111import { formatTimeAgo } from "../../infra/format-time/format-relative.ts";
1212import { defaultRuntime } from "../../runtime.js";
1313import { shortenHomeInString } from "../../utils.js";
14+import { formatCliCommand } from "../command-format.js";
1415import { parseDurationMs } from "../parse-duration.js";
1516import { getNodesTheme, runNodesCommand } from "./cli-utils.js";
1617import { formatPermissions, parseNodeList, parsePairingList } from "./format.js";
1718import { renderPendingPairingRequestsTable } from "./pairing-render.js";
18-import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
19+import {
20+callGatewayCli,
21+callNodeDiagnosticsGatewayCli,
22+nodesCallOpts,
23+resolveNodeDiagnosticsId,
24+} from "./rpc.js";
1925import type { NodeListNode, NodesRpcOpts, PairedNode } from "./types.js";
20262127type PairedNodeListRow = PairedNode & Partial<NodeListNode>;
28+type NodeApprovalState = NonNullable<NodeListNode["approvalState"]>;
29+30+const DEFAULT_NODES_RPC_TIMEOUT_MS = 10_000;
22312332function formatVersionLabel(raw: string) {
2433const trimmed = raw.trim();
@@ -98,6 +107,61 @@ function formatNodeTerminalLabel(node: { nodeId: string; displayName?: string })
98107return sanitizeTerminalText(label);
99108}
100109110+function formatNodeApprovalState(raw: unknown): NodeApprovalState | null {
111+return raw === "approved" ||
112+raw === "pending-approval" ||
113+raw === "pending-reapproval" ||
114+raw === "unapproved"
115+ ? raw
116+ : null;
117+}
118+119+function formatApprovalStateLabel(state: NodeApprovalState): string {
120+if (state === "pending-approval") {
121+return "approval pending";
122+}
123+if (state === "pending-reapproval") {
124+return "reapproval pending";
125+}
126+return state;
127+}
128+129+function isPendingApprovalState(
130+state: NodeApprovalState | null,
131+): state is "pending-approval" | "pending-reapproval" {
132+return state === "pending-approval" || state === "pending-reapproval";
133+}
134+135+function quoteCliArg(value: string): string {
136+if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) {
137+return value;
138+}
139+return `'${value.replaceAll("'", "'\\''")}'`;
140+}
141+142+function formatPendingApprovalCommand(raw: unknown, opts: NodesRpcOpts): string | null {
143+const requestId = normalizeOptionalString(raw);
144+if (!requestId) {
145+return null;
146+}
147+const args = ["openclaw", "nodes", "approve", requestId];
148+const timeout = normalizeOptionalString(opts.timeout);
149+if (timeout && timeout !== String(DEFAULT_NODES_RPC_TIMEOUT_MS)) {
150+args.push("--timeout", timeout);
151+}
152+return formatCliCommand(args.map(quoteCliArg).join(" "));
153+}
154+155+function formatConnectionFlagReminder(opts: NodesRpcOpts): string | null {
156+const flags = [
157+normalizeOptionalString(opts.url) ? "--url" : null,
158+normalizeOptionalString(opts.token) ? "--token" : null,
159+].filter((flag) => flag !== null);
160+return flags.length > 0
161+ ? `Reuse the same ${flags.join("/")} option${flags.length === 1 ? "" : "s"} when rerunning.`
162+ : null;
163+}
164+101165function parseSinceMs(raw: unknown, label: string): number | undefined {
102166if (raw === undefined || raw === null) {
103167return undefined;
@@ -194,7 +258,7 @@ export function registerNodesStatusCommands(nodes: Command) {
194258await runNodesCommand("status", async () => {
195259const connectedOnly = Boolean(opts.connected);
196260const sinceMs = parseSinceMs(opts.lastConnected, "Invalid --last-connected");
197-const result = await callGatewayCli("node.list", opts, {});
261+const result = await callNodeDiagnosticsGatewayCli("node.list", opts, {});
198262const obj: Record<string, unknown> =
199263typeof result === "object" && result !== null ? result : {};
200264const { ok, warn, muted } = getNodesTheme();
@@ -268,6 +332,15 @@ export function registerNodesStatusCommands(nodes: Command) {
268332 : "?";
269333const paired = n.paired ? ok("paired") : warn("unpaired");
270334const connected = n.connected ? ok("connected") : muted("disconnected");
335+const approvalState = formatNodeApprovalState(n.approvalState);
336+const approval =
337+approvalState === "approved"
338+ ? ok("approved")
339+ : isPendingApprovalState(approvalState)
340+ ? warn(formatApprovalStateLabel(approvalState))
341+ : approvalState === "unapproved"
342+ ? warn("unapproved")
343+ : null;
271344const since =
272345typeof n.connectedAtMs === "number"
273346 ? ` (${formatTimeAgo(Math.max(0, now - n.connectedAtMs))})`
@@ -278,7 +351,7 @@ export function registerNodesStatusCommands(nodes: Command) {
278351ID: sanitizeTerminalText(n.nodeId),
279352IP: sanitizeTerminalText(n.remoteIp ?? ""),
280353Detail: detailParts.join(" · "),
281-Status: `${paired} · ${connected}${since}`,
354+Status: `${paired} · ${connected}${since}${approval ? ` · ${approval}` : ""}`,
282355Caps: caps,
283356};
284357});
@@ -297,6 +370,22 @@ export function registerNodesStatusCommands(nodes: Command) {
297370 rows,
298371}).trimEnd(),
299372);
373+for (const node of filtered) {
374+const approvalState = formatNodeApprovalState(node.approvalState);
375+const approveCommand = formatPendingApprovalCommand(node.pendingRequestId, opts);
376+if (isPendingApprovalState(approvalState) && approveCommand) {
377+const action = approvalState === "pending-reapproval" ? "Reapproval" : "Approval";
378+defaultRuntime.log(
379+warn(
380+`${action} pending for ${formatNodeTerminalLabel(node)}. Run ${sanitizeTerminalText(approveCommand)}`,
381+),
382+);
383+const connectionReminder = formatConnectionFlagReminder(opts);
384+if (connectionReminder) {
385+defaultRuntime.log(warn(connectionReminder));
386+}
387+}
388+}
300389});
301390}),
302391);
@@ -308,8 +397,8 @@ export function registerNodesStatusCommands(nodes: Command) {
308397.requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP")
309398.action(async (opts: NodesRpcOpts) => {
310399await runNodesCommand("describe", async () => {
311-const nodeId = await resolveNodeId(opts, opts.node ?? "");
312-const result = await callGatewayCli("node.describe", opts, {
400+const nodeId = await resolveNodeDiagnosticsId(opts, opts.node ?? "");
401+const result = await callNodeDiagnosticsGatewayCli("node.describe", opts, {
313402 nodeId,
314403});
315404if (opts.json) {
@@ -329,6 +418,19 @@ export function registerNodesStatusCommands(nodes: Command) {
329418 ? obj.commands.map(String).filter(Boolean).toSorted()
330419 : [];
331420const perms = formatPermissions(obj.permissions);
421+const approvalState = formatNodeApprovalState(obj.approvalState);
422+const pendingRequestId = normalizeOptionalString(obj.pendingRequestId);
423+const pendingCaps = Array.isArray(obj.pendingDeclaredCaps)
424+ ? obj.pendingDeclaredCaps.map(String).filter(Boolean).toSorted()
425+ : null;
426+const pendingCommands = Array.isArray(obj.pendingDeclaredCommands)
427+ ? obj.pendingDeclaredCommands.map(String).filter(Boolean).toSorted()
428+ : [];
429+const pendingPerms = formatPermissions(obj.pendingDeclaredPermissions);
430+const approveCommand = isPendingApprovalState(approvalState)
431+ ? formatPendingApprovalCommand(pendingRequestId, opts)
432+ : null;
433+const connectionReminder = approveCommand ? formatConnectionFlagReminder(opts) : null;
332434const family = typeof obj.deviceFamily === "string" ? obj.deviceFamily : null;
333435const model = typeof obj.modelIdentifier === "string" ? obj.modelIdentifier : null;
334436const client = formatClientLabel(obj as { clientId?: string; clientMode?: string });
@@ -359,6 +461,27 @@ export function registerNodesStatusCommands(nodes: Command) {
359461versions ? { Field: "Version", Value: sanitizeTerminalText(versions) } : null,
360462pathEnv ? { Field: "PATH", Value: sanitizeTerminalText(pathEnv) } : null,
361463{ Field: "Status", Value: status },
464+approvalState
465+ ? { Field: "Approval", Value: formatApprovalStateLabel(approvalState) }
466+ : null,
467+pendingRequestId
468+ ? { Field: "Pending request", Value: sanitizeTerminalText(pendingRequestId) }
469+ : null,
470+pendingCaps
471+ ? { Field: "Pending caps", Value: sanitizeTerminalText(pendingCaps.join(", ")) }
472+ : null,
473+pendingPerms
474+ ? { Field: "Pending perms", Value: sanitizeTerminalText(pendingPerms) }
475+ : null,
476+approveCommand
477+ ? {
478+Field: approvalState === "pending-reapproval" ? "Reapprove" : "Approve",
479+Value: sanitizeTerminalText(approveCommand),
480+}
481+ : null,
482+approveCommand && connectionReminder
483+ ? { Field: "Connection reminder", Value: connectionReminder }
484+ : null,
362485{ Field: "Caps", Value: caps ? sanitizeTerminalText(caps.join(", ")) : "?" },
363486].filter(Boolean) as Array<{ Field: string; Value: string }>;
364487@@ -376,11 +499,18 @@ export function registerNodesStatusCommands(nodes: Command) {
376499defaultRuntime.log("");
377500defaultRuntime.log(heading("Commands"));
378501if (commands.length === 0) {
379-defaultRuntime.log(muted("- (none reported)"));
380-return;
502+defaultRuntime.log(muted("- (none effective)"));
503+} else {
504+for (const c of commands) {
505+defaultRuntime.log(`- ${c}`);
506+}
381507}
382-for (const c of commands) {
383-defaultRuntime.log(`- ${c}`);
508+if (pendingCommands.length > 0) {
509+defaultRuntime.log("");
510+defaultRuntime.log(heading("Pending commands"));
511+for (const command of pendingCommands) {
512+defaultRuntime.log(`- ${sanitizeTerminalText(command)}`);
513+}
384514}
385515});
386516}),
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。