fluxy-bot 0.15.1 → 0.15.2
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/shared/config.ts +4 -4
- package/supervisor/channels/manager.ts +73 -52
- package/supervisor/channels/types.ts +7 -7
- package/supervisor/channels/whatsapp.ts +49 -4
- package/supervisor/index.ts +4 -4
- package/worker/prompts/fluxy-system-prompt.txt +29 -8
- package/workspace/client/src/App.tsx +1 -1
- package/workspace/client/src/components/deleteme_onboarding/WorkspaceTour.tsx +3 -2
- package/workspace/skills/whatsapp-support/SUPPORT.md +4 -1
package/package.json
CHANGED
package/shared/config.ts
CHANGED
|
@@ -3,10 +3,10 @@ import { paths, DATA_DIR } from './paths.js';
|
|
|
3
3
|
|
|
4
4
|
export interface ChannelConfig {
|
|
5
5
|
enabled: boolean;
|
|
6
|
-
/** '
|
|
7
|
-
mode: '
|
|
8
|
-
/**
|
|
9
|
-
|
|
6
|
+
/** 'channel' = just talk to me (self-chat only), 'business' = admin/customer support */
|
|
7
|
+
mode: 'channel' | 'business';
|
|
8
|
+
/** Phone numbers with admin access (owner, secretary, etc.) — business mode only */
|
|
9
|
+
admins?: string[];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export interface BotConfig {
|
|
@@ -3,10 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Responsibilities:
|
|
5
5
|
* - Manages channel providers (WhatsApp, Telegram, etc.)
|
|
6
|
-
* - Resolves sender role (
|
|
6
|
+
* - Resolves sender role (admin vs customer) based on mode
|
|
7
7
|
* - Routes inbound messages to the agent with appropriate system prompt
|
|
8
|
-
* - Routes
|
|
9
|
-
* - Manages parallel agent instances for customer conversations
|
|
8
|
+
* - Routes agent responses back to channels
|
|
9
|
+
* - Manages parallel agent instances for customer conversations (business mode)
|
|
10
|
+
*
|
|
11
|
+
* Modes:
|
|
12
|
+
* - channel: Just talk to me. Only self-chat (fromMe=true) triggers the agent.
|
|
13
|
+
* All other messages are ignored — it's the user's personal WhatsApp.
|
|
14
|
+
* - business: Fluxy has its own number. Numbers in the admins array get the main
|
|
15
|
+
* system prompt. Everyone else gets the customer support prompt.
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
18
|
import fs from 'fs';
|
|
@@ -30,7 +36,6 @@ interface ChannelManagerOpts {
|
|
|
30
36
|
interface ActiveAgentQuery {
|
|
31
37
|
sender: string;
|
|
32
38
|
channel: ChannelType;
|
|
33
|
-
abortController?: AbortController;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
export class ChannelManager {
|
|
@@ -47,7 +52,7 @@ export class ChannelManager {
|
|
|
47
52
|
/** Initialize channels based on config */
|
|
48
53
|
async init(): Promise<void> {
|
|
49
54
|
const config = loadConfig();
|
|
50
|
-
const channelConfigs =
|
|
55
|
+
const channelConfigs = config.channels;
|
|
51
56
|
|
|
52
57
|
if (!channelConfigs?.whatsapp?.enabled) {
|
|
53
58
|
log.info('[channels] WhatsApp not enabled — skipping');
|
|
@@ -75,7 +80,6 @@ export class ChannelManager {
|
|
|
75
80
|
async connectWhatsApp(): Promise<void> {
|
|
76
81
|
let provider = this.providers.get('whatsapp');
|
|
77
82
|
if (!provider) {
|
|
78
|
-
// Create provider on-demand if not initialized
|
|
79
83
|
const whatsapp = new WhatsAppChannel(
|
|
80
84
|
(sender, senderName, text, fromMe) => this.handleInboundMessage('whatsapp', sender, senderName, text, fromMe),
|
|
81
85
|
(status) => this.handleStatusChange(status),
|
|
@@ -148,27 +152,10 @@ export class ChannelManager {
|
|
|
148
152
|
}
|
|
149
153
|
}
|
|
150
154
|
|
|
151
|
-
/**
|
|
152
|
-
private
|
|
155
|
+
/** Get the channel config, re-reading from disk each time */
|
|
156
|
+
private getChannelConfig(channel: ChannelType): ChannelConfig | undefined {
|
|
153
157
|
const config = loadConfig();
|
|
154
|
-
|
|
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';
|
|
158
|
+
return config.channels?.[channel];
|
|
172
159
|
}
|
|
173
160
|
|
|
174
161
|
/** Handle an incoming message from any channel */
|
|
@@ -179,7 +166,34 @@ export class ChannelManager {
|
|
|
179
166
|
text: string,
|
|
180
167
|
fromMe: boolean,
|
|
181
168
|
) {
|
|
182
|
-
const
|
|
169
|
+
const channelConfig = this.getChannelConfig(channel);
|
|
170
|
+
if (!channelConfig) return;
|
|
171
|
+
|
|
172
|
+
const mode = channelConfig.mode || 'channel';
|
|
173
|
+
|
|
174
|
+
// ── Channel mode: only respond to self-chat (fromMe=true) ──
|
|
175
|
+
if (mode === 'channel') {
|
|
176
|
+
if (!fromMe) {
|
|
177
|
+
// Ignore messages from other people — this is the user's personal WhatsApp
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const message: InboundMessage = {
|
|
182
|
+
channel,
|
|
183
|
+
sender: sender.replace(/@.*/, ''),
|
|
184
|
+
senderName,
|
|
185
|
+
role: 'admin',
|
|
186
|
+
text,
|
|
187
|
+
rawSender: sender,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
log.info(`[channels] Channel mode | self-chat | "${text.slice(0, 60)}"`);
|
|
191
|
+
await this.handleAdminMessage(message);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Business mode: resolve role based on admins array ──
|
|
196
|
+
const role = this.resolveBusinessRole(channelConfig, sender, fromMe);
|
|
183
197
|
|
|
184
198
|
const message: InboundMessage = {
|
|
185
199
|
channel,
|
|
@@ -190,31 +204,47 @@ export class ChannelManager {
|
|
|
190
204
|
rawSender: sender,
|
|
191
205
|
};
|
|
192
206
|
|
|
193
|
-
log.info(`[channels]
|
|
207
|
+
log.info(`[channels] Business mode | ${message.sender} | role=${role} | "${text.slice(0, 60)}"`);
|
|
194
208
|
|
|
195
|
-
if (role === '
|
|
196
|
-
await this.
|
|
209
|
+
if (role === 'admin') {
|
|
210
|
+
await this.handleAdminMessage(message);
|
|
197
211
|
} else {
|
|
198
212
|
await this.handleCustomerMessage(message);
|
|
199
213
|
}
|
|
200
214
|
}
|
|
201
215
|
|
|
202
|
-
/**
|
|
203
|
-
private
|
|
216
|
+
/** Resolve role in business mode — check admins array */
|
|
217
|
+
private resolveBusinessRole(config: ChannelConfig, sender: string, fromMe: boolean): SenderRole {
|
|
218
|
+
// fromMe is always admin (the number Fluxy is connected with)
|
|
219
|
+
if (fromMe) return 'admin';
|
|
220
|
+
|
|
221
|
+
// Check admins array
|
|
222
|
+
if (config.admins?.length) {
|
|
223
|
+
const senderPhone = sender.replace(/@.*/, '').replace(/[^0-9]/g, '');
|
|
224
|
+
for (const admin of config.admins) {
|
|
225
|
+
const adminPhone = admin.replace(/[^0-9]/g, '');
|
|
226
|
+
if (senderPhone === adminPhone || senderPhone.endsWith(adminPhone) || adminPhone.endsWith(senderPhone)) {
|
|
227
|
+
return 'admin';
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return 'customer';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Handle message from an admin — mirrors to chat conversation, uses main system prompt */
|
|
236
|
+
private async handleAdminMessage(msg: InboundMessage) {
|
|
204
237
|
const { workerApi, broadcastFluxy, getModel } = this.opts;
|
|
205
238
|
const model = getModel();
|
|
206
239
|
|
|
207
|
-
// Get or create
|
|
240
|
+
// Get or create conversation (shared with chat for mirroring)
|
|
208
241
|
let convId: string | undefined;
|
|
209
242
|
try {
|
|
210
243
|
const ctx = await workerApi('/api/context/current');
|
|
211
244
|
if (ctx.conversationId) {
|
|
212
245
|
convId = ctx.conversationId;
|
|
213
246
|
} else {
|
|
214
|
-
const conv = await workerApi('/api/conversations', 'POST', {
|
|
215
|
-
title: `WhatsApp`,
|
|
216
|
-
model,
|
|
217
|
-
});
|
|
247
|
+
const conv = await workerApi('/api/conversations', 'POST', { title: 'WhatsApp', model });
|
|
218
248
|
convId = conv.id;
|
|
219
249
|
await workerApi('/api/context/set', 'POST', { conversationId: convId });
|
|
220
250
|
}
|
|
@@ -240,7 +270,7 @@ export class ChannelManager {
|
|
|
240
270
|
message: { role: 'user', content: msg.text, timestamp: new Date().toISOString() },
|
|
241
271
|
});
|
|
242
272
|
|
|
243
|
-
// Fetch
|
|
273
|
+
// Fetch names and recent messages
|
|
244
274
|
let botName = 'Fluxy', humanName = 'Human';
|
|
245
275
|
let recentMessages: RecentMessage[] = [];
|
|
246
276
|
try {
|
|
@@ -261,8 +291,8 @@ export class ChannelManager {
|
|
|
261
291
|
}
|
|
262
292
|
} catch {}
|
|
263
293
|
|
|
264
|
-
//
|
|
265
|
-
const channelContext = `[WhatsApp | ${msg.sender} |
|
|
294
|
+
// Channel context — tells the agent this is a WhatsApp message, respond naturally
|
|
295
|
+
const channelContext = `[WhatsApp | ${msg.sender} | admin]\n`;
|
|
266
296
|
|
|
267
297
|
startFluxyAgentQuery(
|
|
268
298
|
convId,
|
|
@@ -270,7 +300,7 @@ export class ChannelManager {
|
|
|
270
300
|
model,
|
|
271
301
|
(type, eventData) => {
|
|
272
302
|
if (type === 'bot:response' && eventData.content) {
|
|
273
|
-
// Send response back via WhatsApp
|
|
303
|
+
// Send agent's response back via WhatsApp
|
|
274
304
|
this.sendMessage(msg.channel, msg.rawSender, eventData.content).catch((err) => {
|
|
275
305
|
log.warn(`[channels] Failed to send WhatsApp reply: ${err.message}`);
|
|
276
306
|
});
|
|
@@ -305,7 +335,6 @@ export class ChannelManager {
|
|
|
305
335
|
|
|
306
336
|
// Check concurrent limit
|
|
307
337
|
if (this.activeAgents.size >= MAX_CONCURRENT_AGENTS && !this.activeAgents.has(agentKey)) {
|
|
308
|
-
// Queue the message
|
|
309
338
|
log.info(`[channels] Max concurrent agents reached — queuing message from ${msg.sender}`);
|
|
310
339
|
this.messageQueue.push(msg);
|
|
311
340
|
return;
|
|
@@ -317,7 +346,7 @@ export class ChannelManager {
|
|
|
317
346
|
// Load support system prompt from skill
|
|
318
347
|
const supportPrompt = this.loadSupportPrompt();
|
|
319
348
|
|
|
320
|
-
// Fetch agent
|
|
349
|
+
// Fetch agent name
|
|
321
350
|
let botName = 'Fluxy', humanName = 'Human';
|
|
322
351
|
try {
|
|
323
352
|
const status = await workerApi('/api/onboard/status');
|
|
@@ -325,7 +354,6 @@ export class ChannelManager {
|
|
|
325
354
|
humanName = status.userName || 'Human';
|
|
326
355
|
} catch {}
|
|
327
356
|
|
|
328
|
-
// Build channel context
|
|
329
357
|
const channelContext = `[WhatsApp | ${msg.sender} | customer${msg.senderName ? ` | ${msg.senderName}` : ''}]\n`;
|
|
330
358
|
const convId = `channel-${agentKey}-${Date.now()}`;
|
|
331
359
|
|
|
@@ -337,7 +365,6 @@ export class ChannelManager {
|
|
|
337
365
|
model,
|
|
338
366
|
(type, eventData) => {
|
|
339
367
|
if (type === 'bot:response' && eventData.content) {
|
|
340
|
-
// Send response back via WhatsApp
|
|
341
368
|
this.sendMessage(msg.channel, msg.rawSender, eventData.content).catch((err) => {
|
|
342
369
|
log.warn(`[channels] Failed to send customer reply: ${err.message}`);
|
|
343
370
|
});
|
|
@@ -345,12 +372,7 @@ export class ChannelManager {
|
|
|
345
372
|
|
|
346
373
|
if (type === 'bot:done') {
|
|
347
374
|
this.activeAgents.delete(agentKey);
|
|
348
|
-
|
|
349
|
-
if (eventData.usedFileTools) {
|
|
350
|
-
this.opts.restartBackend();
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Process queued messages
|
|
375
|
+
if (eventData.usedFileTools) this.opts.restartBackend();
|
|
354
376
|
this.processQueue();
|
|
355
377
|
}
|
|
356
378
|
},
|
|
@@ -364,7 +386,6 @@ export class ChannelManager {
|
|
|
364
386
|
|
|
365
387
|
/** Load customer-facing system prompt from skills */
|
|
366
388
|
private loadSupportPrompt(): string | undefined {
|
|
367
|
-
// Look for SUPPORT.md in any skill directory
|
|
368
389
|
const skillsDir = path.join(WORKSPACE_DIR, 'skills');
|
|
369
390
|
try {
|
|
370
391
|
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export type ChannelType = 'whatsapp' | 'telegram';
|
|
6
|
-
export type SenderRole = '
|
|
6
|
+
export type SenderRole = 'admin' | 'customer';
|
|
7
7
|
|
|
8
8
|
export interface ChannelConfig {
|
|
9
9
|
enabled: boolean;
|
|
10
|
-
/** '
|
|
11
|
-
mode: '
|
|
12
|
-
/**
|
|
13
|
-
|
|
10
|
+
/** 'channel' = just talk to me (self-chat only), 'business' = admin/customer support */
|
|
11
|
+
mode: 'channel' | 'business';
|
|
12
|
+
/** Phone numbers with admin access (owner, secretary, etc.) — business mode only */
|
|
13
|
+
admins?: string[];
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface InboundMessage {
|
|
17
17
|
channel: ChannelType;
|
|
18
|
-
/** Sender identifier (phone number
|
|
18
|
+
/** Sender identifier (phone number) */
|
|
19
19
|
sender: string;
|
|
20
20
|
/** Sender display name if available */
|
|
21
21
|
senderName?: string;
|
|
@@ -23,7 +23,7 @@ export interface InboundMessage {
|
|
|
23
23
|
role: SenderRole;
|
|
24
24
|
/** Message text content */
|
|
25
25
|
text: string;
|
|
26
|
-
/** Raw sender JID (channel-specific format) */
|
|
26
|
+
/** Raw sender JID (channel-specific format, used for replies) */
|
|
27
27
|
rawSender: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -30,13 +30,18 @@ export class WhatsAppChannel implements ChannelProvider {
|
|
|
30
30
|
|
|
31
31
|
private sock: WASocket | null = null;
|
|
32
32
|
private connected = false;
|
|
33
|
-
private qrData: string | null = null;
|
|
34
|
-
private qrSvg: string | null = null;
|
|
33
|
+
private qrData: string | null = null;
|
|
34
|
+
private qrSvg: string | null = null;
|
|
35
35
|
private onMessage: OnWhatsAppMessage;
|
|
36
36
|
private onStatusChange: (status: ChannelStatus) => void;
|
|
37
37
|
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
38
38
|
private intentionalDisconnect = false;
|
|
39
39
|
|
|
40
|
+
/** Maps LID JIDs to phone JIDs (WhatsApp uses LIDs internally for self-chat) */
|
|
41
|
+
private lidToPhoneMap = new Map<string, string>();
|
|
42
|
+
/** Our own phone JID (number@s.whatsapp.net) */
|
|
43
|
+
private ownPhoneJid: string | null = null;
|
|
44
|
+
|
|
40
45
|
constructor(
|
|
41
46
|
onMessage: OnWhatsAppMessage,
|
|
42
47
|
onStatusChange: (status: ChannelStatus) => void,
|
|
@@ -110,6 +115,42 @@ export class WhatsAppChannel implements ChannelProvider {
|
|
|
110
115
|
|
|
111
116
|
// ── Internal ──
|
|
112
117
|
|
|
118
|
+
/** Translate a JID from LID format to phone format if possible */
|
|
119
|
+
private translateJid(jid: string): string {
|
|
120
|
+
// If it's already a phone JID, return as-is
|
|
121
|
+
if (jid.endsWith('@s.whatsapp.net')) return jid;
|
|
122
|
+
|
|
123
|
+
// Check LID map
|
|
124
|
+
const mapped = this.lidToPhoneMap.get(jid);
|
|
125
|
+
if (mapped) return mapped;
|
|
126
|
+
|
|
127
|
+
// If it's a LID JID and we know our own phone, and this looks like a self-chat LID
|
|
128
|
+
// LID JIDs typically end with @lid or have a long numeric format
|
|
129
|
+
if (this.ownPhoneJid && (jid.includes('@lid') || jid.match(/^\d{15,}@/))) {
|
|
130
|
+
log.info(`[whatsapp] Unmapped LID ${jid} — assuming self-chat, using own phone JID`);
|
|
131
|
+
this.lidToPhoneMap.set(jid, this.ownPhoneJid);
|
|
132
|
+
return this.ownPhoneJid;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return jid;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Build the LID-to-phone mapping from sock.user */
|
|
139
|
+
private buildLidMap() {
|
|
140
|
+
if (!this.sock?.user) return;
|
|
141
|
+
|
|
142
|
+
const user = this.sock.user;
|
|
143
|
+
// user.id is "phone:device@s.whatsapp.net" — extract phone
|
|
144
|
+
const phone = user.id.split(':')[0];
|
|
145
|
+
this.ownPhoneJid = `${phone}@s.whatsapp.net`;
|
|
146
|
+
|
|
147
|
+
// user.lid (if available) is the LID JID
|
|
148
|
+
if ((user as any).lid) {
|
|
149
|
+
this.lidToPhoneMap.set((user as any).lid, this.ownPhoneJid);
|
|
150
|
+
log.info(`[whatsapp] LID map: ${(user as any).lid} → ${this.ownPhoneJid}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
113
154
|
private async connectInternal(): Promise<void> {
|
|
114
155
|
// Ensure auth directory exists
|
|
115
156
|
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
@@ -164,6 +205,7 @@ export class WhatsAppChannel implements ChannelProvider {
|
|
|
164
205
|
this.connected = true;
|
|
165
206
|
this.qrData = null;
|
|
166
207
|
this.qrSvg = null;
|
|
208
|
+
this.buildLidMap();
|
|
167
209
|
log.ok(`[whatsapp] Connected as ${sock.user?.id}`);
|
|
168
210
|
this.emitStatus();
|
|
169
211
|
}
|
|
@@ -207,10 +249,13 @@ export class WhatsAppChannel implements ChannelProvider {
|
|
|
207
249
|
if (!text) continue;
|
|
208
250
|
|
|
209
251
|
const fromMe = msg.key.fromMe || false;
|
|
210
|
-
const
|
|
252
|
+
const rawSender = msg.key.remoteJid || '';
|
|
253
|
+
|
|
254
|
+
// Translate LID JIDs to phone JIDs
|
|
255
|
+
const sender = this.translateJid(rawSender);
|
|
211
256
|
const pushName = msg.pushName || undefined;
|
|
212
257
|
|
|
213
|
-
log.info(`[whatsapp] Message from ${sender} (fromMe=${fromMe}): ${text.slice(0, 80)}`);
|
|
258
|
+
log.info(`[whatsapp] Message from ${sender} (raw=${rawSender}, fromMe=${fromMe}): ${text.slice(0, 80)}`);
|
|
214
259
|
|
|
215
260
|
this.onMessage(sender, pushName, text, fromMe);
|
|
216
261
|
}
|
package/supervisor/index.ts
CHANGED
|
@@ -429,7 +429,7 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
|
|
|
429
429
|
// Enable WhatsApp in config
|
|
430
430
|
const cfg = loadConfig();
|
|
431
431
|
if (!cfg.channels) cfg.channels = {};
|
|
432
|
-
if (!cfg.channels.whatsapp) cfg.channels.whatsapp = { enabled: true, mode: '
|
|
432
|
+
if (!cfg.channels.whatsapp) cfg.channels.whatsapp = { enabled: true, mode: 'channel' };
|
|
433
433
|
cfg.channels.whatsapp.enabled = true;
|
|
434
434
|
saveConfig(cfg);
|
|
435
435
|
|
|
@@ -480,7 +480,7 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
|
|
|
480
480
|
return;
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
-
// POST /api/channels/whatsapp/configure — set mode +
|
|
483
|
+
// POST /api/channels/whatsapp/configure — set mode + admins
|
|
484
484
|
if (req.method === 'POST' && channelPath === '/api/channels/whatsapp/configure') {
|
|
485
485
|
let body = '';
|
|
486
486
|
req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
|
|
@@ -489,9 +489,9 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
|
|
|
489
489
|
const data = JSON.parse(body);
|
|
490
490
|
const cfg = loadConfig();
|
|
491
491
|
if (!cfg.channels) cfg.channels = {};
|
|
492
|
-
if (!cfg.channels.whatsapp) cfg.channels.whatsapp = { enabled: true, mode: '
|
|
492
|
+
if (!cfg.channels.whatsapp) cfg.channels.whatsapp = { enabled: true, mode: 'channel' };
|
|
493
493
|
if (data.mode) cfg.channels.whatsapp.mode = data.mode;
|
|
494
|
-
if (data.
|
|
494
|
+
if (data.admins !== undefined) cfg.channels.whatsapp.admins = data.admins;
|
|
495
495
|
saveConfig(cfg);
|
|
496
496
|
res.writeHead(200);
|
|
497
497
|
res.end(JSON.stringify({ ok: true, config: cfg.channels.whatsapp }));
|
|
@@ -206,6 +206,14 @@ Complex cron tasks can have detailed instruction files in `tasks/{cron-id}.md`.
|
|
|
206
206
|
|
|
207
207
|
You can communicate through messaging channels beyond the chat bubble. Currently supported: **WhatsApp**.
|
|
208
208
|
|
|
209
|
+
### CRITICAL: How WhatsApp Responses Work
|
|
210
|
+
|
|
211
|
+
**Your text response IS the WhatsApp reply.** When you receive a message tagged with `[WhatsApp | ...]`, the supervisor takes whatever you respond with and sends it directly to WhatsApp. You do NOT need to use curl or `/api/channels/send` to reply — just respond normally as if you're talking to the person.
|
|
212
|
+
|
|
213
|
+
**Do NOT use `/api/channels/send` to reply to incoming WhatsApp messages.** That endpoint is ONLY for proactive messages (during pulse, cron, or when you want to initiate a conversation). If you use it to reply, the person will get duplicate messages.
|
|
214
|
+
|
|
215
|
+
**Adjust your style for WhatsApp:** Keep messages shorter and more conversational than chat. No markdown headers, no code blocks unless asked. Think texting, not email.
|
|
216
|
+
|
|
209
217
|
### Channel Config
|
|
210
218
|
|
|
211
219
|
Your channel configuration is injected below (if any channels are configured). It comes from `~/.fluxy/config.json` — a file OUTSIDE your workspace that the supervisor manages.
|
|
@@ -220,28 +228,41 @@ Hi, I'd like to schedule an appointment.
|
|
|
220
228
|
|
|
221
229
|
The format is: `[Channel | phone | role | name (optional)]`
|
|
222
230
|
|
|
223
|
-
- **role=
|
|
231
|
+
- **role=admin**: This is your human or an authorized admin. Use your normal personality, full capabilities, main system prompt.
|
|
224
232
|
- **role=customer**: This is someone else messaging. You're in **support mode** — follow the instructions from your active skill's SUPPORT.md.
|
|
225
233
|
|
|
234
|
+
### WhatsApp Modes
|
|
235
|
+
|
|
236
|
+
**Channel Mode** (default): Your human's own WhatsApp number. Only self-chat triggers you — messages from other people are completely ignored. This is "just talk to me" mode.
|
|
237
|
+
|
|
238
|
+
**Business Mode**: Fluxy has its own dedicated number. Numbers in the `admins` array get admin access (main system prompt). Everyone else is a customer (support prompt).
|
|
239
|
+
|
|
226
240
|
### Setting Up WhatsApp
|
|
227
241
|
|
|
228
242
|
When your human asks to configure WhatsApp:
|
|
229
243
|
1. Start the connection: `curl -s -X POST http://localhost:3000/api/channels/whatsapp/connect`
|
|
230
244
|
2. Tell them to open the QR page: `http://localhost:3000/api/channels/whatsapp/qr-page` (or create a dashboard page that embeds it)
|
|
231
245
|
3. They scan the QR with their WhatsApp app
|
|
232
|
-
4.
|
|
233
|
-
|
|
234
|
-
|
|
246
|
+
4. The default mode is **channel** (self-chat only)
|
|
247
|
+
|
|
248
|
+
To switch to **business mode** with admin numbers:
|
|
249
|
+
```bash
|
|
250
|
+
curl -s -X POST http://localhost:3000/api/channels/whatsapp/configure \
|
|
251
|
+
-H "Content-Type: application/json" \
|
|
252
|
+
-d '{"mode":"business","admins":["+17865551234","+5511999887766"]}'
|
|
253
|
+
```
|
|
235
254
|
|
|
236
|
-
### Sending Messages
|
|
255
|
+
### Sending Proactive Messages
|
|
237
256
|
|
|
238
|
-
To
|
|
257
|
+
To INITIATE a WhatsApp message (during pulse, cron, or when you want to reach out first):
|
|
239
258
|
```bash
|
|
240
259
|
curl -s -X POST http://localhost:3000/api/channels/send \
|
|
241
260
|
-H "Content-Type: application/json" \
|
|
242
261
|
-d '{"channel":"whatsapp","to":"5511999888777","text":"Your appointment is confirmed for tomorrow at 2pm."}'
|
|
243
262
|
```
|
|
244
263
|
|
|
264
|
+
**Remember:** This is ONLY for starting new conversations or sending unprompted messages. When replying to an incoming message, just respond normally — the supervisor handles delivery.
|
|
265
|
+
|
|
245
266
|
### Customer Conversation Logs
|
|
246
267
|
|
|
247
268
|
When you finish a conversation with a **customer** via WhatsApp, save a summary to `whatsapp/{phone}.md`:
|
|
@@ -262,8 +283,8 @@ This is your memory of that customer. Next time they message, read their file fi
|
|
|
262
283
|
| `/api/channels/whatsapp/connect` | POST | Start WhatsApp (triggers QR if needed) |
|
|
263
284
|
| `/api/channels/whatsapp/disconnect` | POST | Disconnect WhatsApp |
|
|
264
285
|
| `/api/channels/whatsapp/logout` | POST | Disconnect + delete credentials |
|
|
265
|
-
| `/api/channels/whatsapp/configure` | POST | Set mode +
|
|
266
|
-
| `/api/channels/send` | POST | Send message via any channel |
|
|
286
|
+
| `/api/channels/whatsapp/configure` | POST | Set mode + admins array |
|
|
287
|
+
| `/api/channels/send` | POST | Send proactive message via any channel |
|
|
267
288
|
|
|
268
289
|
All endpoints are on `http://localhost:3000`.
|
|
269
290
|
|
|
@@ -14,8 +14,9 @@ import './tour-theme.css';
|
|
|
14
14
|
|
|
15
15
|
const TOUR_KEY = 'fluxy_workspace_tour_done';
|
|
16
16
|
|
|
17
|
-
export default function WorkspaceTour() {
|
|
17
|
+
export default function WorkspaceTour({ disabled = false }: { disabled?: boolean }) {
|
|
18
18
|
useEffect(() => {
|
|
19
|
+
if (disabled) return;
|
|
19
20
|
const val = localStorage.getItem(TOUR_KEY);
|
|
20
21
|
if (val === '1') return;
|
|
21
22
|
|
|
@@ -97,7 +98,7 @@ export default function WorkspaceTour() {
|
|
|
97
98
|
}, 800);
|
|
98
99
|
|
|
99
100
|
return () => clearTimeout(timer);
|
|
100
|
-
}, []);
|
|
101
|
+
}, [disabled]);
|
|
101
102
|
|
|
102
103
|
return null;
|
|
103
104
|
}
|
|
@@ -11,12 +11,15 @@ You are a friendly and helpful assistant responding to a customer via WhatsApp.
|
|
|
11
11
|
- Never reveal internal system details, file paths, or technical architecture.
|
|
12
12
|
- Never run destructive commands or modify critical files during customer interactions.
|
|
13
13
|
|
|
14
|
+
## CRITICAL: Response = Reply
|
|
15
|
+
|
|
16
|
+
Your text response IS the WhatsApp reply. The supervisor sends whatever you respond with directly to the customer. Do NOT use curl or `/api/channels/send` to reply — just respond naturally. That endpoint is only for proactive messages.
|
|
17
|
+
|
|
14
18
|
## What You Can Do
|
|
15
19
|
|
|
16
20
|
- Answer FAQs and general questions
|
|
17
21
|
- Provide information from files in your workspace
|
|
18
22
|
- Look up data from your backend API (`/app/api/*`)
|
|
19
|
-
- Send follow-up messages via the channel API
|
|
20
23
|
|
|
21
24
|
## What You Should NOT Do
|
|
22
25
|
|