@alfe.ai/openclaw-chat 0.0.34 → 0.1.0
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.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/plugin.d.cts +71 -5
- package/dist/plugin.d.ts +71 -5
- package/dist/plugin2.cjs +111 -32
- package/dist/plugin2.js +111 -32
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/plugin.d.cts
CHANGED
|
@@ -33,6 +33,28 @@ interface AlfePluginConfig {
|
|
|
33
33
|
/** API key for chat service auth */
|
|
34
34
|
apiKey?: string;
|
|
35
35
|
}
|
|
36
|
+
interface AlfeOutboundChatClient {
|
|
37
|
+
notify(event: string, payload: Record<string, unknown>): void;
|
|
38
|
+
}
|
|
39
|
+
interface AlfeOutboundSessionSummary {
|
|
40
|
+
sessionId: string;
|
|
41
|
+
userId?: string;
|
|
42
|
+
createdAt: string;
|
|
43
|
+
lastMessageAt?: string;
|
|
44
|
+
}
|
|
45
|
+
interface AlfeOutboundSession {
|
|
46
|
+
sessionId: string;
|
|
47
|
+
userId?: string;
|
|
48
|
+
}
|
|
49
|
+
interface AlfeChannelOutboundDeps {
|
|
50
|
+
getChatClient: () => AlfeOutboundChatClient | null;
|
|
51
|
+
listSessions: (filters?: {
|
|
52
|
+
userId?: string;
|
|
53
|
+
}) => Promise<AlfeOutboundSessionSummary[]>;
|
|
54
|
+
getSession: (sessionId: string) => Promise<AlfeOutboundSession | null>;
|
|
55
|
+
createSession: (sessionId: string, agentId: string, channel: string, tenantId?: string, userId?: string) => Promise<unknown>;
|
|
56
|
+
addMessage: (sessionId: string, role: 'user' | 'assistant', content: string, senderId?: string, senderName?: string) => Promise<void>;
|
|
57
|
+
}
|
|
36
58
|
//#endregion
|
|
37
59
|
//#region src/alfe-channel.d.ts
|
|
38
60
|
/** OpenClaw config shape — inline to avoid runtime dependency on openclaw package */
|
|
@@ -43,13 +65,23 @@ interface OpenClawConfig {
|
|
|
43
65
|
};
|
|
44
66
|
[key: string]: unknown;
|
|
45
67
|
}
|
|
68
|
+
interface OutboundDeliveryResult {
|
|
69
|
+
channel: 'alfe';
|
|
70
|
+
messageId: string;
|
|
71
|
+
conversationId: string;
|
|
72
|
+
timestamp: number;
|
|
73
|
+
}
|
|
46
74
|
/**
|
|
47
75
|
* Creates the Alfe ChannelPlugin object for registration with OpenClaw.
|
|
48
76
|
*
|
|
49
77
|
* This follows the same pattern as built-in channels (Telegram, Discord, etc.)
|
|
50
78
|
* but is registered dynamically via api.registerChannel().
|
|
79
|
+
*
|
|
80
|
+
* `deps` enables outbound delivery and peer discovery. When omitted (e.g.
|
|
81
|
+
* during CLI metadata registration), outbound calls throw and the directory
|
|
82
|
+
* is empty — but the channel is still registered for inbound use.
|
|
51
83
|
*/
|
|
52
|
-
declare function createAlfeChannelPlugin(): {
|
|
84
|
+
declare function createAlfeChannelPlugin(deps?: AlfeChannelOutboundDeps): {
|
|
53
85
|
id: string;
|
|
54
86
|
meta: {
|
|
55
87
|
id: string;
|
|
@@ -118,13 +150,47 @@ declare function createAlfeChannelPlugin(): {
|
|
|
118
150
|
}): string | undefined;
|
|
119
151
|
};
|
|
120
152
|
/**
|
|
121
|
-
* Outbound delivery via
|
|
122
|
-
*
|
|
123
|
-
*
|
|
153
|
+
* Outbound delivery via the plugin (`deliveryMode: 'direct'`).
|
|
154
|
+
*
|
|
155
|
+
* OpenClaw calls `resolveTarget` to validate the agent-supplied `to`,
|
|
156
|
+
* then `sendText` / `sendMedia` per chunk. We push each chunk to the
|
|
157
|
+
* chat relay service via `chatClient.notify('agent-message', …)`,
|
|
158
|
+
* which broadcasts to all connected web/mobile clients on that
|
|
159
|
+
* conversation and notifies offline participants.
|
|
124
160
|
*/
|
|
125
161
|
outbound: {
|
|
126
|
-
deliveryMode: "
|
|
162
|
+
deliveryMode: "direct";
|
|
127
163
|
textChunkLimit: number;
|
|
164
|
+
resolveTarget(params: {
|
|
165
|
+
to?: string;
|
|
166
|
+
}): {
|
|
167
|
+
ok: true;
|
|
168
|
+
to: string;
|
|
169
|
+
} | {
|
|
170
|
+
ok: false;
|
|
171
|
+
error: Error;
|
|
172
|
+
};
|
|
173
|
+
sendText(ctx: {
|
|
174
|
+
to: string;
|
|
175
|
+
text: string;
|
|
176
|
+
}): Promise<OutboundDeliveryResult>;
|
|
177
|
+
sendMedia(ctx: {
|
|
178
|
+
to: string;
|
|
179
|
+
text: string;
|
|
180
|
+
mediaUrl?: string;
|
|
181
|
+
}): Promise<OutboundDeliveryResult>;
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Directory adapter — exposes users the agent has chatted with so the
|
|
185
|
+
* `message` tool can suggest valid targets. Backed by the on-disk
|
|
186
|
+
* session store; live-list is the same as static-list since sessions
|
|
187
|
+
* are the only source of truth.
|
|
188
|
+
*/
|
|
189
|
+
directory: {
|
|
190
|
+
listPeers(): Promise<{
|
|
191
|
+
kind: "user";
|
|
192
|
+
id: string;
|
|
193
|
+
}[]>;
|
|
128
194
|
};
|
|
129
195
|
/**
|
|
130
196
|
* Setup adapter — minimal for Alfe since no external tokens are needed.
|
package/dist/plugin.d.ts
CHANGED
|
@@ -33,6 +33,28 @@ interface AlfePluginConfig {
|
|
|
33
33
|
/** API key for chat service auth */
|
|
34
34
|
apiKey?: string;
|
|
35
35
|
}
|
|
36
|
+
interface AlfeOutboundChatClient {
|
|
37
|
+
notify(event: string, payload: Record<string, unknown>): void;
|
|
38
|
+
}
|
|
39
|
+
interface AlfeOutboundSessionSummary {
|
|
40
|
+
sessionId: string;
|
|
41
|
+
userId?: string;
|
|
42
|
+
createdAt: string;
|
|
43
|
+
lastMessageAt?: string;
|
|
44
|
+
}
|
|
45
|
+
interface AlfeOutboundSession {
|
|
46
|
+
sessionId: string;
|
|
47
|
+
userId?: string;
|
|
48
|
+
}
|
|
49
|
+
interface AlfeChannelOutboundDeps {
|
|
50
|
+
getChatClient: () => AlfeOutboundChatClient | null;
|
|
51
|
+
listSessions: (filters?: {
|
|
52
|
+
userId?: string;
|
|
53
|
+
}) => Promise<AlfeOutboundSessionSummary[]>;
|
|
54
|
+
getSession: (sessionId: string) => Promise<AlfeOutboundSession | null>;
|
|
55
|
+
createSession: (sessionId: string, agentId: string, channel: string, tenantId?: string, userId?: string) => Promise<unknown>;
|
|
56
|
+
addMessage: (sessionId: string, role: 'user' | 'assistant', content: string, senderId?: string, senderName?: string) => Promise<void>;
|
|
57
|
+
}
|
|
36
58
|
//#endregion
|
|
37
59
|
//#region src/alfe-channel.d.ts
|
|
38
60
|
/** OpenClaw config shape — inline to avoid runtime dependency on openclaw package */
|
|
@@ -43,13 +65,23 @@ interface OpenClawConfig {
|
|
|
43
65
|
};
|
|
44
66
|
[key: string]: unknown;
|
|
45
67
|
}
|
|
68
|
+
interface OutboundDeliveryResult {
|
|
69
|
+
channel: 'alfe';
|
|
70
|
+
messageId: string;
|
|
71
|
+
conversationId: string;
|
|
72
|
+
timestamp: number;
|
|
73
|
+
}
|
|
46
74
|
/**
|
|
47
75
|
* Creates the Alfe ChannelPlugin object for registration with OpenClaw.
|
|
48
76
|
*
|
|
49
77
|
* This follows the same pattern as built-in channels (Telegram, Discord, etc.)
|
|
50
78
|
* but is registered dynamically via api.registerChannel().
|
|
79
|
+
*
|
|
80
|
+
* `deps` enables outbound delivery and peer discovery. When omitted (e.g.
|
|
81
|
+
* during CLI metadata registration), outbound calls throw and the directory
|
|
82
|
+
* is empty — but the channel is still registered for inbound use.
|
|
51
83
|
*/
|
|
52
|
-
declare function createAlfeChannelPlugin(): {
|
|
84
|
+
declare function createAlfeChannelPlugin(deps?: AlfeChannelOutboundDeps): {
|
|
53
85
|
id: string;
|
|
54
86
|
meta: {
|
|
55
87
|
id: string;
|
|
@@ -118,13 +150,47 @@ declare function createAlfeChannelPlugin(): {
|
|
|
118
150
|
}): string | undefined;
|
|
119
151
|
};
|
|
120
152
|
/**
|
|
121
|
-
* Outbound delivery via
|
|
122
|
-
*
|
|
123
|
-
*
|
|
153
|
+
* Outbound delivery via the plugin (`deliveryMode: 'direct'`).
|
|
154
|
+
*
|
|
155
|
+
* OpenClaw calls `resolveTarget` to validate the agent-supplied `to`,
|
|
156
|
+
* then `sendText` / `sendMedia` per chunk. We push each chunk to the
|
|
157
|
+
* chat relay service via `chatClient.notify('agent-message', …)`,
|
|
158
|
+
* which broadcasts to all connected web/mobile clients on that
|
|
159
|
+
* conversation and notifies offline participants.
|
|
124
160
|
*/
|
|
125
161
|
outbound: {
|
|
126
|
-
deliveryMode: "
|
|
162
|
+
deliveryMode: "direct";
|
|
127
163
|
textChunkLimit: number;
|
|
164
|
+
resolveTarget(params: {
|
|
165
|
+
to?: string;
|
|
166
|
+
}): {
|
|
167
|
+
ok: true;
|
|
168
|
+
to: string;
|
|
169
|
+
} | {
|
|
170
|
+
ok: false;
|
|
171
|
+
error: Error;
|
|
172
|
+
};
|
|
173
|
+
sendText(ctx: {
|
|
174
|
+
to: string;
|
|
175
|
+
text: string;
|
|
176
|
+
}): Promise<OutboundDeliveryResult>;
|
|
177
|
+
sendMedia(ctx: {
|
|
178
|
+
to: string;
|
|
179
|
+
text: string;
|
|
180
|
+
mediaUrl?: string;
|
|
181
|
+
}): Promise<OutboundDeliveryResult>;
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Directory adapter — exposes users the agent has chatted with so the
|
|
185
|
+
* `message` tool can suggest valid targets. Backed by the on-disk
|
|
186
|
+
* session store; live-list is the same as static-list since sessions
|
|
187
|
+
* are the only source of truth.
|
|
188
|
+
*/
|
|
189
|
+
directory: {
|
|
190
|
+
listPeers(): Promise<{
|
|
191
|
+
kind: "user";
|
|
192
|
+
id: string;
|
|
193
|
+
}[]>;
|
|
128
194
|
};
|
|
129
195
|
/**
|
|
130
196
|
* Setup adapter — minimal for Alfe since no external tokens are needed.
|
package/dist/plugin2.cjs
CHANGED
|
@@ -3,12 +3,56 @@ let node_fs_promises = require("node:fs/promises");
|
|
|
3
3
|
let node_path = require("node:path");
|
|
4
4
|
let node_os = require("node:os");
|
|
5
5
|
let _alfe_ai_chat = require("@alfe.ai/chat");
|
|
6
|
-
let
|
|
7
|
-
let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
|
|
6
|
+
let node_crypto = require("node:crypto");
|
|
8
7
|
let node_fs = require("node:fs");
|
|
9
8
|
//#region src/alfe-channel.ts
|
|
9
|
+
/**
|
|
10
|
+
* Alfe channel plugin definition — registers 'alfe' as an OpenClaw channel.
|
|
11
|
+
*
|
|
12
|
+
* Web and mobile clients share the same channel and conversation sessions.
|
|
13
|
+
* Outbound delivery uses `deliveryMode: 'direct'` — the plugin's sendText /
|
|
14
|
+
* sendMedia handlers push messages through the chat relay service.
|
|
15
|
+
*
|
|
16
|
+
* Target formats accepted by `resolveTarget`:
|
|
17
|
+
* user:{clerkUserId} — most recent conversation for that user (created on demand)
|
|
18
|
+
* conv:{conversationId} — a specific existing conversation
|
|
19
|
+
*
|
|
20
|
+
* Config section: channels.alfe in openclaw.yaml
|
|
21
|
+
*/
|
|
10
22
|
const CHANNEL_ID = "alfe";
|
|
11
23
|
const DEFAULT_ACCOUNT_ID = "default";
|
|
24
|
+
async function sendViaChat(deps, ctx, mediaUrl) {
|
|
25
|
+
const client = deps.getChatClient();
|
|
26
|
+
if (!client) throw new Error("Chat service not connected — cannot deliver");
|
|
27
|
+
let conversationId;
|
|
28
|
+
if (ctx.to.startsWith("conv:")) {
|
|
29
|
+
conversationId = ctx.to.slice(5);
|
|
30
|
+
if (!await deps.getSession(conversationId)) throw new Error(`Conversation not found: ${conversationId}`);
|
|
31
|
+
} else {
|
|
32
|
+
const userId = ctx.to.startsWith("user:") ? ctx.to.slice(5) : ctx.to;
|
|
33
|
+
if (!userId) throw new Error("Empty userId in target");
|
|
34
|
+
const sessions = await deps.listSessions({ userId });
|
|
35
|
+
if (sessions.length > 0) conversationId = sessions[0].sessionId;
|
|
36
|
+
else {
|
|
37
|
+
conversationId = `alfe:chat:${userId}:${(0, node_crypto.randomUUID)()}`;
|
|
38
|
+
await deps.createSession(conversationId, "", "alfe", void 0, userId);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
await deps.addMessage(conversationId, "assistant", ctx.text);
|
|
42
|
+
const messageId = (0, node_crypto.randomUUID)();
|
|
43
|
+
client.notify("agent-message", {
|
|
44
|
+
conversationId,
|
|
45
|
+
text: ctx.text,
|
|
46
|
+
sessionKey: conversationId,
|
|
47
|
+
...mediaUrl ? { mediaUrls: [mediaUrl] } : {}
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
channel: "alfe",
|
|
51
|
+
messageId,
|
|
52
|
+
conversationId,
|
|
53
|
+
timestamp: Date.now()
|
|
54
|
+
};
|
|
55
|
+
}
|
|
12
56
|
function getChannelSection(cfg) {
|
|
13
57
|
return cfg.channels?.alfe ?? {};
|
|
14
58
|
}
|
|
@@ -17,8 +61,12 @@ function getChannelSection(cfg) {
|
|
|
17
61
|
*
|
|
18
62
|
* This follows the same pattern as built-in channels (Telegram, Discord, etc.)
|
|
19
63
|
* but is registered dynamically via api.registerChannel().
|
|
64
|
+
*
|
|
65
|
+
* `deps` enables outbound delivery and peer discovery. When omitted (e.g.
|
|
66
|
+
* during CLI metadata registration), outbound calls throw and the directory
|
|
67
|
+
* is empty — but the channel is still registered for inbound use.
|
|
20
68
|
*/
|
|
21
|
-
function createAlfeChannelPlugin() {
|
|
69
|
+
function createAlfeChannelPlugin(deps) {
|
|
22
70
|
return {
|
|
23
71
|
id: CHANNEL_ID,
|
|
24
72
|
meta: {
|
|
@@ -97,9 +145,58 @@ function createAlfeChannelPlugin() {
|
|
|
97
145
|
}
|
|
98
146
|
},
|
|
99
147
|
outbound: {
|
|
100
|
-
deliveryMode: "
|
|
101
|
-
textChunkLimit: 4e3
|
|
148
|
+
deliveryMode: "direct",
|
|
149
|
+
textChunkLimit: 4e3,
|
|
150
|
+
resolveTarget(params) {
|
|
151
|
+
const to = params.to;
|
|
152
|
+
if (!to) return {
|
|
153
|
+
ok: false,
|
|
154
|
+
error: /* @__PURE__ */ new Error("Missing target — use user:{userId} or conv:{conversationId}")
|
|
155
|
+
};
|
|
156
|
+
if (to.startsWith("conv:")) {
|
|
157
|
+
if (!to.slice(5)) return {
|
|
158
|
+
ok: false,
|
|
159
|
+
error: /* @__PURE__ */ new Error("Empty conversation ID")
|
|
160
|
+
};
|
|
161
|
+
return {
|
|
162
|
+
ok: true,
|
|
163
|
+
to
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const userId = to.startsWith("user:") ? to.slice(5) : to;
|
|
167
|
+
if (!userId || userId === "anon") return {
|
|
168
|
+
ok: false,
|
|
169
|
+
error: /* @__PURE__ */ new Error("Invalid target: userId is required")
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
ok: true,
|
|
173
|
+
to: `user:${userId}`
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
async sendText(ctx) {
|
|
177
|
+
if (!deps) throw new Error("Alfe channel deps not configured — outbound disabled");
|
|
178
|
+
return await sendViaChat(deps, ctx, void 0);
|
|
179
|
+
},
|
|
180
|
+
async sendMedia(ctx) {
|
|
181
|
+
if (!deps) throw new Error("Alfe channel deps not configured — outbound disabled");
|
|
182
|
+
return await sendViaChat(deps, ctx, ctx.mediaUrl);
|
|
183
|
+
}
|
|
102
184
|
},
|
|
185
|
+
directory: { async listPeers() {
|
|
186
|
+
if (!deps) return [];
|
|
187
|
+
const sessions = await deps.listSessions();
|
|
188
|
+
const seen = /* @__PURE__ */ new Set();
|
|
189
|
+
const peers = [];
|
|
190
|
+
for (const s of sessions) {
|
|
191
|
+
if (!s.userId || seen.has(s.userId)) continue;
|
|
192
|
+
seen.add(s.userId);
|
|
193
|
+
peers.push({
|
|
194
|
+
kind: "user",
|
|
195
|
+
id: s.userId
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return peers;
|
|
199
|
+
} },
|
|
103
200
|
setup: {
|
|
104
201
|
resolveAccountId(params) {
|
|
105
202
|
return params.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
@@ -268,6 +365,8 @@ async function listSessions(filters, limit = 50) {
|
|
|
268
365
|
sessionId: session.sessionId,
|
|
269
366
|
agentId: session.agentId,
|
|
270
367
|
channel: session.channel,
|
|
368
|
+
...session.tenantId ? { tenantId: session.tenantId } : {},
|
|
369
|
+
...session.userId ? { userId: session.userId } : {},
|
|
271
370
|
createdAt: session.createdAt,
|
|
272
371
|
lastMessageAt: lastMsg ? new Date(lastMsg.timestamp).toISOString() : void 0,
|
|
273
372
|
preview: lastMsg?.content.slice(0, 100),
|
|
@@ -537,7 +636,6 @@ function resolveOpenClawSdk(log) {
|
|
|
537
636
|
let pluginRuntime = null;
|
|
538
637
|
let chatClient = null;
|
|
539
638
|
let connectingPromise = null;
|
|
540
|
-
let metricsClient = null;
|
|
541
639
|
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
542
640
|
const DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
543
641
|
const MAX_REDIRECTS = 5;
|
|
@@ -629,13 +727,6 @@ async function handleAgentRequest(request, log) {
|
|
|
629
727
|
const sessionId = conversationId ?? legacySessionKey;
|
|
630
728
|
if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
|
|
631
729
|
await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId);
|
|
632
|
-
if (metricsClient && userId) metricsClient.recordActivity({
|
|
633
|
-
userId,
|
|
634
|
-
channel: "alfe",
|
|
635
|
-
role: "user"
|
|
636
|
-
}).catch((err) => {
|
|
637
|
-
log.warn(`Metrics report failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
638
|
-
});
|
|
639
730
|
let resolvedOpenClawKey = null;
|
|
640
731
|
const unsubscribe = runtime.events.onAgentEvent((evt) => {
|
|
641
732
|
if (!evt.sessionKey) return;
|
|
@@ -717,13 +808,6 @@ async function handleAgentRequest(request, log) {
|
|
|
717
808
|
sessionKey: resolvedOpenClawKey ?? legacySessionKey,
|
|
718
809
|
...mediaUrls.length ? { mediaUrls } : {}
|
|
719
810
|
});
|
|
720
|
-
if (metricsClient && userId) metricsClient.recordActivity({
|
|
721
|
-
userId,
|
|
722
|
-
channel: "alfe",
|
|
723
|
-
role: "assistant"
|
|
724
|
-
}).catch((err) => {
|
|
725
|
-
log.warn(`Metrics report failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
726
|
-
});
|
|
727
811
|
},
|
|
728
812
|
onRecordError: (err) => {
|
|
729
813
|
log.error(`Session record error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -807,7 +891,13 @@ const plugin = {
|
|
|
807
891
|
activate(api) {
|
|
808
892
|
const log = api.logger;
|
|
809
893
|
const alreadyActivated = globalThis.__alfeChatPluginActivated === true;
|
|
810
|
-
const alfeChannel = createAlfeChannelPlugin(
|
|
894
|
+
const alfeChannel = createAlfeChannelPlugin({
|
|
895
|
+
getChatClient: () => chatClient,
|
|
896
|
+
listSessions,
|
|
897
|
+
getSession,
|
|
898
|
+
createSession,
|
|
899
|
+
addMessage
|
|
900
|
+
});
|
|
811
901
|
api.registerChannel(alfeChannel);
|
|
812
902
|
log.info(`Registered channel: ${alfeChannel.id}`);
|
|
813
903
|
const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
|
|
@@ -820,15 +910,6 @@ const plugin = {
|
|
|
820
910
|
log.info("Chat plugin registering...");
|
|
821
911
|
resolveOpenClawSdk(log);
|
|
822
912
|
pluginRuntime = api.runtime ?? null;
|
|
823
|
-
try {
|
|
824
|
-
const cfg = (0, _alfe_ai_config.resolveConfig)();
|
|
825
|
-
metricsClient = new _alfe_ai_agent_api_client.AgentApiClient({
|
|
826
|
-
apiKey: cfg.apiKey,
|
|
827
|
-
apiUrl: cfg.apiUrl
|
|
828
|
-
});
|
|
829
|
-
} catch {
|
|
830
|
-
log.debug("Metrics client not initialized — activity tracking disabled");
|
|
831
|
-
}
|
|
832
913
|
connectingPromise = Promise.resolve().then(() => {
|
|
833
914
|
try {
|
|
834
915
|
const { apiKey, chatWsUrl } = (0, _alfe_ai_chat.resolveAlfeChat)({
|
|
@@ -908,7 +989,6 @@ const plugin = {
|
|
|
908
989
|
}
|
|
909
990
|
pluginRuntime = null;
|
|
910
991
|
dispatchInbound = null;
|
|
911
|
-
metricsClient = null;
|
|
912
992
|
log.info("Chat plugin deactivated");
|
|
913
993
|
};
|
|
914
994
|
if (typeof api.registerGatewayMethod === "function") {
|
|
@@ -975,7 +1055,6 @@ const plugin = {
|
|
|
975
1055
|
}
|
|
976
1056
|
pluginRuntime = null;
|
|
977
1057
|
dispatchInbound = null;
|
|
978
|
-
metricsClient = null;
|
|
979
1058
|
log.info("Chat plugin deactivated");
|
|
980
1059
|
}
|
|
981
1060
|
};
|
package/dist/plugin2.js
CHANGED
|
@@ -3,12 +3,56 @@ import { mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promi
|
|
|
3
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
|
-
import {
|
|
7
|
-
import { AgentApiClient } from "@alfe.ai/agent-api-client";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
8
7
|
import { existsSync } from "node:fs";
|
|
9
8
|
//#region src/alfe-channel.ts
|
|
9
|
+
/**
|
|
10
|
+
* Alfe channel plugin definition — registers 'alfe' as an OpenClaw channel.
|
|
11
|
+
*
|
|
12
|
+
* Web and mobile clients share the same channel and conversation sessions.
|
|
13
|
+
* Outbound delivery uses `deliveryMode: 'direct'` — the plugin's sendText /
|
|
14
|
+
* sendMedia handlers push messages through the chat relay service.
|
|
15
|
+
*
|
|
16
|
+
* Target formats accepted by `resolveTarget`:
|
|
17
|
+
* user:{clerkUserId} — most recent conversation for that user (created on demand)
|
|
18
|
+
* conv:{conversationId} — a specific existing conversation
|
|
19
|
+
*
|
|
20
|
+
* Config section: channels.alfe in openclaw.yaml
|
|
21
|
+
*/
|
|
10
22
|
const CHANNEL_ID = "alfe";
|
|
11
23
|
const DEFAULT_ACCOUNT_ID = "default";
|
|
24
|
+
async function sendViaChat(deps, ctx, mediaUrl) {
|
|
25
|
+
const client = deps.getChatClient();
|
|
26
|
+
if (!client) throw new Error("Chat service not connected — cannot deliver");
|
|
27
|
+
let conversationId;
|
|
28
|
+
if (ctx.to.startsWith("conv:")) {
|
|
29
|
+
conversationId = ctx.to.slice(5);
|
|
30
|
+
if (!await deps.getSession(conversationId)) throw new Error(`Conversation not found: ${conversationId}`);
|
|
31
|
+
} else {
|
|
32
|
+
const userId = ctx.to.startsWith("user:") ? ctx.to.slice(5) : ctx.to;
|
|
33
|
+
if (!userId) throw new Error("Empty userId in target");
|
|
34
|
+
const sessions = await deps.listSessions({ userId });
|
|
35
|
+
if (sessions.length > 0) conversationId = sessions[0].sessionId;
|
|
36
|
+
else {
|
|
37
|
+
conversationId = `alfe:chat:${userId}:${randomUUID()}`;
|
|
38
|
+
await deps.createSession(conversationId, "", "alfe", void 0, userId);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
await deps.addMessage(conversationId, "assistant", ctx.text);
|
|
42
|
+
const messageId = randomUUID();
|
|
43
|
+
client.notify("agent-message", {
|
|
44
|
+
conversationId,
|
|
45
|
+
text: ctx.text,
|
|
46
|
+
sessionKey: conversationId,
|
|
47
|
+
...mediaUrl ? { mediaUrls: [mediaUrl] } : {}
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
channel: "alfe",
|
|
51
|
+
messageId,
|
|
52
|
+
conversationId,
|
|
53
|
+
timestamp: Date.now()
|
|
54
|
+
};
|
|
55
|
+
}
|
|
12
56
|
function getChannelSection(cfg) {
|
|
13
57
|
return cfg.channels?.alfe ?? {};
|
|
14
58
|
}
|
|
@@ -17,8 +61,12 @@ function getChannelSection(cfg) {
|
|
|
17
61
|
*
|
|
18
62
|
* This follows the same pattern as built-in channels (Telegram, Discord, etc.)
|
|
19
63
|
* but is registered dynamically via api.registerChannel().
|
|
64
|
+
*
|
|
65
|
+
* `deps` enables outbound delivery and peer discovery. When omitted (e.g.
|
|
66
|
+
* during CLI metadata registration), outbound calls throw and the directory
|
|
67
|
+
* is empty — but the channel is still registered for inbound use.
|
|
20
68
|
*/
|
|
21
|
-
function createAlfeChannelPlugin() {
|
|
69
|
+
function createAlfeChannelPlugin(deps) {
|
|
22
70
|
return {
|
|
23
71
|
id: CHANNEL_ID,
|
|
24
72
|
meta: {
|
|
@@ -97,9 +145,58 @@ function createAlfeChannelPlugin() {
|
|
|
97
145
|
}
|
|
98
146
|
},
|
|
99
147
|
outbound: {
|
|
100
|
-
deliveryMode: "
|
|
101
|
-
textChunkLimit: 4e3
|
|
148
|
+
deliveryMode: "direct",
|
|
149
|
+
textChunkLimit: 4e3,
|
|
150
|
+
resolveTarget(params) {
|
|
151
|
+
const to = params.to;
|
|
152
|
+
if (!to) return {
|
|
153
|
+
ok: false,
|
|
154
|
+
error: /* @__PURE__ */ new Error("Missing target — use user:{userId} or conv:{conversationId}")
|
|
155
|
+
};
|
|
156
|
+
if (to.startsWith("conv:")) {
|
|
157
|
+
if (!to.slice(5)) return {
|
|
158
|
+
ok: false,
|
|
159
|
+
error: /* @__PURE__ */ new Error("Empty conversation ID")
|
|
160
|
+
};
|
|
161
|
+
return {
|
|
162
|
+
ok: true,
|
|
163
|
+
to
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const userId = to.startsWith("user:") ? to.slice(5) : to;
|
|
167
|
+
if (!userId || userId === "anon") return {
|
|
168
|
+
ok: false,
|
|
169
|
+
error: /* @__PURE__ */ new Error("Invalid target: userId is required")
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
ok: true,
|
|
173
|
+
to: `user:${userId}`
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
async sendText(ctx) {
|
|
177
|
+
if (!deps) throw new Error("Alfe channel deps not configured — outbound disabled");
|
|
178
|
+
return await sendViaChat(deps, ctx, void 0);
|
|
179
|
+
},
|
|
180
|
+
async sendMedia(ctx) {
|
|
181
|
+
if (!deps) throw new Error("Alfe channel deps not configured — outbound disabled");
|
|
182
|
+
return await sendViaChat(deps, ctx, ctx.mediaUrl);
|
|
183
|
+
}
|
|
102
184
|
},
|
|
185
|
+
directory: { async listPeers() {
|
|
186
|
+
if (!deps) return [];
|
|
187
|
+
const sessions = await deps.listSessions();
|
|
188
|
+
const seen = /* @__PURE__ */ new Set();
|
|
189
|
+
const peers = [];
|
|
190
|
+
for (const s of sessions) {
|
|
191
|
+
if (!s.userId || seen.has(s.userId)) continue;
|
|
192
|
+
seen.add(s.userId);
|
|
193
|
+
peers.push({
|
|
194
|
+
kind: "user",
|
|
195
|
+
id: s.userId
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return peers;
|
|
199
|
+
} },
|
|
103
200
|
setup: {
|
|
104
201
|
resolveAccountId(params) {
|
|
105
202
|
return params.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
@@ -268,6 +365,8 @@ async function listSessions(filters, limit = 50) {
|
|
|
268
365
|
sessionId: session.sessionId,
|
|
269
366
|
agentId: session.agentId,
|
|
270
367
|
channel: session.channel,
|
|
368
|
+
...session.tenantId ? { tenantId: session.tenantId } : {},
|
|
369
|
+
...session.userId ? { userId: session.userId } : {},
|
|
271
370
|
createdAt: session.createdAt,
|
|
272
371
|
lastMessageAt: lastMsg ? new Date(lastMsg.timestamp).toISOString() : void 0,
|
|
273
372
|
preview: lastMsg?.content.slice(0, 100),
|
|
@@ -537,7 +636,6 @@ function resolveOpenClawSdk(log) {
|
|
|
537
636
|
let pluginRuntime = null;
|
|
538
637
|
let chatClient = null;
|
|
539
638
|
let connectingPromise = null;
|
|
540
|
-
let metricsClient = null;
|
|
541
639
|
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
542
640
|
const DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
543
641
|
const MAX_REDIRECTS = 5;
|
|
@@ -629,13 +727,6 @@ async function handleAgentRequest(request, log) {
|
|
|
629
727
|
const sessionId = conversationId ?? legacySessionKey;
|
|
630
728
|
if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
|
|
631
729
|
await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId);
|
|
632
|
-
if (metricsClient && userId) metricsClient.recordActivity({
|
|
633
|
-
userId,
|
|
634
|
-
channel: "alfe",
|
|
635
|
-
role: "user"
|
|
636
|
-
}).catch((err) => {
|
|
637
|
-
log.warn(`Metrics report failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
638
|
-
});
|
|
639
730
|
let resolvedOpenClawKey = null;
|
|
640
731
|
const unsubscribe = runtime.events.onAgentEvent((evt) => {
|
|
641
732
|
if (!evt.sessionKey) return;
|
|
@@ -717,13 +808,6 @@ async function handleAgentRequest(request, log) {
|
|
|
717
808
|
sessionKey: resolvedOpenClawKey ?? legacySessionKey,
|
|
718
809
|
...mediaUrls.length ? { mediaUrls } : {}
|
|
719
810
|
});
|
|
720
|
-
if (metricsClient && userId) metricsClient.recordActivity({
|
|
721
|
-
userId,
|
|
722
|
-
channel: "alfe",
|
|
723
|
-
role: "assistant"
|
|
724
|
-
}).catch((err) => {
|
|
725
|
-
log.warn(`Metrics report failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
726
|
-
});
|
|
727
811
|
},
|
|
728
812
|
onRecordError: (err) => {
|
|
729
813
|
log.error(`Session record error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -807,7 +891,13 @@ const plugin = {
|
|
|
807
891
|
activate(api) {
|
|
808
892
|
const log = api.logger;
|
|
809
893
|
const alreadyActivated = globalThis.__alfeChatPluginActivated === true;
|
|
810
|
-
const alfeChannel = createAlfeChannelPlugin(
|
|
894
|
+
const alfeChannel = createAlfeChannelPlugin({
|
|
895
|
+
getChatClient: () => chatClient,
|
|
896
|
+
listSessions,
|
|
897
|
+
getSession,
|
|
898
|
+
createSession,
|
|
899
|
+
addMessage
|
|
900
|
+
});
|
|
811
901
|
api.registerChannel(alfeChannel);
|
|
812
902
|
log.info(`Registered channel: ${alfeChannel.id}`);
|
|
813
903
|
const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
|
|
@@ -820,15 +910,6 @@ const plugin = {
|
|
|
820
910
|
log.info("Chat plugin registering...");
|
|
821
911
|
resolveOpenClawSdk(log);
|
|
822
912
|
pluginRuntime = api.runtime ?? null;
|
|
823
|
-
try {
|
|
824
|
-
const cfg = resolveConfig();
|
|
825
|
-
metricsClient = new AgentApiClient({
|
|
826
|
-
apiKey: cfg.apiKey,
|
|
827
|
-
apiUrl: cfg.apiUrl
|
|
828
|
-
});
|
|
829
|
-
} catch {
|
|
830
|
-
log.debug("Metrics client not initialized — activity tracking disabled");
|
|
831
|
-
}
|
|
832
913
|
connectingPromise = Promise.resolve().then(() => {
|
|
833
914
|
try {
|
|
834
915
|
const { apiKey, chatWsUrl } = resolveAlfeChat({
|
|
@@ -908,7 +989,6 @@ const plugin = {
|
|
|
908
989
|
}
|
|
909
990
|
pluginRuntime = null;
|
|
910
991
|
dispatchInbound = null;
|
|
911
|
-
metricsClient = null;
|
|
912
992
|
log.info("Chat plugin deactivated");
|
|
913
993
|
};
|
|
914
994
|
if (typeof api.registerGatewayMethod === "function") {
|
|
@@ -975,7 +1055,6 @@ const plugin = {
|
|
|
975
1055
|
}
|
|
976
1056
|
pluginRuntime = null;
|
|
977
1057
|
dispatchInbound = null;
|
|
978
|
-
metricsClient = null;
|
|
979
1058
|
log.info("Chat plugin deactivated");
|
|
980
1059
|
}
|
|
981
1060
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/openclaw-chat",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
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,7 @@
|
|
|
27
27
|
"openclaw.plugin.json"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@alfe.ai/agent-api-client": "^0.1.
|
|
30
|
+
"@alfe.ai/agent-api-client": "^0.1.1",
|
|
31
31
|
"@alfe.ai/chat": "^0.0.8",
|
|
32
32
|
"@alfe.ai/config": "^0.0.8"
|
|
33
33
|
},
|