@alfe.ai/openclaw-chat 0.0.17 → 0.0.19
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/plugin2.cjs +64 -124
- package/dist/plugin2.js +64 -124
- package/openclaw.plugin.json +0 -1
- package/package.json +4 -2
package/dist/plugin2.cjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
let node_module = require("node:module");
|
|
2
|
-
let _alfe_ai_chat = require("@alfe.ai/chat");
|
|
3
2
|
let node_fs_promises = require("node:fs/promises");
|
|
4
3
|
let node_path = require("node:path");
|
|
5
4
|
let node_os = require("node:os");
|
|
5
|
+
let _alfe_ai_chat = require("@alfe.ai/chat");
|
|
6
|
+
let _alfe_ai_config = require("@alfe.ai/config");
|
|
7
|
+
let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
|
|
6
8
|
let node_fs = require("node:fs");
|
|
7
9
|
//#region src/alfe-channel.ts
|
|
8
10
|
const CHANNEL_ID = "alfe";
|
|
@@ -138,31 +140,6 @@ function createAlfeChannelPlugin() {
|
|
|
138
140
|
//#endregion
|
|
139
141
|
//#region src/session-keys.ts
|
|
140
142
|
/**
|
|
141
|
-
* Session key helpers — handles standardized, canonical, and legacy formats.
|
|
142
|
-
*
|
|
143
|
-
* Standardized format (Alfe-controlled):
|
|
144
|
-
* alfe:{mode}:{identity} — single-threaded (SMS, WhatsApp)
|
|
145
|
-
* alfe:{mode}:{identity}:{convId} — multi-threaded (web chat)
|
|
146
|
-
*
|
|
147
|
-
* Wrapped standardized format (from chat inject via callAgent):
|
|
148
|
-
* agent:{ocAgentId}:alfe:{mode}:{identity}
|
|
149
|
-
*
|
|
150
|
-
* OpenClaw canonical format (from resolveAgentRoute):
|
|
151
|
-
* agent:{ocAgentId}:alfe:[default:]direct:{senderId}[:thread:{conversationId}]
|
|
152
|
-
*
|
|
153
|
-
* Legacy formats (deprecated):
|
|
154
|
-
* sms-{agentId}-{phone}
|
|
155
|
-
* wa-{agentId}-{phone}
|
|
156
|
-
* chat-{tenantId}-{agentId}-{suffix}
|
|
157
|
-
* agent:{agentId}:chat-{tenantId}-{agentId}-{suffix}
|
|
158
|
-
*/
|
|
159
|
-
/** Single-threaded channel modes — identity IS the conversation. */
|
|
160
|
-
const SINGLE_THREADED_MODES = new Set([
|
|
161
|
-
"sms",
|
|
162
|
-
"whatsapp",
|
|
163
|
-
"mobile"
|
|
164
|
-
]);
|
|
165
|
-
/**
|
|
166
143
|
* Check if a session key belongs to the Alfe chat channel.
|
|
167
144
|
* Handles standardized, canonical, and legacy formats.
|
|
168
145
|
*/
|
|
@@ -173,87 +150,6 @@ function isAlfeSessionKey(key) {
|
|
|
173
150
|
return false;
|
|
174
151
|
}
|
|
175
152
|
/**
|
|
176
|
-
* Parse session key metadata. Returns available fields from any format.
|
|
177
|
-
*
|
|
178
|
-
* For the callback flow, `conversationId` is the critical field — it maps
|
|
179
|
-
* to the channel registry key used by getChannelCallback().
|
|
180
|
-
*/
|
|
181
|
-
function parseAlfeSessionKey(key) {
|
|
182
|
-
const standardMatch = /^alfe:(\w+):(.+)$/.exec(key);
|
|
183
|
-
if (standardMatch) {
|
|
184
|
-
const [, mode, rest] = standardMatch;
|
|
185
|
-
if (SINGLE_THREADED_MODES.has(mode)) return {
|
|
186
|
-
agentId: "",
|
|
187
|
-
userId: rest,
|
|
188
|
-
conversationId: key,
|
|
189
|
-
tenantId: "",
|
|
190
|
-
mode
|
|
191
|
-
};
|
|
192
|
-
const lastColon = rest.lastIndexOf(":");
|
|
193
|
-
if (lastColon > 0) return {
|
|
194
|
-
agentId: "",
|
|
195
|
-
userId: rest.slice(0, lastColon),
|
|
196
|
-
conversationId: key,
|
|
197
|
-
tenantId: "",
|
|
198
|
-
mode
|
|
199
|
-
};
|
|
200
|
-
return {
|
|
201
|
-
agentId: "",
|
|
202
|
-
userId: rest,
|
|
203
|
-
conversationId: key,
|
|
204
|
-
tenantId: "",
|
|
205
|
-
mode
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
const wrappedStandardMatch = /^agent:([^:]+):alfe:(?!default:|direct:)(\w+):(.+)$/.exec(key);
|
|
209
|
-
if (wrappedStandardMatch) {
|
|
210
|
-
const [, matchAgentId, wrappedMode, rest] = wrappedStandardMatch;
|
|
211
|
-
const standardKey = `alfe:${wrappedMode}:${rest}`;
|
|
212
|
-
if (SINGLE_THREADED_MODES.has(wrappedMode)) return {
|
|
213
|
-
agentId: matchAgentId,
|
|
214
|
-
userId: rest,
|
|
215
|
-
conversationId: standardKey,
|
|
216
|
-
tenantId: "",
|
|
217
|
-
mode: wrappedMode
|
|
218
|
-
};
|
|
219
|
-
const lastColon = rest.lastIndexOf(":");
|
|
220
|
-
if (lastColon > 0) return {
|
|
221
|
-
agentId: matchAgentId,
|
|
222
|
-
userId: rest.slice(0, lastColon),
|
|
223
|
-
conversationId: standardKey,
|
|
224
|
-
tenantId: "",
|
|
225
|
-
mode: wrappedMode
|
|
226
|
-
};
|
|
227
|
-
return {
|
|
228
|
-
agentId: matchAgentId,
|
|
229
|
-
userId: rest,
|
|
230
|
-
conversationId: standardKey,
|
|
231
|
-
tenantId: "",
|
|
232
|
-
mode: wrappedMode
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
const canonicalMatch = /^agent:([^:]+):alfe:(?:default:)?direct:(.+?)(?::thread:(.+))?$/.exec(key);
|
|
236
|
-
if (canonicalMatch) {
|
|
237
|
-
const [, matchAgentId, matchUserId, matchConvId] = canonicalMatch;
|
|
238
|
-
return {
|
|
239
|
-
agentId: matchAgentId,
|
|
240
|
-
userId: matchUserId,
|
|
241
|
-
conversationId: matchConvId || "",
|
|
242
|
-
tenantId: "",
|
|
243
|
-
mode: ""
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
const rawKey = key.includes(":") ? key.slice(key.lastIndexOf(":") + 1) : key;
|
|
247
|
-
const legacyMatch = /^chat-([^-]+)-([^-]+)/.exec(rawKey);
|
|
248
|
-
return {
|
|
249
|
-
agentId: legacyMatch?.[2] ?? "",
|
|
250
|
-
userId: "",
|
|
251
|
-
conversationId: "",
|
|
252
|
-
tenantId: legacyMatch?.[1] ?? "",
|
|
253
|
-
mode: "chat"
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
153
|
* Extract the channel mode from a standardized session key or conversationId.
|
|
258
154
|
* Returns the mode segment (e.g. 'sms', 'whatsapp', 'chat') or fallback.
|
|
259
155
|
*/
|
|
@@ -428,6 +324,34 @@ function resolveOpenClawSdk(log) {
|
|
|
428
324
|
let pluginRuntime = null;
|
|
429
325
|
let chatClient = null;
|
|
430
326
|
let connectingPromise = null;
|
|
327
|
+
let metricsClient = null;
|
|
328
|
+
async function downloadAttachments(attachments, log) {
|
|
329
|
+
const attachDir = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "attachments");
|
|
330
|
+
await (0, node_fs_promises.mkdir)(attachDir, { recursive: true });
|
|
331
|
+
const results = [];
|
|
332
|
+
for (const att of attachments) {
|
|
333
|
+
const filename = att.filename ?? att.id;
|
|
334
|
+
const localPath = (0, node_path.join)(attachDir, `${att.id}_${filename}`);
|
|
335
|
+
try {
|
|
336
|
+
const res = await fetch(att.url);
|
|
337
|
+
if (!res.ok) {
|
|
338
|
+
log.warn(`Failed to download attachment ${att.id}: ${String(res.status)}`);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
342
|
+
await (0, node_fs_promises.writeFile)(localPath, buffer);
|
|
343
|
+
results.push({
|
|
344
|
+
localPath,
|
|
345
|
+
filename,
|
|
346
|
+
mimeType: att.mimeType
|
|
347
|
+
});
|
|
348
|
+
log.info(`Downloaded attachment: ${localPath} (${String(buffer.length)} bytes)`);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
log.error(`Failed to download attachment ${att.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return results;
|
|
354
|
+
}
|
|
431
355
|
async function handleAgentRequest(request, log) {
|
|
432
356
|
const runtime = pluginRuntime;
|
|
433
357
|
if (!runtime) {
|
|
@@ -438,8 +362,8 @@ async function handleAgentRequest(request, log) {
|
|
|
438
362
|
chatClient?.sendResponse(request.id, false, { message: "OpenClaw SDK not available — cannot dispatch" });
|
|
439
363
|
return;
|
|
440
364
|
}
|
|
441
|
-
const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, displayName } = request.params;
|
|
442
|
-
if (!message) {
|
|
365
|
+
const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, displayName, attachments: rawAttachments } = request.params;
|
|
366
|
+
if (!message && !rawAttachments?.length) {
|
|
443
367
|
chatClient?.sendResponse(request.id, false, { message: "Missing message" });
|
|
444
368
|
return;
|
|
445
369
|
}
|
|
@@ -463,6 +387,8 @@ async function handleAgentRequest(request, log) {
|
|
|
463
387
|
});
|
|
464
388
|
});
|
|
465
389
|
try {
|
|
390
|
+
const downloadedFiles = rawAttachments?.length ? await downloadAttachments(rawAttachments, log) : [];
|
|
391
|
+
const bodyForAgent = downloadedFiles.length ? `${message || ""}\n\n[Attached files:\n${downloadedFiles.map((f) => `- ${f.filename}: ${f.localPath}`).join("\n")}]` : void 0;
|
|
466
392
|
const channelMode = extractChannelMode(conversationId ?? "", clientType ?? "chat");
|
|
467
393
|
const channelLabel = channelMode === "sms" ? "SMS" : channelMode === "whatsapp" ? "WhatsApp" : "Alfe";
|
|
468
394
|
const shortConvId = conversationId?.slice(-8) ?? "";
|
|
@@ -485,7 +411,8 @@ async function handleAgentRequest(request, log) {
|
|
|
485
411
|
senderAddress: `user:${senderId}`,
|
|
486
412
|
recipientAddress: "agent",
|
|
487
413
|
conversationLabel,
|
|
488
|
-
rawBody: message,
|
|
414
|
+
rawBody: message || "",
|
|
415
|
+
bodyForAgent,
|
|
489
416
|
messageId: request.id,
|
|
490
417
|
timestamp: Date.now(),
|
|
491
418
|
extraContext: {
|
|
@@ -497,10 +424,20 @@ async function handleAgentRequest(request, log) {
|
|
|
497
424
|
},
|
|
498
425
|
deliver: async (payload) => {
|
|
499
426
|
const responseText = payload.text ?? "";
|
|
427
|
+
const mediaUrls = [...payload.mediaUrl ? [payload.mediaUrl] : [], ...payload.mediaUrls ?? []];
|
|
500
428
|
await addMessage(sessionId, "assistant", responseText);
|
|
501
|
-
chatClient?.
|
|
429
|
+
chatClient?.notify("agent-message", {
|
|
430
|
+
conversationId: conversationId ?? legacySessionKey,
|
|
502
431
|
text: responseText,
|
|
503
|
-
sessionKey: resolvedOpenClawKey ?? legacySessionKey
|
|
432
|
+
sessionKey: resolvedOpenClawKey ?? legacySessionKey,
|
|
433
|
+
...mediaUrls.length ? { mediaUrls } : {}
|
|
434
|
+
});
|
|
435
|
+
if (metricsClient && userId) metricsClient.recordActivity({
|
|
436
|
+
userId,
|
|
437
|
+
channel: "alfe",
|
|
438
|
+
role: "assistant"
|
|
439
|
+
}).catch((err) => {
|
|
440
|
+
log.debug(`Metrics report failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
504
441
|
});
|
|
505
442
|
},
|
|
506
443
|
onRecordError: (err) => {
|
|
@@ -510,6 +447,7 @@ async function handleAgentRequest(request, log) {
|
|
|
510
447
|
log.error(`Dispatch error (${info.kind}): ${err instanceof Error ? err.message : String(err)}`);
|
|
511
448
|
}
|
|
512
449
|
})).route.sessionKey;
|
|
450
|
+
chatClient?.sendResponse(request.id, true, { sessionKey: resolvedOpenClawKey });
|
|
513
451
|
log.info(`Agent dispatch complete: sessionKey=${resolvedOpenClawKey}`);
|
|
514
452
|
} catch (err) {
|
|
515
453
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -586,6 +524,15 @@ const plugin = {
|
|
|
586
524
|
log.info("Chat plugin registering...");
|
|
587
525
|
resolveOpenClawSdk(log);
|
|
588
526
|
pluginRuntime = api.runtime ?? null;
|
|
527
|
+
try {
|
|
528
|
+
const cfg = (0, _alfe_ai_config.resolveConfig)();
|
|
529
|
+
metricsClient = new _alfe_ai_agent_api_client.AgentApiClient({
|
|
530
|
+
apiKey: cfg.apiKey,
|
|
531
|
+
apiUrl: cfg.apiUrl
|
|
532
|
+
});
|
|
533
|
+
} catch {
|
|
534
|
+
log.debug("Metrics client not initialized — activity tracking disabled");
|
|
535
|
+
}
|
|
589
536
|
}
|
|
590
537
|
const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
|
|
591
538
|
if (!alreadyActivated) connectingPromise = Promise.resolve().then(() => {
|
|
@@ -659,19 +606,11 @@ const plugin = {
|
|
|
659
606
|
log.info(`Chat session starting: ${key}`);
|
|
660
607
|
await createSession(key, "", "alfe");
|
|
661
608
|
}, { priority: 50 });
|
|
662
|
-
api.on("
|
|
609
|
+
api.on("message_received", async (...eventArgs) => {
|
|
663
610
|
const event = eventArgs[0];
|
|
664
|
-
const
|
|
665
|
-
if (!
|
|
666
|
-
|
|
667
|
-
if (event.role === "assistant" && chatClient) {
|
|
668
|
-
const parsed = parseAlfeSessionKey(key);
|
|
669
|
-
if (parsed.conversationId) chatClient.notify("agent-message", {
|
|
670
|
-
conversationId: parsed.conversationId,
|
|
671
|
-
text: event.content
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
} else await addMessage(key, event.role, event.content);
|
|
611
|
+
const ctx = eventArgs[1];
|
|
612
|
+
if (!ctx.conversationId || ctx.channelId === "alfe") return;
|
|
613
|
+
await addMessage(ctx.conversationId, "user", event.content, event.from);
|
|
675
614
|
});
|
|
676
615
|
api.on("session_end", (...eventArgs) => {
|
|
677
616
|
const key = eventArgs[0].sessionKey;
|
|
@@ -698,6 +637,7 @@ const plugin = {
|
|
|
698
637
|
}
|
|
699
638
|
pluginRuntime = null;
|
|
700
639
|
dispatchInbound = null;
|
|
640
|
+
metricsClient = null;
|
|
701
641
|
log.info("Chat plugin deactivated");
|
|
702
642
|
}
|
|
703
643
|
};
|
package/dist/plugin2.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { ChatServiceClient, resolveAlfeChat } from "@alfe.ai/chat";
|
|
3
2
|
import { mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
3
|
import { join } from "node:path";
|
|
5
4
|
import { homedir } from "node:os";
|
|
5
|
+
import { ChatServiceClient, resolveAlfeChat } from "@alfe.ai/chat";
|
|
6
|
+
import { resolveConfig } from "@alfe.ai/config";
|
|
7
|
+
import { AgentApiClient } from "@alfe.ai/agent-api-client";
|
|
6
8
|
import { existsSync } from "node:fs";
|
|
7
9
|
//#region src/alfe-channel.ts
|
|
8
10
|
const CHANNEL_ID = "alfe";
|
|
@@ -138,31 +140,6 @@ function createAlfeChannelPlugin() {
|
|
|
138
140
|
//#endregion
|
|
139
141
|
//#region src/session-keys.ts
|
|
140
142
|
/**
|
|
141
|
-
* Session key helpers — handles standardized, canonical, and legacy formats.
|
|
142
|
-
*
|
|
143
|
-
* Standardized format (Alfe-controlled):
|
|
144
|
-
* alfe:{mode}:{identity} — single-threaded (SMS, WhatsApp)
|
|
145
|
-
* alfe:{mode}:{identity}:{convId} — multi-threaded (web chat)
|
|
146
|
-
*
|
|
147
|
-
* Wrapped standardized format (from chat inject via callAgent):
|
|
148
|
-
* agent:{ocAgentId}:alfe:{mode}:{identity}
|
|
149
|
-
*
|
|
150
|
-
* OpenClaw canonical format (from resolveAgentRoute):
|
|
151
|
-
* agent:{ocAgentId}:alfe:[default:]direct:{senderId}[:thread:{conversationId}]
|
|
152
|
-
*
|
|
153
|
-
* Legacy formats (deprecated):
|
|
154
|
-
* sms-{agentId}-{phone}
|
|
155
|
-
* wa-{agentId}-{phone}
|
|
156
|
-
* chat-{tenantId}-{agentId}-{suffix}
|
|
157
|
-
* agent:{agentId}:chat-{tenantId}-{agentId}-{suffix}
|
|
158
|
-
*/
|
|
159
|
-
/** Single-threaded channel modes — identity IS the conversation. */
|
|
160
|
-
const SINGLE_THREADED_MODES = new Set([
|
|
161
|
-
"sms",
|
|
162
|
-
"whatsapp",
|
|
163
|
-
"mobile"
|
|
164
|
-
]);
|
|
165
|
-
/**
|
|
166
143
|
* Check if a session key belongs to the Alfe chat channel.
|
|
167
144
|
* Handles standardized, canonical, and legacy formats.
|
|
168
145
|
*/
|
|
@@ -173,87 +150,6 @@ function isAlfeSessionKey(key) {
|
|
|
173
150
|
return false;
|
|
174
151
|
}
|
|
175
152
|
/**
|
|
176
|
-
* Parse session key metadata. Returns available fields from any format.
|
|
177
|
-
*
|
|
178
|
-
* For the callback flow, `conversationId` is the critical field — it maps
|
|
179
|
-
* to the channel registry key used by getChannelCallback().
|
|
180
|
-
*/
|
|
181
|
-
function parseAlfeSessionKey(key) {
|
|
182
|
-
const standardMatch = /^alfe:(\w+):(.+)$/.exec(key);
|
|
183
|
-
if (standardMatch) {
|
|
184
|
-
const [, mode, rest] = standardMatch;
|
|
185
|
-
if (SINGLE_THREADED_MODES.has(mode)) return {
|
|
186
|
-
agentId: "",
|
|
187
|
-
userId: rest,
|
|
188
|
-
conversationId: key,
|
|
189
|
-
tenantId: "",
|
|
190
|
-
mode
|
|
191
|
-
};
|
|
192
|
-
const lastColon = rest.lastIndexOf(":");
|
|
193
|
-
if (lastColon > 0) return {
|
|
194
|
-
agentId: "",
|
|
195
|
-
userId: rest.slice(0, lastColon),
|
|
196
|
-
conversationId: key,
|
|
197
|
-
tenantId: "",
|
|
198
|
-
mode
|
|
199
|
-
};
|
|
200
|
-
return {
|
|
201
|
-
agentId: "",
|
|
202
|
-
userId: rest,
|
|
203
|
-
conversationId: key,
|
|
204
|
-
tenantId: "",
|
|
205
|
-
mode
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
const wrappedStandardMatch = /^agent:([^:]+):alfe:(?!default:|direct:)(\w+):(.+)$/.exec(key);
|
|
209
|
-
if (wrappedStandardMatch) {
|
|
210
|
-
const [, matchAgentId, wrappedMode, rest] = wrappedStandardMatch;
|
|
211
|
-
const standardKey = `alfe:${wrappedMode}:${rest}`;
|
|
212
|
-
if (SINGLE_THREADED_MODES.has(wrappedMode)) return {
|
|
213
|
-
agentId: matchAgentId,
|
|
214
|
-
userId: rest,
|
|
215
|
-
conversationId: standardKey,
|
|
216
|
-
tenantId: "",
|
|
217
|
-
mode: wrappedMode
|
|
218
|
-
};
|
|
219
|
-
const lastColon = rest.lastIndexOf(":");
|
|
220
|
-
if (lastColon > 0) return {
|
|
221
|
-
agentId: matchAgentId,
|
|
222
|
-
userId: rest.slice(0, lastColon),
|
|
223
|
-
conversationId: standardKey,
|
|
224
|
-
tenantId: "",
|
|
225
|
-
mode: wrappedMode
|
|
226
|
-
};
|
|
227
|
-
return {
|
|
228
|
-
agentId: matchAgentId,
|
|
229
|
-
userId: rest,
|
|
230
|
-
conversationId: standardKey,
|
|
231
|
-
tenantId: "",
|
|
232
|
-
mode: wrappedMode
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
const canonicalMatch = /^agent:([^:]+):alfe:(?:default:)?direct:(.+?)(?::thread:(.+))?$/.exec(key);
|
|
236
|
-
if (canonicalMatch) {
|
|
237
|
-
const [, matchAgentId, matchUserId, matchConvId] = canonicalMatch;
|
|
238
|
-
return {
|
|
239
|
-
agentId: matchAgentId,
|
|
240
|
-
userId: matchUserId,
|
|
241
|
-
conversationId: matchConvId || "",
|
|
242
|
-
tenantId: "",
|
|
243
|
-
mode: ""
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
const rawKey = key.includes(":") ? key.slice(key.lastIndexOf(":") + 1) : key;
|
|
247
|
-
const legacyMatch = /^chat-([^-]+)-([^-]+)/.exec(rawKey);
|
|
248
|
-
return {
|
|
249
|
-
agentId: legacyMatch?.[2] ?? "",
|
|
250
|
-
userId: "",
|
|
251
|
-
conversationId: "",
|
|
252
|
-
tenantId: legacyMatch?.[1] ?? "",
|
|
253
|
-
mode: "chat"
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
153
|
* Extract the channel mode from a standardized session key or conversationId.
|
|
258
154
|
* Returns the mode segment (e.g. 'sms', 'whatsapp', 'chat') or fallback.
|
|
259
155
|
*/
|
|
@@ -428,6 +324,34 @@ function resolveOpenClawSdk(log) {
|
|
|
428
324
|
let pluginRuntime = null;
|
|
429
325
|
let chatClient = null;
|
|
430
326
|
let connectingPromise = null;
|
|
327
|
+
let metricsClient = null;
|
|
328
|
+
async function downloadAttachments(attachments, log) {
|
|
329
|
+
const attachDir = join(homedir(), ".alfe", "attachments");
|
|
330
|
+
await mkdir(attachDir, { recursive: true });
|
|
331
|
+
const results = [];
|
|
332
|
+
for (const att of attachments) {
|
|
333
|
+
const filename = att.filename ?? att.id;
|
|
334
|
+
const localPath = join(attachDir, `${att.id}_${filename}`);
|
|
335
|
+
try {
|
|
336
|
+
const res = await fetch(att.url);
|
|
337
|
+
if (!res.ok) {
|
|
338
|
+
log.warn(`Failed to download attachment ${att.id}: ${String(res.status)}`);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
342
|
+
await writeFile(localPath, buffer);
|
|
343
|
+
results.push({
|
|
344
|
+
localPath,
|
|
345
|
+
filename,
|
|
346
|
+
mimeType: att.mimeType
|
|
347
|
+
});
|
|
348
|
+
log.info(`Downloaded attachment: ${localPath} (${String(buffer.length)} bytes)`);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
log.error(`Failed to download attachment ${att.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return results;
|
|
354
|
+
}
|
|
431
355
|
async function handleAgentRequest(request, log) {
|
|
432
356
|
const runtime = pluginRuntime;
|
|
433
357
|
if (!runtime) {
|
|
@@ -438,8 +362,8 @@ async function handleAgentRequest(request, log) {
|
|
|
438
362
|
chatClient?.sendResponse(request.id, false, { message: "OpenClaw SDK not available — cannot dispatch" });
|
|
439
363
|
return;
|
|
440
364
|
}
|
|
441
|
-
const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, displayName } = request.params;
|
|
442
|
-
if (!message) {
|
|
365
|
+
const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, displayName, attachments: rawAttachments } = request.params;
|
|
366
|
+
if (!message && !rawAttachments?.length) {
|
|
443
367
|
chatClient?.sendResponse(request.id, false, { message: "Missing message" });
|
|
444
368
|
return;
|
|
445
369
|
}
|
|
@@ -463,6 +387,8 @@ async function handleAgentRequest(request, log) {
|
|
|
463
387
|
});
|
|
464
388
|
});
|
|
465
389
|
try {
|
|
390
|
+
const downloadedFiles = rawAttachments?.length ? await downloadAttachments(rawAttachments, log) : [];
|
|
391
|
+
const bodyForAgent = downloadedFiles.length ? `${message || ""}\n\n[Attached files:\n${downloadedFiles.map((f) => `- ${f.filename}: ${f.localPath}`).join("\n")}]` : void 0;
|
|
466
392
|
const channelMode = extractChannelMode(conversationId ?? "", clientType ?? "chat");
|
|
467
393
|
const channelLabel = channelMode === "sms" ? "SMS" : channelMode === "whatsapp" ? "WhatsApp" : "Alfe";
|
|
468
394
|
const shortConvId = conversationId?.slice(-8) ?? "";
|
|
@@ -485,7 +411,8 @@ async function handleAgentRequest(request, log) {
|
|
|
485
411
|
senderAddress: `user:${senderId}`,
|
|
486
412
|
recipientAddress: "agent",
|
|
487
413
|
conversationLabel,
|
|
488
|
-
rawBody: message,
|
|
414
|
+
rawBody: message || "",
|
|
415
|
+
bodyForAgent,
|
|
489
416
|
messageId: request.id,
|
|
490
417
|
timestamp: Date.now(),
|
|
491
418
|
extraContext: {
|
|
@@ -497,10 +424,20 @@ async function handleAgentRequest(request, log) {
|
|
|
497
424
|
},
|
|
498
425
|
deliver: async (payload) => {
|
|
499
426
|
const responseText = payload.text ?? "";
|
|
427
|
+
const mediaUrls = [...payload.mediaUrl ? [payload.mediaUrl] : [], ...payload.mediaUrls ?? []];
|
|
500
428
|
await addMessage(sessionId, "assistant", responseText);
|
|
501
|
-
chatClient?.
|
|
429
|
+
chatClient?.notify("agent-message", {
|
|
430
|
+
conversationId: conversationId ?? legacySessionKey,
|
|
502
431
|
text: responseText,
|
|
503
|
-
sessionKey: resolvedOpenClawKey ?? legacySessionKey
|
|
432
|
+
sessionKey: resolvedOpenClawKey ?? legacySessionKey,
|
|
433
|
+
...mediaUrls.length ? { mediaUrls } : {}
|
|
434
|
+
});
|
|
435
|
+
if (metricsClient && userId) metricsClient.recordActivity({
|
|
436
|
+
userId,
|
|
437
|
+
channel: "alfe",
|
|
438
|
+
role: "assistant"
|
|
439
|
+
}).catch((err) => {
|
|
440
|
+
log.debug(`Metrics report failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
504
441
|
});
|
|
505
442
|
},
|
|
506
443
|
onRecordError: (err) => {
|
|
@@ -510,6 +447,7 @@ async function handleAgentRequest(request, log) {
|
|
|
510
447
|
log.error(`Dispatch error (${info.kind}): ${err instanceof Error ? err.message : String(err)}`);
|
|
511
448
|
}
|
|
512
449
|
})).route.sessionKey;
|
|
450
|
+
chatClient?.sendResponse(request.id, true, { sessionKey: resolvedOpenClawKey });
|
|
513
451
|
log.info(`Agent dispatch complete: sessionKey=${resolvedOpenClawKey}`);
|
|
514
452
|
} catch (err) {
|
|
515
453
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -586,6 +524,15 @@ const plugin = {
|
|
|
586
524
|
log.info("Chat plugin registering...");
|
|
587
525
|
resolveOpenClawSdk(log);
|
|
588
526
|
pluginRuntime = api.runtime ?? null;
|
|
527
|
+
try {
|
|
528
|
+
const cfg = resolveConfig();
|
|
529
|
+
metricsClient = new AgentApiClient({
|
|
530
|
+
apiKey: cfg.apiKey,
|
|
531
|
+
apiUrl: cfg.apiUrl
|
|
532
|
+
});
|
|
533
|
+
} catch {
|
|
534
|
+
log.debug("Metrics client not initialized — activity tracking disabled");
|
|
535
|
+
}
|
|
589
536
|
}
|
|
590
537
|
const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
|
|
591
538
|
if (!alreadyActivated) connectingPromise = Promise.resolve().then(() => {
|
|
@@ -659,19 +606,11 @@ const plugin = {
|
|
|
659
606
|
log.info(`Chat session starting: ${key}`);
|
|
660
607
|
await createSession(key, "", "alfe");
|
|
661
608
|
}, { priority: 50 });
|
|
662
|
-
api.on("
|
|
609
|
+
api.on("message_received", async (...eventArgs) => {
|
|
663
610
|
const event = eventArgs[0];
|
|
664
|
-
const
|
|
665
|
-
if (!
|
|
666
|
-
|
|
667
|
-
if (event.role === "assistant" && chatClient) {
|
|
668
|
-
const parsed = parseAlfeSessionKey(key);
|
|
669
|
-
if (parsed.conversationId) chatClient.notify("agent-message", {
|
|
670
|
-
conversationId: parsed.conversationId,
|
|
671
|
-
text: event.content
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
} else await addMessage(key, event.role, event.content);
|
|
611
|
+
const ctx = eventArgs[1];
|
|
612
|
+
if (!ctx.conversationId || ctx.channelId === "alfe") return;
|
|
613
|
+
await addMessage(ctx.conversationId, "user", event.content, event.from);
|
|
675
614
|
});
|
|
676
615
|
api.on("session_end", (...eventArgs) => {
|
|
677
616
|
const key = eventArgs[0].sessionKey;
|
|
@@ -698,6 +637,7 @@ const plugin = {
|
|
|
698
637
|
}
|
|
699
638
|
pluginRuntime = null;
|
|
700
639
|
dispatchInbound = null;
|
|
640
|
+
metricsClient = null;
|
|
701
641
|
log.info("Chat plugin deactivated");
|
|
702
642
|
}
|
|
703
643
|
};
|
package/openclaw.plugin.json
CHANGED
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.19",
|
|
4
4
|
"description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/plugin.js",
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
"openclaw.plugin.json"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@alfe.ai/
|
|
30
|
+
"@alfe.ai/agent-api-client": "^0.0.7",
|
|
31
|
+
"@alfe.ai/chat": "^0.0.6",
|
|
32
|
+
"@alfe.ai/config": "^0.0.7"
|
|
31
33
|
},
|
|
32
34
|
"peerDependencies": {
|
|
33
35
|
"openclaw": ">=2026.3.0"
|