neoagent 3.2.1-beta.1 → 3.2.1-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/chrome-browser/background.mjs +318 -88
- package/extensions/chrome-browser/http.mjs +136 -0
- package/extensions/chrome-browser/protocol.mjs +511 -89
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +29 -20
- package/flutter_app/lib/main_models.dart +3 -0
- package/flutter_app/lib/main_operations.dart +334 -321
- package/flutter_app/lib/main_settings.dart +4 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
- package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
- package/flutter_app/windows/runner/flutter_window.cpp +143 -32
- package/lib/manager.js +106 -89
- package/lib/schema_migrations.js +67 -13
- package/package.json +20 -13
- package/runtime/paths.js +49 -5
- package/server/guest_agent.js +52 -30
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +4 -6
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +30803 -30805
- package/server/routes/admin.js +1 -1
- package/server/routes/android.js +30 -34
- package/server/routes/browser.js +23 -15
- package/server/routes/desktop.js +18 -1
- package/server/routes/integrations.js +5 -1
- package/server/routes/memory.js +1 -0
- package/server/routes/settings.js +16 -5
- package/server/routes/social_reach.js +12 -3
- package/server/routes/social_video.js +4 -0
- package/server/services/ai/compaction.js +7 -2
- package/server/services/ai/history.js +44 -5
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +358 -162
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +12 -0
- package/server/services/ai/loop/conversation_loop.js +348 -242
- package/server/services/ai/loop/messaging_delivery.js +129 -57
- package/server/services/ai/loop/model_call_guard.js +91 -0
- package/server/services/ai/loop/model_io.js +20 -45
- package/server/services/ai/loop/tool_dispatch.js +19 -8
- package/server/services/ai/loopPolicy.js +24 -19
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +67 -162
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +111 -0
- package/server/services/ai/providers/anthropic.js +2 -2
- package/server/services/ai/providers/claudeCode.js +21 -33
- package/server/services/ai/providers/githubCopilot.js +41 -20
- package/server/services/ai/providers/google.js +23 -5
- package/server/services/ai/providers/grok.js +4 -3
- package/server/services/ai/providers/grokOauth.js +19 -27
- package/server/services/ai/providers/nvidia.js +10 -5
- package/server/services/ai/providers/ollama.js +111 -84
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +39 -5
- package/server/services/ai/providers/openaiCodex.js +11 -4
- package/server/services/ai/providers/openrouter.js +29 -7
- package/server/services/ai/providers/provider_error.js +36 -0
- package/server/services/ai/settings.js +26 -2
- package/server/services/ai/taskAnalysis.js +5 -1
- package/server/services/ai/terminal_reply.js +45 -0
- package/server/services/ai/toolEvidence.js +58 -29
- package/server/services/ai/tools.js +124 -111
- package/server/services/android/controller.js +770 -237
- package/server/services/android/process.js +140 -0
- package/server/services/android/sdk_download.js +143 -0
- package/server/services/android/uia.js +6 -5
- package/server/services/artifacts/store.js +24 -0
- package/server/services/browser/controller.js +736 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +12 -1
- package/server/services/browser/extension/provider.js +59 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- package/server/services/desktop/gateway.js +41 -4
- package/server/services/desktop/protocol.js +3 -0
- package/server/services/desktop/provider.js +39 -42
- package/server/services/desktop/registry.js +137 -52
- package/server/services/integrations/figma/provider.js +78 -12
- package/server/services/integrations/github/common.js +11 -6
- package/server/services/integrations/github/provider.js +52 -53
- package/server/services/integrations/google/provider.js +55 -19
- package/server/services/integrations/home_assistant/network.js +17 -20
- package/server/services/integrations/home_assistant/provider.js +7 -5
- package/server/services/integrations/home_assistant/tools.js +17 -5
- package/server/services/integrations/http.js +51 -0
- package/server/services/integrations/manager.js +158 -53
- package/server/services/integrations/microsoft/provider.js +80 -13
- package/server/services/integrations/neoarchive/provider.js +55 -29
- package/server/services/integrations/neorecall/client.js +17 -10
- package/server/services/integrations/neorecall/provider.js +20 -11
- package/server/services/integrations/notion/provider.js +16 -13
- package/server/services/integrations/oauth_provider.js +115 -51
- package/server/services/integrations/slack/provider.js +98 -9
- package/server/services/integrations/spotify/provider.js +67 -71
- package/server/services/integrations/trello/provider.js +21 -7
- package/server/services/integrations/weather/provider.js +18 -12
- package/server/services/integrations/whatsapp/provider.js +76 -16
- package/server/services/manager.js +87 -1
- package/server/services/memory/embedding_index.js +20 -8
- package/server/services/memory/embeddings.js +151 -90
- package/server/services/memory/ingestion.js +50 -9
- package/server/services/memory/ingestion_documents.js +13 -3
- package/server/services/memory/manager.js +52 -19
- package/server/services/messaging/automation.js +84 -9
- package/server/services/messaging/http_platforms.js +33 -13
- package/server/services/messaging/inbound_queue.js +78 -24
- package/server/services/messaging/inbound_store.js +224 -0
- package/server/services/messaging/manager.js +326 -51
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/network/http.js +210 -0
- package/server/services/network/safe_request.js +307 -0
- package/server/services/runtime/backends/local-vm.js +214 -66
- package/server/services/runtime/manager.js +17 -12
- package/server/services/social_reach/channels/github.js +10 -4
- package/server/services/social_reach/channels/reddit.js +4 -4
- package/server/services/social_reach/channels/rss.js +2 -2
- package/server/services/social_reach/channels/social_video.js +12 -7
- package/server/services/social_reach/channels/v2ex.js +21 -8
- package/server/services/social_reach/channels/x.js +2 -2
- package/server/services/social_reach/channels/xueqiu.js +5 -5
- package/server/services/social_reach/service.js +9 -6
- package/server/services/social_reach/utils.js +65 -14
- package/server/services/social_video/service.js +160 -50
- package/server/services/tasks/integration_runtime.js +18 -8
- package/server/services/tasks/runtime.js +39 -4
- package/server/services/voice/agentBridge.js +17 -4
- package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
- package/server/services/voice/liveSession.js +31 -0
- package/server/services/voice/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- package/server/services/voice/runtimeManager.js +118 -20
- package/server/services/voice/turnRunner.js +6 -0
- package/server/services/wearable/firmware_manifest.js +51 -13
- package/server/services/wearable/service.js +1 -0
- package/server/utils/abort.js +96 -0
- package/server/utils/cloud-security.js +110 -3
- package/server/utils/files.js +31 -0
- package/server/utils/image_payload.js +95 -0
- package/server/utils/retry.js +107 -0
|
@@ -26,6 +26,7 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
26
26
|
WebSocket? _socket;
|
|
27
27
|
Timer? _reconnectTimer;
|
|
28
28
|
Timer? _connectionWatchdogTimer;
|
|
29
|
+
Timer? _helloTimer;
|
|
29
30
|
Timer? _streamTimer;
|
|
30
31
|
bool _streamCaptureInFlight = false;
|
|
31
32
|
// Set true while a click / drag / scroll / typeText / pressKey command is
|
|
@@ -38,6 +39,13 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
38
39
|
// Tracks the current stream quality so the forced post-input capture can use
|
|
39
40
|
// the same setting without re-parsing the original startStream payload.
|
|
40
41
|
int _currentStreamQuality = 80;
|
|
42
|
+
Future<void> _inputCommandQueue = Future<void>.value();
|
|
43
|
+
final Set<String> _pendingCommandIds = <String>{};
|
|
44
|
+
final Set<String> _pendingShellCommandIds = <String>{};
|
|
45
|
+
final Set<String> _cancelledCommandIds = <String>{};
|
|
46
|
+
int _connectionGeneration = 0;
|
|
47
|
+
int _reconnectAttempt = 0;
|
|
48
|
+
bool _disposed = false;
|
|
41
49
|
|
|
42
50
|
String _backendUrl = '';
|
|
43
51
|
String _sessionCookie = '';
|
|
@@ -63,6 +71,10 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
63
71
|
String get activationId => _activationId;
|
|
64
72
|
Map<String, Object?> get status => _status;
|
|
65
73
|
|
|
74
|
+
void _notify() {
|
|
75
|
+
if (!_disposed) notifyListeners();
|
|
76
|
+
}
|
|
77
|
+
|
|
66
78
|
Future<void> bootstrap(SharedPreferences prefs) async {
|
|
67
79
|
_enabled = prefs.getBool(desktopCompanionEnabledPrefsKey) ?? false;
|
|
68
80
|
// Always start unpaused — paused state must not carry over across restarts.
|
|
@@ -88,13 +100,20 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
88
100
|
required String sessionCookie,
|
|
89
101
|
required bool authenticated,
|
|
90
102
|
}) async {
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
final nextBackendUrl = backendUrl.trim();
|
|
104
|
+
final nextSessionCookie = sessionCookie.trim();
|
|
105
|
+
final sessionChanged =
|
|
106
|
+
_backendUrl != nextBackendUrl || _sessionCookie != nextSessionCookie;
|
|
107
|
+
_backendUrl = nextBackendUrl;
|
|
108
|
+
_sessionCookie = nextSessionCookie;
|
|
93
109
|
_authenticated = authenticated;
|
|
94
110
|
if (!_authenticated || !_enabled || _sessionCookie.isEmpty) {
|
|
95
111
|
await disconnect();
|
|
96
112
|
return;
|
|
97
113
|
}
|
|
114
|
+
if (sessionChanged && (_connected || _connecting || _socket != null)) {
|
|
115
|
+
await disconnect();
|
|
116
|
+
}
|
|
98
117
|
_ensureConnectionWatchdog();
|
|
99
118
|
await _ensureConnected();
|
|
100
119
|
}
|
|
@@ -110,7 +129,7 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
110
129
|
);
|
|
111
130
|
}
|
|
112
131
|
await prefs.setBool(desktopCompanionEnabledPrefsKey, value);
|
|
113
|
-
|
|
132
|
+
_notify();
|
|
114
133
|
if (!value) {
|
|
115
134
|
await disconnect();
|
|
116
135
|
return;
|
|
@@ -122,7 +141,7 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
122
141
|
final normalized = value.trim().isEmpty ? _defaultLabel() : value.trim();
|
|
123
142
|
_label = normalized;
|
|
124
143
|
await prefs.setString(desktopCompanionLabelPrefsKey, normalized);
|
|
125
|
-
|
|
144
|
+
_notify();
|
|
126
145
|
if (_connected) {
|
|
127
146
|
_status = {..._status, 'label': normalized};
|
|
128
147
|
await _sendEvent('statusChanged', <String, Object?>{'label': normalized});
|
|
@@ -131,28 +150,30 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
131
150
|
|
|
132
151
|
Future<void> setPaused(bool value, SharedPreferences prefs) async {
|
|
133
152
|
_paused = value;
|
|
134
|
-
|
|
153
|
+
_notify();
|
|
135
154
|
if (_connected) {
|
|
136
155
|
await _sendEvent('statusChanged', <String, Object?>{'paused': value});
|
|
137
156
|
}
|
|
138
157
|
}
|
|
139
158
|
|
|
140
159
|
Future<void> disconnect() async {
|
|
160
|
+
_connectionGeneration++;
|
|
141
161
|
_reconnectTimer?.cancel();
|
|
142
162
|
_reconnectTimer = null;
|
|
143
163
|
_connectionWatchdogTimer?.cancel();
|
|
144
164
|
_connectionWatchdogTimer = null;
|
|
165
|
+
_helloTimer?.cancel();
|
|
166
|
+
_helloTimer = null;
|
|
145
167
|
_stopStreaming();
|
|
146
168
|
_connecting = false;
|
|
147
169
|
_connected = false;
|
|
170
|
+
_cancelPendingCommands();
|
|
148
171
|
final socket = _socket;
|
|
149
172
|
_socket = null;
|
|
150
173
|
if (socket != null) {
|
|
151
|
-
|
|
152
|
-
await socket.close();
|
|
153
|
-
} catch (_) {}
|
|
174
|
+
await _closeSocket(socket);
|
|
154
175
|
}
|
|
155
|
-
|
|
176
|
+
_notify();
|
|
156
177
|
}
|
|
157
178
|
|
|
158
179
|
Future<void> rotateIdentity(SharedPreferences prefs) async {
|
|
@@ -181,7 +202,7 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
181
202
|
'companionEnabled': _enabled,
|
|
182
203
|
'paused': _paused,
|
|
183
204
|
};
|
|
184
|
-
|
|
205
|
+
_notify();
|
|
185
206
|
if (_connected) {
|
|
186
207
|
await _sendEvent('statusChanged', <String, Object?>{
|
|
187
208
|
'permissions': _status['permissions'],
|
|
@@ -217,65 +238,122 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
217
238
|
}
|
|
218
239
|
|
|
219
240
|
Future<void> _ensureConnected() async {
|
|
220
|
-
if (!_enabled || !_authenticated || _sessionCookie.isEmpty)
|
|
241
|
+
if (_disposed || !_enabled || !_authenticated || _sessionCookie.isEmpty) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
221
244
|
if (_connecting || _connected) return;
|
|
245
|
+
final generation = ++_connectionGeneration;
|
|
222
246
|
_connecting = true;
|
|
223
247
|
_errorMessage = null;
|
|
224
|
-
|
|
248
|
+
_notify();
|
|
249
|
+
WebSocket? connectedSocket;
|
|
225
250
|
try {
|
|
226
251
|
final uri = _desktopWsUri(_backendUrl);
|
|
227
|
-
final socket = await
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
252
|
+
final socket = await _openWebSocket(uri);
|
|
253
|
+
connectedSocket = socket;
|
|
254
|
+
if (_disposed || generation != _connectionGeneration) {
|
|
255
|
+
await _closeSocket(socket);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
231
258
|
socket.pingInterval = const Duration(seconds: 25);
|
|
232
259
|
_socket = socket;
|
|
233
260
|
socket.listen(
|
|
234
|
-
_handleMessage,
|
|
235
|
-
onDone: _handleSocketClosed,
|
|
261
|
+
(dynamic raw) => _handleMessage(socket, generation, raw),
|
|
262
|
+
onDone: () => _handleSocketClosed(socket, generation),
|
|
236
263
|
onError: (Object error, StackTrace stackTrace) {
|
|
237
|
-
|
|
238
|
-
|
|
264
|
+
if (_socket == socket && generation == _connectionGeneration) {
|
|
265
|
+
_errorMessage = '$error';
|
|
266
|
+
}
|
|
267
|
+
_handleSocketClosed(socket, generation);
|
|
239
268
|
},
|
|
240
269
|
cancelOnError: true,
|
|
241
270
|
);
|
|
242
|
-
final hello = await _actions
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
271
|
+
final hello = await _actions
|
|
272
|
+
.buildHello(
|
|
273
|
+
deviceId: _deviceId,
|
|
274
|
+
activationId: _activationId,
|
|
275
|
+
label: _label,
|
|
276
|
+
companionEnabled: _enabled,
|
|
277
|
+
paused: _paused,
|
|
278
|
+
activeDisplayId: _activeDisplayId,
|
|
279
|
+
)
|
|
280
|
+
.timeout(const Duration(seconds: 10));
|
|
281
|
+
if (_socket != socket || generation != _connectionGeneration) return;
|
|
250
282
|
socket.add(
|
|
251
283
|
jsonEncode(<String, Object?>{'type': 'hello', 'device': hello}),
|
|
252
284
|
);
|
|
285
|
+
_helloTimer?.cancel();
|
|
286
|
+
_helloTimer = Timer(const Duration(seconds: 10), () {
|
|
287
|
+
if (_socket != socket ||
|
|
288
|
+
generation != _connectionGeneration ||
|
|
289
|
+
_connected) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
_errorMessage = 'Desktop companion handshake timed out.';
|
|
293
|
+
_handleSocketClosed(socket, generation);
|
|
294
|
+
});
|
|
253
295
|
} catch (error) {
|
|
296
|
+
if (connectedSocket != null && connectedSocket == _socket) {
|
|
297
|
+
_socket = null;
|
|
298
|
+
unawaited(_closeSocket(connectedSocket));
|
|
299
|
+
}
|
|
300
|
+
if (_disposed || generation != _connectionGeneration) return;
|
|
254
301
|
_connecting = false;
|
|
255
302
|
_connected = false;
|
|
256
303
|
_errorMessage = '$error';
|
|
257
|
-
|
|
304
|
+
_notify();
|
|
258
305
|
_scheduleReconnect();
|
|
259
306
|
}
|
|
260
307
|
}
|
|
261
308
|
|
|
262
|
-
|
|
309
|
+
Future<WebSocket> _openWebSocket(Uri uri) async {
|
|
310
|
+
final pending = WebSocket.connect(
|
|
311
|
+
uri.toString(),
|
|
312
|
+
headers: <String, Object>{'Cookie': _sessionCookie},
|
|
313
|
+
);
|
|
314
|
+
try {
|
|
315
|
+
return await pending.timeout(const Duration(seconds: 15));
|
|
316
|
+
} on TimeoutException {
|
|
317
|
+
unawaited(() async {
|
|
318
|
+
try {
|
|
319
|
+
final lateSocket = await pending;
|
|
320
|
+
await _closeSocket(lateSocket);
|
|
321
|
+
} catch (_) {}
|
|
322
|
+
}());
|
|
323
|
+
throw TimeoutException(
|
|
324
|
+
'Desktop companion connection timed out.',
|
|
325
|
+
const Duration(seconds: 15),
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
Future<void> _closeSocket(WebSocket socket) async {
|
|
331
|
+
try {
|
|
332
|
+
await socket.close().timeout(const Duration(seconds: 2));
|
|
333
|
+
} catch (_) {}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
void _handleMessage(WebSocket source, int generation, dynamic raw) {
|
|
337
|
+
if (_socket != source || generation != _connectionGeneration) return;
|
|
263
338
|
try {
|
|
264
339
|
final message = jsonDecode(raw as String);
|
|
265
340
|
if (message is! Map) return;
|
|
266
341
|
final type = message['type']?.toString() ?? '';
|
|
267
342
|
if (type == 'hello') {
|
|
343
|
+
_helloTimer?.cancel();
|
|
344
|
+
_helloTimer = null;
|
|
268
345
|
_connecting = false;
|
|
269
346
|
final ok = message['ok'] == true;
|
|
270
347
|
if (!ok) {
|
|
271
348
|
_connected = false;
|
|
272
349
|
_errorMessage =
|
|
273
350
|
message['error']?.toString() ?? 'Desktop companion rejected.';
|
|
274
|
-
|
|
275
|
-
_handleSocketClosed();
|
|
351
|
+
_notify();
|
|
352
|
+
_handleSocketClosed(source, generation);
|
|
276
353
|
return;
|
|
277
354
|
}
|
|
278
355
|
_connected = true;
|
|
356
|
+
_reconnectAttempt = 0;
|
|
279
357
|
_errorMessage = null;
|
|
280
358
|
final device = message['device'];
|
|
281
359
|
_status = device is Map
|
|
@@ -283,18 +361,37 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
283
361
|
: const <String, Object?>{};
|
|
284
362
|
_activeDisplayId =
|
|
285
363
|
_status['activeDisplayId']?.toString() ?? _activeDisplayId;
|
|
286
|
-
|
|
364
|
+
_notify();
|
|
287
365
|
return;
|
|
288
366
|
}
|
|
289
367
|
if (type != 'command') return;
|
|
290
|
-
|
|
368
|
+
final commandMessage = message.cast<String, Object?>();
|
|
369
|
+
final command = commandMessage['command']?.toString() ?? '';
|
|
370
|
+
final commandId = commandMessage['id']?.toString() ?? '';
|
|
371
|
+
if (command != 'cancelCommand' && commandId.isNotEmpty) {
|
|
372
|
+
_pendingCommandIds.add(commandId);
|
|
373
|
+
if (command == 'executeCommand') {
|
|
374
|
+
_pendingShellCommandIds.add(commandId);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (_inputCommands.contains(command)) {
|
|
378
|
+
final previous = _inputCommandQueue;
|
|
379
|
+
_inputCommandQueue = () async {
|
|
380
|
+
try {
|
|
381
|
+
await previous;
|
|
382
|
+
} catch (_) {}
|
|
383
|
+
await _handleCommand(commandMessage, source, generation);
|
|
384
|
+
}();
|
|
385
|
+
} else {
|
|
386
|
+
unawaited(_handleCommand(commandMessage, source, generation));
|
|
387
|
+
}
|
|
291
388
|
} on FormatException catch (error) {
|
|
292
389
|
_errorMessage = 'Ignored malformed desktop companion message: $error';
|
|
293
|
-
|
|
390
|
+
_notify();
|
|
294
391
|
return;
|
|
295
392
|
} catch (error) {
|
|
296
393
|
_errorMessage = 'Desktop companion message handling failed: $error';
|
|
297
|
-
|
|
394
|
+
_notify();
|
|
298
395
|
return;
|
|
299
396
|
}
|
|
300
397
|
}
|
|
@@ -312,7 +409,11 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
312
409
|
'pressKey',
|
|
313
410
|
};
|
|
314
411
|
|
|
315
|
-
Future<void> _handleCommand(
|
|
412
|
+
Future<void> _handleCommand(
|
|
413
|
+
Map<String, Object?> message,
|
|
414
|
+
WebSocket source,
|
|
415
|
+
int generation,
|
|
416
|
+
) async {
|
|
316
417
|
final id = message['id']?.toString() ?? '';
|
|
317
418
|
final command = message['command']?.toString() ?? '';
|
|
318
419
|
final payload = message['payload'] is Map
|
|
@@ -321,23 +422,52 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
321
422
|
)
|
|
322
423
|
: const <String, Object?>{};
|
|
323
424
|
|
|
425
|
+
if (_socket != source || generation != _connectionGeneration) {
|
|
426
|
+
_pendingCommandIds.remove(id);
|
|
427
|
+
_pendingShellCommandIds.remove(id);
|
|
428
|
+
_cancelledCommandIds.remove(id);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
324
432
|
final isInput = _inputCommands.contains(command);
|
|
325
433
|
if (isInput) _inputCommandInFlight = true;
|
|
326
434
|
|
|
327
435
|
try {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
jsonEncode(<String, Object?>{
|
|
436
|
+
if (command != 'cancelCommand' && _cancelledCommandIds.contains(id)) {
|
|
437
|
+
_sendCommandResult(source, generation, <String, Object?>{
|
|
331
438
|
'type': 'result',
|
|
332
439
|
'id': id,
|
|
333
|
-
'ok':
|
|
334
|
-
'
|
|
335
|
-
|
|
336
|
-
|
|
440
|
+
'ok': false,
|
|
441
|
+
'code': 'COMMAND_CANCELLED',
|
|
442
|
+
'error': 'Desktop companion command was cancelled.',
|
|
443
|
+
});
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
final response = await _dispatchCommand(command, payload, commandId: id);
|
|
447
|
+
if (command != 'cancelCommand' && _cancelledCommandIds.contains(id)) {
|
|
448
|
+
_sendCommandResult(source, generation, <String, Object?>{
|
|
449
|
+
'type': 'result',
|
|
450
|
+
'id': id,
|
|
451
|
+
'ok': false,
|
|
452
|
+
'code': 'COMMAND_CANCELLED',
|
|
453
|
+
'error': 'Desktop companion command was cancelled.',
|
|
454
|
+
});
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
_sendCommandResult(source, generation, <String, Object?>{
|
|
458
|
+
'type': 'result',
|
|
459
|
+
'id': id,
|
|
460
|
+
'ok': true,
|
|
461
|
+
'payload': response,
|
|
462
|
+
});
|
|
337
463
|
// Immediately capture a fresh frame after an input action so the user
|
|
338
464
|
// sees the result of their interaction without waiting for the next
|
|
339
465
|
// timer tick.
|
|
340
|
-
if (isInput &&
|
|
466
|
+
if (isInput &&
|
|
467
|
+
_streamTimer != null &&
|
|
468
|
+
_connected &&
|
|
469
|
+
_socket == source &&
|
|
470
|
+
generation == _connectionGeneration) {
|
|
341
471
|
unawaited(
|
|
342
472
|
_captureAndSendBinaryFrame(
|
|
343
473
|
_currentStreamQuality,
|
|
@@ -347,24 +477,43 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
347
477
|
);
|
|
348
478
|
}
|
|
349
479
|
} catch (error) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}),
|
|
357
|
-
);
|
|
480
|
+
_sendCommandResult(source, generation, <String, Object?>{
|
|
481
|
+
'type': 'result',
|
|
482
|
+
'id': id,
|
|
483
|
+
'ok': false,
|
|
484
|
+
'error': '$error',
|
|
485
|
+
});
|
|
358
486
|
} finally {
|
|
359
487
|
if (isInput) _inputCommandInFlight = false;
|
|
488
|
+
_pendingCommandIds.remove(id);
|
|
489
|
+
_pendingShellCommandIds.remove(id);
|
|
490
|
+
_cancelledCommandIds.remove(id);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
void _sendCommandResult(
|
|
495
|
+
WebSocket source,
|
|
496
|
+
int generation,
|
|
497
|
+
Map<String, Object?> message,
|
|
498
|
+
) {
|
|
499
|
+
if (_socket != source || generation != _connectionGeneration) return;
|
|
500
|
+
try {
|
|
501
|
+
source.add(jsonEncode(message));
|
|
502
|
+
} catch (error) {
|
|
503
|
+
_errorMessage = 'Desktop companion response failed: $error';
|
|
504
|
+
_handleSocketClosed(source, generation);
|
|
360
505
|
}
|
|
361
506
|
}
|
|
362
507
|
|
|
363
508
|
Future<Map<String, Object?>> _dispatchCommand(
|
|
364
509
|
String command,
|
|
365
|
-
Map<String, Object?> payload,
|
|
366
|
-
|
|
367
|
-
|
|
510
|
+
Map<String, Object?> payload, {
|
|
511
|
+
required String commandId,
|
|
512
|
+
}) async {
|
|
513
|
+
if (_paused &&
|
|
514
|
+
command != 'getStatus' &&
|
|
515
|
+
command != 'pauseControl' &&
|
|
516
|
+
command != 'cancelCommand') {
|
|
368
517
|
throw Exception('Desktop companion is paused locally.');
|
|
369
518
|
}
|
|
370
519
|
switch (command) {
|
|
@@ -387,23 +536,23 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
387
536
|
);
|
|
388
537
|
case 'click':
|
|
389
538
|
return _actions.click(
|
|
390
|
-
x: (payload
|
|
391
|
-
y: (payload
|
|
539
|
+
x: _requiredCoordinate(payload, 'x'),
|
|
540
|
+
y: _requiredCoordinate(payload, 'y'),
|
|
392
541
|
button: payload['button']?.toString() ?? 'left',
|
|
393
542
|
displayId: _activeDisplayId,
|
|
394
543
|
);
|
|
395
544
|
case 'mouseMove':
|
|
396
545
|
return _actions.mouseMove(
|
|
397
|
-
x: (payload
|
|
398
|
-
y: (payload
|
|
546
|
+
x: _requiredCoordinate(payload, 'x'),
|
|
547
|
+
y: _requiredCoordinate(payload, 'y'),
|
|
399
548
|
displayId: _activeDisplayId,
|
|
400
549
|
);
|
|
401
550
|
case 'drag':
|
|
402
551
|
return _actions.drag(
|
|
403
|
-
x1: (payload
|
|
404
|
-
y1: (payload
|
|
405
|
-
x2: (payload
|
|
406
|
-
y2: (payload
|
|
552
|
+
x1: _requiredCoordinate(payload, 'x1'),
|
|
553
|
+
y1: _requiredCoordinate(payload, 'y1'),
|
|
554
|
+
x2: _requiredCoordinate(payload, 'x2'),
|
|
555
|
+
y2: _requiredCoordinate(payload, 'y2'),
|
|
407
556
|
durationMs: (payload['durationMs'] as num?)?.round() ?? 280,
|
|
408
557
|
displayId: _activeDisplayId,
|
|
409
558
|
);
|
|
@@ -433,28 +582,54 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
433
582
|
'activeDisplayId': status['activeDisplayId'] ?? 'primary',
|
|
434
583
|
};
|
|
435
584
|
case 'selectDisplay':
|
|
436
|
-
final displayId =
|
|
437
|
-
|
|
585
|
+
final displayId = await _resolveDisplaySelection(
|
|
586
|
+
payload['displayId']?.toString() ?? '',
|
|
587
|
+
);
|
|
438
588
|
final prefs = await SharedPreferences.getInstance();
|
|
439
|
-
await prefs.setString(
|
|
589
|
+
final persisted = await prefs.setString(
|
|
590
|
+
desktopCompanionActiveDisplayPrefsKey,
|
|
591
|
+
displayId,
|
|
592
|
+
);
|
|
593
|
+
if (!persisted) {
|
|
594
|
+
throw StateError('Unable to persist the selected desktop display.');
|
|
595
|
+
}
|
|
596
|
+
_activeDisplayId = displayId;
|
|
440
597
|
_status = <String, Object?>{..._status, 'activeDisplayId': displayId};
|
|
441
|
-
|
|
442
|
-
notifyListeners();
|
|
598
|
+
_notify();
|
|
443
599
|
return <String, Object?>{'success': true, 'activeDisplayId': displayId};
|
|
444
600
|
case 'getTree':
|
|
445
601
|
return _actions.getTree();
|
|
446
602
|
case 'pauseControl':
|
|
447
603
|
final paused = payload['paused'] != false;
|
|
448
604
|
_paused = paused;
|
|
449
|
-
|
|
605
|
+
_notify();
|
|
450
606
|
return <String, Object?>{'success': true, 'paused': _paused};
|
|
451
607
|
case 'executeCommand':
|
|
452
608
|
return _actions.executeShellCommand(
|
|
609
|
+
commandId: commandId,
|
|
453
610
|
command: payload['command']?.toString() ?? '',
|
|
454
611
|
cwd: payload['cwd']?.toString(),
|
|
455
612
|
timeoutMs: (payload['timeout'] as num?)?.toInt(),
|
|
456
613
|
stdinInput: payload['stdin_input']?.toString(),
|
|
614
|
+
requestedPty: payload['pty'] == true,
|
|
615
|
+
inputs: payload['inputs'] is List
|
|
616
|
+
? (payload['inputs'] as List)
|
|
617
|
+
.map((value) => value.toString())
|
|
618
|
+
.toList(growable: false)
|
|
619
|
+
: const <String>[],
|
|
457
620
|
);
|
|
621
|
+
case 'cancelCommand':
|
|
622
|
+
final targetId = payload['commandId']?.toString() ?? '';
|
|
623
|
+
if (targetId.isNotEmpty && _pendingCommandIds.contains(targetId)) {
|
|
624
|
+
_cancelledCommandIds.add(targetId);
|
|
625
|
+
}
|
|
626
|
+
if (_pendingShellCommandIds.contains(targetId)) {
|
|
627
|
+
return _actions.cancelShellCommand(targetId);
|
|
628
|
+
}
|
|
629
|
+
return <String, Object?>{
|
|
630
|
+
'success': targetId.isNotEmpty,
|
|
631
|
+
'cancelled': _cancelledCommandIds.contains(targetId),
|
|
632
|
+
};
|
|
458
633
|
case 'ping':
|
|
459
634
|
return <String, Object?>{'pong': true};
|
|
460
635
|
default:
|
|
@@ -462,49 +637,86 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
462
637
|
}
|
|
463
638
|
}
|
|
464
639
|
|
|
465
|
-
|
|
640
|
+
int _requiredCoordinate(Map<String, Object?> payload, String key) {
|
|
641
|
+
final value = payload[key];
|
|
642
|
+
if (value is! num || !value.isFinite) {
|
|
643
|
+
throw FormatException('$key must be a finite number.');
|
|
644
|
+
}
|
|
645
|
+
return value.round();
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
void _handleSocketClosed(WebSocket source, int generation) {
|
|
649
|
+
if (_socket != source || generation != _connectionGeneration) return;
|
|
650
|
+
_connectionGeneration++;
|
|
651
|
+
_helloTimer?.cancel();
|
|
652
|
+
_helloTimer = null;
|
|
466
653
|
_stopStreaming();
|
|
467
654
|
_socket = null;
|
|
468
655
|
_connecting = false;
|
|
469
656
|
_connected = false;
|
|
470
|
-
|
|
657
|
+
_cancelPendingCommands();
|
|
658
|
+
unawaited(_closeSocket(source));
|
|
659
|
+
if (_disposed) return;
|
|
660
|
+
_notify();
|
|
471
661
|
_scheduleReconnect();
|
|
472
662
|
}
|
|
473
663
|
|
|
664
|
+
void _cancelPendingCommands() {
|
|
665
|
+
for (final commandId in _pendingCommandIds.toList(growable: false)) {
|
|
666
|
+
_cancelledCommandIds.add(commandId);
|
|
667
|
+
if (_pendingShellCommandIds.contains(commandId)) {
|
|
668
|
+
unawaited(_actions.cancelShellCommand(commandId));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
474
673
|
@override
|
|
475
674
|
void dispose() {
|
|
675
|
+
_disposed = true;
|
|
676
|
+
_connectionGeneration++;
|
|
476
677
|
_reconnectTimer?.cancel();
|
|
477
678
|
_reconnectTimer = null;
|
|
478
679
|
_connectionWatchdogTimer?.cancel();
|
|
479
680
|
_connectionWatchdogTimer = null;
|
|
681
|
+
_helloTimer?.cancel();
|
|
682
|
+
_helloTimer = null;
|
|
480
683
|
_stopStreaming();
|
|
481
684
|
_connecting = false;
|
|
482
685
|
_connected = false;
|
|
483
686
|
_enabled = false;
|
|
687
|
+
_cancelPendingCommands();
|
|
484
688
|
final socket = _socket;
|
|
485
689
|
_socket = null;
|
|
486
690
|
if (socket != null) {
|
|
487
|
-
|
|
488
|
-
socket.close();
|
|
489
|
-
} catch (_) {}
|
|
691
|
+
unawaited(_closeSocket(socket));
|
|
490
692
|
}
|
|
491
693
|
super.dispose();
|
|
492
694
|
}
|
|
493
695
|
|
|
494
696
|
void _scheduleReconnect() {
|
|
495
|
-
if (!_enabled || !_authenticated || _sessionCookie.isEmpty)
|
|
697
|
+
if (_disposed || !_enabled || !_authenticated || _sessionCookie.isEmpty) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
496
700
|
_ensureConnectionWatchdog();
|
|
497
701
|
_reconnectTimer?.cancel();
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
702
|
+
final exponentialMs = min(60000, 1000 * (1 << min(_reconnectAttempt, 6)));
|
|
703
|
+
_reconnectAttempt++;
|
|
704
|
+
final jitterMs = Random().nextInt(max(1, exponentialMs ~/ 4));
|
|
705
|
+
_reconnectTimer = Timer(
|
|
706
|
+
Duration(milliseconds: exponentialMs + jitterMs),
|
|
707
|
+
() {
|
|
708
|
+
unawaited(_ensureConnected());
|
|
709
|
+
},
|
|
710
|
+
);
|
|
501
711
|
}
|
|
502
712
|
|
|
503
713
|
void _ensureConnectionWatchdog() {
|
|
504
|
-
if (!_enabled || !_authenticated || _sessionCookie.isEmpty)
|
|
714
|
+
if (_disposed || !_enabled || !_authenticated || _sessionCookie.isEmpty) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
505
717
|
if (_connectionWatchdogTimer != null) return;
|
|
506
718
|
_connectionWatchdogTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
|
507
|
-
if (!_enabled || !_authenticated || _sessionCookie.isEmpty) {
|
|
719
|
+
if (_disposed || !_enabled || !_authenticated || _sessionCookie.isEmpty) {
|
|
508
720
|
_connectionWatchdogTimer?.cancel();
|
|
509
721
|
_connectionWatchdogTimer = null;
|
|
510
722
|
return;
|
|
@@ -530,14 +742,16 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
530
742
|
Future<Map<String, Object?>> _startStreaming(
|
|
531
743
|
Map<String, Object?> payload,
|
|
532
744
|
) async {
|
|
533
|
-
_streamTimer?.cancel();
|
|
534
|
-
final generation = ++_streamGeneration;
|
|
535
745
|
final fps = ((payload['fps'] as num?)?.round() ?? 15).clamp(1, 20);
|
|
536
746
|
final quality = ((payload['quality'] as num?)?.round() ?? 80).clamp(30, 95);
|
|
537
747
|
final displayId = payload['displayId']?.toString().trim();
|
|
748
|
+
var selectedDisplayId = _activeDisplayId;
|
|
538
749
|
if (displayId != null && displayId.isNotEmpty) {
|
|
539
|
-
|
|
750
|
+
selectedDisplayId = await _resolveDisplaySelection(displayId);
|
|
540
751
|
}
|
|
752
|
+
_streamTimer?.cancel();
|
|
753
|
+
final generation = ++_streamGeneration;
|
|
754
|
+
_activeDisplayId = selectedDisplayId;
|
|
541
755
|
final interval = Duration(milliseconds: max(1, (1000 / fps).floor()));
|
|
542
756
|
_frameSeq = 0;
|
|
543
757
|
_currentStreamQuality = quality;
|
|
@@ -553,6 +767,19 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
553
767
|
};
|
|
554
768
|
}
|
|
555
769
|
|
|
770
|
+
Future<String> _resolveDisplaySelection(String requested) async {
|
|
771
|
+
final status = await _actions.getStatus(
|
|
772
|
+
label: _label,
|
|
773
|
+
paused: _paused,
|
|
774
|
+
activeDisplayId: _activeDisplayId,
|
|
775
|
+
);
|
|
776
|
+
return resolveDesktopDisplaySelection(
|
|
777
|
+
status['displays'],
|
|
778
|
+
requested,
|
|
779
|
+
activeDisplayId: status['activeDisplayId']?.toString(),
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
|
|
556
783
|
Map<String, Object?> _stopStreaming() {
|
|
557
784
|
_streamTimer?.cancel();
|
|
558
785
|
_streamTimer = null;
|
|
@@ -601,7 +828,7 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
601
828
|
socket.add(frame);
|
|
602
829
|
} catch (error) {
|
|
603
830
|
_errorMessage = 'Desktop stream capture failed: $error';
|
|
604
|
-
|
|
831
|
+
_notify();
|
|
605
832
|
} finally {
|
|
606
833
|
_streamCaptureInFlight = false;
|
|
607
834
|
}
|
|
@@ -671,7 +898,13 @@ class DesktopCompanionManager extends ChangeNotifier {
|
|
|
671
898
|
Uri _desktopWsUri(String backendUrl) {
|
|
672
899
|
final base = Uri.parse(backendUrl);
|
|
673
900
|
final scheme = base.scheme == 'https' ? 'wss' : 'ws';
|
|
674
|
-
|
|
901
|
+
final basePath = base.path.replaceFirst(RegExp(r'/+$'), '');
|
|
902
|
+
return base.replace(
|
|
903
|
+
scheme: scheme,
|
|
904
|
+
path: '$basePath/api/desktop/ws',
|
|
905
|
+
query: '',
|
|
906
|
+
fragment: '',
|
|
907
|
+
);
|
|
675
908
|
}
|
|
676
909
|
|
|
677
910
|
String _defaultLabel() {
|