@alfe.ai/openclaw-chat 0.0.14 → 0.0.16
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.cjs +7 -0
- package/dist/index.d.cts +42 -0
- package/dist/plugin.cjs +2 -0
- package/dist/plugin.d.cts +242 -0
- package/dist/plugin2.cjs +686 -0
- package/dist/plugin2.d.cts +2 -0
- package/dist/plugin2.js +112 -17
- package/package.json +8 -6
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
const require_plugin = require("./plugin2.cjs");
|
|
6
|
+
exports.createAlfeChannelPlugin = require_plugin.createAlfeChannelPlugin;
|
|
7
|
+
exports.default = require_plugin.plugin;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { a as AlfeResolvedAccount, i as AlfePluginConfig, n as createAlfeChannelPlugin, r as AlfeChannelConfig, t as plugin } from "./plugin.cjs";
|
|
2
|
+
|
|
3
|
+
//#region src/session-store.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Session Store — persists chat sessions to the local filesystem.
|
|
7
|
+
*
|
|
8
|
+
* Storage layout:
|
|
9
|
+
* ~/.alfe/sessions/chat/{sessionId}.json
|
|
10
|
+
*
|
|
11
|
+
* Each session file contains metadata and the full message history.
|
|
12
|
+
* Sessions are written on every message to ensure durability.
|
|
13
|
+
* Old sessions are cleaned up automatically (30-day TTL, 1000 max).
|
|
14
|
+
*/
|
|
15
|
+
interface ChatMessage {
|
|
16
|
+
role: 'user' | 'assistant';
|
|
17
|
+
content: string;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
senderId?: string;
|
|
20
|
+
senderName?: string;
|
|
21
|
+
}
|
|
22
|
+
interface SessionData {
|
|
23
|
+
sessionId: string;
|
|
24
|
+
agentId: string;
|
|
25
|
+
channel: string;
|
|
26
|
+
tenantId?: string;
|
|
27
|
+
userId?: string;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
updatedAt: string;
|
|
30
|
+
messages: ChatMessage[];
|
|
31
|
+
}
|
|
32
|
+
interface SessionSummary {
|
|
33
|
+
sessionId: string;
|
|
34
|
+
agentId: string;
|
|
35
|
+
channel: string;
|
|
36
|
+
createdAt: string;
|
|
37
|
+
lastMessageAt?: string;
|
|
38
|
+
preview?: string;
|
|
39
|
+
messageCount: number;
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { type AlfeChannelConfig, type AlfePluginConfig, type AlfeResolvedAccount, type ChatMessage, type SessionData, type SessionSummary, createAlfeChannelPlugin, plugin as default };
|
package/dist/plugin.cjs
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Types for the Alfe chat channel plugin.
|
|
4
|
+
*
|
|
5
|
+
* SDK types (PluginRuntime, OpenClawPluginApi, OpenClawConfig, etc.)
|
|
6
|
+
* are imported from openclaw/plugin-sdk at usage sites.
|
|
7
|
+
* This file only contains Alfe-specific domain types.
|
|
8
|
+
*/
|
|
9
|
+
interface AlfeChannelAccountConfig {
|
|
10
|
+
/** Whether this account is enabled. */
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
/** Allowed sender identifiers (user IDs, email addresses). */
|
|
13
|
+
allowFrom?: string | string[];
|
|
14
|
+
/** Default delivery target. */
|
|
15
|
+
defaultTo?: string;
|
|
16
|
+
/** DM policy (open, allowlist, etc.). */
|
|
17
|
+
dmPolicy?: string;
|
|
18
|
+
}
|
|
19
|
+
interface AlfeChannelConfig {
|
|
20
|
+
/** Whether the Alfe channel is enabled. */
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
/** Allowed sender identifiers. */
|
|
23
|
+
allowFrom?: string | string[];
|
|
24
|
+
/** Default delivery target for outbound messages. */
|
|
25
|
+
defaultTo?: string;
|
|
26
|
+
/** DM policy. */
|
|
27
|
+
dmPolicy?: string;
|
|
28
|
+
/** Named accounts (multi-account support). */
|
|
29
|
+
accounts?: Record<string, AlfeChannelAccountConfig>;
|
|
30
|
+
}
|
|
31
|
+
interface AlfeResolvedAccount {
|
|
32
|
+
accountId: string;
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
allowFrom: string[];
|
|
35
|
+
defaultTo?: string;
|
|
36
|
+
dmPolicy?: string;
|
|
37
|
+
}
|
|
38
|
+
interface AlfePluginConfig {
|
|
39
|
+
/** Agent ID this plugin is associated with. */
|
|
40
|
+
agentId?: string;
|
|
41
|
+
/** Chat service WebSocket URL (e.g. wss://chat.dev.alfe.ai/ws) */
|
|
42
|
+
chatWsUrl?: string;
|
|
43
|
+
/** API key for chat service auth */
|
|
44
|
+
apiKey?: string;
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/alfe-channel.d.ts
|
|
48
|
+
/** OpenClaw config shape — inline to avoid runtime dependency on openclaw package */
|
|
49
|
+
interface OpenClawConfig {
|
|
50
|
+
channels?: {
|
|
51
|
+
alfe?: AlfeChannelConfig;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
};
|
|
54
|
+
[key: string]: unknown;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Creates the Alfe ChannelPlugin object for registration with OpenClaw.
|
|
58
|
+
*
|
|
59
|
+
* This follows the same pattern as built-in channels (Telegram, Discord, etc.)
|
|
60
|
+
* but is registered dynamically via api.registerChannel().
|
|
61
|
+
*/
|
|
62
|
+
declare function createAlfeChannelPlugin(): {
|
|
63
|
+
id: string;
|
|
64
|
+
meta: {
|
|
65
|
+
id: string;
|
|
66
|
+
label: string;
|
|
67
|
+
selectionLabel: string;
|
|
68
|
+
detailLabel: string;
|
|
69
|
+
docsPath: string;
|
|
70
|
+
docsLabel: string;
|
|
71
|
+
blurb: string;
|
|
72
|
+
systemImage: string;
|
|
73
|
+
order: number;
|
|
74
|
+
aliases: string[];
|
|
75
|
+
forceAccountBinding: boolean;
|
|
76
|
+
showConfigured: boolean;
|
|
77
|
+
};
|
|
78
|
+
capabilities: {
|
|
79
|
+
chatTypes: ("direct" | "group")[];
|
|
80
|
+
reactions: boolean;
|
|
81
|
+
edit: boolean;
|
|
82
|
+
unsend: boolean;
|
|
83
|
+
reply: boolean;
|
|
84
|
+
effects: boolean;
|
|
85
|
+
groupManagement: boolean;
|
|
86
|
+
threads: boolean;
|
|
87
|
+
media: boolean;
|
|
88
|
+
nativeCommands: boolean;
|
|
89
|
+
polls: boolean;
|
|
90
|
+
};
|
|
91
|
+
config: {
|
|
92
|
+
/**
|
|
93
|
+
* List configured account IDs.
|
|
94
|
+
* Supports multi-account via channels.alfe.accounts, with a
|
|
95
|
+
* default account derived from the top-level channels.alfe section.
|
|
96
|
+
*/
|
|
97
|
+
listAccountIds(cfg: OpenClawConfig): string[];
|
|
98
|
+
/**
|
|
99
|
+
* Resolve account config for a given account ID.
|
|
100
|
+
*/
|
|
101
|
+
resolveAccount(cfg: OpenClawConfig, accountId?: string | null): AlfeResolvedAccount;
|
|
102
|
+
/**
|
|
103
|
+
* Default account ID.
|
|
104
|
+
*/
|
|
105
|
+
defaultAccountId(): string;
|
|
106
|
+
/**
|
|
107
|
+
* Check if account is enabled.
|
|
108
|
+
*/
|
|
109
|
+
isEnabled(account: AlfeResolvedAccount): boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Check if account is configured (always true for Alfe — no external tokens needed).
|
|
112
|
+
*/
|
|
113
|
+
isConfigured(): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Describe the account state for status display.
|
|
116
|
+
*/
|
|
117
|
+
describeAccount(account: AlfeResolvedAccount): {
|
|
118
|
+
accountId: string;
|
|
119
|
+
enabled: boolean;
|
|
120
|
+
configured: boolean;
|
|
121
|
+
dmPolicy: string | undefined;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Resolve allow-from list for an account.
|
|
125
|
+
*/
|
|
126
|
+
resolveAllowFrom(params: {
|
|
127
|
+
cfg: OpenClawConfig;
|
|
128
|
+
accountId?: string | null;
|
|
129
|
+
}): string[];
|
|
130
|
+
/**
|
|
131
|
+
* Resolve default outbound target.
|
|
132
|
+
*/
|
|
133
|
+
resolveDefaultTo(params: {
|
|
134
|
+
cfg: OpenClawConfig;
|
|
135
|
+
accountId?: string | null;
|
|
136
|
+
}): string | undefined;
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Outbound delivery via gateway.
|
|
140
|
+
* The chat relay service on Fly.io handles actual delivery
|
|
141
|
+
* to connected web/mobile clients via the gateway.
|
|
142
|
+
*/
|
|
143
|
+
outbound: {
|
|
144
|
+
deliveryMode: "gateway";
|
|
145
|
+
textChunkLimit: number;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Setup adapter — minimal for Alfe since no external tokens are needed.
|
|
149
|
+
*/
|
|
150
|
+
setup: {
|
|
151
|
+
resolveAccountId(params: {
|
|
152
|
+
cfg: OpenClawConfig;
|
|
153
|
+
accountId?: string;
|
|
154
|
+
input?: Record<string, unknown>;
|
|
155
|
+
}): string;
|
|
156
|
+
applyAccountConfig(params: {
|
|
157
|
+
cfg: OpenClawConfig;
|
|
158
|
+
accountId: string;
|
|
159
|
+
input: Record<string, unknown>;
|
|
160
|
+
}): OpenClawConfig;
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/plugin.d.ts
|
|
165
|
+
interface PluginLogger {
|
|
166
|
+
info(msg: string, ...args: unknown[]): void;
|
|
167
|
+
warn(msg: string, ...args: unknown[]): void;
|
|
168
|
+
error(msg: string, ...args: unknown[]): void;
|
|
169
|
+
debug(msg: string, ...args: unknown[]): void;
|
|
170
|
+
}
|
|
171
|
+
interface PluginRuntime {
|
|
172
|
+
config: {
|
|
173
|
+
loadConfig(): Record<string, unknown>;
|
|
174
|
+
};
|
|
175
|
+
events: {
|
|
176
|
+
onAgentEvent(listener: (evt: AgentEventPayload) => void): () => void;
|
|
177
|
+
};
|
|
178
|
+
subagent: {
|
|
179
|
+
run(params: {
|
|
180
|
+
sessionKey: string;
|
|
181
|
+
message: string;
|
|
182
|
+
idempotencyKey?: string;
|
|
183
|
+
deliver?: boolean;
|
|
184
|
+
}): Promise<{
|
|
185
|
+
runId: string;
|
|
186
|
+
}>;
|
|
187
|
+
waitForRun(params: {
|
|
188
|
+
runId: string;
|
|
189
|
+
timeoutMs?: number;
|
|
190
|
+
}): Promise<{
|
|
191
|
+
status: string;
|
|
192
|
+
error?: string;
|
|
193
|
+
}>;
|
|
194
|
+
};
|
|
195
|
+
channel: {
|
|
196
|
+
routing: {
|
|
197
|
+
resolveAgentRoute(input: Record<string, unknown>): Record<string, unknown>;
|
|
198
|
+
buildAgentSessionKey(params: Record<string, unknown>): string;
|
|
199
|
+
};
|
|
200
|
+
session: {
|
|
201
|
+
resolveStorePath(params: Record<string, unknown>): string;
|
|
202
|
+
readSessionUpdatedAt(params: Record<string, unknown>): number | undefined;
|
|
203
|
+
recordInboundSession(params: Record<string, unknown>): Promise<void>;
|
|
204
|
+
};
|
|
205
|
+
reply: {
|
|
206
|
+
resolveEnvelopeFormatOptions(cfg: Record<string, unknown>): Record<string, unknown>;
|
|
207
|
+
formatAgentEnvelope(params: Record<string, unknown>): string;
|
|
208
|
+
finalizeInboundContext(params: Record<string, unknown>): Record<string, unknown>;
|
|
209
|
+
dispatchReplyWithBufferedBlockDispatcher(params: Record<string, unknown>): Promise<unknown>;
|
|
210
|
+
dispatchReplyFromConfig(params: Record<string, unknown>): Promise<unknown>;
|
|
211
|
+
};
|
|
212
|
+
[key: string]: unknown;
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
interface AgentEventPayload {
|
|
216
|
+
runId: string;
|
|
217
|
+
seq: number;
|
|
218
|
+
stream: string;
|
|
219
|
+
ts: number;
|
|
220
|
+
data: Record<string, unknown>;
|
|
221
|
+
sessionKey?: string;
|
|
222
|
+
}
|
|
223
|
+
interface PluginApi {
|
|
224
|
+
logger: PluginLogger;
|
|
225
|
+
config?: Record<string, unknown>;
|
|
226
|
+
runtime?: PluginRuntime;
|
|
227
|
+
registerChannel(channel: ReturnType<typeof createAlfeChannelPlugin>): void;
|
|
228
|
+
registerGatewayMethod?(name: string, handler: (...args: unknown[]) => Promise<unknown>): void;
|
|
229
|
+
on(event: string, handler: (...args: unknown[]) => void | Promise<void>, options?: {
|
|
230
|
+
priority?: number;
|
|
231
|
+
}): void;
|
|
232
|
+
}
|
|
233
|
+
declare const plugin: {
|
|
234
|
+
id: string;
|
|
235
|
+
name: string;
|
|
236
|
+
description: string;
|
|
237
|
+
version: string;
|
|
238
|
+
activate(api: PluginApi): void;
|
|
239
|
+
deactivate(api: PluginApi): Promise<void>;
|
|
240
|
+
};
|
|
241
|
+
//#endregion
|
|
242
|
+
export { AlfeResolvedAccount as a, AlfePluginConfig as i, createAlfeChannelPlugin as n, AlfeChannelConfig as r, plugin as t };
|
package/dist/plugin2.cjs
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
let node_module = require("node:module");
|
|
2
|
+
let _alfe_ai_chat = require("@alfe.ai/chat");
|
|
3
|
+
let node_fs_promises = require("node:fs/promises");
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
let node_os = require("node:os");
|
|
6
|
+
let node_fs = require("node:fs");
|
|
7
|
+
//#region src/alfe-channel.ts
|
|
8
|
+
const CHANNEL_ID = "alfe";
|
|
9
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
10
|
+
function getChannelSection(cfg) {
|
|
11
|
+
return cfg.channels?.alfe ?? {};
|
|
12
|
+
}
|
|
13
|
+
function normalizeAllowFrom(raw) {
|
|
14
|
+
if (!raw) return [];
|
|
15
|
+
if (typeof raw === "string") return [raw];
|
|
16
|
+
return raw;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Creates the Alfe ChannelPlugin object for registration with OpenClaw.
|
|
20
|
+
*
|
|
21
|
+
* This follows the same pattern as built-in channels (Telegram, Discord, etc.)
|
|
22
|
+
* but is registered dynamically via api.registerChannel().
|
|
23
|
+
*/
|
|
24
|
+
function createAlfeChannelPlugin() {
|
|
25
|
+
return {
|
|
26
|
+
id: CHANNEL_ID,
|
|
27
|
+
meta: {
|
|
28
|
+
id: CHANNEL_ID,
|
|
29
|
+
label: "Alfe",
|
|
30
|
+
selectionLabel: "Alfe (Web & Mobile)",
|
|
31
|
+
detailLabel: "Alfe",
|
|
32
|
+
docsPath: "/channels/alfe",
|
|
33
|
+
docsLabel: "alfe",
|
|
34
|
+
blurb: "Alfe native chat — web widget and mobile app conversations.",
|
|
35
|
+
systemImage: "bubble.left.and.text.bubble.right",
|
|
36
|
+
order: 100,
|
|
37
|
+
aliases: ["alfe-web", "alfe-mobile"],
|
|
38
|
+
forceAccountBinding: false,
|
|
39
|
+
showConfigured: true
|
|
40
|
+
},
|
|
41
|
+
capabilities: {
|
|
42
|
+
chatTypes: ["direct", "group"],
|
|
43
|
+
reactions: false,
|
|
44
|
+
edit: false,
|
|
45
|
+
unsend: false,
|
|
46
|
+
reply: true,
|
|
47
|
+
effects: false,
|
|
48
|
+
groupManagement: false,
|
|
49
|
+
threads: false,
|
|
50
|
+
media: true,
|
|
51
|
+
nativeCommands: false,
|
|
52
|
+
polls: false
|
|
53
|
+
},
|
|
54
|
+
config: {
|
|
55
|
+
listAccountIds(cfg) {
|
|
56
|
+
const section = getChannelSection(cfg);
|
|
57
|
+
const ids = [];
|
|
58
|
+
if (section.enabled !== false && (section.allowFrom ?? section.defaultTo ?? section.dmPolicy)) ids.push(DEFAULT_ACCOUNT_ID);
|
|
59
|
+
if (section.accounts) {
|
|
60
|
+
for (const id of Object.keys(section.accounts)) if (!ids.includes(id)) ids.push(id);
|
|
61
|
+
}
|
|
62
|
+
if (ids.length === 0 && section.enabled !== false) ids.push(DEFAULT_ACCOUNT_ID);
|
|
63
|
+
return ids;
|
|
64
|
+
},
|
|
65
|
+
resolveAccount(cfg, accountId) {
|
|
66
|
+
const section = getChannelSection(cfg);
|
|
67
|
+
const id = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
68
|
+
const accountSection = section.accounts?.[id];
|
|
69
|
+
if (accountSection) return {
|
|
70
|
+
accountId: id,
|
|
71
|
+
enabled: accountSection.enabled !== false,
|
|
72
|
+
allowFrom: normalizeAllowFrom(accountSection.allowFrom),
|
|
73
|
+
defaultTo: accountSection.defaultTo,
|
|
74
|
+
dmPolicy: accountSection.dmPolicy
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
accountId: id,
|
|
78
|
+
enabled: section.enabled !== false,
|
|
79
|
+
allowFrom: normalizeAllowFrom(section.allowFrom),
|
|
80
|
+
defaultTo: section.defaultTo,
|
|
81
|
+
dmPolicy: section.dmPolicy
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
defaultAccountId() {
|
|
85
|
+
return DEFAULT_ACCOUNT_ID;
|
|
86
|
+
},
|
|
87
|
+
isEnabled(account) {
|
|
88
|
+
return account.enabled;
|
|
89
|
+
},
|
|
90
|
+
isConfigured() {
|
|
91
|
+
return true;
|
|
92
|
+
},
|
|
93
|
+
describeAccount(account) {
|
|
94
|
+
return {
|
|
95
|
+
accountId: account.accountId,
|
|
96
|
+
enabled: account.enabled,
|
|
97
|
+
configured: true,
|
|
98
|
+
dmPolicy: account.dmPolicy
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
resolveAllowFrom(params) {
|
|
102
|
+
const section = getChannelSection(params.cfg);
|
|
103
|
+
const id = params.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
104
|
+
return normalizeAllowFrom((section.accounts?.[id])?.allowFrom ?? section.allowFrom);
|
|
105
|
+
},
|
|
106
|
+
resolveDefaultTo(params) {
|
|
107
|
+
const section = getChannelSection(params.cfg);
|
|
108
|
+
const id = params.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
109
|
+
return section.accounts?.[id]?.defaultTo ?? section.defaultTo;
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
outbound: {
|
|
113
|
+
deliveryMode: "gateway",
|
|
114
|
+
textChunkLimit: 4e3
|
|
115
|
+
},
|
|
116
|
+
setup: {
|
|
117
|
+
resolveAccountId(params) {
|
|
118
|
+
return params.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
119
|
+
},
|
|
120
|
+
applyAccountConfig(params) {
|
|
121
|
+
const cfg = { ...params.cfg };
|
|
122
|
+
cfg.channels ??= {};
|
|
123
|
+
cfg.channels.alfe ??= {};
|
|
124
|
+
const section = cfg.channels.alfe;
|
|
125
|
+
if (params.accountId === DEFAULT_ACCOUNT_ID) section.enabled = true;
|
|
126
|
+
else {
|
|
127
|
+
section.accounts ??= {};
|
|
128
|
+
section.accounts[params.accountId] = {
|
|
129
|
+
enabled: true,
|
|
130
|
+
...params.input
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return cfg;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/session-keys.ts
|
|
140
|
+
/**
|
|
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
|
+
* OpenClaw canonical format (from resolveAgentRoute):
|
|
148
|
+
* agent:{ocAgentId}:alfe:[default:]direct:{senderId}[:thread:{conversationId}]
|
|
149
|
+
*
|
|
150
|
+
* Legacy formats (deprecated):
|
|
151
|
+
* sms-{agentId}-{phone}
|
|
152
|
+
* wa-{agentId}-{phone}
|
|
153
|
+
* chat-{tenantId}-{agentId}-{suffix}
|
|
154
|
+
* agent:{agentId}:chat-{tenantId}-{agentId}-{suffix}
|
|
155
|
+
*/
|
|
156
|
+
/** Single-threaded channel modes — identity IS the conversation. */
|
|
157
|
+
const SINGLE_THREADED_MODES = new Set([
|
|
158
|
+
"sms",
|
|
159
|
+
"whatsapp",
|
|
160
|
+
"mobile"
|
|
161
|
+
]);
|
|
162
|
+
/**
|
|
163
|
+
* Check if a session key belongs to the Alfe chat channel.
|
|
164
|
+
* Handles standardized, canonical, and legacy formats.
|
|
165
|
+
*/
|
|
166
|
+
function isAlfeSessionKey(key) {
|
|
167
|
+
if (key.startsWith("alfe:")) return true;
|
|
168
|
+
if (key.includes(":alfe:")) return true;
|
|
169
|
+
if (key.includes("chat-") || key.startsWith("sms-") || key.startsWith("wa-")) return true;
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Parse session key metadata. Returns available fields from any format.
|
|
174
|
+
*
|
|
175
|
+
* For the callback flow, `conversationId` is the critical field — it maps
|
|
176
|
+
* to the channel registry key used by getChannelCallback().
|
|
177
|
+
*/
|
|
178
|
+
function parseAlfeSessionKey(key) {
|
|
179
|
+
const standardMatch = /^alfe:(\w+):(.+)$/.exec(key);
|
|
180
|
+
if (standardMatch) {
|
|
181
|
+
const [, mode, rest] = standardMatch;
|
|
182
|
+
if (SINGLE_THREADED_MODES.has(mode)) return {
|
|
183
|
+
agentId: "",
|
|
184
|
+
userId: rest,
|
|
185
|
+
conversationId: key,
|
|
186
|
+
tenantId: "",
|
|
187
|
+
mode
|
|
188
|
+
};
|
|
189
|
+
const lastColon = rest.lastIndexOf(":");
|
|
190
|
+
if (lastColon > 0) return {
|
|
191
|
+
agentId: "",
|
|
192
|
+
userId: rest.slice(0, lastColon),
|
|
193
|
+
conversationId: key,
|
|
194
|
+
tenantId: "",
|
|
195
|
+
mode
|
|
196
|
+
};
|
|
197
|
+
return {
|
|
198
|
+
agentId: "",
|
|
199
|
+
userId: rest,
|
|
200
|
+
conversationId: key,
|
|
201
|
+
tenantId: "",
|
|
202
|
+
mode
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const canonicalMatch = /^agent:([^:]+):alfe:(?:default:)?direct:(.+?)(?::thread:(.+))?$/.exec(key);
|
|
206
|
+
if (canonicalMatch) {
|
|
207
|
+
const [, matchAgentId, matchUserId, matchConvId] = canonicalMatch;
|
|
208
|
+
return {
|
|
209
|
+
agentId: matchAgentId,
|
|
210
|
+
userId: matchUserId,
|
|
211
|
+
conversationId: matchConvId || "",
|
|
212
|
+
tenantId: "",
|
|
213
|
+
mode: ""
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const rawKey = key.includes(":") ? key.slice(key.lastIndexOf(":") + 1) : key;
|
|
217
|
+
const legacyMatch = /^chat-([^-]+)-([^-]+)/.exec(rawKey);
|
|
218
|
+
return {
|
|
219
|
+
agentId: legacyMatch?.[2] ?? "",
|
|
220
|
+
userId: "",
|
|
221
|
+
conversationId: "",
|
|
222
|
+
tenantId: legacyMatch?.[1] ?? "",
|
|
223
|
+
mode: "chat"
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Extract the channel mode from a standardized session key or conversationId.
|
|
228
|
+
* Returns the mode segment (e.g. 'sms', 'whatsapp', 'chat') or fallback.
|
|
229
|
+
*/
|
|
230
|
+
function extractChannelMode(conversationId, fallback = "chat") {
|
|
231
|
+
return /^alfe:(\w+):/.exec(conversationId)?.[1] ?? fallback;
|
|
232
|
+
}
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/session-store.ts
|
|
235
|
+
/**
|
|
236
|
+
* Session Store — persists chat sessions to the local filesystem.
|
|
237
|
+
*
|
|
238
|
+
* Storage layout:
|
|
239
|
+
* ~/.alfe/sessions/chat/{sessionId}.json
|
|
240
|
+
*
|
|
241
|
+
* Each session file contains metadata and the full message history.
|
|
242
|
+
* Sessions are written on every message to ensure durability.
|
|
243
|
+
* Old sessions are cleaned up automatically (30-day TTL, 1000 max).
|
|
244
|
+
*/
|
|
245
|
+
const SESSIONS_DIR = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "sessions", "chat");
|
|
246
|
+
const MAX_SESSIONS = 1e3;
|
|
247
|
+
const MAX_AGE_MS = 720 * 60 * 60 * 1e3;
|
|
248
|
+
const CLEANUP_INTERVAL_MS = 36e5;
|
|
249
|
+
let lastCleanupAt = 0;
|
|
250
|
+
async function ensureDir() {
|
|
251
|
+
if (!(0, node_fs.existsSync)(SESSIONS_DIR)) await (0, node_fs_promises.mkdir)(SESSIONS_DIR, { recursive: true });
|
|
252
|
+
}
|
|
253
|
+
function sessionPath(sessionId) {
|
|
254
|
+
return (0, node_path.join)(SESSIONS_DIR, `${sessionId.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`);
|
|
255
|
+
}
|
|
256
|
+
async function cleanupOldSessions() {
|
|
257
|
+
if (Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return;
|
|
258
|
+
lastCleanupAt = Date.now();
|
|
259
|
+
try {
|
|
260
|
+
const jsonFiles = (await (0, node_fs_promises.readdir)(SESSIONS_DIR)).filter((f) => f.endsWith(".json"));
|
|
261
|
+
if (jsonFiles.length <= MAX_SESSIONS) {
|
|
262
|
+
const now = Date.now();
|
|
263
|
+
for (const file of jsonFiles) try {
|
|
264
|
+
const filePath = (0, node_path.join)(SESSIONS_DIR, file);
|
|
265
|
+
if (now - (await (0, node_fs_promises.stat)(filePath)).mtimeMs > MAX_AGE_MS) await (0, node_fs_promises.unlink)(filePath);
|
|
266
|
+
} catch {}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const fileStats = [];
|
|
270
|
+
for (const file of jsonFiles) try {
|
|
271
|
+
const filePath = (0, node_path.join)(SESSIONS_DIR, file);
|
|
272
|
+
const fileStat = await (0, node_fs_promises.stat)(filePath);
|
|
273
|
+
fileStats.push({
|
|
274
|
+
path: filePath,
|
|
275
|
+
mtimeMs: fileStat.mtimeMs
|
|
276
|
+
});
|
|
277
|
+
} catch {}
|
|
278
|
+
fileStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
279
|
+
const now = Date.now();
|
|
280
|
+
let remaining = fileStats.length;
|
|
281
|
+
for (const entry of fileStats) {
|
|
282
|
+
if (!(now - entry.mtimeMs > MAX_AGE_MS) && !(remaining > MAX_SESSIONS)) break;
|
|
283
|
+
try {
|
|
284
|
+
await (0, node_fs_promises.unlink)(entry.path);
|
|
285
|
+
remaining--;
|
|
286
|
+
} catch {}
|
|
287
|
+
}
|
|
288
|
+
} catch {}
|
|
289
|
+
}
|
|
290
|
+
async function getSession(sessionId) {
|
|
291
|
+
try {
|
|
292
|
+
const data = await (0, node_fs_promises.readFile)(sessionPath(sessionId), "utf-8");
|
|
293
|
+
return JSON.parse(data);
|
|
294
|
+
} catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async function saveSession(session) {
|
|
299
|
+
await ensureDir();
|
|
300
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
301
|
+
await (0, node_fs_promises.writeFile)(sessionPath(session.sessionId), JSON.stringify(session, null, 2), "utf-8");
|
|
302
|
+
}
|
|
303
|
+
async function createSession(sessionId, agentId, channel, tenantId, userId) {
|
|
304
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
305
|
+
const session = {
|
|
306
|
+
sessionId,
|
|
307
|
+
agentId,
|
|
308
|
+
channel,
|
|
309
|
+
tenantId,
|
|
310
|
+
userId,
|
|
311
|
+
createdAt: now,
|
|
312
|
+
updatedAt: now,
|
|
313
|
+
messages: []
|
|
314
|
+
};
|
|
315
|
+
await saveSession(session);
|
|
316
|
+
cleanupOldSessions();
|
|
317
|
+
return session;
|
|
318
|
+
}
|
|
319
|
+
async function addMessage(sessionId, role, content, senderId, senderName) {
|
|
320
|
+
const session = await getSession(sessionId);
|
|
321
|
+
if (!session) return;
|
|
322
|
+
session.messages.push({
|
|
323
|
+
role,
|
|
324
|
+
content,
|
|
325
|
+
timestamp: Date.now(),
|
|
326
|
+
...senderId ? { senderId } : {},
|
|
327
|
+
...senderName ? { senderName } : {}
|
|
328
|
+
});
|
|
329
|
+
await saveSession(session);
|
|
330
|
+
}
|
|
331
|
+
async function listSessions(filters, limit = 50) {
|
|
332
|
+
await ensureDir();
|
|
333
|
+
let files;
|
|
334
|
+
try {
|
|
335
|
+
files = await (0, node_fs_promises.readdir)(SESSIONS_DIR);
|
|
336
|
+
} catch {
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
340
|
+
const summaries = [];
|
|
341
|
+
for (const file of jsonFiles) try {
|
|
342
|
+
const data = await (0, node_fs_promises.readFile)((0, node_path.join)(SESSIONS_DIR, file), "utf-8");
|
|
343
|
+
const session = JSON.parse(data);
|
|
344
|
+
if (filters?.agentId && session.agentId !== filters.agentId) continue;
|
|
345
|
+
if (filters?.channel && session.channel !== filters.channel) continue;
|
|
346
|
+
if (filters?.tenantId && session.tenantId !== filters.tenantId) continue;
|
|
347
|
+
if (filters?.userId && session.userId !== filters.userId) continue;
|
|
348
|
+
const lastMsg = session.messages.at(-1);
|
|
349
|
+
summaries.push({
|
|
350
|
+
sessionId: session.sessionId,
|
|
351
|
+
agentId: session.agentId,
|
|
352
|
+
channel: session.channel,
|
|
353
|
+
createdAt: session.createdAt,
|
|
354
|
+
lastMessageAt: lastMsg ? new Date(lastMsg.timestamp).toISOString() : void 0,
|
|
355
|
+
preview: lastMsg?.content.slice(0, 100),
|
|
356
|
+
messageCount: session.messages.length
|
|
357
|
+
});
|
|
358
|
+
} catch {}
|
|
359
|
+
summaries.sort((a, b) => {
|
|
360
|
+
const aTime = a.lastMessageAt ?? a.createdAt;
|
|
361
|
+
return (b.lastMessageAt ?? b.createdAt).localeCompare(aTime);
|
|
362
|
+
});
|
|
363
|
+
return summaries.slice(0, limit);
|
|
364
|
+
}
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region src/plugin.ts
|
|
367
|
+
/**
|
|
368
|
+
* @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
|
|
369
|
+
*
|
|
370
|
+
* Registers the 'alfe' channel with OpenClaw. Messages are dispatched
|
|
371
|
+
* through OpenClaw's auto-reply pipeline via dispatchInboundDirectDmWithRuntime(),
|
|
372
|
+
* the same API built-in channels (Slack, Discord, Telegram) use.
|
|
373
|
+
*
|
|
374
|
+
* Architecture:
|
|
375
|
+
* Chat Service (Fly.io) ←WS→ ChatServiceClient
|
|
376
|
+
* → dispatchInboundDirectDmWithRuntime() → Agent (auto-reply pipeline)
|
|
377
|
+
* ← onAgentEvent streaming ← (real-time deltas)
|
|
378
|
+
* ← deliver() callback ← (final response)
|
|
379
|
+
*/
|
|
380
|
+
let dispatchInbound = null;
|
|
381
|
+
/**
|
|
382
|
+
* Resolve OpenClaw SDK functions from the global install.
|
|
383
|
+
* The openclaw package is installed globally (/usr/lib/node_modules/openclaw/)
|
|
384
|
+
* but is NOT in the plugin's node_modules. Built-in extensions can import
|
|
385
|
+
* directly; external plugins must use createRequire.
|
|
386
|
+
*/
|
|
387
|
+
function resolveOpenClawSdk(log) {
|
|
388
|
+
for (const globalPath of ["/usr/lib/node_modules/openclaw/package.json", "/usr/local/lib/node_modules/openclaw/package.json"]) try {
|
|
389
|
+
const channelInbound = (0, node_module.createRequire)(globalPath)("openclaw/plugin-sdk/channel-inbound");
|
|
390
|
+
if (channelInbound.dispatchInboundDirectDmWithRuntime) {
|
|
391
|
+
dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
|
|
392
|
+
log.info(`Resolved OpenClaw SDK from ${globalPath}`);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
} catch {}
|
|
396
|
+
log.warn("OpenClaw SDK not resolvable — chat dispatch will not work");
|
|
397
|
+
}
|
|
398
|
+
let pluginRuntime = null;
|
|
399
|
+
let chatClient = null;
|
|
400
|
+
let connectingPromise = null;
|
|
401
|
+
async function handleAgentRequest(request, log) {
|
|
402
|
+
const runtime = pluginRuntime;
|
|
403
|
+
if (!runtime) {
|
|
404
|
+
chatClient?.sendResponse(request.id, false, { message: "Plugin runtime not initialized" });
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (!dispatchInbound) {
|
|
408
|
+
chatClient?.sendResponse(request.id, false, { message: "OpenClaw SDK not available — cannot dispatch" });
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, displayName } = request.params;
|
|
412
|
+
if (!message) {
|
|
413
|
+
chatClient?.sendResponse(request.id, false, { message: "Missing message" });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const senderId = displayName ?? userId ?? "anon";
|
|
417
|
+
const cfg = runtime.config.loadConfig();
|
|
418
|
+
const sessionId = conversationId ?? legacySessionKey;
|
|
419
|
+
if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
|
|
420
|
+
await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId);
|
|
421
|
+
let resolvedOpenClawKey = null;
|
|
422
|
+
const unsubscribe = runtime.events.onAgentEvent((evt) => {
|
|
423
|
+
if (!evt.sessionKey) return;
|
|
424
|
+
if (evt.stream !== "assistant") return;
|
|
425
|
+
resolvedOpenClawKey ??= evt.sessionKey;
|
|
426
|
+
if (evt.sessionKey !== resolvedOpenClawKey) return;
|
|
427
|
+
chatClient?.sendEvent("chat", {
|
|
428
|
+
runId: evt.runId,
|
|
429
|
+
sessionKey: legacySessionKey,
|
|
430
|
+
seq: evt.seq,
|
|
431
|
+
state: "delta",
|
|
432
|
+
message: evt.data
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
try {
|
|
436
|
+
const channelMode = extractChannelMode(conversationId ?? "", clientType ?? "chat");
|
|
437
|
+
const channelLabel = channelMode === "sms" ? "SMS" : channelMode === "whatsapp" ? "WhatsApp" : "Alfe";
|
|
438
|
+
const shortConvId = conversationId?.slice(-8) ?? "";
|
|
439
|
+
const userLabel = displayName ?? userId ?? senderId;
|
|
440
|
+
const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
|
|
441
|
+
resolvedOpenClawKey = (await dispatchInbound({
|
|
442
|
+
cfg,
|
|
443
|
+
runtime: { channel: runtime.channel },
|
|
444
|
+
channel: "alfe",
|
|
445
|
+
channelLabel,
|
|
446
|
+
accountId: "default",
|
|
447
|
+
peer: conversationType === "group" ? {
|
|
448
|
+
kind: "group",
|
|
449
|
+
id: conversationId ?? senderId
|
|
450
|
+
} : {
|
|
451
|
+
kind: "direct",
|
|
452
|
+
id: conversationId ? `${senderId}:conv:${conversationId}` : senderId
|
|
453
|
+
},
|
|
454
|
+
senderId,
|
|
455
|
+
senderAddress: `user:${senderId}`,
|
|
456
|
+
recipientAddress: "agent",
|
|
457
|
+
conversationLabel,
|
|
458
|
+
rawBody: message,
|
|
459
|
+
messageId: request.id,
|
|
460
|
+
timestamp: Date.now(),
|
|
461
|
+
extraContext: {
|
|
462
|
+
...tenantId ? { TenantId: tenantId } : {},
|
|
463
|
+
...clientType ? { ClientType: clientType } : {},
|
|
464
|
+
...conversationId ? { ConversationId: conversationId } : {},
|
|
465
|
+
...displayName ? { SenderName: displayName } : {},
|
|
466
|
+
ChannelMode: channelMode
|
|
467
|
+
},
|
|
468
|
+
deliver: async (payload) => {
|
|
469
|
+
const responseText = payload.text ?? "";
|
|
470
|
+
await addMessage(sessionId, "assistant", responseText);
|
|
471
|
+
chatClient?.sendResponse(request.id, true, {
|
|
472
|
+
text: responseText,
|
|
473
|
+
sessionKey: resolvedOpenClawKey ?? legacySessionKey
|
|
474
|
+
});
|
|
475
|
+
},
|
|
476
|
+
onRecordError: (err) => {
|
|
477
|
+
log.error(`Session record error: ${err instanceof Error ? err.message : String(err)}`);
|
|
478
|
+
},
|
|
479
|
+
onDispatchError: (err, info) => {
|
|
480
|
+
log.error(`Dispatch error (${info.kind}): ${err instanceof Error ? err.message : String(err)}`);
|
|
481
|
+
}
|
|
482
|
+
})).route.sessionKey;
|
|
483
|
+
log.info(`Agent dispatch complete: sessionKey=${resolvedOpenClawKey}`);
|
|
484
|
+
} catch (err) {
|
|
485
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
486
|
+
log.error(`Agent dispatch failed: ${errMsg}`);
|
|
487
|
+
chatClient?.sendResponse(request.id, false, { message: errMsg });
|
|
488
|
+
} finally {
|
|
489
|
+
unsubscribe();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async function handleSessionsList(request, log) {
|
|
493
|
+
try {
|
|
494
|
+
const params = request.params;
|
|
495
|
+
const sessions = await listSessions({
|
|
496
|
+
channel: params.channel,
|
|
497
|
+
tenantId: params.tenantId,
|
|
498
|
+
userId: params.userId
|
|
499
|
+
});
|
|
500
|
+
chatClient?.sendResponse(request.id, true, { sessions });
|
|
501
|
+
} catch (err) {
|
|
502
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
503
|
+
log.error(`sessions.list failed: ${errMsg}`);
|
|
504
|
+
chatClient?.sendResponse(request.id, false, { message: errMsg });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async function handleSessionsGet(request, log) {
|
|
508
|
+
try {
|
|
509
|
+
const params = request.params;
|
|
510
|
+
const session = await getSession(params.sessionId);
|
|
511
|
+
if (!session) {
|
|
512
|
+
chatClient?.sendResponse(request.id, true, {
|
|
513
|
+
ok: false,
|
|
514
|
+
error: "Session not found"
|
|
515
|
+
});
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (params.userId && session.userId && session.userId !== params.userId) {
|
|
519
|
+
chatClient?.sendResponse(request.id, true, {
|
|
520
|
+
ok: false,
|
|
521
|
+
error: "Session not found"
|
|
522
|
+
});
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
chatClient?.sendResponse(request.id, true, {
|
|
526
|
+
sessionId: session.sessionId,
|
|
527
|
+
agentId: session.agentId,
|
|
528
|
+
channel: session.channel,
|
|
529
|
+
createdAt: session.createdAt,
|
|
530
|
+
messages: session.messages.map((m) => ({
|
|
531
|
+
id: `msg-${String(m.timestamp)}`,
|
|
532
|
+
role: m.role,
|
|
533
|
+
content: m.content,
|
|
534
|
+
timestamp: m.timestamp
|
|
535
|
+
}))
|
|
536
|
+
});
|
|
537
|
+
} catch (err) {
|
|
538
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
539
|
+
log.error(`sessions.get failed: ${errMsg}`);
|
|
540
|
+
chatClient?.sendResponse(request.id, false, { message: errMsg });
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const plugin = {
|
|
544
|
+
id: "@alfe.ai/openclaw-chat",
|
|
545
|
+
name: "Chat Plugin",
|
|
546
|
+
description: "Chat conversation channel — web widget and mobile app share unified chat sessions",
|
|
547
|
+
version: "0.0.8",
|
|
548
|
+
activate(api) {
|
|
549
|
+
const log = api.logger;
|
|
550
|
+
const alreadyActivated = globalThis.__alfeChatPluginActivated === true;
|
|
551
|
+
globalThis.__alfeChatPluginActivated = true;
|
|
552
|
+
const alfeChannel = createAlfeChannelPlugin();
|
|
553
|
+
api.registerChannel(alfeChannel);
|
|
554
|
+
log.info(`Registered channel: ${alfeChannel.id}`);
|
|
555
|
+
if (!alreadyActivated) {
|
|
556
|
+
log.info("Chat plugin registering...");
|
|
557
|
+
resolveOpenClawSdk(log);
|
|
558
|
+
pluginRuntime = api.runtime ?? null;
|
|
559
|
+
}
|
|
560
|
+
const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
|
|
561
|
+
if (!alreadyActivated) connectingPromise = Promise.resolve().then(() => {
|
|
562
|
+
try {
|
|
563
|
+
const { apiKey, chatWsUrl } = (0, _alfe_ai_chat.resolveAlfeChat)({
|
|
564
|
+
apiKey: pluginConfig.apiKey,
|
|
565
|
+
chatWsUrl: pluginConfig.chatWsUrl
|
|
566
|
+
});
|
|
567
|
+
if (chatWsUrl && apiKey) {
|
|
568
|
+
log.info(`Connecting to chat service: ${chatWsUrl}`);
|
|
569
|
+
chatClient = new _alfe_ai_chat.ChatServiceClient({
|
|
570
|
+
wsUrl: chatWsUrl,
|
|
571
|
+
apiKey,
|
|
572
|
+
onRequest: (request) => {
|
|
573
|
+
if (request.method === "agent") handleAgentRequest(request, log);
|
|
574
|
+
else if (request.method === "sessions.list") handleSessionsList(request, log);
|
|
575
|
+
else if (request.method === "sessions.get") handleSessionsGet(request, log);
|
|
576
|
+
else chatClient?.sendResponse(request.id, false, { message: `Unknown method: ${request.method}` });
|
|
577
|
+
},
|
|
578
|
+
onConnectionChange: (connected) => {
|
|
579
|
+
log.info(`Chat service connection: ${connected ? "connected" : "disconnected"}`);
|
|
580
|
+
},
|
|
581
|
+
logger: log
|
|
582
|
+
});
|
|
583
|
+
chatClient.start();
|
|
584
|
+
log.info("Chat service relay started");
|
|
585
|
+
} else log.info("Chat service URL not configured — running without chat service relay");
|
|
586
|
+
} catch (err) {
|
|
587
|
+
log.error(`Failed to initialize chat service: ${err instanceof Error ? err.message : String(err)}`);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
if (typeof api.registerGatewayMethod === "function") {
|
|
591
|
+
api.registerGatewayMethod("sessions.list", async (...args) => {
|
|
592
|
+
const params = args[0];
|
|
593
|
+
return { sessions: await listSessions({
|
|
594
|
+
channel: params.channel,
|
|
595
|
+
tenantId: params.tenantId,
|
|
596
|
+
userId: params.userId
|
|
597
|
+
}) };
|
|
598
|
+
});
|
|
599
|
+
api.registerGatewayMethod("sessions.get", async (...args) => {
|
|
600
|
+
const params = args[0];
|
|
601
|
+
const session = await getSession(params.sessionId);
|
|
602
|
+
if (!session) return {
|
|
603
|
+
ok: false,
|
|
604
|
+
error: "Session not found"
|
|
605
|
+
};
|
|
606
|
+
if (params.userId && session.userId && session.userId !== params.userId) return {
|
|
607
|
+
ok: false,
|
|
608
|
+
error: "Session not found"
|
|
609
|
+
};
|
|
610
|
+
return {
|
|
611
|
+
sessionId: session.sessionId,
|
|
612
|
+
agentId: session.agentId,
|
|
613
|
+
channel: session.channel,
|
|
614
|
+
createdAt: session.createdAt,
|
|
615
|
+
messages: session.messages.map((m) => ({
|
|
616
|
+
id: `msg-${String(m.timestamp)}`,
|
|
617
|
+
role: m.role,
|
|
618
|
+
content: m.content,
|
|
619
|
+
timestamp: m.timestamp
|
|
620
|
+
}))
|
|
621
|
+
};
|
|
622
|
+
});
|
|
623
|
+
log.info("Registered gateway RPC methods: sessions.list, sessions.get");
|
|
624
|
+
}
|
|
625
|
+
if (!alreadyActivated) {
|
|
626
|
+
api.on("session_start", async (...eventArgs) => {
|
|
627
|
+
const key = eventArgs[0].sessionKey;
|
|
628
|
+
if (!key || isAlfeSessionKey(key)) return;
|
|
629
|
+
log.info(`Chat session starting: ${key}`);
|
|
630
|
+
await createSession(key, "", "alfe");
|
|
631
|
+
}, { priority: 50 });
|
|
632
|
+
api.on("message", async (...eventArgs) => {
|
|
633
|
+
const event = eventArgs[0];
|
|
634
|
+
const key = event.sessionKey;
|
|
635
|
+
if (!key) return;
|
|
636
|
+
if (isAlfeSessionKey(key)) {
|
|
637
|
+
if (event.role === "assistant" && chatClient) {
|
|
638
|
+
const parsed = parseAlfeSessionKey(key);
|
|
639
|
+
if (parsed.conversationId) chatClient.notify("agent-message", {
|
|
640
|
+
conversationId: parsed.conversationId,
|
|
641
|
+
text: event.content
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
} else await addMessage(key, event.role, event.content);
|
|
645
|
+
});
|
|
646
|
+
api.on("session_end", (...eventArgs) => {
|
|
647
|
+
const key = eventArgs[0].sessionKey;
|
|
648
|
+
if (!key || !isAlfeSessionKey(key)) return;
|
|
649
|
+
log.info(`Chat session ending: ${key}`);
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
log.info("Chat plugin registered");
|
|
653
|
+
},
|
|
654
|
+
async deactivate(api) {
|
|
655
|
+
globalThis.__alfeChatPluginActivated = false;
|
|
656
|
+
const log = api.logger;
|
|
657
|
+
log.info("Chat plugin deactivating...");
|
|
658
|
+
if (connectingPromise) {
|
|
659
|
+
await connectingPromise.catch((err) => {
|
|
660
|
+
api.logger.debug(`Connection attempt failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
661
|
+
});
|
|
662
|
+
connectingPromise = null;
|
|
663
|
+
}
|
|
664
|
+
if (chatClient) {
|
|
665
|
+
chatClient.stop();
|
|
666
|
+
chatClient = null;
|
|
667
|
+
log.info("Chat service client stopped");
|
|
668
|
+
}
|
|
669
|
+
pluginRuntime = null;
|
|
670
|
+
dispatchInbound = null;
|
|
671
|
+
log.info("Chat plugin deactivated");
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
//#endregion
|
|
675
|
+
Object.defineProperty(exports, "createAlfeChannelPlugin", {
|
|
676
|
+
enumerable: true,
|
|
677
|
+
get: function() {
|
|
678
|
+
return createAlfeChannelPlugin;
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
Object.defineProperty(exports, "plugin", {
|
|
682
|
+
enumerable: true,
|
|
683
|
+
get: function() {
|
|
684
|
+
return plugin;
|
|
685
|
+
}
|
|
686
|
+
});
|
package/dist/plugin2.js
CHANGED
|
@@ -138,28 +138,98 @@ function createAlfeChannelPlugin() {
|
|
|
138
138
|
//#endregion
|
|
139
139
|
//#region src/session-keys.ts
|
|
140
140
|
/**
|
|
141
|
-
* Session key helpers — handles
|
|
141
|
+
* Session key helpers — handles standardized, canonical, and legacy formats.
|
|
142
142
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
143
|
+
* Standardized format (Alfe-controlled):
|
|
144
|
+
* alfe:{mode}:{identity} — single-threaded (SMS, WhatsApp)
|
|
145
|
+
* alfe:{mode}:{identity}:{convId} — multi-threaded (web chat)
|
|
146
146
|
*
|
|
147
|
-
*
|
|
147
|
+
* OpenClaw canonical format (from resolveAgentRoute):
|
|
148
|
+
* agent:{ocAgentId}:alfe:[default:]direct:{senderId}[:thread:{conversationId}]
|
|
149
|
+
*
|
|
150
|
+
* Legacy formats (deprecated):
|
|
151
|
+
* sms-{agentId}-{phone}
|
|
152
|
+
* wa-{agentId}-{phone}
|
|
148
153
|
* chat-{tenantId}-{agentId}-{suffix}
|
|
149
154
|
* agent:{agentId}:chat-{tenantId}-{agentId}-{suffix}
|
|
150
|
-
*
|
|
151
|
-
* The plugin may receive either format depending on which
|
|
152
|
-
* OpenClaw event fires and whether the new dispatch path is active.
|
|
153
155
|
*/
|
|
156
|
+
/** Single-threaded channel modes — identity IS the conversation. */
|
|
157
|
+
const SINGLE_THREADED_MODES = new Set([
|
|
158
|
+
"sms",
|
|
159
|
+
"whatsapp",
|
|
160
|
+
"mobile"
|
|
161
|
+
]);
|
|
154
162
|
/**
|
|
155
163
|
* Check if a session key belongs to the Alfe chat channel.
|
|
156
|
-
* Handles
|
|
164
|
+
* Handles standardized, canonical, and legacy formats.
|
|
157
165
|
*/
|
|
158
166
|
function isAlfeSessionKey(key) {
|
|
167
|
+
if (key.startsWith("alfe:")) return true;
|
|
159
168
|
if (key.includes(":alfe:")) return true;
|
|
160
|
-
if (key.includes("chat-")) return true;
|
|
169
|
+
if (key.includes("chat-") || key.startsWith("sms-") || key.startsWith("wa-")) return true;
|
|
161
170
|
return false;
|
|
162
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Parse session key metadata. Returns available fields from any format.
|
|
174
|
+
*
|
|
175
|
+
* For the callback flow, `conversationId` is the critical field — it maps
|
|
176
|
+
* to the channel registry key used by getChannelCallback().
|
|
177
|
+
*/
|
|
178
|
+
function parseAlfeSessionKey(key) {
|
|
179
|
+
const standardMatch = /^alfe:(\w+):(.+)$/.exec(key);
|
|
180
|
+
if (standardMatch) {
|
|
181
|
+
const [, mode, rest] = standardMatch;
|
|
182
|
+
if (SINGLE_THREADED_MODES.has(mode)) return {
|
|
183
|
+
agentId: "",
|
|
184
|
+
userId: rest,
|
|
185
|
+
conversationId: key,
|
|
186
|
+
tenantId: "",
|
|
187
|
+
mode
|
|
188
|
+
};
|
|
189
|
+
const lastColon = rest.lastIndexOf(":");
|
|
190
|
+
if (lastColon > 0) return {
|
|
191
|
+
agentId: "",
|
|
192
|
+
userId: rest.slice(0, lastColon),
|
|
193
|
+
conversationId: key,
|
|
194
|
+
tenantId: "",
|
|
195
|
+
mode
|
|
196
|
+
};
|
|
197
|
+
return {
|
|
198
|
+
agentId: "",
|
|
199
|
+
userId: rest,
|
|
200
|
+
conversationId: key,
|
|
201
|
+
tenantId: "",
|
|
202
|
+
mode
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const canonicalMatch = /^agent:([^:]+):alfe:(?:default:)?direct:(.+?)(?::thread:(.+))?$/.exec(key);
|
|
206
|
+
if (canonicalMatch) {
|
|
207
|
+
const [, matchAgentId, matchUserId, matchConvId] = canonicalMatch;
|
|
208
|
+
return {
|
|
209
|
+
agentId: matchAgentId,
|
|
210
|
+
userId: matchUserId,
|
|
211
|
+
conversationId: matchConvId || "",
|
|
212
|
+
tenantId: "",
|
|
213
|
+
mode: ""
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const rawKey = key.includes(":") ? key.slice(key.lastIndexOf(":") + 1) : key;
|
|
217
|
+
const legacyMatch = /^chat-([^-]+)-([^-]+)/.exec(rawKey);
|
|
218
|
+
return {
|
|
219
|
+
agentId: legacyMatch?.[2] ?? "",
|
|
220
|
+
userId: "",
|
|
221
|
+
conversationId: "",
|
|
222
|
+
tenantId: legacyMatch?.[1] ?? "",
|
|
223
|
+
mode: "chat"
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Extract the channel mode from a standardized session key or conversationId.
|
|
228
|
+
* Returns the mode segment (e.g. 'sms', 'whatsapp', 'chat') or fallback.
|
|
229
|
+
*/
|
|
230
|
+
function extractChannelMode(conversationId, fallback = "chat") {
|
|
231
|
+
return /^alfe:(\w+):/.exec(conversationId)?.[1] ?? fallback;
|
|
232
|
+
}
|
|
163
233
|
//#endregion
|
|
164
234
|
//#region src/session-store.ts
|
|
165
235
|
/**
|
|
@@ -274,6 +344,7 @@ async function listSessions(filters, limit = 50) {
|
|
|
274
344
|
if (filters?.agentId && session.agentId !== filters.agentId) continue;
|
|
275
345
|
if (filters?.channel && session.channel !== filters.channel) continue;
|
|
276
346
|
if (filters?.tenantId && session.tenantId !== filters.tenantId) continue;
|
|
347
|
+
if (filters?.userId && session.userId !== filters.userId) continue;
|
|
277
348
|
const lastMsg = session.messages.at(-1);
|
|
278
349
|
summaries.push({
|
|
279
350
|
sessionId: session.sessionId,
|
|
@@ -362,14 +433,16 @@ async function handleAgentRequest(request, log) {
|
|
|
362
433
|
});
|
|
363
434
|
});
|
|
364
435
|
try {
|
|
436
|
+
const channelMode = extractChannelMode(conversationId ?? "", clientType ?? "chat");
|
|
437
|
+
const channelLabel = channelMode === "sms" ? "SMS" : channelMode === "whatsapp" ? "WhatsApp" : "Alfe";
|
|
365
438
|
const shortConvId = conversationId?.slice(-8) ?? "";
|
|
366
439
|
const userLabel = displayName ?? userId ?? senderId;
|
|
367
|
-
const conversationLabel = conversationType === "group" ? shortConvId ? `[
|
|
440
|
+
const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
|
|
368
441
|
resolvedOpenClawKey = (await dispatchInbound({
|
|
369
442
|
cfg,
|
|
370
443
|
runtime: { channel: runtime.channel },
|
|
371
444
|
channel: "alfe",
|
|
372
|
-
channelLabel
|
|
445
|
+
channelLabel,
|
|
373
446
|
accountId: "default",
|
|
374
447
|
peer: conversationType === "group" ? {
|
|
375
448
|
kind: "group",
|
|
@@ -389,7 +462,8 @@ async function handleAgentRequest(request, log) {
|
|
|
389
462
|
...tenantId ? { TenantId: tenantId } : {},
|
|
390
463
|
...clientType ? { ClientType: clientType } : {},
|
|
391
464
|
...conversationId ? { ConversationId: conversationId } : {},
|
|
392
|
-
...displayName ? { SenderName: displayName } : {}
|
|
465
|
+
...displayName ? { SenderName: displayName } : {},
|
|
466
|
+
ChannelMode: channelMode
|
|
393
467
|
},
|
|
394
468
|
deliver: async (payload) => {
|
|
395
469
|
const responseText = payload.text ?? "";
|
|
@@ -420,7 +494,8 @@ async function handleSessionsList(request, log) {
|
|
|
420
494
|
const params = request.params;
|
|
421
495
|
const sessions = await listSessions({
|
|
422
496
|
channel: params.channel,
|
|
423
|
-
tenantId: params.tenantId
|
|
497
|
+
tenantId: params.tenantId,
|
|
498
|
+
userId: params.userId
|
|
424
499
|
});
|
|
425
500
|
chatClient?.sendResponse(request.id, true, { sessions });
|
|
426
501
|
} catch (err) {
|
|
@@ -440,6 +515,13 @@ async function handleSessionsGet(request, log) {
|
|
|
440
515
|
});
|
|
441
516
|
return;
|
|
442
517
|
}
|
|
518
|
+
if (params.userId && session.userId && session.userId !== params.userId) {
|
|
519
|
+
chatClient?.sendResponse(request.id, true, {
|
|
520
|
+
ok: false,
|
|
521
|
+
error: "Session not found"
|
|
522
|
+
});
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
443
525
|
chatClient?.sendResponse(request.id, true, {
|
|
444
526
|
sessionId: session.sessionId,
|
|
445
527
|
agentId: session.agentId,
|
|
@@ -510,7 +592,8 @@ const plugin = {
|
|
|
510
592
|
const params = args[0];
|
|
511
593
|
return { sessions: await listSessions({
|
|
512
594
|
channel: params.channel,
|
|
513
|
-
tenantId: params.tenantId
|
|
595
|
+
tenantId: params.tenantId,
|
|
596
|
+
userId: params.userId
|
|
514
597
|
}) };
|
|
515
598
|
});
|
|
516
599
|
api.registerGatewayMethod("sessions.get", async (...args) => {
|
|
@@ -520,6 +603,10 @@ const plugin = {
|
|
|
520
603
|
ok: false,
|
|
521
604
|
error: "Session not found"
|
|
522
605
|
};
|
|
606
|
+
if (params.userId && session.userId && session.userId !== params.userId) return {
|
|
607
|
+
ok: false,
|
|
608
|
+
error: "Session not found"
|
|
609
|
+
};
|
|
523
610
|
return {
|
|
524
611
|
sessionId: session.sessionId,
|
|
525
612
|
agentId: session.agentId,
|
|
@@ -545,8 +632,16 @@ const plugin = {
|
|
|
545
632
|
api.on("message", async (...eventArgs) => {
|
|
546
633
|
const event = eventArgs[0];
|
|
547
634
|
const key = event.sessionKey;
|
|
548
|
-
if (!key
|
|
549
|
-
|
|
635
|
+
if (!key) return;
|
|
636
|
+
if (isAlfeSessionKey(key)) {
|
|
637
|
+
if (event.role === "assistant" && chatClient) {
|
|
638
|
+
const parsed = parseAlfeSessionKey(key);
|
|
639
|
+
if (parsed.conversationId) chatClient.notify("agent-message", {
|
|
640
|
+
conversationId: parsed.conversationId,
|
|
641
|
+
text: event.content
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
} else await addMessage(key, event.role, event.content);
|
|
550
645
|
});
|
|
551
646
|
api.on("session_end", (...eventArgs) => {
|
|
552
647
|
const key = eventArgs[0].sessionKey;
|
package/package.json
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/openclaw-chat",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/plugin.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"
|
|
11
|
-
"
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"require": "./dist/index.cjs",
|
|
12
|
+
"import": "./dist/index.js"
|
|
12
13
|
},
|
|
13
14
|
"./plugin": {
|
|
14
|
-
"
|
|
15
|
-
"
|
|
15
|
+
"types": "./dist/plugin.d.ts",
|
|
16
|
+
"require": "./dist/plugin.cjs",
|
|
17
|
+
"import": "./dist/plugin.js"
|
|
16
18
|
}
|
|
17
19
|
},
|
|
18
20
|
"openclaw": {
|
|
@@ -25,7 +27,7 @@
|
|
|
25
27
|
"openclaw.plugin.json"
|
|
26
28
|
],
|
|
27
29
|
"dependencies": {
|
|
28
|
-
"@alfe.ai/chat": "^0.0.
|
|
30
|
+
"@alfe.ai/chat": "^0.0.6"
|
|
29
31
|
},
|
|
30
32
|
"peerDependencies": {
|
|
31
33
|
"openclaw": ">=2026.3.0"
|