neoagent 2.4.1-beta.8 → 2.4.1
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/.env.example +33 -3
- package/LICENSE +111 -56
- package/README.md +8 -3
- package/docs/configuration.md +8 -0
- package/docs/getting-started.md +9 -3
- package/docs/index.md +4 -0
- package/extensions/chrome-browser/background.mjs +45 -7
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +39 -8
- package/extensions/chrome-browser/manifest.json +3 -2
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +15 -2
- package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +3 -0
- package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +10 -34
- package/flutter_app/lib/main_admin.dart +14 -1
- package/flutter_app/lib/main_app_shell.dart +377 -340
- package/flutter_app/lib/main_chat.dart +707 -227
- package/flutter_app/lib/main_controller.dart +338 -46
- package/flutter_app/lib/main_devices.dart +851 -122
- package/flutter_app/lib/main_integrations.dart +255 -6
- package/flutter_app/lib/main_launcher.dart +1 -1
- package/flutter_app/lib/main_model_picker.dart +685 -0
- package/flutter_app/lib/main_models.dart +212 -0
- package/flutter_app/lib/main_navigation.dart +1 -9
- package/flutter_app/lib/main_operations.dart +1417 -614
- package/flutter_app/lib/main_settings.dart +764 -887
- package/flutter_app/lib/main_shared.dart +971 -443
- package/flutter_app/lib/main_spacing.dart +4 -4
- package/flutter_app/lib/main_theme.dart +22 -14
- package/flutter_app/lib/main_unified.dart +5 -72
- package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
- package/flutter_app/lib/src/backend_client.dart +79 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
- package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +205 -35
- package/flutter_app/lib/src/theme/palette.dart +76 -34
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/pubspec.lock +2 -2
- package/flutter_app/pubspec.yaml +7 -1
- package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
- package/flutter_app/tool/generate_desktop_branding.py +154 -152
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +40 -9
- package/flutter_app/web/favicon_light.svg +43 -0
- package/flutter_app/web/icons/Icon-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/windows/runner/main.cpp +7 -1
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/lib/manager.js +445 -83
- package/package.json +17 -3
- package/runtime/paths.js +3 -1
- package/server/admin/access.js +198 -0
- package/server/admin/admin.css +268 -0
- package/server/admin/admin.js +348 -0
- package/server/admin/analytics.js +128 -0
- package/server/admin/index.html +1015 -0
- package/server/admin/login.html +290 -0
- package/server/admin/logo.svg +43 -0
- package/server/admin/sql.js +134 -0
- package/server/admin/users.js +147 -0
- package/server/config/origins.js +3 -1
- package/server/db/database.js +92 -0
- package/server/db/ftsQuery.js +27 -0
- package/server/http/routes.js +1 -0
- package/server/http/static.js +23 -6
- package/server/index.js +1 -1
- package/server/middleware/adminAuth.js +48 -0
- package/server/middleware/auth.js +1 -40
- package/server/public/.last_build_id +1 -1
- package/server/public/app_icon.png +0 -0
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_512.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
- package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/web/icons/Icon-192.png +0 -0
- package/server/public/favicon.png +0 -0
- package/server/public/favicon.svg +40 -9
- package/server/public/favicon_light.svg +43 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/icons/Icon-192-light.png +0 -0
- package/server/public/icons/Icon-192.png +0 -0
- package/server/public/icons/Icon-512-light.png +0 -0
- package/server/public/icons/Icon-512.png +0 -0
- package/server/public/icons/Icon-maskable-192-light.png +0 -0
- package/server/public/icons/Icon-maskable-192.png +0 -0
- package/server/public/icons/Icon-maskable-512-light.png +0 -0
- package/server/public/icons/Icon-maskable-512.png +0 -0
- package/server/public/main.dart.js +88944 -85720
- package/server/routes/admin.js +632 -0
- package/server/routes/agent_profiles.js +3 -0
- package/server/routes/agents.js +1 -1
- package/server/routes/auth.js +3 -1
- package/server/routes/browser.js +14 -0
- package/server/routes/browser_extension.js +21 -4
- package/server/routes/desktop.js +10 -0
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +23 -1
- package/server/routes/messaging.js +2 -0
- package/server/routes/screenHistory.js +14 -8
- package/server/routes/settings.js +14 -2
- package/server/routes/stream.js +12 -3
- package/server/routes/triggers.js +4 -4
- package/server/routes/voice_assistant.js +36 -1
- package/server/routes/workspace.js +86 -0
- package/server/services/account/admin_two_factor.js +132 -0
- package/server/services/account/password_policy.js +6 -1
- package/server/services/ai/completion.js +44 -0
- package/server/services/ai/engine.js +321 -500
- package/server/services/ai/imageAnalysis.js +9 -5
- package/server/services/ai/logFormat.js +46 -0
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/messagingFallback.js +228 -0
- package/server/services/ai/models.js +194 -55
- package/server/services/ai/providerRetry.js +169 -0
- package/server/services/ai/providers/anthropic.js +15 -4
- package/server/services/ai/providers/google.js +38 -11
- package/server/services/ai/providers/grok.js +19 -68
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/providers/nvidia.js +154 -0
- package/server/services/ai/providers/ollama.js +76 -39
- package/server/services/ai/providers/openai.js +20 -31
- package/server/services/ai/providers/openaiCodex.js +6 -2
- package/server/services/ai/providers/openaiCompatible.js +70 -0
- package/server/services/ai/providers/openrouter.js +162 -0
- package/server/services/ai/settings.js +30 -0
- package/server/services/ai/systemPrompt.js +51 -29
- package/server/services/ai/taskAnalysis.js +60 -1
- package/server/services/ai/toolEvidence.js +207 -0
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/ai/tools.js +80 -15
- package/server/services/android/controller.js +36 -1
- package/server/services/browser/controller.js +18 -0
- package/server/services/browser/extension/manifest.js +33 -0
- package/server/services/browser/extension/provider.js +12 -6
- package/server/services/browser/extension/registry.js +229 -18
- package/server/services/desktop/protocol.js +1 -0
- package/server/services/desktop/provider.js +4 -0
- package/server/services/desktop/registry.js +104 -1
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/integrations/google/provider.js +13 -0
- package/server/services/integrations/home_assistant/constants.js +88 -0
- package/server/services/integrations/home_assistant/network.js +207 -0
- package/server/services/integrations/home_assistant/provider.js +249 -0
- package/server/services/integrations/home_assistant/snapshot.js +98 -0
- package/server/services/integrations/home_assistant/tools.js +101 -0
- package/server/services/integrations/manager.js +5 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/trello/provider.js +8 -2
- package/server/services/manager.js +51 -47
- package/server/services/mcp/client.js +208 -268
- package/server/services/mcp/client_support.js +172 -0
- package/server/services/mcp/recovery.js +116 -0
- package/server/services/mcp/tool_operations.js +123 -0
- package/server/services/memory/ingestion.js +185 -370
- package/server/services/memory/ingestion_coverage.js +129 -0
- package/server/services/memory/ingestion_documents.js +123 -0
- package/server/services/memory/ingestion_support.js +171 -0
- package/server/services/memory/intelligence.js +181 -0
- package/server/services/memory/manager.js +476 -32
- package/server/services/messaging/automation.js +71 -134
- package/server/services/messaging/formatting_guides.js +26 -1
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/manager.js +25 -8
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/runtime/manager.js +7 -3
- package/server/services/runtime/settings.js +17 -0
- package/server/services/streaming/android-stream.js +296 -32
- package/server/services/streaming/stream-hub.js +1 -1
- package/server/services/tasks/integration_runtime.js +4 -1
- package/server/services/tasks/runtime.js +415 -56
- package/server/services/tasks/task_repository.js +48 -6
- package/server/services/websocket.js +8 -2
- package/server/services/widgets/service.js +17 -1
- package/server/services/workspace/manager.js +116 -0
- package/server/utils/logger.js +44 -6
- package/server/utils/security.js +3 -0
- package/server/utils/version.js +29 -19
- package/server/services/memory/openhuman_uplift.test.js +0 -98
|
@@ -8,6 +8,9 @@ const {
|
|
|
8
8
|
|
|
9
9
|
const DEFAULT_PAIRING_TTL_MS = 10 * 60 * 1000;
|
|
10
10
|
const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 1000;
|
|
11
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 25 * 1000;
|
|
12
|
+
const DEFAULT_HEARTBEAT_TIMEOUT_MS = 75 * 1000;
|
|
13
|
+
const DEFAULT_PRESENCE_TOUCH_INTERVAL_MS = 15 * 1000;
|
|
11
14
|
|
|
12
15
|
function sha256(value) {
|
|
13
16
|
return crypto.createHash('sha256').update(String(value || '')).digest('hex');
|
|
@@ -42,9 +45,21 @@ class BrowserExtensionRegistry {
|
|
|
42
45
|
this.db = options.db || db;
|
|
43
46
|
this.commandTimeoutMs = Number(options.commandTimeoutMs || process.env.NEOAGENT_BROWSER_EXTENSION_COMMAND_TIMEOUT_MS || DEFAULT_COMMAND_TIMEOUT_MS);
|
|
44
47
|
this.pairingTtlMs = Number(options.pairingTtlMs || process.env.NEOAGENT_BROWSER_EXTENSION_PAIRING_TTL_MS || DEFAULT_PAIRING_TTL_MS);
|
|
48
|
+
this.heartbeatIntervalMs = Number(options.heartbeatIntervalMs || process.env.NEOAGENT_BROWSER_EXTENSION_HEARTBEAT_INTERVAL_MS || DEFAULT_HEARTBEAT_INTERVAL_MS);
|
|
49
|
+
this.heartbeatTimeoutMs = Number(options.heartbeatTimeoutMs || process.env.NEOAGENT_BROWSER_EXTENSION_HEARTBEAT_TIMEOUT_MS || DEFAULT_HEARTBEAT_TIMEOUT_MS);
|
|
50
|
+
this.presenceTouchIntervalMs = Number(options.presenceTouchIntervalMs || process.env.NEOAGENT_BROWSER_EXTENSION_PRESENCE_TOUCH_INTERVAL_MS || DEFAULT_PRESENCE_TOUCH_INTERVAL_MS);
|
|
45
51
|
this.connectionsByUser = new Map();
|
|
46
52
|
}
|
|
47
53
|
|
|
54
|
+
#getUserConnections(userId, create = false) {
|
|
55
|
+
const key = String(userId || '').trim();
|
|
56
|
+
if (!key) return null;
|
|
57
|
+
if (!this.connectionsByUser.has(key) && create) {
|
|
58
|
+
this.connectionsByUser.set(key, new Map());
|
|
59
|
+
}
|
|
60
|
+
return this.connectionsByUser.get(key) || null;
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
createPairingRequest(options = {}) {
|
|
49
64
|
const pairingId = crypto.randomUUID();
|
|
50
65
|
const pairingSecret = randomSecret(48);
|
|
@@ -129,16 +144,42 @@ class BrowserExtensionRegistry {
|
|
|
129
144
|
};
|
|
130
145
|
|
|
131
146
|
const tx = this.db.transaction(() => {
|
|
147
|
+
// Find other active tokens with the same name for this user.
|
|
148
|
+
// If they are currently offline, revoke them to clean up old installs/duplicates.
|
|
149
|
+
const duplicates = this.db.prepare(
|
|
150
|
+
`SELECT id FROM browser_extension_tokens
|
|
151
|
+
WHERE user_id = ? AND name = ? AND status = 'active'`
|
|
152
|
+
).all(row.user_id, extensionName);
|
|
153
|
+
|
|
154
|
+
for (const dup of duplicates) {
|
|
155
|
+
if (!this.isConnected(row.user_id, dup.id)) {
|
|
156
|
+
this.db.prepare(
|
|
157
|
+
`UPDATE browser_extension_tokens
|
|
158
|
+
SET status = 'revoked', revoked_at = datetime('now')
|
|
159
|
+
WHERE id = ?`
|
|
160
|
+
).run(dup.id);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
132
164
|
this.db.prepare(
|
|
133
165
|
`UPDATE browser_extension_pairing_requests
|
|
134
166
|
SET status = 'claimed', claimed_at = datetime('now')
|
|
135
167
|
WHERE id = ?`
|
|
136
168
|
).run(row.id);
|
|
169
|
+
|
|
137
170
|
this.db.prepare(
|
|
138
171
|
`INSERT INTO browser_extension_tokens (
|
|
139
172
|
id, user_id, token_hash, name, status, metadata_json
|
|
140
173
|
) VALUES (?, ?, ?, ?, 'active', ?)`
|
|
141
174
|
).run(tokenId, row.user_id, sha256(token), extensionName, safeJson(metadata));
|
|
175
|
+
|
|
176
|
+
// Auto-select the newly paired token on claim
|
|
177
|
+
const write = this.db.prepare(
|
|
178
|
+
`INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?)
|
|
179
|
+
ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value`
|
|
180
|
+
);
|
|
181
|
+
write.run(row.user_id, 'browser_extension_token_id', tokenId);
|
|
182
|
+
write.run(row.user_id, 'selected_browser_extension_token_id', tokenId);
|
|
142
183
|
});
|
|
143
184
|
tx();
|
|
144
185
|
|
|
@@ -164,7 +205,8 @@ class BrowserExtensionRegistry {
|
|
|
164
205
|
|
|
165
206
|
registerConnection(tokenRow, ws, meta = {}) {
|
|
166
207
|
const userId = String(tokenRow.user_id);
|
|
167
|
-
const
|
|
208
|
+
const userMap = this.#getUserConnections(userId, true);
|
|
209
|
+
const existing = userMap.get(tokenRow.id);
|
|
168
210
|
if (existing && existing.ws !== ws) {
|
|
169
211
|
existing.close('replaced by a newer extension connection');
|
|
170
212
|
}
|
|
@@ -176,8 +218,11 @@ class BrowserExtensionRegistry {
|
|
|
176
218
|
tokenId: tokenRow.id,
|
|
177
219
|
meta: { ...tokenRow.metadata, ...meta },
|
|
178
220
|
timeoutMs: this.commandTimeoutMs,
|
|
221
|
+
heartbeatIntervalMs: this.heartbeatIntervalMs,
|
|
222
|
+
heartbeatTimeoutMs: this.heartbeatTimeoutMs,
|
|
223
|
+
presenceTouchIntervalMs: this.presenceTouchIntervalMs,
|
|
179
224
|
});
|
|
180
|
-
|
|
225
|
+
userMap.set(tokenRow.id, connection);
|
|
181
226
|
this.db.prepare(
|
|
182
227
|
`UPDATE browser_extension_tokens
|
|
183
228
|
SET last_connected_at = datetime('now'), last_seen_at = datetime('now')
|
|
@@ -188,21 +233,102 @@ class BrowserExtensionRegistry {
|
|
|
188
233
|
|
|
189
234
|
unregisterConnection(connection) {
|
|
190
235
|
const userId = String(connection.userId);
|
|
191
|
-
|
|
192
|
-
|
|
236
|
+
const userMap = this.#getUserConnections(userId);
|
|
237
|
+
if (userMap?.get(connection.tokenId) === connection) {
|
|
238
|
+
userMap.delete(connection.tokenId);
|
|
239
|
+
if (userMap.size === 0) {
|
|
240
|
+
this.connectionsByUser.delete(userId);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
getSelectedTokenId(userId) {
|
|
246
|
+
const value = this.db.prepare(
|
|
247
|
+
`SELECT value FROM user_settings WHERE user_id = ? AND key = ?`
|
|
248
|
+
).get(userId, 'browser_extension_token_id')?.value || null;
|
|
249
|
+
const normalized = String(value || '').trim();
|
|
250
|
+
if (!normalized || normalized === 'null') return null;
|
|
251
|
+
const existing = this.db.prepare(
|
|
252
|
+
`SELECT id FROM browser_extension_tokens WHERE user_id = ? AND id = ? AND status = 'active'`
|
|
253
|
+
).get(userId, normalized);
|
|
254
|
+
if (!existing) return null;
|
|
255
|
+
return normalized;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
setSelectedTokenId(userId, tokenId) {
|
|
259
|
+
const normalized = String(tokenId || '').trim();
|
|
260
|
+
if (!normalized) {
|
|
261
|
+
const error = new Error('Browser extension token id is required.');
|
|
262
|
+
error.status = 400;
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
const existing = this.db.prepare(
|
|
266
|
+
`SELECT id FROM browser_extension_tokens WHERE user_id = ? AND id = ? AND status = 'active'`
|
|
267
|
+
).get(userId, normalized);
|
|
268
|
+
if (!existing) {
|
|
269
|
+
const error = new Error('Browser extension device not found.');
|
|
270
|
+
error.status = 404;
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
const write = this.db.prepare(
|
|
274
|
+
`INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?)
|
|
275
|
+
ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value`
|
|
276
|
+
);
|
|
277
|
+
write.run(userId, 'browser_extension_token_id', normalized);
|
|
278
|
+
write.run(userId, 'selected_browser_extension_token_id', normalized);
|
|
279
|
+
return { success: true, selectedTokenId: normalized };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
getConnection(userId, tokenId = null) {
|
|
283
|
+
const userMap = this.#getUserConnections(userId);
|
|
284
|
+
if (!userMap) return null;
|
|
285
|
+
const explicit = String(tokenId || '').trim();
|
|
286
|
+
if (explicit) return userMap.get(explicit) || null;
|
|
287
|
+
|
|
288
|
+
const selected = this.getSelectedTokenId(userId);
|
|
289
|
+
if (selected && userMap.get(selected)?.isOpen()) {
|
|
290
|
+
return userMap.get(selected);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Auto-select online connection if selected is offline/unset
|
|
294
|
+
const online = Array.from(userMap.values()).filter((connection) => connection.isOpen());
|
|
295
|
+
if (online.length > 0) {
|
|
296
|
+
online.sort((a, b) => String(b.connectedAt || '').localeCompare(String(a.connectedAt || '')));
|
|
297
|
+
const activeConnection = online[0];
|
|
298
|
+
try {
|
|
299
|
+
const write = this.db.prepare(
|
|
300
|
+
`INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?)
|
|
301
|
+
ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value`
|
|
302
|
+
);
|
|
303
|
+
write.run(userId, 'browser_extension_token_id', activeConnection.tokenId);
|
|
304
|
+
write.run(userId, 'selected_browser_extension_token_id', activeConnection.tokenId);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
console.error('[Registry] failed to auto-select online extension', err);
|
|
307
|
+
}
|
|
308
|
+
return activeConnection;
|
|
193
309
|
}
|
|
310
|
+
|
|
311
|
+
if (selected) {
|
|
312
|
+
return userMap.get(selected) || null;
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
194
315
|
}
|
|
195
316
|
|
|
196
|
-
|
|
197
|
-
return this.
|
|
317
|
+
isConnected(userId, tokenId = null) {
|
|
318
|
+
return Boolean(this.getConnection(userId, tokenId)?.isOpen());
|
|
198
319
|
}
|
|
199
320
|
|
|
200
|
-
|
|
201
|
-
|
|
321
|
+
touchPresence(userId, tokenId) {
|
|
322
|
+
const userMap = this.#getUserConnections(userId);
|
|
323
|
+
const connection = userMap?.get(String(tokenId));
|
|
324
|
+
if (!connection?.isOpen()) return;
|
|
325
|
+
this.db.prepare(
|
|
326
|
+
`UPDATE browser_extension_tokens SET last_seen_at = datetime('now') WHERE id = ?`
|
|
327
|
+
).run(tokenId);
|
|
202
328
|
}
|
|
203
329
|
|
|
204
330
|
async dispatch(userId, command, payload = {}, options = {}) {
|
|
205
|
-
const connection = this.getConnection(userId);
|
|
331
|
+
const connection = this.getConnection(userId, options.tokenId || payload.tokenId || null);
|
|
206
332
|
if (!connection || !connection.isOpen()) {
|
|
207
333
|
throw new ExtensionBrowserUnavailableError();
|
|
208
334
|
}
|
|
@@ -214,20 +340,41 @@ class BrowserExtensionRegistry {
|
|
|
214
340
|
}
|
|
215
341
|
|
|
216
342
|
getStatus(userId) {
|
|
217
|
-
const
|
|
343
|
+
const userMap = this.#getUserConnections(userId);
|
|
344
|
+
let selectedTokenId = this.getSelectedTokenId(userId);
|
|
345
|
+
let connected = selectedTokenId
|
|
346
|
+
? userMap?.get(selectedTokenId)
|
|
347
|
+
: null;
|
|
348
|
+
if (!connected?.isOpen()) {
|
|
349
|
+
connected = this.getConnection(userId);
|
|
350
|
+
if (connected?.isOpen()) {
|
|
351
|
+
selectedTokenId = connected.tokenId;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const effectiveSelectedTokenId = selectedTokenId || connected?.tokenId || null;
|
|
218
355
|
const tokens = this.db.prepare(
|
|
219
356
|
`SELECT id, name, status, last_connected_at, last_seen_at, revoked_at, created_at, metadata_json
|
|
220
357
|
FROM browser_extension_tokens
|
|
221
358
|
WHERE user_id = ?
|
|
222
359
|
ORDER BY created_at DESC`
|
|
223
|
-
).all(userId).map((row) =>
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
360
|
+
).all(userId).map((row) => {
|
|
361
|
+
const connection = userMap?.get(row.id) || null;
|
|
362
|
+
return {
|
|
363
|
+
...row,
|
|
364
|
+
tokenId: row.id,
|
|
365
|
+
deviceId: row.id,
|
|
366
|
+
connected: Boolean(connection?.isOpen()),
|
|
367
|
+
online: Boolean(connection?.isOpen()),
|
|
368
|
+
selected: row.id === effectiveSelectedTokenId,
|
|
369
|
+
metadata: parseJson(row.metadata_json),
|
|
370
|
+
connectedMeta: connection?.meta || null,
|
|
371
|
+
metadata_json: undefined,
|
|
372
|
+
};
|
|
373
|
+
});
|
|
228
374
|
return {
|
|
229
375
|
connected: Boolean(connected?.isOpen()),
|
|
230
376
|
activeTokenId: connected?.tokenId || null,
|
|
377
|
+
selectedTokenId: effectiveSelectedTokenId,
|
|
231
378
|
tokens,
|
|
232
379
|
connectedMeta: connected?.meta || null,
|
|
233
380
|
};
|
|
@@ -249,7 +396,7 @@ class BrowserExtensionRegistry {
|
|
|
249
396
|
).run(userId);
|
|
250
397
|
}
|
|
251
398
|
|
|
252
|
-
const connection = this.getConnection(userId);
|
|
399
|
+
const connection = this.getConnection(userId, targetTokenId);
|
|
253
400
|
if (connection && (!targetTokenId || connection.tokenId === targetTokenId)) {
|
|
254
401
|
connection.close('extension token revoked');
|
|
255
402
|
}
|
|
@@ -258,25 +405,43 @@ class BrowserExtensionRegistry {
|
|
|
258
405
|
|
|
259
406
|
closeAll() {
|
|
260
407
|
for (const connection of this.connectionsByUser.values()) {
|
|
261
|
-
connection
|
|
408
|
+
if (connection instanceof Map) {
|
|
409
|
+
for (const nested of connection.values()) {
|
|
410
|
+
nested.close('server shutdown');
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
connection.close('server shutdown');
|
|
414
|
+
}
|
|
262
415
|
}
|
|
263
416
|
this.connectionsByUser.clear();
|
|
264
417
|
}
|
|
265
418
|
}
|
|
266
419
|
|
|
267
420
|
class ExtensionBrowserConnection {
|
|
268
|
-
constructor({ registry, ws, userId, tokenId, meta, timeoutMs }) {
|
|
421
|
+
constructor({ registry, ws, userId, tokenId, meta, timeoutMs, heartbeatIntervalMs, heartbeatTimeoutMs, presenceTouchIntervalMs }) {
|
|
269
422
|
this.registry = registry;
|
|
270
423
|
this.ws = ws;
|
|
271
424
|
this.userId = userId;
|
|
272
425
|
this.tokenId = tokenId;
|
|
273
426
|
this.meta = meta || {};
|
|
274
427
|
this.timeoutMs = timeoutMs;
|
|
428
|
+
this.heartbeatIntervalMs = heartbeatIntervalMs;
|
|
429
|
+
this.heartbeatTimeoutMs = heartbeatTimeoutMs;
|
|
430
|
+
this.presenceTouchIntervalMs = presenceTouchIntervalMs;
|
|
275
431
|
this.pending = new Map();
|
|
432
|
+
this.connectedAt = new Date().toISOString();
|
|
433
|
+
this.lastPongAt = Date.now();
|
|
434
|
+
this.lastPresenceTouchAt = 0;
|
|
435
|
+
this.heartbeatTimer = null;
|
|
276
436
|
|
|
277
437
|
ws.on('message', (data) => this.#handleMessage(data));
|
|
438
|
+
ws.on('pong', () => {
|
|
439
|
+
this.lastPongAt = Date.now();
|
|
440
|
+
this.touchPresence();
|
|
441
|
+
});
|
|
278
442
|
ws.on('close', () => this.#closePending(new ExtensionBrowserUnavailableError('Extension browser disconnected.')));
|
|
279
443
|
ws.on('error', (error) => this.#closePending(error));
|
|
444
|
+
this.#startHeartbeat();
|
|
280
445
|
}
|
|
281
446
|
|
|
282
447
|
isOpen() {
|
|
@@ -293,6 +458,21 @@ class ExtensionBrowserConnection {
|
|
|
293
458
|
this.#closePending(new ExtensionBrowserUnavailableError('Extension browser disconnected.'));
|
|
294
459
|
}
|
|
295
460
|
|
|
461
|
+
touchPresence({ force = false } = {}) {
|
|
462
|
+
const now = Date.now();
|
|
463
|
+
const intervalMs = Number(this.presenceTouchIntervalMs);
|
|
464
|
+
if (
|
|
465
|
+
!force
|
|
466
|
+
&& Number.isFinite(intervalMs)
|
|
467
|
+
&& intervalMs > 0
|
|
468
|
+
&& now - this.lastPresenceTouchAt < intervalMs
|
|
469
|
+
) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
this.lastPresenceTouchAt = now;
|
|
473
|
+
this.registry.touchPresence(this.userId, this.tokenId);
|
|
474
|
+
}
|
|
475
|
+
|
|
296
476
|
sendCommand(command, payload = {}, options = {}) {
|
|
297
477
|
if (!this.isOpen()) {
|
|
298
478
|
return Promise.reject(new ExtensionBrowserUnavailableError());
|
|
@@ -317,6 +497,8 @@ class ExtensionBrowserConnection {
|
|
|
317
497
|
}
|
|
318
498
|
|
|
319
499
|
#handleMessage(data) {
|
|
500
|
+
this.lastPongAt = Date.now();
|
|
501
|
+
this.touchPresence();
|
|
320
502
|
let message;
|
|
321
503
|
try {
|
|
322
504
|
message = parseExtensionMessage(data);
|
|
@@ -339,12 +521,41 @@ class ExtensionBrowserConnection {
|
|
|
339
521
|
|
|
340
522
|
#closePending(error) {
|
|
341
523
|
this.registry.unregisterConnection(this);
|
|
524
|
+
if (this.heartbeatTimer) {
|
|
525
|
+
clearInterval(this.heartbeatTimer);
|
|
526
|
+
this.heartbeatTimer = null;
|
|
527
|
+
}
|
|
342
528
|
for (const pending of this.pending.values()) {
|
|
343
529
|
clearTimeout(pending.timer);
|
|
344
530
|
pending.reject(error);
|
|
345
531
|
}
|
|
346
532
|
this.pending.clear();
|
|
347
533
|
}
|
|
534
|
+
|
|
535
|
+
#startHeartbeat() {
|
|
536
|
+
const intervalMs = Number(this.heartbeatIntervalMs);
|
|
537
|
+
const timeoutMs = Number(this.heartbeatTimeoutMs);
|
|
538
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0) return;
|
|
539
|
+
this.touchPresence({ force: true });
|
|
540
|
+
this.heartbeatTimer = setInterval(() => {
|
|
541
|
+
if (!this.isOpen()) {
|
|
542
|
+
this.#closePending(new ExtensionBrowserUnavailableError('Extension browser disconnected.'));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0 && Date.now() - this.lastPongAt > timeoutMs) {
|
|
546
|
+
try { this.ws.terminate(); } catch {}
|
|
547
|
+
this.#closePending(new ExtensionBrowserUnavailableError('Extension browser heartbeat timed out.'));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
this.touchPresence();
|
|
551
|
+
try {
|
|
552
|
+
this.ws.ping();
|
|
553
|
+
} catch (error) {
|
|
554
|
+
this.#closePending(error);
|
|
555
|
+
}
|
|
556
|
+
}, intervalMs);
|
|
557
|
+
this.heartbeatTimer.unref?.();
|
|
558
|
+
}
|
|
348
559
|
}
|
|
349
560
|
|
|
350
561
|
module.exports = {
|
|
@@ -148,6 +148,10 @@ class DesktopProvider {
|
|
|
148
148
|
return this._dispatch(DESKTOP_COMMANDS.CLICK, { ...options, x, y });
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
mouseMove(x, y, options = {}) {
|
|
152
|
+
return this._dispatch(DESKTOP_COMMANDS.MOUSE_MOVE, { ...options, x, y });
|
|
153
|
+
}
|
|
154
|
+
|
|
151
155
|
drag(options = {}) {
|
|
152
156
|
return this._dispatch(DESKTOP_COMMANDS.DRAG, options);
|
|
153
157
|
}
|
|
@@ -10,6 +10,9 @@ const {
|
|
|
10
10
|
} = require('./protocol');
|
|
11
11
|
|
|
12
12
|
const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 1000;
|
|
13
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 25 * 1000;
|
|
14
|
+
const DEFAULT_HEARTBEAT_TIMEOUT_MS = 75 * 1000;
|
|
15
|
+
const DEFAULT_PRESENCE_TOUCH_INTERVAL_MS = 15 * 1000;
|
|
13
16
|
|
|
14
17
|
function safeJson(value) {
|
|
15
18
|
try {
|
|
@@ -47,6 +50,21 @@ class DesktopCompanionRegistry {
|
|
|
47
50
|
|| process.env.NEOAGENT_DESKTOP_COMMAND_TIMEOUT_MS
|
|
48
51
|
|| DEFAULT_COMMAND_TIMEOUT_MS,
|
|
49
52
|
);
|
|
53
|
+
this.heartbeatIntervalMs = Number(
|
|
54
|
+
options.heartbeatIntervalMs
|
|
55
|
+
|| process.env.NEOAGENT_DESKTOP_HEARTBEAT_INTERVAL_MS
|
|
56
|
+
|| DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
57
|
+
);
|
|
58
|
+
this.heartbeatTimeoutMs = Number(
|
|
59
|
+
options.heartbeatTimeoutMs
|
|
60
|
+
|| process.env.NEOAGENT_DESKTOP_HEARTBEAT_TIMEOUT_MS
|
|
61
|
+
|| DEFAULT_HEARTBEAT_TIMEOUT_MS,
|
|
62
|
+
);
|
|
63
|
+
this.presenceTouchIntervalMs = Number(
|
|
64
|
+
options.presenceTouchIntervalMs
|
|
65
|
+
|| process.env.NEOAGENT_DESKTOP_PRESENCE_TOUCH_INTERVAL_MS
|
|
66
|
+
|| DEFAULT_PRESENCE_TOUCH_INTERVAL_MS,
|
|
67
|
+
);
|
|
50
68
|
this.connectionsByUser = new Map();
|
|
51
69
|
}
|
|
52
70
|
|
|
@@ -200,6 +218,9 @@ class DesktopCompanionRegistry {
|
|
|
200
218
|
platform: record.platform,
|
|
201
219
|
},
|
|
202
220
|
timeoutMs: this.commandTimeoutMs,
|
|
221
|
+
heartbeatIntervalMs: this.heartbeatIntervalMs,
|
|
222
|
+
heartbeatTimeoutMs: this.heartbeatTimeoutMs,
|
|
223
|
+
presenceTouchIntervalMs: this.presenceTouchIntervalMs,
|
|
203
224
|
});
|
|
204
225
|
// Install the new connection in the map BEFORE closing the old one.
|
|
205
226
|
// This ensures that when the old socket's async 'close' event fires and
|
|
@@ -275,6 +296,19 @@ class DesktopCompanionRegistry {
|
|
|
275
296
|
return this.getDeviceRecordByDeviceId(userId, deviceId);
|
|
276
297
|
}
|
|
277
298
|
|
|
299
|
+
touchPresence(userId, deviceId) {
|
|
300
|
+
const userMap = this._getUserMap(userId);
|
|
301
|
+
const connection = userMap?.get(String(deviceId));
|
|
302
|
+
if (!connection?.isOpen()) return;
|
|
303
|
+
this.db.prepare(
|
|
304
|
+
`UPDATE desktop_companion_devices
|
|
305
|
+
SET status = 'online',
|
|
306
|
+
last_seen_at = datetime('now'),
|
|
307
|
+
updated_at = datetime('now')
|
|
308
|
+
WHERE user_id = ? AND device_id = ? AND revoked_at IS NULL`
|
|
309
|
+
).run(userId, deviceId);
|
|
310
|
+
}
|
|
311
|
+
|
|
278
312
|
isConnected(userId) {
|
|
279
313
|
const userMap = this._getUserMap(userId);
|
|
280
314
|
return userMap != null && userMap.size > 0;
|
|
@@ -493,7 +527,19 @@ class DesktopCompanionRegistry {
|
|
|
493
527
|
}
|
|
494
528
|
|
|
495
529
|
class DesktopCompanionConnection {
|
|
496
|
-
constructor({
|
|
530
|
+
constructor({
|
|
531
|
+
registry,
|
|
532
|
+
ws,
|
|
533
|
+
userId,
|
|
534
|
+
sessionId,
|
|
535
|
+
deviceId,
|
|
536
|
+
recordId,
|
|
537
|
+
meta,
|
|
538
|
+
timeoutMs,
|
|
539
|
+
heartbeatIntervalMs,
|
|
540
|
+
heartbeatTimeoutMs,
|
|
541
|
+
presenceTouchIntervalMs,
|
|
542
|
+
}) {
|
|
497
543
|
this.registry = registry;
|
|
498
544
|
this.ws = ws;
|
|
499
545
|
this.userId = userId;
|
|
@@ -502,11 +548,22 @@ class DesktopCompanionConnection {
|
|
|
502
548
|
this.recordId = recordId;
|
|
503
549
|
this.meta = meta || {};
|
|
504
550
|
this.timeoutMs = timeoutMs;
|
|
551
|
+
this.heartbeatIntervalMs = heartbeatIntervalMs;
|
|
552
|
+
this.heartbeatTimeoutMs = heartbeatTimeoutMs;
|
|
553
|
+
this.presenceTouchIntervalMs = presenceTouchIntervalMs;
|
|
505
554
|
this.pending = new Map();
|
|
555
|
+
this.lastPongAt = Date.now();
|
|
556
|
+
this.lastPresenceTouchAt = 0;
|
|
557
|
+
this.heartbeatTimer = null;
|
|
506
558
|
|
|
507
559
|
ws.on('message', (data) => this._handleMessage(data));
|
|
560
|
+
ws.on('pong', () => {
|
|
561
|
+
this.lastPongAt = Date.now();
|
|
562
|
+
this.touchPresence();
|
|
563
|
+
});
|
|
508
564
|
ws.on('close', () => this._closePending(new DesktopCompanionUnavailableError('Desktop companion disconnected.')));
|
|
509
565
|
ws.on('error', (error) => this._closePending(error));
|
|
566
|
+
this._startHeartbeat();
|
|
510
567
|
}
|
|
511
568
|
|
|
512
569
|
isOpen() {
|
|
@@ -525,6 +582,21 @@ class DesktopCompanionConnection {
|
|
|
525
582
|
this._closePending(new DesktopCompanionUnavailableError('Desktop companion disconnected.'));
|
|
526
583
|
}
|
|
527
584
|
|
|
585
|
+
touchPresence({ force = false } = {}) {
|
|
586
|
+
const now = Date.now();
|
|
587
|
+
const intervalMs = Number(this.presenceTouchIntervalMs);
|
|
588
|
+
if (
|
|
589
|
+
!force
|
|
590
|
+
&& Number.isFinite(intervalMs)
|
|
591
|
+
&& intervalMs > 0
|
|
592
|
+
&& now - this.lastPresenceTouchAt < intervalMs
|
|
593
|
+
) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
this.lastPresenceTouchAt = now;
|
|
597
|
+
this.registry.touchPresence(this.userId, this.deviceId);
|
|
598
|
+
}
|
|
599
|
+
|
|
528
600
|
sendCommand(command, payload = {}, options = {}) {
|
|
529
601
|
if (!this.isOpen()) {
|
|
530
602
|
return Promise.reject(new DesktopCompanionUnavailableError());
|
|
@@ -549,6 +621,8 @@ class DesktopCompanionConnection {
|
|
|
549
621
|
}
|
|
550
622
|
|
|
551
623
|
_handleMessage(data) {
|
|
624
|
+
this.lastPongAt = Date.now();
|
|
625
|
+
this.touchPresence();
|
|
552
626
|
if (Buffer.isBuffer(data) && data.length > 0 && data[0] === FRAME_TYPE_VIDEO) {
|
|
553
627
|
return;
|
|
554
628
|
}
|
|
@@ -585,6 +659,10 @@ class DesktopCompanionConnection {
|
|
|
585
659
|
}
|
|
586
660
|
|
|
587
661
|
_closePending(error) {
|
|
662
|
+
if (this.heartbeatTimer) {
|
|
663
|
+
clearInterval(this.heartbeatTimer);
|
|
664
|
+
this.heartbeatTimer = null;
|
|
665
|
+
}
|
|
588
666
|
for (const pending of this.pending.values()) {
|
|
589
667
|
clearTimeout(pending.timer);
|
|
590
668
|
pending.reject(error);
|
|
@@ -596,6 +674,31 @@ class DesktopCompanionConnection {
|
|
|
596
674
|
// mark the device offline.
|
|
597
675
|
this.registry.unregisterConnection(this);
|
|
598
676
|
}
|
|
677
|
+
|
|
678
|
+
_startHeartbeat() {
|
|
679
|
+
const intervalMs = Number(this.heartbeatIntervalMs);
|
|
680
|
+
const timeoutMs = Number(this.heartbeatTimeoutMs);
|
|
681
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0) return;
|
|
682
|
+
this.touchPresence({ force: true });
|
|
683
|
+
this.heartbeatTimer = setInterval(() => {
|
|
684
|
+
if (!this.isOpen()) {
|
|
685
|
+
this._closePending(new DesktopCompanionUnavailableError('Desktop companion disconnected.'));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0 && Date.now() - this.lastPongAt > timeoutMs) {
|
|
689
|
+
try { this.ws.terminate(); } catch {}
|
|
690
|
+
this._closePending(new DesktopCompanionUnavailableError('Desktop companion heartbeat timed out.'));
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
this.touchPresence();
|
|
694
|
+
try {
|
|
695
|
+
this.ws.ping();
|
|
696
|
+
} catch (error) {
|
|
697
|
+
this._closePending(error);
|
|
698
|
+
}
|
|
699
|
+
}, intervalMs);
|
|
700
|
+
this.heartbeatTimer.unref?.();
|
|
701
|
+
}
|
|
599
702
|
}
|
|
600
703
|
|
|
601
704
|
module.exports = {
|