mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-17 15:47:28 +00:00
133 lines
3.6 KiB
TypeScript
133 lines
3.6 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import WebSocket from "ws";
|
|
|
|
export type GatewayReqFrame = { type: "req"; id: string; method: string; params?: unknown };
|
|
export type GatewayResFrame = {
|
|
type: "res";
|
|
id: string;
|
|
ok: boolean;
|
|
payload?: unknown;
|
|
error?: unknown;
|
|
};
|
|
export type GatewayEventFrame = { type: "event"; event: string; seq?: number; payload?: unknown };
|
|
export type GatewayFrame =
|
|
| GatewayReqFrame
|
|
| GatewayResFrame
|
|
| GatewayEventFrame
|
|
| { type: string; [key: string]: unknown };
|
|
|
|
export function createArgReader(argv = process.argv.slice(2)) {
|
|
const get = (flag: string) => {
|
|
const idx = argv.indexOf(flag);
|
|
if (idx !== -1 && idx + 1 < argv.length) {
|
|
return argv[idx + 1];
|
|
}
|
|
return undefined;
|
|
};
|
|
const has = (flag: string) => argv.includes(flag);
|
|
return { argv, get, has };
|
|
}
|
|
|
|
export function resolveGatewayUrl(urlRaw: string): URL {
|
|
const url = new URL(urlRaw.includes("://") ? urlRaw : `wss://${urlRaw}`);
|
|
if (!url.port) {
|
|
url.port = url.protocol === "wss:" ? "443" : "80";
|
|
}
|
|
return url;
|
|
}
|
|
|
|
function toText(data: WebSocket.RawData): string {
|
|
if (typeof data === "string") {
|
|
return data;
|
|
}
|
|
if (data instanceof ArrayBuffer) {
|
|
return Buffer.from(data).toString("utf8");
|
|
}
|
|
if (Array.isArray(data)) {
|
|
return Buffer.concat(data.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
}
|
|
return Buffer.from(data as Buffer).toString("utf8");
|
|
}
|
|
|
|
export function createGatewayWsClient(params: {
|
|
url: string;
|
|
handshakeTimeoutMs?: number;
|
|
openTimeoutMs?: number;
|
|
onEvent?: (evt: GatewayEventFrame) => void;
|
|
}) {
|
|
const ws = new WebSocket(params.url, { handshakeTimeout: params.handshakeTimeoutMs ?? 8000 });
|
|
const pending = new Map<
|
|
string,
|
|
{
|
|
resolve: (res: GatewayResFrame) => void;
|
|
reject: (err: Error) => void;
|
|
timeout: ReturnType<typeof setTimeout>;
|
|
}
|
|
>();
|
|
|
|
const request = (method: string, paramsObj?: unknown, timeoutMs = 12_000) =>
|
|
new Promise<GatewayResFrame>((resolve, reject) => {
|
|
const id = randomUUID();
|
|
const frame: GatewayReqFrame = { type: "req", id, method, params: paramsObj };
|
|
const timeout = setTimeout(() => {
|
|
pending.delete(id);
|
|
reject(new Error(`timeout waiting for ${method}`));
|
|
}, timeoutMs);
|
|
pending.set(id, { resolve, reject, timeout });
|
|
ws.send(JSON.stringify(frame));
|
|
});
|
|
|
|
const waitOpen = () =>
|
|
new Promise<void>((resolve, reject) => {
|
|
const t = setTimeout(
|
|
() => reject(new Error("ws open timeout")),
|
|
params.openTimeoutMs ?? 8000,
|
|
);
|
|
ws.once("open", () => {
|
|
clearTimeout(t);
|
|
resolve();
|
|
});
|
|
ws.once("error", (err) => {
|
|
clearTimeout(t);
|
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
});
|
|
});
|
|
|
|
ws.on("message", (data) => {
|
|
const text = toText(data);
|
|
let frame: GatewayFrame | null = null;
|
|
try {
|
|
frame = JSON.parse(text) as GatewayFrame;
|
|
} catch {
|
|
return;
|
|
}
|
|
if (!frame || typeof frame !== "object" || !("type" in frame)) {
|
|
return;
|
|
}
|
|
if (frame.type === "res") {
|
|
const res = frame as GatewayResFrame;
|
|
const waiter = pending.get(res.id);
|
|
if (waiter) {
|
|
pending.delete(res.id);
|
|
clearTimeout(waiter.timeout);
|
|
waiter.resolve(res);
|
|
}
|
|
return;
|
|
}
|
|
if (frame.type === "event") {
|
|
const evt = frame as GatewayEventFrame;
|
|
params.onEvent?.(evt);
|
|
}
|
|
});
|
|
|
|
const close = () => {
|
|
for (const waiter of pending.values()) {
|
|
clearTimeout(waiter.timeout);
|
|
}
|
|
pending.clear();
|
|
ws.close();
|
|
};
|
|
|
|
return { ws, request, waitOpen, close };
|
|
}
|