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
|
@@ -7,6 +7,22 @@ const FORCE_KILL_GRACE_MS = 5000;
|
|
|
7
7
|
const MAX_STDOUT_CHARS = 50000;
|
|
8
8
|
const MAX_STDERR_CHARS = 10000;
|
|
9
9
|
|
|
10
|
+
function abortedResult(command, cwd, startedAt = Date.now()) {
|
|
11
|
+
return {
|
|
12
|
+
exitCode: null,
|
|
13
|
+
stdout: '',
|
|
14
|
+
stderr: 'Command aborted before it started.',
|
|
15
|
+
killed: true,
|
|
16
|
+
timedOut: false,
|
|
17
|
+
aborted: true,
|
|
18
|
+
signal: null,
|
|
19
|
+
durationMs: Date.now() - startedAt,
|
|
20
|
+
pid: null,
|
|
21
|
+
command,
|
|
22
|
+
cwd,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
10
26
|
function resolveDefaultShell() {
|
|
11
27
|
const candidates = [
|
|
12
28
|
process.env.SHELL,
|
|
@@ -63,6 +79,22 @@ function terminateProcess(proc, signal = 'SIGTERM') {
|
|
|
63
79
|
proc.kill?.(signal) || proc.kill?.();
|
|
64
80
|
}
|
|
65
81
|
|
|
82
|
+
function terminateWithEscalation(proc, isActive) {
|
|
83
|
+
terminateProcess(proc, 'SIGTERM');
|
|
84
|
+
if (proc.__neoagentForceKillTimer) return;
|
|
85
|
+
proc.__neoagentForceKillTimer = setTimeout(() => {
|
|
86
|
+
if (isActive()) terminateProcess(proc, 'SIGKILL');
|
|
87
|
+
}, FORCE_KILL_GRACE_MS);
|
|
88
|
+
proc.__neoagentForceKillTimer.unref?.();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function clearForceKill(proc) {
|
|
92
|
+
if (proc?.__neoagentForceKillTimer) {
|
|
93
|
+
clearTimeout(proc.__neoagentForceKillTimer);
|
|
94
|
+
proc.__neoagentForceKillTimer = null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
66
98
|
function shellSupportsPipefail(shellPath) {
|
|
67
99
|
const normalized = String(shellPath || '').trim().toLowerCase();
|
|
68
100
|
return /(?:^|\/)(?:bash|zsh|ksh|mksh|yash)$/.test(normalized);
|
|
@@ -111,6 +143,7 @@ class CLIExecutor {
|
|
|
111
143
|
const cwd = options.cwd || process.env.HOME;
|
|
112
144
|
const timeout = clampTimeout(options.timeout, DEFAULT_TIMEOUT_MS);
|
|
113
145
|
const stdinInput = options.stdinInput;
|
|
146
|
+
if (options.signal?.aborted) return abortedResult(command, cwd);
|
|
114
147
|
|
|
115
148
|
return new Promise((resolve) => {
|
|
116
149
|
let stdout = '';
|
|
@@ -131,6 +164,13 @@ class CLIExecutor {
|
|
|
131
164
|
const pid = proc.pid;
|
|
132
165
|
this.activeProcesses.set(pid, proc);
|
|
133
166
|
options.onSpawn?.(pid);
|
|
167
|
+
const onAbort = () => {
|
|
168
|
+
killed = true;
|
|
169
|
+
proc.__neoagentKilled = true;
|
|
170
|
+
proc.__neoagentKillReason = 'aborted';
|
|
171
|
+
terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
|
|
172
|
+
};
|
|
173
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
134
174
|
|
|
135
175
|
proc.stdout.on('data', (data) => {
|
|
136
176
|
stdout += data.toString();
|
|
@@ -156,14 +196,14 @@ class CLIExecutor {
|
|
|
156
196
|
timedOut = true;
|
|
157
197
|
proc.__neoagentKilled = true;
|
|
158
198
|
proc.__neoagentKillReason = 'timeout';
|
|
159
|
-
|
|
160
|
-
setTimeout(() => {
|
|
161
|
-
if (!proc.killed) terminateProcess(proc, 'SIGKILL');
|
|
162
|
-
}, FORCE_KILL_GRACE_MS);
|
|
199
|
+
terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
|
|
163
200
|
}, timeout);
|
|
201
|
+
timer.unref?.();
|
|
164
202
|
|
|
165
203
|
proc.on('close', (code, signal) => {
|
|
166
204
|
clearTimeout(timer);
|
|
205
|
+
clearForceKill(proc);
|
|
206
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
167
207
|
this.activeProcesses.delete(pid);
|
|
168
208
|
const durationMs = Date.now() - startedAt;
|
|
169
209
|
|
|
@@ -173,6 +213,7 @@ class CLIExecutor {
|
|
|
173
213
|
stderr: truncateOutput(stderr.trim(), MAX_STDERR_CHARS),
|
|
174
214
|
killed: killed || proc.__neoagentKilled === true,
|
|
175
215
|
timedOut: timedOut || proc.__neoagentKillReason === 'timeout',
|
|
216
|
+
aborted: proc.__neoagentKillReason === 'aborted',
|
|
176
217
|
signal: signal || null,
|
|
177
218
|
durationMs,
|
|
178
219
|
pid,
|
|
@@ -183,6 +224,8 @@ class CLIExecutor {
|
|
|
183
224
|
|
|
184
225
|
proc.on('error', (err) => {
|
|
185
226
|
clearTimeout(timer);
|
|
227
|
+
clearForceKill(proc);
|
|
228
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
186
229
|
this.activeProcesses.delete(pid);
|
|
187
230
|
resolve({
|
|
188
231
|
exitCode: -1,
|
|
@@ -204,6 +247,7 @@ class CLIExecutor {
|
|
|
204
247
|
async executeInteractive(command, inputs = [], options = {}) {
|
|
205
248
|
const cwd = options.cwd || process.env.HOME;
|
|
206
249
|
const timeout = clampTimeout(options.timeout, DEFAULT_INTERACTIVE_TIMEOUT_MS);
|
|
250
|
+
if (options.signal?.aborted) return abortedResult(command, cwd);
|
|
207
251
|
|
|
208
252
|
return new Promise((resolve) => {
|
|
209
253
|
let output = '';
|
|
@@ -231,6 +275,13 @@ class CLIExecutor {
|
|
|
231
275
|
const pid = proc.pid;
|
|
232
276
|
this.activeProcesses.set(pid, proc);
|
|
233
277
|
options.onSpawn?.(pid);
|
|
278
|
+
const onAbort = () => {
|
|
279
|
+
killed = true;
|
|
280
|
+
proc.__neoagentKilled = true;
|
|
281
|
+
proc.__neoagentKillReason = 'aborted';
|
|
282
|
+
terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
|
|
283
|
+
};
|
|
284
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
234
285
|
|
|
235
286
|
proc.onData((data) => {
|
|
236
287
|
output += data;
|
|
@@ -256,11 +307,14 @@ class CLIExecutor {
|
|
|
256
307
|
timedOut = true;
|
|
257
308
|
proc.__neoagentKilled = true;
|
|
258
309
|
proc.__neoagentKillReason = 'timeout';
|
|
259
|
-
proc.
|
|
310
|
+
terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
|
|
260
311
|
}, timeout);
|
|
312
|
+
timer.unref?.();
|
|
261
313
|
|
|
262
314
|
proc.onExit(({ exitCode, signal }) => {
|
|
263
315
|
clearTimeout(timer);
|
|
316
|
+
clearForceKill(proc);
|
|
317
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
264
318
|
this.activeProcesses.delete(pid);
|
|
265
319
|
|
|
266
320
|
const cleanOutput = output.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '').trim();
|
|
@@ -270,6 +324,7 @@ class CLIExecutor {
|
|
|
270
324
|
stderr: '',
|
|
271
325
|
killed: killed || proc.__neoagentKilled === true,
|
|
272
326
|
timedOut: timedOut || proc.__neoagentKillReason === 'timeout',
|
|
327
|
+
aborted: proc.__neoagentKillReason === 'aborted',
|
|
273
328
|
signal: typeof signal === 'number' ? String(signal) : signal || null,
|
|
274
329
|
durationMs: Date.now() - startedAt,
|
|
275
330
|
pid,
|
|
@@ -286,8 +341,7 @@ class CLIExecutor {
|
|
|
286
341
|
if (proc) {
|
|
287
342
|
proc.__neoagentKilled = true;
|
|
288
343
|
proc.__neoagentKillReason = reason;
|
|
289
|
-
|
|
290
|
-
this.activeProcesses.delete(pid);
|
|
344
|
+
terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
|
|
291
345
|
return true;
|
|
292
346
|
}
|
|
293
347
|
return false;
|
|
@@ -301,9 +355,8 @@ class CLIExecutor {
|
|
|
301
355
|
for (const [pid, proc] of this.activeProcesses) {
|
|
302
356
|
proc.__neoagentKilled = true;
|
|
303
357
|
proc.__neoagentKillReason = reason;
|
|
304
|
-
|
|
358
|
+
terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
|
|
305
359
|
}
|
|
306
|
-
this.activeProcesses.clear();
|
|
307
360
|
}
|
|
308
361
|
}
|
|
309
362
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const { WebSocketServer } = require('ws');
|
|
2
4
|
const {
|
|
3
5
|
DESKTOP_COMPANION_WS_PATH,
|
|
@@ -15,6 +17,9 @@ const {
|
|
|
15
17
|
const UPGRADE_RATE_LIMIT_WINDOW_MS = 60 * 1000;
|
|
16
18
|
const UPGRADE_RATE_LIMIT_MAX_ATTEMPTS = 30;
|
|
17
19
|
const UPGRADE_RATE_LIMIT_ENTRY_TTL_MS = 10 * 60 * 1000;
|
|
20
|
+
const UPGRADE_AUTH_TIMEOUT_MS = 5000;
|
|
21
|
+
const HELLO_TIMEOUT_MS = 5000;
|
|
22
|
+
const MAX_HELLO_BYTES = 64 * 1024;
|
|
18
23
|
|
|
19
24
|
function rejectUpgrade(socket, statusCode, message) {
|
|
20
25
|
try {
|
|
@@ -88,6 +93,8 @@ function bindDesktopCompanionGateway(httpServer, app, sessionMiddleware, streamH
|
|
|
88
93
|
});
|
|
89
94
|
const upgradeAttempts = new Map();
|
|
90
95
|
const upgradeThrottleObserver = createUpgradeThrottleObserver();
|
|
96
|
+
let closing = false;
|
|
97
|
+
let closePromise = null;
|
|
91
98
|
|
|
92
99
|
if (app?.locals) {
|
|
93
100
|
app.locals.getDesktopGatewayRateLimitSnapshot = () => upgradeThrottleObserver.snapshot();
|
|
@@ -115,7 +122,7 @@ function bindDesktopCompanionGateway(httpServer, app, sessionMiddleware, streamH
|
|
|
115
122
|
return true;
|
|
116
123
|
}
|
|
117
124
|
|
|
118
|
-
|
|
125
|
+
const handleUpgrade = (req, socket, head) => {
|
|
119
126
|
let url;
|
|
120
127
|
try {
|
|
121
128
|
url = new URL(req.url, 'http://localhost');
|
|
@@ -125,6 +132,10 @@ function bindDesktopCompanionGateway(httpServer, app, sessionMiddleware, streamH
|
|
|
125
132
|
if (url.pathname !== DESKTOP_COMPANION_WS_PATH) {
|
|
126
133
|
return;
|
|
127
134
|
}
|
|
135
|
+
if (closing) {
|
|
136
|
+
rejectUpgrade(socket, 503, 'Service Unavailable');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
128
139
|
|
|
129
140
|
const remoteAddress = remoteAddressFromRequest(req);
|
|
130
141
|
if (!allowUpgradeAttempt(remoteAddress)) {
|
|
@@ -133,7 +144,16 @@ function bindDesktopCompanionGateway(httpServer, app, sessionMiddleware, streamH
|
|
|
133
144
|
return;
|
|
134
145
|
}
|
|
135
146
|
|
|
147
|
+
const authTimer = setTimeout(() => {
|
|
148
|
+
rejectUpgrade(socket, 504, 'Gateway Timeout');
|
|
149
|
+
}, UPGRADE_AUTH_TIMEOUT_MS);
|
|
150
|
+
authTimer.unref?.();
|
|
136
151
|
sessionMiddleware(req, {}, (err) => {
|
|
152
|
+
clearTimeout(authTimer);
|
|
153
|
+
if (closing || socket.destroyed) {
|
|
154
|
+
if (!socket.destroyed) rejectUpgrade(socket, 503, 'Service Unavailable');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
137
157
|
if (err) {
|
|
138
158
|
rejectUpgrade(socket, 500, 'Session Error');
|
|
139
159
|
return;
|
|
@@ -155,11 +175,18 @@ function bindDesktopCompanionGateway(httpServer, app, sessionMiddleware, streamH
|
|
|
155
175
|
if (!initialized) {
|
|
156
176
|
try { ws.close(1008, 'Desktop companion hello timed out'); } catch {}
|
|
157
177
|
}
|
|
158
|
-
},
|
|
178
|
+
}, HELLO_TIMEOUT_MS);
|
|
179
|
+
helloTimer.unref?.();
|
|
180
|
+
const clearHelloTimer = () => clearTimeout(helloTimer);
|
|
181
|
+
ws.once('close', clearHelloTimer);
|
|
182
|
+
ws.once('error', clearHelloTimer);
|
|
159
183
|
|
|
160
184
|
ws.once('message', (data) => {
|
|
161
185
|
clearTimeout(helloTimer);
|
|
162
186
|
try {
|
|
187
|
+
if (Buffer.byteLength(data) > MAX_HELLO_BYTES) {
|
|
188
|
+
throw Object.assign(new Error('Desktop companion hello is too large.'), { status: 413 });
|
|
189
|
+
}
|
|
163
190
|
const message = parseDesktopMessage(data);
|
|
164
191
|
if (!isDesktopCompanionHello(message)) {
|
|
165
192
|
throw Object.assign(new Error('Desktop companion hello is required.'), { status: 400 });
|
|
@@ -227,11 +254,21 @@ function bindDesktopCompanionGateway(httpServer, app, sessionMiddleware, streamH
|
|
|
227
254
|
});
|
|
228
255
|
});
|
|
229
256
|
});
|
|
230
|
-
}
|
|
257
|
+
};
|
|
258
|
+
httpServer.on('upgrade', handleUpgrade);
|
|
231
259
|
|
|
232
260
|
if (app?.locals) {
|
|
233
261
|
app.locals.desktopCompanionGateway = {
|
|
234
|
-
close: () =>
|
|
262
|
+
close: () => {
|
|
263
|
+
if (closePromise) return closePromise;
|
|
264
|
+
closing = true;
|
|
265
|
+
httpServer.removeListener('upgrade', handleUpgrade);
|
|
266
|
+
for (const client of wss.clients) {
|
|
267
|
+
try { client.terminate(); } catch {}
|
|
268
|
+
}
|
|
269
|
+
closePromise = new Promise((resolve) => wss.close(() => resolve()));
|
|
270
|
+
return closePromise;
|
|
271
|
+
},
|
|
235
272
|
};
|
|
236
273
|
}
|
|
237
274
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const DESKTOP_COMPANION_WS_PATH = '/api/desktop/ws';
|
|
2
4
|
|
|
3
5
|
const DESKTOP_COMMANDS = Object.freeze({
|
|
@@ -17,6 +19,7 @@ const DESKTOP_COMMANDS = Object.freeze({
|
|
|
17
19
|
GET_TREE: 'getTree',
|
|
18
20
|
PAUSE_CONTROL: 'pauseControl',
|
|
19
21
|
EXECUTE_COMMAND: 'executeCommand',
|
|
22
|
+
CANCEL_COMMAND: 'cancelCommand',
|
|
20
23
|
PING: 'ping',
|
|
21
24
|
MOUSE_MOVE: 'mouseMove',
|
|
22
25
|
});
|
|
@@ -1,28 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const fs = require('fs');
|
|
2
4
|
const path = require('path');
|
|
3
5
|
const { DATA_DIR } = require('../../../runtime/paths');
|
|
6
|
+
const { writeBufferAtomic } = require('../../utils/files');
|
|
7
|
+
const { decodeBase64Image } = require('../../utils/image_payload');
|
|
4
8
|
const {
|
|
5
9
|
DESKTOP_COMMANDS,
|
|
6
|
-
DesktopCompanionSelectionError,
|
|
7
10
|
DesktopCompanionUnavailableError,
|
|
8
11
|
} = require('./protocol');
|
|
9
12
|
|
|
10
13
|
const SCREENSHOTS_DIR = path.join(DATA_DIR, 'screenshots');
|
|
11
14
|
if (!fs.existsSync(SCREENSHOTS_DIR)) fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
12
15
|
|
|
13
|
-
function extractBase64Image(value) {
|
|
14
|
-
const text = String(value || '');
|
|
15
|
-
if (!text) return null;
|
|
16
|
-
const match = text.match(/^data:image\/(?:png|jpeg|jpg);base64,(.+)$/i);
|
|
17
|
-
return match ? match[1] : text;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function guessExtension(result = {}) {
|
|
21
|
-
const mime = String(result.contentType || result.mimeType || 'image/png').toLowerCase();
|
|
22
|
-
if (mime.includes('jpeg') || mime.includes('jpg')) return 'jpg';
|
|
23
|
-
return 'png';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
16
|
class DesktopProvider {
|
|
27
17
|
constructor(options = {}) {
|
|
28
18
|
this.registry = options.registry;
|
|
@@ -36,24 +26,21 @@ class DesktopProvider {
|
|
|
36
26
|
}
|
|
37
27
|
}
|
|
38
28
|
|
|
39
|
-
_writeScreenshotArtifact(
|
|
40
|
-
const buffer = Buffer.from(base64, 'base64');
|
|
41
|
-
const extension = guessExtension(result);
|
|
42
|
-
const contentType = result.contentType || (extension === 'jpg' ? 'image/jpeg' : 'image/png');
|
|
29
|
+
async _writeScreenshotArtifact(image, result = {}, options = {}) {
|
|
43
30
|
if (this.artifactStore && this.userId != null) {
|
|
44
|
-
const artifact = this.artifactStore.
|
|
31
|
+
const artifact = await this.artifactStore.createBufferArtifact(this.userId, {
|
|
45
32
|
kind: 'desktop-screenshot',
|
|
46
33
|
backend: 'desktop-companion',
|
|
47
|
-
extension,
|
|
48
|
-
contentType,
|
|
34
|
+
extension: image.extension,
|
|
35
|
+
contentType: image.contentType,
|
|
49
36
|
filenameBase: 'desktop-companion-screenshot',
|
|
37
|
+
content: image.buffer,
|
|
38
|
+
signal: options.signal,
|
|
50
39
|
metadata: {
|
|
51
40
|
deviceId: result.device?.deviceId || null,
|
|
52
41
|
displayId: result.displayId || result.device?.activeDisplayId || null,
|
|
53
42
|
},
|
|
54
43
|
});
|
|
55
|
-
fs.writeFileSync(artifact.storagePath, buffer);
|
|
56
|
-
this.artifactStore.finalizeFile(artifact.artifactId, artifact.storagePath);
|
|
57
44
|
return {
|
|
58
45
|
screenshotPath: artifact.url,
|
|
59
46
|
artifactId: artifact.artifactId,
|
|
@@ -62,9 +49,9 @@ class DesktopProvider {
|
|
|
62
49
|
};
|
|
63
50
|
}
|
|
64
51
|
|
|
65
|
-
const filename = `desktop_${Date.now()}_${Math.random().toString(16).slice(2)}.${extension}`;
|
|
52
|
+
const filename = `desktop_${Date.now()}_${Math.random().toString(16).slice(2)}.${image.extension}`;
|
|
66
53
|
const fullPath = path.join(SCREENSHOTS_DIR, filename);
|
|
67
|
-
|
|
54
|
+
await writeBufferAtomic(fullPath, image.buffer, { signal: options.signal });
|
|
68
55
|
return {
|
|
69
56
|
screenshotPath: `/screenshots/${filename}`,
|
|
70
57
|
artifactId: null,
|
|
@@ -73,13 +60,14 @@ class DesktopProvider {
|
|
|
73
60
|
};
|
|
74
61
|
}
|
|
75
62
|
|
|
76
|
-
_materialize(result) {
|
|
63
|
+
async _materialize(result, options = {}) {
|
|
77
64
|
if (!result || typeof result !== 'object') return result;
|
|
78
65
|
const raw = result.screenshotDataUrl || result.screenshotData || result.screenshotBase64;
|
|
79
66
|
if (!raw) return result;
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
67
|
+
const image = decodeBase64Image(raw, {
|
|
68
|
+
allowedTypes: ['image/png', 'image/jpeg'],
|
|
69
|
+
});
|
|
70
|
+
const screenshot = await this._writeScreenshotArtifact(image, result, options);
|
|
83
71
|
const next = { ...result, ...screenshot };
|
|
84
72
|
delete next.screenshotDataUrl;
|
|
85
73
|
delete next.screenshotData;
|
|
@@ -89,16 +77,19 @@ class DesktopProvider {
|
|
|
89
77
|
|
|
90
78
|
async _dispatch(command, payload = {}, options = {}) {
|
|
91
79
|
this._assertReady();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
80
|
+
const safePayload = { ...(payload || {}) };
|
|
81
|
+
const signal = options.signal || safePayload.signal || null;
|
|
82
|
+
const timeoutMs = options.timeoutMs ?? safePayload.timeoutMs;
|
|
83
|
+
delete safePayload.signal;
|
|
84
|
+
delete safePayload.timeoutMs;
|
|
85
|
+
const result = await this.registry.dispatch(
|
|
86
|
+
this.userId,
|
|
87
|
+
safePayload.deviceId || null,
|
|
88
|
+
command,
|
|
89
|
+
safePayload,
|
|
90
|
+
{ ...options, signal, timeoutMs },
|
|
91
|
+
);
|
|
92
|
+
return this._materialize(result, { signal });
|
|
102
93
|
}
|
|
103
94
|
|
|
104
95
|
getStatus() {
|
|
@@ -121,9 +112,9 @@ class DesktopProvider {
|
|
|
121
112
|
return this.registry.revoke(this.userId, deviceId);
|
|
122
113
|
}
|
|
123
114
|
|
|
124
|
-
pauseDevice(deviceId, paused = true) {
|
|
115
|
+
pauseDevice(deviceId, paused = true, options = {}) {
|
|
125
116
|
this._assertReady();
|
|
126
|
-
return this.registry.pause(this.userId, deviceId, paused);
|
|
117
|
+
return this.registry.pause(this.userId, deviceId, paused, options);
|
|
127
118
|
}
|
|
128
119
|
|
|
129
120
|
screenshot(options = {}) {
|
|
@@ -137,7 +128,7 @@ class DesktopProvider {
|
|
|
137
128
|
|
|
138
129
|
stopStream(options = {}) {
|
|
139
130
|
this._assertReady();
|
|
140
|
-
return this.registry.stopStream(this.userId, options.deviceId || null);
|
|
131
|
+
return this.registry.stopStream(this.userId, options.deviceId || null, options);
|
|
141
132
|
}
|
|
142
133
|
|
|
143
134
|
observe(options = {}) {
|
|
@@ -192,6 +183,12 @@ class DesktopProvider {
|
|
|
192
183
|
stdin_input: options.stdinInput || null,
|
|
193
184
|
pty: options.pty === true,
|
|
194
185
|
inputs: options.inputs || [],
|
|
186
|
+
signal: options.signal,
|
|
187
|
+
}, {
|
|
188
|
+
signal: options.signal,
|
|
189
|
+
timeoutMs: Number(options.timeout || 0) > 0
|
|
190
|
+
? Number(options.timeout) + 10_000
|
|
191
|
+
: undefined,
|
|
195
192
|
});
|
|
196
193
|
}
|
|
197
194
|
}
|