neoagent 3.1.1-beta.2 → 3.1.1-beta.3

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.
@@ -0,0 +1,260 @@
1
+ 'use strict';
2
+
3
+ const db = require('../../db/database');
4
+ const { resolveAgentId } = require('../agents/manager');
5
+
6
+ const DISCOVERABLE_PLATFORMS = Object.freeze([
7
+ 'whatsapp',
8
+ 'discord',
9
+ 'telegram',
10
+ 'slack',
11
+ ]);
12
+
13
+ const SOURCE_RANK = Object.freeze({
14
+ discovered: 0,
15
+ live: 0,
16
+ default: 1,
17
+ recent: 2,
18
+ manual: 3,
19
+ });
20
+
21
+ const PLATFORM_LABELS = Object.freeze({
22
+ whatsapp: 'WhatsApp',
23
+ discord: 'Discord',
24
+ telegram: 'Telegram',
25
+ slack: 'Slack',
26
+ });
27
+
28
+ const MAX_TARGETS = 80;
29
+
30
+ function normalizeText(value) {
31
+ return String(value || '').trim();
32
+ }
33
+
34
+ function platformLabel(platform) {
35
+ return PLATFORM_LABELS[platform] || platform.replace(/_/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase());
36
+ }
37
+
38
+ function normalizeSource(source) {
39
+ const normalized = normalizeText(source).toLowerCase();
40
+ if (normalized === 'live') return 'discovered';
41
+ if (SOURCE_RANK[normalized] != null) return normalized;
42
+ return 'discovered';
43
+ }
44
+
45
+ function normalizeTarget(input = {}, fallback = {}) {
46
+ const platform = normalizeText(input.platform || fallback.platform).toLowerCase();
47
+ const to = normalizeText(input.to || input.value || fallback.to);
48
+ if (!platform || !to) return null;
49
+ const label = normalizeText(input.label) || to;
50
+ const source = normalizeSource(input.source || fallback.source);
51
+ return {
52
+ platform,
53
+ platformLabel: platformLabel(platform),
54
+ to,
55
+ label,
56
+ subtitle: normalizeText(input.subtitle) || platformLabel(platform),
57
+ source,
58
+ connected: input.connected !== false,
59
+ supportsDelivery: input.supportsDelivery !== false,
60
+ };
61
+ }
62
+
63
+ function metadataFromRow(row) {
64
+ try {
65
+ return row?.metadata ? JSON.parse(row.metadata) : {};
66
+ } catch {
67
+ return {};
68
+ }
69
+ }
70
+
71
+ function parseSettingValue(value) {
72
+ try {
73
+ const parsed = JSON.parse(value || '""');
74
+ return normalizeText(parsed);
75
+ } catch {
76
+ return normalizeText(value);
77
+ }
78
+ }
79
+
80
+ function labelFromMetadata(platform, row) {
81
+ const metadata = metadataFromRow(row);
82
+ return normalizeText(
83
+ metadata.groupName
84
+ || metadata.group_name
85
+ || metadata.channelName
86
+ || metadata.channel_name
87
+ || metadata.guildName
88
+ || metadata.guild_name
89
+ || metadata.senderName
90
+ || metadata.sender_name
91
+ || row.platform_chat_id
92
+ || '',
93
+ ) || normalizeText(row.platform_chat_id);
94
+ }
95
+
96
+ class TaskDeliveryTargetService {
97
+ constructor(options = {}) {
98
+ this.app = options.app || null;
99
+ this.db = options.db || db;
100
+ }
101
+
102
+ async listTargets(userId, options = {}) {
103
+ const agentId = resolveAgentId(userId, options.agentId || options.agent_id || null);
104
+ const platformFilter = normalizeText(options.platform).toLowerCase();
105
+ const query = normalizeText(options.q || options.query).toLowerCase();
106
+ const discovered = await this._discoverTargets(userId, agentId, platformFilter);
107
+ const defaults = this._defaultTargets(userId, agentId, platformFilter);
108
+ const recent = this._recentTargets(userId, agentId, platformFilter);
109
+ return this._filterAndSort([...discovered, ...defaults, ...recent], query);
110
+ }
111
+
112
+ async _discoverTargets(userId, agentId, platformFilter) {
113
+ const platforms = platformFilter ? [platformFilter] : DISCOVERABLE_PLATFORMS;
114
+ const results = [];
115
+ for (const platform of platforms) {
116
+ if (!DISCOVERABLE_PLATFORMS.includes(platform)) continue;
117
+ if (platform === 'slack') {
118
+ results.push(...await this._discoverSlackTargets(userId, agentId));
119
+ } else {
120
+ results.push(...await this._discoverMessagingPlatformTargets(userId, agentId, platform));
121
+ }
122
+ }
123
+ return results;
124
+ }
125
+
126
+ async _discoverMessagingPlatformTargets(userId, agentId, platform) {
127
+ const manager = this.app?.locals?.messagingManager || null;
128
+ if (!manager || typeof manager.listAccessTargets !== 'function') return [];
129
+ let rawTargets = [];
130
+ try {
131
+ rawTargets = await manager.listAccessTargets(userId, platform, { agentId });
132
+ } catch {
133
+ rawTargets = [];
134
+ }
135
+ return (Array.isArray(rawTargets) ? rawTargets : [])
136
+ .map((target) => normalizeTarget({
137
+ ...target,
138
+ platform,
139
+ to: target.to || target.value,
140
+ source: 'discovered',
141
+ }))
142
+ .filter(Boolean);
143
+ }
144
+
145
+ async _discoverSlackTargets(userId, agentId) {
146
+ const integrationManager = this.app?.locals?.integrationManager || null;
147
+ if (!integrationManager || typeof integrationManager.executeTool !== 'function') return [];
148
+ let response;
149
+ try {
150
+ response = await integrationManager.executeTool(
151
+ userId,
152
+ 'slack_list_conversations',
153
+ {
154
+ types: 'public_channel,private_channel,im,mpim',
155
+ limit: 200,
156
+ },
157
+ agentId,
158
+ );
159
+ } catch {
160
+ return [];
161
+ }
162
+ if (response?.error) return [];
163
+ const channels = response?.result?.channels || response?.result?.response_metadata?.channels || [];
164
+ if (!Array.isArray(channels)) return [];
165
+ return channels
166
+ .map((channel) => {
167
+ const id = normalizeText(channel.id);
168
+ if (!id) return null;
169
+ const isDirect = channel.is_im === true;
170
+ const isPrivate = channel.is_private === true;
171
+ const name = normalizeText(channel.name || channel.user || id);
172
+ return normalizeTarget({
173
+ platform: 'slack',
174
+ to: id,
175
+ label: isDirect ? name : `#${name}`,
176
+ subtitle: isDirect
177
+ ? 'Slack direct message'
178
+ : (isPrivate ? 'Private Slack channel' : 'Slack channel'),
179
+ source: 'discovered',
180
+ });
181
+ })
182
+ .filter(Boolean);
183
+ }
184
+
185
+ _defaultTargets(userId, agentId, platformFilter) {
186
+ const rows = this.db.prepare(
187
+ `SELECT key, value
188
+ FROM agent_settings
189
+ WHERE user_id = ? AND agent_id = ? AND key IN ('last_platform', 'last_chat_id')`
190
+ ).all(userId, agentId);
191
+ const values = Object.fromEntries(rows.map((row) => [row.key, parseSettingValue(row.value)]));
192
+ const platform = normalizeText(values.last_platform).toLowerCase();
193
+ const to = normalizeText(values.last_chat_id);
194
+ if (!platform || !to || (platformFilter && platform !== platformFilter)) return [];
195
+ return [normalizeTarget({
196
+ platform,
197
+ to,
198
+ label: to,
199
+ subtitle: 'Current default channel',
200
+ source: 'default',
201
+ })].filter(Boolean);
202
+ }
203
+
204
+ _recentTargets(userId, agentId, platformFilter) {
205
+ const params = [userId, agentId];
206
+ let platformClause = '';
207
+ if (platformFilter) {
208
+ platformClause = 'AND platform = ?';
209
+ params.push(platformFilter);
210
+ }
211
+ params.push(80);
212
+ const rows = this.db.prepare(
213
+ `SELECT platform, platform_chat_id, metadata
214
+ FROM messages
215
+ WHERE user_id = ?
216
+ AND agent_id = ?
217
+ AND platform IS NOT NULL
218
+ AND platform_chat_id IS NOT NULL
219
+ ${platformClause}
220
+ ORDER BY id DESC
221
+ LIMIT ?`
222
+ ).all(...params);
223
+ return rows
224
+ .map((row) => normalizeTarget({
225
+ platform: row.platform,
226
+ to: row.platform_chat_id,
227
+ label: labelFromMetadata(row.platform, row),
228
+ subtitle: 'Recent conversation',
229
+ source: 'recent',
230
+ }))
231
+ .filter(Boolean);
232
+ }
233
+
234
+ _filterAndSort(targets, query) {
235
+ const seen = new Set();
236
+ const unique = [];
237
+ for (const target of targets) {
238
+ const key = `${target.platform}:${target.to}`;
239
+ if (seen.has(key)) continue;
240
+ seen.add(key);
241
+ if (query) {
242
+ const haystack = `${target.platformLabel} ${target.label} ${target.subtitle} ${target.to}`.toLowerCase();
243
+ if (!haystack.includes(query)) continue;
244
+ }
245
+ unique.push(target);
246
+ }
247
+ return unique.sort((left, right) => {
248
+ const leftRank = SOURCE_RANK[left.source] ?? 9;
249
+ const rightRank = SOURCE_RANK[right.source] ?? 9;
250
+ if (leftRank !== rightRank) return leftRank - rightRank;
251
+ if (left.platformLabel !== right.platformLabel) return left.platformLabel.localeCompare(right.platformLabel);
252
+ return left.label.localeCompare(right.label);
253
+ }).slice(0, MAX_TARGETS);
254
+ }
255
+ }
256
+
257
+ module.exports = {
258
+ TaskDeliveryTargetService,
259
+ normalizeTarget,
260
+ };
@@ -10,13 +10,44 @@ function normalizeWhatsAppId(value) {
10
10
  return primary;
11
11
  }
12
12
 
13
+ function normalizeWhatsAppGroupId(value) {
14
+ const raw = String(value || '').trim().toLowerCase();
15
+ if (!raw) return '';
16
+ const jid = raw.includes('@') ? raw.split(':')[0] : raw;
17
+ return jid.endsWith('@g.us') ? jid : '';
18
+ }
19
+
20
+ function normalizeWhatsAppWhitelistEntry(value) {
21
+ const raw = String(value || '').trim();
22
+ if (!raw) return '';
23
+
24
+ const prefixed = raw.match(/^([a-z_]+):(.*)$/i);
25
+ if (prefixed) {
26
+ const scope = prefixed[1].trim().toLowerCase();
27
+ const scopedValue = prefixed[2].trim();
28
+ if (scope === 'group' || scope === 'chat') {
29
+ const groupId = normalizeWhatsAppGroupId(scopedValue);
30
+ if (groupId) return `${scope}:${groupId}`;
31
+ }
32
+ if (scope === 'user' || scope === 'phone' || scope === 'phone_number') {
33
+ const normalized = normalizeWhatsAppId(scopedValue);
34
+ return normalized ? `${scope === 'phone' ? 'phone_number' : scope}:${normalized}` : '';
35
+ }
36
+ }
37
+
38
+ const groupId = normalizeWhatsAppGroupId(raw);
39
+ if (groupId) return `group:${groupId}`;
40
+
41
+ return normalizeWhatsAppId(raw);
42
+ }
43
+
13
44
  function normalizeWhatsAppWhitelist(values) {
14
45
  if (!Array.isArray(values)) return [];
15
46
 
16
47
  const seen = new Set();
17
48
  const normalized = [];
18
49
  for (const value of values) {
19
- const entry = normalizeWhatsAppId(value);
50
+ const entry = normalizeWhatsAppWhitelistEntry(value);
20
51
  if (!entry || seen.has(entry)) continue;
21
52
  seen.add(entry);
22
53
  normalized.push(entry);
@@ -41,6 +72,7 @@ function toWhatsAppJid(value) {
41
72
 
42
73
  module.exports = {
43
74
  normalizeWhatsAppId,
75
+ normalizeWhatsAppGroupId,
44
76
  normalizeWhatsAppWhitelist,
45
77
  toWhatsAppJid,
46
78
  };