neoagent 1.1.0 → 1.1.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/lib/manager.js +11 -1
- package/package.json +1 -1
- package/server/index.js +17 -268
- package/server/public/app.html +10 -0
- package/server/public/css/styles.css +64 -0
- package/server/public/js/app.js +53 -5
- package/server/routes/memory.js +1 -1
- package/server/routes/settings.js +78 -4
- package/server/routes/telnyx.js +35 -0
- package/server/services/ai/compaction.js +3 -2
- package/server/services/ai/engine.js +7 -1176
- package/server/services/ai/systemPrompt.js +113 -0
- package/server/services/ai/tools.js +1096 -0
- package/server/services/manager.js +172 -0
- package/server/services/messaging/base.js +12 -0
- package/server/services/messaging/discord.js +12 -18
- package/server/services/messaging/telegram.js +41 -47
- package/server/services/messaging/whatsapp.js +7 -8
- package/server/utils/logger.js +46 -0
- package/server/utils/whatsapp.js +46 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const db = require('../db/database');
|
|
4
|
+
const { MemoryManager } = require('./memory/manager');
|
|
5
|
+
const { MCPClient } = require('./mcp/client');
|
|
6
|
+
const { BrowserController } = require('./browser/controller');
|
|
7
|
+
const { AgentEngine } = require('./ai/engine');
|
|
8
|
+
const { MultiStepOrchestrator } = require('./ai/multiStep');
|
|
9
|
+
const { MessagingManager } = require('./messaging/manager');
|
|
10
|
+
const { Scheduler } = require('./scheduler/cron');
|
|
11
|
+
const { setupWebSocket } = require('./websocket');
|
|
12
|
+
const { detectPromptInjection } = require('../utils/security');
|
|
13
|
+
const { normalizeWhatsAppId } = require('../utils/whatsapp');
|
|
14
|
+
const { randomUUID } = require('crypto');
|
|
15
|
+
|
|
16
|
+
async function startServices(app, io) {
|
|
17
|
+
try {
|
|
18
|
+
const memoryManager = new MemoryManager();
|
|
19
|
+
app.locals.memoryManager = memoryManager;
|
|
20
|
+
|
|
21
|
+
const mcpClient = new MCPClient();
|
|
22
|
+
app.locals.mcpClient = mcpClient;
|
|
23
|
+
|
|
24
|
+
const browserController = new BrowserController();
|
|
25
|
+
const headlessSetting = db.prepare('SELECT value FROM user_settings WHERE key = ? ORDER BY user_id LIMIT 1').get('headless_browser');
|
|
26
|
+
if (headlessSetting) {
|
|
27
|
+
const val = headlessSetting.value;
|
|
28
|
+
browserController.headless = val !== 'false' && val !== false && val !== '0';
|
|
29
|
+
}
|
|
30
|
+
app.locals.browserController = browserController;
|
|
31
|
+
|
|
32
|
+
const agentEngine = new AgentEngine(io, { memoryManager, mcpClient, browserController, messagingManager: null });
|
|
33
|
+
app.locals.agentEngine = agentEngine;
|
|
34
|
+
|
|
35
|
+
const multiStep = new MultiStepOrchestrator(agentEngine, io);
|
|
36
|
+
app.locals.multiStep = multiStep;
|
|
37
|
+
|
|
38
|
+
const messagingManager = new MessagingManager(io);
|
|
39
|
+
app.locals.messagingManager = messagingManager;
|
|
40
|
+
agentEngine.messagingManager = messagingManager;
|
|
41
|
+
|
|
42
|
+
messagingManager.restoreConnections().catch(err => console.error('[Messaging] Restore error:', err.message));
|
|
43
|
+
|
|
44
|
+
const users = db.prepare('SELECT id FROM users').all();
|
|
45
|
+
for (const u of users) {
|
|
46
|
+
mcpClient.loadFromDB(u.id).catch(err => console.error('[MCP] Auto-start error:', err.message));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const userQueues = {};
|
|
50
|
+
app.locals.userQueues = userQueues;
|
|
51
|
+
|
|
52
|
+
async function processMessage(userId, msg) {
|
|
53
|
+
if (!userQueues[userId]) userQueues[userId] = { running: false, pending: [] };
|
|
54
|
+
const q = userQueues[userId];
|
|
55
|
+
|
|
56
|
+
if (q.running) {
|
|
57
|
+
const last = q.pending[q.pending.length - 1];
|
|
58
|
+
if (last && last.platform === msg.platform && last.chatId === msg.chatId) {
|
|
59
|
+
last.content += '\n' + msg.content;
|
|
60
|
+
last.messageId = msg.messageId;
|
|
61
|
+
} else {
|
|
62
|
+
q.pending.push({ ...msg });
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
q.running = true;
|
|
68
|
+
try {
|
|
69
|
+
await messagingManager.markRead(userId, msg.platform, msg.chatId, msg.messageId).catch(() => { });
|
|
70
|
+
await messagingManager.sendTyping(userId, msg.platform, msg.chatId, true).catch(() => { });
|
|
71
|
+
|
|
72
|
+
const mediaNote = msg.localMediaPath
|
|
73
|
+
? `\nMedia attached at: ${msg.localMediaPath} (type: ${msg.mediaType}). You can reference or forward it with send_message media_path.`
|
|
74
|
+
: '';
|
|
75
|
+
|
|
76
|
+
if (detectPromptInjection(msg.content)) {
|
|
77
|
+
console.warn(`[Security] Possible prompt injection attempt from ${msg.sender} on ${msg.platform}: ${msg.content.slice(0, 200)}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const isVoiceCall = msg.platform === 'telnyx' && msg.mediaType === 'voice';
|
|
81
|
+
const isVoiceNote = !isVoiceCall && msg.mediaType === 'audio';
|
|
82
|
+
const isDiscordGuild = msg.platform === 'discord' && msg.isGroup;
|
|
83
|
+
|
|
84
|
+
const discordContext = (isDiscordGuild && Array.isArray(msg.channelContext) && msg.channelContext.length)
|
|
85
|
+
? '\n\nRecent channel context (oldest → newest):\n' +
|
|
86
|
+
msg.channelContext.map(m => `[${m.author}]: ${m.content}`).join('\n')
|
|
87
|
+
: '';
|
|
88
|
+
|
|
89
|
+
const sttNote = isVoiceNote
|
|
90
|
+
? '\n[Note: This message was sent as a voice note and transcribed via speech-to-text. The transcription may not be perfectly accurate.]'
|
|
91
|
+
: '';
|
|
92
|
+
|
|
93
|
+
const prompt = isVoiceCall
|
|
94
|
+
? `You are on a live phone call. The caller (${msg.senderName || msg.sender}) said:\n<caller_speech>\n${msg.content}\n</caller_speech>\n\nRespond via send_message with platform="telnyx" and to="${msg.chatId}".`
|
|
95
|
+
: `You received a ${msg.platform} message from ${msg.senderName || msg.sender} (chat: ${msg.chatId}):\n<external_message>\n${msg.content}\n</external_message>${mediaNote}${discordContext}${sttNote}\n\nReply via send_message with platform="${msg.platform}" and to="${msg.chatId}".`;
|
|
96
|
+
|
|
97
|
+
let convRow = db.prepare(
|
|
98
|
+
'SELECT id FROM conversations WHERE user_id = ? AND platform = ? AND platform_chat_id = ?'
|
|
99
|
+
).get(userId, msg.platform, msg.chatId);
|
|
100
|
+
|
|
101
|
+
if (!convRow) {
|
|
102
|
+
const convId = randomUUID();
|
|
103
|
+
db.prepare(
|
|
104
|
+
'INSERT INTO conversations (id, user_id, platform, platform_chat_id, title) VALUES (?, ?, ?, ?, ?)'
|
|
105
|
+
).run(convId, userId, msg.platform, msg.chatId, `${msg.platform} — ${msg.senderName || msg.sender || msg.chatId}`);
|
|
106
|
+
convRow = { id: convId };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const runOpts = { triggerSource: 'messaging', conversationId: convRow.id, source: msg.platform, chatId: msg.chatId, context: { rawUserMessage: msg.content } };
|
|
110
|
+
if (msg.localMediaPath) runOpts.mediaAttachments = [{ path: msg.localMediaPath, type: msg.mediaType }];
|
|
111
|
+
|
|
112
|
+
await agentEngine.run(userId, prompt, runOpts);
|
|
113
|
+
} finally {
|
|
114
|
+
await messagingManager.sendTyping(userId, msg.platform, msg.chatId, false).catch(() => { });
|
|
115
|
+
q.running = false;
|
|
116
|
+
if (q.pending.length > 0) {
|
|
117
|
+
const next = q.pending.shift();
|
|
118
|
+
processMessage(userId, next);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
messagingManager.registerHandler(async (userId, msg) => {
|
|
124
|
+
if (msg.platform !== 'discord' && msg.platform !== 'telegram') {
|
|
125
|
+
const whitelistRow = db.prepare('SELECT value FROM user_settings WHERE user_id = ? AND key = ?')
|
|
126
|
+
.get(userId, `platform_whitelist_${msg.platform}`);
|
|
127
|
+
if (whitelistRow) {
|
|
128
|
+
try {
|
|
129
|
+
const whitelist = JSON.parse(whitelistRow.value);
|
|
130
|
+
if (Array.isArray(whitelist) && whitelist.length > 0) {
|
|
131
|
+
const normalize = msg.platform === 'whatsapp'
|
|
132
|
+
? normalizeWhatsAppId
|
|
133
|
+
: (id) => String(id || '').replace(/[^0-9+]/g, '');
|
|
134
|
+
const senderNorm = normalize(msg.sender || msg.chatId);
|
|
135
|
+
const allowed = whitelist.some((n) => normalize(n) === senderNorm);
|
|
136
|
+
if (!allowed) {
|
|
137
|
+
console.log(`[Messaging] Blocked ${msg.platform} message from ${msg.sender} (not in whitelist)`);
|
|
138
|
+
io.to(`user:${userId}`).emit('messaging:blocked_sender', {
|
|
139
|
+
platform: msg.platform,
|
|
140
|
+
sender: msg.sender,
|
|
141
|
+
chatId: msg.chatId,
|
|
142
|
+
senderName: msg.senderName || null
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch { }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const upsertSetting = db.prepare('INSERT OR REPLACE INTO user_settings (user_id, key, value) VALUES (?, ?, ?)');
|
|
152
|
+
upsertSetting.run(userId, 'last_platform', msg.platform);
|
|
153
|
+
upsertSetting.run(userId, 'last_chat_id', msg.chatId);
|
|
154
|
+
|
|
155
|
+
await processMessage(userId, msg);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const scheduler = new Scheduler(io, agentEngine);
|
|
159
|
+
app.locals.scheduler = scheduler;
|
|
160
|
+
agentEngine.scheduler = scheduler;
|
|
161
|
+
scheduler.start();
|
|
162
|
+
|
|
163
|
+
setupWebSocket(io, { agentEngine, messagingManager, mcpClient, scheduler, memoryManager, app });
|
|
164
|
+
app.locals.io = io;
|
|
165
|
+
|
|
166
|
+
console.log('All services initialized');
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error('Service init error:', err);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { startServices };
|
|
@@ -9,6 +9,18 @@ class BasePlatform extends EventEmitter {
|
|
|
9
9
|
this.supportsGroups = false;
|
|
10
10
|
this.supportsMedia = false;
|
|
11
11
|
this.supportsVoice = false;
|
|
12
|
+
this.allowedEntries = new Set();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
setAllowedEntries(entries) {
|
|
16
|
+
if (Array.isArray(entries)) {
|
|
17
|
+
this.allowedEntries = new Set(entries.map(String));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_checkAccess(id) {
|
|
22
|
+
if (this.allowedEntries.size === 0) return true;
|
|
23
|
+
return this.allowedEntries.has(String(id));
|
|
12
24
|
}
|
|
13
25
|
|
|
14
26
|
async connect() { throw new Error('connect() not implemented'); }
|
|
@@ -26,7 +26,9 @@ class DiscordPlatform extends BasePlatform {
|
|
|
26
26
|
this.supportsMedia = false;
|
|
27
27
|
|
|
28
28
|
this.token = config.token || '';
|
|
29
|
-
|
|
29
|
+
if (Array.isArray(config.allowedIds)) {
|
|
30
|
+
this.setAllowedEntries(config.allowedIds);
|
|
31
|
+
}
|
|
30
32
|
|
|
31
33
|
this._client = null;
|
|
32
34
|
this._botUser = null;
|
|
@@ -82,31 +84,23 @@ class DiscordPlatform extends BasePlatform {
|
|
|
82
84
|
|
|
83
85
|
// ── Whitelist ──────────────────────────────────────────────────────────────
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
setAllowedEntries(entries) {
|
|
87
|
-
this.allowedEntries = Array.isArray(entries) ? entries : [];
|
|
88
|
-
console.log(`[Discord] Whitelist updated: ${this.allowedEntries.length} entry(ies)`);
|
|
89
|
-
}
|
|
87
|
+
// Inherits setAllowedEntries from BasePlatform
|
|
90
88
|
|
|
91
89
|
/** Returns {allowed, requireMention} */
|
|
92
90
|
_checkAccess(message) {
|
|
93
|
-
|
|
91
|
+
// Empty whitelist: block everyone (add via the allow popup)
|
|
92
|
+
if (this.allowedEntries.size === 0) return { allowed: false, requireMention: false };
|
|
93
|
+
|
|
94
|
+
// Check prefixed entries
|
|
94
95
|
const userId = message.author.id;
|
|
95
96
|
const guildId = message.guildId || null;
|
|
96
97
|
const channelId = message.channelId;
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const colon = entry.indexOf(':');
|
|
103
|
-
const type = colon > 0 ? entry.slice(0, colon) : 'user';
|
|
104
|
-
const id = colon > 0 ? entry.slice(colon + 1) : entry;
|
|
99
|
+
if (super._checkAccess(`user:${userId}`)) return { allowed: true, requireMention: false };
|
|
100
|
+
if (super._checkAccess(userId)) return { allowed: true, requireMention: false }; // legacy
|
|
101
|
+
if (guildId && super._checkAccess(`guild:${guildId}`)) return { allowed: true, requireMention: true };
|
|
102
|
+
if (super._checkAccess(`channel:${channelId}`)) return { allowed: true, requireMention: true };
|
|
105
103
|
|
|
106
|
-
if (type === 'user' && id === userId) return { allowed: true, requireMention: false };
|
|
107
|
-
if (type === 'guild' && id === guildId) return { allowed: true, requireMention: true };
|
|
108
|
-
if (type === 'channel' && id === channelId) return { allowed: true, requireMention: true };
|
|
109
|
-
}
|
|
110
104
|
return { allowed: false, requireMention: false };
|
|
111
105
|
}
|
|
112
106
|
|
|
@@ -19,13 +19,15 @@ class TelegramPlatform extends BasePlatform {
|
|
|
19
19
|
constructor(config = {}) {
|
|
20
20
|
super('telegram', config);
|
|
21
21
|
this.supportsGroups = true;
|
|
22
|
-
this.supportsMedia
|
|
22
|
+
this.supportsMedia = false;
|
|
23
23
|
|
|
24
|
-
this.botToken
|
|
25
|
-
|
|
24
|
+
this.botToken = config.botToken || '';
|
|
25
|
+
if (Array.isArray(config.allowedIds)) {
|
|
26
|
+
this.setAllowedEntries(config.allowedIds);
|
|
27
|
+
}
|
|
26
28
|
|
|
27
|
-
this._bot
|
|
28
|
-
this._botUser
|
|
29
|
+
this._bot = null;
|
|
30
|
+
this._botUser = null;
|
|
29
31
|
// In-memory ring buffer of recent messages per raw chatId (Telegram has no fetch history API for bots)
|
|
30
32
|
this._contextBuffers = new Map();
|
|
31
33
|
this._contextMaxSize = 25;
|
|
@@ -36,7 +38,7 @@ class TelegramPlatform extends BasePlatform {
|
|
|
36
38
|
async connect() {
|
|
37
39
|
if (!this.botToken) throw new Error('Telegram bot token is required');
|
|
38
40
|
|
|
39
|
-
if (this._bot) { try { await this._bot.stopPolling(); } catch {} this._bot = null; }
|
|
41
|
+
if (this._bot) { try { await this._bot.stopPolling(); } catch { } this._bot = null; }
|
|
40
42
|
|
|
41
43
|
this._bot = new TelegramBot(this.botToken, { polling: true });
|
|
42
44
|
|
|
@@ -66,45 +68,37 @@ class TelegramPlatform extends BasePlatform {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
async disconnect() {
|
|
69
|
-
if (this._bot) { try { await this._bot.stopPolling(); } catch {} this._bot = null; }
|
|
70
|
-
this.status
|
|
71
|
+
if (this._bot) { try { await this._bot.stopPolling(); } catch { } this._bot = null; }
|
|
72
|
+
this.status = 'disconnected';
|
|
71
73
|
this._botUser = null;
|
|
72
74
|
this.emit('disconnected', { manual: true });
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
async logout()
|
|
76
|
-
getStatus()
|
|
77
|
-
getAuthInfo()
|
|
77
|
+
async logout() { await this.disconnect(); }
|
|
78
|
+
getStatus() { return this.status; }
|
|
79
|
+
getAuthInfo() { return this._botUser ? { username: this._botUser.username, id: this._botUser.id } : null; }
|
|
78
80
|
|
|
79
81
|
// ── Whitelist ──────────────────────────────────────────────────────────────
|
|
80
82
|
|
|
81
|
-
setAllowedEntries
|
|
82
|
-
this.allowedEntries = Array.isArray(entries) ? entries : [];
|
|
83
|
-
console.log(`[Telegram] Whitelist updated: ${this.allowedEntries.length} entry(ies)`);
|
|
84
|
-
}
|
|
83
|
+
// Inherits setAllowedEntries from BasePlatform
|
|
85
84
|
|
|
86
85
|
/** Returns {allowed, requireMention} */
|
|
87
86
|
_checkAccess(msg) {
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const chatId = String(msg.chat.id); // negative for groups
|
|
87
|
+
const userId = String(msg.from.id);
|
|
88
|
+
const chatId = String(msg.chat.id); // negative for groups
|
|
91
89
|
|
|
92
|
-
if (
|
|
90
|
+
if (this.allowedEntries.size === 0) return { allowed: false, requireMention: false };
|
|
93
91
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const id = colon > 0 ? entry.slice(colon + 1) : entry;
|
|
92
|
+
if (super._checkAccess(`user:${userId}`)) return { allowed: true, requireMention: false };
|
|
93
|
+
if (super._checkAccess(userId)) return { allowed: true, requireMention: false }; // legacy
|
|
94
|
+
if (super._checkAccess(`group:${chatId}`)) return { allowed: true, requireMention: true };
|
|
98
95
|
|
|
99
|
-
if (type === 'user' && id === userId) return { allowed: true, requireMention: false };
|
|
100
|
-
if (type === 'group' && id === chatId) return { allowed: true, requireMention: true };
|
|
101
|
-
}
|
|
102
96
|
return { allowed: false, requireMention: false };
|
|
103
97
|
}
|
|
104
98
|
|
|
105
99
|
_isMentioned(msg) {
|
|
106
100
|
if (!this._botUser) return false;
|
|
107
|
-
const text
|
|
101
|
+
const text = msg.text || msg.caption || '';
|
|
108
102
|
const entities = msg.entities || msg.caption_entities || [];
|
|
109
103
|
for (const e of entities) {
|
|
110
104
|
if (e.type === 'mention') {
|
|
@@ -141,9 +135,9 @@ class TelegramPlatform extends BasePlatform {
|
|
|
141
135
|
async _handleMessage(msg) {
|
|
142
136
|
if (!msg.from || msg.from.is_bot) return;
|
|
143
137
|
|
|
144
|
-
const isPrivate
|
|
145
|
-
const userId
|
|
146
|
-
const rawChatId
|
|
138
|
+
const isPrivate = msg.chat.type === 'private';
|
|
139
|
+
const userId = String(msg.from.id);
|
|
140
|
+
const rawChatId = String(msg.chat.id);
|
|
147
141
|
const outputChatId = isPrivate ? `dm_${userId}` : rawChatId;
|
|
148
142
|
|
|
149
143
|
const text = msg.text || msg.caption || '';
|
|
@@ -152,9 +146,9 @@ class TelegramPlatform extends BasePlatform {
|
|
|
152
146
|
|
|
153
147
|
// Always record into context buffer (even blocked messages add context)
|
|
154
148
|
this._addToContext(rawChatId, {
|
|
155
|
-
author:
|
|
149
|
+
author: senderName,
|
|
156
150
|
content: text || (msg.photo ? '[photo]' : msg.document ? '[document]' : '[empty]'),
|
|
157
|
-
mine:
|
|
151
|
+
mine: false,
|
|
158
152
|
});
|
|
159
153
|
|
|
160
154
|
const { allowed, requireMention } = this._checkAccess(msg);
|
|
@@ -169,8 +163,8 @@ class TelegramPlatform extends BasePlatform {
|
|
|
169
163
|
});
|
|
170
164
|
|
|
171
165
|
this.emit('blocked_sender', {
|
|
172
|
-
sender:
|
|
173
|
-
chatId:
|
|
166
|
+
sender: userId,
|
|
167
|
+
chatId: outputChatId,
|
|
174
168
|
senderName,
|
|
175
169
|
groupName: msg.chat.title || null,
|
|
176
170
|
suggestions,
|
|
@@ -182,7 +176,7 @@ class TelegramPlatform extends BasePlatform {
|
|
|
182
176
|
if (requireMention && !this._isMentioned(msg)) return;
|
|
183
177
|
|
|
184
178
|
let content = requireMention ? this._stripMention(text) : text;
|
|
185
|
-
if (!content && msg.photo)
|
|
179
|
+
if (!content && msg.photo) content = `[photo]`;
|
|
186
180
|
if (!content && msg.document) content = `[document: ${msg.document.file_name || 'file'}]`;
|
|
187
181
|
if (!content) return;
|
|
188
182
|
|
|
@@ -193,18 +187,18 @@ class TelegramPlatform extends BasePlatform {
|
|
|
193
187
|
const channelContext = (!isPrivate && requireMention) ? this._getContext(rawChatId) : null;
|
|
194
188
|
|
|
195
189
|
this.emit('message', {
|
|
196
|
-
platform:
|
|
197
|
-
chatId:
|
|
198
|
-
sender:
|
|
199
|
-
senderName:
|
|
190
|
+
platform: 'telegram',
|
|
191
|
+
chatId: outputChatId,
|
|
192
|
+
sender: userId,
|
|
193
|
+
senderName: fullSenderName,
|
|
200
194
|
content,
|
|
201
|
-
mediaType:
|
|
202
|
-
isGroup:
|
|
203
|
-
messageId:
|
|
204
|
-
timestamp:
|
|
195
|
+
mediaType: null,
|
|
196
|
+
isGroup: !isPrivate,
|
|
197
|
+
messageId: String(msg.message_id),
|
|
198
|
+
timestamp: new Date(msg.date * 1000).toISOString(),
|
|
205
199
|
channelContext,
|
|
206
|
-
channelName:
|
|
207
|
-
groupName:
|
|
200
|
+
channelName: isPrivate ? null : (msg.chat.title || rawChatId),
|
|
201
|
+
groupName: msg.chat.title || null,
|
|
208
202
|
});
|
|
209
203
|
}
|
|
210
204
|
|
|
@@ -222,9 +216,9 @@ class TelegramPlatform extends BasePlatform {
|
|
|
222
216
|
// Store outgoing message in context buffer
|
|
223
217
|
if (this._botUser) {
|
|
224
218
|
this._addToContext(telegramChatId, {
|
|
225
|
-
author:
|
|
219
|
+
author: `[bot] ${this._botUser.username}`,
|
|
226
220
|
content,
|
|
227
|
-
mine:
|
|
221
|
+
mine: true,
|
|
228
222
|
});
|
|
229
223
|
}
|
|
230
224
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { BasePlatform } = require('./base');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const { toWhatsAppJid } = require('../../utils/whatsapp');
|
|
4
5
|
|
|
5
6
|
const AUTH_DIR = path.join(__dirname, '..', '..', '..', 'data', 'whatsapp-auth');
|
|
6
7
|
|
|
@@ -208,10 +209,8 @@ class WhatsAppPlatform extends BasePlatform {
|
|
|
208
209
|
throw new Error('WhatsApp not connected');
|
|
209
210
|
}
|
|
210
211
|
|
|
211
|
-
|
|
212
|
-
if (!jid
|
|
213
|
-
jid = jid.replace(/[^0-9]/g, '') + '@s.whatsapp.net';
|
|
214
|
-
}
|
|
212
|
+
const jid = toWhatsAppJid(to);
|
|
213
|
+
if (!jid) throw new Error('Invalid WhatsApp recipient');
|
|
215
214
|
|
|
216
215
|
if (options.mediaPath) {
|
|
217
216
|
const ext = path.extname(options.mediaPath).toLowerCase();
|
|
@@ -244,16 +243,16 @@ class WhatsAppPlatform extends BasePlatform {
|
|
|
244
243
|
|
|
245
244
|
async markRead(chatId, messageId) {
|
|
246
245
|
if (!this.sock) return;
|
|
247
|
-
|
|
248
|
-
if (!jid
|
|
246
|
+
const jid = toWhatsAppJid(chatId);
|
|
247
|
+
if (!jid) return;
|
|
249
248
|
// readMessages expects full message keys; we do a best-effort read
|
|
250
249
|
await this.sock.sendReadReceipt(jid, null, [messageId]).catch(() => { });
|
|
251
250
|
}
|
|
252
251
|
|
|
253
252
|
async sendTyping(chatId, isTyping) {
|
|
254
253
|
if (!this.sock || this.status !== 'connected') return;
|
|
255
|
-
|
|
256
|
-
if (!jid
|
|
254
|
+
const jid = toWhatsAppJid(chatId);
|
|
255
|
+
if (!jid) return;
|
|
257
256
|
await this.sock.sendPresenceUpdate(isTyping ? 'composing' : 'paused', jid).catch(() => { });
|
|
258
257
|
}
|
|
259
258
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Intercepts console methods and broadcasts logs via Socket.IO
|
|
5
|
+
* @param {import('socket.io').Server} io
|
|
6
|
+
*/
|
|
7
|
+
function setupConsoleInterceptor(io) {
|
|
8
|
+
const logHistory = [];
|
|
9
|
+
const MAX_LOG_HISTORY = 200;
|
|
10
|
+
|
|
11
|
+
function broadcastLog(type, args) {
|
|
12
|
+
const msg = Array.from(args).map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ');
|
|
13
|
+
const logEntry = { type, message: msg, timestamp: new Date().toISOString() };
|
|
14
|
+
logHistory.push(logEntry);
|
|
15
|
+
if (logHistory.length > MAX_LOG_HISTORY) logHistory.shift();
|
|
16
|
+
|
|
17
|
+
// Broadcast only to authenticated user rooms
|
|
18
|
+
for (const [, socket] of io.sockets.sockets) {
|
|
19
|
+
const uid = socket.request?.session?.userId;
|
|
20
|
+
if (uid) socket.emit('server:log', logEntry);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const originalConsole = {
|
|
25
|
+
log: console.log,
|
|
26
|
+
error: console.error,
|
|
27
|
+
warn: console.warn,
|
|
28
|
+
info: console.info
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
console.log = function (...args) { originalConsole.log.apply(console, args); broadcastLog('log', args); };
|
|
32
|
+
console.error = function (...args) { originalConsole.error.apply(console, args); broadcastLog('error', args); };
|
|
33
|
+
console.warn = function (...args) { originalConsole.warn.apply(console, args); broadcastLog('warn', args); };
|
|
34
|
+
console.info = function (...args) { originalConsole.info.apply(console, args); broadcastLog('info', args); };
|
|
35
|
+
|
|
36
|
+
io.on('connection', (socket) => {
|
|
37
|
+
socket.on('client:request_logs', () => {
|
|
38
|
+
if (!socket.request?.session?.userId) return;
|
|
39
|
+
socket.emit('server:log_history', logHistory);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return logHistory;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { setupConsoleInterceptor };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function normalizeWhatsAppId(value) {
|
|
2
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
3
|
+
if (!raw) return '';
|
|
4
|
+
|
|
5
|
+
const base = raw.includes('@') ? raw.split('@')[0] : raw;
|
|
6
|
+
const primary = base.includes(':') ? base.split(':')[0] : base;
|
|
7
|
+
const digits = primary.replace(/\D/g, '');
|
|
8
|
+
if (digits) return digits;
|
|
9
|
+
|
|
10
|
+
return primary;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeWhatsAppWhitelist(values) {
|
|
14
|
+
if (!Array.isArray(values)) return [];
|
|
15
|
+
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
const normalized = [];
|
|
18
|
+
for (const value of values) {
|
|
19
|
+
const entry = normalizeWhatsAppId(value);
|
|
20
|
+
if (!entry || seen.has(entry)) continue;
|
|
21
|
+
seen.add(entry);
|
|
22
|
+
normalized.push(entry);
|
|
23
|
+
}
|
|
24
|
+
return normalized;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toWhatsAppJid(value) {
|
|
28
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
29
|
+
if (!raw) return '';
|
|
30
|
+
if (raw.includes('@')) {
|
|
31
|
+
const jid = raw.split(':')[0];
|
|
32
|
+
if (jid.endsWith('@s.whatsapp.net') || jid.endsWith('@g.us') || jid.endsWith('@lid')) {
|
|
33
|
+
return jid;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const normalized = normalizeWhatsAppId(raw);
|
|
38
|
+
if (!normalized) return '';
|
|
39
|
+
return `${normalized}@s.whatsapp.net`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
normalizeWhatsAppId,
|
|
44
|
+
normalizeWhatsAppWhitelist,
|
|
45
|
+
toWhatsAppJid,
|
|
46
|
+
};
|