@alfe.ai/openclaw-chat 0.0.19 → 0.0.21
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.cts +7 -0
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin2.cjs +215 -30
- package/dist/plugin2.js +219 -31
- package/package.json +2 -2
package/dist/plugin.d.cts
CHANGED
|
@@ -225,6 +225,13 @@ interface PluginApi {
|
|
|
225
225
|
config?: Record<string, unknown>;
|
|
226
226
|
runtime?: PluginRuntime;
|
|
227
227
|
registerChannel(channel: ReturnType<typeof createAlfeChannelPlugin>): void;
|
|
228
|
+
registerTool?(tool: {
|
|
229
|
+
name: string;
|
|
230
|
+
description: string;
|
|
231
|
+
label: string;
|
|
232
|
+
parameters: Record<string, unknown>;
|
|
233
|
+
execute: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
234
|
+
}): void;
|
|
228
235
|
registerGatewayMethod?(name: string, handler: (...args: unknown[]) => Promise<unknown>): void;
|
|
229
236
|
on(event: string, handler: (...args: unknown[]) => void | Promise<void>, options?: {
|
|
230
237
|
priority?: number;
|
package/dist/plugin.d.ts
CHANGED
|
@@ -225,6 +225,13 @@ interface PluginApi {
|
|
|
225
225
|
config?: Record<string, unknown>;
|
|
226
226
|
runtime?: PluginRuntime;
|
|
227
227
|
registerChannel(channel: ReturnType<typeof createAlfeChannelPlugin>): void;
|
|
228
|
+
registerTool?(tool: {
|
|
229
|
+
name: string;
|
|
230
|
+
description: string;
|
|
231
|
+
label: string;
|
|
232
|
+
parameters: Record<string, unknown>;
|
|
233
|
+
execute: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
234
|
+
}): void;
|
|
228
235
|
registerGatewayMethod?(name: string, handler: (...args: unknown[]) => Promise<unknown>): void;
|
|
229
236
|
on(event: string, handler: (...args: unknown[]) => void | Promise<void>, options?: {
|
|
230
237
|
priority?: number;
|
package/dist/plugin2.cjs
CHANGED
|
@@ -151,7 +151,7 @@ function isAlfeSessionKey(key) {
|
|
|
151
151
|
}
|
|
152
152
|
/**
|
|
153
153
|
* Extract the channel mode from a standardized session key or conversationId.
|
|
154
|
-
* Returns the mode segment (e.g. 'sms', 'whatsapp', 'chat') or fallback.
|
|
154
|
+
* Returns the mode segment (e.g. 'sms', 'whatsapp', 'chat', 'a2a') or fallback.
|
|
155
155
|
*/
|
|
156
156
|
function extractChannelMode(conversationId, fallback = "chat") {
|
|
157
157
|
return /^alfe:(\w+):/.exec(conversationId)?.[1] ?? fallback;
|
|
@@ -181,7 +181,6 @@ function sessionPath(sessionId) {
|
|
|
181
181
|
}
|
|
182
182
|
async function cleanupOldSessions() {
|
|
183
183
|
if (Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return;
|
|
184
|
-
lastCleanupAt = Date.now();
|
|
185
184
|
try {
|
|
186
185
|
const jsonFiles = (await (0, node_fs_promises.readdir)(SESSIONS_DIR)).filter((f) => f.endsWith(".json"));
|
|
187
186
|
if (jsonFiles.length <= MAX_SESSIONS) {
|
|
@@ -211,6 +210,7 @@ async function cleanupOldSessions() {
|
|
|
211
210
|
remaining--;
|
|
212
211
|
} catch {}
|
|
213
212
|
}
|
|
213
|
+
lastCleanupAt = Date.now();
|
|
214
214
|
} catch {}
|
|
215
215
|
}
|
|
216
216
|
async function getSession(sessionId) {
|
|
@@ -242,17 +242,24 @@ async function createSession(sessionId, agentId, channel, tenantId, userId) {
|
|
|
242
242
|
cleanupOldSessions();
|
|
243
243
|
return session;
|
|
244
244
|
}
|
|
245
|
+
const writeLocks = /* @__PURE__ */ new Map();
|
|
245
246
|
async function addMessage(sessionId, role, content, senderId, senderName) {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
247
|
+
const next = (writeLocks.get(sessionId) ?? Promise.resolve()).then(async () => {
|
|
248
|
+
const session = await getSession(sessionId);
|
|
249
|
+
if (!session) return;
|
|
250
|
+
session.messages.push({
|
|
251
|
+
role,
|
|
252
|
+
content,
|
|
253
|
+
timestamp: Date.now(),
|
|
254
|
+
...senderId ? { senderId } : {},
|
|
255
|
+
...senderName ? { senderName } : {}
|
|
256
|
+
});
|
|
257
|
+
await saveSession(session);
|
|
258
|
+
}).finally(() => {
|
|
259
|
+
if (writeLocks.get(sessionId) === next) writeLocks.delete(sessionId);
|
|
254
260
|
});
|
|
255
|
-
|
|
261
|
+
writeLocks.set(sessionId, next);
|
|
262
|
+
await next;
|
|
256
263
|
}
|
|
257
264
|
async function listSessions(filters, limit = 50) {
|
|
258
265
|
await ensureDir();
|
|
@@ -289,6 +296,112 @@ async function listSessions(filters, limit = 50) {
|
|
|
289
296
|
return summaries.slice(0, limit);
|
|
290
297
|
}
|
|
291
298
|
//#endregion
|
|
299
|
+
//#region src/a2a-tools.ts
|
|
300
|
+
function ok(result) {
|
|
301
|
+
return { content: [{
|
|
302
|
+
type: "text",
|
|
303
|
+
text: JSON.stringify(result)
|
|
304
|
+
}] };
|
|
305
|
+
}
|
|
306
|
+
function errResult(message) {
|
|
307
|
+
return { content: [{
|
|
308
|
+
type: "text",
|
|
309
|
+
text: JSON.stringify({ error: message })
|
|
310
|
+
}] };
|
|
311
|
+
}
|
|
312
|
+
function defineTool(def) {
|
|
313
|
+
return {
|
|
314
|
+
name: def.name,
|
|
315
|
+
description: def.description,
|
|
316
|
+
label: def.name,
|
|
317
|
+
parameters: def.parameters,
|
|
318
|
+
execute: async (_toolCallId, params) => {
|
|
319
|
+
try {
|
|
320
|
+
return ok(await def.handler(params));
|
|
321
|
+
} catch (e) {
|
|
322
|
+
return errResult(e.message);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function buildA2ATools(chatClient, log) {
|
|
328
|
+
return [
|
|
329
|
+
defineTool({
|
|
330
|
+
name: "list_agents",
|
|
331
|
+
description: "List other agents in your organization that you can communicate with.",
|
|
332
|
+
parameters: {
|
|
333
|
+
type: "object",
|
|
334
|
+
properties: {},
|
|
335
|
+
required: []
|
|
336
|
+
},
|
|
337
|
+
handler: async () => {
|
|
338
|
+
log.info("list_agents tool called");
|
|
339
|
+
return await chatClient.sendRequest("a2a.list-agents", {});
|
|
340
|
+
}
|
|
341
|
+
}),
|
|
342
|
+
defineTool({
|
|
343
|
+
name: "message_agent",
|
|
344
|
+
description: "Send a message to another agent. Starts a new conversation thread, or continues an existing one. The conversation will bounce back and forth automatically until one agent calls end_conversation() or says [RESOLVED].",
|
|
345
|
+
parameters: {
|
|
346
|
+
type: "object",
|
|
347
|
+
properties: {
|
|
348
|
+
agent_id: {
|
|
349
|
+
type: "string",
|
|
350
|
+
description: "Target agent ID (use list_agents to find available agents)"
|
|
351
|
+
},
|
|
352
|
+
message: {
|
|
353
|
+
type: "string",
|
|
354
|
+
description: "Message to send to the agent"
|
|
355
|
+
},
|
|
356
|
+
thread_id: {
|
|
357
|
+
type: "string",
|
|
358
|
+
description: "Continue an existing conversation thread (omit to start a new one)"
|
|
359
|
+
},
|
|
360
|
+
max_depth: {
|
|
361
|
+
type: "number",
|
|
362
|
+
description: "Maximum number of back-and-forth exchanges (default: 10)"
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
required: ["agent_id", "message"]
|
|
366
|
+
},
|
|
367
|
+
handler: async (params) => {
|
|
368
|
+
const agentId = params.agent_id;
|
|
369
|
+
const message = params.message;
|
|
370
|
+
const threadId = params.thread_id;
|
|
371
|
+
const maxDepth = params.max_depth ?? 10;
|
|
372
|
+
if (!agentId || !message) throw new Error("agent_id and message are required");
|
|
373
|
+
log.info(`message_agent tool: sending to ${agentId}`);
|
|
374
|
+
return await chatClient.sendRequest("a2a.send", {
|
|
375
|
+
targetAgentId: agentId,
|
|
376
|
+
message,
|
|
377
|
+
threadId,
|
|
378
|
+
maxDepth
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}),
|
|
382
|
+
defineTool({
|
|
383
|
+
name: "end_conversation",
|
|
384
|
+
description: "End the current agent-to-agent conversation. Call this when the discussion is resolved and no further exchanges are needed.",
|
|
385
|
+
parameters: {
|
|
386
|
+
type: "object",
|
|
387
|
+
properties: { summary: {
|
|
388
|
+
type: "string",
|
|
389
|
+
description: "Brief summary of what was discussed or resolved"
|
|
390
|
+
} },
|
|
391
|
+
required: []
|
|
392
|
+
},
|
|
393
|
+
handler: (params) => {
|
|
394
|
+
const summary = params.summary;
|
|
395
|
+
log.info(`end_conversation tool called${summary ? `: ${summary}` : ""}`);
|
|
396
|
+
return Promise.resolve({
|
|
397
|
+
status: "conversation_ended",
|
|
398
|
+
summary
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
];
|
|
403
|
+
}
|
|
404
|
+
//#endregion
|
|
292
405
|
//#region src/plugin.ts
|
|
293
406
|
/**
|
|
294
407
|
* @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
|
|
@@ -305,17 +418,29 @@ async function listSessions(filters, limit = 50) {
|
|
|
305
418
|
*/
|
|
306
419
|
let dispatchInbound = null;
|
|
307
420
|
/**
|
|
308
|
-
* Resolve OpenClaw SDK functions from the
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
421
|
+
* Resolve OpenClaw SDK functions from the running process.
|
|
422
|
+
*
|
|
423
|
+
* The openclaw package is NOT in the plugin's node_modules (it's a peer dep).
|
|
424
|
+
* Since the plugin runs inside OpenClaw's process, we anchor resolution to
|
|
425
|
+
* OpenClaw's own entry module via require.main, then fall back to deriving
|
|
426
|
+
* the global modules path from process.execPath.
|
|
312
427
|
*/
|
|
313
428
|
function resolveOpenClawSdk(log) {
|
|
314
|
-
|
|
315
|
-
|
|
429
|
+
const anchors = [require.main?.filename, process.argv[1]].filter(Boolean);
|
|
430
|
+
for (const anchor of anchors) try {
|
|
431
|
+
const channelInbound = (0, node_module.createRequire)(anchor)("openclaw/plugin-sdk/channel-inbound");
|
|
432
|
+
if (channelInbound.dispatchInboundDirectDmWithRuntime) {
|
|
433
|
+
dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
|
|
434
|
+
log.info(`Resolved OpenClaw SDK from ${anchor}`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
} catch {}
|
|
438
|
+
try {
|
|
439
|
+
const derivedPath = (0, node_path.join)((0, node_path.resolve)((0, node_path.dirname)(process.execPath), ".."), "lib", "node_modules", "openclaw", "package.json");
|
|
440
|
+
const channelInbound = (0, node_module.createRequire)(derivedPath)("openclaw/plugin-sdk/channel-inbound");
|
|
316
441
|
if (channelInbound.dispatchInboundDirectDmWithRuntime) {
|
|
317
442
|
dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
|
|
318
|
-
log.info(`Resolved OpenClaw SDK from ${
|
|
443
|
+
log.info(`Resolved OpenClaw SDK from ${derivedPath}`);
|
|
319
444
|
return;
|
|
320
445
|
}
|
|
321
446
|
} catch {}
|
|
@@ -325,20 +450,30 @@ let pluginRuntime = null;
|
|
|
325
450
|
let chatClient = null;
|
|
326
451
|
let connectingPromise = null;
|
|
327
452
|
let metricsClient = null;
|
|
453
|
+
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
454
|
+
const DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
328
455
|
async function downloadAttachments(attachments, log) {
|
|
329
456
|
const attachDir = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "attachments");
|
|
330
457
|
await (0, node_fs_promises.mkdir)(attachDir, { recursive: true });
|
|
331
458
|
const results = [];
|
|
332
459
|
for (const att of attachments) {
|
|
333
|
-
const filename = att.filename ?? att.id;
|
|
460
|
+
const filename = (att.filename ?? att.id).replace(/[/\\]/g, "_").replace(/\.\./g, "_").replace(/\0/g, "");
|
|
334
461
|
const localPath = (0, node_path.join)(attachDir, `${att.id}_${filename}`);
|
|
462
|
+
const controller = new AbortController();
|
|
463
|
+
const timeout = setTimeout(() => {
|
|
464
|
+
controller.abort();
|
|
465
|
+
}, DOWNLOAD_TIMEOUT_MS);
|
|
335
466
|
try {
|
|
336
|
-
const res = await fetch(att.url);
|
|
467
|
+
const res = await fetch(att.url, { signal: controller.signal });
|
|
337
468
|
if (!res.ok) {
|
|
338
469
|
log.warn(`Failed to download attachment ${att.id}: ${String(res.status)}`);
|
|
339
470
|
continue;
|
|
340
471
|
}
|
|
341
472
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
473
|
+
if (buffer.length > MAX_FILE_SIZE) {
|
|
474
|
+
log.warn(`Attachment ${att.id} exceeds max size (${String(buffer.length)} bytes) — skipping`);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
342
477
|
await (0, node_fs_promises.writeFile)(localPath, buffer);
|
|
343
478
|
results.push({
|
|
344
479
|
localPath,
|
|
@@ -348,6 +483,8 @@ async function downloadAttachments(attachments, log) {
|
|
|
348
483
|
log.info(`Downloaded attachment: ${localPath} (${String(buffer.length)} bytes)`);
|
|
349
484
|
} catch (err) {
|
|
350
485
|
log.error(`Failed to download attachment ${att.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
486
|
+
} finally {
|
|
487
|
+
clearTimeout(timeout);
|
|
351
488
|
}
|
|
352
489
|
}
|
|
353
490
|
return results;
|
|
@@ -362,7 +499,8 @@ async function handleAgentRequest(request, log) {
|
|
|
362
499
|
chatClient?.sendResponse(request.id, false, { message: "OpenClaw SDK not available — cannot dispatch" });
|
|
363
500
|
return;
|
|
364
501
|
}
|
|
365
|
-
const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, displayName, attachments: rawAttachments } = request.params;
|
|
502
|
+
const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, displayName, attachments: rawAttachments, a2a } = request.params;
|
|
503
|
+
const isA2A = !!a2a;
|
|
366
504
|
if (!message && !rawAttachments?.length) {
|
|
367
505
|
chatClient?.sendResponse(request.id, false, { message: "Missing message" });
|
|
368
506
|
return;
|
|
@@ -389,11 +527,13 @@ async function handleAgentRequest(request, log) {
|
|
|
389
527
|
try {
|
|
390
528
|
const downloadedFiles = rawAttachments?.length ? await downloadAttachments(rawAttachments, log) : [];
|
|
391
529
|
const bodyForAgent = downloadedFiles.length ? `${message || ""}\n\n[Attached files:\n${downloadedFiles.map((f) => `- ${f.filename}: ${f.localPath}`).join("\n")}]` : void 0;
|
|
392
|
-
const channelMode = extractChannelMode(conversationId ?? "", clientType ?? "chat");
|
|
393
|
-
const channelLabel = channelMode === "sms" ? "SMS" : channelMode === "whatsapp" ? "WhatsApp" : "Alfe";
|
|
530
|
+
const channelMode = isA2A ? "a2a" : extractChannelMode(conversationId ?? "", clientType ?? "chat");
|
|
531
|
+
const channelLabel = channelMode === "sms" ? "SMS" : channelMode === "whatsapp" ? "WhatsApp" : channelMode === "a2a" ? "Agent" : "Alfe";
|
|
394
532
|
const shortConvId = conversationId?.slice(-8) ?? "";
|
|
395
533
|
const userLabel = displayName ?? userId ?? senderId;
|
|
396
534
|
const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
|
|
535
|
+
let a2aResponseBuffer = "";
|
|
536
|
+
let a2aResolved = false;
|
|
397
537
|
resolvedOpenClawKey = (await dispatchInbound({
|
|
398
538
|
cfg,
|
|
399
539
|
runtime: { channel: runtime.channel },
|
|
@@ -420,12 +560,29 @@ async function handleAgentRequest(request, log) {
|
|
|
420
560
|
...clientType ? { ClientType: clientType } : {},
|
|
421
561
|
...conversationId ? { ConversationId: conversationId } : {},
|
|
422
562
|
...displayName ? { SenderName: displayName } : {},
|
|
423
|
-
ChannelMode: channelMode
|
|
563
|
+
ChannelMode: channelMode,
|
|
564
|
+
...isA2A ? {
|
|
565
|
+
CallerType: "agent",
|
|
566
|
+
CallerAgentId: a2a.sourceAgentId,
|
|
567
|
+
CallerAgentName: a2a.sourceAgentName,
|
|
568
|
+
InteractionDepth: String(a2a.depth),
|
|
569
|
+
A2ASystemPrompt: [
|
|
570
|
+
`This is an agent-to-agent conversation with ${a2a.sourceAgentName}.`,
|
|
571
|
+
"Rules:",
|
|
572
|
+
"- Only respond if you have new information, a question, or an action to coordinate.",
|
|
573
|
+
"- If the conversation is complete, call the end_conversation() tool OR end your message with [RESOLVED].",
|
|
574
|
+
"- Do NOT respond just to acknowledge — that creates infinite loops."
|
|
575
|
+
].join("\n")
|
|
576
|
+
} : {}
|
|
424
577
|
},
|
|
425
578
|
deliver: async (payload) => {
|
|
426
579
|
const responseText = payload.text ?? "";
|
|
427
580
|
const mediaUrls = [...payload.mediaUrl ? [payload.mediaUrl] : [], ...payload.mediaUrls ?? []];
|
|
428
581
|
await addMessage(sessionId, "assistant", responseText);
|
|
582
|
+
if (isA2A) {
|
|
583
|
+
a2aResponseBuffer += responseText;
|
|
584
|
+
if (responseText.includes("\"status\":\"conversation_ended\"") || responseText.includes("\"status\": \"conversation_ended\"")) a2aResolved = true;
|
|
585
|
+
}
|
|
429
586
|
chatClient?.notify("agent-message", {
|
|
430
587
|
conversationId: conversationId ?? legacySessionKey,
|
|
431
588
|
text: responseText,
|
|
@@ -437,7 +594,7 @@ async function handleAgentRequest(request, log) {
|
|
|
437
594
|
channel: "alfe",
|
|
438
595
|
role: "assistant"
|
|
439
596
|
}).catch((err) => {
|
|
440
|
-
log.
|
|
597
|
+
log.warn(`Metrics report failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
441
598
|
});
|
|
442
599
|
},
|
|
443
600
|
onRecordError: (err) => {
|
|
@@ -447,6 +604,12 @@ async function handleAgentRequest(request, log) {
|
|
|
447
604
|
log.error(`Dispatch error (${info.kind}): ${err instanceof Error ? err.message : String(err)}`);
|
|
448
605
|
}
|
|
449
606
|
})).route.sessionKey;
|
|
607
|
+
if (isA2A && a2aResponseBuffer) chatClient?.notify("a2a-complete", {
|
|
608
|
+
conversationId: conversationId ?? legacySessionKey,
|
|
609
|
+
fullText: a2aResponseBuffer,
|
|
610
|
+
a2a,
|
|
611
|
+
resolved: a2aResolved
|
|
612
|
+
});
|
|
450
613
|
chatClient?.sendResponse(request.id, true, { sessionKey: resolvedOpenClawKey });
|
|
451
614
|
log.info(`Agent dispatch complete: sessionKey=${resolvedOpenClawKey}`);
|
|
452
615
|
} catch (err) {
|
|
@@ -516,10 +679,15 @@ const plugin = {
|
|
|
516
679
|
activate(api) {
|
|
517
680
|
const log = api.logger;
|
|
518
681
|
const alreadyActivated = globalThis.__alfeChatPluginActivated === true;
|
|
519
|
-
|
|
682
|
+
const isGatewayMode = !!api.runtime;
|
|
520
683
|
const alfeChannel = createAlfeChannelPlugin();
|
|
521
684
|
api.registerChannel(alfeChannel);
|
|
522
685
|
log.info(`Registered channel: ${alfeChannel.id}`);
|
|
686
|
+
if (!isGatewayMode) {
|
|
687
|
+
log.debug("Management command context — skipping persistent resource init");
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
globalThis.__alfeChatPluginActivated = true;
|
|
523
691
|
if (!alreadyActivated) {
|
|
524
692
|
log.info("Chat plugin registering...");
|
|
525
693
|
resolveOpenClawSdk(log);
|
|
@@ -547,10 +715,17 @@ const plugin = {
|
|
|
547
715
|
wsUrl: chatWsUrl,
|
|
548
716
|
apiKey,
|
|
549
717
|
onRequest: (request) => {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
718
|
+
const handle = async () => {
|
|
719
|
+
if (request.method === "agent") await handleAgentRequest(request, log);
|
|
720
|
+
else if (request.method === "sessions.list") await handleSessionsList(request, log);
|
|
721
|
+
else if (request.method === "sessions.get") await handleSessionsGet(request, log);
|
|
722
|
+
else chatClient?.sendResponse(request.id, false, { message: `Unknown method: ${request.method}` });
|
|
723
|
+
};
|
|
724
|
+
handle().catch((err) => {
|
|
725
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
726
|
+
log.error(`Request handler crashed: ${errMsg}`);
|
|
727
|
+
chatClient?.sendResponse(request.id, false, { message: "Internal error" });
|
|
728
|
+
});
|
|
554
729
|
},
|
|
555
730
|
onConnectionChange: (connected) => {
|
|
556
731
|
log.info(`Chat service connection: ${connected ? "connected" : "disconnected"}`);
|
|
@@ -599,20 +774,30 @@ const plugin = {
|
|
|
599
774
|
});
|
|
600
775
|
log.info("Registered gateway RPC methods: sessions.list, sessions.get");
|
|
601
776
|
}
|
|
777
|
+
if (!alreadyActivated && typeof api.registerTool === "function") connectingPromise?.then(() => {
|
|
778
|
+
if (chatClient && typeof api.registerTool === "function") {
|
|
779
|
+
const a2aTools = buildA2ATools(chatClient, log);
|
|
780
|
+
for (const tool of a2aTools) api.registerTool(tool);
|
|
781
|
+
log.info(`Registered ${String(a2aTools.length)} agent-to-agent tools`);
|
|
782
|
+
}
|
|
783
|
+
}).catch(() => {});
|
|
602
784
|
if (!alreadyActivated) {
|
|
603
785
|
api.on("session_start", async (...eventArgs) => {
|
|
786
|
+
if (!pluginRuntime) return;
|
|
604
787
|
const key = eventArgs[0].sessionKey;
|
|
605
788
|
if (!key || isAlfeSessionKey(key)) return;
|
|
606
789
|
log.info(`Chat session starting: ${key}`);
|
|
607
790
|
await createSession(key, "", "alfe");
|
|
608
791
|
}, { priority: 50 });
|
|
609
792
|
api.on("message_received", async (...eventArgs) => {
|
|
793
|
+
if (!pluginRuntime) return;
|
|
610
794
|
const event = eventArgs[0];
|
|
611
795
|
const ctx = eventArgs[1];
|
|
612
796
|
if (!ctx.conversationId || ctx.channelId === "alfe") return;
|
|
613
797
|
await addMessage(ctx.conversationId, "user", event.content, event.from);
|
|
614
798
|
});
|
|
615
799
|
api.on("session_end", (...eventArgs) => {
|
|
800
|
+
if (!pluginRuntime) return;
|
|
616
801
|
const key = eventArgs[0].sessionKey;
|
|
617
802
|
if (!key || !isAlfeSessionKey(key)) return;
|
|
618
803
|
log.info(`Chat session ending: ${key}`);
|
package/dist/plugin2.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { ChatServiceClient, resolveAlfeChat } from "@alfe.ai/chat";
|
|
6
6
|
import { resolveConfig } from "@alfe.ai/config";
|
|
7
7
|
import { AgentApiClient } from "@alfe.ai/agent-api-client";
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
|
+
//#region \0rolldown/runtime.js
|
|
10
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
11
|
+
//#endregion
|
|
9
12
|
//#region src/alfe-channel.ts
|
|
10
13
|
const CHANNEL_ID = "alfe";
|
|
11
14
|
const DEFAULT_ACCOUNT_ID = "default";
|
|
@@ -151,7 +154,7 @@ function isAlfeSessionKey(key) {
|
|
|
151
154
|
}
|
|
152
155
|
/**
|
|
153
156
|
* Extract the channel mode from a standardized session key or conversationId.
|
|
154
|
-
* Returns the mode segment (e.g. 'sms', 'whatsapp', 'chat') or fallback.
|
|
157
|
+
* Returns the mode segment (e.g. 'sms', 'whatsapp', 'chat', 'a2a') or fallback.
|
|
155
158
|
*/
|
|
156
159
|
function extractChannelMode(conversationId, fallback = "chat") {
|
|
157
160
|
return /^alfe:(\w+):/.exec(conversationId)?.[1] ?? fallback;
|
|
@@ -181,7 +184,6 @@ function sessionPath(sessionId) {
|
|
|
181
184
|
}
|
|
182
185
|
async function cleanupOldSessions() {
|
|
183
186
|
if (Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return;
|
|
184
|
-
lastCleanupAt = Date.now();
|
|
185
187
|
try {
|
|
186
188
|
const jsonFiles = (await readdir(SESSIONS_DIR)).filter((f) => f.endsWith(".json"));
|
|
187
189
|
if (jsonFiles.length <= MAX_SESSIONS) {
|
|
@@ -211,6 +213,7 @@ async function cleanupOldSessions() {
|
|
|
211
213
|
remaining--;
|
|
212
214
|
} catch {}
|
|
213
215
|
}
|
|
216
|
+
lastCleanupAt = Date.now();
|
|
214
217
|
} catch {}
|
|
215
218
|
}
|
|
216
219
|
async function getSession(sessionId) {
|
|
@@ -242,17 +245,24 @@ async function createSession(sessionId, agentId, channel, tenantId, userId) {
|
|
|
242
245
|
cleanupOldSessions();
|
|
243
246
|
return session;
|
|
244
247
|
}
|
|
248
|
+
const writeLocks = /* @__PURE__ */ new Map();
|
|
245
249
|
async function addMessage(sessionId, role, content, senderId, senderName) {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
const next = (writeLocks.get(sessionId) ?? Promise.resolve()).then(async () => {
|
|
251
|
+
const session = await getSession(sessionId);
|
|
252
|
+
if (!session) return;
|
|
253
|
+
session.messages.push({
|
|
254
|
+
role,
|
|
255
|
+
content,
|
|
256
|
+
timestamp: Date.now(),
|
|
257
|
+
...senderId ? { senderId } : {},
|
|
258
|
+
...senderName ? { senderName } : {}
|
|
259
|
+
});
|
|
260
|
+
await saveSession(session);
|
|
261
|
+
}).finally(() => {
|
|
262
|
+
if (writeLocks.get(sessionId) === next) writeLocks.delete(sessionId);
|
|
254
263
|
});
|
|
255
|
-
|
|
264
|
+
writeLocks.set(sessionId, next);
|
|
265
|
+
await next;
|
|
256
266
|
}
|
|
257
267
|
async function listSessions(filters, limit = 50) {
|
|
258
268
|
await ensureDir();
|
|
@@ -289,6 +299,112 @@ async function listSessions(filters, limit = 50) {
|
|
|
289
299
|
return summaries.slice(0, limit);
|
|
290
300
|
}
|
|
291
301
|
//#endregion
|
|
302
|
+
//#region src/a2a-tools.ts
|
|
303
|
+
function ok(result) {
|
|
304
|
+
return { content: [{
|
|
305
|
+
type: "text",
|
|
306
|
+
text: JSON.stringify(result)
|
|
307
|
+
}] };
|
|
308
|
+
}
|
|
309
|
+
function errResult(message) {
|
|
310
|
+
return { content: [{
|
|
311
|
+
type: "text",
|
|
312
|
+
text: JSON.stringify({ error: message })
|
|
313
|
+
}] };
|
|
314
|
+
}
|
|
315
|
+
function defineTool(def) {
|
|
316
|
+
return {
|
|
317
|
+
name: def.name,
|
|
318
|
+
description: def.description,
|
|
319
|
+
label: def.name,
|
|
320
|
+
parameters: def.parameters,
|
|
321
|
+
execute: async (_toolCallId, params) => {
|
|
322
|
+
try {
|
|
323
|
+
return ok(await def.handler(params));
|
|
324
|
+
} catch (e) {
|
|
325
|
+
return errResult(e.message);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function buildA2ATools(chatClient, log) {
|
|
331
|
+
return [
|
|
332
|
+
defineTool({
|
|
333
|
+
name: "list_agents",
|
|
334
|
+
description: "List other agents in your organization that you can communicate with.",
|
|
335
|
+
parameters: {
|
|
336
|
+
type: "object",
|
|
337
|
+
properties: {},
|
|
338
|
+
required: []
|
|
339
|
+
},
|
|
340
|
+
handler: async () => {
|
|
341
|
+
log.info("list_agents tool called");
|
|
342
|
+
return await chatClient.sendRequest("a2a.list-agents", {});
|
|
343
|
+
}
|
|
344
|
+
}),
|
|
345
|
+
defineTool({
|
|
346
|
+
name: "message_agent",
|
|
347
|
+
description: "Send a message to another agent. Starts a new conversation thread, or continues an existing one. The conversation will bounce back and forth automatically until one agent calls end_conversation() or says [RESOLVED].",
|
|
348
|
+
parameters: {
|
|
349
|
+
type: "object",
|
|
350
|
+
properties: {
|
|
351
|
+
agent_id: {
|
|
352
|
+
type: "string",
|
|
353
|
+
description: "Target agent ID (use list_agents to find available agents)"
|
|
354
|
+
},
|
|
355
|
+
message: {
|
|
356
|
+
type: "string",
|
|
357
|
+
description: "Message to send to the agent"
|
|
358
|
+
},
|
|
359
|
+
thread_id: {
|
|
360
|
+
type: "string",
|
|
361
|
+
description: "Continue an existing conversation thread (omit to start a new one)"
|
|
362
|
+
},
|
|
363
|
+
max_depth: {
|
|
364
|
+
type: "number",
|
|
365
|
+
description: "Maximum number of back-and-forth exchanges (default: 10)"
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
required: ["agent_id", "message"]
|
|
369
|
+
},
|
|
370
|
+
handler: async (params) => {
|
|
371
|
+
const agentId = params.agent_id;
|
|
372
|
+
const message = params.message;
|
|
373
|
+
const threadId = params.thread_id;
|
|
374
|
+
const maxDepth = params.max_depth ?? 10;
|
|
375
|
+
if (!agentId || !message) throw new Error("agent_id and message are required");
|
|
376
|
+
log.info(`message_agent tool: sending to ${agentId}`);
|
|
377
|
+
return await chatClient.sendRequest("a2a.send", {
|
|
378
|
+
targetAgentId: agentId,
|
|
379
|
+
message,
|
|
380
|
+
threadId,
|
|
381
|
+
maxDepth
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}),
|
|
385
|
+
defineTool({
|
|
386
|
+
name: "end_conversation",
|
|
387
|
+
description: "End the current agent-to-agent conversation. Call this when the discussion is resolved and no further exchanges are needed.",
|
|
388
|
+
parameters: {
|
|
389
|
+
type: "object",
|
|
390
|
+
properties: { summary: {
|
|
391
|
+
type: "string",
|
|
392
|
+
description: "Brief summary of what was discussed or resolved"
|
|
393
|
+
} },
|
|
394
|
+
required: []
|
|
395
|
+
},
|
|
396
|
+
handler: (params) => {
|
|
397
|
+
const summary = params.summary;
|
|
398
|
+
log.info(`end_conversation tool called${summary ? `: ${summary}` : ""}`);
|
|
399
|
+
return Promise.resolve({
|
|
400
|
+
status: "conversation_ended",
|
|
401
|
+
summary
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
];
|
|
406
|
+
}
|
|
407
|
+
//#endregion
|
|
292
408
|
//#region src/plugin.ts
|
|
293
409
|
/**
|
|
294
410
|
* @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
|
|
@@ -305,17 +421,29 @@ async function listSessions(filters, limit = 50) {
|
|
|
305
421
|
*/
|
|
306
422
|
let dispatchInbound = null;
|
|
307
423
|
/**
|
|
308
|
-
* Resolve OpenClaw SDK functions from the
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
424
|
+
* Resolve OpenClaw SDK functions from the running process.
|
|
425
|
+
*
|
|
426
|
+
* The openclaw package is NOT in the plugin's node_modules (it's a peer dep).
|
|
427
|
+
* Since the plugin runs inside OpenClaw's process, we anchor resolution to
|
|
428
|
+
* OpenClaw's own entry module via require.main, then fall back to deriving
|
|
429
|
+
* the global modules path from process.execPath.
|
|
312
430
|
*/
|
|
313
431
|
function resolveOpenClawSdk(log) {
|
|
314
|
-
|
|
315
|
-
|
|
432
|
+
const anchors = [__require.main?.filename, process.argv[1]].filter(Boolean);
|
|
433
|
+
for (const anchor of anchors) try {
|
|
434
|
+
const channelInbound = createRequire(anchor)("openclaw/plugin-sdk/channel-inbound");
|
|
435
|
+
if (channelInbound.dispatchInboundDirectDmWithRuntime) {
|
|
436
|
+
dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
|
|
437
|
+
log.info(`Resolved OpenClaw SDK from ${anchor}`);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
} catch {}
|
|
441
|
+
try {
|
|
442
|
+
const derivedPath = join(resolve(dirname(process.execPath), ".."), "lib", "node_modules", "openclaw", "package.json");
|
|
443
|
+
const channelInbound = createRequire(derivedPath)("openclaw/plugin-sdk/channel-inbound");
|
|
316
444
|
if (channelInbound.dispatchInboundDirectDmWithRuntime) {
|
|
317
445
|
dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
|
|
318
|
-
log.info(`Resolved OpenClaw SDK from ${
|
|
446
|
+
log.info(`Resolved OpenClaw SDK from ${derivedPath}`);
|
|
319
447
|
return;
|
|
320
448
|
}
|
|
321
449
|
} catch {}
|
|
@@ -325,20 +453,30 @@ let pluginRuntime = null;
|
|
|
325
453
|
let chatClient = null;
|
|
326
454
|
let connectingPromise = null;
|
|
327
455
|
let metricsClient = null;
|
|
456
|
+
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
457
|
+
const DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
328
458
|
async function downloadAttachments(attachments, log) {
|
|
329
459
|
const attachDir = join(homedir(), ".alfe", "attachments");
|
|
330
460
|
await mkdir(attachDir, { recursive: true });
|
|
331
461
|
const results = [];
|
|
332
462
|
for (const att of attachments) {
|
|
333
|
-
const filename = att.filename ?? att.id;
|
|
463
|
+
const filename = (att.filename ?? att.id).replace(/[/\\]/g, "_").replace(/\.\./g, "_").replace(/\0/g, "");
|
|
334
464
|
const localPath = join(attachDir, `${att.id}_${filename}`);
|
|
465
|
+
const controller = new AbortController();
|
|
466
|
+
const timeout = setTimeout(() => {
|
|
467
|
+
controller.abort();
|
|
468
|
+
}, DOWNLOAD_TIMEOUT_MS);
|
|
335
469
|
try {
|
|
336
|
-
const res = await fetch(att.url);
|
|
470
|
+
const res = await fetch(att.url, { signal: controller.signal });
|
|
337
471
|
if (!res.ok) {
|
|
338
472
|
log.warn(`Failed to download attachment ${att.id}: ${String(res.status)}`);
|
|
339
473
|
continue;
|
|
340
474
|
}
|
|
341
475
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
476
|
+
if (buffer.length > MAX_FILE_SIZE) {
|
|
477
|
+
log.warn(`Attachment ${att.id} exceeds max size (${String(buffer.length)} bytes) — skipping`);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
342
480
|
await writeFile(localPath, buffer);
|
|
343
481
|
results.push({
|
|
344
482
|
localPath,
|
|
@@ -348,6 +486,8 @@ async function downloadAttachments(attachments, log) {
|
|
|
348
486
|
log.info(`Downloaded attachment: ${localPath} (${String(buffer.length)} bytes)`);
|
|
349
487
|
} catch (err) {
|
|
350
488
|
log.error(`Failed to download attachment ${att.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
489
|
+
} finally {
|
|
490
|
+
clearTimeout(timeout);
|
|
351
491
|
}
|
|
352
492
|
}
|
|
353
493
|
return results;
|
|
@@ -362,7 +502,8 @@ async function handleAgentRequest(request, log) {
|
|
|
362
502
|
chatClient?.sendResponse(request.id, false, { message: "OpenClaw SDK not available — cannot dispatch" });
|
|
363
503
|
return;
|
|
364
504
|
}
|
|
365
|
-
const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, displayName, attachments: rawAttachments } = request.params;
|
|
505
|
+
const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, displayName, attachments: rawAttachments, a2a } = request.params;
|
|
506
|
+
const isA2A = !!a2a;
|
|
366
507
|
if (!message && !rawAttachments?.length) {
|
|
367
508
|
chatClient?.sendResponse(request.id, false, { message: "Missing message" });
|
|
368
509
|
return;
|
|
@@ -389,11 +530,13 @@ async function handleAgentRequest(request, log) {
|
|
|
389
530
|
try {
|
|
390
531
|
const downloadedFiles = rawAttachments?.length ? await downloadAttachments(rawAttachments, log) : [];
|
|
391
532
|
const bodyForAgent = downloadedFiles.length ? `${message || ""}\n\n[Attached files:\n${downloadedFiles.map((f) => `- ${f.filename}: ${f.localPath}`).join("\n")}]` : void 0;
|
|
392
|
-
const channelMode = extractChannelMode(conversationId ?? "", clientType ?? "chat");
|
|
393
|
-
const channelLabel = channelMode === "sms" ? "SMS" : channelMode === "whatsapp" ? "WhatsApp" : "Alfe";
|
|
533
|
+
const channelMode = isA2A ? "a2a" : extractChannelMode(conversationId ?? "", clientType ?? "chat");
|
|
534
|
+
const channelLabel = channelMode === "sms" ? "SMS" : channelMode === "whatsapp" ? "WhatsApp" : channelMode === "a2a" ? "Agent" : "Alfe";
|
|
394
535
|
const shortConvId = conversationId?.slice(-8) ?? "";
|
|
395
536
|
const userLabel = displayName ?? userId ?? senderId;
|
|
396
537
|
const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
|
|
538
|
+
let a2aResponseBuffer = "";
|
|
539
|
+
let a2aResolved = false;
|
|
397
540
|
resolvedOpenClawKey = (await dispatchInbound({
|
|
398
541
|
cfg,
|
|
399
542
|
runtime: { channel: runtime.channel },
|
|
@@ -420,12 +563,29 @@ async function handleAgentRequest(request, log) {
|
|
|
420
563
|
...clientType ? { ClientType: clientType } : {},
|
|
421
564
|
...conversationId ? { ConversationId: conversationId } : {},
|
|
422
565
|
...displayName ? { SenderName: displayName } : {},
|
|
423
|
-
ChannelMode: channelMode
|
|
566
|
+
ChannelMode: channelMode,
|
|
567
|
+
...isA2A ? {
|
|
568
|
+
CallerType: "agent",
|
|
569
|
+
CallerAgentId: a2a.sourceAgentId,
|
|
570
|
+
CallerAgentName: a2a.sourceAgentName,
|
|
571
|
+
InteractionDepth: String(a2a.depth),
|
|
572
|
+
A2ASystemPrompt: [
|
|
573
|
+
`This is an agent-to-agent conversation with ${a2a.sourceAgentName}.`,
|
|
574
|
+
"Rules:",
|
|
575
|
+
"- Only respond if you have new information, a question, or an action to coordinate.",
|
|
576
|
+
"- If the conversation is complete, call the end_conversation() tool OR end your message with [RESOLVED].",
|
|
577
|
+
"- Do NOT respond just to acknowledge — that creates infinite loops."
|
|
578
|
+
].join("\n")
|
|
579
|
+
} : {}
|
|
424
580
|
},
|
|
425
581
|
deliver: async (payload) => {
|
|
426
582
|
const responseText = payload.text ?? "";
|
|
427
583
|
const mediaUrls = [...payload.mediaUrl ? [payload.mediaUrl] : [], ...payload.mediaUrls ?? []];
|
|
428
584
|
await addMessage(sessionId, "assistant", responseText);
|
|
585
|
+
if (isA2A) {
|
|
586
|
+
a2aResponseBuffer += responseText;
|
|
587
|
+
if (responseText.includes("\"status\":\"conversation_ended\"") || responseText.includes("\"status\": \"conversation_ended\"")) a2aResolved = true;
|
|
588
|
+
}
|
|
429
589
|
chatClient?.notify("agent-message", {
|
|
430
590
|
conversationId: conversationId ?? legacySessionKey,
|
|
431
591
|
text: responseText,
|
|
@@ -437,7 +597,7 @@ async function handleAgentRequest(request, log) {
|
|
|
437
597
|
channel: "alfe",
|
|
438
598
|
role: "assistant"
|
|
439
599
|
}).catch((err) => {
|
|
440
|
-
log.
|
|
600
|
+
log.warn(`Metrics report failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
441
601
|
});
|
|
442
602
|
},
|
|
443
603
|
onRecordError: (err) => {
|
|
@@ -447,6 +607,12 @@ async function handleAgentRequest(request, log) {
|
|
|
447
607
|
log.error(`Dispatch error (${info.kind}): ${err instanceof Error ? err.message : String(err)}`);
|
|
448
608
|
}
|
|
449
609
|
})).route.sessionKey;
|
|
610
|
+
if (isA2A && a2aResponseBuffer) chatClient?.notify("a2a-complete", {
|
|
611
|
+
conversationId: conversationId ?? legacySessionKey,
|
|
612
|
+
fullText: a2aResponseBuffer,
|
|
613
|
+
a2a,
|
|
614
|
+
resolved: a2aResolved
|
|
615
|
+
});
|
|
450
616
|
chatClient?.sendResponse(request.id, true, { sessionKey: resolvedOpenClawKey });
|
|
451
617
|
log.info(`Agent dispatch complete: sessionKey=${resolvedOpenClawKey}`);
|
|
452
618
|
} catch (err) {
|
|
@@ -516,10 +682,15 @@ const plugin = {
|
|
|
516
682
|
activate(api) {
|
|
517
683
|
const log = api.logger;
|
|
518
684
|
const alreadyActivated = globalThis.__alfeChatPluginActivated === true;
|
|
519
|
-
|
|
685
|
+
const isGatewayMode = !!api.runtime;
|
|
520
686
|
const alfeChannel = createAlfeChannelPlugin();
|
|
521
687
|
api.registerChannel(alfeChannel);
|
|
522
688
|
log.info(`Registered channel: ${alfeChannel.id}`);
|
|
689
|
+
if (!isGatewayMode) {
|
|
690
|
+
log.debug("Management command context — skipping persistent resource init");
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
globalThis.__alfeChatPluginActivated = true;
|
|
523
694
|
if (!alreadyActivated) {
|
|
524
695
|
log.info("Chat plugin registering...");
|
|
525
696
|
resolveOpenClawSdk(log);
|
|
@@ -547,10 +718,17 @@ const plugin = {
|
|
|
547
718
|
wsUrl: chatWsUrl,
|
|
548
719
|
apiKey,
|
|
549
720
|
onRequest: (request) => {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
721
|
+
const handle = async () => {
|
|
722
|
+
if (request.method === "agent") await handleAgentRequest(request, log);
|
|
723
|
+
else if (request.method === "sessions.list") await handleSessionsList(request, log);
|
|
724
|
+
else if (request.method === "sessions.get") await handleSessionsGet(request, log);
|
|
725
|
+
else chatClient?.sendResponse(request.id, false, { message: `Unknown method: ${request.method}` });
|
|
726
|
+
};
|
|
727
|
+
handle().catch((err) => {
|
|
728
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
729
|
+
log.error(`Request handler crashed: ${errMsg}`);
|
|
730
|
+
chatClient?.sendResponse(request.id, false, { message: "Internal error" });
|
|
731
|
+
});
|
|
554
732
|
},
|
|
555
733
|
onConnectionChange: (connected) => {
|
|
556
734
|
log.info(`Chat service connection: ${connected ? "connected" : "disconnected"}`);
|
|
@@ -599,20 +777,30 @@ const plugin = {
|
|
|
599
777
|
});
|
|
600
778
|
log.info("Registered gateway RPC methods: sessions.list, sessions.get");
|
|
601
779
|
}
|
|
780
|
+
if (!alreadyActivated && typeof api.registerTool === "function") connectingPromise?.then(() => {
|
|
781
|
+
if (chatClient && typeof api.registerTool === "function") {
|
|
782
|
+
const a2aTools = buildA2ATools(chatClient, log);
|
|
783
|
+
for (const tool of a2aTools) api.registerTool(tool);
|
|
784
|
+
log.info(`Registered ${String(a2aTools.length)} agent-to-agent tools`);
|
|
785
|
+
}
|
|
786
|
+
}).catch(() => {});
|
|
602
787
|
if (!alreadyActivated) {
|
|
603
788
|
api.on("session_start", async (...eventArgs) => {
|
|
789
|
+
if (!pluginRuntime) return;
|
|
604
790
|
const key = eventArgs[0].sessionKey;
|
|
605
791
|
if (!key || isAlfeSessionKey(key)) return;
|
|
606
792
|
log.info(`Chat session starting: ${key}`);
|
|
607
793
|
await createSession(key, "", "alfe");
|
|
608
794
|
}, { priority: 50 });
|
|
609
795
|
api.on("message_received", async (...eventArgs) => {
|
|
796
|
+
if (!pluginRuntime) return;
|
|
610
797
|
const event = eventArgs[0];
|
|
611
798
|
const ctx = eventArgs[1];
|
|
612
799
|
if (!ctx.conversationId || ctx.channelId === "alfe") return;
|
|
613
800
|
await addMessage(ctx.conversationId, "user", event.content, event.from);
|
|
614
801
|
});
|
|
615
802
|
api.on("session_end", (...eventArgs) => {
|
|
803
|
+
if (!pluginRuntime) return;
|
|
616
804
|
const key = eventArgs[0].sessionKey;
|
|
617
805
|
if (!key || !isAlfeSessionKey(key)) return;
|
|
618
806
|
log.info(`Chat session ending: ${key}`);
|
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.21",
|
|
4
4
|
"description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/plugin.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@alfe.ai/agent-api-client": "^0.0.7",
|
|
31
|
-
"@alfe.ai/chat": "^0.0.
|
|
31
|
+
"@alfe.ai/chat": "^0.0.7",
|
|
32
32
|
"@alfe.ai/config": "^0.0.7"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|