mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 09:41:24 +00:00
refactor: split gateway server methods
This commit is contained in:
500
src/gateway/server-methods/nodes.ts
Normal file
500
src/gateway/server-methods/nodes.ts
Normal file
@@ -0,0 +1,500 @@
|
||||
import {
|
||||
approveNodePairing,
|
||||
listNodePairing,
|
||||
rejectNodePairing,
|
||||
renamePairedNode,
|
||||
requestNodePairing,
|
||||
verifyNodeToken,
|
||||
} from "../../infra/node-pairing.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
formatValidationErrors,
|
||||
validateNodeDescribeParams,
|
||||
validateNodeInvokeParams,
|
||||
validateNodeListParams,
|
||||
validateNodePairApproveParams,
|
||||
validateNodePairListParams,
|
||||
validateNodePairRejectParams,
|
||||
validateNodePairRequestParams,
|
||||
validateNodePairVerifyParams,
|
||||
validateNodeRenameParams,
|
||||
} from "../protocol/index.js";
|
||||
import { formatForLog } from "../ws-log.js";
|
||||
import type { GatewayRequestHandlers } from "./types.js";
|
||||
|
||||
export const nodeHandlers: GatewayRequestHandlers = {
|
||||
"node.pair.request": async ({ params, respond, context }) => {
|
||||
if (!validateNodePairRequestParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid node.pair.request params: ${formatValidationErrors(validateNodePairRequestParams.errors)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const p = params as {
|
||||
nodeId: string;
|
||||
displayName?: string;
|
||||
platform?: string;
|
||||
version?: string;
|
||||
deviceFamily?: string;
|
||||
modelIdentifier?: string;
|
||||
caps?: string[];
|
||||
commands?: string[];
|
||||
remoteIp?: string;
|
||||
silent?: boolean;
|
||||
};
|
||||
try {
|
||||
const result = await requestNodePairing({
|
||||
nodeId: p.nodeId,
|
||||
displayName: p.displayName,
|
||||
platform: p.platform,
|
||||
version: p.version,
|
||||
deviceFamily: p.deviceFamily,
|
||||
modelIdentifier: p.modelIdentifier,
|
||||
caps: p.caps,
|
||||
commands: p.commands,
|
||||
remoteIp: p.remoteIp,
|
||||
silent: p.silent,
|
||||
});
|
||||
if (result.status === "pending" && result.created) {
|
||||
context.broadcast("node.pair.requested", result.request, {
|
||||
dropIfSlow: true,
|
||||
});
|
||||
}
|
||||
respond(true, result, undefined);
|
||||
} catch (err) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)),
|
||||
);
|
||||
}
|
||||
},
|
||||
"node.pair.list": async ({ params, respond }) => {
|
||||
if (!validateNodePairListParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid node.pair.list params: ${formatValidationErrors(validateNodePairListParams.errors)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const list = await listNodePairing();
|
||||
respond(true, list, undefined);
|
||||
} catch (err) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)),
|
||||
);
|
||||
}
|
||||
},
|
||||
"node.pair.approve": async ({ params, respond, context }) => {
|
||||
if (!validateNodePairApproveParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid node.pair.approve params: ${formatValidationErrors(validateNodePairApproveParams.errors)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { requestId } = params as { requestId: string };
|
||||
try {
|
||||
const approved = await approveNodePairing(requestId);
|
||||
if (!approved) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "unknown requestId"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
context.broadcast(
|
||||
"node.pair.resolved",
|
||||
{
|
||||
requestId,
|
||||
nodeId: approved.node.nodeId,
|
||||
decision: "approved",
|
||||
ts: Date.now(),
|
||||
},
|
||||
{ dropIfSlow: true },
|
||||
);
|
||||
respond(true, approved, undefined);
|
||||
} catch (err) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)),
|
||||
);
|
||||
}
|
||||
},
|
||||
"node.pair.reject": async ({ params, respond, context }) => {
|
||||
if (!validateNodePairRejectParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid node.pair.reject params: ${formatValidationErrors(validateNodePairRejectParams.errors)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { requestId } = params as { requestId: string };
|
||||
try {
|
||||
const rejected = await rejectNodePairing(requestId);
|
||||
if (!rejected) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "unknown requestId"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
context.broadcast(
|
||||
"node.pair.resolved",
|
||||
{
|
||||
requestId,
|
||||
nodeId: rejected.nodeId,
|
||||
decision: "rejected",
|
||||
ts: Date.now(),
|
||||
},
|
||||
{ dropIfSlow: true },
|
||||
);
|
||||
respond(true, rejected, undefined);
|
||||
} catch (err) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)),
|
||||
);
|
||||
}
|
||||
},
|
||||
"node.pair.verify": async ({ params, respond }) => {
|
||||
if (!validateNodePairVerifyParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid node.pair.verify params: ${formatValidationErrors(validateNodePairVerifyParams.errors)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { nodeId, token } = params as {
|
||||
nodeId: string;
|
||||
token: string;
|
||||
};
|
||||
try {
|
||||
const result = await verifyNodeToken(nodeId, token);
|
||||
respond(true, result, undefined);
|
||||
} catch (err) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)),
|
||||
);
|
||||
}
|
||||
},
|
||||
"node.rename": async ({ params, respond }) => {
|
||||
if (!validateNodeRenameParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid node.rename params: ${formatValidationErrors(validateNodeRenameParams.errors)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { nodeId, displayName } = params as {
|
||||
nodeId: string;
|
||||
displayName: string;
|
||||
};
|
||||
try {
|
||||
const trimmed = displayName.trim();
|
||||
if (!trimmed) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "displayName required"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const updated = await renamePairedNode(nodeId, trimmed);
|
||||
if (!updated) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "unknown nodeId"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
respond(true, { nodeId: updated.nodeId, displayName: updated.displayName }, undefined);
|
||||
} catch (err) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)),
|
||||
);
|
||||
}
|
||||
},
|
||||
"node.list": async ({ params, respond, context }) => {
|
||||
if (!validateNodeListParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid node.list params: ${formatValidationErrors(validateNodeListParams.errors)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const list = await listNodePairing();
|
||||
const pairedById = new Map(list.paired.map((n) => [n.nodeId, n]));
|
||||
const connected = context.bridge?.listConnected?.() ?? [];
|
||||
const connectedById = new Map(connected.map((n) => [n.nodeId, n]));
|
||||
const nodeIds = new Set<string>([
|
||||
...pairedById.keys(),
|
||||
...connectedById.keys(),
|
||||
]);
|
||||
|
||||
const nodes = [...nodeIds].map((nodeId) => {
|
||||
const paired = pairedById.get(nodeId);
|
||||
const live = connectedById.get(nodeId);
|
||||
|
||||
const caps = [
|
||||
...new Set(
|
||||
(live?.caps ?? paired?.caps ?? [])
|
||||
.map((c) => String(c).trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
].sort();
|
||||
|
||||
const commands = [
|
||||
...new Set(
|
||||
(live?.commands ?? paired?.commands ?? [])
|
||||
.map((c) => String(c).trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
].sort();
|
||||
|
||||
return {
|
||||
nodeId,
|
||||
displayName: live?.displayName ?? paired?.displayName,
|
||||
platform: live?.platform ?? paired?.platform,
|
||||
version: live?.version ?? paired?.version,
|
||||
deviceFamily: live?.deviceFamily ?? paired?.deviceFamily,
|
||||
modelIdentifier: live?.modelIdentifier ?? paired?.modelIdentifier,
|
||||
remoteIp: live?.remoteIp ?? paired?.remoteIp,
|
||||
caps,
|
||||
commands,
|
||||
permissions: live?.permissions ?? paired?.permissions,
|
||||
paired: Boolean(paired),
|
||||
connected: Boolean(live),
|
||||
};
|
||||
});
|
||||
|
||||
nodes.sort((a, b) => {
|
||||
if (a.connected !== b.connected) return a.connected ? -1 : 1;
|
||||
const an = (a.displayName ?? a.nodeId).toLowerCase();
|
||||
const bn = (b.displayName ?? b.nodeId).toLowerCase();
|
||||
if (an < bn) return -1;
|
||||
if (an > bn) return 1;
|
||||
return a.nodeId.localeCompare(b.nodeId);
|
||||
});
|
||||
|
||||
respond(true, { ts: Date.now(), nodes }, undefined);
|
||||
} catch (err) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)),
|
||||
);
|
||||
}
|
||||
},
|
||||
"node.describe": async ({ params, respond, context }) => {
|
||||
if (!validateNodeDescribeParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid node.describe params: ${formatValidationErrors(validateNodeDescribeParams.errors)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { nodeId } = params as { nodeId: string };
|
||||
const id = String(nodeId ?? "").trim();
|
||||
if (!id) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "nodeId required"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const list = await listNodePairing();
|
||||
const paired = list.paired.find((n) => n.nodeId === id);
|
||||
const connected = context.bridge?.listConnected?.() ?? [];
|
||||
const live = connected.find((n) => n.nodeId === id);
|
||||
|
||||
if (!paired && !live) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "unknown nodeId"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const caps = [
|
||||
...new Set(
|
||||
(live?.caps ?? paired?.caps ?? [])
|
||||
.map((c) => String(c).trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
].sort();
|
||||
|
||||
const commands = [
|
||||
...new Set(
|
||||
(live?.commands ?? paired?.commands ?? [])
|
||||
.map((c) => String(c).trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
].sort();
|
||||
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
ts: Date.now(),
|
||||
nodeId: id,
|
||||
displayName: live?.displayName ?? paired?.displayName,
|
||||
platform: live?.platform ?? paired?.platform,
|
||||
version: live?.version ?? paired?.version,
|
||||
deviceFamily: live?.deviceFamily ?? paired?.deviceFamily,
|
||||
modelIdentifier: live?.modelIdentifier ?? paired?.modelIdentifier,
|
||||
remoteIp: live?.remoteIp ?? paired?.remoteIp,
|
||||
caps,
|
||||
commands,
|
||||
permissions: live?.permissions ?? paired?.permissions,
|
||||
paired: Boolean(paired),
|
||||
connected: Boolean(live),
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
} catch (err) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)),
|
||||
);
|
||||
}
|
||||
},
|
||||
"node.invoke": async ({ params, respond, context }) => {
|
||||
if (!validateNodeInvokeParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid node.invoke params: ${formatValidationErrors(validateNodeInvokeParams.errors)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!context.bridge) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.UNAVAILABLE, "bridge not running"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const p = params as {
|
||||
nodeId: string;
|
||||
command: string;
|
||||
params?: unknown;
|
||||
timeoutMs?: number;
|
||||
idempotencyKey: string;
|
||||
};
|
||||
const nodeId = String(p.nodeId ?? "").trim();
|
||||
const command = String(p.command ?? "").trim();
|
||||
if (!nodeId || !command) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "nodeId and command required"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const paramsJSON =
|
||||
"params" in p && p.params !== undefined ? JSON.stringify(p.params) : null;
|
||||
const res = await context.bridge.invoke({
|
||||
nodeId,
|
||||
command,
|
||||
paramsJSON,
|
||||
timeoutMs: p.timeoutMs,
|
||||
});
|
||||
if (!res.ok) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.UNAVAILABLE,
|
||||
res.error?.message ?? "node invoke failed",
|
||||
{ details: { nodeError: res.error ?? null } },
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const payload =
|
||||
typeof res.payloadJSON === "string" && res.payloadJSON.trim()
|
||||
? (() => {
|
||||
try {
|
||||
return JSON.parse(res.payloadJSON) as unknown;
|
||||
} catch {
|
||||
return { payloadJSON: res.payloadJSON };
|
||||
}
|
||||
})()
|
||||
: undefined;
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
ok: true,
|
||||
nodeId,
|
||||
command,
|
||||
payload,
|
||||
payloadJSON: res.payloadJSON ?? null,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
} catch (err) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user