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.
- package/flutter_app/lib/main_controller.dart +17 -0
- package/flutter_app/lib/main_models.dart +55 -0
- package/flutter_app/lib/main_operations.dart +530 -2
- package/flutter_app/lib/src/backend_client.dart +20 -0
- package/lib/schema_migrations.js +23 -0
- package/package.json +1 -1
- package/server/db/database.js +6 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +63376 -62995
- package/server/routes/tasks.js +17 -0
- package/server/services/ai/tools.js +1 -1
- package/server/services/memory/consolidation.js +2 -0
- package/server/services/memory/manager.js +70 -8
- package/server/services/memory/retention.js +66 -0
- package/server/services/memory/routing.js +68 -0
- package/server/services/messaging/access_policy.js +39 -2
- package/server/services/messaging/manager.js +9 -4
- package/server/services/tasks/delivery_targets.js +260 -0
- package/server/utils/whatsapp.js +33 -1
|
@@ -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
|
+
};
|
package/server/utils/whatsapp.js
CHANGED
|
@@ -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 =
|
|
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
|
};
|