neoagent 2.1.18-beta.93 → 2.1.18-beta.94
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 +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/services/integrations/google/common.js +135 -8
- package/server/services/integrations/google/gmail.js +4 -3
- package/server/services/messaging/discord.js +7 -0
- package/server/services/messaging/manager.js +15 -3
- package/server/services/messaging/whatsapp.js +26 -3
- package/server/services/websocket.js +5 -0
package/package.json
CHANGED
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"59aa584fdf100e6c78c785d8a5b565d1de4b48
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "769435206" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { TextDecoder } = require('util');
|
|
5
6
|
|
|
6
7
|
function bufferToBase64Url(buffer) {
|
|
7
8
|
return Buffer.from(buffer)
|
|
@@ -15,13 +16,125 @@ function stringToBase64Url(text) {
|
|
|
15
16
|
return bufferToBase64Url(Buffer.from(String(text || ''), 'utf8'));
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
function
|
|
19
|
-
if (!value) return
|
|
20
|
-
const
|
|
19
|
+
function base64UrlToBuffer(value) {
|
|
20
|
+
if (!value) return Buffer.alloc(0);
|
|
21
|
+
const raw = String(value);
|
|
22
|
+
const normalized = raw
|
|
21
23
|
.replace(/-/g, '+')
|
|
22
24
|
.replace(/_/g, '/')
|
|
23
|
-
.padEnd(Math.ceil(
|
|
24
|
-
return Buffer.from(normalized, 'base64')
|
|
25
|
+
.padEnd(Math.ceil(raw.length / 4) * 4, '=');
|
|
26
|
+
return Buffer.from(normalized, 'base64');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeCharset(charset) {
|
|
30
|
+
const normalized = String(charset || 'utf-8').trim().toLowerCase();
|
|
31
|
+
if (!normalized) return 'utf-8';
|
|
32
|
+
if (normalized === 'utf8') return 'utf-8';
|
|
33
|
+
if (normalized === 'us-ascii') return 'ascii';
|
|
34
|
+
if (
|
|
35
|
+
normalized === 'latin1'
|
|
36
|
+
|| normalized === 'latin-1'
|
|
37
|
+
|| normalized === 'iso-8859-1'
|
|
38
|
+
|| normalized === 'iso8859-1'
|
|
39
|
+
) {
|
|
40
|
+
return 'windows-1252';
|
|
41
|
+
}
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function decodeBytes(buffer, charset = 'utf-8') {
|
|
46
|
+
const bytes = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer || '');
|
|
47
|
+
const candidates = [
|
|
48
|
+
normalizeCharset(charset),
|
|
49
|
+
'utf-8',
|
|
50
|
+
'windows-1252',
|
|
51
|
+
];
|
|
52
|
+
for (const candidate of candidates) {
|
|
53
|
+
try {
|
|
54
|
+
return new TextDecoder(candidate, { fatal: false }).decode(bytes);
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
return bytes.toString('utf8');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mojibakeScore(value) {
|
|
61
|
+
const text = String(value || '');
|
|
62
|
+
let score = 0;
|
|
63
|
+
for (const char of text) {
|
|
64
|
+
if (char === '\uFFFD') score += 4;
|
|
65
|
+
else if (char === 'Ã' || char === 'Â') score += 3;
|
|
66
|
+
else if (char === 'â' || char === 'ð' || char === 'ï') score += 2;
|
|
67
|
+
}
|
|
68
|
+
return score;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function repairMojibake(value) {
|
|
72
|
+
let current = String(value || '');
|
|
73
|
+
for (let i = 0; i < 2; i += 1) {
|
|
74
|
+
if (mojibakeScore(current) === 0) break;
|
|
75
|
+
const repaired = Buffer.from(current, 'latin1').toString('utf8');
|
|
76
|
+
if (!repaired || mojibakeScore(repaired) >= mojibakeScore(current)) break;
|
|
77
|
+
current = repaired;
|
|
78
|
+
}
|
|
79
|
+
return current;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function base64UrlToString(value) {
|
|
83
|
+
return repairMojibake(decodeBytes(base64UrlToBuffer(value), 'utf-8'));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseCharset(value) {
|
|
87
|
+
const match = /charset\s*=\s*("?)([^";\s]+)\1/i.exec(String(value || ''));
|
|
88
|
+
return match?.[2] || 'utf-8';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function decodeQuotedPrintableToBuffer(value) {
|
|
92
|
+
const cleaned = String(value || '').replace(/=\r?\n/g, '');
|
|
93
|
+
const bytes = [];
|
|
94
|
+
for (let i = 0; i < cleaned.length; i += 1) {
|
|
95
|
+
const char = cleaned[i];
|
|
96
|
+
if (
|
|
97
|
+
char === '='
|
|
98
|
+
&& i + 2 < cleaned.length
|
|
99
|
+
&& /^[0-9A-Fa-f]{2}$/.test(cleaned.slice(i + 1, i + 3))
|
|
100
|
+
) {
|
|
101
|
+
bytes.push(Number.parseInt(cleaned.slice(i + 1, i + 3), 16));
|
|
102
|
+
i += 2;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
bytes.push(char.charCodeAt(0));
|
|
106
|
+
}
|
|
107
|
+
return Buffer.from(bytes);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function decodeMimeWord(charset, encoding, value) {
|
|
111
|
+
const normalizedEncoding = String(encoding || '').trim().toUpperCase();
|
|
112
|
+
if (normalizedEncoding === 'B') {
|
|
113
|
+
return repairMojibake(
|
|
114
|
+
decodeBytes(Buffer.from(String(value || ''), 'base64'), charset),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (normalizedEncoding === 'Q') {
|
|
118
|
+
return repairMojibake(
|
|
119
|
+
decodeBytes(
|
|
120
|
+
decodeQuotedPrintableToBuffer(String(value || '').replace(/_/g, ' ')),
|
|
121
|
+
charset,
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return String(value || '');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function decodeMimeWords(value) {
|
|
129
|
+
return String(value || '').replace(
|
|
130
|
+
/=\?([^?]+)\?([bqBQ])\?([^?]*)\?=/g,
|
|
131
|
+
(_, charset, encoding, encodedText) =>
|
|
132
|
+
decodeMimeWord(charset, encoding, encodedText),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeDecodedText(value) {
|
|
137
|
+
return repairMojibake(decodeMimeWords(value));
|
|
25
138
|
}
|
|
26
139
|
|
|
27
140
|
function getHeader(headers, name) {
|
|
@@ -31,7 +144,7 @@ function getHeader(headers, name) {
|
|
|
31
144
|
(header) => String(header?.name || '').toLowerCase() === normalized,
|
|
32
145
|
)
|
|
33
146
|
: null;
|
|
34
|
-
return match
|
|
147
|
+
return match ? normalizeDecodedText(match.value) : null;
|
|
35
148
|
}
|
|
36
149
|
|
|
37
150
|
function stripHtml(html) {
|
|
@@ -45,11 +158,23 @@ function stripHtml(html) {
|
|
|
45
158
|
|
|
46
159
|
function extractMessageBody(payload) {
|
|
47
160
|
if (!payload) return '';
|
|
161
|
+
const headers = Array.isArray(payload.headers) ? payload.headers : [];
|
|
162
|
+
const contentType = getHeader(headers, 'Content-Type') || '';
|
|
163
|
+
const transferEncoding = (getHeader(headers, 'Content-Transfer-Encoding') || '').toLowerCase();
|
|
164
|
+
const charset = parseCharset(contentType);
|
|
48
165
|
if (payload.body?.data && payload.mimeType === 'text/plain') {
|
|
49
|
-
|
|
166
|
+
const raw = base64UrlToBuffer(payload.body.data);
|
|
167
|
+
const decoded = transferEncoding.includes('quoted-printable')
|
|
168
|
+
? decodeBytes(decodeQuotedPrintableToBuffer(raw.toString('latin1')), charset)
|
|
169
|
+
: decodeBytes(raw, charset);
|
|
170
|
+
return normalizeDecodedText(decoded);
|
|
50
171
|
}
|
|
51
172
|
if (payload.body?.data && payload.mimeType === 'text/html') {
|
|
52
|
-
|
|
173
|
+
const raw = base64UrlToBuffer(payload.body.data);
|
|
174
|
+
const decoded = transferEncoding.includes('quoted-printable')
|
|
175
|
+
? decodeBytes(decodeQuotedPrintableToBuffer(raw.toString('latin1')), charset)
|
|
176
|
+
: decodeBytes(raw, charset);
|
|
177
|
+
return stripHtml(normalizeDecodedText(decoded));
|
|
53
178
|
}
|
|
54
179
|
if (!Array.isArray(payload.parts)) return '';
|
|
55
180
|
|
|
@@ -135,11 +260,13 @@ async function executeGoogleApiRequest(auth, args, options = {}) {
|
|
|
135
260
|
}
|
|
136
261
|
|
|
137
262
|
module.exports = {
|
|
263
|
+
base64UrlToBuffer,
|
|
138
264
|
coerceStringList,
|
|
139
265
|
ensureParentDir,
|
|
140
266
|
executeGoogleApiRequest,
|
|
141
267
|
extractMessageBody,
|
|
142
268
|
getHeader,
|
|
269
|
+
normalizeDecodedText,
|
|
143
270
|
stringToBase64Url,
|
|
144
271
|
summarizeFile,
|
|
145
272
|
};
|
|
@@ -6,6 +6,7 @@ const {
|
|
|
6
6
|
executeGoogleApiRequest,
|
|
7
7
|
extractMessageBody,
|
|
8
8
|
getHeader,
|
|
9
|
+
normalizeDecodedText,
|
|
9
10
|
stringToBase64Url,
|
|
10
11
|
} = require('./common');
|
|
11
12
|
|
|
@@ -150,7 +151,7 @@ function summarizeMessage(message) {
|
|
|
150
151
|
id: message.id,
|
|
151
152
|
threadId: message.threadId,
|
|
152
153
|
labelIds: Array.isArray(message.labelIds) ? message.labelIds : [],
|
|
153
|
-
snippet: message.snippet || '',
|
|
154
|
+
snippet: normalizeDecodedText(message.snippet || ''),
|
|
154
155
|
internalDate: message.internalDate
|
|
155
156
|
? new Date(Number(message.internalDate)).toISOString()
|
|
156
157
|
: null,
|
|
@@ -218,7 +219,7 @@ async function executeGmailTool(toolName, args, auth) {
|
|
|
218
219
|
: null;
|
|
219
220
|
return {
|
|
220
221
|
id: thread.id || '',
|
|
221
|
-
snippet: thread.snippet || '',
|
|
222
|
+
snippet: normalizeDecodedText(thread.snippet || ''),
|
|
222
223
|
historyId: thread.historyId || null,
|
|
223
224
|
messageCount: Array.isArray(thread.messages)
|
|
224
225
|
? thread.messages.length
|
|
@@ -242,7 +243,7 @@ async function executeGmailTool(toolName, args, auth) {
|
|
|
242
243
|
return {
|
|
243
244
|
id: thread.id || '',
|
|
244
245
|
historyId: thread.historyId || null,
|
|
245
|
-
snippet: thread.snippet || '',
|
|
246
|
+
snippet: normalizeDecodedText(thread.snippet || ''),
|
|
246
247
|
messages: (Array.isArray(thread.messages) ? thread.messages : []).map(
|
|
247
248
|
summarizeMessage,
|
|
248
249
|
),
|
|
@@ -32,6 +32,7 @@ class DiscordPlatform extends BasePlatform {
|
|
|
32
32
|
|
|
33
33
|
this._client = null;
|
|
34
34
|
this._botUser = null;
|
|
35
|
+
this._manualDisconnect = false;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -39,6 +40,7 @@ class DiscordPlatform extends BasePlatform {
|
|
|
39
40
|
async connect() {
|
|
40
41
|
if (!this.token) throw new Error('Discord bot token is required');
|
|
41
42
|
|
|
43
|
+
this._manualDisconnect = false;
|
|
42
44
|
if (this._client) { try { this._client.destroy(); } catch { } this._client = null; }
|
|
43
45
|
|
|
44
46
|
this._client = new Client({
|
|
@@ -66,6 +68,7 @@ class DiscordPlatform extends BasePlatform {
|
|
|
66
68
|
this._client.once('error', (err) => { clearTimeout(timeout); reject(err); });
|
|
67
69
|
this._client.on('error', (err) => console.error('[Discord] Client error:', err.message));
|
|
68
70
|
this._client.on('shardDisconnect', (event, shardId) => {
|
|
71
|
+
if (this._manualDisconnect) return;
|
|
69
72
|
this.status = 'disconnected';
|
|
70
73
|
console.warn(`[Discord] Shard ${shardId} disconnected (${event?.code || 'unknown'})`);
|
|
71
74
|
this.emit('disconnected', {
|
|
@@ -76,15 +79,18 @@ class DiscordPlatform extends BasePlatform {
|
|
|
76
79
|
});
|
|
77
80
|
});
|
|
78
81
|
this._client.on('shardReconnecting', (shardId) => {
|
|
82
|
+
if (this._manualDisconnect) return;
|
|
79
83
|
this.status = 'connecting';
|
|
80
84
|
console.log(`[Discord] Shard ${shardId} reconnecting`);
|
|
81
85
|
});
|
|
82
86
|
this._client.on('shardResume', (_replayedEvents, shardId) => {
|
|
87
|
+
if (this._manualDisconnect) return;
|
|
83
88
|
this.status = 'connected';
|
|
84
89
|
console.log(`[Discord] Shard ${shardId} resumed`);
|
|
85
90
|
this.emit('connected');
|
|
86
91
|
});
|
|
87
92
|
this._client.on('invalidated', () => {
|
|
93
|
+
if (this._manualDisconnect) return;
|
|
88
94
|
this.status = 'logged_out';
|
|
89
95
|
console.warn('[Discord] Session invalidated');
|
|
90
96
|
this.emit('logged_out');
|
|
@@ -96,6 +102,7 @@ class DiscordPlatform extends BasePlatform {
|
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
async disconnect() {
|
|
105
|
+
this._manualDisconnect = true;
|
|
99
106
|
if (this._client) { try { this._client.destroy(); } catch { } this._client = null; }
|
|
100
107
|
this.status = 'disconnected';
|
|
101
108
|
this._botUser = null;
|
|
@@ -107,6 +107,10 @@ class MessagingManager extends EventEmitter {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
async ingestMessage(userId, platformName, msg, options = {}) {
|
|
110
|
+
if (this.isShuttingDown) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
110
114
|
const agentId = this._agentId(userId, {
|
|
111
115
|
...options,
|
|
112
116
|
agentId: options?.agentId ?? msg?.agentId ?? null,
|
|
@@ -136,9 +140,16 @@ class MessagingManager extends EventEmitter {
|
|
|
136
140
|
|
|
137
141
|
const enrichedMsg = { ...msg, agentId, platform: platformName };
|
|
138
142
|
|
|
143
|
+
if (this.isShuttingDown) {
|
|
144
|
+
return enrichedMsg;
|
|
145
|
+
}
|
|
146
|
+
|
|
139
147
|
this.io.to(`user:${userId}`).emit('messaging:message', enrichedMsg);
|
|
140
148
|
|
|
141
149
|
for (const handler of this.messageHandlers) {
|
|
150
|
+
if (this.isShuttingDown) {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
142
153
|
try {
|
|
143
154
|
await handler(userId, enrichedMsg);
|
|
144
155
|
} catch (err) {
|
|
@@ -332,14 +343,14 @@ class MessagingManager extends EventEmitter {
|
|
|
332
343
|
const currentPlatform = () => this.platforms.get(key) === platform;
|
|
333
344
|
|
|
334
345
|
platform.on('qr', (qr) => {
|
|
335
|
-
if (!currentPlatform()) return;
|
|
346
|
+
if (!currentPlatform() || this.isShuttingDown) return;
|
|
336
347
|
this.io.to(`user:${userId}`).emit('messaging:qr', { agentId, platform: platformName, qr });
|
|
337
348
|
db.prepare('UPDATE platform_connections SET status = ?, config = ? WHERE user_id = ? AND agent_id = ? AND platform = ?')
|
|
338
349
|
.run('awaiting_qr', storedConfig, userId, agentId, platformName);
|
|
339
350
|
});
|
|
340
351
|
|
|
341
352
|
platform.on('connected', () => {
|
|
342
|
-
if (!currentPlatform()) return;
|
|
353
|
+
if (!currentPlatform() || this.isShuttingDown) return;
|
|
343
354
|
this.io.to(`user:${userId}`).emit('messaging:connected', { agentId, platform: platformName });
|
|
344
355
|
db.prepare('UPDATE platform_connections SET status = ?, last_connected = datetime(\'now\') WHERE user_id = ? AND agent_id = ? AND platform = ?')
|
|
345
356
|
.run('connected', userId, agentId, platformName);
|
|
@@ -355,7 +366,7 @@ class MessagingManager extends EventEmitter {
|
|
|
355
366
|
});
|
|
356
367
|
|
|
357
368
|
platform.on('logged_out', () => {
|
|
358
|
-
if (!currentPlatform()) return;
|
|
369
|
+
if (!currentPlatform() || this.isShuttingDown) return;
|
|
359
370
|
this.io.to(`user:${userId}`).emit('messaging:logged_out', { agentId, platform: platformName });
|
|
360
371
|
db.prepare('UPDATE platform_connections SET status = ? WHERE user_id = ? AND agent_id = ? AND platform = ?')
|
|
361
372
|
.run('logged_out', userId, agentId, platformName);
|
|
@@ -385,6 +396,7 @@ class MessagingManager extends EventEmitter {
|
|
|
385
396
|
});
|
|
386
397
|
|
|
387
398
|
platform.on('message', async (msg) => {
|
|
399
|
+
if (this.isShuttingDown) return;
|
|
388
400
|
await this.ingestMessage(userId, platformName, msg, { agentId });
|
|
389
401
|
});
|
|
390
402
|
|
|
@@ -16,6 +16,8 @@ class WhatsAppPlatform extends BasePlatform {
|
|
|
16
16
|
this.reconnectAttempts = 0;
|
|
17
17
|
this.maxReconnect = 5;
|
|
18
18
|
this.authDir = config.authDir || AUTH_DIR;
|
|
19
|
+
this._manualDisconnect = false;
|
|
20
|
+
this._reconnectTimer = null;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
_ownIds() {
|
|
@@ -53,6 +55,11 @@ class WhatsAppPlatform extends BasePlatform {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
async connect() {
|
|
58
|
+
this._manualDisconnect = false;
|
|
59
|
+
if (this._reconnectTimer) {
|
|
60
|
+
clearTimeout(this._reconnectTimer);
|
|
61
|
+
this._reconnectTimer = null;
|
|
62
|
+
}
|
|
56
63
|
if (!fs.existsSync(this.authDir)) fs.mkdirSync(this.authDir, { recursive: true });
|
|
57
64
|
|
|
58
65
|
const {
|
|
@@ -108,15 +115,21 @@ class WhatsAppPlatform extends BasePlatform {
|
|
|
108
115
|
|
|
109
116
|
if (connection === 'close') {
|
|
110
117
|
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
111
|
-
const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
|
|
118
|
+
const shouldReconnect = !this._manualDisconnect && statusCode !== DisconnectReason.loggedOut;
|
|
112
119
|
|
|
113
120
|
this.status = 'disconnected';
|
|
114
|
-
this.emit('disconnected', { statusCode, shouldReconnect });
|
|
121
|
+
this.emit('disconnected', { statusCode, shouldReconnect, manual: this._manualDisconnect });
|
|
115
122
|
|
|
116
123
|
if (shouldReconnect && this.reconnectAttempts < this.maxReconnect) {
|
|
117
124
|
this.reconnectAttempts++;
|
|
118
125
|
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
|
119
|
-
setTimeout(() =>
|
|
126
|
+
this._reconnectTimer = setTimeout(() => {
|
|
127
|
+
this._reconnectTimer = null;
|
|
128
|
+
if (this._manualDisconnect) return;
|
|
129
|
+
this.connect().catch((err) => {
|
|
130
|
+
console.error('[WhatsApp] Reconnect failed:', err.message);
|
|
131
|
+
});
|
|
132
|
+
}, delay);
|
|
120
133
|
} else if (statusCode === DisconnectReason.loggedOut) {
|
|
121
134
|
fs.rmSync(this.authDir, { recursive: true, force: true });
|
|
122
135
|
this.emit('logged_out');
|
|
@@ -234,6 +247,11 @@ class WhatsAppPlatform extends BasePlatform {
|
|
|
234
247
|
}
|
|
235
248
|
|
|
236
249
|
async disconnect() {
|
|
250
|
+
this._manualDisconnect = true;
|
|
251
|
+
if (this._reconnectTimer) {
|
|
252
|
+
clearTimeout(this._reconnectTimer);
|
|
253
|
+
this._reconnectTimer = null;
|
|
254
|
+
}
|
|
237
255
|
if (this.sock) {
|
|
238
256
|
this.sock.end();
|
|
239
257
|
this.sock = null;
|
|
@@ -328,6 +346,11 @@ class WhatsAppPlatform extends BasePlatform {
|
|
|
328
346
|
}
|
|
329
347
|
|
|
330
348
|
async logout() {
|
|
349
|
+
this._manualDisconnect = true;
|
|
350
|
+
if (this._reconnectTimer) {
|
|
351
|
+
clearTimeout(this._reconnectTimer);
|
|
352
|
+
this._reconnectTimer = null;
|
|
353
|
+
}
|
|
331
354
|
if (this.sock) {
|
|
332
355
|
await this.sock.logout();
|
|
333
356
|
this.sock = null;
|
|
@@ -807,6 +807,11 @@ function setupWebSocket(io, services) {
|
|
|
807
807
|
// ── Disconnect ──
|
|
808
808
|
|
|
809
809
|
socket.on('disconnect', () => {
|
|
810
|
+
if (!voiceRuntimeManager || typeof voiceRuntimeManager.closeSession !== 'function') {
|
|
811
|
+
socket.data.voiceSessionIds?.clear?.();
|
|
812
|
+
console.log(`[WS] User ${userId} disconnected (${socket.id})`);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
810
815
|
const activeVoiceSessionIds = Array.from(socket.data.voiceSessionIds || []);
|
|
811
816
|
for (const sessionId of activeVoiceSessionIds) {
|
|
812
817
|
void voiceRuntimeManager.closeSession(sessionId, 'socket_disconnected').catch((err) => {
|