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
|
@@ -25,10 +25,19 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
25
25
|
final DesktopCompanionActions _actions;
|
|
26
26
|
WebSocket? _socket;
|
|
27
27
|
Timer? _reconnectTimer;
|
|
28
|
+
Timer? _connectionWatchdogTimer;
|
|
28
29
|
Timer? _streamTimer;
|
|
29
30
|
bool _streamCaptureInFlight = false;
|
|
31
|
+
// Set true while a click / drag / scroll / typeText / pressKey command is
|
|
32
|
+
// being executed. _captureAndSendBinaryFrame respects this flag so it does
|
|
33
|
+
// not compete with the input command for the native bridge or the WebSocket
|
|
34
|
+
// send buffer, and a fresh frame is forced immediately after the action.
|
|
35
|
+
bool _inputCommandInFlight = false;
|
|
30
36
|
int _frameSeq = 0;
|
|
31
37
|
int _streamGeneration = 0;
|
|
38
|
+
// Tracks the current stream quality so the forced post-input capture can use
|
|
39
|
+
// the same setting without re-parsing the original startStream payload.
|
|
40
|
+
int _currentStreamQuality = 80;
|
|
32
41
|
|
|
33
42
|
String _backendUrl = '';
|
|
34
43
|
String _sessionCookie = '';
|
|
@@ -86,6 +95,7 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
86
95
|
await disconnect();
|
|
87
96
|
return;
|
|
88
97
|
}
|
|
98
|
+
_ensureConnectionWatchdog();
|
|
89
99
|
await _ensureConnected();
|
|
90
100
|
}
|
|
91
101
|
|
|
@@ -130,6 +140,8 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
130
140
|
Future<void> disconnect() async {
|
|
131
141
|
_reconnectTimer?.cancel();
|
|
132
142
|
_reconnectTimer = null;
|
|
143
|
+
_connectionWatchdogTimer?.cancel();
|
|
144
|
+
_connectionWatchdogTimer = null;
|
|
133
145
|
_stopStreaming();
|
|
134
146
|
_connecting = false;
|
|
135
147
|
_connected = false;
|
|
@@ -216,6 +228,7 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
216
228
|
uri.toString(),
|
|
217
229
|
headers: <String, Object>{'Cookie': _sessionCookie},
|
|
218
230
|
);
|
|
231
|
+
socket.pingInterval = const Duration(seconds: 25);
|
|
219
232
|
_socket = socket;
|
|
220
233
|
socket.listen(
|
|
221
234
|
_handleMessage,
|
|
@@ -286,6 +299,19 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
286
299
|
}
|
|
287
300
|
}
|
|
288
301
|
|
|
302
|
+
// Commands that interact with the remote machine's input system. While one
|
|
303
|
+
// of these is executing we pause frame captures so the WebSocket send buffer
|
|
304
|
+
// is clear for the result message, and to avoid the native bridge being busy
|
|
305
|
+
// with a screenshot when the click/drag/etc. needs to run.
|
|
306
|
+
static const _inputCommands = <String>{
|
|
307
|
+
'click',
|
|
308
|
+
'mouseMove',
|
|
309
|
+
'drag',
|
|
310
|
+
'scroll',
|
|
311
|
+
'typeText',
|
|
312
|
+
'pressKey',
|
|
313
|
+
};
|
|
314
|
+
|
|
289
315
|
Future<void> _handleCommand(Map<String, Object?> message) async {
|
|
290
316
|
final id = message['id']?.toString() ?? '';
|
|
291
317
|
final command = message['command']?.toString() ?? '';
|
|
@@ -294,6 +320,10 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
294
320
|
(key, value) => MapEntry(key.toString(), value),
|
|
295
321
|
)
|
|
296
322
|
: const <String, Object?>{};
|
|
323
|
+
|
|
324
|
+
final isInput = _inputCommands.contains(command);
|
|
325
|
+
if (isInput) _inputCommandInFlight = true;
|
|
326
|
+
|
|
297
327
|
try {
|
|
298
328
|
final response = await _dispatchCommand(command, payload);
|
|
299
329
|
_socket?.add(
|
|
@@ -304,6 +334,18 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
304
334
|
'payload': response,
|
|
305
335
|
}),
|
|
306
336
|
);
|
|
337
|
+
// Immediately capture a fresh frame after an input action so the user
|
|
338
|
+
// sees the result of their interaction without waiting for the next
|
|
339
|
+
// timer tick.
|
|
340
|
+
if (isInput && _streamTimer != null && _connected) {
|
|
341
|
+
unawaited(
|
|
342
|
+
_captureAndSendBinaryFrame(
|
|
343
|
+
_currentStreamQuality,
|
|
344
|
+
_streamGeneration,
|
|
345
|
+
forced: true,
|
|
346
|
+
),
|
|
347
|
+
);
|
|
348
|
+
}
|
|
307
349
|
} catch (error) {
|
|
308
350
|
_socket?.add(
|
|
309
351
|
jsonEncode(<String, Object?>{
|
|
@@ -313,6 +355,8 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
313
355
|
'error': '$error',
|
|
314
356
|
}),
|
|
315
357
|
);
|
|
358
|
+
} finally {
|
|
359
|
+
if (isInput) _inputCommandInFlight = false;
|
|
316
360
|
}
|
|
317
361
|
}
|
|
318
362
|
|
|
@@ -348,6 +392,12 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
348
392
|
button: payload['button']?.toString() ?? 'left',
|
|
349
393
|
displayId: _activeDisplayId,
|
|
350
394
|
);
|
|
395
|
+
case 'mouseMove':
|
|
396
|
+
return _actions.mouseMove(
|
|
397
|
+
x: (payload['x'] as num?)?.round() ?? 0,
|
|
398
|
+
y: (payload['y'] as num?)?.round() ?? 0,
|
|
399
|
+
displayId: _activeDisplayId,
|
|
400
|
+
);
|
|
351
401
|
case 'drag':
|
|
352
402
|
return _actions.drag(
|
|
353
403
|
x1: (payload['x1'] as num?)?.round() ?? 0,
|
|
@@ -425,6 +475,8 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
425
475
|
void dispose() {
|
|
426
476
|
_reconnectTimer?.cancel();
|
|
427
477
|
_reconnectTimer = null;
|
|
478
|
+
_connectionWatchdogTimer?.cancel();
|
|
479
|
+
_connectionWatchdogTimer = null;
|
|
428
480
|
_stopStreaming();
|
|
429
481
|
_connecting = false;
|
|
430
482
|
_connected = false;
|
|
@@ -441,12 +493,28 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
441
493
|
|
|
442
494
|
void _scheduleReconnect() {
|
|
443
495
|
if (!_enabled || !_authenticated || _sessionCookie.isEmpty) return;
|
|
496
|
+
_ensureConnectionWatchdog();
|
|
444
497
|
_reconnectTimer?.cancel();
|
|
445
498
|
_reconnectTimer = Timer(const Duration(seconds: 5), () {
|
|
446
499
|
unawaited(_ensureConnected());
|
|
447
500
|
});
|
|
448
501
|
}
|
|
449
502
|
|
|
503
|
+
void _ensureConnectionWatchdog() {
|
|
504
|
+
if (!_enabled || !_authenticated || _sessionCookie.isEmpty) return;
|
|
505
|
+
if (_connectionWatchdogTimer != null) return;
|
|
506
|
+
_connectionWatchdogTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
|
507
|
+
if (!_enabled || !_authenticated || _sessionCookie.isEmpty) {
|
|
508
|
+
_connectionWatchdogTimer?.cancel();
|
|
509
|
+
_connectionWatchdogTimer = null;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (!_connected && !_connecting) {
|
|
513
|
+
unawaited(_ensureConnected());
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
450
518
|
Future<void> _sendEvent(String event, Map<String, Object?> payload) async {
|
|
451
519
|
final socket = _socket;
|
|
452
520
|
if (socket == null || !_connected) return;
|
|
@@ -472,6 +540,7 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
472
540
|
}
|
|
473
541
|
final interval = Duration(milliseconds: max(1, (1000 / fps).floor()));
|
|
474
542
|
_frameSeq = 0;
|
|
543
|
+
_currentStreamQuality = quality;
|
|
475
544
|
_streamTimer = Timer.periodic(interval, (_) {
|
|
476
545
|
unawaited(_captureAndSendBinaryFrame(quality, generation));
|
|
477
546
|
});
|
|
@@ -492,7 +561,11 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
492
561
|
return <String, Object?>{'success': true};
|
|
493
562
|
}
|
|
494
563
|
|
|
495
|
-
Future<void> _captureAndSendBinaryFrame(
|
|
564
|
+
Future<void> _captureAndSendBinaryFrame(
|
|
565
|
+
int quality,
|
|
566
|
+
int generation, {
|
|
567
|
+
bool forced = false,
|
|
568
|
+
}) async {
|
|
496
569
|
final socket = _socket;
|
|
497
570
|
if (socket == null ||
|
|
498
571
|
!_connected ||
|
|
@@ -500,6 +573,9 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
500
573
|
generation != _streamGeneration) {
|
|
501
574
|
return;
|
|
502
575
|
}
|
|
576
|
+
// If an input command is actively running, skip this frame unless we were
|
|
577
|
+
// explicitly forced (i.e. this IS the post-input refresh capture).
|
|
578
|
+
if (!forced && _inputCommandInFlight) return;
|
|
503
579
|
_streamCaptureInFlight = true;
|
|
504
580
|
try {
|
|
505
581
|
final snapshot = await _actions.captureSnapshot(
|
|
@@ -44,6 +44,19 @@ class DesktopNativeBridge {
|
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
Future<void> mouseMove({
|
|
48
|
+
required int x,
|
|
49
|
+
required int y,
|
|
50
|
+
String? displayId,
|
|
51
|
+
}) {
|
|
52
|
+
return _channel.invokeMethod<void>('mouseMove', <String, Object?>{
|
|
53
|
+
'x': x,
|
|
54
|
+
'y': y,
|
|
55
|
+
if (displayId != null && displayId.trim().isNotEmpty)
|
|
56
|
+
'displayId': displayId.trim(),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
Future<void> drag({
|
|
48
61
|
required int x1,
|
|
49
62
|
required int y1,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
class PasswordStrengthEvaluation {
|
|
2
|
+
const PasswordStrengthEvaluation({
|
|
3
|
+
required this.score,
|
|
4
|
+
required this.containsUserInfo,
|
|
5
|
+
required this.obviousPattern,
|
|
6
|
+
required this.hasMinimumLength,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
final int score;
|
|
10
|
+
final bool containsUserInfo;
|
|
11
|
+
final bool obviousPattern;
|
|
12
|
+
final bool hasMinimumLength;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
bool hasSequentialPattern(String input) {
|
|
16
|
+
if (input.length < 4) return false;
|
|
17
|
+
|
|
18
|
+
// check for ascending/descending sequences of length >= 4
|
|
19
|
+
for (int i = 0; i <= input.length - 4; i++) {
|
|
20
|
+
bool asc = true;
|
|
21
|
+
bool desc = true;
|
|
22
|
+
for (int j = 0; j < 3; j++) {
|
|
23
|
+
if (input.codeUnitAt(i + j + 1) != input.codeUnitAt(i + j) + 1) asc = false;
|
|
24
|
+
if (input.codeUnitAt(i + j + 1) != input.codeUnitAt(i + j) - 1) desc = false;
|
|
25
|
+
}
|
|
26
|
+
if (asc || desc) return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
PasswordStrengthEvaluation evaluatePasswordStrength({
|
|
32
|
+
required String password,
|
|
33
|
+
String username = '',
|
|
34
|
+
String email = '',
|
|
35
|
+
}) {
|
|
36
|
+
final value = password.trim();
|
|
37
|
+
if (value.isEmpty) {
|
|
38
|
+
return const PasswordStrengthEvaluation(
|
|
39
|
+
score: 0,
|
|
40
|
+
containsUserInfo: false,
|
|
41
|
+
obviousPattern: false,
|
|
42
|
+
hasMinimumLength: false,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
final lower = RegExp(r'[a-z]').hasMatch(value);
|
|
47
|
+
final upper = RegExp(r'[A-Z]').hasMatch(value);
|
|
48
|
+
final digits = RegExp(r'[0-9]').hasMatch(value);
|
|
49
|
+
final symbols = RegExp(r'[^A-Za-z0-9]').hasMatch(value);
|
|
50
|
+
final variety = <bool>[lower, upper, digits, symbols]
|
|
51
|
+
.where((item) => item)
|
|
52
|
+
.length;
|
|
53
|
+
final normalized = value.toLowerCase();
|
|
54
|
+
final userHints = <String>{
|
|
55
|
+
username.trim().toLowerCase(),
|
|
56
|
+
email.trim().toLowerCase(),
|
|
57
|
+
email.trim().toLowerCase().split('@').first,
|
|
58
|
+
}.where((item) => item.length >= 3);
|
|
59
|
+
final containsUserInfo = userHints.any(normalized.contains);
|
|
60
|
+
final obviousPattern =
|
|
61
|
+
RegExp(r'(.)\1\1').hasMatch(value) ||
|
|
62
|
+
normalized.contains('password') ||
|
|
63
|
+
normalized.contains('123456') ||
|
|
64
|
+
normalized.contains('qwerty') ||
|
|
65
|
+
normalized.contains('letmein') ||
|
|
66
|
+
normalized.contains('welcome') ||
|
|
67
|
+
normalized.contains('admin') ||
|
|
68
|
+
normalized.contains('neoagent') ||
|
|
69
|
+
hasSequentialPattern(normalized);
|
|
70
|
+
|
|
71
|
+
var score = 0;
|
|
72
|
+
if (value.length >= 8) score += 1;
|
|
73
|
+
if (value.length >= 12) score += 1;
|
|
74
|
+
if (variety >= 3) score += 1;
|
|
75
|
+
if (variety == 4 || value.length >= 16) score += 1;
|
|
76
|
+
if (containsUserInfo || obviousPattern) score -= 1;
|
|
77
|
+
|
|
78
|
+
return PasswordStrengthEvaluation(
|
|
79
|
+
score: score.clamp(0, 4),
|
|
80
|
+
containsUserInfo: containsUserInfo,
|
|
81
|
+
obviousPattern: obviousPattern,
|
|
82
|
+
hasMinimumLength: value.length >= 8,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import 'dart:async';
|
|
1
2
|
import 'dart:typed_data';
|
|
2
3
|
|
|
3
4
|
import 'package:flutter/material.dart';
|
|
@@ -9,19 +10,31 @@ class StreamRenderer extends StatefulWidget {
|
|
|
9
10
|
required this.socket,
|
|
10
11
|
required this.deviceId,
|
|
11
12
|
required this.platform,
|
|
12
|
-
|
|
13
|
+
this.remoteResolution,
|
|
13
14
|
this.onTap,
|
|
15
|
+
this.onSwipe,
|
|
14
16
|
this.onType,
|
|
17
|
+
this.onHover,
|
|
18
|
+
this.onFirstFrame,
|
|
19
|
+
this.onFrameTimeout,
|
|
20
|
+
this.firstFrameTimeout = const Duration(seconds: 8),
|
|
15
21
|
this.fit = BoxFit.contain,
|
|
22
|
+
this.alignment = Alignment.center,
|
|
16
23
|
});
|
|
17
24
|
|
|
18
25
|
final io.Socket socket;
|
|
19
26
|
final String deviceId;
|
|
20
27
|
final String platform;
|
|
21
|
-
final Size remoteResolution;
|
|
28
|
+
final Size? remoteResolution;
|
|
22
29
|
final void Function(double x, double y)? onTap;
|
|
30
|
+
final void Function(double x1, double y1, double x2, double y2)? onSwipe;
|
|
23
31
|
final void Function(String text)? onType;
|
|
32
|
+
final void Function(double x, double y)? onHover;
|
|
33
|
+
final VoidCallback? onFirstFrame;
|
|
34
|
+
final VoidCallback? onFrameTimeout;
|
|
35
|
+
final Duration firstFrameTimeout;
|
|
24
36
|
final BoxFit fit;
|
|
37
|
+
final Alignment alignment;
|
|
25
38
|
|
|
26
39
|
@override
|
|
27
40
|
State<StreamRenderer> createState() => _StreamRendererState();
|
|
@@ -29,6 +42,17 @@ class StreamRenderer extends StatefulWidget {
|
|
|
29
42
|
|
|
30
43
|
class _StreamRendererState extends State<StreamRenderer> {
|
|
31
44
|
Uint8List? _frame;
|
|
45
|
+
Size? _frameSize;
|
|
46
|
+
ImageStream? _imageStream;
|
|
47
|
+
ImageStreamListener? _imageListener;
|
|
48
|
+
Offset? _dragStart;
|
|
49
|
+
Offset? _dragEnd;
|
|
50
|
+
|
|
51
|
+
Timer? _hoverThrottleTimer;
|
|
52
|
+
Timer? _firstFrameTimer;
|
|
53
|
+
bool _firstFrameTimedOut = false;
|
|
54
|
+
Offset? _pendingHoverOffset;
|
|
55
|
+
DateTime _lastHoverTime = DateTime.fromMillisecondsSinceEpoch(0);
|
|
32
56
|
|
|
33
57
|
@override
|
|
34
58
|
void initState() {
|
|
@@ -38,6 +62,7 @@ class _StreamRendererState extends State<StreamRenderer> {
|
|
|
38
62
|
'deviceId': widget.deviceId,
|
|
39
63
|
'platform': widget.platform,
|
|
40
64
|
});
|
|
65
|
+
_startFirstFrameTimer();
|
|
41
66
|
}
|
|
42
67
|
|
|
43
68
|
@override
|
|
@@ -48,6 +73,10 @@ class _StreamRendererState extends State<StreamRenderer> {
|
|
|
48
73
|
oldWidget.platform == widget.platform) {
|
|
49
74
|
return;
|
|
50
75
|
}
|
|
76
|
+
_frameSize = null;
|
|
77
|
+
_frame = null;
|
|
78
|
+
_detachImageListener();
|
|
79
|
+
_firstFrameTimer?.cancel();
|
|
51
80
|
oldWidget.socket.off('stream:frame', _onFrame);
|
|
52
81
|
oldWidget.socket.emit('stream:unsubscribe', <String, Object?>{
|
|
53
82
|
'deviceId': oldWidget.deviceId,
|
|
@@ -58,6 +87,24 @@ class _StreamRendererState extends State<StreamRenderer> {
|
|
|
58
87
|
'deviceId': widget.deviceId,
|
|
59
88
|
'platform': widget.platform,
|
|
60
89
|
});
|
|
90
|
+
_startFirstFrameTimer();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
void _startFirstFrameTimer() {
|
|
94
|
+
_firstFrameTimer?.cancel();
|
|
95
|
+
_firstFrameTimedOut = false;
|
|
96
|
+
if (widget.onFrameTimeout == null ||
|
|
97
|
+
widget.firstFrameTimeout <= Duration.zero ||
|
|
98
|
+
_frame != null) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
_firstFrameTimer = Timer(widget.firstFrameTimeout, () {
|
|
102
|
+
if (!mounted || _frame != null) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
_firstFrameTimedOut = true;
|
|
106
|
+
widget.onFrameTimeout?.call();
|
|
107
|
+
});
|
|
61
108
|
}
|
|
62
109
|
|
|
63
110
|
void _onFrame(dynamic data) {
|
|
@@ -84,9 +131,47 @@ class _StreamRendererState extends State<StreamRenderer> {
|
|
|
84
131
|
_ => null,
|
|
85
132
|
};
|
|
86
133
|
if (frame == null || frame.isEmpty || !mounted) return;
|
|
134
|
+
final hadFrame = _frame != null;
|
|
135
|
+
if (!hadFrame) {
|
|
136
|
+
_firstFrameTimer?.cancel();
|
|
137
|
+
_firstFrameTimer = null;
|
|
138
|
+
if (!_firstFrameTimedOut) {
|
|
139
|
+
widget.onFirstFrame?.call();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (_frameSize == null) {
|
|
143
|
+
_resolveFrameSize(frame);
|
|
144
|
+
}
|
|
87
145
|
setState(() => _frame = frame);
|
|
88
146
|
}
|
|
89
147
|
|
|
148
|
+
void _resolveFrameSize(Uint8List frame) {
|
|
149
|
+
_detachImageListener();
|
|
150
|
+
final provider = MemoryImage(frame);
|
|
151
|
+
final stream = provider.resolve(const ImageConfiguration());
|
|
152
|
+
final listener = ImageStreamListener((image, _) {
|
|
153
|
+
if (!mounted || _frameSize != null) return;
|
|
154
|
+
setState(() {
|
|
155
|
+
_frameSize = Size(
|
|
156
|
+
image.image.width.toDouble(),
|
|
157
|
+
image.image.height.toDouble(),
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
_detachImageListener();
|
|
161
|
+
});
|
|
162
|
+
_imageStream = stream;
|
|
163
|
+
_imageListener = listener;
|
|
164
|
+
stream.addListener(listener);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
void _detachImageListener() {
|
|
168
|
+
if (_imageStream != null && _imageListener != null) {
|
|
169
|
+
_imageStream!.removeListener(_imageListener!);
|
|
170
|
+
}
|
|
171
|
+
_imageStream = null;
|
|
172
|
+
_imageListener = null;
|
|
173
|
+
}
|
|
174
|
+
|
|
90
175
|
@override
|
|
91
176
|
Widget build(BuildContext context) {
|
|
92
177
|
final frame = _frame;
|
|
@@ -95,61 +180,146 @@ class _StreamRendererState extends State<StreamRenderer> {
|
|
|
95
180
|
}
|
|
96
181
|
return LayoutBuilder(
|
|
97
182
|
builder: (context, constraints) {
|
|
98
|
-
return
|
|
99
|
-
|
|
100
|
-
onTapDown: widget.onTap == null
|
|
183
|
+
return MouseRegion(
|
|
184
|
+
onHover: widget.onHover == null
|
|
101
185
|
? null
|
|
102
|
-
: (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
186
|
+
: (event) =>
|
|
187
|
+
_handleHoverEvent(event.localPosition, constraints.biggest),
|
|
188
|
+
child: GestureDetector(
|
|
189
|
+
behavior: HitTestBehavior.opaque,
|
|
190
|
+
onTapDown: widget.onTap == null
|
|
191
|
+
? null
|
|
192
|
+
: (details) => _handleTap(details, constraints.biggest),
|
|
193
|
+
onPanStart: widget.onSwipe == null
|
|
194
|
+
? null
|
|
195
|
+
: (details) {
|
|
196
|
+
_dragStart = details.localPosition;
|
|
197
|
+
_dragEnd = details.localPosition;
|
|
198
|
+
},
|
|
199
|
+
onPanUpdate: widget.onSwipe == null
|
|
200
|
+
? null
|
|
201
|
+
: (details) {
|
|
202
|
+
_dragEnd = details.localPosition;
|
|
203
|
+
},
|
|
204
|
+
onPanEnd: widget.onSwipe == null
|
|
205
|
+
? null
|
|
206
|
+
: (_) {
|
|
207
|
+
final start = _dragStart;
|
|
208
|
+
final end = _dragEnd;
|
|
209
|
+
_dragStart = null;
|
|
210
|
+
_dragEnd = null;
|
|
211
|
+
if (start == null ||
|
|
212
|
+
end == null ||
|
|
213
|
+
(start - end).distance < 12) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
_handleSwipe(start, end, constraints.biggest);
|
|
217
|
+
},
|
|
218
|
+
child: Image.memory(
|
|
219
|
+
frame,
|
|
220
|
+
gaplessPlayback: true,
|
|
221
|
+
fit: widget.fit,
|
|
222
|
+
alignment: widget.alignment,
|
|
223
|
+
width: constraints.maxWidth,
|
|
224
|
+
height: constraints.maxHeight,
|
|
225
|
+
),
|
|
109
226
|
),
|
|
110
227
|
);
|
|
111
228
|
},
|
|
112
229
|
);
|
|
113
230
|
}
|
|
114
231
|
|
|
232
|
+
void _handleHoverEvent(Offset localPosition, Size boxSize) {
|
|
233
|
+
if (widget.onHover == null) return;
|
|
234
|
+
_pendingHoverOffset = localPosition;
|
|
235
|
+
final now = DateTime.now();
|
|
236
|
+
final elapsed = now.difference(_lastHoverTime);
|
|
237
|
+
const throttleDuration = Duration(milliseconds: 70);
|
|
238
|
+
|
|
239
|
+
if (elapsed >= throttleDuration) {
|
|
240
|
+
_sendPendingHover(boxSize);
|
|
241
|
+
} else {
|
|
242
|
+
_hoverThrottleTimer ??= Timer(throttleDuration - elapsed, () {
|
|
243
|
+
_hoverThrottleTimer = null;
|
|
244
|
+
_sendPendingHover(boxSize);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
void _sendPendingHover(Size boxSize) {
|
|
250
|
+
final offset = _pendingHoverOffset;
|
|
251
|
+
if (offset == null) return;
|
|
252
|
+
_pendingHoverOffset = null;
|
|
253
|
+
_lastHoverTime = DateTime.now();
|
|
254
|
+
|
|
255
|
+
final point = _mapToRemote(offset, boxSize);
|
|
256
|
+
if (point != null) {
|
|
257
|
+
widget.onHover?.call(point.dx, point.dy);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
115
261
|
void _handleTap(TapDownDetails details, Size boxSize) {
|
|
116
|
-
final
|
|
117
|
-
if (
|
|
262
|
+
final point = _mapToRemote(details.localPosition, boxSize);
|
|
263
|
+
if (point == null) {
|
|
118
264
|
return;
|
|
119
265
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
renderHeight = boxSize.height;
|
|
128
|
-
renderWidth = renderHeight * imageAspect;
|
|
129
|
-
offsetX = (boxSize.width - renderWidth) / 2;
|
|
130
|
-
} else {
|
|
131
|
-
renderWidth = boxSize.width;
|
|
132
|
-
renderHeight = renderWidth / imageAspect;
|
|
133
|
-
offsetY = (boxSize.height - renderHeight) / 2;
|
|
134
|
-
}
|
|
135
|
-
final localX = details.localPosition.dx - offsetX;
|
|
136
|
-
final localY = details.localPosition.dy - offsetY;
|
|
137
|
-
if (localX < 0 || localY < 0 || localX > renderWidth || localY > renderHeight) {
|
|
266
|
+
widget.onTap?.call(point.dx, point.dy);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
void _handleSwipe(Offset start, Offset end, Size boxSize) {
|
|
270
|
+
final mappedStart = _mapToRemote(start, boxSize);
|
|
271
|
+
final mappedEnd = _mapToRemote(end, boxSize);
|
|
272
|
+
if (mappedStart == null || mappedEnd == null) {
|
|
138
273
|
return;
|
|
139
274
|
}
|
|
140
|
-
widget.
|
|
141
|
-
|
|
142
|
-
|
|
275
|
+
widget.onSwipe?.call(
|
|
276
|
+
mappedStart.dx,
|
|
277
|
+
mappedStart.dy,
|
|
278
|
+
mappedEnd.dx,
|
|
279
|
+
mappedEnd.dy,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
Offset? _mapToRemote(Offset localPosition, Size boxSize) {
|
|
284
|
+
final remote = widget.remoteResolution ?? _frameSize;
|
|
285
|
+
if (remote == null ||
|
|
286
|
+
remote.width <= 0 ||
|
|
287
|
+
remote.height <= 0 ||
|
|
288
|
+
boxSize.width <= 0 ||
|
|
289
|
+
boxSize.height <= 0) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
final remoteSize = Size(remote.width, remote.height);
|
|
293
|
+
final fitted = applyBoxFit(widget.fit, remoteSize, boxSize);
|
|
294
|
+
final sourceRect = widget.alignment.inscribe(
|
|
295
|
+
fitted.source,
|
|
296
|
+
Offset.zero & remoteSize,
|
|
297
|
+
);
|
|
298
|
+
final destRect = widget.alignment.inscribe(
|
|
299
|
+
fitted.destination,
|
|
300
|
+
Offset.zero & boxSize,
|
|
301
|
+
);
|
|
302
|
+
if (!destRect.contains(localPosition)) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
final localX = localPosition.dx - destRect.left;
|
|
306
|
+
final localY = localPosition.dy - destRect.top;
|
|
307
|
+
return Offset(
|
|
308
|
+
sourceRect.left + localX * fitted.source.width / destRect.width,
|
|
309
|
+
sourceRect.top + localY * fitted.source.height / destRect.height,
|
|
143
310
|
);
|
|
144
311
|
}
|
|
145
312
|
|
|
146
313
|
@override
|
|
147
314
|
void dispose() {
|
|
315
|
+
_hoverThrottleTimer?.cancel();
|
|
316
|
+
_firstFrameTimer?.cancel();
|
|
148
317
|
widget.socket.emit('stream:unsubscribe', <String, Object?>{
|
|
149
318
|
'deviceId': widget.deviceId,
|
|
150
319
|
'platform': widget.platform,
|
|
151
320
|
});
|
|
152
321
|
widget.socket.off('stream:frame', _onFrame);
|
|
322
|
+
_detachImageListener();
|
|
153
323
|
super.dispose();
|
|
154
324
|
}
|
|
155
325
|
}
|