@alfe.ai/openclaw-chat 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin2.js +283 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +11 -3
package/dist/plugin.d.ts
CHANGED
|
@@ -149,6 +149,12 @@ interface AlfePluginConfig {
|
|
|
149
149
|
daemonSocket?: string;
|
|
150
150
|
/** Agent ID this plugin is associated with. */
|
|
151
151
|
agentId?: string;
|
|
152
|
+
/** Chat service WebSocket URL (e.g. wss://chat.dev.alfe.ai/ws) */
|
|
153
|
+
chatWsUrl?: string;
|
|
154
|
+
/** API key for chat service auth */
|
|
155
|
+
apiKey?: string;
|
|
156
|
+
/** OpenClaw local gateway URL override (default: ws://127.0.0.1:18789) */
|
|
157
|
+
openclawGatewayUrl?: string;
|
|
152
158
|
}
|
|
153
159
|
interface Logger {
|
|
154
160
|
info(msg: string, ...args: unknown[]): void;
|
package/dist/plugin2.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
+
import { ChatServiceClient } from "@alfe.ai/chat";
|
|
4
|
+
import WebSocket from "ws";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
3
6
|
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
4
7
|
import { existsSync } from "node:fs";
|
|
5
8
|
//#region src/alfe-channel.ts
|
|
@@ -134,6 +137,239 @@ function createAlfeChannelPlugin() {
|
|
|
134
137
|
};
|
|
135
138
|
}
|
|
136
139
|
//#endregion
|
|
140
|
+
//#region src/runtime-relay.ts
|
|
141
|
+
/**
|
|
142
|
+
* RuntimeRelay — bridges the chat service to the local OpenClaw runtime.
|
|
143
|
+
*
|
|
144
|
+
* Connects to OpenClaw's local WebSocket server (typically ws://127.0.0.1:18789),
|
|
145
|
+
* performs the protocol handshake, and forwards RPC requests from the chat
|
|
146
|
+
* service to the runtime. Responses and streaming events are routed back
|
|
147
|
+
* via callbacks.
|
|
148
|
+
*
|
|
149
|
+
* Modeled after the voice relay's LocalGatewayClient pattern.
|
|
150
|
+
*/
|
|
151
|
+
const RECONNECT_DELAYS = [
|
|
152
|
+
500,
|
|
153
|
+
1e3,
|
|
154
|
+
2e3,
|
|
155
|
+
4e3,
|
|
156
|
+
8e3,
|
|
157
|
+
15e3,
|
|
158
|
+
3e4
|
|
159
|
+
];
|
|
160
|
+
var RuntimeRelay = class {
|
|
161
|
+
ws = null;
|
|
162
|
+
pending = /* @__PURE__ */ new Map();
|
|
163
|
+
connected = false;
|
|
164
|
+
stopped = false;
|
|
165
|
+
retryCount = 0;
|
|
166
|
+
retryTimer = null;
|
|
167
|
+
connectSent = false;
|
|
168
|
+
wsUrl;
|
|
169
|
+
token;
|
|
170
|
+
log;
|
|
171
|
+
constructor(options) {
|
|
172
|
+
this.options = options;
|
|
173
|
+
this.wsUrl = options.wsUrl ?? "ws://127.0.0.1:18789";
|
|
174
|
+
this.token = options.token ?? "";
|
|
175
|
+
this.log = options.logger;
|
|
176
|
+
}
|
|
177
|
+
get isConnected() {
|
|
178
|
+
return this.connected && this.ws?.readyState === WebSocket.OPEN;
|
|
179
|
+
}
|
|
180
|
+
start() {
|
|
181
|
+
this.log.debug("RuntimeRelay starting...");
|
|
182
|
+
this.stopped = false;
|
|
183
|
+
this.doConnect();
|
|
184
|
+
}
|
|
185
|
+
stop() {
|
|
186
|
+
this.log.debug("RuntimeRelay stopping...");
|
|
187
|
+
this.stopped = true;
|
|
188
|
+
if (this.retryTimer) {
|
|
189
|
+
clearTimeout(this.retryTimer);
|
|
190
|
+
this.retryTimer = null;
|
|
191
|
+
}
|
|
192
|
+
if (this.ws) {
|
|
193
|
+
try {
|
|
194
|
+
this.ws.close(1e3);
|
|
195
|
+
} catch {}
|
|
196
|
+
this.ws = null;
|
|
197
|
+
}
|
|
198
|
+
this.connected = false;
|
|
199
|
+
this.flushPending(/* @__PURE__ */ new Error("RuntimeRelay stopped"));
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Forward an RPC request from the chat service to the runtime.
|
|
203
|
+
* The response routes back via the callback.
|
|
204
|
+
*/
|
|
205
|
+
forwardRequest(msg, onResponse) {
|
|
206
|
+
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
207
|
+
onResponse({
|
|
208
|
+
id: msg.id,
|
|
209
|
+
ok: false,
|
|
210
|
+
error: { message: "Runtime not connected" }
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const timeoutMs = msg.method === "agent" ? 13e4 : 3e4;
|
|
215
|
+
const timer = setTimeout(() => {
|
|
216
|
+
this.pending.delete(msg.id);
|
|
217
|
+
onResponse({
|
|
218
|
+
id: msg.id,
|
|
219
|
+
ok: false,
|
|
220
|
+
error: { message: `'${msg.method}' timed out` }
|
|
221
|
+
});
|
|
222
|
+
}, timeoutMs);
|
|
223
|
+
this.pending.set(msg.id, {
|
|
224
|
+
resolve: (payload) => {
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
onResponse({
|
|
227
|
+
id: msg.id,
|
|
228
|
+
ok: true,
|
|
229
|
+
payload
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
reject: (err) => {
|
|
233
|
+
clearTimeout(timer);
|
|
234
|
+
onResponse({
|
|
235
|
+
id: msg.id,
|
|
236
|
+
ok: false,
|
|
237
|
+
error: { message: err.message }
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
expectFinal: msg.method === "agent"
|
|
241
|
+
});
|
|
242
|
+
this.ws.send(JSON.stringify({
|
|
243
|
+
type: "req",
|
|
244
|
+
id: msg.id,
|
|
245
|
+
method: msg.method,
|
|
246
|
+
params: msg.params
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
doConnect() {
|
|
250
|
+
if (this.stopped) return;
|
|
251
|
+
this.log.info(`Connecting to runtime at ${this.wsUrl}...`);
|
|
252
|
+
this.ws = new WebSocket(this.wsUrl, { maxPayload: 25 * 1024 * 1024 });
|
|
253
|
+
this.ws.on("open", () => {
|
|
254
|
+
this.log.info("Connected to runtime — authenticating...");
|
|
255
|
+
this.retryCount = 0;
|
|
256
|
+
setTimeout(() => {
|
|
257
|
+
this.sendConnect();
|
|
258
|
+
}, 750);
|
|
259
|
+
});
|
|
260
|
+
this.ws.on("message", (data) => {
|
|
261
|
+
const text = Buffer.isBuffer(data) ? data.toString("utf-8") : Buffer.from(data).toString("utf-8");
|
|
262
|
+
this.handleMessage(text);
|
|
263
|
+
});
|
|
264
|
+
this.ws.on("close", (code) => {
|
|
265
|
+
this.log.warn(`Runtime disconnected (${String(code)})`);
|
|
266
|
+
this.connected = false;
|
|
267
|
+
this.flushPending(/* @__PURE__ */ new Error(`Runtime closed (${String(code)})`));
|
|
268
|
+
this.scheduleReconnect();
|
|
269
|
+
});
|
|
270
|
+
this.ws.on("error", (err) => {
|
|
271
|
+
this.log.error(`Runtime WS error: ${err.message}`);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
async sendConnect() {
|
|
275
|
+
if (this.connectSent) return;
|
|
276
|
+
this.connectSent = true;
|
|
277
|
+
const id = randomUUID();
|
|
278
|
+
const timeoutMs = 1e4;
|
|
279
|
+
try {
|
|
280
|
+
await new Promise((resolve, reject) => {
|
|
281
|
+
const timer = setTimeout(() => {
|
|
282
|
+
this.pending.delete(id);
|
|
283
|
+
reject(/* @__PURE__ */ new Error("Connect handshake timed out"));
|
|
284
|
+
}, timeoutMs);
|
|
285
|
+
this.pending.set(id, {
|
|
286
|
+
resolve: () => {
|
|
287
|
+
clearTimeout(timer);
|
|
288
|
+
resolve();
|
|
289
|
+
},
|
|
290
|
+
reject: (err) => {
|
|
291
|
+
clearTimeout(timer);
|
|
292
|
+
reject(err);
|
|
293
|
+
},
|
|
294
|
+
expectFinal: false
|
|
295
|
+
});
|
|
296
|
+
this.ws?.send(JSON.stringify({
|
|
297
|
+
type: "req",
|
|
298
|
+
id,
|
|
299
|
+
method: "connect",
|
|
300
|
+
params: {
|
|
301
|
+
minProtocol: 3,
|
|
302
|
+
maxProtocol: 3,
|
|
303
|
+
client: {
|
|
304
|
+
id: "chat-relay",
|
|
305
|
+
displayName: "Alfe Chat Relay",
|
|
306
|
+
version: "1.0.0",
|
|
307
|
+
platform: process.platform,
|
|
308
|
+
mode: "backend",
|
|
309
|
+
instanceId: `chat-${Date.now().toString(36)}`
|
|
310
|
+
},
|
|
311
|
+
caps: [],
|
|
312
|
+
role: "operator",
|
|
313
|
+
scopes: ["operator.admin"],
|
|
314
|
+
auth: this.token ? { token: this.token } : void 0
|
|
315
|
+
}
|
|
316
|
+
}));
|
|
317
|
+
});
|
|
318
|
+
this.connected = true;
|
|
319
|
+
this.log.info("Runtime authenticated — relay active");
|
|
320
|
+
} catch (err) {
|
|
321
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
322
|
+
this.log.error(`Runtime connect failed: ${errMsg}`);
|
|
323
|
+
this.ws?.close(1008, "connect failed");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
handleMessage(raw) {
|
|
327
|
+
try {
|
|
328
|
+
const parsed = JSON.parse(raw);
|
|
329
|
+
if (parsed.event) {
|
|
330
|
+
const payload = parsed.payload;
|
|
331
|
+
if (parsed.event === "connect.challenge" && payload?.nonce) {
|
|
332
|
+
this.connectSent = false;
|
|
333
|
+
this.sendConnect();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (payload) this.options.onEvent(parsed.event, payload);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if ("id" in parsed && "ok" in parsed) {
|
|
340
|
+
const pending = this.pending.get(parsed.id);
|
|
341
|
+
if (!pending) return;
|
|
342
|
+
const payload = parsed.payload;
|
|
343
|
+
if (pending.expectFinal && payload?.status === "accepted") return;
|
|
344
|
+
this.pending.delete(parsed.id);
|
|
345
|
+
if (parsed.ok) pending.resolve(payload);
|
|
346
|
+
else {
|
|
347
|
+
const error = parsed.error;
|
|
348
|
+
pending.reject(new Error(error?.message ?? "unknown error"));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} catch (err) {
|
|
352
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
353
|
+
this.log.error(`Runtime message parse error: ${errMsg}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
scheduleReconnect() {
|
|
357
|
+
if (this.stopped) return;
|
|
358
|
+
const delay = RECONNECT_DELAYS[Math.min(this.retryCount, RECONNECT_DELAYS.length - 1)];
|
|
359
|
+
this.retryCount++;
|
|
360
|
+
this.connectSent = false;
|
|
361
|
+
this.log.info(`Reconnecting to runtime in ${String(delay)}ms (attempt ${String(this.retryCount)})...`);
|
|
362
|
+
this.retryTimer = setTimeout(() => {
|
|
363
|
+
this.retryTimer = null;
|
|
364
|
+
this.doConnect();
|
|
365
|
+
}, delay);
|
|
366
|
+
}
|
|
367
|
+
flushPending(err) {
|
|
368
|
+
for (const [, p] of this.pending) p.reject(err);
|
|
369
|
+
this.pending.clear();
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
//#endregion
|
|
137
373
|
//#region src/session-store.ts
|
|
138
374
|
/**
|
|
139
375
|
* Session Store — persists chat sessions to the local filesystem.
|
|
@@ -246,6 +482,8 @@ const CHAT_CAPABILITIES = [
|
|
|
246
482
|
"chat.sessions"
|
|
247
483
|
];
|
|
248
484
|
let daemonIpcClient = null;
|
|
485
|
+
let chatClient = null;
|
|
486
|
+
let runtimeRelay = null;
|
|
249
487
|
/**
|
|
250
488
|
* Attempt to connect to the Alfe daemon IPC socket.
|
|
251
489
|
* Returns null if @alfe.ai/openclaw is not available or daemon is unreachable.
|
|
@@ -294,6 +532,41 @@ const plugin = {
|
|
|
294
532
|
api.registerChannel(alfeChannel);
|
|
295
533
|
log.info(`Registered channel: ${alfeChannel.id} (${alfeChannel.meta.label})`);
|
|
296
534
|
daemonIpcClient = await connectToDaemon(pluginConfig.daemonSocket ?? process.env.ALFE_GATEWAY_SOCKET ?? DEFAULT_SOCKET_PATH, log);
|
|
535
|
+
const chatWsUrl = pluginConfig.chatWsUrl ?? process.env.ALFE_CHAT_WS_URL;
|
|
536
|
+
const apiKey = pluginConfig.apiKey ?? process.env.ALFE_API_KEY;
|
|
537
|
+
const runtimeUrl = pluginConfig.openclawGatewayUrl ?? process.env.OPENCLAW_GATEWAY_URL ?? "ws://127.0.0.1:18789";
|
|
538
|
+
const runtimeToken = process.env.OPENCLAW_GATEWAY_TOKEN ?? "";
|
|
539
|
+
if (chatWsUrl && apiKey) {
|
|
540
|
+
log.info(`Connecting to chat service: ${chatWsUrl}`);
|
|
541
|
+
runtimeRelay = new RuntimeRelay({
|
|
542
|
+
wsUrl: runtimeUrl,
|
|
543
|
+
token: runtimeToken,
|
|
544
|
+
onEvent: (event, payload) => {
|
|
545
|
+
chatClient?.sendEvent(event, payload);
|
|
546
|
+
},
|
|
547
|
+
logger: log
|
|
548
|
+
});
|
|
549
|
+
chatClient = new ChatServiceClient({
|
|
550
|
+
wsUrl: chatWsUrl,
|
|
551
|
+
apiKey,
|
|
552
|
+
onRequest: (request) => {
|
|
553
|
+
if (!runtimeRelay?.isConnected) {
|
|
554
|
+
chatClient?.sendResponse(request.id, false, { message: "Runtime not connected" });
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
runtimeRelay.forwardRequest(request, (response) => {
|
|
558
|
+
chatClient?.send(response);
|
|
559
|
+
});
|
|
560
|
+
},
|
|
561
|
+
onConnectionChange: (connected) => {
|
|
562
|
+
log.info(`Chat service connection: ${connected ? "connected" : "disconnected"}`);
|
|
563
|
+
},
|
|
564
|
+
logger: log
|
|
565
|
+
});
|
|
566
|
+
runtimeRelay.start();
|
|
567
|
+
chatClient.start();
|
|
568
|
+
log.info("Chat service relay started");
|
|
569
|
+
} else log.info("Chat service URL not configured — running without chat service relay");
|
|
297
570
|
if (typeof api.registerGatewayMethod === "function") {
|
|
298
571
|
api.registerGatewayMethod("chat.send", (...args) => {
|
|
299
572
|
const { sessionId, content, clientType } = args[0];
|
|
@@ -365,6 +638,16 @@ const plugin = {
|
|
|
365
638
|
globalThis.__alfeChatPluginActivated = false;
|
|
366
639
|
const log = api.logger;
|
|
367
640
|
log.info("Alfe Chat plugin deactivating...");
|
|
641
|
+
if (chatClient) {
|
|
642
|
+
chatClient.stop();
|
|
643
|
+
chatClient = null;
|
|
644
|
+
log.info("Chat service client stopped");
|
|
645
|
+
}
|
|
646
|
+
if (runtimeRelay) {
|
|
647
|
+
runtimeRelay.stop();
|
|
648
|
+
runtimeRelay = null;
|
|
649
|
+
log.info("Runtime relay stopped");
|
|
650
|
+
}
|
|
368
651
|
if (daemonIpcClient) {
|
|
369
652
|
try {
|
|
370
653
|
daemonIpcClient.stop();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/openclaw-chat",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/plugin.js",
|
|
@@ -21,13 +21,21 @@
|
|
|
21
21
|
]
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
|
-
"dist"
|
|
24
|
+
"dist",
|
|
25
|
+
"openclaw.plugin.json"
|
|
25
26
|
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"ws": "^8.18.0",
|
|
29
|
+
"@alfe.ai/chat": "^0.0.1"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/ws": "^8.5.13"
|
|
33
|
+
},
|
|
26
34
|
"license": "UNLICENSED",
|
|
27
35
|
"scripts": {
|
|
28
36
|
"build": "tsdown",
|
|
29
37
|
"dev": "tsdown --watch",
|
|
30
|
-
"test": "vitest run",
|
|
38
|
+
"test": "vitest run --passWithNoTests",
|
|
31
39
|
"test:watch": "vitest",
|
|
32
40
|
"typecheck": "tsc --noEmit",
|
|
33
41
|
"lint": "eslint ."
|