@alfe.ai/openclaw-chat 0.0.4 → 0.0.6
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/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/plugin.d.ts +93 -82
- package/dist/plugin2.js +153 -354
- package/package.json +1 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as AlfeResolvedAccount,
|
|
1
|
+
import { a as AlfeResolvedAccount, i as AlfePluginConfig, n as createAlfeChannelPlugin, r as AlfeChannelConfig, t as plugin } from "./plugin.js";
|
|
2
2
|
|
|
3
3
|
//#region src/session-store.d.ts
|
|
4
4
|
|
|
@@ -10,6 +10,7 @@ import { a as AlfeResolvedAccount, c as OpenClawConfig, d as createAlfeChannelPl
|
|
|
10
10
|
*
|
|
11
11
|
* Each session file contains metadata and the full message history.
|
|
12
12
|
* Sessions are written on every message to ensure durability.
|
|
13
|
+
* Old sessions are cleaned up automatically (30-day TTL, 1000 max).
|
|
13
14
|
*/
|
|
14
15
|
interface ChatMessage {
|
|
15
16
|
role: 'user' | 'assistant';
|
|
@@ -36,4 +37,4 @@ interface SessionSummary {
|
|
|
36
37
|
messageCount: number;
|
|
37
38
|
}
|
|
38
39
|
//#endregion
|
|
39
|
-
export {
|
|
40
|
+
export { type AlfeChannelConfig, type AlfePluginConfig, type AlfeResolvedAccount, type ChatMessage, type SessionData, type SessionSummary, createAlfeChannelPlugin, plugin as default };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { n as createAlfeChannelPlugin, t as plugin } from "./plugin2.js";
|
|
2
|
-
export { createAlfeChannelPlugin, plugin as default
|
|
2
|
+
export { createAlfeChannelPlugin, plugin as default };
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Types for the Alfe chat channel plugin.
|
|
4
|
+
*
|
|
5
|
+
* SDK types (PluginRuntime, OpenClawPluginApi, OpenClawConfig, etc.)
|
|
6
|
+
* are imported from openclaw/plugin-sdk at usage sites.
|
|
7
|
+
* This file only contains Alfe-specific domain types.
|
|
8
|
+
*/
|
|
9
|
+
interface AlfeChannelAccountConfig {
|
|
10
|
+
/** Whether this account is enabled. */
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
/** Allowed sender identifiers (user IDs, email addresses). */
|
|
13
|
+
allowFrom?: string | string[];
|
|
14
|
+
/** Default delivery target. */
|
|
15
|
+
defaultTo?: string;
|
|
16
|
+
/** DM policy (open, allowlist, etc.). */
|
|
17
|
+
dmPolicy?: string;
|
|
18
|
+
}
|
|
19
|
+
interface AlfeChannelConfig {
|
|
20
|
+
/** Whether the Alfe channel is enabled. */
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
/** Allowed sender identifiers. */
|
|
23
|
+
allowFrom?: string | string[];
|
|
24
|
+
/** Default delivery target for outbound messages. */
|
|
25
|
+
defaultTo?: string;
|
|
26
|
+
/** DM policy. */
|
|
27
|
+
dmPolicy?: string;
|
|
28
|
+
/** Named accounts (multi-account support). */
|
|
29
|
+
accounts?: Record<string, AlfeChannelAccountConfig>;
|
|
30
|
+
}
|
|
31
|
+
interface AlfeResolvedAccount {
|
|
32
|
+
accountId: string;
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
allowFrom: string[];
|
|
35
|
+
defaultTo?: string;
|
|
36
|
+
dmPolicy?: string;
|
|
37
|
+
}
|
|
38
|
+
interface AlfePluginConfig {
|
|
39
|
+
/** Agent ID this plugin is associated with. */
|
|
40
|
+
agentId?: string;
|
|
41
|
+
/** Chat service WebSocket URL (e.g. wss://chat.dev.alfe.ai/ws) */
|
|
42
|
+
chatWsUrl?: string;
|
|
43
|
+
/** API key for chat service auth */
|
|
44
|
+
apiKey?: string;
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
1
47
|
//#region src/alfe-channel.d.ts
|
|
2
|
-
|
|
48
|
+
/** OpenClaw config shape — inline to avoid runtime dependency on openclaw package */
|
|
49
|
+
interface OpenClawConfig {
|
|
50
|
+
channels?: {
|
|
51
|
+
alfe?: AlfeChannelConfig;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
};
|
|
54
|
+
[key: string]: unknown;
|
|
55
|
+
}
|
|
3
56
|
/**
|
|
4
57
|
* Creates the Alfe ChannelPlugin object for registration with OpenClaw.
|
|
5
58
|
*
|
|
@@ -108,106 +161,64 @@ declare function createAlfeChannelPlugin(): {
|
|
|
108
161
|
};
|
|
109
162
|
};
|
|
110
163
|
//#endregion
|
|
111
|
-
//#region src/
|
|
112
|
-
|
|
113
|
-
* Types for the Alfe chat channel plugin.
|
|
114
|
-
*
|
|
115
|
-
* The Alfe channel registers with OpenClaw as a first-class channel,
|
|
116
|
-
* allowing web and mobile clients to share conversation sessions.
|
|
117
|
-
*/
|
|
118
|
-
interface AlfeChannelAccountConfig {
|
|
119
|
-
/** Whether this account is enabled. */
|
|
120
|
-
enabled?: boolean;
|
|
121
|
-
/** Allowed sender identifiers (user IDs, email addresses). */
|
|
122
|
-
allowFrom?: string | string[];
|
|
123
|
-
/** Default delivery target. */
|
|
124
|
-
defaultTo?: string;
|
|
125
|
-
/** DM policy (open, allowlist, etc.). */
|
|
126
|
-
dmPolicy?: string;
|
|
127
|
-
}
|
|
128
|
-
interface AlfeChannelConfig {
|
|
129
|
-
/** Whether the Alfe channel is enabled. */
|
|
130
|
-
enabled?: boolean;
|
|
131
|
-
/** Allowed sender identifiers. */
|
|
132
|
-
allowFrom?: string | string[];
|
|
133
|
-
/** Default delivery target for outbound messages. */
|
|
134
|
-
defaultTo?: string;
|
|
135
|
-
/** DM policy. */
|
|
136
|
-
dmPolicy?: string;
|
|
137
|
-
/** Named accounts (multi-account support). */
|
|
138
|
-
accounts?: Record<string, AlfeChannelAccountConfig>;
|
|
139
|
-
}
|
|
140
|
-
interface AlfeResolvedAccount {
|
|
141
|
-
accountId: string;
|
|
142
|
-
enabled: boolean;
|
|
143
|
-
allowFrom: string[];
|
|
144
|
-
defaultTo?: string;
|
|
145
|
-
dmPolicy?: string;
|
|
146
|
-
}
|
|
147
|
-
interface AlfePluginConfig {
|
|
148
|
-
/** Alfe daemon IPC socket path override. */
|
|
149
|
-
daemonSocket?: string;
|
|
150
|
-
/** Agent ID this plugin is associated with. */
|
|
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;
|
|
158
|
-
}
|
|
159
|
-
interface Logger {
|
|
164
|
+
//#region src/plugin.d.ts
|
|
165
|
+
interface PluginLogger {
|
|
160
166
|
info(msg: string, ...args: unknown[]): void;
|
|
161
167
|
warn(msg: string, ...args: unknown[]): void;
|
|
162
168
|
error(msg: string, ...args: unknown[]): void;
|
|
163
169
|
debug(msg: string, ...args: unknown[]): void;
|
|
164
170
|
}
|
|
165
|
-
interface
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
171
|
+
interface PluginRuntime {
|
|
172
|
+
config: {
|
|
173
|
+
loadConfig(): Record<string, unknown>;
|
|
174
|
+
};
|
|
175
|
+
events: {
|
|
176
|
+
onAgentEvent(listener: (evt: AgentEventPayload) => void): () => void;
|
|
169
177
|
};
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
178
|
+
subagent: {
|
|
179
|
+
run(params: {
|
|
180
|
+
sessionKey: string;
|
|
181
|
+
message: string;
|
|
182
|
+
idempotencyKey?: string;
|
|
183
|
+
deliver?: boolean;
|
|
184
|
+
}): Promise<{
|
|
185
|
+
runId: string;
|
|
186
|
+
}>;
|
|
187
|
+
waitForRun(params: {
|
|
188
|
+
runId: string;
|
|
189
|
+
timeoutMs?: number;
|
|
190
|
+
}): Promise<{
|
|
191
|
+
status: string;
|
|
192
|
+
error?: string;
|
|
174
193
|
}>;
|
|
175
|
-
[key: string]: unknown;
|
|
176
194
|
};
|
|
177
|
-
|
|
195
|
+
channel: unknown;
|
|
196
|
+
}
|
|
197
|
+
interface AgentEventPayload {
|
|
198
|
+
runId: string;
|
|
199
|
+
seq: number;
|
|
200
|
+
stream: string;
|
|
201
|
+
ts: number;
|
|
202
|
+
data: Record<string, unknown>;
|
|
203
|
+
sessionKey?: string;
|
|
178
204
|
}
|
|
179
|
-
interface
|
|
180
|
-
logger:
|
|
181
|
-
config?:
|
|
205
|
+
interface PluginApi {
|
|
206
|
+
logger: PluginLogger;
|
|
207
|
+
config?: Record<string, unknown>;
|
|
208
|
+
runtime?: PluginRuntime;
|
|
182
209
|
registerChannel(channel: ReturnType<typeof createAlfeChannelPlugin>): void;
|
|
183
210
|
registerGatewayMethod?(name: string, handler: (...args: unknown[]) => Promise<unknown>): void;
|
|
184
211
|
on(event: string, handler: (...args: unknown[]) => void | Promise<void>, options?: {
|
|
185
212
|
priority?: number;
|
|
186
213
|
}): void;
|
|
187
214
|
}
|
|
188
|
-
interface IPCClient {
|
|
189
|
-
on(event: string, handler: (...args: unknown[]) => void | Promise<void>): void;
|
|
190
|
-
start(): void;
|
|
191
|
-
stop(): void;
|
|
192
|
-
request(method: string, params: Record<string, unknown>): Promise<{
|
|
193
|
-
ok: boolean;
|
|
194
|
-
error?: {
|
|
195
|
-
message: string;
|
|
196
|
-
};
|
|
197
|
-
}>;
|
|
198
|
-
}
|
|
199
|
-
interface OpenClawModule {
|
|
200
|
-
IPCClient: new (socketPath: string, log: Logger) => IPCClient;
|
|
201
|
-
}
|
|
202
|
-
//#endregion
|
|
203
|
-
//#region src/plugin.d.ts
|
|
204
215
|
declare const plugin: {
|
|
205
216
|
id: string;
|
|
206
217
|
name: string;
|
|
207
218
|
description: string;
|
|
208
219
|
version: string;
|
|
209
|
-
activate(api:
|
|
210
|
-
deactivate(api:
|
|
220
|
+
activate(api: PluginApi): void;
|
|
221
|
+
deactivate(api: PluginApi): void;
|
|
211
222
|
};
|
|
212
223
|
//#endregion
|
|
213
|
-
export { AlfeResolvedAccount as a,
|
|
224
|
+
export { AlfeResolvedAccount as a, AlfePluginConfig as i, createAlfeChannelPlugin as n, AlfeChannelConfig as r, plugin as t };
|
package/dist/plugin2.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
+
import { ChatServiceClient, resolveAlfeChat } from "@alfe.ai/chat";
|
|
2
|
+
import { mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
1
3
|
import { join } from "node:path";
|
|
2
4
|
import { homedir } from "node:os";
|
|
3
|
-
import { ChatServiceClient, resolveAlfeChat } from "@alfe.ai/chat";
|
|
4
|
-
import WebSocket from "ws";
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
6
|
-
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
7
5
|
import { existsSync } from "node:fs";
|
|
8
6
|
//#region src/alfe-channel.ts
|
|
9
7
|
const CHANNEL_ID = "alfe";
|
|
@@ -137,238 +135,36 @@ function createAlfeChannelPlugin() {
|
|
|
137
135
|
};
|
|
138
136
|
}
|
|
139
137
|
//#endregion
|
|
140
|
-
//#region src/
|
|
138
|
+
//#region src/session-keys.ts
|
|
141
139
|
/**
|
|
142
|
-
*
|
|
140
|
+
* Session key helpers — handles both raw and prefixed formats.
|
|
143
141
|
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* service to the runtime. Responses and streaming events are routed back
|
|
147
|
-
* via callbacks.
|
|
142
|
+
* Raw format: "chat-{tenantId}-{agentId}-{suffix}"
|
|
143
|
+
* Prefixed format: "agent:{agentId}:chat-{tenantId}-{agentId}-{suffix}"
|
|
148
144
|
*
|
|
149
|
-
*
|
|
145
|
+
* The chat adapter wraps keys with "agent:{agentId}:" before sending
|
|
146
|
+
* to the daemon. The plugin may receive either format depending on
|
|
147
|
+
* which OpenClaw event fires.
|
|
150
148
|
*/
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
};
|
|
149
|
+
/**
|
|
150
|
+
* Check if a session key belongs to the Alfe chat channel.
|
|
151
|
+
* Handles both raw and prefixed formats.
|
|
152
|
+
*/
|
|
153
|
+
function isAlfeSessionKey(key) {
|
|
154
|
+
return key.includes("chat-") || key.includes("alfe:") || key.includes(":alfe:");
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Strip the "agent:{agentId}:" prefix and extract tenantId + agentId.
|
|
158
|
+
* Returns empty strings if the key doesn't match the expected format.
|
|
159
|
+
*/
|
|
160
|
+
function parseAlfeSessionKey(key) {
|
|
161
|
+
const rawKey = key.includes(":") ? key.slice(key.lastIndexOf(":") + 1) : key;
|
|
162
|
+
const match = /^chat-([^-]+)-([^-]+)/.exec(rawKey);
|
|
163
|
+
return {
|
|
164
|
+
tenantId: match?.[1] ?? "",
|
|
165
|
+
agentId: match?.[2] ?? ""
|
|
166
|
+
};
|
|
167
|
+
}
|
|
372
168
|
//#endregion
|
|
373
169
|
//#region src/session-store.ts
|
|
374
170
|
/**
|
|
@@ -379,14 +175,53 @@ var RuntimeRelay = class {
|
|
|
379
175
|
*
|
|
380
176
|
* Each session file contains metadata and the full message history.
|
|
381
177
|
* Sessions are written on every message to ensure durability.
|
|
178
|
+
* Old sessions are cleaned up automatically (30-day TTL, 1000 max).
|
|
382
179
|
*/
|
|
383
180
|
const SESSIONS_DIR = join(homedir(), ".alfe", "sessions", "chat");
|
|
181
|
+
const MAX_SESSIONS = 1e3;
|
|
182
|
+
const MAX_AGE_MS = 720 * 60 * 60 * 1e3;
|
|
183
|
+
const CLEANUP_INTERVAL_MS = 36e5;
|
|
184
|
+
let lastCleanupAt = 0;
|
|
384
185
|
async function ensureDir() {
|
|
385
186
|
if (!existsSync(SESSIONS_DIR)) await mkdir(SESSIONS_DIR, { recursive: true });
|
|
386
187
|
}
|
|
387
188
|
function sessionPath(sessionId) {
|
|
388
189
|
return join(SESSIONS_DIR, `${sessionId.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`);
|
|
389
190
|
}
|
|
191
|
+
async function cleanupOldSessions() {
|
|
192
|
+
if (Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return;
|
|
193
|
+
lastCleanupAt = Date.now();
|
|
194
|
+
try {
|
|
195
|
+
const jsonFiles = (await readdir(SESSIONS_DIR)).filter((f) => f.endsWith(".json"));
|
|
196
|
+
if (jsonFiles.length <= MAX_SESSIONS) {
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
for (const file of jsonFiles) try {
|
|
199
|
+
const filePath = join(SESSIONS_DIR, file);
|
|
200
|
+
if (now - (await stat(filePath)).mtimeMs > MAX_AGE_MS) await unlink(filePath);
|
|
201
|
+
} catch {}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const fileStats = [];
|
|
205
|
+
for (const file of jsonFiles) try {
|
|
206
|
+
const filePath = join(SESSIONS_DIR, file);
|
|
207
|
+
const fileStat = await stat(filePath);
|
|
208
|
+
fileStats.push({
|
|
209
|
+
path: filePath,
|
|
210
|
+
mtimeMs: fileStat.mtimeMs
|
|
211
|
+
});
|
|
212
|
+
} catch {}
|
|
213
|
+
fileStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
let remaining = fileStats.length;
|
|
216
|
+
for (const entry of fileStats) {
|
|
217
|
+
if (!(now - entry.mtimeMs > MAX_AGE_MS) && !(remaining > MAX_SESSIONS)) break;
|
|
218
|
+
try {
|
|
219
|
+
await unlink(entry.path);
|
|
220
|
+
remaining--;
|
|
221
|
+
} catch {}
|
|
222
|
+
}
|
|
223
|
+
} catch {}
|
|
224
|
+
}
|
|
390
225
|
async function getSession(sessionId) {
|
|
391
226
|
try {
|
|
392
227
|
const data = await readFile(sessionPath(sessionId), "utf-8");
|
|
@@ -413,6 +248,7 @@ async function createSession(sessionId, agentId, channel, tenantId, userId) {
|
|
|
413
248
|
messages: []
|
|
414
249
|
};
|
|
415
250
|
await saveSession(session);
|
|
251
|
+
cleanupOldSessions();
|
|
416
252
|
return session;
|
|
417
253
|
}
|
|
418
254
|
async function addMessage(sessionId, role, content) {
|
|
@@ -425,7 +261,7 @@ async function addMessage(sessionId, role, content) {
|
|
|
425
261
|
});
|
|
426
262
|
await saveSession(session);
|
|
427
263
|
}
|
|
428
|
-
async function listSessions(filters) {
|
|
264
|
+
async function listSessions(filters, limit = 50) {
|
|
429
265
|
await ensureDir();
|
|
430
266
|
let files;
|
|
431
267
|
try {
|
|
@@ -456,62 +292,65 @@ async function listSessions(filters) {
|
|
|
456
292
|
const aTime = a.lastMessageAt ?? a.createdAt;
|
|
457
293
|
return (b.lastMessageAt ?? b.createdAt).localeCompare(aTime);
|
|
458
294
|
});
|
|
459
|
-
return summaries;
|
|
295
|
+
return summaries.slice(0, limit);
|
|
460
296
|
}
|
|
461
297
|
//#endregion
|
|
462
298
|
//#region src/plugin.ts
|
|
463
299
|
/**
|
|
464
300
|
* @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
|
|
465
301
|
*
|
|
466
|
-
* Registers the 'alfe' channel with OpenClaw
|
|
467
|
-
*
|
|
468
|
-
*
|
|
302
|
+
* Registers the 'alfe' channel with OpenClaw. Messages are dispatched
|
|
303
|
+
* in-process via runtime.subagent.run() — no separate WebSocket to the
|
|
304
|
+
* runtime needed.
|
|
469
305
|
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
* - Registers gateway RPC methods for message delivery and session queries
|
|
474
|
-
* - Hooks into session lifecycle events
|
|
475
|
-
* - Persists chat sessions to ~/.alfe/sessions/chat/
|
|
476
|
-
* - Gracefully degrades if the daemon is unavailable
|
|
306
|
+
* Architecture:
|
|
307
|
+
* Chat Service (Fly.io) ←WS→ ChatServiceClient → runtime.subagent.run() → Agent (in-process)
|
|
308
|
+
* ← onAgentEvent streaming ←
|
|
477
309
|
*/
|
|
478
|
-
|
|
479
|
-
const CHAT_CAPABILITIES = [
|
|
480
|
-
"chat.web",
|
|
481
|
-
"chat.mobile",
|
|
482
|
-
"chat.sessions"
|
|
483
|
-
];
|
|
484
|
-
let daemonIpcClient = null;
|
|
310
|
+
let pluginRuntime = null;
|
|
485
311
|
let chatClient = null;
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
312
|
+
async function handleAgentRequest(request, log) {
|
|
313
|
+
const runtime = pluginRuntime;
|
|
314
|
+
if (!runtime) {
|
|
315
|
+
chatClient?.sendResponse(request.id, false, { message: "Plugin runtime not initialized" });
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const { message, sessionKey } = request.params;
|
|
319
|
+
if (!message || !sessionKey) {
|
|
320
|
+
chatClient?.sendResponse(request.id, false, { message: "Missing message or sessionKey" });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const unsubscribe = runtime.events.onAgentEvent((evt) => {
|
|
324
|
+
if (evt.sessionKey !== sessionKey) return;
|
|
325
|
+
if (evt.stream === "assistant") chatClient?.sendEvent("chat", {
|
|
326
|
+
runId: evt.runId,
|
|
327
|
+
sessionKey,
|
|
328
|
+
seq: evt.seq,
|
|
329
|
+
state: "delta",
|
|
330
|
+
message: evt.data
|
|
331
|
+
});
|
|
332
|
+
});
|
|
492
333
|
try {
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const response = await client.request("capability.register", {
|
|
498
|
-
plugin: "@alfe.ai/openclaw-chat",
|
|
499
|
-
capabilities: [...CHAT_CAPABILITIES]
|
|
500
|
-
});
|
|
501
|
-
if (response.ok) log.info("Chat capabilities registered with daemon");
|
|
502
|
-
else log.warn(`Failed to register chat capabilities: ${response.error?.message ?? "unknown"}`);
|
|
334
|
+
const { runId } = await runtime.subagent.run({
|
|
335
|
+
sessionKey,
|
|
336
|
+
message,
|
|
337
|
+
deliver: true
|
|
503
338
|
});
|
|
504
|
-
|
|
505
|
-
|
|
339
|
+
const result = await runtime.subagent.waitForRun({
|
|
340
|
+
runId,
|
|
341
|
+
timeoutMs: 12e4
|
|
506
342
|
});
|
|
507
|
-
|
|
508
|
-
|
|
343
|
+
if (result.status === "ok") chatClient?.sendResponse(request.id, true, {
|
|
344
|
+
text: "",
|
|
345
|
+
sessionKey
|
|
509
346
|
});
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
log.
|
|
514
|
-
|
|
347
|
+
else chatClient?.sendResponse(request.id, false, { message: result.error ?? `Agent run ${result.status}` });
|
|
348
|
+
} catch (err) {
|
|
349
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
350
|
+
log.error(`Agent dispatch failed: ${errMsg}`);
|
|
351
|
+
chatClient?.sendResponse(request.id, false, { message: errMsg });
|
|
352
|
+
} finally {
|
|
353
|
+
unsubscribe();
|
|
515
354
|
}
|
|
516
355
|
}
|
|
517
356
|
const plugin = {
|
|
@@ -519,80 +358,56 @@ const plugin = {
|
|
|
519
358
|
name: "Alfe Chat Plugin",
|
|
520
359
|
description: "Alfe conversation channel — web widget and mobile app share unified chat sessions",
|
|
521
360
|
version: "0.3.0",
|
|
522
|
-
|
|
361
|
+
activate(api) {
|
|
523
362
|
if (globalThis.__alfeChatPluginActivated) {
|
|
524
363
|
api.logger.debug("Alfe Chat plugin already activated, skipping re-init");
|
|
525
364
|
return;
|
|
526
365
|
}
|
|
527
366
|
globalThis.__alfeChatPluginActivated = true;
|
|
367
|
+
pluginRuntime = api.runtime ?? null;
|
|
528
368
|
const log = api.logger;
|
|
529
|
-
log.info("Alfe Chat plugin
|
|
530
|
-
const pluginConfig = (api.config ?? {}).plugins?.entries?.["@alfe.ai/openclaw-chat"]?.config ?? {};
|
|
369
|
+
log.info("Alfe Chat plugin registering...");
|
|
531
370
|
const alfeChannel = createAlfeChannelPlugin();
|
|
532
371
|
api.registerChannel(alfeChannel);
|
|
533
|
-
log.info(`Registered channel: ${alfeChannel.id}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
apiKey,
|
|
554
|
-
onRequest: (request) => {
|
|
555
|
-
if (!runtimeRelay?.isConnected) {
|
|
556
|
-
chatClient?.sendResponse(request.id, false, { message: "Runtime not connected" });
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
runtimeRelay.forwardRequest(request, (response) => {
|
|
560
|
-
chatClient?.send(response);
|
|
372
|
+
log.info(`Registered channel: ${alfeChannel.id}`);
|
|
373
|
+
const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
|
|
374
|
+
(async () => {
|
|
375
|
+
try {
|
|
376
|
+
const { apiKey, chatWsUrl } = await resolveAlfeChat({
|
|
377
|
+
apiKey: pluginConfig.apiKey,
|
|
378
|
+
chatWsUrl: pluginConfig.chatWsUrl
|
|
379
|
+
});
|
|
380
|
+
if (chatWsUrl && apiKey) {
|
|
381
|
+
log.info(`Connecting to chat service: ${chatWsUrl}`);
|
|
382
|
+
chatClient = new ChatServiceClient({
|
|
383
|
+
wsUrl: chatWsUrl,
|
|
384
|
+
apiKey,
|
|
385
|
+
onRequest: (request) => {
|
|
386
|
+
if (request.method === "agent") handleAgentRequest(request, log);
|
|
387
|
+
},
|
|
388
|
+
onConnectionChange: (connected) => {
|
|
389
|
+
log.info(`Chat service connection: ${connected ? "connected" : "disconnected"}`);
|
|
390
|
+
},
|
|
391
|
+
logger: log
|
|
561
392
|
});
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
chatClient.start();
|
|
570
|
-
log.info("Chat service relay started");
|
|
571
|
-
} else log.info("Chat service URL not configured — running without chat service relay");
|
|
393
|
+
chatClient.start();
|
|
394
|
+
log.info("Chat service relay started");
|
|
395
|
+
} else log.info("Chat service URL not configured — running without chat service relay");
|
|
396
|
+
} catch (err) {
|
|
397
|
+
log.error(`Failed to initialize chat service: ${err instanceof Error ? err.message : String(err)}`);
|
|
398
|
+
}
|
|
399
|
+
})();
|
|
572
400
|
if (typeof api.registerGatewayMethod === "function") {
|
|
573
|
-
api.registerGatewayMethod("chat.send", (...args) => {
|
|
574
|
-
const { sessionId, content, clientType } = args[0];
|
|
575
|
-
log.info(`chat.send RPC: session=${sessionId}, client=${clientType ?? "unknown"}, content=${content.slice(0, 50)}...`);
|
|
576
|
-
return Promise.resolve({
|
|
577
|
-
ok: true,
|
|
578
|
-
sessionId,
|
|
579
|
-
channel: "alfe"
|
|
580
|
-
});
|
|
581
|
-
});
|
|
582
|
-
log.info("Registered gateway RPC method: chat.send");
|
|
583
401
|
api.registerGatewayMethod("sessions.list", async (...args) => {
|
|
584
402
|
const params = args[0];
|
|
585
|
-
log.info(`sessions.list RPC: agentId=${params.agentId ?? "*"}, channel=${params.channel ?? "*"}`);
|
|
586
403
|
return { sessions: await listSessions({
|
|
587
404
|
agentId: params.agentId,
|
|
588
405
|
channel: params.channel,
|
|
589
406
|
tenantId: params.tenantId
|
|
590
407
|
}) };
|
|
591
408
|
});
|
|
592
|
-
log.info("Registered gateway RPC method: sessions.list");
|
|
593
409
|
api.registerGatewayMethod("sessions.get", async (...args) => {
|
|
594
410
|
const params = args[0];
|
|
595
|
-
log.info(`sessions.get RPC: sessionId=${params.sessionId}`);
|
|
596
411
|
const session = await getSession(params.sessionId);
|
|
597
412
|
if (!session) return {
|
|
598
413
|
ok: false,
|
|
@@ -611,30 +426,27 @@ const plugin = {
|
|
|
611
426
|
}))
|
|
612
427
|
};
|
|
613
428
|
});
|
|
614
|
-
log.info("Registered gateway RPC
|
|
429
|
+
log.info("Registered gateway RPC methods: sessions.list, sessions.get");
|
|
615
430
|
}
|
|
616
431
|
api.on("session_start", async (...eventArgs) => {
|
|
617
432
|
const key = eventArgs[0].sessionKey;
|
|
618
|
-
if (!key) return;
|
|
619
|
-
if (!(key.startsWith("chat-") || key.includes("alfe:") || key.includes(":alfe:"))) return;
|
|
433
|
+
if (!key || !isAlfeSessionKey(key)) return;
|
|
620
434
|
log.info(`Alfe chat session starting: ${key}`);
|
|
621
|
-
const
|
|
622
|
-
await createSession(key,
|
|
435
|
+
const { tenantId, agentId: parsedAgentId } = parseAlfeSessionKey(key);
|
|
436
|
+
await createSession(key, parsedAgentId, "alfe", tenantId);
|
|
623
437
|
}, { priority: 50 });
|
|
624
438
|
api.on("message", async (...eventArgs) => {
|
|
625
439
|
const event = eventArgs[0];
|
|
626
440
|
const key = event.sessionKey;
|
|
627
|
-
if (!key) return;
|
|
628
|
-
if (!(key.startsWith("chat-") || key.includes("alfe:") || key.includes(":alfe:"))) return;
|
|
441
|
+
if (!key || !isAlfeSessionKey(key)) return;
|
|
629
442
|
await addMessage(key, event.role, event.content);
|
|
630
443
|
});
|
|
631
444
|
api.on("session_end", (...eventArgs) => {
|
|
632
445
|
const key = eventArgs[0].sessionKey;
|
|
633
|
-
if (!key) return;
|
|
634
|
-
if (!(key.startsWith("chat-") || key.includes("alfe:") || key.includes(":alfe:"))) return;
|
|
446
|
+
if (!key || !isAlfeSessionKey(key)) return;
|
|
635
447
|
log.info(`Alfe chat session ending: ${key}`);
|
|
636
448
|
});
|
|
637
|
-
log.info("Alfe Chat plugin
|
|
449
|
+
log.info("Alfe Chat plugin registered");
|
|
638
450
|
},
|
|
639
451
|
deactivate(api) {
|
|
640
452
|
globalThis.__alfeChatPluginActivated = false;
|
|
@@ -645,20 +457,7 @@ const plugin = {
|
|
|
645
457
|
chatClient = null;
|
|
646
458
|
log.info("Chat service client stopped");
|
|
647
459
|
}
|
|
648
|
-
|
|
649
|
-
runtimeRelay.stop();
|
|
650
|
-
runtimeRelay = null;
|
|
651
|
-
log.info("Runtime relay stopped");
|
|
652
|
-
}
|
|
653
|
-
if (daemonIpcClient) {
|
|
654
|
-
try {
|
|
655
|
-
daemonIpcClient.stop();
|
|
656
|
-
log.info("Disconnected from Alfe daemon");
|
|
657
|
-
} catch (err) {
|
|
658
|
-
log.debug(`Error disconnecting from daemon: ${err.message}`);
|
|
659
|
-
}
|
|
660
|
-
daemonIpcClient = null;
|
|
661
|
-
}
|
|
460
|
+
pluginRuntime = null;
|
|
662
461
|
log.info("Alfe Chat plugin deactivated");
|
|
663
462
|
}
|
|
664
463
|
};
|
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.6",
|
|
4
4
|
"description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/plugin.js",
|
|
@@ -25,12 +25,8 @@
|
|
|
25
25
|
"openclaw.plugin.json"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"ws": "^8.18.0",
|
|
29
28
|
"@alfe.ai/chat": "^0.0.2"
|
|
30
29
|
},
|
|
31
|
-
"devDependencies": {
|
|
32
|
-
"@types/ws": "^8.5.13"
|
|
33
|
-
},
|
|
34
30
|
"license": "UNLICENSED",
|
|
35
31
|
"scripts": {
|
|
36
32
|
"build": "tsdown",
|