neoagent 1.6.0 → 2.0.0
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/README.md +18 -4
- package/docs/configuration.md +2 -2
- package/docs/skills.md +1 -1
- package/lib/manager.js +64 -2
- package/package.json +9 -2
- package/server/config/origins.js +34 -0
- package/server/db/database.js +0 -13
- package/server/http/errors.js +17 -0
- package/server/http/middleware.js +81 -0
- package/server/http/routes.js +45 -0
- package/server/http/socket.js +23 -0
- package/server/http/static.js +50 -0
- package/server/index.js +50 -188
- package/server/public/.last_build_id +1 -0
- package/server/public/assets/AssetManifest.bin +1 -0
- package/server/public/assets/AssetManifest.bin.json +1 -0
- package/server/public/assets/AssetManifest.json +1 -0
- package/server/public/assets/FontManifest.json +1 -0
- package/server/public/assets/NOTICES +33454 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/packages/cupertino_icons/assets/CupertinoIcons.ttf +0 -0
- package/server/public/assets/shaders/ink_sparkle.frag +126 -0
- package/server/public/assets/web/icons/Icon-192.png +0 -0
- package/server/public/canvaskit/canvaskit.js +192 -0
- package/server/public/canvaskit/canvaskit.js.symbols +12142 -0
- package/server/public/canvaskit/canvaskit.wasm +0 -0
- package/server/public/canvaskit/chromium/canvaskit.js +192 -0
- package/server/public/canvaskit/chromium/canvaskit.js.symbols +11106 -0
- package/server/public/canvaskit/chromium/canvaskit.wasm +0 -0
- package/server/public/canvaskit/skwasm.js +140 -0
- package/server/public/canvaskit/skwasm.js.symbols +12164 -0
- package/server/public/canvaskit/skwasm.wasm +0 -0
- package/server/public/canvaskit/skwasm_heavy.js +140 -0
- package/server/public/canvaskit/skwasm_heavy.js.symbols +13766 -0
- package/server/public/canvaskit/skwasm_heavy.wasm +0 -0
- package/server/public/favicon.png +0 -0
- package/server/public/flutter.js +32 -0
- package/server/public/flutter_bootstrap.js +43 -0
- package/server/public/flutter_service_worker.js +208 -0
- package/server/public/icons/Icon-192.png +0 -0
- package/server/public/icons/Icon-512.png +0 -0
- package/server/public/icons/Icon-maskable-192.png +0 -0
- package/server/public/icons/Icon-maskable-512.png +0 -0
- package/server/public/index.html +38 -0
- package/server/public/main.dart.js +103124 -0
- package/server/public/manifest.json +35 -0
- package/server/public/version.json +1 -0
- package/server/services/ai/models.js +2 -8
- package/server/services/ai/tools.js +0 -47
- package/server/services/browser/controller.js +34 -0
- package/server/services/manager.js +49 -118
- package/server/services/messaging/automation.js +210 -0
- package/server/utils/version.js +37 -0
- package/server/public/app.html +0 -682
- package/server/public/assets/world-office-dark.png +0 -0
- package/server/public/assets/world-office-light.png +0 -0
- package/server/public/css/app.css +0 -941
- package/server/public/css/styles.css +0 -963
- package/server/public/favicon.svg +0 -17
- package/server/public/js/app.js +0 -4105
- package/server/public/login.html +0 -313
- package/server/routes/protocols.js +0 -87
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "NeoAgent",
|
|
3
|
+
"short_name": "NeoAgent",
|
|
4
|
+
"start_url": ".",
|
|
5
|
+
"display": "standalone",
|
|
6
|
+
"background_color": "#0175C2",
|
|
7
|
+
"theme_color": "#0175C2",
|
|
8
|
+
"description": "NeoAgent Flutter client for web and Android.",
|
|
9
|
+
"orientation": "portrait-primary",
|
|
10
|
+
"prefer_related_applications": false,
|
|
11
|
+
"icons": [
|
|
12
|
+
{
|
|
13
|
+
"src": "icons/Icon-192.png",
|
|
14
|
+
"sizes": "192x192",
|
|
15
|
+
"type": "image/png"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"src": "icons/Icon-512.png",
|
|
19
|
+
"sizes": "512x512",
|
|
20
|
+
"type": "image/png"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"src": "icons/Icon-maskable-192.png",
|
|
24
|
+
"sizes": "192x192",
|
|
25
|
+
"type": "image/png",
|
|
26
|
+
"purpose": "maskable"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"src": "icons/Icon-maskable-512.png",
|
|
30
|
+
"sizes": "512x512",
|
|
31
|
+
"type": "image/png",
|
|
32
|
+
"purpose": "maskable"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"app_name":"neoagent_flutter","version":"1.0.0","build_number":"1","package_name":"neoagent_flutter"}
|
|
@@ -29,16 +29,10 @@ const STATIC_MODELS = [
|
|
|
29
29
|
purpose: 'general'
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
|
-
id: '
|
|
33
|
-
label: 'Qwen
|
|
32
|
+
id: 'qwen3.5:4b',
|
|
33
|
+
label: 'Qwen 3.5 4B (Local / Ollama)',
|
|
34
34
|
provider: 'ollama',
|
|
35
35
|
purpose: 'general'
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
id: 'qwen2.5-coder:7b',
|
|
39
|
-
label: 'Qwen 2.5 Coder 7B (Local / Coding)',
|
|
40
|
-
provider: 'ollama',
|
|
41
|
-
purpose: 'coding'
|
|
42
36
|
}
|
|
43
37
|
];
|
|
44
38
|
|
|
@@ -157,20 +157,6 @@ function getAvailableTools(app, options = {}) {
|
|
|
157
157
|
required: ['query']
|
|
158
158
|
}
|
|
159
159
|
},
|
|
160
|
-
{
|
|
161
|
-
name: 'manage_protocols',
|
|
162
|
-
description: 'Read, list, create, update, or delete text-based protocols (a pre-set list of instructions/actions). If user asks to execute a protocol, you should read it and follow its instructions.',
|
|
163
|
-
parameters: {
|
|
164
|
-
type: 'object',
|
|
165
|
-
properties: {
|
|
166
|
-
action: { type: 'string', enum: ['list', 'read', 'create', 'update', 'delete'], description: 'The protocol action to perform.' },
|
|
167
|
-
name: { type: 'string', description: 'Name of the protocol (required for read, create, update, delete)' },
|
|
168
|
-
description: { type: 'string', description: 'Description of the protocol (optional for create/update)' },
|
|
169
|
-
content: { type: 'string', description: 'Text content/instructions of the protocol (required for create/update)' }
|
|
170
|
-
},
|
|
171
|
-
required: ['action']
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
160
|
{
|
|
175
161
|
name: 'memory_save',
|
|
176
162
|
description: 'Save ONE specific, self-contained fact to long-term semantic memory. RULES: (1) One discrete fact per call — if you have 10 facts, call this 10 times. (2) The ENTIRE value must be IN the content string itself — never write a pointer/reference like "user shared a profile" or "see chat history for details". That is useless. (3) Content must be a complete statement a stranger could read cold and understand. GOOD: "Neo lives in Braunschweig, Germany" / "Neo prefers dark mode" / "Neo\'s project WorldEndArchive crawls and compresses websites to offline JSON archives". BAD: "User pasted a profile dump" / "Neo shared lots of details — see chat history" / "Neo gave a big list of projects".',
|
|
@@ -747,39 +733,6 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
747
733
|
}
|
|
748
734
|
}
|
|
749
735
|
|
|
750
|
-
case 'manage_protocols': {
|
|
751
|
-
try {
|
|
752
|
-
if (args.action === 'list') {
|
|
753
|
-
const list = db.prepare('SELECT name, description, updated_at FROM protocols WHERE user_id = ?').all(userId);
|
|
754
|
-
return { protocols: list };
|
|
755
|
-
} else if (args.action === 'read') {
|
|
756
|
-
if (!args.name) return { error: "name is required" };
|
|
757
|
-
const p = db.prepare('SELECT * FROM protocols WHERE name = ? AND user_id = ?').get(args.name, userId);
|
|
758
|
-
return p ? { name: p.name, description: p.description, content: p.content } : { error: `Protocol '${args.name}' not found` };
|
|
759
|
-
} else if (args.action === 'create') {
|
|
760
|
-
if (!args.name || !args.content) return { error: "name and content are required" };
|
|
761
|
-
db.prepare('INSERT INTO protocols (user_id, name, description, content) VALUES (?, ?, ?, ?)').run(userId, args.name, args.description || '', args.content);
|
|
762
|
-
return { success: true, message: `Protocol '${args.name}' created.` };
|
|
763
|
-
} else if (args.action === 'update') {
|
|
764
|
-
if (!args.name || !args.content) return { error: "name and content are required" };
|
|
765
|
-
const p = db.prepare('SELECT id FROM protocols WHERE name = ? AND user_id = ?').get(args.name, userId);
|
|
766
|
-
if (!p) return { error: `Protocol '${args.name}' not found` };
|
|
767
|
-
db.prepare("UPDATE protocols SET description = ?, content = ?, updated_at = datetime('now') WHERE id = ?").run(args.description || '', args.content, p.id);
|
|
768
|
-
return { success: true, message: `Protocol '${args.name}' updated.` };
|
|
769
|
-
} else if (args.action === 'delete') {
|
|
770
|
-
if (!args.name) return { error: "name is required" };
|
|
771
|
-
const p = db.prepare('SELECT id FROM protocols WHERE name = ? AND user_id = ?').get(args.name, userId);
|
|
772
|
-
if (!p) return { error: `Protocol '${args.name}' not found` };
|
|
773
|
-
db.prepare('DELETE FROM protocols WHERE id = ?').run(p.id);
|
|
774
|
-
return { success: true, message: `Protocol '${args.name}' deleted.` };
|
|
775
|
-
}
|
|
776
|
-
return { error: 'Invalid action' };
|
|
777
|
-
} catch (err) {
|
|
778
|
-
if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') return { error: 'Protocol with this name already exists' };
|
|
779
|
-
return { error: err.message };
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
736
|
case 'memory_save': {
|
|
784
737
|
const { MemoryManager } = require('../memory/manager');
|
|
785
738
|
const mm = new MemoryManager();
|
|
@@ -20,6 +20,39 @@ const VIEWPORTS = [
|
|
|
20
20
|
{ width: 1920, height: 1080 },
|
|
21
21
|
];
|
|
22
22
|
|
|
23
|
+
function resolveBrowserExecutablePath() {
|
|
24
|
+
const explicitPath =
|
|
25
|
+
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
|
26
|
+
process.env.CHROME_BIN ||
|
|
27
|
+
process.env.CHROMIUM_BIN;
|
|
28
|
+
|
|
29
|
+
if (explicitPath && fs.existsSync(explicitPath)) return explicitPath;
|
|
30
|
+
|
|
31
|
+
const platformCandidates = process.platform === 'darwin'
|
|
32
|
+
? [
|
|
33
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
34
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
35
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
36
|
+
]
|
|
37
|
+
: process.platform === 'win32'
|
|
38
|
+
? [
|
|
39
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
40
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
41
|
+
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
42
|
+
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
43
|
+
]
|
|
44
|
+
: [
|
|
45
|
+
'/usr/bin/google-chrome',
|
|
46
|
+
'/usr/bin/google-chrome-stable',
|
|
47
|
+
'/usr/bin/chromium',
|
|
48
|
+
'/usr/bin/chromium-browser',
|
|
49
|
+
'/snap/bin/chromium',
|
|
50
|
+
'/usr/bin/microsoft-edge',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
return platformCandidates.find((candidate) => fs.existsSync(candidate)) || null;
|
|
54
|
+
}
|
|
55
|
+
|
|
23
56
|
function rand(min, max) {
|
|
24
57
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
25
58
|
}
|
|
@@ -127,6 +160,7 @@ class BrowserController {
|
|
|
127
160
|
|
|
128
161
|
this.browser = await puppeteer.launch({
|
|
129
162
|
headless: this.headless ? 'new' : false,
|
|
163
|
+
executablePath: resolveBrowserExecutablePath() || undefined,
|
|
130
164
|
args: [
|
|
131
165
|
'--no-sandbox',
|
|
132
166
|
'--disable-setuid-sandbox',
|
|
@@ -11,9 +11,7 @@ const { SkillRunner } = require('./ai/toolRunner');
|
|
|
11
11
|
const { MessagingManager } = require('./messaging/manager');
|
|
12
12
|
const { Scheduler } = require('./scheduler/cron');
|
|
13
13
|
const { setupWebSocket } = require('./websocket');
|
|
14
|
-
const {
|
|
15
|
-
const { normalizeWhatsAppId } = require('../utils/whatsapp');
|
|
16
|
-
const { randomUUID } = require('crypto');
|
|
14
|
+
const { registerMessagingAutomation } = require('./messaging/automation');
|
|
17
15
|
|
|
18
16
|
async function startServices(app, io) {
|
|
19
17
|
try {
|
|
@@ -62,120 +60,11 @@ async function startServices(app, io) {
|
|
|
62
60
|
mcpClient.loadFromDB(u.id).catch(err => console.error('[MCP] Auto-start error:', err.message));
|
|
63
61
|
}
|
|
64
62
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const q = userQueues[userId];
|
|
71
|
-
|
|
72
|
-
if (q.running) {
|
|
73
|
-
const last = q.pending[q.pending.length - 1];
|
|
74
|
-
if (last && last.platform === msg.platform && last.chatId === msg.chatId) {
|
|
75
|
-
last.content += '\n' + msg.content;
|
|
76
|
-
last.messageId = msg.messageId;
|
|
77
|
-
} else {
|
|
78
|
-
q.pending.push({ ...msg });
|
|
79
|
-
}
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
q.running = true;
|
|
84
|
-
try {
|
|
85
|
-
await messagingManager.markRead(userId, msg.platform, msg.chatId, msg.messageId).catch(() => { });
|
|
86
|
-
await messagingManager.sendTyping(userId, msg.platform, msg.chatId, true).catch(() => { });
|
|
87
|
-
|
|
88
|
-
const mediaNote = msg.localMediaPath
|
|
89
|
-
? `\nMedia attached at: ${msg.localMediaPath} (type: ${msg.mediaType}). You can reference or forward it with send_message media_path.`
|
|
90
|
-
: '';
|
|
91
|
-
|
|
92
|
-
if (detectPromptInjection(msg.content)) {
|
|
93
|
-
console.warn(`[Security] Possible prompt injection attempt from ${msg.sender} on ${msg.platform}: ${msg.content.slice(0, 200)}`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const isVoiceCall = msg.platform === 'telnyx' && msg.mediaType === 'voice';
|
|
97
|
-
const isVoiceNote = !isVoiceCall && msg.mediaType === 'audio';
|
|
98
|
-
const isDiscordGuild = msg.platform === 'discord' && msg.isGroup;
|
|
99
|
-
|
|
100
|
-
const discordContext = (isDiscordGuild && Array.isArray(msg.channelContext) && msg.channelContext.length)
|
|
101
|
-
? '\n\nRecent channel context (oldest → newest):\n' +
|
|
102
|
-
msg.channelContext.map(m => `[${m.author}]: ${m.content}`).join('\n')
|
|
103
|
-
: '';
|
|
104
|
-
|
|
105
|
-
const sttNote = isVoiceNote
|
|
106
|
-
? '\n[Note: This message was sent as a voice note and transcribed via speech-to-text. The transcription may not be perfectly accurate.]'
|
|
107
|
-
: '';
|
|
108
|
-
|
|
109
|
-
const prompt = isVoiceCall
|
|
110
|
-
? `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}".`
|
|
111
|
-
: `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}".`;
|
|
112
|
-
|
|
113
|
-
let convRow = db.prepare(
|
|
114
|
-
'SELECT id FROM conversations WHERE user_id = ? AND platform = ? AND platform_chat_id = ?'
|
|
115
|
-
).get(userId, msg.platform, msg.chatId);
|
|
116
|
-
|
|
117
|
-
if (!convRow) {
|
|
118
|
-
const convId = randomUUID();
|
|
119
|
-
db.prepare(
|
|
120
|
-
'INSERT INTO conversations (id, user_id, platform, platform_chat_id, title) VALUES (?, ?, ?, ?, ?)'
|
|
121
|
-
).run(convId, userId, msg.platform, msg.chatId, `${msg.platform} — ${msg.senderName || msg.sender || msg.chatId}`);
|
|
122
|
-
convRow = { id: convId };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const runOpts = { triggerSource: 'messaging', conversationId: convRow.id, source: msg.platform, chatId: msg.chatId, context: { rawUserMessage: msg.content } };
|
|
126
|
-
if (msg.localMediaPath) runOpts.mediaAttachments = [{ path: msg.localMediaPath, type: msg.mediaType }];
|
|
127
|
-
|
|
128
|
-
await agentEngine.run(userId, prompt, runOpts);
|
|
129
|
-
} finally {
|
|
130
|
-
await messagingManager.sendTyping(userId, msg.platform, msg.chatId, false).catch(() => { });
|
|
131
|
-
q.running = false;
|
|
132
|
-
if (q.pending.length > 0) {
|
|
133
|
-
const next = q.pending.shift();
|
|
134
|
-
processMessage(userId, next);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
messagingManager.registerHandler(async (userId, msg) => {
|
|
140
|
-
if (msg.platform !== 'discord' && msg.platform !== 'telegram') {
|
|
141
|
-
const whitelistRow = db.prepare('SELECT value FROM user_settings WHERE user_id = ? AND key = ?')
|
|
142
|
-
.get(userId, `platform_whitelist_${msg.platform}`);
|
|
143
|
-
const normalize = msg.platform === 'whatsapp'
|
|
144
|
-
? normalizeWhatsAppId
|
|
145
|
-
: (id) => String(id || '').replace(/[^0-9+]/g, '');
|
|
146
|
-
|
|
147
|
-
let whitelist = [];
|
|
148
|
-
if (whitelistRow) {
|
|
149
|
-
try {
|
|
150
|
-
const parsed = JSON.parse(whitelistRow.value);
|
|
151
|
-
if (Array.isArray(parsed)) whitelist = parsed;
|
|
152
|
-
} catch { }
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const enforceEmptyWhitelist = msg.platform === 'whatsapp';
|
|
156
|
-
const shouldCheckWhitelist = whitelist.length > 0 || enforceEmptyWhitelist;
|
|
157
|
-
|
|
158
|
-
if (shouldCheckWhitelist) {
|
|
159
|
-
const senderNorm = normalize(msg.sender || msg.chatId);
|
|
160
|
-
const allowed = whitelist.some((n) => normalize(n) === senderNorm);
|
|
161
|
-
if (!allowed) {
|
|
162
|
-
console.log(`[Messaging] Blocked ${msg.platform} message from ${msg.sender} (not in whitelist)`);
|
|
163
|
-
io.to(`user:${userId}`).emit('messaging:blocked_sender', {
|
|
164
|
-
platform: msg.platform,
|
|
165
|
-
sender: msg.sender,
|
|
166
|
-
chatId: msg.chatId,
|
|
167
|
-
senderName: msg.senderName || null
|
|
168
|
-
});
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const upsertSetting = db.prepare('INSERT OR REPLACE INTO user_settings (user_id, key, value) VALUES (?, ?, ?)');
|
|
175
|
-
upsertSetting.run(userId, 'last_platform', msg.platform);
|
|
176
|
-
upsertSetting.run(userId, 'last_chat_id', msg.chatId);
|
|
177
|
-
|
|
178
|
-
await processMessage(userId, msg);
|
|
63
|
+
registerMessagingAutomation({
|
|
64
|
+
app,
|
|
65
|
+
io,
|
|
66
|
+
messagingManager,
|
|
67
|
+
agentEngine,
|
|
179
68
|
});
|
|
180
69
|
|
|
181
70
|
const scheduler = new Scheduler(io, agentEngine, app);
|
|
@@ -199,4 +88,46 @@ async function startServices(app, io) {
|
|
|
199
88
|
}
|
|
200
89
|
}
|
|
201
90
|
|
|
202
|
-
|
|
91
|
+
async function stopServices(app) {
|
|
92
|
+
const tasks = [];
|
|
93
|
+
|
|
94
|
+
if (app.locals.scheduler) {
|
|
95
|
+
try {
|
|
96
|
+
app.locals.scheduler.stop();
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error('[Scheduler] Stop error:', err.message);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (app.locals.mcpClient) {
|
|
103
|
+
tasks.push(
|
|
104
|
+
app.locals.mcpClient.shutdown().catch((err) => {
|
|
105
|
+
console.error('[MCP] Shutdown error:', err.message);
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (app.locals.browserController) {
|
|
111
|
+
tasks.push(
|
|
112
|
+
app.locals.browserController.closeBrowser().catch((err) => {
|
|
113
|
+
console.error('[Browser] Shutdown error:', err.message);
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (app.locals.messagingManager?.platforms instanceof Map) {
|
|
119
|
+
for (const platform of app.locals.messagingManager.platforms.values()) {
|
|
120
|
+
if (typeof platform.disconnect === 'function') {
|
|
121
|
+
tasks.push(
|
|
122
|
+
platform.disconnect().catch((err) => {
|
|
123
|
+
console.error('[Messaging] Disconnect error:', err.message);
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await Promise.allSettled(tasks);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { startServices, stopServices };
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const db = require('../../db/database');
|
|
4
|
+
const { detectPromptInjection } = require('../../utils/security');
|
|
5
|
+
const { normalizeWhatsAppId } = require('../../utils/whatsapp');
|
|
6
|
+
const { randomUUID } = require('crypto');
|
|
7
|
+
|
|
8
|
+
function registerMessagingAutomation({ app, io, messagingManager, agentEngine }) {
|
|
9
|
+
const userQueues = {};
|
|
10
|
+
app.locals.userQueues = userQueues;
|
|
11
|
+
|
|
12
|
+
messagingManager.registerHandler(async (userId, msg) => {
|
|
13
|
+
if (!(await isAllowedMessagingSender({ io, userId, msg }))) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const upsertSetting = db.prepare(
|
|
18
|
+
'INSERT OR REPLACE INTO user_settings (user_id, key, value) VALUES (?, ?, ?)'
|
|
19
|
+
);
|
|
20
|
+
upsertSetting.run(userId, 'last_platform', msg.platform);
|
|
21
|
+
upsertSetting.run(userId, 'last_chat_id', msg.chatId);
|
|
22
|
+
|
|
23
|
+
await processQueuedMessage({
|
|
24
|
+
userQueues,
|
|
25
|
+
messagingManager,
|
|
26
|
+
agentEngine,
|
|
27
|
+
userId,
|
|
28
|
+
msg
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function processQueuedMessage({
|
|
34
|
+
userQueues,
|
|
35
|
+
messagingManager,
|
|
36
|
+
agentEngine,
|
|
37
|
+
userId,
|
|
38
|
+
msg
|
|
39
|
+
}) {
|
|
40
|
+
if (!userQueues[userId]) {
|
|
41
|
+
userQueues[userId] = { running: false, pending: [] };
|
|
42
|
+
}
|
|
43
|
+
const queue = userQueues[userId];
|
|
44
|
+
|
|
45
|
+
if (queue.running) {
|
|
46
|
+
const last = queue.pending[queue.pending.length - 1];
|
|
47
|
+
if (last && last.platform === msg.platform && last.chatId === msg.chatId) {
|
|
48
|
+
last.content += `\n${msg.content}`;
|
|
49
|
+
last.messageId = msg.messageId;
|
|
50
|
+
} else {
|
|
51
|
+
queue.pending.push({ ...msg });
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
queue.running = true;
|
|
57
|
+
try {
|
|
58
|
+
await messagingManager
|
|
59
|
+
.markRead(userId, msg.platform, msg.chatId, msg.messageId)
|
|
60
|
+
.catch(() => {});
|
|
61
|
+
await messagingManager
|
|
62
|
+
.sendTyping(userId, msg.platform, msg.chatId, true)
|
|
63
|
+
.catch(() => {});
|
|
64
|
+
|
|
65
|
+
const prompt = buildIncomingPrompt(msg);
|
|
66
|
+
const conversationId = ensureConversation(userId, msg);
|
|
67
|
+
const runOptions = {
|
|
68
|
+
triggerSource: 'messaging',
|
|
69
|
+
conversationId,
|
|
70
|
+
source: msg.platform,
|
|
71
|
+
chatId: msg.chatId,
|
|
72
|
+
context: { rawUserMessage: msg.content }
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (msg.localMediaPath) {
|
|
76
|
+
runOptions.mediaAttachments = [
|
|
77
|
+
{ path: msg.localMediaPath, type: msg.mediaType }
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await agentEngine.run(userId, prompt, runOptions);
|
|
82
|
+
} finally {
|
|
83
|
+
await messagingManager
|
|
84
|
+
.sendTyping(userId, msg.platform, msg.chatId, false)
|
|
85
|
+
.catch(() => {});
|
|
86
|
+
queue.running = false;
|
|
87
|
+
if (queue.pending.length > 0) {
|
|
88
|
+
const next = queue.pending.shift();
|
|
89
|
+
await processQueuedMessage({
|
|
90
|
+
userQueues,
|
|
91
|
+
messagingManager,
|
|
92
|
+
agentEngine,
|
|
93
|
+
userId,
|
|
94
|
+
msg: next
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function ensureConversation(userId, msg) {
|
|
101
|
+
let conversation = db
|
|
102
|
+
.prepare(
|
|
103
|
+
'SELECT id FROM conversations WHERE user_id = ? AND platform = ? AND platform_chat_id = ?'
|
|
104
|
+
)
|
|
105
|
+
.get(userId, msg.platform, msg.chatId);
|
|
106
|
+
|
|
107
|
+
if (conversation) {
|
|
108
|
+
return conversation.id;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const conversationId = randomUUID();
|
|
112
|
+
db.prepare(
|
|
113
|
+
'INSERT INTO conversations (id, user_id, platform, platform_chat_id, title) VALUES (?, ?, ?, ?, ?)'
|
|
114
|
+
).run(
|
|
115
|
+
conversationId,
|
|
116
|
+
userId,
|
|
117
|
+
msg.platform,
|
|
118
|
+
msg.chatId,
|
|
119
|
+
`${msg.platform} — ${msg.senderName || msg.sender || msg.chatId}`
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return conversationId;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildIncomingPrompt(msg) {
|
|
126
|
+
const mediaNote = msg.localMediaPath
|
|
127
|
+
? `\nMedia attached at: ${msg.localMediaPath} (type: ${msg.mediaType}). You can reference or forward it with send_message media_path.`
|
|
128
|
+
: '';
|
|
129
|
+
|
|
130
|
+
if (detectPromptInjection(msg.content)) {
|
|
131
|
+
console.warn(
|
|
132
|
+
`[Security] Possible prompt injection attempt from ${msg.sender} on ${msg.platform}: ${msg.content.slice(0, 200)}`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const isVoiceCall = msg.platform === 'telnyx' && msg.mediaType === 'voice';
|
|
137
|
+
const isVoiceNote = !isVoiceCall && msg.mediaType === 'audio';
|
|
138
|
+
const isDiscordGuild = msg.platform === 'discord' && msg.isGroup;
|
|
139
|
+
|
|
140
|
+
const discordContext =
|
|
141
|
+
isDiscordGuild &&
|
|
142
|
+
Array.isArray(msg.channelContext) &&
|
|
143
|
+
msg.channelContext.length
|
|
144
|
+
? '\n\nRecent channel context (oldest → newest):\n' +
|
|
145
|
+
msg.channelContext.map((item) => `[${item.author}]: ${item.content}`).join('\n')
|
|
146
|
+
: '';
|
|
147
|
+
|
|
148
|
+
const sttNote = isVoiceNote
|
|
149
|
+
? '\n[Note: This message was sent as a voice note and transcribed via speech-to-text. The transcription may not be perfectly accurate.]'
|
|
150
|
+
: '';
|
|
151
|
+
|
|
152
|
+
if (isVoiceCall) {
|
|
153
|
+
return `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}".`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return `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}".`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function isAllowedMessagingSender({ io, userId, msg }) {
|
|
160
|
+
if (msg.platform === 'discord' || msg.platform === 'telegram') {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const whitelistRow = db
|
|
165
|
+
.prepare('SELECT value FROM user_settings WHERE user_id = ? AND key = ?')
|
|
166
|
+
.get(userId, `platform_whitelist_${msg.platform}`);
|
|
167
|
+
|
|
168
|
+
const normalize =
|
|
169
|
+
msg.platform === 'whatsapp'
|
|
170
|
+
? normalizeWhatsAppId
|
|
171
|
+
: (id) => String(id || '').replace(/[^0-9+]/g, '');
|
|
172
|
+
|
|
173
|
+
let whitelist = [];
|
|
174
|
+
if (whitelistRow) {
|
|
175
|
+
try {
|
|
176
|
+
const parsed = JSON.parse(whitelistRow.value);
|
|
177
|
+
if (Array.isArray(parsed)) whitelist = parsed;
|
|
178
|
+
} catch {
|
|
179
|
+
whitelist = [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const enforceEmptyWhitelist = msg.platform === 'whatsapp';
|
|
184
|
+
const shouldCheckWhitelist = whitelist.length > 0 || enforceEmptyWhitelist;
|
|
185
|
+
|
|
186
|
+
if (!shouldCheckWhitelist) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const senderNorm = normalize(msg.sender || msg.chatId);
|
|
191
|
+
const allowed = whitelist.some((entry) => normalize(entry) === senderNorm);
|
|
192
|
+
if (allowed) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log(
|
|
197
|
+
`[Messaging] Blocked ${msg.platform} message from ${msg.sender} (not in whitelist)`
|
|
198
|
+
);
|
|
199
|
+
io.to(`user:${userId}`).emit('messaging:blocked_sender', {
|
|
200
|
+
platform: msg.platform,
|
|
201
|
+
sender: msg.sender,
|
|
202
|
+
chatId: msg.chatId,
|
|
203
|
+
senderName: msg.senderName || null
|
|
204
|
+
});
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = {
|
|
209
|
+
registerMessagingAutomation
|
|
210
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const { APP_DIR } = require('../../runtime/paths');
|
|
5
|
+
const packageJson = require('../../package.json');
|
|
6
|
+
|
|
7
|
+
function getVersionInfo() {
|
|
8
|
+
let version = packageJson.version;
|
|
9
|
+
let gitSha = null;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
version =
|
|
13
|
+
execSync('git describe --tags --always --dirty', {
|
|
14
|
+
cwd: APP_DIR,
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
17
|
+
})
|
|
18
|
+
.trim()
|
|
19
|
+
.replace(/^v/, '') || packageJson.version;
|
|
20
|
+
gitSha = execSync('git rev-parse --short HEAD', {
|
|
21
|
+
cwd: APP_DIR,
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
24
|
+
}).trim();
|
|
25
|
+
} catch {
|
|
26
|
+
gitSha = process.env.GIT_SHA || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
name: packageJson.name,
|
|
31
|
+
version,
|
|
32
|
+
packageVersion: packageJson.version,
|
|
33
|
+
gitSha
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { getVersionInfo };
|