fluxy-bot 0.13.6 → 0.15.1
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/package.json +1 -1
- package/scripts/install.ps1 +11 -0
- package/scripts/install.sh +6 -0
- package/shared/config.ts +9 -10
- package/supervisor/channels/manager.ts +393 -0
- package/supervisor/channels/types.ts +43 -41
- package/supervisor/channels/whatsapp.ts +252 -0
- package/supervisor/fluxy-agent.ts +24 -149
- package/supervisor/index.ts +194 -30
- package/supervisor/scheduler.ts +0 -9
- package/worker/db.ts +0 -62
- package/worker/index.ts +1 -42
- package/worker/prompts/fluxy-system-prompt.txt +67 -0
- package/workspace/skills/whatsapp-support/.claude-plugin/plugin.json +5 -0
- package/workspace/skills/whatsapp-support/SKILL.md +21 -0
- package/workspace/skills/whatsapp-support/SUPPORT.md +38 -0
- package/supervisor/channels/chat-channel.ts +0 -44
- package/supervisor/channels/index.ts +0 -56
- package/supervisor/channels/role-resolver.ts +0 -41
- package/supervisor/channels/router.ts +0 -245
- package/supervisor/channels/whatsapp-channel.ts +0 -177
- package/worker/prompts/customer-system-prompt.txt +0 -46
- package/workspace/skills/code-reviewer/.claude-plugin/plugin.json +0 -5
- package/workspace/skills/code-reviewer/SKILL.md +0 -36
- package/workspace/skills/daily-standup/.claude-plugin/plugin.json +0 -5
- package/workspace/skills/daily-standup/SKILL.md +0 -42
- package/workspace/skills/workspace-helper/.claude-plugin/plugin.json +0 -5
- package/workspace/skills/workspace-helper/SKILL.md +0 -55
package/package.json
CHANGED
package/scripts/install.ps1
CHANGED
|
@@ -244,6 +244,17 @@ function Install-Fluxy {
|
|
|
244
244
|
} catch {}
|
|
245
245
|
Pop-Location
|
|
246
246
|
|
|
247
|
+
# Install workspace dependencies (rebuilds native modules for this platform)
|
|
248
|
+
$wsDir = Join-Path $FLUXY_HOME "workspace"
|
|
249
|
+
if (Test-Path (Join-Path $wsDir "package.json")) {
|
|
250
|
+
Write-Down "Installing workspace dependencies..."
|
|
251
|
+
Push-Location $wsDir
|
|
252
|
+
try {
|
|
253
|
+
& $NPM install --omit=dev 2>$null
|
|
254
|
+
} catch {}
|
|
255
|
+
Pop-Location
|
|
256
|
+
}
|
|
257
|
+
|
|
247
258
|
# Verify
|
|
248
259
|
$cliPath = Join-Path $FLUXY_HOME "bin\cli.js"
|
|
249
260
|
if (-not (Test-Path $cliPath)) {
|
package/scripts/install.sh
CHANGED
|
@@ -203,6 +203,12 @@ install_fluxy() {
|
|
|
203
203
|
printf " ${BLUE}↓${RESET} Installing dependencies...\n"
|
|
204
204
|
(cd "$FLUXY_HOME" && "$NPM" install --omit=dev 2>/dev/null)
|
|
205
205
|
|
|
206
|
+
# Install workspace dependencies (rebuilds native modules for this platform)
|
|
207
|
+
if [ -f "$FLUXY_HOME/workspace/package.json" ]; then
|
|
208
|
+
printf " ${BLUE}↓${RESET} Installing workspace dependencies...\n"
|
|
209
|
+
(cd "$FLUXY_HOME/workspace" && "$NPM" install --omit=dev 2>/dev/null)
|
|
210
|
+
fi
|
|
211
|
+
|
|
206
212
|
# Verify
|
|
207
213
|
if [ ! -f "$FLUXY_HOME/bin/cli.js" ]; then
|
|
208
214
|
printf " ${RED}✗${RESET} Installation failed\n"
|
package/shared/config.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import { paths, DATA_DIR } from './paths.js';
|
|
3
3
|
|
|
4
|
-
export
|
|
4
|
+
export interface ChannelConfig {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
/** 'shared' = user's own number, 'dedicated' = Fluxy has its own number */
|
|
7
|
+
mode: 'shared' | 'dedicated';
|
|
8
|
+
/** The human/owner's phone number (used for role resolution in dedicated mode) */
|
|
9
|
+
humanPhone?: string;
|
|
10
|
+
}
|
|
5
11
|
|
|
6
12
|
export interface BotConfig {
|
|
7
13
|
port: number;
|
|
@@ -27,17 +33,10 @@ export interface BotConfig {
|
|
|
27
33
|
privateKey: string;
|
|
28
34
|
address: string;
|
|
29
35
|
};
|
|
30
|
-
tunnelUrl?: string;
|
|
31
36
|
channels?: {
|
|
32
|
-
whatsapp?:
|
|
33
|
-
telegram?: { enabled: boolean; botToken?: string };
|
|
34
|
-
};
|
|
35
|
-
customerMode?: {
|
|
36
|
-
enabled: boolean;
|
|
37
|
-
systemPromptFile?: string;
|
|
38
|
-
businessName?: string;
|
|
39
|
-
businessDescription?: string;
|
|
37
|
+
whatsapp?: ChannelConfig;
|
|
40
38
|
};
|
|
39
|
+
tunnelUrl?: string;
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
const DEFAULTS: BotConfig = {
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel Manager — orchestrates multi-channel messaging.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Manages channel providers (WhatsApp, Telegram, etc.)
|
|
6
|
+
* - Resolves sender role (human vs customer)
|
|
7
|
+
* - Routes inbound messages to the agent with appropriate system prompt
|
|
8
|
+
* - Routes outbound messages from agent/API back to channels
|
|
9
|
+
* - Manages parallel agent instances for customer conversations
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { loadConfig } from '../../shared/config.js';
|
|
15
|
+
import { WORKSPACE_DIR } from '../../shared/paths.js';
|
|
16
|
+
import { log } from '../../shared/logger.js';
|
|
17
|
+
import { startFluxyAgentQuery, type RecentMessage } from '../fluxy-agent.js';
|
|
18
|
+
import { WhatsAppChannel } from './whatsapp.js';
|
|
19
|
+
import type { ChannelConfig, ChannelProvider, ChannelStatus, ChannelType, InboundMessage, SenderRole } from './types.js';
|
|
20
|
+
|
|
21
|
+
const MAX_CONCURRENT_AGENTS = 5;
|
|
22
|
+
|
|
23
|
+
interface ChannelManagerOpts {
|
|
24
|
+
broadcastFluxy: (type: string, data: any) => void;
|
|
25
|
+
workerApi: (path: string, method?: string, body?: any) => Promise<any>;
|
|
26
|
+
restartBackend: () => void;
|
|
27
|
+
getModel: () => string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ActiveAgentQuery {
|
|
31
|
+
sender: string;
|
|
32
|
+
channel: ChannelType;
|
|
33
|
+
abortController?: AbortController;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class ChannelManager {
|
|
37
|
+
private providers = new Map<ChannelType, ChannelProvider>();
|
|
38
|
+
private opts: ChannelManagerOpts;
|
|
39
|
+
private activeAgents = new Map<string, ActiveAgentQuery>();
|
|
40
|
+
private messageQueue: InboundMessage[] = [];
|
|
41
|
+
private statusListeners: ((status: ChannelStatus) => void)[] = [];
|
|
42
|
+
|
|
43
|
+
constructor(opts: ChannelManagerOpts) {
|
|
44
|
+
this.opts = opts;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Initialize channels based on config */
|
|
48
|
+
async init(): Promise<void> {
|
|
49
|
+
const config = loadConfig();
|
|
50
|
+
const channelConfigs = (config as any).channels as Record<string, ChannelConfig> | undefined;
|
|
51
|
+
|
|
52
|
+
if (!channelConfigs?.whatsapp?.enabled) {
|
|
53
|
+
log.info('[channels] WhatsApp not enabled — skipping');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
log.info('[channels] Initializing WhatsApp channel...');
|
|
58
|
+
const whatsapp = new WhatsAppChannel(
|
|
59
|
+
(sender, senderName, text, fromMe) => this.handleInboundMessage('whatsapp', sender, senderName, text, fromMe),
|
|
60
|
+
(status) => this.handleStatusChange(status),
|
|
61
|
+
);
|
|
62
|
+
this.providers.set('whatsapp', whatsapp);
|
|
63
|
+
|
|
64
|
+
// Auto-connect if credentials exist (previously linked)
|
|
65
|
+
if (whatsapp.hasCredentials()) {
|
|
66
|
+
try {
|
|
67
|
+
await whatsapp.connect();
|
|
68
|
+
} catch (err: any) {
|
|
69
|
+
log.warn(`[channels] WhatsApp auto-connect failed: ${err.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Start WhatsApp connection (triggers QR flow if no credentials) */
|
|
75
|
+
async connectWhatsApp(): Promise<void> {
|
|
76
|
+
let provider = this.providers.get('whatsapp');
|
|
77
|
+
if (!provider) {
|
|
78
|
+
// Create provider on-demand if not initialized
|
|
79
|
+
const whatsapp = new WhatsAppChannel(
|
|
80
|
+
(sender, senderName, text, fromMe) => this.handleInboundMessage('whatsapp', sender, senderName, text, fromMe),
|
|
81
|
+
(status) => this.handleStatusChange(status),
|
|
82
|
+
);
|
|
83
|
+
this.providers.set('whatsapp', whatsapp);
|
|
84
|
+
provider = whatsapp;
|
|
85
|
+
}
|
|
86
|
+
await provider.connect();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Disconnect a specific channel */
|
|
90
|
+
async disconnectChannel(type: ChannelType): Promise<void> {
|
|
91
|
+
const provider = this.providers.get(type);
|
|
92
|
+
if (provider) {
|
|
93
|
+
await provider.disconnect();
|
|
94
|
+
this.providers.delete(type);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Disconnect all channels */
|
|
99
|
+
async disconnectAll(): Promise<void> {
|
|
100
|
+
for (const [, provider] of this.providers) {
|
|
101
|
+
await provider.disconnect();
|
|
102
|
+
}
|
|
103
|
+
this.providers.clear();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Send a message via a specific channel */
|
|
107
|
+
async sendMessage(channel: ChannelType, to: string, text: string): Promise<void> {
|
|
108
|
+
const provider = this.providers.get(channel);
|
|
109
|
+
if (!provider) throw new Error(`Channel ${channel} not available`);
|
|
110
|
+
await provider.sendMessage(to, text);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Get status of all channels */
|
|
114
|
+
getStatuses(): ChannelStatus[] {
|
|
115
|
+
return Array.from(this.providers.values()).map((p) => p.getStatus());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Get status of a specific channel */
|
|
119
|
+
getStatus(type: ChannelType): ChannelStatus | null {
|
|
120
|
+
return this.providers.get(type)?.getStatus() || null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Get QR code SVG for a channel */
|
|
124
|
+
getQrCode(type: ChannelType): string | null {
|
|
125
|
+
return this.providers.get(type)?.getQrCode() || null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Register a listener for status changes (used for WS broadcasting) */
|
|
129
|
+
onStatusChange(listener: (status: ChannelStatus) => void) {
|
|
130
|
+
this.statusListeners.push(listener);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Delete WhatsApp credentials and disconnect */
|
|
134
|
+
async logoutWhatsApp(): Promise<void> {
|
|
135
|
+
const provider = this.providers.get('whatsapp') as WhatsAppChannel | undefined;
|
|
136
|
+
if (provider) {
|
|
137
|
+
await provider.disconnect();
|
|
138
|
+
await provider.deleteCredentials();
|
|
139
|
+
this.providers.delete('whatsapp');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Internal ──
|
|
144
|
+
|
|
145
|
+
private handleStatusChange(status: ChannelStatus) {
|
|
146
|
+
for (const listener of this.statusListeners) {
|
|
147
|
+
listener(status);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Resolve sender role based on channel config */
|
|
152
|
+
private resolveRole(channel: ChannelType, sender: string, fromMe: boolean): SenderRole {
|
|
153
|
+
const config = loadConfig();
|
|
154
|
+
const channelConfigs = (config as any).channels as Record<string, ChannelConfig> | undefined;
|
|
155
|
+
const channelConfig = channelConfigs?.[channel];
|
|
156
|
+
|
|
157
|
+
if (!channelConfig) return 'customer';
|
|
158
|
+
|
|
159
|
+
if (channelConfig.mode === 'shared') {
|
|
160
|
+
// Shared number mode: fromMe messages are from the human
|
|
161
|
+
return fromMe ? 'human' : 'customer';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Dedicated number mode: check if sender matches human's phone
|
|
165
|
+
if (channelConfig.humanPhone) {
|
|
166
|
+
const senderPhone = sender.replace(/@.*/, '').replace(/[^0-9]/g, '');
|
|
167
|
+
const humanPhone = channelConfig.humanPhone.replace(/[^0-9]/g, '');
|
|
168
|
+
if (senderPhone === humanPhone) return 'human';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return 'customer';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Handle an incoming message from any channel */
|
|
175
|
+
private async handleInboundMessage(
|
|
176
|
+
channel: ChannelType,
|
|
177
|
+
sender: string,
|
|
178
|
+
senderName: string | undefined,
|
|
179
|
+
text: string,
|
|
180
|
+
fromMe: boolean,
|
|
181
|
+
) {
|
|
182
|
+
const role = this.resolveRole(channel, sender, fromMe);
|
|
183
|
+
|
|
184
|
+
const message: InboundMessage = {
|
|
185
|
+
channel,
|
|
186
|
+
sender: sender.replace(/@.*/, ''),
|
|
187
|
+
senderName,
|
|
188
|
+
role,
|
|
189
|
+
text,
|
|
190
|
+
rawSender: sender,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
log.info(`[channels] Inbound ${channel} | ${message.sender} | role=${role} | "${text.slice(0, 60)}"`);
|
|
194
|
+
|
|
195
|
+
if (role === 'human') {
|
|
196
|
+
await this.handleHumanMessage(message);
|
|
197
|
+
} else {
|
|
198
|
+
await this.handleCustomerMessage(message);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Handle message from the human/owner — mirrors to chat conversation */
|
|
203
|
+
private async handleHumanMessage(msg: InboundMessage) {
|
|
204
|
+
const { workerApi, broadcastFluxy, getModel } = this.opts;
|
|
205
|
+
const model = getModel();
|
|
206
|
+
|
|
207
|
+
// Get or create the human's WhatsApp conversation (mirrored with chat)
|
|
208
|
+
let convId: string | undefined;
|
|
209
|
+
try {
|
|
210
|
+
const ctx = await workerApi('/api/context/current');
|
|
211
|
+
if (ctx.conversationId) {
|
|
212
|
+
convId = ctx.conversationId;
|
|
213
|
+
} else {
|
|
214
|
+
const conv = await workerApi('/api/conversations', 'POST', {
|
|
215
|
+
title: `WhatsApp`,
|
|
216
|
+
model,
|
|
217
|
+
});
|
|
218
|
+
convId = conv.id;
|
|
219
|
+
await workerApi('/api/context/set', 'POST', { conversationId: convId });
|
|
220
|
+
}
|
|
221
|
+
} catch (err: any) {
|
|
222
|
+
log.warn(`[channels] Failed to get/create conversation: ${err.message}`);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Save user message to DB
|
|
227
|
+
try {
|
|
228
|
+
await workerApi(`/api/conversations/${convId}/messages`, 'POST', {
|
|
229
|
+
role: 'user',
|
|
230
|
+
content: msg.text,
|
|
231
|
+
meta: { model, channel: msg.channel },
|
|
232
|
+
});
|
|
233
|
+
} catch (err: any) {
|
|
234
|
+
log.warn(`[channels] DB persist error: ${err.message}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Broadcast to chat clients (mirroring)
|
|
238
|
+
broadcastFluxy('chat:sync', {
|
|
239
|
+
conversationId: convId,
|
|
240
|
+
message: { role: 'user', content: msg.text, timestamp: new Date().toISOString() },
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Fetch agent/user names and recent messages
|
|
244
|
+
let botName = 'Fluxy', humanName = 'Human';
|
|
245
|
+
let recentMessages: RecentMessage[] = [];
|
|
246
|
+
try {
|
|
247
|
+
const [status, recentRaw] = await Promise.all([
|
|
248
|
+
workerApi('/api/onboard/status'),
|
|
249
|
+
workerApi(`/api/conversations/${convId}/messages/recent?limit=20`),
|
|
250
|
+
]);
|
|
251
|
+
botName = status.agentName || 'Fluxy';
|
|
252
|
+
humanName = status.userName || 'Human';
|
|
253
|
+
if (Array.isArray(recentRaw)) {
|
|
254
|
+
const filtered = recentRaw.filter((m: any) => m.role === 'user' || m.role === 'assistant');
|
|
255
|
+
if (filtered.length > 0) {
|
|
256
|
+
recentMessages = filtered.slice(0, -1).map((m: any) => ({
|
|
257
|
+
role: m.role as 'user' | 'assistant',
|
|
258
|
+
content: m.content,
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
} catch {}
|
|
263
|
+
|
|
264
|
+
// Run agent with main system prompt (same as chat)
|
|
265
|
+
const channelContext = `[WhatsApp | ${msg.sender} | human]\n`;
|
|
266
|
+
|
|
267
|
+
startFluxyAgentQuery(
|
|
268
|
+
convId,
|
|
269
|
+
channelContext + msg.text,
|
|
270
|
+
model,
|
|
271
|
+
(type, eventData) => {
|
|
272
|
+
if (type === 'bot:response' && eventData.content) {
|
|
273
|
+
// Send response back via WhatsApp
|
|
274
|
+
this.sendMessage(msg.channel, msg.rawSender, eventData.content).catch((err) => {
|
|
275
|
+
log.warn(`[channels] Failed to send WhatsApp reply: ${err.message}`);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Save to DB
|
|
279
|
+
workerApi(`/api/conversations/${convId}/messages`, 'POST', {
|
|
280
|
+
role: 'assistant',
|
|
281
|
+
content: eventData.content,
|
|
282
|
+
meta: { model },
|
|
283
|
+
}).catch(() => {});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Mirror streaming to chat clients
|
|
287
|
+
if (type === 'bot:token' || type === 'bot:response' || type === 'bot:typing' || type === 'bot:tool') {
|
|
288
|
+
broadcastFluxy(type, eventData);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (type === 'bot:done' && eventData.usedFileTools) {
|
|
292
|
+
this.opts.restartBackend();
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
undefined,
|
|
296
|
+
undefined,
|
|
297
|
+
{ botName, humanName },
|
|
298
|
+
recentMessages,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Handle message from a customer — runs support agent in parallel */
|
|
303
|
+
private async handleCustomerMessage(msg: InboundMessage) {
|
|
304
|
+
const agentKey = `${msg.channel}:${msg.sender}`;
|
|
305
|
+
|
|
306
|
+
// Check concurrent limit
|
|
307
|
+
if (this.activeAgents.size >= MAX_CONCURRENT_AGENTS && !this.activeAgents.has(agentKey)) {
|
|
308
|
+
// Queue the message
|
|
309
|
+
log.info(`[channels] Max concurrent agents reached — queuing message from ${msg.sender}`);
|
|
310
|
+
this.messageQueue.push(msg);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const { workerApi, getModel } = this.opts;
|
|
315
|
+
const model = getModel();
|
|
316
|
+
|
|
317
|
+
// Load support system prompt from skill
|
|
318
|
+
const supportPrompt = this.loadSupportPrompt();
|
|
319
|
+
|
|
320
|
+
// Fetch agent/user names
|
|
321
|
+
let botName = 'Fluxy', humanName = 'Human';
|
|
322
|
+
try {
|
|
323
|
+
const status = await workerApi('/api/onboard/status');
|
|
324
|
+
botName = status.agentName || 'Fluxy';
|
|
325
|
+
humanName = status.userName || 'Human';
|
|
326
|
+
} catch {}
|
|
327
|
+
|
|
328
|
+
// Build channel context
|
|
329
|
+
const channelContext = `[WhatsApp | ${msg.sender} | customer${msg.senderName ? ` | ${msg.senderName}` : ''}]\n`;
|
|
330
|
+
const convId = `channel-${agentKey}-${Date.now()}`;
|
|
331
|
+
|
|
332
|
+
this.activeAgents.set(agentKey, { sender: msg.sender, channel: msg.channel });
|
|
333
|
+
|
|
334
|
+
startFluxyAgentQuery(
|
|
335
|
+
convId,
|
|
336
|
+
channelContext + msg.text,
|
|
337
|
+
model,
|
|
338
|
+
(type, eventData) => {
|
|
339
|
+
if (type === 'bot:response' && eventData.content) {
|
|
340
|
+
// Send response back via WhatsApp
|
|
341
|
+
this.sendMessage(msg.channel, msg.rawSender, eventData.content).catch((err) => {
|
|
342
|
+
log.warn(`[channels] Failed to send customer reply: ${err.message}`);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (type === 'bot:done') {
|
|
347
|
+
this.activeAgents.delete(agentKey);
|
|
348
|
+
|
|
349
|
+
if (eventData.usedFileTools) {
|
|
350
|
+
this.opts.restartBackend();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Process queued messages
|
|
354
|
+
this.processQueue();
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
undefined,
|
|
358
|
+
undefined,
|
|
359
|
+
{ botName, humanName },
|
|
360
|
+
undefined,
|
|
361
|
+
supportPrompt,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Load customer-facing system prompt from skills */
|
|
366
|
+
private loadSupportPrompt(): string | undefined {
|
|
367
|
+
// Look for SUPPORT.md in any skill directory
|
|
368
|
+
const skillsDir = path.join(WORKSPACE_DIR, 'skills');
|
|
369
|
+
try {
|
|
370
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
371
|
+
if (!entry.isDirectory()) continue;
|
|
372
|
+
const supportPath = path.join(skillsDir, entry.name, 'SUPPORT.md');
|
|
373
|
+
if (fs.existsSync(supportPath)) {
|
|
374
|
+
const content = fs.readFileSync(supportPath, 'utf-8').trim();
|
|
375
|
+
if (content) {
|
|
376
|
+
log.info(`[channels] Loaded support prompt from skill: ${entry.name}`);
|
|
377
|
+
return content;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} catch {}
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Process queued messages when an agent slot frees up */
|
|
386
|
+
private processQueue() {
|
|
387
|
+
while (this.messageQueue.length > 0 && this.activeAgents.size < MAX_CONCURRENT_AGENTS) {
|
|
388
|
+
const queued = this.messageQueue.shift()!;
|
|
389
|
+
log.info(`[channels] Processing queued message from ${queued.sender}`);
|
|
390
|
+
this.handleCustomerMessage(queued);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
@@ -1,55 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Defines the channel abstraction, message formats, and role system.
|
|
2
|
+
* Shared types for the multi-channel messaging system.
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
|
-
export type ChannelType = '
|
|
7
|
-
export type SenderRole = '
|
|
5
|
+
export type ChannelType = 'whatsapp' | 'telegram';
|
|
6
|
+
export type SenderRole = 'human' | 'customer';
|
|
8
7
|
|
|
9
|
-
export interface
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
role
|
|
14
|
-
|
|
8
|
+
export interface ChannelConfig {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
/** 'shared' = user's own number, 'dedicated' = Fluxy has its own number */
|
|
11
|
+
mode: 'shared' | 'dedicated';
|
|
12
|
+
/** The human/owner's phone number (used for role resolution in dedicated mode) */
|
|
13
|
+
humanPhone?: string;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
export interface
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
export interface InboundMessage {
|
|
17
|
+
channel: ChannelType;
|
|
18
|
+
/** Sender identifier (phone number / JID) */
|
|
19
|
+
sender: string;
|
|
20
|
+
/** Sender display name if available */
|
|
21
|
+
senderName?: string;
|
|
22
|
+
/** Resolved role of the sender */
|
|
23
|
+
role: SenderRole;
|
|
24
|
+
/** Message text content */
|
|
25
|
+
text: string;
|
|
26
|
+
/** Raw sender JID (channel-specific format) */
|
|
27
|
+
rawSender: string;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
export interface
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
export interface OutboundMessage {
|
|
31
|
+
channel: ChannelType;
|
|
32
|
+
to: string;
|
|
33
|
+
text: string;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
export interface ChannelStatus {
|
|
37
|
+
channel: ChannelType;
|
|
38
|
+
connected: boolean;
|
|
39
|
+
/** Additional info like phone number, QR state, etc. */
|
|
40
|
+
info?: Record<string, any>;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
export interface
|
|
43
|
+
export interface ChannelProvider {
|
|
42
44
|
readonly type: ChannelType;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
/** Send a message
|
|
48
|
-
sendMessage(to: string,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
|
|
45
|
+
/** Start the channel connection (may trigger QR flow) */
|
|
46
|
+
connect(): Promise<void>;
|
|
47
|
+
/** Disconnect and clean up */
|
|
48
|
+
disconnect(): Promise<void>;
|
|
49
|
+
/** Send a text message */
|
|
50
|
+
sendMessage(to: string, text: string): Promise<void>;
|
|
51
|
+
/** Get current connection status */
|
|
52
|
+
getStatus(): ChannelStatus;
|
|
53
|
+
/** Get current QR code data (base64 SVG) or null if not in QR state */
|
|
54
|
+
getQrCode(): string | null;
|
|
55
|
+
/** Whether auth credentials exist (previously connected) */
|
|
56
|
+
hasCredentials(): boolean;
|
|
55
57
|
}
|