neoagent 3.2.1-beta.2 → 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 +347 -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 +8 -41
- 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 +15 -28
- package/server/services/ai/providers/githubCopilot.js +36 -16
- 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 +13 -22
- package/server/services/ai/providers/nvidia.js +10 -5
- package/server/services/ai/providers/ollama.js +102 -82
- 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
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
export const EXTENSION_PROTOCOL_VERSION = 1;
|
|
2
|
+
|
|
3
|
+
export const MESSAGE_TYPES = Object.freeze({
|
|
4
|
+
COMMAND: 'command',
|
|
5
|
+
RESULT: 'result',
|
|
6
|
+
URL_VALIDATION_REQUEST: 'urlValidationRequest',
|
|
7
|
+
URL_VALIDATION_RESULT: 'urlValidationResult',
|
|
8
|
+
});
|
|
9
|
+
|
|
1
10
|
export const COMMANDS = Object.freeze({
|
|
2
11
|
LAUNCH: 'launch',
|
|
3
12
|
NAVIGATE: 'navigate',
|
|
@@ -13,8 +22,21 @@ export const COMMANDS = Object.freeze({
|
|
|
13
22
|
CLOSE: 'close',
|
|
14
23
|
GET_PAGE_INFO: 'getPageInfo',
|
|
15
24
|
GET_COOKIES: 'getCookies',
|
|
25
|
+
CANCEL_COMMAND: 'cancelCommand',
|
|
16
26
|
});
|
|
17
27
|
|
|
28
|
+
function abortError(signal) {
|
|
29
|
+
if (signal?.reason instanceof Error) return signal.reason;
|
|
30
|
+
const error = new Error(String(signal?.reason || 'Browser extension command aborted.'));
|
|
31
|
+
error.name = 'AbortError';
|
|
32
|
+
error.code = 'ABORT_ERR';
|
|
33
|
+
return error;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function throwIfAborted(signal) {
|
|
37
|
+
if (signal?.aborted) throw abortError(signal);
|
|
38
|
+
}
|
|
39
|
+
|
|
18
40
|
function chromeCall(chromeApi, namespace, method, ...args) {
|
|
19
41
|
return new Promise((resolve, reject) => {
|
|
20
42
|
chromeApi[namespace][method](...args, (result) => {
|
|
@@ -28,8 +50,50 @@ function chromeCall(chromeApi, namespace, method, ...args) {
|
|
|
28
50
|
});
|
|
29
51
|
}
|
|
30
52
|
|
|
31
|
-
function
|
|
32
|
-
|
|
53
|
+
function waitForAbortable(promise, signal = null) {
|
|
54
|
+
throwIfAborted(signal);
|
|
55
|
+
if (!signal) return promise;
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
let settled = false;
|
|
58
|
+
const finish = (callback, value) => {
|
|
59
|
+
if (settled) return;
|
|
60
|
+
settled = true;
|
|
61
|
+
signal.removeEventListener('abort', onAbort);
|
|
62
|
+
callback(value);
|
|
63
|
+
};
|
|
64
|
+
const onAbort = () => finish(reject, abortError(signal));
|
|
65
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
66
|
+
if (signal.aborted) onAbort();
|
|
67
|
+
Promise.resolve(promise).then(
|
|
68
|
+
(value) => finish(resolve, value),
|
|
69
|
+
(error) => finish(reject, error),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function optionalResult(promise, fallback, signal = null) {
|
|
75
|
+
try {
|
|
76
|
+
return await promise;
|
|
77
|
+
} catch {
|
|
78
|
+
throwIfAborted(signal);
|
|
79
|
+
return fallback;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function delay(ms, signal = null) {
|
|
84
|
+
throwIfAborted(signal);
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
signal?.removeEventListener('abort', onAbort);
|
|
88
|
+
resolve();
|
|
89
|
+
}, ms);
|
|
90
|
+
const onAbort = () => {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
reject(abortError(signal));
|
|
93
|
+
};
|
|
94
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
95
|
+
if (signal?.aborted) onAbort();
|
|
96
|
+
});
|
|
33
97
|
}
|
|
34
98
|
|
|
35
99
|
function jsString(value) {
|
|
@@ -58,113 +122,410 @@ function keyCodeFor(key) {
|
|
|
58
122
|
return map[normalized] || (normalized.length === 1 ? normalized.toUpperCase().charCodeAt(0) : 0);
|
|
59
123
|
}
|
|
60
124
|
|
|
61
|
-
|
|
125
|
+
function normalizePointCoordinate(value, label) {
|
|
126
|
+
if (value == null || value === '') {
|
|
127
|
+
throw new Error(`${label} coordinate is required.`);
|
|
128
|
+
}
|
|
129
|
+
const normalized = Number(value);
|
|
130
|
+
if (!Number.isFinite(normalized)) {
|
|
131
|
+
throw new Error(`${label} coordinate must be a finite number.`);
|
|
132
|
+
}
|
|
133
|
+
return Math.round(normalized);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function ipv4Octets(hostname) {
|
|
137
|
+
if (!/^\d{1,3}(?:\.\d{1,3}){3}$/.test(hostname)) return null;
|
|
138
|
+
const parts = hostname.split('.').map(Number);
|
|
139
|
+
return parts.every((part) => part >= 0 && part <= 255) ? parts : null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function isPrivateNetworkHostname(hostname) {
|
|
143
|
+
const normalized = String(hostname || '')
|
|
144
|
+
.toLowerCase()
|
|
145
|
+
.replace(/^\[|\]$/g, '')
|
|
146
|
+
.replace(/\.$/, '');
|
|
147
|
+
if (!normalized) return true;
|
|
148
|
+
if (
|
|
149
|
+
normalized === 'localhost'
|
|
150
|
+
|| normalized === 'localhost.localdomain'
|
|
151
|
+
|| normalized.endsWith('.localhost')
|
|
152
|
+
|| normalized.endsWith('.local')
|
|
153
|
+
|| normalized.endsWith('.internal')
|
|
154
|
+
) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const octets = ipv4Octets(normalized);
|
|
159
|
+
if (octets) {
|
|
160
|
+
const [a, b, c] = octets;
|
|
161
|
+
return a === 0
|
|
162
|
+
|| a === 10
|
|
163
|
+
|| a === 127
|
|
164
|
+
|| a >= 224
|
|
165
|
+
|| (a === 100 && b >= 64 && b <= 127)
|
|
166
|
+
|| (a === 169 && b === 254)
|
|
167
|
+
|| (a === 172 && b >= 16 && b <= 31)
|
|
168
|
+
|| (a === 192 && b === 168)
|
|
169
|
+
|| (a === 192 && b === 0 && c === 0)
|
|
170
|
+
|| (a === 192 && b === 0 && c === 2)
|
|
171
|
+
|| (a === 198 && (b === 18 || b === 19))
|
|
172
|
+
|| (a === 198 && b === 51 && c === 100)
|
|
173
|
+
|| (a === 203 && b === 0 && c === 113);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (normalized.includes(':')) {
|
|
177
|
+
if (normalized === '::' || normalized === '::1') return true;
|
|
178
|
+
if (normalized.startsWith('::ffff:')) return true;
|
|
179
|
+
const first = Number.parseInt(normalized.split(':').find(Boolean) || '0', 16);
|
|
180
|
+
if (!Number.isFinite(first)) return true;
|
|
181
|
+
if ((first & 0xfe00) === 0xfc00) return true;
|
|
182
|
+
if ((first & 0xffc0) === 0xfe80) return true;
|
|
183
|
+
if ((first & 0xffc0) === 0xfec0) return true;
|
|
184
|
+
if ((first & 0xff00) === 0xff00) return true;
|
|
185
|
+
if (normalized.startsWith('2001:db8:')) return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function normalizeNetworkValidationUrl(value) {
|
|
192
|
+
const raw = String(value || '');
|
|
193
|
+
if (!raw || raw.length > 8192) return null;
|
|
194
|
+
let parsed;
|
|
195
|
+
try {
|
|
196
|
+
parsed = new URL(raw);
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) return null;
|
|
201
|
+
if (parsed.username || parsed.password || isPrivateNetworkHostname(parsed.hostname)) return null;
|
|
202
|
+
const protocol = parsed.protocol === 'ws:'
|
|
203
|
+
? 'http:'
|
|
204
|
+
: (parsed.protocol === 'wss:' ? 'https:' : parsed.protocol);
|
|
205
|
+
return `${protocol}//${parsed.host}/`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const PAGE_ACCESS_COMMANDS = new Set([
|
|
209
|
+
COMMANDS.LAUNCH,
|
|
210
|
+
COMMANDS.CLICK,
|
|
211
|
+
COMMANDS.CLICK_POINT,
|
|
212
|
+
COMMANDS.TYPE,
|
|
213
|
+
COMMANDS.TYPE_TEXT,
|
|
214
|
+
COMMANDS.PRESS_KEY,
|
|
215
|
+
COMMANDS.SCROLL,
|
|
216
|
+
COMMANDS.EXTRACT,
|
|
217
|
+
COMMANDS.EVALUATE,
|
|
218
|
+
COMMANDS.SCREENSHOT,
|
|
219
|
+
COMMANDS.GET_PAGE_INFO,
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
export function createBrowserProtocol(chromeApi, options = {}) {
|
|
62
223
|
let attachedTabId = null;
|
|
63
224
|
let activeTabId = null;
|
|
225
|
+
const pausedRequests = new Map();
|
|
226
|
+
const maxPausedRequests = Math.max(1, Math.min(Number(options.maxPausedRequests) || 256, 1024));
|
|
227
|
+
const validateUrl = typeof options.validateUrl === 'function'
|
|
228
|
+
? options.validateUrl
|
|
229
|
+
: async () => false;
|
|
64
230
|
|
|
65
231
|
const debuggee = () => ({ tabId: activeTabId });
|
|
66
232
|
|
|
67
|
-
|
|
233
|
+
const call = (signal, namespace, method, ...args) => waitForAbortable(
|
|
234
|
+
chromeCall(chromeApi, namespace, method, ...args),
|
|
235
|
+
signal,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
function abortPausedRequests(tabId, reason) {
|
|
239
|
+
for (const entry of pausedRequests.values()) {
|
|
240
|
+
if (entry.tabId === tabId && !entry.controller.signal.aborted) {
|
|
241
|
+
entry.controller.abort(reason);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function sendToTab(tabId, method, params = {}, signal = null) {
|
|
247
|
+
try {
|
|
248
|
+
return await call(signal, 'debugger', 'sendCommand', { tabId }, method, params);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (
|
|
251
|
+
tabId === attachedTabId
|
|
252
|
+
&& /not attached|no tab with given id|target closed/i.test(String(error?.message || ''))
|
|
253
|
+
) {
|
|
254
|
+
attachedTabId = null;
|
|
255
|
+
}
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function isNetworkUrlAllowed(url, signal = null) {
|
|
261
|
+
throwIfAborted(signal);
|
|
262
|
+
const normalized = normalizeNetworkValidationUrl(url);
|
|
263
|
+
if (!normalized) return false;
|
|
264
|
+
const result = await validateUrl(normalized, { signal });
|
|
265
|
+
throwIfAborted(signal);
|
|
266
|
+
return result === true || result?.allowed === true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function handlePausedRequest(source, params) {
|
|
270
|
+
const tabId = source?.tabId;
|
|
271
|
+
const requestId = String(params?.requestId || '');
|
|
272
|
+
if (tabId == null || tabId !== attachedTabId || !requestId) return;
|
|
273
|
+
if (pausedRequests.size >= maxPausedRequests) {
|
|
274
|
+
await sendToTab(tabId, 'Fetch.failRequest', {
|
|
275
|
+
requestId,
|
|
276
|
+
errorReason: 'BlockedByClient',
|
|
277
|
+
}).catch(() => {});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const key = `${tabId}:${requestId}`;
|
|
282
|
+
const previous = pausedRequests.get(key);
|
|
283
|
+
previous?.controller.abort(new Error('Browser request interception was superseded.'));
|
|
284
|
+
const controller = new AbortController();
|
|
285
|
+
pausedRequests.set(key, { tabId, controller });
|
|
286
|
+
let allowed = false;
|
|
287
|
+
try {
|
|
288
|
+
allowed = await isNetworkUrlAllowed(params?.request?.url, controller.signal);
|
|
289
|
+
} catch {
|
|
290
|
+
allowed = false;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
await sendToTab(
|
|
294
|
+
tabId,
|
|
295
|
+
allowed ? 'Fetch.continueRequest' : 'Fetch.failRequest',
|
|
296
|
+
allowed
|
|
297
|
+
? { requestId }
|
|
298
|
+
: { requestId, errorReason: 'BlockedByClient' },
|
|
299
|
+
);
|
|
300
|
+
} catch {
|
|
301
|
+
// Detach and tab-close races make the paused request disappear on their own.
|
|
302
|
+
} finally {
|
|
303
|
+
if (pausedRequests.get(key)?.controller === controller) pausedRequests.delete(key);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
chromeApi.debugger?.onEvent?.addListener?.((source, method, params) => {
|
|
308
|
+
if (method !== 'Fetch.requestPaused') return;
|
|
309
|
+
handlePausedRequest(source, params).catch(() => {});
|
|
310
|
+
});
|
|
311
|
+
chromeApi.debugger?.onDetach?.addListener?.((source) => {
|
|
312
|
+
if (source?.tabId === attachedTabId) {
|
|
313
|
+
abortPausedRequests(source.tabId, new Error('Browser debugger detached.'));
|
|
314
|
+
attachedTabId = null;
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
chromeApi.tabs?.onRemoved?.addListener?.((tabId) => {
|
|
318
|
+
if (tabId === activeTabId) activeTabId = null;
|
|
319
|
+
if (tabId === attachedTabId) {
|
|
320
|
+
abortPausedRequests(tabId, new Error('Browser tab closed.'));
|
|
321
|
+
attachedTabId = null;
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
async function ensureTab(signal = null) {
|
|
326
|
+
throwIfAborted(signal);
|
|
68
327
|
if (activeTabId != null) {
|
|
69
328
|
try {
|
|
70
|
-
await
|
|
329
|
+
await call(signal, 'tabs', 'get', activeTabId);
|
|
71
330
|
return activeTabId;
|
|
72
331
|
} catch {
|
|
332
|
+
throwIfAborted(signal);
|
|
73
333
|
activeTabId = null;
|
|
74
334
|
}
|
|
75
335
|
}
|
|
76
336
|
|
|
77
|
-
const tabs = await
|
|
337
|
+
const tabs = await call(signal, 'tabs', 'query', { active: true, currentWindow: true });
|
|
78
338
|
if (tabs && tabs[0]?.id != null) {
|
|
79
339
|
activeTabId = tabs[0].id;
|
|
80
340
|
return activeTabId;
|
|
81
341
|
}
|
|
82
|
-
const tab = await
|
|
342
|
+
const tab = await call(signal, 'tabs', 'create', { url: 'about:blank', active: true });
|
|
83
343
|
activeTabId = tab.id;
|
|
84
344
|
return activeTabId;
|
|
85
345
|
}
|
|
86
346
|
|
|
87
|
-
async function attach() {
|
|
88
|
-
await ensureTab();
|
|
347
|
+
async function attach(signal = null) {
|
|
348
|
+
await ensureTab(signal);
|
|
89
349
|
if (attachedTabId === activeTabId) return;
|
|
90
350
|
if (attachedTabId != null) {
|
|
91
|
-
|
|
351
|
+
abortPausedRequests(attachedTabId, new Error('Browser control moved to another tab.'));
|
|
352
|
+
await optionalResult(
|
|
353
|
+
call(signal, 'debugger', 'detach', { tabId: attachedTabId }),
|
|
354
|
+
undefined,
|
|
355
|
+
signal,
|
|
356
|
+
);
|
|
92
357
|
}
|
|
93
358
|
try {
|
|
94
|
-
await
|
|
359
|
+
await call(signal, 'debugger', 'attach', debuggee(), '1.3');
|
|
95
360
|
} catch (error) {
|
|
361
|
+
throwIfAborted(signal);
|
|
96
362
|
if (!/another debugger|already attached|debugger is already attached/i.test(error.message)) {
|
|
97
363
|
throw error;
|
|
98
364
|
}
|
|
99
365
|
}
|
|
100
366
|
attachedTabId = activeTabId;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
367
|
+
try {
|
|
368
|
+
await sendToTab(attachedTabId, 'Fetch.enable', {
|
|
369
|
+
patterns: [
|
|
370
|
+
{ urlPattern: 'http://*/*', requestStage: 'Request' },
|
|
371
|
+
{ urlPattern: 'https://*/*', requestStage: 'Request' },
|
|
372
|
+
{ urlPattern: 'ws://*/*', requestStage: 'Request' },
|
|
373
|
+
{ urlPattern: 'wss://*/*', requestStage: 'Request' },
|
|
374
|
+
],
|
|
375
|
+
}, signal);
|
|
376
|
+
await optionalResult(send('Page.enable', {}, signal), undefined, signal);
|
|
377
|
+
await optionalResult(send('Runtime.enable', {}, signal), undefined, signal);
|
|
378
|
+
await optionalResult(send('DOM.enable', {}, signal), undefined, signal);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
const failedTabId = attachedTabId;
|
|
381
|
+
abortPausedRequests(failedTabId, error);
|
|
382
|
+
attachedTabId = null;
|
|
383
|
+
if (failedTabId != null) {
|
|
384
|
+
await chromeCall(chromeApi, 'debugger', 'detach', { tabId: failedTabId }).catch(() => {});
|
|
385
|
+
}
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
104
388
|
}
|
|
105
389
|
|
|
106
|
-
async function send(method, params = {}) {
|
|
107
|
-
|
|
390
|
+
async function send(method, params = {}, signal = null) {
|
|
391
|
+
const tabId = attachedTabId ?? activeTabId;
|
|
392
|
+
if (tabId == null) throw new Error('No browser tab is available.');
|
|
393
|
+
return sendToTab(tabId, method, params, signal);
|
|
108
394
|
}
|
|
109
395
|
|
|
110
|
-
async function evalJs(expression, options = {}) {
|
|
111
|
-
await attach();
|
|
396
|
+
async function evalJs(expression, options = {}, signal = null) {
|
|
397
|
+
await attach(signal);
|
|
112
398
|
const response = await send('Runtime.evaluate', {
|
|
113
399
|
expression,
|
|
114
400
|
awaitPromise: options.awaitPromise !== false,
|
|
115
401
|
returnByValue: true,
|
|
116
|
-
});
|
|
402
|
+
}, signal);
|
|
117
403
|
if (response?.exceptionDetails) {
|
|
118
404
|
throw new Error(response.exceptionDetails.text || 'JavaScript evaluation failed.');
|
|
119
405
|
}
|
|
120
406
|
return response?.result?.value;
|
|
121
407
|
}
|
|
122
408
|
|
|
123
|
-
async function waitForLoad(timeoutMs = 30000) {
|
|
409
|
+
async function waitForLoad(timeoutMs = 30000, signal = null) {
|
|
124
410
|
const started = Date.now();
|
|
125
411
|
while (Date.now() - started < timeoutMs) {
|
|
126
|
-
|
|
127
|
-
|
|
412
|
+
throwIfAborted(signal);
|
|
413
|
+
const ready = await optionalResult(
|
|
414
|
+
evalJs(
|
|
415
|
+
'document.readyState === "complete" || document.readyState === "interactive"',
|
|
416
|
+
{ awaitPromise: false },
|
|
417
|
+
signal,
|
|
418
|
+
),
|
|
419
|
+
false,
|
|
420
|
+
signal,
|
|
421
|
+
);
|
|
128
422
|
if (ready) return;
|
|
129
|
-
await delay(250);
|
|
423
|
+
await delay(250, signal);
|
|
130
424
|
}
|
|
425
|
+
throw new Error(`Page did not finish loading within ${timeoutMs}ms.`);
|
|
131
426
|
}
|
|
132
427
|
|
|
133
|
-
async function
|
|
428
|
+
async function markCurrentDocument(signal = null) {
|
|
429
|
+
const key = `__neoagent_document_${crypto.randomUUID().replace(/-/g, '')}`;
|
|
430
|
+
const token = crypto.randomUUID();
|
|
431
|
+
const marked = await optionalResult(
|
|
432
|
+
evalJs(
|
|
433
|
+
`globalThis[${jsString(key)}] = ${jsString(token)}; true`,
|
|
434
|
+
{ awaitPromise: false },
|
|
435
|
+
signal,
|
|
436
|
+
),
|
|
437
|
+
false,
|
|
438
|
+
signal,
|
|
439
|
+
);
|
|
440
|
+
return marked ? { key, token } : null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function waitForDocumentReplacement(marker, timeoutMs = 30000, signal = null) {
|
|
444
|
+
if (!marker) {
|
|
445
|
+
await delay(250, signal);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const started = Date.now();
|
|
449
|
+
const expression = `globalThis[${jsString(marker.key)}] !== ${jsString(marker.token)}`;
|
|
450
|
+
while (Date.now() - started < timeoutMs) {
|
|
451
|
+
throwIfAborted(signal);
|
|
452
|
+
const replaced = await optionalResult(
|
|
453
|
+
evalJs(expression, { awaitPromise: false }, signal),
|
|
454
|
+
false,
|
|
455
|
+
signal,
|
|
456
|
+
);
|
|
457
|
+
if (replaced) return;
|
|
458
|
+
await delay(100, signal);
|
|
459
|
+
}
|
|
460
|
+
throw new Error(`Page navigation did not commit within ${timeoutMs}ms.`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function assertNavigationSucceeded(signal = null) {
|
|
464
|
+
const frameTree = await send('Page.getFrameTree', {}, signal);
|
|
465
|
+
const unreachableUrl = String(frameTree?.frameTree?.frame?.unreachableUrl || '');
|
|
466
|
+
if (unreachableUrl) {
|
|
467
|
+
throw new Error('Browser navigation failed before the destination loaded.');
|
|
468
|
+
}
|
|
469
|
+
await assertCurrentPageAllowed(signal);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function waitForSelector(selector, timeoutMs = 10000, signal = null) {
|
|
134
473
|
if (!selector) return;
|
|
135
474
|
const expression = `Boolean(document.querySelector(${jsString(selector)}))`;
|
|
136
475
|
const started = Date.now();
|
|
137
476
|
while (Date.now() - started < timeoutMs) {
|
|
138
|
-
|
|
139
|
-
await
|
|
477
|
+
throwIfAborted(signal);
|
|
478
|
+
if (await optionalResult(evalJs(expression, { awaitPromise: false }, signal), false, signal)) return;
|
|
479
|
+
await delay(200, signal);
|
|
140
480
|
}
|
|
481
|
+
throw new Error(`Element not found within ${timeoutMs}ms: ${selector}`);
|
|
141
482
|
}
|
|
142
483
|
|
|
143
|
-
async function currentTab() {
|
|
144
|
-
await ensureTab();
|
|
145
|
-
return
|
|
484
|
+
async function currentTab(signal = null) {
|
|
485
|
+
await ensureTab(signal);
|
|
486
|
+
return call(signal, 'tabs', 'get', activeTabId);
|
|
146
487
|
}
|
|
147
488
|
|
|
148
|
-
async function
|
|
149
|
-
await
|
|
489
|
+
async function assertCurrentPageAllowed(signal = null) {
|
|
490
|
+
const tab = await currentTab(signal);
|
|
491
|
+
const url = String(tab?.url || '');
|
|
492
|
+
if (url === 'about:blank') return tab;
|
|
493
|
+
if (!await isNetworkUrlAllowed(url, signal)) {
|
|
494
|
+
throw new Error('The current browser page is not permitted.');
|
|
495
|
+
}
|
|
496
|
+
return tab;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function screenshotDataUrl(options = {}, signal = null) {
|
|
500
|
+
throwIfAborted(signal);
|
|
501
|
+
await attach(signal);
|
|
150
502
|
const capture = await send('Page.captureScreenshot', {
|
|
151
503
|
format: 'png',
|
|
152
504
|
captureBeyondViewport: options.fullPage === true,
|
|
153
505
|
fromSurface: true,
|
|
154
|
-
});
|
|
155
|
-
|
|
506
|
+
}, signal);
|
|
507
|
+
const encoded = String(capture?.data || '');
|
|
508
|
+
if (encoded.length > 24 * 1024 * 1024) {
|
|
509
|
+
throw new Error('Browser screenshot is too large to transfer. Try a viewport screenshot instead.');
|
|
510
|
+
}
|
|
511
|
+
return `data:image/png;base64,${encoded}`;
|
|
156
512
|
}
|
|
157
513
|
|
|
158
|
-
async function pageSnapshot(options = {}) {
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
const
|
|
514
|
+
async function pageSnapshot(options = {}, signal = null) {
|
|
515
|
+
throwIfAborted(signal);
|
|
516
|
+
const tab = await currentTab(signal);
|
|
517
|
+
const title = await optionalResult(
|
|
518
|
+
evalJs('document.title || ""', { awaitPromise: false }, signal),
|
|
519
|
+
tab.title || '',
|
|
520
|
+
signal,
|
|
521
|
+
);
|
|
522
|
+
const bodyText = await optionalResult(evalJs(`(() => {
|
|
162
523
|
const body = document.body;
|
|
163
524
|
if (!body) return '';
|
|
164
525
|
const clone = body.cloneNode(true);
|
|
165
526
|
clone.querySelectorAll('script, style, noscript').forEach((node) => node.remove());
|
|
166
527
|
return String(clone.innerText || '').slice(0, 10000);
|
|
167
|
-
})()
|
|
528
|
+
})()`, {}, signal), '', signal);
|
|
168
529
|
const result = {
|
|
169
530
|
title: title || tab.title || '',
|
|
170
531
|
url: tab.url || '',
|
|
@@ -172,12 +533,13 @@ export function createBrowserProtocol(chromeApi) {
|
|
|
172
533
|
bodyText,
|
|
173
534
|
};
|
|
174
535
|
if (options.screenshot !== false) {
|
|
175
|
-
result.screenshotDataUrl = await screenshotDataUrl(options);
|
|
536
|
+
result.screenshotDataUrl = await screenshotDataUrl(options, signal);
|
|
176
537
|
}
|
|
177
538
|
return result;
|
|
178
539
|
}
|
|
179
540
|
|
|
180
|
-
async function locateTarget(payload = {}) {
|
|
541
|
+
async function locateTarget(payload = {}, signal = null) {
|
|
542
|
+
throwIfAborted(signal);
|
|
181
543
|
const selector = String(payload.selector || '').trim();
|
|
182
544
|
const text = String(payload.text || '').trim().toLowerCase();
|
|
183
545
|
const expression = selector
|
|
@@ -194,42 +556,50 @@ export function createBrowserProtocol(chromeApi) {
|
|
|
194
556
|
const rect = target.getBoundingClientRect();
|
|
195
557
|
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
196
558
|
})()`;
|
|
197
|
-
const point = await evalJs(expression);
|
|
559
|
+
const point = await evalJs(expression, {}, signal);
|
|
198
560
|
if (!point || !Number.isFinite(Number(point.x)) || !Number.isFinite(Number(point.y))) {
|
|
199
561
|
throw new Error(selector ? `Element not found: ${selector}` : `No clickable element found with text: ${payload.text}`);
|
|
200
562
|
}
|
|
201
563
|
return { x: Math.round(point.x), y: Math.round(point.y) };
|
|
202
564
|
}
|
|
203
565
|
|
|
204
|
-
async function clickPoint(x, y) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
await send('Input.dispatchMouseEvent', { type: '
|
|
210
|
-
await send('Input.dispatchMouseEvent', { type: '
|
|
211
|
-
await
|
|
566
|
+
async function clickPoint(x, y, signal = null) {
|
|
567
|
+
throwIfAborted(signal);
|
|
568
|
+
await attach(signal);
|
|
569
|
+
const px = Math.max(0, normalizePointCoordinate(x, 'x'));
|
|
570
|
+
const py = Math.max(0, normalizePointCoordinate(y, 'y'));
|
|
571
|
+
await send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: px, y: py }, signal);
|
|
572
|
+
await send('Input.dispatchMouseEvent', { type: 'mousePressed', x: px, y: py, button: 'left', clickCount: 1 }, signal);
|
|
573
|
+
await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: px, y: py, button: 'left', clickCount: 1 }, signal);
|
|
574
|
+
await delay(500, signal);
|
|
212
575
|
return { x: px, y: py };
|
|
213
576
|
}
|
|
214
577
|
|
|
215
|
-
async function typeKey(key) {
|
|
216
|
-
await attach();
|
|
578
|
+
async function typeKey(key, signal = null) {
|
|
579
|
+
await attach(signal);
|
|
217
580
|
const normalized = String(key || '').trim();
|
|
218
581
|
const code = keyCodeFor(normalized);
|
|
219
|
-
await send('Input.dispatchKeyEvent', { type: 'keyDown', key: normalized, windowsVirtualKeyCode: code });
|
|
220
|
-
await send('Input.dispatchKeyEvent', { type: 'keyUp', key: normalized, windowsVirtualKeyCode: code });
|
|
582
|
+
await send('Input.dispatchKeyEvent', { type: 'keyDown', key: normalized, windowsVirtualKeyCode: code }, signal);
|
|
583
|
+
await send('Input.dispatchKeyEvent', { type: 'keyUp', key: normalized, windowsVirtualKeyCode: code }, signal);
|
|
221
584
|
}
|
|
222
585
|
|
|
223
|
-
async function run(command, payload = {}) {
|
|
586
|
+
async function run(command, payload = {}, options = {}) {
|
|
587
|
+
const signal = options.signal || null;
|
|
588
|
+
throwIfAborted(signal);
|
|
589
|
+
if (PAGE_ACCESS_COMMANDS.has(command)) {
|
|
590
|
+
await assertCurrentPageAllowed(signal);
|
|
591
|
+
}
|
|
224
592
|
switch (command) {
|
|
225
593
|
case COMMANDS.GET_COOKIES: {
|
|
226
594
|
const domains = Array.isArray(payload.domains)
|
|
227
595
|
? payload.domains.map((item) => String(item || '').replace(/^\./, '').toLowerCase()).filter(Boolean)
|
|
228
596
|
: [];
|
|
229
597
|
if (domains.length === 0) throw new Error('At least one cookie domain is required.');
|
|
598
|
+
if (domains.length > 50) throw new Error('At most 50 cookie domains may be requested.');
|
|
230
599
|
const all = [];
|
|
231
600
|
for (const domain of domains) {
|
|
232
|
-
|
|
601
|
+
throwIfAborted(signal);
|
|
602
|
+
const cookies = await call(signal, 'cookies', 'getAll', { domain });
|
|
233
603
|
all.push(...(Array.isArray(cookies) ? cookies : []));
|
|
234
604
|
}
|
|
235
605
|
const seen = new Set();
|
|
@@ -258,50 +628,84 @@ export function createBrowserProtocol(chromeApi) {
|
|
|
258
628
|
};
|
|
259
629
|
}
|
|
260
630
|
case COMMANDS.LAUNCH:
|
|
261
|
-
await attach();
|
|
262
|
-
return pageSnapshot({ screenshot: false });
|
|
631
|
+
await attach(signal);
|
|
632
|
+
return pageSnapshot({ screenshot: false }, signal);
|
|
263
633
|
case COMMANDS.NAVIGATE:
|
|
264
|
-
await attach();
|
|
265
634
|
if (!payload.url) throw new Error('url required');
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
635
|
+
if (!await isNetworkUrlAllowed(payload.url, signal)) {
|
|
636
|
+
throw new Error('This browser URL is not permitted.');
|
|
637
|
+
}
|
|
638
|
+
await attach(signal);
|
|
639
|
+
{
|
|
640
|
+
const marker = await markCurrentDocument(signal);
|
|
641
|
+
let completed = false;
|
|
642
|
+
try {
|
|
643
|
+
const navigation = await send('Page.navigate', { url: String(payload.url) }, signal);
|
|
644
|
+
if (navigation?.errorText) {
|
|
645
|
+
throw new Error(`Browser navigation failed: ${navigation.errorText}`);
|
|
646
|
+
}
|
|
647
|
+
if (navigation?.isDownload === true) {
|
|
648
|
+
throw new Error('Browser navigation started a download instead of loading a page.');
|
|
649
|
+
}
|
|
650
|
+
if (navigation?.loaderId) {
|
|
651
|
+
await waitForDocumentReplacement(marker, 30000, signal);
|
|
652
|
+
} else {
|
|
653
|
+
await delay(100, signal);
|
|
654
|
+
}
|
|
655
|
+
await waitForLoad(30000, signal);
|
|
656
|
+
await assertNavigationSucceeded(signal);
|
|
657
|
+
await waitForSelector(payload.waitFor, 10000, signal);
|
|
658
|
+
throwIfAborted(signal);
|
|
659
|
+
completed = true;
|
|
660
|
+
return pageSnapshot(payload, signal);
|
|
661
|
+
} finally {
|
|
662
|
+
if (!completed) await send('Page.stopLoading').catch(() => {});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
270
665
|
case COMMANDS.CLICK: {
|
|
271
|
-
const point = await locateTarget(payload);
|
|
272
|
-
await clickPoint(point.x, point.y);
|
|
273
|
-
|
|
666
|
+
const point = await locateTarget(payload, signal);
|
|
667
|
+
await clickPoint(point.x, point.y, signal);
|
|
668
|
+
throwIfAborted(signal);
|
|
669
|
+
return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
|
|
274
670
|
}
|
|
275
671
|
case COMMANDS.CLICK_POINT:
|
|
276
|
-
await clickPoint(payload.x, payload.y);
|
|
277
|
-
|
|
672
|
+
await clickPoint(payload.x, payload.y, signal);
|
|
673
|
+
throwIfAborted(signal);
|
|
674
|
+
return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
|
|
278
675
|
case COMMANDS.TYPE:
|
|
279
676
|
if (!payload.selector) throw new Error('selector required');
|
|
280
677
|
if (payload.clear !== false) {
|
|
281
678
|
await evalJs(`(() => {
|
|
282
679
|
const el = document.querySelector(${jsString(payload.selector)});
|
|
283
|
-
if (!el) throw new Error(
|
|
680
|
+
if (!el) throw new Error(${jsString(`Element not found: ${payload.selector}`)});
|
|
284
681
|
el.focus();
|
|
285
682
|
if ('value' in el) el.value = '';
|
|
286
|
-
})()
|
|
683
|
+
})()`, {}, signal);
|
|
287
684
|
} else {
|
|
288
|
-
await evalJs(`document.querySelector(${jsString(payload.selector)})?.focus()
|
|
685
|
+
await evalJs(`document.querySelector(${jsString(payload.selector)})?.focus()`, {}, signal);
|
|
289
686
|
}
|
|
290
|
-
await send('Input.insertText', { text: String(payload.text || '') });
|
|
291
|
-
|
|
292
|
-
|
|
687
|
+
await send('Input.insertText', { text: String(payload.text || '') }, signal);
|
|
688
|
+
throwIfAborted(signal);
|
|
689
|
+
if (payload.pressEnter) await typeKey('Enter', signal);
|
|
690
|
+
return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
|
|
293
691
|
case COMMANDS.TYPE_TEXT:
|
|
294
|
-
await attach();
|
|
295
|
-
await send('Input.insertText', { text: String(payload.text || '') });
|
|
296
|
-
|
|
297
|
-
|
|
692
|
+
await attach(signal);
|
|
693
|
+
await send('Input.insertText', { text: String(payload.text || '') }, signal);
|
|
694
|
+
throwIfAborted(signal);
|
|
695
|
+
if (payload.pressEnter) await typeKey('Enter', signal);
|
|
696
|
+
return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
|
|
298
697
|
case COMMANDS.PRESS_KEY:
|
|
299
|
-
await typeKey(payload.key);
|
|
300
|
-
|
|
698
|
+
await typeKey(payload.key, signal);
|
|
699
|
+
throwIfAborted(signal);
|
|
700
|
+
return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
|
|
301
701
|
case COMMANDS.SCROLL:
|
|
302
|
-
await evalJs(
|
|
303
|
-
|
|
304
|
-
|
|
702
|
+
await evalJs(
|
|
703
|
+
`window.scrollBy(${Math.round(Number(payload.deltaX) || 0)}, ${Math.round(Number(payload.deltaY) || 0)})`,
|
|
704
|
+
{},
|
|
705
|
+
signal,
|
|
706
|
+
);
|
|
707
|
+
await delay(250, signal);
|
|
708
|
+
return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
|
|
305
709
|
case COMMANDS.EXTRACT: {
|
|
306
710
|
const selector = payload.selector || 'body';
|
|
307
711
|
const attribute = payload.attribute || '';
|
|
@@ -314,24 +718,40 @@ export function createBrowserProtocol(chromeApi) {
|
|
|
314
718
|
return el.innerText || '';
|
|
315
719
|
};
|
|
316
720
|
const els = Array.from(document.querySelectorAll(${jsString(selector)}));
|
|
317
|
-
if (${payload.all === true})
|
|
721
|
+
if (${payload.all === true}) {
|
|
722
|
+
return { results: els.slice(0, 100).map((el) => String(read(el)).slice(0, 50000)) };
|
|
723
|
+
}
|
|
318
724
|
return { result: els[0] ? String(read(els[0])).slice(0, 50000) : '' };
|
|
319
725
|
})()`;
|
|
320
|
-
return evalJs(expression);
|
|
726
|
+
return evalJs(expression, {}, signal);
|
|
321
727
|
}
|
|
322
728
|
case COMMANDS.EVALUATE: {
|
|
323
|
-
|
|
324
|
-
|
|
729
|
+
if (String(payload.script || '').length > 10000) {
|
|
730
|
+
throw new Error('script exceeds maximum length (10000)');
|
|
731
|
+
}
|
|
732
|
+
const value = await evalJs(buildIsolatedEvaluationExpression(payload.script), {}, signal);
|
|
733
|
+
throwIfAborted(signal);
|
|
734
|
+
const serialized = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
735
|
+
const maxChars = 1024 * 1024;
|
|
736
|
+
return {
|
|
737
|
+
result: String(serialized ?? '').slice(0, maxChars),
|
|
738
|
+
truncated: String(serialized ?? '').length > maxChars,
|
|
739
|
+
};
|
|
325
740
|
}
|
|
326
741
|
case COMMANDS.SCREENSHOT:
|
|
327
|
-
return { screenshotDataUrl: await screenshotDataUrl(payload), fullPage: payload.fullPage === true };
|
|
742
|
+
return { screenshotDataUrl: await screenshotDataUrl(payload, signal), fullPage: payload.fullPage === true };
|
|
328
743
|
case COMMANDS.GET_PAGE_INFO: {
|
|
329
|
-
const tab = await currentTab();
|
|
744
|
+
const tab = await currentTab(signal);
|
|
330
745
|
return { url: tab.url || null, title: tab.title || null };
|
|
331
746
|
}
|
|
332
747
|
case COMMANDS.CLOSE:
|
|
333
748
|
if (attachedTabId != null) {
|
|
334
|
-
|
|
749
|
+
abortPausedRequests(attachedTabId, new Error('Browser control closed.'));
|
|
750
|
+
await optionalResult(
|
|
751
|
+
call(signal, 'debugger', 'detach', { tabId: attachedTabId }),
|
|
752
|
+
undefined,
|
|
753
|
+
signal,
|
|
754
|
+
);
|
|
335
755
|
}
|
|
336
756
|
attachedTabId = null;
|
|
337
757
|
return { success: true };
|
|
@@ -347,6 +767,8 @@ export function createBrowserProtocol(chromeApi) {
|
|
|
347
767
|
attach,
|
|
348
768
|
send,
|
|
349
769
|
evalJs,
|
|
770
|
+
handlePausedRequest,
|
|
771
|
+
isNetworkUrlAllowed,
|
|
350
772
|
},
|
|
351
773
|
};
|
|
352
774
|
}
|