neoagent 2.1.18-beta.20 → 2.1.18-beta.21

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.1.18-beta.20",
3
+ "version": "2.1.18-beta.21",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"425cfb54d01a9472b3e81d9e76fd63a4a44cfb
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "3746008503" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "3528621237" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });
@@ -125,6 +125,7 @@ async function processQueuedMessage({
125
125
  queue.running = true;
126
126
  let stopTypingKeepalive = async () => {};
127
127
  try {
128
+ const runId = randomUUID();
128
129
  await messagingManager
129
130
  .markRead(userId, msg.platform, msg.chatId, msg.messageId, { agentId })
130
131
  .catch(() => {});
@@ -132,6 +133,7 @@ async function processQueuedMessage({
132
133
  messagingManager,
133
134
  userId,
134
135
  agentId,
136
+ runId,
135
137
  platform: msg.platform,
136
138
  chatId: msg.chatId
137
139
  });
@@ -139,6 +141,7 @@ async function processQueuedMessage({
139
141
  const prompt = buildIncomingPrompt(msg);
140
142
  const conversationId = ensureConversation(userId, msg);
141
143
  const runOptions = {
144
+ runId,
142
145
  agentId,
143
146
  triggerSource: 'messaging',
144
147
  conversationId,
@@ -180,6 +183,7 @@ function startTypingKeepalive({
180
183
  messagingManager,
181
184
  userId,
182
185
  agentId,
186
+ runId,
183
187
  platform,
184
188
  chatId,
185
189
  intervalMs = 4000
@@ -187,6 +191,26 @@ function startTypingKeepalive({
187
191
  let stopped = false;
188
192
  let timer = null;
189
193
  let releaseWait = null;
194
+ let stopPromise = null;
195
+
196
+ const matchesRunDelivery = (event) => (
197
+ event?.runId
198
+ && runId
199
+ && event.runId === runId
200
+ && event.userId === userId
201
+ && event.platform === platform
202
+ && event.to === chatId
203
+ );
204
+
205
+ const onMessageSent = (event) => {
206
+ if (matchesRunDelivery(event)) {
207
+ stop().catch(() => {});
208
+ }
209
+ };
210
+
211
+ if (typeof messagingManager?.on === 'function' && typeof messagingManager?.off === 'function') {
212
+ messagingManager.on('message_sent', onMessageSent);
213
+ }
190
214
 
191
215
  const wait = () =>
192
216
  new Promise((resolve) => {
@@ -205,21 +229,30 @@ function startTypingKeepalive({
205
229
  }
206
230
  })();
207
231
 
208
- return async () => {
209
- stopped = true;
210
- if (timer) {
211
- clearTimeout(timer);
212
- timer = null;
213
- }
214
- if (releaseWait) {
215
- releaseWait();
216
- releaseWait = null;
217
- }
218
- await loop.catch(() => {});
219
- await messagingManager
220
- .sendTyping(userId, platform, chatId, false, { agentId })
221
- .catch(() => {});
232
+ const stop = async () => {
233
+ if (stopPromise) return stopPromise;
234
+ stopPromise = (async () => {
235
+ if (typeof messagingManager?.off === 'function') {
236
+ messagingManager.off('message_sent', onMessageSent);
237
+ }
238
+ stopped = true;
239
+ if (timer) {
240
+ clearTimeout(timer);
241
+ timer = null;
242
+ }
243
+ if (releaseWait) {
244
+ releaseWait();
245
+ releaseWait = null;
246
+ }
247
+ await loop.catch(() => {});
248
+ await messagingManager
249
+ .sendTyping(userId, platform, chatId, false, { agentId })
250
+ .catch(() => {});
251
+ })();
252
+ return stopPromise;
222
253
  };
254
+
255
+ return stop;
223
256
  }
224
257
 
225
258
  function ensureConversation(userId, msg) {
@@ -331,7 +364,12 @@ async function isAllowedMessagingSender({ io, userId, msg }) {
331
364
  const shouldCheckWhitelist = whitelist.length > 0;
332
365
 
333
366
  if (!shouldCheckWhitelist) {
334
- return true;
367
+ if (!msg.isGroup) return true;
368
+ console.log(
369
+ `[Messaging] Blocked ${msg.platform} group message from ${msg.sender} (no group allowlist configured)`
370
+ );
371
+ emitBlockedSenderSuggestion({ io, userId, msg });
372
+ return false;
335
373
  }
336
374
 
337
375
  const candidates = messagingAllowlistCandidates(msg).map(normalize).filter(Boolean);
@@ -346,6 +384,11 @@ async function isAllowedMessagingSender({ io, userId, msg }) {
346
384
  console.log(
347
385
  `[Messaging] Blocked ${msg.platform} message from ${msg.sender} (not in whitelist)`
348
386
  );
387
+ emitBlockedSenderSuggestion({ io, userId, msg });
388
+ return false;
389
+ }
390
+
391
+ function emitBlockedSenderSuggestion({ io, userId, msg }) {
349
392
  const suggestions = [];
350
393
  if (msg.platform === 'whatsapp') {
351
394
  const normalizedSender = normalizeWhatsAppId(msg.sender || msg.chatId);
@@ -367,7 +410,7 @@ async function isAllowedMessagingSender({ io, userId, msg }) {
367
410
  if (chatId && chatId !== sender) {
368
411
  suggestions.push({
369
412
  label: `Add chat (${chatId})`,
370
- prefixedId: msg.isGroup ? `channel:${chatId}` : chatId
413
+ prefixedId: msg.isGroup ? `group:${chatId}` : chatId
371
414
  });
372
415
  }
373
416
  }
@@ -378,10 +421,10 @@ async function isAllowedMessagingSender({ io, userId, msg }) {
378
421
  senderName: msg.senderName || null,
379
422
  suggestions: suggestions.length > 0 ? suggestions : null
380
423
  });
381
- return false;
382
424
  }
383
425
 
384
426
  module.exports = {
385
427
  isAllowedMessagingSender,
386
- registerMessagingAutomation
428
+ registerMessagingAutomation,
429
+ startTypingKeepalive
387
430
  };
@@ -10,7 +10,7 @@ const {
10
10
 
11
11
  /**
12
12
  * Whitelist entry format (prefixed strings):
13
- * "user:SNOWFLAKE" → always respond, no mention needed (DMs + guild messages)
13
+ * "user:SNOWFLAKE" → allow DMs; allow guild messages only when @mentioned
14
14
  * "guild:SNOWFLAKE" → respond in any channel of this server when @mentioned
15
15
  * "channel:SNOWFLAKE" → respond in this channel when @mentioned
16
16
  * "SNOWFLAKE" → legacy plain ID, treated as "user"
@@ -112,23 +112,25 @@ class DiscordPlatform extends BasePlatform {
112
112
 
113
113
  /** Returns {allowed, requireMention} */
114
114
  _checkAccess(message) {
115
- // Default behavior with no allow-list: respond in DMs and require a mention in guilds.
116
- if (this.allowedEntries.size === 0) {
117
- const isDM = message.channel.type === ChannelType.DM;
118
- return { allowed: true, requireMention: !isDM };
119
- }
120
-
121
- // Check prefixed entries
115
+ const isDM = message.channel.type === ChannelType.DM;
122
116
  const userId = message.author.id;
123
117
  const guildId = message.guildId || null;
124
118
  const channelId = message.channelId;
119
+ const userAllowed = super._checkAccess(`user:${userId}`) || super._checkAccess(userId);
125
120
 
126
- if (super._checkAccess(`user:${userId}`)) return { allowed: true, requireMention: false };
127
- if (super._checkAccess(userId)) return { allowed: true, requireMention: false }; // legacy
128
- if (guildId && super._checkAccess(`guild:${guildId}`)) return { allowed: true, requireMention: true };
129
- if (super._checkAccess(`channel:${channelId}`)) return { allowed: true, requireMention: true };
121
+ if (isDM) {
122
+ return {
123
+ allowed: this.allowedEntries.size === 0 || userAllowed,
124
+ requireMention: false,
125
+ };
126
+ }
130
127
 
131
- return { allowed: false, requireMention: false };
128
+ return {
129
+ allowed: userAllowed
130
+ || (guildId && super._checkAccess(`guild:${guildId}`))
131
+ || super._checkAccess(`channel:${channelId}`),
132
+ requireMention: true,
133
+ };
132
134
  }
133
135
 
134
136
  _isMentioned(message) {
@@ -171,6 +173,8 @@ class DiscordPlatform extends BasePlatform {
171
173
 
172
174
  const { allowed, requireMention } = this._checkAccess(message);
173
175
 
176
+ if (requireMention && !this._isMentioned(message)) return;
177
+
174
178
  if (!allowed) {
175
179
  const suggestions = [
176
180
  { label: `Add user (${message.author.username})`, prefixedId: `user:${userId}` },
@@ -188,9 +192,6 @@ class DiscordPlatform extends BasePlatform {
188
192
  return;
189
193
  }
190
194
 
191
- // guild/channel entries require @mention to activate
192
- if (requireMention && !this._isMentioned(message)) return;
193
-
194
195
  let content = requireMention ? this._stripMention(message.content) : (message.content || '');
195
196
  if (message.attachments.size > 0) {
196
197
  const urls = [...message.attachments.values()].map(a => a.url).join(', ');
@@ -241,8 +242,9 @@ class DiscordPlatform extends BasePlatform {
241
242
  return { success: true };
242
243
  }
243
244
 
244
- async sendTyping(chatId, _isTyping) {
245
+ async sendTyping(chatId, isTyping) {
245
246
  if (!this._client || this.status !== 'connected') return;
247
+ if (!isTyping) return;
246
248
  try {
247
249
  if (chatId.startsWith('dm_')) {
248
250
  const user = await this._client.users.fetch(chatId.slice(3));
@@ -282,19 +282,34 @@ class SlackPlatform extends BasePlatform {
282
282
 
283
283
  const event = body.event || body;
284
284
  if (event.type !== 'message' || event.subtype || !event.text) {
285
+ if (event.type !== 'app_mention') {
286
+ return { handled: true, status: 202, body: 'ignored' };
287
+ }
288
+ }
289
+ if (event.subtype || !event.text) {
285
290
  return { handled: true, status: 202, body: 'ignored' };
286
291
  }
287
292
  if (this._botUserId && event.user === this._botUserId) {
288
293
  return { handled: true, status: 202, body: 'ignored' };
289
294
  }
295
+ const isGroup = String(event.channel_type || '') !== 'im';
296
+ const wasMentioned = event.type === 'app_mention'
297
+ || (this._botUserId && String(event.text || '').includes(`<@${this._botUserId}>`));
298
+ if (isGroup && !wasMentioned) {
299
+ return { handled: true, status: 202, body: 'ignored' };
300
+ }
301
+ const content = this._botUserId
302
+ ? String(event.text).replace(new RegExp(`<@${this._botUserId}>`, 'g'), '').trim()
303
+ : String(event.text);
304
+ if (!content) return { handled: true, status: 202, body: 'ignored' };
290
305
  this.emit('message', {
291
306
  platform: 'slack',
292
307
  chatId: String(event.channel || 'slack'),
293
308
  sender: String(event.user || event.bot_id || 'slack'),
294
309
  senderName: event.username || null,
295
- content: String(event.text),
310
+ content,
296
311
  mediaType: null,
297
- isGroup: String(event.channel_type || '') !== 'im',
312
+ isGroup,
298
313
  messageId: String(event.client_msg_id || event.ts || crypto.randomUUID()),
299
314
  timestamp: event.event_ts ? new Date(Number(event.event_ts) * 1000).toISOString() : new Date().toISOString(),
300
315
  threadTs: event.thread_ts || null,
@@ -431,12 +446,17 @@ class MatrixPlatform extends BasePlatform {
431
446
  if (event.sender && this.userId && event.sender === this.userId) continue;
432
447
  const content = event.content?.body || '';
433
448
  if (!content) continue;
449
+ if (this.userId && !content.includes(this.userId)) continue;
450
+ const cleanContent = this.userId
451
+ ? String(content).replaceAll(this.userId, '').trim()
452
+ : String(content);
453
+ if (!cleanContent) continue;
434
454
  this.emit('message', {
435
455
  platform: 'matrix',
436
456
  chatId: roomId,
437
457
  sender: String(event.sender || roomId),
438
458
  senderName: event.sender || null,
439
- content: String(content),
459
+ content: cleanContent,
440
460
  mediaType: null,
441
461
  isGroup: true,
442
462
  messageId: String(event.event_id || crypto.randomUUID()),
@@ -739,14 +759,23 @@ class IrcPlatform extends BasePlatform {
739
759
  if (!match) continue;
740
760
  const [, nick, target, content] = match;
741
761
  if (nick === this.nick) continue;
762
+ const isGroup = target.startsWith('#');
763
+ if (isGroup) {
764
+ const escaped = this.nick.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
765
+ if (!new RegExp(`(^|\\s)${escaped}[:,]?\\b`, 'i').test(content)) continue;
766
+ }
767
+ const cleanContent = isGroup
768
+ ? content.replace(new RegExp(`(^|\\s)${this.nick.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[:,]?\\s*`, 'i'), ' ').trim()
769
+ : content;
770
+ if (!cleanContent) continue;
742
771
  this.emit('message', {
743
772
  platform: this.name,
744
773
  chatId: target,
745
774
  sender: nick,
746
775
  senderName: nick,
747
- content,
776
+ content: cleanContent,
748
777
  mediaType: null,
749
- isGroup: target.startsWith('#'),
778
+ isGroup,
750
779
  messageId: crypto.randomUUID(),
751
780
  timestamp: new Date().toISOString(),
752
781
  });
@@ -1,3 +1,4 @@
1
+ const EventEmitter = require('events');
1
2
  const db = require('../../db/database');
2
3
  const fs = require('fs');
3
4
  const path = require('path');
@@ -62,8 +63,9 @@ class IMessageMessagingPlatform extends BlueBubblesPlatform {
62
63
  constructor(config = {}) { super('imessage', config); }
63
64
  }
64
65
 
65
- class MessagingManager {
66
+ class MessagingManager extends EventEmitter {
66
67
  constructor(io) {
68
+ super();
67
69
  this.io = io;
68
70
  this.platforms = new Map();
69
71
  this.messageHandlers = [];
@@ -342,6 +344,17 @@ class MessagingManager {
342
344
  runId
343
345
  });
344
346
 
347
+ this.emit('message_sent', {
348
+ userId,
349
+ agentId,
350
+ platform: platformName,
351
+ to,
352
+ content,
353
+ mediaPath,
354
+ runId,
355
+ result
356
+ });
357
+
345
358
  return { success: true, result };
346
359
  }
347
360
 
@@ -100,33 +100,55 @@ class TelegramPlatform extends BasePlatform {
100
100
  const userId = String(msg.from.id);
101
101
  const chatId = String(msg.chat.id);
102
102
  const isPrivate = msg.chat.type === 'private';
103
+ const userAllowed = super._checkAccess(`user:${userId}`) || super._checkAccess(userId);
103
104
 
104
- if (this.allowedEntries.size === 0) return { allowed: true, requireMention: !isPrivate };
105
-
106
- if (super._checkAccess(`user:${userId}`)) return { allowed: true, requireMention: false };
107
- if (super._checkAccess(userId)) return { allowed: true, requireMention: false };
108
- if (super._checkAccess(`group:${chatId}`)) return { allowed: true, requireMention: true };
105
+ if (isPrivate) {
106
+ return {
107
+ allowed: this.allowedEntries.size === 0 || userAllowed,
108
+ requireMention: false,
109
+ };
110
+ }
109
111
 
110
- return { allowed: false, requireMention: false };
112
+ return {
113
+ allowed: userAllowed || super._checkAccess(`group:${chatId}`),
114
+ requireMention: true,
115
+ };
111
116
  }
112
117
 
113
118
  _isMentioned(msg) {
114
119
  if (!this._botUser) return false;
115
120
  const text = msg.text || msg.caption || '';
116
121
  const entities = msg.entities || msg.caption_entities || [];
122
+ const botId = String(this._botUser.id || '');
123
+ const botUsername = String(this._botUser.username || '').toLowerCase();
124
+ const botMention = `@${botUsername}`;
117
125
  for (const e of entities) {
126
+ const value = text.slice(e.offset, e.offset + e.length).toLowerCase();
118
127
  if (e.type === 'mention') {
119
- const mention = text.slice(e.offset, e.offset + e.length);
120
- if (mention.toLowerCase() === `@${this._botUser.username.toLowerCase()}`) return true;
128
+ if (value === botMention) return true;
129
+ }
130
+ if (e.type === 'bot_command') {
131
+ if (value.endsWith(botMention)) return true;
121
132
  }
133
+ if (e.type === 'text_mention' && String(e.user?.id || '') === botId) {
134
+ return true;
135
+ }
136
+ }
137
+ if (msg.reply_to_message?.from && String(msg.reply_to_message.from.id || '') === botId) {
138
+ return true;
139
+ }
140
+ if (botUsername) {
141
+ const escaped = botUsername.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
142
+ if (new RegExp(`(^|\\s)@${escaped}\\b`, 'i').test(text)) return true;
122
143
  }
123
144
  return false;
124
145
  }
125
146
 
126
147
  _stripMention(text) {
127
148
  if (!this._botUser) return (text || '').trim();
149
+ const username = String(this._botUser.username || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
128
150
  return (text || '')
129
- .replace(new RegExp(`@${this._botUser.username}`, 'gi'), '')
151
+ .replace(new RegExp(`@${username}`, 'gi'), '')
130
152
  .replace(/\s{2,}/g, ' ')
131
153
  .trim();
132
154
  }
@@ -168,6 +190,8 @@ class TelegramPlatform extends BasePlatform {
168
190
 
169
191
  const { allowed, requireMention } = this._checkAccess(msg);
170
192
 
193
+ if (requireMention && !this._isMentioned(msg)) return;
194
+
171
195
  if (!allowed) {
172
196
  const suggestions = [
173
197
  { label: `Add user (${senderName})`, prefixedId: `user:${userId}` },
@@ -187,8 +211,6 @@ class TelegramPlatform extends BasePlatform {
187
211
  return;
188
212
  }
189
213
 
190
- if (requireMention && !this._isMentioned(msg)) return;
191
-
192
214
  let content = requireMention ? this._stripMention(text) : text;
193
215
  if (!content && msg.photo) content = `[photo]`;
194
216
  if (!content && msg.document) content = `[document: ${msg.document.file_name || 'file'}]`;
@@ -235,8 +257,9 @@ class TelegramPlatform extends BasePlatform {
235
257
  return { success: true };
236
258
  }
237
259
 
238
- async sendTyping(chatId, _isTyping) {
260
+ async sendTyping(chatId, isTyping) {
239
261
  if (!this._bot || this.status !== 'connected') return;
262
+ if (!isTyping) return;
240
263
  try {
241
264
  const id = chatId.startsWith('dm_') ? chatId.slice(3) : chatId;
242
265
  await this._bot.telegram.sendChatAction(id, 'typing');
@@ -1,7 +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
+ const { normalizeWhatsAppId, toWhatsAppJid } = require('../../utils/whatsapp');
5
5
  const { DATA_DIR } = require('../../../runtime/paths');
6
6
 
7
7
  const AUTH_DIR = path.join(DATA_DIR, 'whatsapp-auth');
@@ -18,6 +18,40 @@ class WhatsAppPlatform extends BasePlatform {
18
18
  this.authDir = config.authDir || AUTH_DIR;
19
19
  }
20
20
 
21
+ _ownIds() {
22
+ return new Set([
23
+ this.sock?.user?.id,
24
+ this.sock?.user?.jid,
25
+ ]
26
+ .map(normalizeWhatsAppId)
27
+ .filter(Boolean));
28
+ }
29
+
30
+ _contextInfo(message = {}) {
31
+ return message.extendedTextMessage?.contextInfo
32
+ || message.imageMessage?.contextInfo
33
+ || message.videoMessage?.contextInfo
34
+ || message.documentMessage?.contextInfo
35
+ || message.audioMessage?.contextInfo
36
+ || message.conversation?.contextInfo
37
+ || null;
38
+ }
39
+
40
+ _isGroupAddressedToBot(message = {}) {
41
+ const ownIds = this._ownIds();
42
+ if (ownIds.size === 0) return false;
43
+ const contextInfo = this._contextInfo(message);
44
+ const mentions = Array.isArray(contextInfo?.mentionedJid) ? contextInfo.mentionedJid : [];
45
+ if (mentions.some((jid) => ownIds.has(normalizeWhatsAppId(jid)))) return true;
46
+ if (ownIds.has(normalizeWhatsAppId(contextInfo?.participant))) return true;
47
+ const text = message.conversation
48
+ || message.extendedTextMessage?.text
49
+ || message.imageMessage?.caption
50
+ || message.videoMessage?.caption
51
+ || '';
52
+ return [...ownIds].some((id) => text.includes(`@${id}`));
53
+ }
54
+
21
55
  async connect() {
22
56
  if (!fs.existsSync(this.authDir)) fs.mkdirSync(this.authDir, { recursive: true });
23
57
 
@@ -133,6 +167,7 @@ class WhatsAppPlatform extends BasePlatform {
133
167
  }
134
168
 
135
169
  if (!content && !mediaType) continue;
170
+ if (isGroup && !this._isGroupAddressedToBot(msg.message || {})) continue;
136
171
 
137
172
  let localMediaPath = null;
138
173
  if (mediaType && mediaType !== 'sticker') {