neoagent 3.2.1-beta.1 → 3.2.1-beta.11
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 +654 -90
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +157 -24
- package/flutter_app/lib/main_integrations.dart +607 -8
- package/flutter_app/lib/main_models.dart +7 -2
- package/flutter_app/lib/main_operations.dart +334 -370
- package/flutter_app/lib/main_security.dart +266 -112
- package/flutter_app/lib/main_settings.dart +342 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/backend_client.dart +97 -0
- 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/landing/index.html +3 -1
- package/lib/manager.js +106 -89
- package/lib/schema_migrations.js +115 -13
- package/package.json +30 -15
- package/runtime/paths.js +49 -5
- package/server/db/database.js +2 -2
- package/server/guest-agent.cli.package.json +13 -0
- package/server/guest_agent.js +85 -40
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +12 -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 +2 -2
- package/server/public/main.dart.js +81930 -80509
- package/server/routes/admin.js +1 -1
- package/server/routes/android.js +30 -34
- package/server/routes/behavior.js +80 -0
- package/server/routes/browser.js +23 -15
- package/server/routes/desktop.js +18 -1
- package/server/routes/integrations.js +107 -1
- package/server/routes/memory.js +1 -0
- package/server/routes/settings.js +20 -5
- package/server/routes/social_reach.js +12 -3
- package/server/routes/social_video.js +4 -0
- package/server/services/agents/manager.js +1 -1
- package/server/services/ai/capabilityHealth.js +62 -96
- package/server/services/ai/compaction.js +7 -2
- package/server/services/ai/history.js +45 -6
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +452 -176
- package/server/services/ai/loop/blank_recovery.js +5 -4
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +291 -8
- package/server/services/ai/loop/conversation_loop.js +515 -342
- package/server/services/ai/loop/messaging_delivery.js +176 -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/progress_classification.js +2 -0
- package/server/services/ai/loop/tool_dispatch.js +19 -8
- package/server/services/ai/loopPolicy.js +48 -21
- package/server/services/ai/messagingFallback.js +17 -17
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_failure_cache.js +108 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +68 -163
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +166 -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 +135 -97
- 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/systemPrompt.js +32 -123
- package/server/services/ai/taskAnalysis.js +104 -11
- package/server/services/ai/toolEvidence.js +256 -29
- package/server/services/ai/tools.js +248 -117
- 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/behavior/config.js +251 -0
- package/server/services/behavior/defaults.js +68 -0
- package/server/services/behavior/delivery.js +176 -0
- package/server/services/behavior/index.js +43 -0
- package/server/services/behavior/model_client.js +35 -0
- package/server/services/behavior/modules/agent_identity.js +37 -0
- package/server/services/behavior/modules/channel_style.js +28 -0
- package/server/services/behavior/modules/index.js +29 -0
- package/server/services/behavior/modules/norms.js +101 -0
- package/server/services/behavior/modules/persona.js +48 -0
- package/server/services/behavior/modules/persona_prompt.js +33 -0
- package/server/services/behavior/modules/social_memory.js +86 -0
- package/server/services/behavior/modules/social_observability.js +94 -0
- package/server/services/behavior/modules/social_signals.js +41 -0
- package/server/services/behavior/modules/theory_of_mind.js +110 -0
- package/server/services/behavior/modules/turn_taking.js +237 -0
- package/server/services/behavior/pipeline.js +285 -0
- package/server/services/behavior/registry.js +78 -0
- package/server/services/behavior/signals.js +107 -0
- package/server/services/behavior/state.js +99 -0
- package/server/services/behavior/system_prompt.js +75 -0
- package/server/services/browser/controller.js +843 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +15 -1
- package/server/services/browser/extension/provider.js +71 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- package/server/services/credentials/bitwarden_cli.js +322 -0
- package/server/services/credentials/broker.js +594 -0
- 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/bitwarden/constants.js +14 -0
- package/server/services/integrations/bitwarden/provider.js +197 -0
- package/server/services/integrations/bitwarden/snapshot.js +65 -0
- 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 +159 -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/registry.js +2 -0
- 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 +110 -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 +66 -52
- package/server/services/messaging/access_policy.js +10 -6
- package/server/services/messaging/automation.js +240 -34
- package/server/services/messaging/discord.js +11 -1
- package/server/services/messaging/formatting_guides.js +5 -4
- package/server/services/messaging/http_platforms.js +37 -16
- package/server/services/messaging/inbound_queue.js +143 -28
- package/server/services/messaging/inbound_store.js +257 -0
- package/server/services/messaging/manager.js +344 -51
- package/server/services/messaging/telegram.js +10 -1
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/messaging/whatsapp.js +33 -14
- 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 +227 -67
- package/server/services/runtime/docker-vm-manager.js +9 -0
- package/server/services/runtime/guest_bootstrap.js +30 -4
- package/server/services/runtime/guest_image.js +43 -12
- package/server/services/runtime/manager.js +77 -23
- package/server/services/runtime/validation.js +7 -6
- package/server/services/security/tool_categories.js +6 -0
- 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 +13 -8
- 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/captions.js +2 -2
- package/server/services/social_video/service.js +343 -68
- 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/message.js +1 -1
- package/server/services/voice/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- package/server/services/voice/runtime.js +2 -2
- 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/logger.js +19 -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,24 @@ export const COMMANDS = Object.freeze({
|
|
|
13
22
|
CLOSE: 'close',
|
|
14
23
|
GET_PAGE_INFO: 'getPageInfo',
|
|
15
24
|
GET_COOKIES: 'getCookies',
|
|
25
|
+
FILL_CREDENTIAL: 'fillCredential',
|
|
26
|
+
SUBMIT_CREDENTIAL: 'submitCredential',
|
|
27
|
+
CANCEL_CREDENTIAL: 'cancelCredential',
|
|
28
|
+
CANCEL_COMMAND: 'cancelCommand',
|
|
16
29
|
});
|
|
17
30
|
|
|
31
|
+
function abortError(signal) {
|
|
32
|
+
if (signal?.reason instanceof Error) return signal.reason;
|
|
33
|
+
const error = new Error(String(signal?.reason || 'Browser extension command aborted.'));
|
|
34
|
+
error.name = 'AbortError';
|
|
35
|
+
error.code = 'ABORT_ERR';
|
|
36
|
+
return error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function throwIfAborted(signal) {
|
|
40
|
+
if (signal?.aborted) throw abortError(signal);
|
|
41
|
+
}
|
|
42
|
+
|
|
18
43
|
function chromeCall(chromeApi, namespace, method, ...args) {
|
|
19
44
|
return new Promise((resolve, reject) => {
|
|
20
45
|
chromeApi[namespace][method](...args, (result) => {
|
|
@@ -28,8 +53,50 @@ function chromeCall(chromeApi, namespace, method, ...args) {
|
|
|
28
53
|
});
|
|
29
54
|
}
|
|
30
55
|
|
|
31
|
-
function
|
|
32
|
-
|
|
56
|
+
function waitForAbortable(promise, signal = null) {
|
|
57
|
+
throwIfAborted(signal);
|
|
58
|
+
if (!signal) return promise;
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
let settled = false;
|
|
61
|
+
const finish = (callback, value) => {
|
|
62
|
+
if (settled) return;
|
|
63
|
+
settled = true;
|
|
64
|
+
signal.removeEventListener('abort', onAbort);
|
|
65
|
+
callback(value);
|
|
66
|
+
};
|
|
67
|
+
const onAbort = () => finish(reject, abortError(signal));
|
|
68
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
69
|
+
if (signal.aborted) onAbort();
|
|
70
|
+
Promise.resolve(promise).then(
|
|
71
|
+
(value) => finish(resolve, value),
|
|
72
|
+
(error) => finish(reject, error),
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function optionalResult(promise, fallback, signal = null) {
|
|
78
|
+
try {
|
|
79
|
+
return await promise;
|
|
80
|
+
} catch {
|
|
81
|
+
throwIfAborted(signal);
|
|
82
|
+
return fallback;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function delay(ms, signal = null) {
|
|
87
|
+
throwIfAborted(signal);
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const timer = setTimeout(() => {
|
|
90
|
+
signal?.removeEventListener('abort', onAbort);
|
|
91
|
+
resolve();
|
|
92
|
+
}, ms);
|
|
93
|
+
const onAbort = () => {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
reject(abortError(signal));
|
|
96
|
+
};
|
|
97
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
98
|
+
if (signal?.aborted) onAbort();
|
|
99
|
+
});
|
|
33
100
|
}
|
|
34
101
|
|
|
35
102
|
function jsString(value) {
|
|
@@ -58,113 +125,414 @@ function keyCodeFor(key) {
|
|
|
58
125
|
return map[normalized] || (normalized.length === 1 ? normalized.toUpperCase().charCodeAt(0) : 0);
|
|
59
126
|
}
|
|
60
127
|
|
|
61
|
-
|
|
128
|
+
function normalizePointCoordinate(value, label) {
|
|
129
|
+
if (value == null || value === '') {
|
|
130
|
+
throw new Error(`${label} coordinate is required.`);
|
|
131
|
+
}
|
|
132
|
+
const normalized = Number(value);
|
|
133
|
+
if (!Number.isFinite(normalized)) {
|
|
134
|
+
throw new Error(`${label} coordinate must be a finite number.`);
|
|
135
|
+
}
|
|
136
|
+
return Math.round(normalized);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function ipv4Octets(hostname) {
|
|
140
|
+
if (!/^\d{1,3}(?:\.\d{1,3}){3}$/.test(hostname)) return null;
|
|
141
|
+
const parts = hostname.split('.').map(Number);
|
|
142
|
+
return parts.every((part) => part >= 0 && part <= 255) ? parts : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function isPrivateNetworkHostname(hostname) {
|
|
146
|
+
const normalized = String(hostname || '')
|
|
147
|
+
.toLowerCase()
|
|
148
|
+
.replace(/^\[|\]$/g, '')
|
|
149
|
+
.replace(/\.$/, '');
|
|
150
|
+
if (!normalized) return true;
|
|
151
|
+
if (
|
|
152
|
+
normalized === 'localhost'
|
|
153
|
+
|| normalized === 'localhost.localdomain'
|
|
154
|
+
|| normalized.endsWith('.localhost')
|
|
155
|
+
|| normalized.endsWith('.local')
|
|
156
|
+
|| normalized.endsWith('.internal')
|
|
157
|
+
) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const octets = ipv4Octets(normalized);
|
|
162
|
+
if (octets) {
|
|
163
|
+
const [a, b, c] = octets;
|
|
164
|
+
return a === 0
|
|
165
|
+
|| a === 10
|
|
166
|
+
|| a === 127
|
|
167
|
+
|| a >= 224
|
|
168
|
+
|| (a === 100 && b >= 64 && b <= 127)
|
|
169
|
+
|| (a === 169 && b === 254)
|
|
170
|
+
|| (a === 172 && b >= 16 && b <= 31)
|
|
171
|
+
|| (a === 192 && b === 168)
|
|
172
|
+
|| (a === 192 && b === 0 && c === 0)
|
|
173
|
+
|| (a === 192 && b === 0 && c === 2)
|
|
174
|
+
|| (a === 198 && (b === 18 || b === 19))
|
|
175
|
+
|| (a === 198 && b === 51 && c === 100)
|
|
176
|
+
|| (a === 203 && b === 0 && c === 113);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (normalized.includes(':')) {
|
|
180
|
+
if (normalized === '::' || normalized === '::1') return true;
|
|
181
|
+
if (normalized.startsWith('::ffff:')) return true;
|
|
182
|
+
const first = Number.parseInt(normalized.split(':').find(Boolean) || '0', 16);
|
|
183
|
+
if (!Number.isFinite(first)) return true;
|
|
184
|
+
if ((first & 0xfe00) === 0xfc00) return true;
|
|
185
|
+
if ((first & 0xffc0) === 0xfe80) return true;
|
|
186
|
+
if ((first & 0xffc0) === 0xfec0) return true;
|
|
187
|
+
if ((first & 0xff00) === 0xff00) return true;
|
|
188
|
+
if (normalized.startsWith('2001:db8:')) return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function normalizeNetworkValidationUrl(value) {
|
|
195
|
+
const raw = String(value || '');
|
|
196
|
+
if (!raw || raw.length > 8192) return null;
|
|
197
|
+
let parsed;
|
|
198
|
+
try {
|
|
199
|
+
parsed = new URL(raw);
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) return null;
|
|
204
|
+
if (parsed.username || parsed.password || isPrivateNetworkHostname(parsed.hostname)) return null;
|
|
205
|
+
const protocol = parsed.protocol === 'ws:'
|
|
206
|
+
? 'http:'
|
|
207
|
+
: (parsed.protocol === 'wss:' ? 'https:' : parsed.protocol);
|
|
208
|
+
return `${protocol}//${parsed.host}/`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const PAGE_ACCESS_COMMANDS = new Set([
|
|
212
|
+
COMMANDS.LAUNCH,
|
|
213
|
+
COMMANDS.CLICK,
|
|
214
|
+
COMMANDS.CLICK_POINT,
|
|
215
|
+
COMMANDS.TYPE,
|
|
216
|
+
COMMANDS.TYPE_TEXT,
|
|
217
|
+
COMMANDS.PRESS_KEY,
|
|
218
|
+
COMMANDS.SCROLL,
|
|
219
|
+
COMMANDS.EXTRACT,
|
|
220
|
+
COMMANDS.EVALUATE,
|
|
221
|
+
COMMANDS.SCREENSHOT,
|
|
222
|
+
COMMANDS.GET_PAGE_INFO,
|
|
223
|
+
COMMANDS.FILL_CREDENTIAL,
|
|
224
|
+
COMMANDS.SUBMIT_CREDENTIAL,
|
|
225
|
+
COMMANDS.CANCEL_CREDENTIAL,
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
export function createBrowserProtocol(chromeApi, options = {}) {
|
|
62
229
|
let attachedTabId = null;
|
|
63
230
|
let activeTabId = null;
|
|
231
|
+
let protectedCredentialFill = null;
|
|
232
|
+
const pausedRequests = new Map();
|
|
233
|
+
const maxPausedRequests = Math.max(1, Math.min(Number(options.maxPausedRequests) || 256, 1024));
|
|
234
|
+
const validateUrl = typeof options.validateUrl === 'function'
|
|
235
|
+
? options.validateUrl
|
|
236
|
+
: async () => false;
|
|
64
237
|
|
|
65
238
|
const debuggee = () => ({ tabId: activeTabId });
|
|
66
239
|
|
|
67
|
-
|
|
240
|
+
const call = (signal, namespace, method, ...args) => waitForAbortable(
|
|
241
|
+
chromeCall(chromeApi, namespace, method, ...args),
|
|
242
|
+
signal,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
function abortPausedRequests(tabId, reason) {
|
|
246
|
+
for (const entry of pausedRequests.values()) {
|
|
247
|
+
if (entry.tabId === tabId && !entry.controller.signal.aborted) {
|
|
248
|
+
entry.controller.abort(reason);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function sendToTab(tabId, method, params = {}, signal = null) {
|
|
254
|
+
try {
|
|
255
|
+
return await call(signal, 'debugger', 'sendCommand', { tabId }, method, params);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (
|
|
258
|
+
tabId === attachedTabId
|
|
259
|
+
&& /not attached|no tab with given id|target closed/i.test(String(error?.message || ''))
|
|
260
|
+
) {
|
|
261
|
+
attachedTabId = null;
|
|
262
|
+
}
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function isNetworkUrlAllowed(url, signal = null) {
|
|
268
|
+
throwIfAborted(signal);
|
|
269
|
+
const normalized = normalizeNetworkValidationUrl(url);
|
|
270
|
+
if (!normalized) return false;
|
|
271
|
+
const result = await validateUrl(normalized, { signal });
|
|
272
|
+
throwIfAborted(signal);
|
|
273
|
+
return result === true || result?.allowed === true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function handlePausedRequest(source, params) {
|
|
277
|
+
const tabId = source?.tabId;
|
|
278
|
+
const requestId = String(params?.requestId || '');
|
|
279
|
+
if (tabId == null || tabId !== attachedTabId || !requestId) return;
|
|
280
|
+
if (pausedRequests.size >= maxPausedRequests) {
|
|
281
|
+
await sendToTab(tabId, 'Fetch.failRequest', {
|
|
282
|
+
requestId,
|
|
283
|
+
errorReason: 'BlockedByClient',
|
|
284
|
+
}).catch(() => {});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const key = `${tabId}:${requestId}`;
|
|
289
|
+
const previous = pausedRequests.get(key);
|
|
290
|
+
previous?.controller.abort(new Error('Browser request interception was superseded.'));
|
|
291
|
+
const controller = new AbortController();
|
|
292
|
+
pausedRequests.set(key, { tabId, controller });
|
|
293
|
+
let allowed = false;
|
|
294
|
+
try {
|
|
295
|
+
allowed = await isNetworkUrlAllowed(params?.request?.url, controller.signal);
|
|
296
|
+
} catch {
|
|
297
|
+
allowed = false;
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
await sendToTab(
|
|
301
|
+
tabId,
|
|
302
|
+
allowed ? 'Fetch.continueRequest' : 'Fetch.failRequest',
|
|
303
|
+
allowed
|
|
304
|
+
? { requestId }
|
|
305
|
+
: { requestId, errorReason: 'BlockedByClient' },
|
|
306
|
+
);
|
|
307
|
+
} catch {
|
|
308
|
+
// Detach and tab-close races make the paused request disappear on their own.
|
|
309
|
+
} finally {
|
|
310
|
+
if (pausedRequests.get(key)?.controller === controller) pausedRequests.delete(key);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
chromeApi.debugger?.onEvent?.addListener?.((source, method, params) => {
|
|
315
|
+
if (method !== 'Fetch.requestPaused') return;
|
|
316
|
+
handlePausedRequest(source, params).catch(() => {});
|
|
317
|
+
});
|
|
318
|
+
chromeApi.debugger?.onDetach?.addListener?.((source) => {
|
|
319
|
+
if (source?.tabId === attachedTabId) {
|
|
320
|
+
abortPausedRequests(source.tabId, new Error('Browser debugger detached.'));
|
|
321
|
+
attachedTabId = null;
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
chromeApi.tabs?.onRemoved?.addListener?.((tabId) => {
|
|
325
|
+
if (tabId === activeTabId) activeTabId = null;
|
|
326
|
+
if (tabId === attachedTabId) {
|
|
327
|
+
abortPausedRequests(tabId, new Error('Browser tab closed.'));
|
|
328
|
+
attachedTabId = null;
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
async function ensureTab(signal = null) {
|
|
333
|
+
throwIfAborted(signal);
|
|
68
334
|
if (activeTabId != null) {
|
|
69
335
|
try {
|
|
70
|
-
await
|
|
336
|
+
await call(signal, 'tabs', 'get', activeTabId);
|
|
71
337
|
return activeTabId;
|
|
72
338
|
} catch {
|
|
339
|
+
throwIfAborted(signal);
|
|
73
340
|
activeTabId = null;
|
|
74
341
|
}
|
|
75
342
|
}
|
|
76
343
|
|
|
77
|
-
const tabs = await
|
|
344
|
+
const tabs = await call(signal, 'tabs', 'query', { active: true, currentWindow: true });
|
|
78
345
|
if (tabs && tabs[0]?.id != null) {
|
|
79
346
|
activeTabId = tabs[0].id;
|
|
80
347
|
return activeTabId;
|
|
81
348
|
}
|
|
82
|
-
const tab = await
|
|
349
|
+
const tab = await call(signal, 'tabs', 'create', { url: 'about:blank', active: true });
|
|
83
350
|
activeTabId = tab.id;
|
|
84
351
|
return activeTabId;
|
|
85
352
|
}
|
|
86
353
|
|
|
87
|
-
async function attach() {
|
|
88
|
-
await ensureTab();
|
|
354
|
+
async function attach(signal = null) {
|
|
355
|
+
await ensureTab(signal);
|
|
89
356
|
if (attachedTabId === activeTabId) return;
|
|
90
357
|
if (attachedTabId != null) {
|
|
91
|
-
|
|
358
|
+
abortPausedRequests(attachedTabId, new Error('Browser control moved to another tab.'));
|
|
359
|
+
await optionalResult(
|
|
360
|
+
call(signal, 'debugger', 'detach', { tabId: attachedTabId }),
|
|
361
|
+
undefined,
|
|
362
|
+
signal,
|
|
363
|
+
);
|
|
92
364
|
}
|
|
93
365
|
try {
|
|
94
|
-
await
|
|
366
|
+
await call(signal, 'debugger', 'attach', debuggee(), '1.3');
|
|
95
367
|
} catch (error) {
|
|
368
|
+
throwIfAborted(signal);
|
|
96
369
|
if (!/another debugger|already attached|debugger is already attached/i.test(error.message)) {
|
|
97
370
|
throw error;
|
|
98
371
|
}
|
|
99
372
|
}
|
|
100
373
|
attachedTabId = activeTabId;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
374
|
+
try {
|
|
375
|
+
await sendToTab(attachedTabId, 'Fetch.enable', {
|
|
376
|
+
patterns: [
|
|
377
|
+
{ urlPattern: 'http://*/*', requestStage: 'Request' },
|
|
378
|
+
{ urlPattern: 'https://*/*', requestStage: 'Request' },
|
|
379
|
+
{ urlPattern: 'ws://*/*', requestStage: 'Request' },
|
|
380
|
+
{ urlPattern: 'wss://*/*', requestStage: 'Request' },
|
|
381
|
+
],
|
|
382
|
+
}, signal);
|
|
383
|
+
await optionalResult(send('Page.enable', {}, signal), undefined, signal);
|
|
384
|
+
await optionalResult(send('Runtime.enable', {}, signal), undefined, signal);
|
|
385
|
+
await optionalResult(send('DOM.enable', {}, signal), undefined, signal);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
const failedTabId = attachedTabId;
|
|
388
|
+
abortPausedRequests(failedTabId, error);
|
|
389
|
+
attachedTabId = null;
|
|
390
|
+
if (failedTabId != null) {
|
|
391
|
+
await chromeCall(chromeApi, 'debugger', 'detach', { tabId: failedTabId }).catch(() => {});
|
|
392
|
+
}
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
104
395
|
}
|
|
105
396
|
|
|
106
|
-
async function send(method, params = {}) {
|
|
107
|
-
|
|
397
|
+
async function send(method, params = {}, signal = null) {
|
|
398
|
+
const tabId = attachedTabId ?? activeTabId;
|
|
399
|
+
if (tabId == null) throw new Error('No browser tab is available.');
|
|
400
|
+
return sendToTab(tabId, method, params, signal);
|
|
108
401
|
}
|
|
109
402
|
|
|
110
|
-
async function evalJs(expression, options = {}) {
|
|
111
|
-
await attach();
|
|
403
|
+
async function evalJs(expression, options = {}, signal = null) {
|
|
404
|
+
await attach(signal);
|
|
112
405
|
const response = await send('Runtime.evaluate', {
|
|
113
406
|
expression,
|
|
114
407
|
awaitPromise: options.awaitPromise !== false,
|
|
115
408
|
returnByValue: true,
|
|
116
|
-
});
|
|
409
|
+
}, signal);
|
|
117
410
|
if (response?.exceptionDetails) {
|
|
118
411
|
throw new Error(response.exceptionDetails.text || 'JavaScript evaluation failed.');
|
|
119
412
|
}
|
|
120
413
|
return response?.result?.value;
|
|
121
414
|
}
|
|
122
415
|
|
|
123
|
-
async function waitForLoad(timeoutMs = 30000) {
|
|
416
|
+
async function waitForLoad(timeoutMs = 30000, signal = null) {
|
|
124
417
|
const started = Date.now();
|
|
125
418
|
while (Date.now() - started < timeoutMs) {
|
|
126
|
-
|
|
127
|
-
|
|
419
|
+
throwIfAborted(signal);
|
|
420
|
+
const ready = await optionalResult(
|
|
421
|
+
evalJs(
|
|
422
|
+
'document.readyState === "complete" || document.readyState === "interactive"',
|
|
423
|
+
{ awaitPromise: false },
|
|
424
|
+
signal,
|
|
425
|
+
),
|
|
426
|
+
false,
|
|
427
|
+
signal,
|
|
428
|
+
);
|
|
128
429
|
if (ready) return;
|
|
129
|
-
await delay(250);
|
|
430
|
+
await delay(250, signal);
|
|
130
431
|
}
|
|
432
|
+
throw new Error(`Page did not finish loading within ${timeoutMs}ms.`);
|
|
131
433
|
}
|
|
132
434
|
|
|
133
|
-
async function
|
|
435
|
+
async function markCurrentDocument(signal = null) {
|
|
436
|
+
const key = `__neoagent_document_${crypto.randomUUID().replace(/-/g, '')}`;
|
|
437
|
+
const token = crypto.randomUUID();
|
|
438
|
+
const marked = await optionalResult(
|
|
439
|
+
evalJs(
|
|
440
|
+
`globalThis[${jsString(key)}] = ${jsString(token)}; true`,
|
|
441
|
+
{ awaitPromise: false },
|
|
442
|
+
signal,
|
|
443
|
+
),
|
|
444
|
+
false,
|
|
445
|
+
signal,
|
|
446
|
+
);
|
|
447
|
+
return marked ? { key, token } : null;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function waitForDocumentReplacement(marker, timeoutMs = 30000, signal = null) {
|
|
451
|
+
if (!marker) {
|
|
452
|
+
await delay(250, signal);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const started = Date.now();
|
|
456
|
+
const expression = `globalThis[${jsString(marker.key)}] !== ${jsString(marker.token)}`;
|
|
457
|
+
while (Date.now() - started < timeoutMs) {
|
|
458
|
+
throwIfAborted(signal);
|
|
459
|
+
const replaced = await optionalResult(
|
|
460
|
+
evalJs(expression, { awaitPromise: false }, signal),
|
|
461
|
+
false,
|
|
462
|
+
signal,
|
|
463
|
+
);
|
|
464
|
+
if (replaced) return;
|
|
465
|
+
await delay(100, signal);
|
|
466
|
+
}
|
|
467
|
+
throw new Error(`Page navigation did not commit within ${timeoutMs}ms.`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function assertNavigationSucceeded(signal = null) {
|
|
471
|
+
const frameTree = await send('Page.getFrameTree', {}, signal);
|
|
472
|
+
const unreachableUrl = String(frameTree?.frameTree?.frame?.unreachableUrl || '');
|
|
473
|
+
if (unreachableUrl) {
|
|
474
|
+
throw new Error('Browser navigation failed before the destination loaded.');
|
|
475
|
+
}
|
|
476
|
+
await assertCurrentPageAllowed(signal);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function waitForSelector(selector, timeoutMs = 10000, signal = null) {
|
|
134
480
|
if (!selector) return;
|
|
135
481
|
const expression = `Boolean(document.querySelector(${jsString(selector)}))`;
|
|
136
482
|
const started = Date.now();
|
|
137
483
|
while (Date.now() - started < timeoutMs) {
|
|
138
|
-
|
|
139
|
-
await
|
|
484
|
+
throwIfAborted(signal);
|
|
485
|
+
if (await optionalResult(evalJs(expression, { awaitPromise: false }, signal), false, signal)) return;
|
|
486
|
+
await delay(200, signal);
|
|
140
487
|
}
|
|
488
|
+
throw new Error(`Element not found within ${timeoutMs}ms: ${selector}`);
|
|
141
489
|
}
|
|
142
490
|
|
|
143
|
-
async function currentTab() {
|
|
144
|
-
await ensureTab();
|
|
145
|
-
return
|
|
491
|
+
async function currentTab(signal = null) {
|
|
492
|
+
await ensureTab(signal);
|
|
493
|
+
return call(signal, 'tabs', 'get', activeTabId);
|
|
146
494
|
}
|
|
147
495
|
|
|
148
|
-
async function
|
|
149
|
-
await
|
|
496
|
+
async function assertCurrentPageAllowed(signal = null) {
|
|
497
|
+
const tab = await currentTab(signal);
|
|
498
|
+
const url = String(tab?.url || '');
|
|
499
|
+
if (url === 'about:blank') return tab;
|
|
500
|
+
if (!await isNetworkUrlAllowed(url, signal)) {
|
|
501
|
+
throw new Error('The current browser page is not permitted.');
|
|
502
|
+
}
|
|
503
|
+
return tab;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function screenshotDataUrl(options = {}, signal = null) {
|
|
507
|
+
throwIfAborted(signal);
|
|
508
|
+
await attach(signal);
|
|
150
509
|
const capture = await send('Page.captureScreenshot', {
|
|
151
510
|
format: 'png',
|
|
152
511
|
captureBeyondViewport: options.fullPage === true,
|
|
153
512
|
fromSurface: true,
|
|
154
|
-
});
|
|
155
|
-
|
|
513
|
+
}, signal);
|
|
514
|
+
const encoded = String(capture?.data || '');
|
|
515
|
+
if (encoded.length > 24 * 1024 * 1024) {
|
|
516
|
+
throw new Error('Browser screenshot is too large to transfer. Try a viewport screenshot instead.');
|
|
517
|
+
}
|
|
518
|
+
return `data:image/png;base64,${encoded}`;
|
|
156
519
|
}
|
|
157
520
|
|
|
158
|
-
async function pageSnapshot(options = {}) {
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
const
|
|
521
|
+
async function pageSnapshot(options = {}, signal = null) {
|
|
522
|
+
throwIfAborted(signal);
|
|
523
|
+
const tab = await currentTab(signal);
|
|
524
|
+
const title = await optionalResult(
|
|
525
|
+
evalJs('document.title || ""', { awaitPromise: false }, signal),
|
|
526
|
+
tab.title || '',
|
|
527
|
+
signal,
|
|
528
|
+
);
|
|
529
|
+
const bodyText = await optionalResult(evalJs(`(() => {
|
|
162
530
|
const body = document.body;
|
|
163
531
|
if (!body) return '';
|
|
164
532
|
const clone = body.cloneNode(true);
|
|
165
533
|
clone.querySelectorAll('script, style, noscript').forEach((node) => node.remove());
|
|
166
534
|
return String(clone.innerText || '').slice(0, 10000);
|
|
167
|
-
})()
|
|
535
|
+
})()`, {}, signal), '', signal);
|
|
168
536
|
const result = {
|
|
169
537
|
title: title || tab.title || '',
|
|
170
538
|
url: tab.url || '',
|
|
@@ -172,12 +540,13 @@ export function createBrowserProtocol(chromeApi) {
|
|
|
172
540
|
bodyText,
|
|
173
541
|
};
|
|
174
542
|
if (options.screenshot !== false) {
|
|
175
|
-
result.screenshotDataUrl = await screenshotDataUrl(options);
|
|
543
|
+
result.screenshotDataUrl = await screenshotDataUrl(options, signal);
|
|
176
544
|
}
|
|
177
545
|
return result;
|
|
178
546
|
}
|
|
179
547
|
|
|
180
|
-
async function locateTarget(payload = {}) {
|
|
548
|
+
async function locateTarget(payload = {}, signal = null) {
|
|
549
|
+
throwIfAborted(signal);
|
|
181
550
|
const selector = String(payload.selector || '').trim();
|
|
182
551
|
const text = String(payload.text || '').trim().toLowerCase();
|
|
183
552
|
const expression = selector
|
|
@@ -194,42 +563,71 @@ export function createBrowserProtocol(chromeApi) {
|
|
|
194
563
|
const rect = target.getBoundingClientRect();
|
|
195
564
|
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
196
565
|
})()`;
|
|
197
|
-
const point = await evalJs(expression);
|
|
566
|
+
const point = await evalJs(expression, {}, signal);
|
|
198
567
|
if (!point || !Number.isFinite(Number(point.x)) || !Number.isFinite(Number(point.y))) {
|
|
199
568
|
throw new Error(selector ? `Element not found: ${selector}` : `No clickable element found with text: ${payload.text}`);
|
|
200
569
|
}
|
|
201
570
|
return { x: Math.round(point.x), y: Math.round(point.y) };
|
|
202
571
|
}
|
|
203
572
|
|
|
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
|
|
573
|
+
async function clickPoint(x, y, signal = null) {
|
|
574
|
+
throwIfAborted(signal);
|
|
575
|
+
await attach(signal);
|
|
576
|
+
const px = Math.max(0, normalizePointCoordinate(x, 'x'));
|
|
577
|
+
const py = Math.max(0, normalizePointCoordinate(y, 'y'));
|
|
578
|
+
await send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: px, y: py }, signal);
|
|
579
|
+
await send('Input.dispatchMouseEvent', { type: 'mousePressed', x: px, y: py, button: 'left', clickCount: 1 }, signal);
|
|
580
|
+
await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: px, y: py, button: 'left', clickCount: 1 }, signal);
|
|
581
|
+
await delay(500, signal);
|
|
212
582
|
return { x: px, y: py };
|
|
213
583
|
}
|
|
214
584
|
|
|
215
|
-
async function typeKey(key) {
|
|
216
|
-
await attach();
|
|
585
|
+
async function typeKey(key, signal = null) {
|
|
586
|
+
await attach(signal);
|
|
217
587
|
const normalized = String(key || '').trim();
|
|
218
588
|
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 });
|
|
589
|
+
await send('Input.dispatchKeyEvent', { type: 'keyDown', key: normalized, windowsVirtualKeyCode: code }, signal);
|
|
590
|
+
await send('Input.dispatchKeyEvent', { type: 'keyUp', key: normalized, windowsVirtualKeyCode: code }, signal);
|
|
221
591
|
}
|
|
222
592
|
|
|
223
|
-
async function run(command, payload = {}) {
|
|
593
|
+
async function run(command, payload = {}, options = {}) {
|
|
594
|
+
const signal = options.signal || null;
|
|
595
|
+
throwIfAborted(signal);
|
|
596
|
+
if (protectedCredentialFill?.expiresAt <= Date.now()) {
|
|
597
|
+
const expired = protectedCredentialFill;
|
|
598
|
+
await evalJs(`(() => {
|
|
599
|
+
for (const selector of [${jsString(expired.usernameSelector)}, ${jsString(expired.passwordSelector)}].filter(Boolean)) {
|
|
600
|
+
const el = document.querySelector(selector);
|
|
601
|
+
if (el && 'value' in el) el.value = '';
|
|
602
|
+
}
|
|
603
|
+
})()`, {}, signal).catch(() => {});
|
|
604
|
+
protectedCredentialFill = null;
|
|
605
|
+
}
|
|
606
|
+
if (
|
|
607
|
+
protectedCredentialFill
|
|
608
|
+
&& ![
|
|
609
|
+
COMMANDS.GET_PAGE_INFO,
|
|
610
|
+
COMMANDS.SUBMIT_CREDENTIAL,
|
|
611
|
+
COMMANDS.CANCEL_CREDENTIAL,
|
|
612
|
+
COMMANDS.CLOSE,
|
|
613
|
+
].includes(command)
|
|
614
|
+
) {
|
|
615
|
+
throw new Error('Browser control is paused while a protected credential fill is active. Submit or cancel it first.');
|
|
616
|
+
}
|
|
617
|
+
if (PAGE_ACCESS_COMMANDS.has(command)) {
|
|
618
|
+
await assertCurrentPageAllowed(signal);
|
|
619
|
+
}
|
|
224
620
|
switch (command) {
|
|
225
621
|
case COMMANDS.GET_COOKIES: {
|
|
226
622
|
const domains = Array.isArray(payload.domains)
|
|
227
623
|
? payload.domains.map((item) => String(item || '').replace(/^\./, '').toLowerCase()).filter(Boolean)
|
|
228
624
|
: [];
|
|
229
625
|
if (domains.length === 0) throw new Error('At least one cookie domain is required.');
|
|
626
|
+
if (domains.length > 50) throw new Error('At most 50 cookie domains may be requested.');
|
|
230
627
|
const all = [];
|
|
231
628
|
for (const domain of domains) {
|
|
232
|
-
|
|
629
|
+
throwIfAborted(signal);
|
|
630
|
+
const cookies = await call(signal, 'cookies', 'getAll', { domain });
|
|
233
631
|
all.push(...(Array.isArray(cookies) ? cookies : []));
|
|
234
632
|
}
|
|
235
633
|
const seen = new Set();
|
|
@@ -258,50 +656,185 @@ export function createBrowserProtocol(chromeApi) {
|
|
|
258
656
|
};
|
|
259
657
|
}
|
|
260
658
|
case COMMANDS.LAUNCH:
|
|
261
|
-
await attach();
|
|
262
|
-
return pageSnapshot({ screenshot: false });
|
|
659
|
+
await attach(signal);
|
|
660
|
+
return pageSnapshot({ screenshot: false }, signal);
|
|
263
661
|
case COMMANDS.NAVIGATE:
|
|
264
|
-
await attach();
|
|
265
662
|
if (!payload.url) throw new Error('url required');
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
663
|
+
if (!await isNetworkUrlAllowed(payload.url, signal)) {
|
|
664
|
+
throw new Error('This browser URL is not permitted.');
|
|
665
|
+
}
|
|
666
|
+
await attach(signal);
|
|
667
|
+
{
|
|
668
|
+
const marker = await markCurrentDocument(signal);
|
|
669
|
+
let completed = false;
|
|
670
|
+
try {
|
|
671
|
+
const navigation = await send('Page.navigate', { url: String(payload.url) }, signal);
|
|
672
|
+
if (navigation?.errorText) {
|
|
673
|
+
throw new Error(`Browser navigation failed: ${navigation.errorText}`);
|
|
674
|
+
}
|
|
675
|
+
if (navigation?.isDownload === true) {
|
|
676
|
+
throw new Error('Browser navigation started a download instead of loading a page.');
|
|
677
|
+
}
|
|
678
|
+
if (navigation?.loaderId) {
|
|
679
|
+
await waitForDocumentReplacement(marker, 30000, signal);
|
|
680
|
+
} else {
|
|
681
|
+
await delay(100, signal);
|
|
682
|
+
}
|
|
683
|
+
await waitForLoad(30000, signal);
|
|
684
|
+
await assertNavigationSucceeded(signal);
|
|
685
|
+
await waitForSelector(payload.waitFor, 10000, signal);
|
|
686
|
+
throwIfAborted(signal);
|
|
687
|
+
completed = true;
|
|
688
|
+
return pageSnapshot(payload, signal);
|
|
689
|
+
} finally {
|
|
690
|
+
if (!completed) await send('Page.stopLoading').catch(() => {});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
270
693
|
case COMMANDS.CLICK: {
|
|
271
|
-
const point = await locateTarget(payload);
|
|
272
|
-
await clickPoint(point.x, point.y);
|
|
273
|
-
|
|
694
|
+
const point = await locateTarget(payload, signal);
|
|
695
|
+
await clickPoint(point.x, point.y, signal);
|
|
696
|
+
throwIfAborted(signal);
|
|
697
|
+
return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
|
|
274
698
|
}
|
|
275
699
|
case COMMANDS.CLICK_POINT:
|
|
276
|
-
await clickPoint(payload.x, payload.y);
|
|
277
|
-
|
|
700
|
+
await clickPoint(payload.x, payload.y, signal);
|
|
701
|
+
throwIfAborted(signal);
|
|
702
|
+
return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
|
|
278
703
|
case COMMANDS.TYPE:
|
|
279
704
|
if (!payload.selector) throw new Error('selector required');
|
|
280
705
|
if (payload.clear !== false) {
|
|
281
706
|
await evalJs(`(() => {
|
|
282
707
|
const el = document.querySelector(${jsString(payload.selector)});
|
|
283
|
-
if (!el) throw new Error(
|
|
708
|
+
if (!el) throw new Error(${jsString(`Element not found: ${payload.selector}`)});
|
|
284
709
|
el.focus();
|
|
285
710
|
if ('value' in el) el.value = '';
|
|
286
|
-
})()
|
|
711
|
+
})()`, {}, signal);
|
|
287
712
|
} else {
|
|
288
|
-
await evalJs(`document.querySelector(${jsString(payload.selector)})?.focus()
|
|
713
|
+
await evalJs(`document.querySelector(${jsString(payload.selector)})?.focus()`, {}, signal);
|
|
289
714
|
}
|
|
290
|
-
await send('Input.insertText', { text: String(payload.text || '') });
|
|
291
|
-
|
|
292
|
-
|
|
715
|
+
await send('Input.insertText', { text: String(payload.text || '') }, signal);
|
|
716
|
+
throwIfAborted(signal);
|
|
717
|
+
if (payload.pressEnter) await typeKey('Enter', signal);
|
|
718
|
+
return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
|
|
293
719
|
case COMMANDS.TYPE_TEXT:
|
|
294
|
-
await attach();
|
|
295
|
-
await send('Input.insertText', { text: String(payload.text || '') });
|
|
296
|
-
|
|
297
|
-
|
|
720
|
+
await attach(signal);
|
|
721
|
+
await send('Input.insertText', { text: String(payload.text || '') }, signal);
|
|
722
|
+
throwIfAborted(signal);
|
|
723
|
+
if (payload.pressEnter) await typeKey('Enter', signal);
|
|
724
|
+
return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
|
|
725
|
+
case COMMANDS.FILL_CREDENTIAL: {
|
|
726
|
+
if (protectedCredentialFill) throw new Error('A protected credential fill is already active.');
|
|
727
|
+
const allowedOrigin = new URL(String(payload.allowedOrigin || '')).origin;
|
|
728
|
+
const tab = await currentTab(signal);
|
|
729
|
+
if (new URL(String(tab.url || '')).origin !== allowedOrigin) {
|
|
730
|
+
throw new Error('The browser origin changed before credential fill.');
|
|
731
|
+
}
|
|
732
|
+
const usernameSelector = String(payload.usernameSelector || '').trim();
|
|
733
|
+
const passwordSelector = String(payload.passwordSelector || '').trim();
|
|
734
|
+
if (!usernameSelector && !passwordSelector) throw new Error('At least one credential field selector is required.');
|
|
735
|
+
await attach(signal);
|
|
736
|
+
const marker = await markCurrentDocument(signal);
|
|
737
|
+
await send('Page.reload', {}, signal);
|
|
738
|
+
await waitForDocumentReplacement(marker, 30000, signal);
|
|
739
|
+
await waitForLoad(30000, signal);
|
|
740
|
+
const reloaded = await currentTab(signal);
|
|
741
|
+
if (new URL(String(reloaded.url || '')).origin !== allowedOrigin) {
|
|
742
|
+
throw new Error('The browser origin changed while preparing credential fill.');
|
|
743
|
+
}
|
|
744
|
+
if (usernameSelector) {
|
|
745
|
+
await waitForSelector(usernameSelector, 10000, signal);
|
|
746
|
+
await evalJs(`(() => {
|
|
747
|
+
const el = document.querySelector(${jsString(usernameSelector)});
|
|
748
|
+
if (!el) throw new Error('Username field not found.');
|
|
749
|
+
el.value = ${jsString(String(payload.username || ''))};
|
|
750
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
751
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
752
|
+
})()`, {}, signal);
|
|
753
|
+
}
|
|
754
|
+
if (passwordSelector) {
|
|
755
|
+
await waitForSelector(passwordSelector, 10000, signal);
|
|
756
|
+
await evalJs(`(() => {
|
|
757
|
+
const el = document.querySelector(${jsString(passwordSelector)});
|
|
758
|
+
if (!el) throw new Error('Password field not found.');
|
|
759
|
+
el.value = ${jsString(String(payload.password || ''))};
|
|
760
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
761
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
762
|
+
})()`, {}, signal);
|
|
763
|
+
}
|
|
764
|
+
const protectedFillId = globalThis.crypto.randomUUID();
|
|
765
|
+
protectedCredentialFill = {
|
|
766
|
+
id: protectedFillId,
|
|
767
|
+
allowedOrigin,
|
|
768
|
+
usernameSelector,
|
|
769
|
+
passwordSelector,
|
|
770
|
+
submitSelector: passwordSelector || usernameSelector,
|
|
771
|
+
expiresAt: Date.now() + 5 * 60 * 1000,
|
|
772
|
+
};
|
|
773
|
+
return { success: true, protectedFillId, origin: allowedOrigin };
|
|
774
|
+
}
|
|
775
|
+
case COMMANDS.SUBMIT_CREDENTIAL: {
|
|
776
|
+
const fill = protectedCredentialFill;
|
|
777
|
+
if (!fill || fill.id !== String(payload.protectedFillId || '')) {
|
|
778
|
+
throw new Error('Protected credential fill is missing or expired.');
|
|
779
|
+
}
|
|
780
|
+
const tab = await currentTab(signal);
|
|
781
|
+
if (new URL(String(tab.url || '')).origin !== fill.allowedOrigin) {
|
|
782
|
+
protectedCredentialFill = null;
|
|
783
|
+
throw new Error('The protected credential page changed before submission.');
|
|
784
|
+
}
|
|
785
|
+
try {
|
|
786
|
+
await evalJs(`(() => {
|
|
787
|
+
const el = document.querySelector(${jsString(fill.submitSelector)});
|
|
788
|
+
if (!el) throw new Error('Credential field not found.');
|
|
789
|
+
const form = el.form;
|
|
790
|
+
if (form && typeof form.requestSubmit === 'function') form.requestSubmit();
|
|
791
|
+
else el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
792
|
+
})()`, {}, signal);
|
|
793
|
+
await delay(500, signal);
|
|
794
|
+
await waitForLoad(10000, signal).catch(() => {});
|
|
795
|
+
const resultTab = await currentTab(signal);
|
|
796
|
+
return {
|
|
797
|
+
success: true,
|
|
798
|
+
url: resultTab.url || null,
|
|
799
|
+
title: resultTab.title || null,
|
|
800
|
+
protected: false,
|
|
801
|
+
};
|
|
802
|
+
} finally {
|
|
803
|
+
await evalJs(`(() => {
|
|
804
|
+
for (const selector of [${jsString(fill.usernameSelector)}, ${jsString(fill.passwordSelector)}].filter(Boolean)) {
|
|
805
|
+
const el = document.querySelector(selector);
|
|
806
|
+
if (el && 'value' in el) el.value = '';
|
|
807
|
+
}
|
|
808
|
+
})()`, {}, signal).catch(() => {});
|
|
809
|
+
protectedCredentialFill = null;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
case COMMANDS.CANCEL_CREDENTIAL: {
|
|
813
|
+
const fill = protectedCredentialFill;
|
|
814
|
+
if (!fill || fill.id !== String(payload.protectedFillId || '')) {
|
|
815
|
+
throw new Error('Protected credential fill is missing or expired.');
|
|
816
|
+
}
|
|
817
|
+
await evalJs(`(() => {
|
|
818
|
+
for (const selector of [${jsString(fill.usernameSelector)}, ${jsString(fill.passwordSelector)}].filter(Boolean)) {
|
|
819
|
+
const el = document.querySelector(selector);
|
|
820
|
+
if (el && 'value' in el) el.value = '';
|
|
821
|
+
}
|
|
822
|
+
})()`, {}, signal).catch(() => {});
|
|
823
|
+
protectedCredentialFill = null;
|
|
824
|
+
return { success: true, protected: false };
|
|
825
|
+
}
|
|
298
826
|
case COMMANDS.PRESS_KEY:
|
|
299
|
-
await typeKey(payload.key);
|
|
300
|
-
|
|
827
|
+
await typeKey(payload.key, signal);
|
|
828
|
+
throwIfAborted(signal);
|
|
829
|
+
return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
|
|
301
830
|
case COMMANDS.SCROLL:
|
|
302
|
-
await evalJs(
|
|
303
|
-
|
|
304
|
-
|
|
831
|
+
await evalJs(
|
|
832
|
+
`window.scrollBy(${Math.round(Number(payload.deltaX) || 0)}, ${Math.round(Number(payload.deltaY) || 0)})`,
|
|
833
|
+
{},
|
|
834
|
+
signal,
|
|
835
|
+
);
|
|
836
|
+
await delay(250, signal);
|
|
837
|
+
return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
|
|
305
838
|
case COMMANDS.EXTRACT: {
|
|
306
839
|
const selector = payload.selector || 'body';
|
|
307
840
|
const attribute = payload.attribute || '';
|
|
@@ -314,26 +847,55 @@ export function createBrowserProtocol(chromeApi) {
|
|
|
314
847
|
return el.innerText || '';
|
|
315
848
|
};
|
|
316
849
|
const els = Array.from(document.querySelectorAll(${jsString(selector)}));
|
|
317
|
-
if (${payload.all === true})
|
|
850
|
+
if (${payload.all === true}) {
|
|
851
|
+
return { results: els.slice(0, 100).map((el) => String(read(el)).slice(0, 50000)) };
|
|
852
|
+
}
|
|
318
853
|
return { result: els[0] ? String(read(els[0])).slice(0, 50000) : '' };
|
|
319
854
|
})()`;
|
|
320
|
-
return evalJs(expression);
|
|
855
|
+
return evalJs(expression, {}, signal);
|
|
321
856
|
}
|
|
322
857
|
case COMMANDS.EVALUATE: {
|
|
323
|
-
|
|
324
|
-
|
|
858
|
+
if (String(payload.script || '').length > 10000) {
|
|
859
|
+
throw new Error('script exceeds maximum length (10000)');
|
|
860
|
+
}
|
|
861
|
+
const value = await evalJs(buildIsolatedEvaluationExpression(payload.script), {}, signal);
|
|
862
|
+
throwIfAborted(signal);
|
|
863
|
+
const serialized = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
864
|
+
const maxChars = 1024 * 1024;
|
|
865
|
+
return {
|
|
866
|
+
result: String(serialized ?? '').slice(0, maxChars),
|
|
867
|
+
truncated: String(serialized ?? '').length > maxChars,
|
|
868
|
+
};
|
|
325
869
|
}
|
|
326
870
|
case COMMANDS.SCREENSHOT:
|
|
327
|
-
return { screenshotDataUrl: await screenshotDataUrl(payload), fullPage: payload.fullPage === true };
|
|
871
|
+
return { screenshotDataUrl: await screenshotDataUrl(payload, signal), fullPage: payload.fullPage === true };
|
|
328
872
|
case COMMANDS.GET_PAGE_INFO: {
|
|
329
|
-
const tab = await currentTab();
|
|
330
|
-
return {
|
|
873
|
+
const tab = await currentTab(signal);
|
|
874
|
+
return {
|
|
875
|
+
url: tab.url || null,
|
|
876
|
+
title: tab.title || null,
|
|
877
|
+
protectedCredentialFill: Boolean(protectedCredentialFill),
|
|
878
|
+
};
|
|
331
879
|
}
|
|
332
880
|
case COMMANDS.CLOSE:
|
|
881
|
+
if (protectedCredentialFill) {
|
|
882
|
+
await evalJs(`(() => {
|
|
883
|
+
for (const selector of [${jsString(protectedCredentialFill.usernameSelector)}, ${jsString(protectedCredentialFill.passwordSelector)}].filter(Boolean)) {
|
|
884
|
+
const el = document.querySelector(selector);
|
|
885
|
+
if (el && 'value' in el) el.value = '';
|
|
886
|
+
}
|
|
887
|
+
})()`, {}, signal).catch(() => {});
|
|
888
|
+
}
|
|
333
889
|
if (attachedTabId != null) {
|
|
334
|
-
|
|
890
|
+
abortPausedRequests(attachedTabId, new Error('Browser control closed.'));
|
|
891
|
+
await optionalResult(
|
|
892
|
+
call(signal, 'debugger', 'detach', { tabId: attachedTabId }),
|
|
893
|
+
undefined,
|
|
894
|
+
signal,
|
|
895
|
+
);
|
|
335
896
|
}
|
|
336
897
|
attachedTabId = null;
|
|
898
|
+
protectedCredentialFill = null;
|
|
337
899
|
return { success: true };
|
|
338
900
|
default:
|
|
339
901
|
throw new Error(`Unsupported command: ${command}`);
|
|
@@ -347,6 +909,8 @@ export function createBrowserProtocol(chromeApi) {
|
|
|
347
909
|
attach,
|
|
348
910
|
send,
|
|
349
911
|
evalJs,
|
|
912
|
+
handlePausedRequest,
|
|
913
|
+
isNetworkUrlAllowed,
|
|
350
914
|
},
|
|
351
915
|
};
|
|
352
916
|
}
|