104 lines
2.6 KiB
TypeScript
104 lines
2.6 KiB
TypeScript
/**
|
|
* OpenClaw Gateway WebSocket client.
|
|
* Connects to the local OpenClaw gateway (ws://127.0.0.1:18789)
|
|
* and provides a simple RPC interface.
|
|
*/
|
|
import WebSocket from 'ws';
|
|
|
|
interface RpcFrame {
|
|
id: string;
|
|
method: string;
|
|
params?: unknown;
|
|
}
|
|
|
|
interface RpcResponse {
|
|
id: string;
|
|
result?: unknown;
|
|
error?: { message: string };
|
|
}
|
|
|
|
export class OpenClawClient {
|
|
private ws: WebSocket | null = null;
|
|
private pending = new Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
|
|
private readonly gatewayUrl: string;
|
|
private readonly token: string;
|
|
private msgCounter = 0;
|
|
private connected = false;
|
|
|
|
constructor(gatewayUrl: string, token: string) {
|
|
this.gatewayUrl = gatewayUrl;
|
|
this.token = token;
|
|
}
|
|
|
|
connect(): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
this.ws = new WebSocket(this.gatewayUrl, {
|
|
headers: { Authorization: `Bearer ${this.token}` },
|
|
});
|
|
|
|
this.ws.once('open', () => {
|
|
this.connected = true;
|
|
resolve();
|
|
});
|
|
|
|
this.ws.once('error', (err) => {
|
|
if (!this.connected) reject(err);
|
|
});
|
|
|
|
this.ws.on('message', (raw) => {
|
|
try {
|
|
const frame: RpcResponse = JSON.parse(raw.toString());
|
|
const pending = this.pending.get(frame.id);
|
|
if (!pending) return;
|
|
this.pending.delete(frame.id);
|
|
if (frame.error) {
|
|
pending.reject(new Error(frame.error.message));
|
|
} else {
|
|
pending.resolve(frame.result);
|
|
}
|
|
} catch {
|
|
// ignore malformed frames
|
|
}
|
|
});
|
|
|
|
this.ws.on('close', () => {
|
|
this.connected = false;
|
|
// Reject all pending RPCs
|
|
for (const [, p] of this.pending) {
|
|
p.reject(new Error('OpenClaw gateway disconnected'));
|
|
}
|
|
this.pending.clear();
|
|
});
|
|
});
|
|
}
|
|
|
|
isConnected(): boolean {
|
|
return this.connected && this.ws?.readyState === WebSocket.OPEN;
|
|
}
|
|
|
|
rpc(method: string, params?: unknown): Promise<unknown> {
|
|
return new Promise((resolve, reject) => {
|
|
if (!this.isConnected()) {
|
|
reject(new Error('Not connected to OpenClaw gateway'));
|
|
return;
|
|
}
|
|
const id = String(++this.msgCounter);
|
|
const frame: RpcFrame = { id, method, params };
|
|
this.pending.set(id, { resolve, reject });
|
|
this.ws!.send(JSON.stringify(frame));
|
|
|
|
// Timeout after 30s
|
|
setTimeout(() => {
|
|
if (this.pending.has(id)) {
|
|
this.pending.delete(id);
|
|
reject(new Error(`RPC timeout: ${method}`));
|
|
}
|
|
}, 30_000);
|
|
});
|
|
}
|
|
|
|
close(): void {
|
|
this.ws?.close();
|
|
}
|
|
}
|