Gateway: list/describe node capabilities and commands
parent
efed2ae30f
commit
742027a447
|
|
@ -38,6 +38,8 @@ import {
|
||||||
GatewayFrameSchema,
|
GatewayFrameSchema,
|
||||||
type HelloOk,
|
type HelloOk,
|
||||||
HelloOkSchema,
|
HelloOkSchema,
|
||||||
|
type NodeDescribeParams,
|
||||||
|
NodeDescribeParamsSchema,
|
||||||
type NodeInvokeParams,
|
type NodeInvokeParams,
|
||||||
NodeInvokeParamsSchema,
|
NodeInvokeParamsSchema,
|
||||||
type NodeListParams,
|
type NodeListParams,
|
||||||
|
|
@ -111,6 +113,9 @@ export const validateNodePairVerifyParams = ajv.compile<NodePairVerifyParams>(
|
||||||
);
|
);
|
||||||
export const validateNodeListParams =
|
export const validateNodeListParams =
|
||||||
ajv.compile<NodeListParams>(NodeListParamsSchema);
|
ajv.compile<NodeListParams>(NodeListParamsSchema);
|
||||||
|
export const validateNodeDescribeParams = ajv.compile<NodeDescribeParams>(
|
||||||
|
NodeDescribeParamsSchema,
|
||||||
|
);
|
||||||
export const validateNodeInvokeParams = ajv.compile<NodeInvokeParams>(
|
export const validateNodeInvokeParams = ajv.compile<NodeInvokeParams>(
|
||||||
NodeInvokeParamsSchema,
|
NodeInvokeParamsSchema,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,10 @@ export const NodePairRequestParamsSchema = Type.Object(
|
||||||
displayName: Type.Optional(NonEmptyString),
|
displayName: Type.Optional(NonEmptyString),
|
||||||
platform: Type.Optional(NonEmptyString),
|
platform: Type.Optional(NonEmptyString),
|
||||||
version: Type.Optional(NonEmptyString),
|
version: Type.Optional(NonEmptyString),
|
||||||
|
deviceFamily: Type.Optional(NonEmptyString),
|
||||||
|
modelIdentifier: Type.Optional(NonEmptyString),
|
||||||
|
caps: Type.Optional(Type.Array(NonEmptyString)),
|
||||||
|
commands: Type.Optional(Type.Array(NonEmptyString)),
|
||||||
remoteIp: Type.Optional(NonEmptyString),
|
remoteIp: Type.Optional(NonEmptyString),
|
||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
|
|
@ -252,6 +256,11 @@ export const NodeListParamsSchema = Type.Object(
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const NodeDescribeParamsSchema = Type.Object(
|
||||||
|
{ nodeId: NonEmptyString },
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
export const NodeInvokeParamsSchema = Type.Object(
|
export const NodeInvokeParamsSchema = Type.Object(
|
||||||
{
|
{
|
||||||
nodeId: NonEmptyString,
|
nodeId: NonEmptyString,
|
||||||
|
|
@ -528,6 +537,7 @@ export const ProtocolSchemas: Record<string, TSchema> = {
|
||||||
NodePairRejectParams: NodePairRejectParamsSchema,
|
NodePairRejectParams: NodePairRejectParamsSchema,
|
||||||
NodePairVerifyParams: NodePairVerifyParamsSchema,
|
NodePairVerifyParams: NodePairVerifyParamsSchema,
|
||||||
NodeListParams: NodeListParamsSchema,
|
NodeListParams: NodeListParamsSchema,
|
||||||
|
NodeDescribeParams: NodeDescribeParamsSchema,
|
||||||
NodeInvokeParams: NodeInvokeParamsSchema,
|
NodeInvokeParams: NodeInvokeParamsSchema,
|
||||||
SessionsListParams: SessionsListParamsSchema,
|
SessionsListParams: SessionsListParamsSchema,
|
||||||
SessionsPatchParams: SessionsPatchParamsSchema,
|
SessionsPatchParams: SessionsPatchParamsSchema,
|
||||||
|
|
@ -568,6 +578,7 @@ export type NodePairApproveParams = Static<typeof NodePairApproveParamsSchema>;
|
||||||
export type NodePairRejectParams = Static<typeof NodePairRejectParamsSchema>;
|
export type NodePairRejectParams = Static<typeof NodePairRejectParamsSchema>;
|
||||||
export type NodePairVerifyParams = Static<typeof NodePairVerifyParamsSchema>;
|
export type NodePairVerifyParams = Static<typeof NodePairVerifyParamsSchema>;
|
||||||
export type NodeListParams = Static<typeof NodeListParamsSchema>;
|
export type NodeListParams = Static<typeof NodeListParamsSchema>;
|
||||||
|
export type NodeDescribeParams = Static<typeof NodeDescribeParamsSchema>;
|
||||||
export type NodeInvokeParams = Static<typeof NodeInvokeParamsSchema>;
|
export type NodeInvokeParams = Static<typeof NodeInvokeParamsSchema>;
|
||||||
export type SessionsListParams = Static<typeof SessionsListParamsSchema>;
|
export type SessionsListParams = Static<typeof SessionsListParamsSchema>;
|
||||||
export type SessionsPatchParams = Static<typeof SessionsPatchParamsSchema>;
|
export type SessionsPatchParams = Static<typeof SessionsPatchParamsSchema>;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ type BridgeClientInfo = {
|
||||||
platform?: string;
|
platform?: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
remoteIp?: string;
|
remoteIp?: string;
|
||||||
|
deviceFamily?: string;
|
||||||
|
modelIdentifier?: string;
|
||||||
|
caps?: string[];
|
||||||
|
commands?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type BridgeStartOpts = {
|
type BridgeStartOpts = {
|
||||||
|
|
@ -543,23 +547,188 @@ describe("gateway server", () => {
|
||||||
try {
|
try {
|
||||||
await connectOk(ws);
|
await connectOk(ws);
|
||||||
|
|
||||||
const res = await rpcReq(ws, "node.invoke", {
|
const res = await rpcReq(ws, "node.invoke", {
|
||||||
nodeId: "ios-node",
|
nodeId: "ios-node",
|
||||||
command: "canvas.eval",
|
command: "canvas.eval",
|
||||||
params: { javaScript: "2+2" },
|
params: { javaScript: "2+2" },
|
||||||
timeoutMs: 123,
|
timeoutMs: 123,
|
||||||
idempotencyKey: "idem-1",
|
idempotencyKey: "idem-1",
|
||||||
});
|
});
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
|
|
||||||
expect(bridgeInvoke).toHaveBeenCalledWith(
|
expect(bridgeInvoke).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
nodeId: "ios-node",
|
nodeId: "ios-node",
|
||||||
command: "canvas.eval",
|
command: "canvas.eval",
|
||||||
paramsJSON: JSON.stringify({ javaScript: "2+2" }),
|
paramsJSON: JSON.stringify({ javaScript: "2+2" }),
|
||||||
timeoutMs: 123,
|
timeoutMs: 123,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
ws.close();
|
||||||
|
await server.close();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await fs.rm(homeDir, { recursive: true, force: true });
|
||||||
|
if (prevHome === undefined) {
|
||||||
|
delete process.env.HOME;
|
||||||
|
} else {
|
||||||
|
process.env.HOME = prevHome;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("node.describe returns supported invoke commands for paired nodes", async () => {
|
||||||
|
const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-home-"));
|
||||||
|
const prevHome = process.env.HOME;
|
||||||
|
process.env.HOME = homeDir;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { server, ws } = await startServerWithClient();
|
||||||
|
try {
|
||||||
|
await connectOk(ws);
|
||||||
|
|
||||||
|
const reqRes = await rpcReq<{
|
||||||
|
status?: string;
|
||||||
|
request?: { requestId?: string };
|
||||||
|
}>(ws, "node.pair.request", {
|
||||||
|
nodeId: "n1",
|
||||||
|
displayName: "iPad",
|
||||||
|
platform: "iPadOS",
|
||||||
|
version: "dev",
|
||||||
|
deviceFamily: "iPad",
|
||||||
|
modelIdentifier: "iPad16,6",
|
||||||
|
caps: ["canvas", "camera"],
|
||||||
|
commands: ["canvas.eval", "canvas.snapshot", "camera.snap"],
|
||||||
|
remoteIp: "10.0.0.10",
|
||||||
|
});
|
||||||
|
expect(reqRes.ok).toBe(true);
|
||||||
|
const requestId = reqRes.payload?.request?.requestId;
|
||||||
|
expect(typeof requestId).toBe("string");
|
||||||
|
|
||||||
|
const approveRes = await rpcReq(ws, "node.pair.approve", {
|
||||||
|
requestId,
|
||||||
|
});
|
||||||
|
expect(approveRes.ok).toBe(true);
|
||||||
|
|
||||||
|
const describeRes = await rpcReq<{ commands?: string[] }>(
|
||||||
|
ws,
|
||||||
|
"node.describe",
|
||||||
|
{ nodeId: "n1" },
|
||||||
|
);
|
||||||
|
expect(describeRes.ok).toBe(true);
|
||||||
|
expect(describeRes.payload?.commands).toEqual([
|
||||||
|
"camera.snap",
|
||||||
|
"canvas.eval",
|
||||||
|
"canvas.snapshot",
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
ws.close();
|
||||||
|
await server.close();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await fs.rm(homeDir, { recursive: true, force: true });
|
||||||
|
if (prevHome === undefined) {
|
||||||
|
delete process.env.HOME;
|
||||||
|
} else {
|
||||||
|
process.env.HOME = prevHome;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("node.list includes connected unpaired nodes with capabilities + commands", async () => {
|
||||||
|
const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-home-"));
|
||||||
|
const prevHome = process.env.HOME;
|
||||||
|
process.env.HOME = homeDir;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { server, ws } = await startServerWithClient();
|
||||||
|
try {
|
||||||
|
await connectOk(ws);
|
||||||
|
|
||||||
|
const reqRes = await rpcReq<{
|
||||||
|
status?: string;
|
||||||
|
request?: { requestId?: string };
|
||||||
|
}>(ws, "node.pair.request", {
|
||||||
|
nodeId: "p1",
|
||||||
|
displayName: "Paired",
|
||||||
|
platform: "iPadOS",
|
||||||
|
version: "dev",
|
||||||
|
deviceFamily: "iPad",
|
||||||
|
modelIdentifier: "iPad16,6",
|
||||||
|
caps: ["canvas"],
|
||||||
|
commands: ["canvas.eval"],
|
||||||
|
remoteIp: "10.0.0.10",
|
||||||
|
});
|
||||||
|
expect(reqRes.ok).toBe(true);
|
||||||
|
const requestId = reqRes.payload?.request?.requestId;
|
||||||
|
expect(typeof requestId).toBe("string");
|
||||||
|
|
||||||
|
const approveRes = await rpcReq(ws, "node.pair.approve", { requestId });
|
||||||
|
expect(approveRes.ok).toBe(true);
|
||||||
|
|
||||||
|
bridgeListConnected.mockReturnValueOnce([
|
||||||
|
{
|
||||||
|
nodeId: "p1",
|
||||||
|
displayName: "Paired Live",
|
||||||
|
platform: "iPadOS",
|
||||||
|
version: "dev-live",
|
||||||
|
remoteIp: "10.0.0.11",
|
||||||
|
deviceFamily: "iPad",
|
||||||
|
modelIdentifier: "iPad16,6",
|
||||||
|
caps: ["canvas", "camera"],
|
||||||
|
commands: ["canvas.snapshot", "canvas.eval"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nodeId: "u1",
|
||||||
|
displayName: "Unpaired Live",
|
||||||
|
platform: "Android",
|
||||||
|
version: "dev",
|
||||||
|
remoteIp: "10.0.0.12",
|
||||||
|
deviceFamily: "Android",
|
||||||
|
modelIdentifier: "samsung SM-X926B",
|
||||||
|
caps: ["canvas"],
|
||||||
|
commands: ["canvas.eval"],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const listRes = await rpcReq<{
|
||||||
|
nodes?: Array<{
|
||||||
|
nodeId: string;
|
||||||
|
paired?: boolean;
|
||||||
|
connected?: boolean;
|
||||||
|
caps?: string[];
|
||||||
|
commands?: string[];
|
||||||
|
displayName?: string;
|
||||||
|
remoteIp?: string;
|
||||||
|
}>;
|
||||||
|
}>(ws, "node.list", {});
|
||||||
|
expect(listRes.ok).toBe(true);
|
||||||
|
const nodes = listRes.payload?.nodes ?? [];
|
||||||
|
|
||||||
|
const pairedNode = nodes.find((n) => n.nodeId === "p1");
|
||||||
|
expect(pairedNode).toMatchObject({
|
||||||
|
nodeId: "p1",
|
||||||
|
paired: true,
|
||||||
|
connected: true,
|
||||||
|
displayName: "Paired Live",
|
||||||
|
remoteIp: "10.0.0.11",
|
||||||
|
});
|
||||||
|
expect(pairedNode?.caps?.slice().sort()).toEqual(["camera", "canvas"]);
|
||||||
|
expect(pairedNode?.commands?.slice().sort()).toEqual([
|
||||||
|
"canvas.eval",
|
||||||
|
"canvas.snapshot",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const unpairedNode = nodes.find((n) => n.nodeId === "u1");
|
||||||
|
expect(unpairedNode).toMatchObject({
|
||||||
|
nodeId: "u1",
|
||||||
|
paired: false,
|
||||||
|
connected: true,
|
||||||
|
displayName: "Unpaired Live",
|
||||||
|
});
|
||||||
|
expect(unpairedNode?.caps).toEqual(["canvas"]);
|
||||||
|
expect(unpairedNode?.commands).toEqual(["canvas.eval"]);
|
||||||
} finally {
|
} finally {
|
||||||
ws.close();
|
ws.close();
|
||||||
await server.close();
|
await server.close();
|
||||||
|
|
@ -2275,11 +2444,11 @@ describe("gateway server", () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const abortRes = await onceMessage(
|
const abortRes = await onceMessage(
|
||||||
ws,
|
ws,
|
||||||
(o) => o.type === "res" && o.id === "abort-mismatch-1",
|
(o) => o.type === "res" && o.id === "abort-mismatch-1",
|
||||||
10_000,
|
10_000,
|
||||||
);
|
);
|
||||||
expect(abortRes.ok).toBe(false);
|
expect(abortRes.ok).toBe(false);
|
||||||
expect(abortRes.error?.code).toBe("INVALID_REQUEST");
|
expect(abortRes.error?.code).toBe("INVALID_REQUEST");
|
||||||
|
|
||||||
|
|
@ -2292,18 +2461,18 @@ describe("gateway server", () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const abortRes2 = await onceMessage(
|
const abortRes2 = await onceMessage(
|
||||||
ws,
|
ws,
|
||||||
(o) => o.type === "res" && o.id === "abort-mismatch-2",
|
(o) => o.type === "res" && o.id === "abort-mismatch-2",
|
||||||
10_000,
|
10_000,
|
||||||
);
|
);
|
||||||
expect(abortRes2.ok).toBe(true);
|
expect(abortRes2.ok).toBe(true);
|
||||||
|
|
||||||
const sendRes = await onceMessage(
|
const sendRes = await onceMessage(
|
||||||
ws,
|
ws,
|
||||||
(o) => o.type === "res" && o.id === "send-mismatch-1",
|
(o) => o.type === "res" && o.id === "send-mismatch-1",
|
||||||
10_000,
|
10_000,
|
||||||
);
|
);
|
||||||
expect(sendRes.ok).toBe(true);
|
expect(sendRes.ok).toBe(true);
|
||||||
|
|
||||||
ws.close();
|
ws.close();
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ import {
|
||||||
validateCronRunsParams,
|
validateCronRunsParams,
|
||||||
validateCronStatusParams,
|
validateCronStatusParams,
|
||||||
validateCronUpdateParams,
|
validateCronUpdateParams,
|
||||||
|
validateNodeDescribeParams,
|
||||||
validateNodeInvokeParams,
|
validateNodeInvokeParams,
|
||||||
validateNodeListParams,
|
validateNodeListParams,
|
||||||
validateNodePairApproveParams,
|
validateNodePairApproveParams,
|
||||||
|
|
@ -194,6 +195,7 @@ const METHODS = [
|
||||||
"node.pair.reject",
|
"node.pair.reject",
|
||||||
"node.pair.verify",
|
"node.pair.verify",
|
||||||
"node.list",
|
"node.list",
|
||||||
|
"node.describe",
|
||||||
"node.invoke",
|
"node.invoke",
|
||||||
"cron.list",
|
"cron.list",
|
||||||
"cron.status",
|
"cron.status",
|
||||||
|
|
@ -2777,6 +2779,10 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
platform?: string;
|
platform?: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
|
deviceFamily?: string;
|
||||||
|
modelIdentifier?: string;
|
||||||
|
caps?: string[];
|
||||||
|
commands?: string[];
|
||||||
remoteIp?: string;
|
remoteIp?: string;
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
|
|
@ -2785,6 +2791,10 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||||
displayName: p.displayName,
|
displayName: p.displayName,
|
||||||
platform: p.platform,
|
platform: p.platform,
|
||||||
version: p.version,
|
version: p.version,
|
||||||
|
deviceFamily: p.deviceFamily,
|
||||||
|
modelIdentifier: p.modelIdentifier,
|
||||||
|
caps: p.caps,
|
||||||
|
commands: p.commands,
|
||||||
remoteIp: p.remoteIp,
|
remoteIp: p.remoteIp,
|
||||||
});
|
});
|
||||||
if (result.status === "pending" && result.created) {
|
if (result.status === "pending" && result.created) {
|
||||||
|
|
@ -2960,25 +2970,64 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const list = await listNodePairing();
|
const list = await listNodePairing();
|
||||||
|
const pairedById = new Map(
|
||||||
|
list.paired.map((n) => [n.nodeId, n]),
|
||||||
|
);
|
||||||
|
|
||||||
const connected = bridge?.listConnected?.() ?? [];
|
const connected = bridge?.listConnected?.() ?? [];
|
||||||
const connectedById = new Map(
|
const connectedById = new Map(
|
||||||
connected.map((n) => [n.nodeId, n]),
|
connected.map((n) => [n.nodeId, n]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const nodes = list.paired.map((n) => {
|
const nodeIds = new Set<string>([
|
||||||
const live = connectedById.get(n.nodeId);
|
...pairedById.keys(),
|
||||||
return {
|
...connectedById.keys(),
|
||||||
nodeId: n.nodeId,
|
]);
|
||||||
displayName: live?.displayName ?? n.displayName,
|
|
||||||
platform: live?.platform ?? n.platform,
|
const nodes = [...nodeIds].map((nodeId) => {
|
||||||
version: live?.version ?? n.version,
|
const paired = pairedById.get(nodeId);
|
||||||
deviceFamily: live?.deviceFamily ?? n.deviceFamily,
|
const live = connectedById.get(nodeId);
|
||||||
modelIdentifier: live?.modelIdentifier ?? n.modelIdentifier,
|
|
||||||
remoteIp: live?.remoteIp ?? n.remoteIp,
|
const caps = [
|
||||||
caps: live?.caps ?? n.caps,
|
...new Set(
|
||||||
connected: Boolean(live),
|
(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,
|
||||||
|
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);
|
respond(true, { ts: Date.now(), nodes }, undefined);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -2990,6 +3039,89 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "node.describe": {
|
||||||
|
const params = (req.params ?? {}) as Record<string, unknown>;
|
||||||
|
if (!validateNodeDescribeParams(params)) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(
|
||||||
|
ErrorCodes.INVALID_REQUEST,
|
||||||
|
`invalid node.describe params: ${formatValidationErrors(validateNodeDescribeParams.errors)}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const { nodeId } = params as { nodeId: string };
|
||||||
|
const id = String(nodeId ?? "").trim();
|
||||||
|
if (!id) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, "nodeId required"),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const list = await listNodePairing();
|
||||||
|
const paired = list.paired.find((n) => n.nodeId === id);
|
||||||
|
const connected = bridge?.listConnected?.() ?? [];
|
||||||
|
const live = connected.find((n) => n.nodeId === id);
|
||||||
|
|
||||||
|
if (!paired && !live) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, "unknown nodeId"),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
paired: Boolean(paired),
|
||||||
|
connected: Boolean(live),
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "node.invoke": {
|
case "node.invoke": {
|
||||||
const params = (req.params ?? {}) as Record<string, unknown>;
|
const params = (req.params ?? {}) as Record<string, unknown>;
|
||||||
if (!validateNodeInvokeParams(params)) {
|
if (!validateNodeInvokeParams(params)) {
|
||||||
|
|
@ -3011,20 +3143,20 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const p = params as {
|
const p = params as {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
command: string;
|
command: string;
|
||||||
params?: unknown;
|
params?: unknown;
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
idempotencyKey: string;
|
idempotencyKey: string;
|
||||||
};
|
};
|
||||||
const nodeId = String(p.nodeId ?? "").trim();
|
const nodeId = String(p.nodeId ?? "").trim();
|
||||||
const command = String(p.command ?? "").trim();
|
const command = String(p.command ?? "").trim();
|
||||||
if (!nodeId || !command) {
|
if (!nodeId || !command) {
|
||||||
respond(
|
respond(
|
||||||
false,
|
false,
|
||||||
undefined,
|
undefined,
|
||||||
errorShape(
|
errorShape(
|
||||||
ErrorCodes.INVALID_REQUEST,
|
ErrorCodes.INVALID_REQUEST,
|
||||||
"nodeId and command required",
|
"nodeId and command required",
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue