@hybridaione/hybridclaw 0.2.3 → 0.2.7
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/.github/workflows/ci.yml +70 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +95 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +57 -19
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +72 -7
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1405 -0
- package/container/src/browser-tools.ts +672 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +65 -16
- package/container/src/index.ts +463 -110
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +107 -18
- package/container/src/tools.ts +600 -225
- package/container/src/types.ts +49 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +15 -4
- package/dist/agent.js.map +1 -1
- package/dist/audit-cli.d.ts.map +1 -1
- package/dist/audit-cli.js +4 -2
- package/dist/audit-cli.js.map +1 -1
- package/dist/audit-events.d.ts.map +1 -1
- package/dist/audit-events.js +53 -3
- package/dist/audit-events.js.map +1 -1
- package/dist/audit-trail.d.ts.map +1 -1
- package/dist/audit-trail.js +17 -8
- package/dist/audit-trail.js.map +1 -1
- package/dist/channels/discord/attachments.d.ts.map +1 -1
- package/dist/channels/discord/attachments.js +14 -7
- package/dist/channels/discord/attachments.js.map +1 -1
- package/dist/channels/discord/debounce.d.ts +9 -0
- package/dist/channels/discord/debounce.d.ts.map +1 -0
- package/dist/channels/discord/debounce.js +20 -0
- package/dist/channels/discord/debounce.js.map +1 -0
- package/dist/channels/discord/delivery.d.ts +4 -1
- package/dist/channels/discord/delivery.d.ts.map +1 -1
- package/dist/channels/discord/delivery.js +24 -4
- package/dist/channels/discord/delivery.js.map +1 -1
- package/dist/channels/discord/human-delay.d.ts +16 -0
- package/dist/channels/discord/human-delay.d.ts.map +1 -0
- package/dist/channels/discord/human-delay.js +29 -0
- package/dist/channels/discord/human-delay.js.map +1 -0
- package/dist/channels/discord/inbound.d.ts +31 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +156 -6
- package/dist/channels/discord/inbound.js.map +1 -1
- package/dist/channels/discord/mentions.d.ts.map +1 -1
- package/dist/channels/discord/mentions.js +16 -4
- package/dist/channels/discord/mentions.js.map +1 -1
- package/dist/channels/discord/presence.d.ts +33 -0
- package/dist/channels/discord/presence.d.ts.map +1 -0
- package/dist/channels/discord/presence.js +111 -0
- package/dist/channels/discord/presence.js.map +1 -0
- package/dist/channels/discord/rate-limiter.d.ts +14 -0
- package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
- package/dist/channels/discord/rate-limiter.js +49 -0
- package/dist/channels/discord/rate-limiter.js.map +1 -0
- package/dist/channels/discord/reactions.d.ts +38 -0
- package/dist/channels/discord/reactions.d.ts.map +1 -0
- package/dist/channels/discord/reactions.js +151 -0
- package/dist/channels/discord/reactions.js.map +1 -0
- package/dist/channels/discord/runtime.d.ts +7 -4
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +755 -150
- package/dist/channels/discord/runtime.js.map +1 -1
- package/dist/channels/discord/stream.d.ts +4 -1
- package/dist/channels/discord/stream.d.ts.map +1 -1
- package/dist/channels/discord/stream.js +19 -12
- package/dist/channels/discord/stream.js.map +1 -1
- package/dist/channels/discord/tool-actions.d.ts.map +1 -1
- package/dist/channels/discord/tool-actions.js +24 -12
- package/dist/channels/discord/tool-actions.js.map +1 -1
- package/dist/channels/discord/typing.d.ts +15 -0
- package/dist/channels/discord/typing.d.ts.map +1 -0
- package/dist/channels/discord/typing.js +106 -0
- package/dist/channels/discord/typing.js.map +1 -0
- package/dist/chunk.d.ts.map +1 -1
- package/dist/chunk.js +4 -2
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +77 -23
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +113 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +59 -26
- package/dist/container-runner.js.map +1 -1
- package/dist/container-setup.d.ts.map +1 -1
- package/dist/container-setup.js +10 -9
- package/dist/container-setup.js.map +1 -1
- package/dist/conversation.d.ts +2 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +1 -1
- package/dist/conversation.js.map +1 -1
- package/dist/db.d.ts +118 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1568 -50
- package/dist/db.js.map +1 -1
- package/dist/delegation-manager.d.ts.map +1 -1
- package/dist/delegation-manager.js +3 -2
- package/dist/delegation-manager.js.map +1 -1
- package/dist/gateway-client.d.ts +2 -2
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +10 -4
- package/dist/gateway-client.js.map +1 -1
- package/dist/gateway-service.d.ts +3 -3
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +624 -87
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-types.d.ts +24 -0
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +303 -26
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +20 -10
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +62 -20
- package/dist/heartbeat.js.map +1 -1
- package/dist/hybridai-bots.d.ts.map +1 -1
- package/dist/hybridai-bots.js +4 -2
- package/dist/hybridai-bots.js.map +1 -1
- package/dist/hybridai-models.d.ts +8 -0
- package/dist/hybridai-models.d.ts.map +1 -0
- package/dist/hybridai-models.js +94 -0
- package/dist/hybridai-models.js.map +1 -0
- package/dist/instruction-approval-audit.d.ts.map +1 -1
- package/dist/instruction-approval-audit.js.map +1 -1
- package/dist/instruction-integrity.d.ts.map +1 -1
- package/dist/instruction-integrity.js +8 -2
- package/dist/instruction-integrity.js.map +1 -1
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +6 -1
- package/dist/ipc.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/memory-consolidation.d.ts +17 -0
- package/dist/memory-consolidation.d.ts.map +1 -0
- package/dist/memory-consolidation.js +25 -0
- package/dist/memory-consolidation.js.map +1 -0
- package/dist/memory-service.d.ts +200 -0
- package/dist/memory-service.d.ts.map +1 -0
- package/dist/memory-service.js +294 -0
- package/dist/memory-service.js.map +1 -0
- package/dist/mount-security.d.ts.map +1 -1
- package/dist/mount-security.js +31 -7
- package/dist/mount-security.js.map +1 -1
- package/dist/observability-ingest.d.ts.map +1 -1
- package/dist/observability-ingest.js +32 -11
- package/dist/observability-ingest.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +32 -9
- package/dist/onboarding.js.map +1 -1
- package/dist/proactive-policy.d.ts.map +1 -1
- package/dist/proactive-policy.js +2 -1
- package/dist/proactive-policy.js.map +1 -1
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +11 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +104 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +503 -24
- package/dist/runtime-config.js.map +1 -1
- package/dist/scheduled-task-runner.d.ts +1 -0
- package/dist/scheduled-task-runner.d.ts.map +1 -1
- package/dist/scheduled-task-runner.js +43 -10
- package/dist/scheduled-task-runner.js.map +1 -1
- package/dist/scheduler.d.ts +43 -4
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +530 -56
- package/dist/scheduler.js.map +1 -1
- package/dist/session-export.d.ts +26 -0
- package/dist/session-export.d.ts.map +1 -0
- package/dist/session-export.js +149 -0
- package/dist/session-export.js.map +1 -0
- package/dist/session-maintenance.d.ts.map +1 -1
- package/dist/session-maintenance.js +75 -13
- package/dist/session-maintenance.js.map +1 -1
- package/dist/session-transcripts.d.ts.map +1 -1
- package/dist/session-transcripts.js.map +1 -1
- package/dist/side-effects.d.ts.map +1 -1
- package/dist/side-effects.js +14 -2
- package/dist/side-effects.js.map +1 -1
- package/dist/skills-guard.d.ts.map +1 -1
- package/dist/skills-guard.js +893 -130
- package/dist/skills-guard.js.map +1 -1
- package/dist/skills.d.ts +5 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +29 -15
- package/dist/skills.js.map +1 -1
- package/dist/token-efficiency.d.ts.map +1 -1
- package/dist/token-efficiency.js.map +1 -1
- package/dist/tui.js +92 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +150 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -1
- package/dist/types.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +42 -14
- package/dist/update.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +49 -9
- package/dist/workspace.js.map +1 -1
- package/docs/chat.html +9 -3
- package/docs/index.html +72 -13
- package/package.json +8 -2
- package/src/agent.ts +31 -4
- package/src/audit-cli.ts +44 -16
- package/src/audit-events.ts +69 -5
- package/src/audit-trail.ts +41 -15
- package/src/channels/discord/attachments.ts +81 -27
- package/src/channels/discord/debounce.ts +25 -0
- package/src/channels/discord/delivery.ts +65 -14
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +190 -8
- package/src/channels/discord/mentions.ts +42 -18
- package/src/channels/discord/presence.ts +148 -0
- package/src/channels/discord/rate-limiter.ts +58 -0
- package/src/channels/discord/reactions.ts +211 -0
- package/src/channels/discord/runtime.ts +1161 -205
- package/src/channels/discord/stream.ts +85 -31
- package/src/channels/discord/tool-actions.ts +78 -37
- package/src/channels/discord/typing.ts +140 -0
- package/src/chunk.ts +12 -4
- package/src/cli.ts +174 -57
- package/src/config.ts +208 -36
- package/src/container-runner.ts +134 -42
- package/src/container-setup.ts +57 -22
- package/src/conversation.ts +9 -7
- package/src/db.ts +2217 -84
- package/src/delegation-manager.ts +6 -2
- package/src/gateway-client.ts +41 -17
- package/src/gateway-service.ts +996 -220
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +487 -52
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +99 -22
- package/src/hybridai-bots.ts +14 -5
- package/src/hybridai-models.ts +158 -0
- package/src/instruction-approval-audit.ts +4 -1
- package/src/instruction-integrity.ts +30 -9
- package/src/ipc.ts +23 -5
- package/src/logger.ts +4 -1
- package/src/memory-consolidation.ts +41 -0
- package/src/memory-service.ts +606 -0
- package/src/mount-security.ts +58 -13
- package/src/observability-ingest.ts +134 -35
- package/src/onboarding.ts +126 -35
- package/src/proactive-policy.ts +3 -1
- package/src/prompt-hooks.ts +42 -17
- package/src/runtime-config.ts +1112 -122
- package/src/scheduled-task-runner.ts +77 -11
- package/src/scheduler.ts +683 -60
- package/src/session-export.ts +196 -0
- package/src/session-maintenance.ts +125 -22
- package/src/session-transcripts.ts +12 -3
- package/src/side-effects.ts +28 -5
- package/src/skills-guard.ts +1067 -219
- package/src/skills.ts +163 -65
- package/src/token-efficiency.ts +31 -9
- package/src/tui.ts +166 -25
- package/src/types.ts +199 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +335 -0
- package/tests/discord.basic.test.ts +213 -4
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/hybridai-client.test.ts +112 -0
- package/tests/hybridai-models.test.ts +46 -0
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- package/tests/token-usage.cache.test.ts +128 -0
- package/vitest.e2e.config.ts +3 -1
- package/vitest.integration.config.ts +3 -1
- package/vitest.live.config.ts +3 -1
- package/vitest.unit.config.ts +9 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { execFile, spawnSync } from 'child_process';
|
|
2
|
-
import { createHash } from 'crypto';
|
|
3
|
-
import { lookup } from 'dns/promises';
|
|
4
|
-
import fs from 'fs';
|
|
5
|
-
import net from 'net';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import { promisify } from 'util';
|
|
1
|
+
import { execFile, spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { lookup } from 'node:dns/promises';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import net from 'node:net';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
8
|
|
|
9
9
|
import type { ToolDefinition } from './types.js';
|
|
10
10
|
|
|
@@ -13,14 +13,21 @@ const execFileAsync = promisify(execFile);
|
|
|
13
13
|
const WORKSPACE_ROOT = '/workspace';
|
|
14
14
|
const BROWSER_SOCKET_ROOT = '/tmp/hybridclaw-browser';
|
|
15
15
|
const BROWSER_ARTIFACT_ROOT = path.join(WORKSPACE_ROOT, '.browser-artifacts');
|
|
16
|
+
const DISCORD_MEDIA_CACHE_ROOT = '/discord-media-cache';
|
|
16
17
|
const BROWSER_DEFAULT_TIMEOUT_MS = 45_000;
|
|
17
18
|
const BROWSER_MAX_SNAPSHOT_CHARS = 12_000;
|
|
18
19
|
const BROWSER_RUNTIME_ROOT = path.join(WORKSPACE_ROOT, '.hybridclaw-runtime');
|
|
19
20
|
const BROWSER_TMP_HOME = path.join(BROWSER_RUNTIME_ROOT, 'home');
|
|
20
21
|
const BROWSER_NPM_CACHE = path.join(BROWSER_RUNTIME_ROOT, 'npm-cache');
|
|
21
22
|
const BROWSER_XDG_CACHE = path.join(BROWSER_RUNTIME_ROOT, 'cache');
|
|
22
|
-
const BROWSER_PLAYWRIGHT_CACHE = path.join(
|
|
23
|
-
|
|
23
|
+
const BROWSER_PLAYWRIGHT_CACHE = path.join(
|
|
24
|
+
BROWSER_RUNTIME_ROOT,
|
|
25
|
+
'ms-playwright',
|
|
26
|
+
);
|
|
27
|
+
const BROWSER_PROFILE_ROOT = path.join(
|
|
28
|
+
BROWSER_RUNTIME_ROOT,
|
|
29
|
+
'browser-profiles',
|
|
30
|
+
);
|
|
24
31
|
const ENV_FALSEY = new Set(['0', 'false', 'no', 'off']);
|
|
25
32
|
const BOT_DETECTION_PATTERNS = [
|
|
26
33
|
'access denied',
|
|
@@ -92,11 +99,55 @@ const CLEAR_NETWORK_TIMINGS_SCRIPT = `(() => {
|
|
|
92
99
|
return true;
|
|
93
100
|
})()`;
|
|
94
101
|
|
|
102
|
+
const FIND_FILE_INPUT_SELECTORS_SCRIPT = `(() => {
|
|
103
|
+
const selectors = [];
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
const esc = (value) => {
|
|
106
|
+
const text = String(value || '');
|
|
107
|
+
if (typeof CSS !== 'undefined' && CSS && typeof CSS.escape === 'function') {
|
|
108
|
+
return CSS.escape(text);
|
|
109
|
+
}
|
|
110
|
+
return text.replace(/["\\\\]/g, '\\\\$&');
|
|
111
|
+
};
|
|
112
|
+
const push = (selector) => {
|
|
113
|
+
const normalized = String(selector || '').trim();
|
|
114
|
+
if (!normalized || seen.has(normalized)) return;
|
|
115
|
+
seen.add(normalized);
|
|
116
|
+
selectors.push(normalized);
|
|
117
|
+
};
|
|
118
|
+
const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
|
|
119
|
+
for (const input of inputs) {
|
|
120
|
+
const id = input.getAttribute('id');
|
|
121
|
+
if (id) push(\`#\${esc(id)}\`);
|
|
122
|
+
|
|
123
|
+
const name = input.getAttribute('name');
|
|
124
|
+
if (name) push(\`input[type="file"][name="\${esc(name)}"]\`);
|
|
125
|
+
|
|
126
|
+
const accept = input.getAttribute('accept');
|
|
127
|
+
if (accept) push(\`input[type="file"][accept="\${esc(accept)}"]\`);
|
|
128
|
+
|
|
129
|
+
const form = input.closest('form');
|
|
130
|
+
const formId = form ? form.getAttribute('id') : null;
|
|
131
|
+
if (formId) {
|
|
132
|
+
if (name) {
|
|
133
|
+
push(\`#\${esc(formId)} input[type="file"][name="\${esc(name)}"]\`);
|
|
134
|
+
}
|
|
135
|
+
push(\`#\${esc(formId)} input[type="file"]\`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
push('input[type="file"]');
|
|
139
|
+
return selectors.slice(0, 10);
|
|
140
|
+
})()`;
|
|
141
|
+
|
|
95
142
|
type SnapshotMode = 'default' | 'interactive' | 'full';
|
|
96
143
|
type FrameTarget = {
|
|
97
144
|
raw: string;
|
|
98
145
|
isMain: boolean;
|
|
99
146
|
};
|
|
147
|
+
type UploadTarget = {
|
|
148
|
+
raw: string;
|
|
149
|
+
source: 'ref' | 'selector';
|
|
150
|
+
};
|
|
100
151
|
type BrowserModelContext = {
|
|
101
152
|
baseUrl: string;
|
|
102
153
|
apiKey: string;
|
|
@@ -134,7 +185,9 @@ export function setBrowserModelContext(
|
|
|
134
185
|
chatbotId: string,
|
|
135
186
|
): void {
|
|
136
187
|
currentBrowserModelContext = {
|
|
137
|
-
baseUrl: String(baseUrl || '')
|
|
188
|
+
baseUrl: String(baseUrl || '')
|
|
189
|
+
.trim()
|
|
190
|
+
.replace(/\/+$/, ''),
|
|
138
191
|
apiKey: String(apiKey || '').trim(),
|
|
139
192
|
model: String(model || '').trim(),
|
|
140
193
|
chatbotId: String(chatbotId || '').trim(),
|
|
@@ -178,7 +231,9 @@ function shouldPersistSessionState(): boolean {
|
|
|
178
231
|
function resolveProfileRoot(): string {
|
|
179
232
|
const configured = String(process.env.BROWSER_PROFILE_ROOT || '').trim();
|
|
180
233
|
if (!configured) return ensureWritableDir(BROWSER_PROFILE_ROOT);
|
|
181
|
-
const resolved = path.isAbsolute(configured)
|
|
234
|
+
const resolved = path.isAbsolute(configured)
|
|
235
|
+
? configured
|
|
236
|
+
: path.resolve(WORKSPACE_ROOT, configured);
|
|
182
237
|
return ensureWritableDir(resolved);
|
|
183
238
|
}
|
|
184
239
|
|
|
@@ -206,7 +261,9 @@ function resolveRunner(): BrowserRunner | null {
|
|
|
206
261
|
return cachedRunner;
|
|
207
262
|
}
|
|
208
263
|
|
|
209
|
-
const whichAgentBrowser = spawnSync('which', ['agent-browser'], {
|
|
264
|
+
const whichAgentBrowser = spawnSync('which', ['agent-browser'], {
|
|
265
|
+
encoding: 'utf-8',
|
|
266
|
+
});
|
|
210
267
|
if (whichAgentBrowser.status === 0 && whichAgentBrowser.stdout.trim()) {
|
|
211
268
|
cachedRunner = { cmd: whichAgentBrowser.stdout.trim(), prefixArgs: [] };
|
|
212
269
|
return cachedRunner;
|
|
@@ -214,7 +271,10 @@ function resolveRunner(): BrowserRunner | null {
|
|
|
214
271
|
|
|
215
272
|
const whichNpx = spawnSync('which', ['npx'], { encoding: 'utf-8' });
|
|
216
273
|
if (whichNpx.status === 0 && whichNpx.stdout.trim()) {
|
|
217
|
-
cachedRunner = {
|
|
274
|
+
cachedRunner = {
|
|
275
|
+
cmd: whichNpx.stdout.trim(),
|
|
276
|
+
prefixArgs: ['--yes', 'agent-browser'],
|
|
277
|
+
};
|
|
218
278
|
return cachedRunner;
|
|
219
279
|
}
|
|
220
280
|
|
|
@@ -238,14 +298,18 @@ function getSession(sessionId: string): BrowserSession {
|
|
|
238
298
|
let profileDir: string | undefined;
|
|
239
299
|
if (shouldPersistProfiles()) {
|
|
240
300
|
try {
|
|
241
|
-
profileDir = ensureWritableDir(
|
|
301
|
+
profileDir = ensureWritableDir(
|
|
302
|
+
path.join(resolveProfileRoot(), runtimeKey),
|
|
303
|
+
);
|
|
242
304
|
} catch {
|
|
243
305
|
// Fallback to ephemeral browser context if profile dir cannot be created.
|
|
244
306
|
profileDir = undefined;
|
|
245
307
|
}
|
|
246
308
|
}
|
|
247
309
|
|
|
248
|
-
const stateName = shouldPersistSessionState()
|
|
310
|
+
const stateName = shouldPersistSessionState()
|
|
311
|
+
? deriveStableId(sessionKey, 48)
|
|
312
|
+
: undefined;
|
|
249
313
|
|
|
250
314
|
const session: BrowserSession = {
|
|
251
315
|
sessionKey,
|
|
@@ -304,7 +368,10 @@ function removeSession(sessionId: string): void {
|
|
|
304
368
|
|
|
305
369
|
function isPrivateIpv4(ip: string): boolean {
|
|
306
370
|
const parts = ip.split('.').map((part) => Number.parseInt(part, 10));
|
|
307
|
-
if (
|
|
371
|
+
if (
|
|
372
|
+
parts.length !== 4 ||
|
|
373
|
+
parts.some((part) => Number.isNaN(part) || part < 0 || part > 255)
|
|
374
|
+
) {
|
|
308
375
|
return false;
|
|
309
376
|
}
|
|
310
377
|
const [a, b] = parts;
|
|
@@ -340,7 +407,12 @@ function isPrivateIp(ip: string): boolean {
|
|
|
340
407
|
async function isPrivateHost(hostname: string): Promise<boolean> {
|
|
341
408
|
const host = hostname.trim().toLowerCase();
|
|
342
409
|
if (!host) return true;
|
|
343
|
-
if (
|
|
410
|
+
if (
|
|
411
|
+
host === 'localhost' ||
|
|
412
|
+
host.endsWith('.localhost') ||
|
|
413
|
+
host.endsWith('.local')
|
|
414
|
+
)
|
|
415
|
+
return true;
|
|
344
416
|
if (net.isIP(host) > 0) return isPrivateIp(host);
|
|
345
417
|
try {
|
|
346
418
|
const resolved = await lookup(host, { all: true, verbatim: true });
|
|
@@ -371,7 +443,9 @@ async function assertNavigationUrl(raw: unknown): Promise<URL> {
|
|
|
371
443
|
throw new Error(`Unsupported URL protocol: ${parsed.protocol}`);
|
|
372
444
|
}
|
|
373
445
|
|
|
374
|
-
const allowPrivate =
|
|
446
|
+
const allowPrivate =
|
|
447
|
+
String(process.env.BROWSER_ALLOW_PRIVATE_NETWORK || '').toLowerCase() ===
|
|
448
|
+
'true';
|
|
375
449
|
if (!allowPrivate && (await isPrivateHost(parsed.hostname))) {
|
|
376
450
|
throw new Error(
|
|
377
451
|
`Navigation blocked by SSRF guard: private or loopback host (${parsed.hostname}). ` +
|
|
@@ -399,6 +473,85 @@ function ensureRef(raw: unknown): string {
|
|
|
399
473
|
return ref.startsWith('@') ? ref : `@${ref}`;
|
|
400
474
|
}
|
|
401
475
|
|
|
476
|
+
function resolveUploadTarget(args: Record<string, unknown>): UploadTarget {
|
|
477
|
+
const selector = String(args.selector || args.target || '').trim();
|
|
478
|
+
if (selector) return { raw: selector, source: 'selector' };
|
|
479
|
+
|
|
480
|
+
const ref = String(args.ref || '').trim();
|
|
481
|
+
if (!ref) {
|
|
482
|
+
throw new Error('ref is required (or provide selector)');
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
raw: ref.startsWith('@') ? ref : `@${ref}`,
|
|
486
|
+
source: 'ref',
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function normalizeUploadPath(rawPath: string): string | null {
|
|
491
|
+
const trimmed = rawPath.trim();
|
|
492
|
+
if (!trimmed) return null;
|
|
493
|
+
const normalizedInput = trimmed.replace(/\\/g, '/');
|
|
494
|
+
const candidate = normalizedInput.startsWith('/')
|
|
495
|
+
? path.posix.normalize(normalizedInput)
|
|
496
|
+
: path.posix.normalize(path.posix.join(WORKSPACE_ROOT, normalizedInput));
|
|
497
|
+
if (
|
|
498
|
+
!(
|
|
499
|
+
candidate === WORKSPACE_ROOT || candidate.startsWith(`${WORKSPACE_ROOT}/`)
|
|
500
|
+
) &&
|
|
501
|
+
!(
|
|
502
|
+
candidate === DISCORD_MEDIA_CACHE_ROOT ||
|
|
503
|
+
candidate.startsWith(`${DISCORD_MEDIA_CACHE_ROOT}/`)
|
|
504
|
+
)
|
|
505
|
+
) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
return candidate;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function resolveUploadPaths(args: Record<string, unknown>): string[] {
|
|
512
|
+
const candidates: string[] = [];
|
|
513
|
+
const addPath = (value: unknown): void => {
|
|
514
|
+
if (typeof value === 'string') {
|
|
515
|
+
const trimmed = value.trim();
|
|
516
|
+
if (trimmed) candidates.push(trimmed);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (Array.isArray(value)) {
|
|
520
|
+
for (const item of value) {
|
|
521
|
+
if (typeof item !== 'string') continue;
|
|
522
|
+
const trimmed = item.trim();
|
|
523
|
+
if (trimmed) candidates.push(trimmed);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
addPath(args.path);
|
|
529
|
+
addPath(args.file);
|
|
530
|
+
addPath(args.files);
|
|
531
|
+
addPath(args.paths);
|
|
532
|
+
|
|
533
|
+
const deduped: string[] = [];
|
|
534
|
+
const seen = new Set<string>();
|
|
535
|
+
for (const raw of candidates) {
|
|
536
|
+
const normalized = normalizeUploadPath(raw);
|
|
537
|
+
if (!normalized) {
|
|
538
|
+
throw new Error(
|
|
539
|
+
`invalid upload path "${raw}" (must stay within /workspace or /discord-media-cache)`,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
if (!fs.existsSync(normalized)) {
|
|
543
|
+
throw new Error(`upload file not found: ${normalized}`);
|
|
544
|
+
}
|
|
545
|
+
if (seen.has(normalized)) continue;
|
|
546
|
+
seen.add(normalized);
|
|
547
|
+
deduped.push(normalized);
|
|
548
|
+
}
|
|
549
|
+
if (deduped.length === 0) {
|
|
550
|
+
throw new Error('path is required (or provide files/paths)');
|
|
551
|
+
}
|
|
552
|
+
return deduped;
|
|
553
|
+
}
|
|
554
|
+
|
|
402
555
|
function resolveOutputPath(rawPath: unknown, extension: 'png' | 'pdf'): string {
|
|
403
556
|
fs.mkdirSync(BROWSER_ARTIFACT_ROOT, { recursive: true });
|
|
404
557
|
|
|
@@ -409,7 +562,9 @@ function resolveOutputPath(rawPath: unknown, extension: 'png' | 'pdf'): string {
|
|
|
409
562
|
}
|
|
410
563
|
|
|
411
564
|
if (path.isAbsolute(requested)) {
|
|
412
|
-
throw new Error(
|
|
565
|
+
throw new Error(
|
|
566
|
+
'Absolute output paths are not allowed. Use a relative path.',
|
|
567
|
+
);
|
|
413
568
|
}
|
|
414
569
|
const normalized = requested.replace(/\\/g, '/');
|
|
415
570
|
const clean = path.posix.normalize(normalized);
|
|
@@ -417,7 +572,9 @@ function resolveOutputPath(rawPath: unknown, extension: 'png' | 'pdf'): string {
|
|
|
417
572
|
throw new Error('Output path escapes browser artifacts directory.');
|
|
418
573
|
}
|
|
419
574
|
|
|
420
|
-
const withExt = clean.endsWith(`.${extension}`)
|
|
575
|
+
const withExt = clean.endsWith(`.${extension}`)
|
|
576
|
+
? clean
|
|
577
|
+
: `${clean}.${extension}`;
|
|
421
578
|
const resolved = path.resolve(BROWSER_ARTIFACT_ROOT, withExt);
|
|
422
579
|
const root = path.resolve(BROWSER_ARTIFACT_ROOT);
|
|
423
580
|
if (resolved !== root && !resolved.startsWith(root + path.sep)) {
|
|
@@ -430,7 +587,10 @@ function resolveOutputPath(rawPath: unknown, extension: 'png' | 'pdf'): string {
|
|
|
430
587
|
function createTempScreenshotPath(prefix: string): string {
|
|
431
588
|
fs.mkdirSync(BROWSER_ARTIFACT_ROOT, { recursive: true });
|
|
432
589
|
const nonce = Math.random().toString(36).slice(2, 10);
|
|
433
|
-
return path.join(
|
|
590
|
+
return path.join(
|
|
591
|
+
BROWSER_ARTIFACT_ROOT,
|
|
592
|
+
`${prefix}-${Date.now()}-${nonce}.png`,
|
|
593
|
+
);
|
|
434
594
|
}
|
|
435
595
|
|
|
436
596
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
@@ -441,7 +601,8 @@ function asRecord(value: unknown): Record<string, unknown> | null {
|
|
|
441
601
|
function normalizeSnapshotMode(rawMode: unknown): SnapshotMode {
|
|
442
602
|
if (rawMode == null || String(rawMode).trim() === '') return 'default';
|
|
443
603
|
const mode = String(rawMode).trim().toLowerCase();
|
|
444
|
-
if (mode === 'default' || mode === 'interactive' || mode === 'full')
|
|
604
|
+
if (mode === 'default' || mode === 'interactive' || mode === 'full')
|
|
605
|
+
return mode;
|
|
445
606
|
throw new Error('mode must be one of "default", "interactive", or "full"');
|
|
446
607
|
}
|
|
447
608
|
|
|
@@ -455,12 +616,17 @@ function parseOptionalFrame(raw: unknown): FrameTarget | null {
|
|
|
455
616
|
};
|
|
456
617
|
}
|
|
457
618
|
|
|
458
|
-
async function applyFrameTarget(
|
|
619
|
+
async function applyFrameTarget(
|
|
620
|
+
sessionId: string,
|
|
621
|
+
target: FrameTarget | null,
|
|
622
|
+
): Promise<void> {
|
|
459
623
|
if (!target) return;
|
|
460
624
|
const commandArgs = target.isMain ? ['main'] : [target.raw];
|
|
461
625
|
const frameResult = await runAgentBrowser(sessionId, 'frame', commandArgs);
|
|
462
626
|
if (!frameResult.success) {
|
|
463
|
-
throw new Error(
|
|
627
|
+
throw new Error(
|
|
628
|
+
frameResult.error || `failed to switch to frame "${target.raw}"`,
|
|
629
|
+
);
|
|
464
630
|
}
|
|
465
631
|
}
|
|
466
632
|
|
|
@@ -469,7 +635,9 @@ async function runBrowserEval(
|
|
|
469
635
|
script: string,
|
|
470
636
|
timeoutMs = 30_000,
|
|
471
637
|
): Promise<{ success: boolean; result?: unknown; error?: string }> {
|
|
472
|
-
const response = await runAgentBrowser(sessionId, 'eval', [script], {
|
|
638
|
+
const response = await runAgentBrowser(sessionId, 'eval', [script], {
|
|
639
|
+
timeoutMs,
|
|
640
|
+
});
|
|
473
641
|
if (!response.success) {
|
|
474
642
|
return { success: false, error: response.error || 'browser eval failed' };
|
|
475
643
|
}
|
|
@@ -483,7 +651,10 @@ function normalizeFrameMetadata(raw: unknown): Record<string, unknown>[] {
|
|
|
483
651
|
for (const item of raw) {
|
|
484
652
|
const entry = asRecord(item);
|
|
485
653
|
if (!entry) continue;
|
|
486
|
-
const index =
|
|
654
|
+
const index =
|
|
655
|
+
typeof entry.index === 'number' && Number.isFinite(entry.index)
|
|
656
|
+
? entry.index
|
|
657
|
+
: null;
|
|
487
658
|
const id = typeof entry.id === 'string' ? entry.id : null;
|
|
488
659
|
const name = typeof entry.name === 'string' ? entry.name : null;
|
|
489
660
|
const title = typeof entry.title === 'string' ? entry.title : null;
|
|
@@ -503,13 +674,43 @@ function normalizeImageList(raw: unknown): Record<string, unknown>[] {
|
|
|
503
674
|
const src = typeof entry.src === 'string' ? entry.src : '';
|
|
504
675
|
if (!src || src.startsWith('data:')) continue;
|
|
505
676
|
const alt = typeof entry.alt === 'string' ? entry.alt : '';
|
|
506
|
-
const width =
|
|
507
|
-
|
|
677
|
+
const width =
|
|
678
|
+
typeof entry.width === 'number' && Number.isFinite(entry.width)
|
|
679
|
+
? entry.width
|
|
680
|
+
: null;
|
|
681
|
+
const height =
|
|
682
|
+
typeof entry.height === 'number' && Number.isFinite(entry.height)
|
|
683
|
+
? entry.height
|
|
684
|
+
: null;
|
|
508
685
|
images.push({ src, alt, width, height });
|
|
509
686
|
}
|
|
510
687
|
return images;
|
|
511
688
|
}
|
|
512
689
|
|
|
690
|
+
function normalizeStringList(raw: unknown, max = 10): string[] {
|
|
691
|
+
if (!Array.isArray(raw)) return [];
|
|
692
|
+
const values: string[] = [];
|
|
693
|
+
const seen = new Set<string>();
|
|
694
|
+
for (const item of raw) {
|
|
695
|
+
if (typeof item !== 'string') continue;
|
|
696
|
+
const normalized = item.trim();
|
|
697
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
698
|
+
seen.add(normalized);
|
|
699
|
+
values.push(normalized);
|
|
700
|
+
if (values.length >= max) break;
|
|
701
|
+
}
|
|
702
|
+
return values;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function isUploadTypeMismatchError(message: string): boolean {
|
|
706
|
+
const normalized = String(message || '').toLowerCase();
|
|
707
|
+
if (!normalized) return false;
|
|
708
|
+
return (
|
|
709
|
+
normalized.includes('setinputfiles') ||
|
|
710
|
+
normalized.includes('not an htmlinputelement')
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
513
714
|
function normalizeTrackedRequests(raw: unknown): Record<string, unknown>[] {
|
|
514
715
|
if (!Array.isArray(raw)) return [];
|
|
515
716
|
const requests: Record<string, unknown>[] = [];
|
|
@@ -519,8 +720,12 @@ function normalizeTrackedRequests(raw: unknown): Record<string, unknown>[] {
|
|
|
519
720
|
const url = typeof entry.url === 'string' ? entry.url : '';
|
|
520
721
|
if (!url) continue;
|
|
521
722
|
const method = typeof entry.method === 'string' ? entry.method : null;
|
|
522
|
-
const type =
|
|
523
|
-
|
|
723
|
+
const type =
|
|
724
|
+
typeof entry.resourceType === 'string' ? entry.resourceType : null;
|
|
725
|
+
const timestamp =
|
|
726
|
+
typeof entry.timestamp === 'number' && Number.isFinite(entry.timestamp)
|
|
727
|
+
? entry.timestamp
|
|
728
|
+
: null;
|
|
524
729
|
requests.push({
|
|
525
730
|
url,
|
|
526
731
|
method,
|
|
@@ -534,7 +739,10 @@ function normalizeTrackedRequests(raw: unknown): Record<string, unknown>[] {
|
|
|
534
739
|
return requests;
|
|
535
740
|
}
|
|
536
741
|
|
|
537
|
-
function normalizePerformanceRequests(
|
|
742
|
+
function normalizePerformanceRequests(
|
|
743
|
+
raw: unknown,
|
|
744
|
+
filter?: string,
|
|
745
|
+
): Record<string, unknown>[] {
|
|
538
746
|
if (!Array.isArray(raw)) return [];
|
|
539
747
|
const loweredFilter = (filter || '').toLowerCase();
|
|
540
748
|
const requests: Record<string, unknown>[] = [];
|
|
@@ -545,10 +753,19 @@ function normalizePerformanceRequests(raw: unknown, filter?: string): Record<str
|
|
|
545
753
|
if (!url) continue;
|
|
546
754
|
if (loweredFilter && !url.toLowerCase().includes(loweredFilter)) continue;
|
|
547
755
|
const type = typeof entry.type === 'string' ? entry.type : null;
|
|
548
|
-
const duration =
|
|
756
|
+
const duration =
|
|
757
|
+
typeof entry.duration === 'number' && Number.isFinite(entry.duration)
|
|
758
|
+
? entry.duration
|
|
759
|
+
: null;
|
|
549
760
|
const transferSize =
|
|
550
|
-
typeof entry.transfer_size === 'number' &&
|
|
551
|
-
|
|
761
|
+
typeof entry.transfer_size === 'number' &&
|
|
762
|
+
Number.isFinite(entry.transfer_size)
|
|
763
|
+
? entry.transfer_size
|
|
764
|
+
: null;
|
|
765
|
+
const startTime =
|
|
766
|
+
typeof entry.start_time === 'number' && Number.isFinite(entry.start_time)
|
|
767
|
+
? entry.start_time
|
|
768
|
+
: null;
|
|
552
769
|
requests.push({
|
|
553
770
|
url,
|
|
554
771
|
method: 'GET',
|
|
@@ -563,11 +780,15 @@ function normalizePerformanceRequests(raw: unknown, filter?: string): Record<str
|
|
|
563
780
|
return requests;
|
|
564
781
|
}
|
|
565
782
|
|
|
566
|
-
function buildBotDetectionWarning(
|
|
783
|
+
function buildBotDetectionWarning(
|
|
784
|
+
titleValue: unknown,
|
|
785
|
+
): Record<string, unknown> | null {
|
|
567
786
|
const title = String(titleValue || '').trim();
|
|
568
787
|
if (!title) return null;
|
|
569
788
|
const lower = title.toLowerCase();
|
|
570
|
-
const matched = BOT_DETECTION_PATTERNS.find((pattern) =>
|
|
789
|
+
const matched = BOT_DETECTION_PATTERNS.find((pattern) =>
|
|
790
|
+
lower.includes(pattern),
|
|
791
|
+
);
|
|
571
792
|
if (!matched) return null;
|
|
572
793
|
const hintOverride = String(process.env.BROWSER_STEALTH_HINT || '').trim();
|
|
573
794
|
const hint =
|
|
@@ -611,22 +832,33 @@ function extractVisionTextContent(content: unknown): string {
|
|
|
611
832
|
return chunks.join('\n').trim();
|
|
612
833
|
}
|
|
613
834
|
|
|
614
|
-
async function callVisionModel(
|
|
835
|
+
async function callVisionModel(
|
|
836
|
+
question: string,
|
|
837
|
+
imageBase64: string,
|
|
838
|
+
): Promise<{ model: string; analysis: string }> {
|
|
615
839
|
const apiKey = currentBrowserModelContext.apiKey;
|
|
616
840
|
const baseUrl = currentBrowserModelContext.baseUrl;
|
|
617
841
|
const model = currentBrowserModelContext.model;
|
|
618
842
|
const chatbotId = currentBrowserModelContext.chatbotId;
|
|
619
843
|
if (!apiKey) {
|
|
620
|
-
throw new Error(
|
|
844
|
+
throw new Error(
|
|
845
|
+
'browser_vision is not configured: missing active request API key context.',
|
|
846
|
+
);
|
|
621
847
|
}
|
|
622
848
|
if (!baseUrl) {
|
|
623
|
-
throw new Error(
|
|
849
|
+
throw new Error(
|
|
850
|
+
'browser_vision is not configured: missing active request base URL context.',
|
|
851
|
+
);
|
|
624
852
|
}
|
|
625
853
|
if (!model) {
|
|
626
|
-
throw new Error(
|
|
854
|
+
throw new Error(
|
|
855
|
+
'browser_vision is not configured: missing active request model context.',
|
|
856
|
+
);
|
|
627
857
|
}
|
|
628
858
|
if (!chatbotId) {
|
|
629
|
-
throw new Error(
|
|
859
|
+
throw new Error(
|
|
860
|
+
'browser_vision is not configured: missing active request chatbot_id context.',
|
|
861
|
+
);
|
|
630
862
|
}
|
|
631
863
|
const endpoint = `${baseUrl}/v1/chat/completions`;
|
|
632
864
|
const payload = {
|
|
@@ -638,7 +870,10 @@ async function callVisionModel(question: string, imageBase64: string): Promise<{
|
|
|
638
870
|
role: 'user',
|
|
639
871
|
content: [
|
|
640
872
|
{ type: 'text', text: question },
|
|
641
|
-
{
|
|
873
|
+
{
|
|
874
|
+
type: 'image_url',
|
|
875
|
+
image_url: { url: `data:image/png;base64,${imageBase64}` },
|
|
876
|
+
},
|
|
642
877
|
],
|
|
643
878
|
},
|
|
644
879
|
],
|
|
@@ -655,8 +890,11 @@ async function callVisionModel(question: string, imageBase64: string): Promise<{
|
|
|
655
890
|
|
|
656
891
|
const bodyText = await response.text();
|
|
657
892
|
if (!response.ok) {
|
|
658
|
-
const details =
|
|
659
|
-
|
|
893
|
+
const details =
|
|
894
|
+
bodyText.length > 600 ? `${bodyText.slice(0, 600)}...` : bodyText;
|
|
895
|
+
throw new Error(
|
|
896
|
+
`vision API request failed (${response.status}): ${details}`,
|
|
897
|
+
);
|
|
660
898
|
}
|
|
661
899
|
|
|
662
900
|
let parsed: unknown;
|
|
@@ -697,7 +935,10 @@ async function runAgentBrowser(
|
|
|
697
935
|
};
|
|
698
936
|
}
|
|
699
937
|
|
|
700
|
-
const timeoutMs = Math.max(
|
|
938
|
+
const timeoutMs = Math.max(
|
|
939
|
+
1_000,
|
|
940
|
+
Math.min(options.timeoutMs ?? BROWSER_DEFAULT_TIMEOUT_MS, 180_000),
|
|
941
|
+
);
|
|
701
942
|
const session = getSession(sessionId);
|
|
702
943
|
const homeDir = resolveWritableHome();
|
|
703
944
|
const npmCacheDir = ensureWritableDir(BROWSER_NPM_CACHE);
|
|
@@ -742,27 +983,47 @@ async function runAgentBrowser(
|
|
|
742
983
|
return { success: true, data: {} };
|
|
743
984
|
}
|
|
744
985
|
|
|
745
|
-
let parsed:
|
|
986
|
+
let parsed: unknown;
|
|
746
987
|
try {
|
|
747
988
|
parsed = JSON.parse(output);
|
|
748
989
|
} catch {
|
|
749
990
|
return { success: true, data: { raw: output } };
|
|
750
991
|
}
|
|
751
992
|
|
|
752
|
-
if (parsed && typeof parsed === 'object' && parsed
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
993
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
994
|
+
const parsedRecord = parsed as Record<string, unknown>;
|
|
995
|
+
if (parsedRecord.success === false) {
|
|
996
|
+
return {
|
|
997
|
+
success: false,
|
|
998
|
+
error: String(parsedRecord.error || 'browser command failed'),
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
if ('data' in parsedRecord) {
|
|
1002
|
+
return { success: true, data: parsedRecord.data };
|
|
1003
|
+
}
|
|
757
1004
|
}
|
|
758
1005
|
return { success: true, data: parsed };
|
|
759
|
-
} catch (err:
|
|
760
|
-
const
|
|
761
|
-
|
|
1006
|
+
} catch (err: unknown) {
|
|
1007
|
+
const errorRecord = err as {
|
|
1008
|
+
stderr?: unknown;
|
|
1009
|
+
stdout?: unknown;
|
|
1010
|
+
code?: unknown;
|
|
1011
|
+
message?: unknown;
|
|
1012
|
+
};
|
|
1013
|
+
const stderr =
|
|
1014
|
+
typeof errorRecord.stderr === 'string' ? errorRecord.stderr.trim() : '';
|
|
1015
|
+
const stdout =
|
|
1016
|
+
typeof errorRecord.stdout === 'string' ? errorRecord.stdout.trim() : '';
|
|
762
1017
|
const timeoutHint =
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1018
|
+
errorRecord.code === 'ETIMEDOUT' ||
|
|
1019
|
+
/timed out/i.test(String(errorRecord.message || ''))
|
|
1020
|
+
? ` (timeout ${timeoutMs}ms)`
|
|
1021
|
+
: '';
|
|
1022
|
+
const msg = stderr || stdout || String(errorRecord.message || err);
|
|
1023
|
+
return {
|
|
1024
|
+
success: false,
|
|
1025
|
+
error: `browser command failed${timeoutHint}: ${msg}`,
|
|
1026
|
+
};
|
|
766
1027
|
}
|
|
767
1028
|
}
|
|
768
1029
|
|
|
@@ -774,38 +1035,63 @@ function failure(message: string): string {
|
|
|
774
1035
|
return JSON.stringify({ success: false, error: message }, null, 2);
|
|
775
1036
|
}
|
|
776
1037
|
|
|
777
|
-
export async function executeBrowserTool(
|
|
1038
|
+
export async function executeBrowserTool(
|
|
1039
|
+
name: string,
|
|
1040
|
+
args: Record<string, unknown>,
|
|
1041
|
+
sessionId: string,
|
|
1042
|
+
): Promise<string> {
|
|
778
1043
|
try {
|
|
779
1044
|
const effectiveSessionId = normalizeSessionKey(sessionId || 'default');
|
|
780
1045
|
switch (name) {
|
|
781
1046
|
case 'browser_navigate': {
|
|
782
1047
|
const parsed = await assertNavigationUrl(args.url);
|
|
783
|
-
const result = await runAgentBrowser(
|
|
784
|
-
|
|
1048
|
+
const result = await runAgentBrowser(
|
|
1049
|
+
effectiveSessionId,
|
|
1050
|
+
'open',
|
|
1051
|
+
[parsed.toString()],
|
|
1052
|
+
{ timeoutMs: 60_000 },
|
|
1053
|
+
);
|
|
1054
|
+
if (!result.success)
|
|
1055
|
+
return failure(result.error || 'navigation failed');
|
|
785
1056
|
const data = (result.data || {}) as Record<string, unknown>;
|
|
786
1057
|
const title = String(data.title || '');
|
|
787
1058
|
const botWarning = buildBotDetectionWarning(title);
|
|
788
|
-
const textEval = await runBrowserEval(
|
|
1059
|
+
const textEval = await runBrowserEval(
|
|
1060
|
+
effectiveSessionId,
|
|
1061
|
+
EXTRACT_TEXT_PREVIEW_SCRIPT,
|
|
1062
|
+
20_000,
|
|
1063
|
+
);
|
|
789
1064
|
const textData = textEval.success ? asRecord(textEval.result) : null;
|
|
790
|
-
const contentPreview =
|
|
1065
|
+
const contentPreview =
|
|
1066
|
+
typeof textData?.preview === 'string' ? textData.preview : '';
|
|
791
1067
|
const contentLength =
|
|
792
|
-
typeof textData?.text_length === 'number' &&
|
|
1068
|
+
typeof textData?.text_length === 'number' &&
|
|
1069
|
+
Number.isFinite(textData.text_length)
|
|
793
1070
|
? Math.max(0, Math.floor(textData.text_length))
|
|
794
1071
|
: 0;
|
|
795
1072
|
const contentPreviewTruncated = textData?.preview_truncated === true;
|
|
796
1073
|
const hasNoscript = textData?.has_noscript === true;
|
|
797
1074
|
const rootShell = textData?.root_shell === true;
|
|
798
|
-
const readyState =
|
|
799
|
-
|
|
1075
|
+
const readyState =
|
|
1076
|
+
typeof textData?.ready_state === 'string' ? textData.ready_state : '';
|
|
1077
|
+
const extractionHint = buildReadExtractionHint({
|
|
1078
|
+
contentLength,
|
|
1079
|
+
hasNoscript,
|
|
1080
|
+
rootShell,
|
|
1081
|
+
});
|
|
800
1082
|
// Best-effort priming so browser_network has request listeners active quickly.
|
|
801
|
-
await runAgentBrowser(effectiveSessionId, 'network', [
|
|
1083
|
+
await runAgentBrowser(effectiveSessionId, 'network', [
|
|
1084
|
+
'requests',
|
|
1085
|
+
]).catch(() => undefined);
|
|
802
1086
|
return success({
|
|
803
1087
|
url: data.url || parsed.toString(),
|
|
804
1088
|
title,
|
|
805
1089
|
session_id: effectiveSessionId,
|
|
806
1090
|
content_text_length: contentLength,
|
|
807
1091
|
...(contentPreview ? { content_preview: contentPreview } : {}),
|
|
808
|
-
...(contentPreview
|
|
1092
|
+
...(contentPreview
|
|
1093
|
+
? { content_preview_truncated: contentPreviewTruncated }
|
|
1094
|
+
: {}),
|
|
809
1095
|
...(readyState ? { ready_state: readyState } : {}),
|
|
810
1096
|
...(hasNoscript ? { has_noscript: true } : {}),
|
|
811
1097
|
...(rootShell ? { root_shell: true } : {}),
|
|
@@ -822,18 +1108,31 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
|
|
|
822
1108
|
else if (mode === 'full') commandArgs = [];
|
|
823
1109
|
else commandArgs = full ? [] : ['-i', '-c'];
|
|
824
1110
|
|
|
825
|
-
const result = await runAgentBrowser(
|
|
1111
|
+
const result = await runAgentBrowser(
|
|
1112
|
+
effectiveSessionId,
|
|
1113
|
+
'snapshot',
|
|
1114
|
+
commandArgs,
|
|
1115
|
+
{ timeoutMs: 45_000 },
|
|
1116
|
+
);
|
|
826
1117
|
if (!result.success) return failure(result.error || 'snapshot failed');
|
|
827
1118
|
const data = (result.data || {}) as Record<string, unknown>;
|
|
828
1119
|
const rawSnapshot = String(data.snapshot || '');
|
|
829
1120
|
const truncated = truncateSnapshot(rawSnapshot);
|
|
830
|
-
const frameEval = await runBrowserEval(
|
|
831
|
-
|
|
1121
|
+
const frameEval = await runBrowserEval(
|
|
1122
|
+
effectiveSessionId,
|
|
1123
|
+
EXTRACT_IFRAMES_SCRIPT,
|
|
1124
|
+
15_000,
|
|
1125
|
+
);
|
|
1126
|
+
const frames = frameEval.success
|
|
1127
|
+
? normalizeFrameMetadata(frameEval.result)
|
|
1128
|
+
: [];
|
|
832
1129
|
return success({
|
|
833
1130
|
snapshot: truncated.text,
|
|
834
1131
|
truncated: truncated.truncated,
|
|
835
1132
|
element_count:
|
|
836
|
-
data.refs && typeof data.refs === 'object'
|
|
1133
|
+
data.refs && typeof data.refs === 'object'
|
|
1134
|
+
? Object.keys(data.refs as Record<string, unknown>).length
|
|
1135
|
+
: 0,
|
|
837
1136
|
url: data.url || data.origin || '',
|
|
838
1137
|
mode,
|
|
839
1138
|
...(frames.length > 0 ? { frames, frame_count: frames.length } : {}),
|
|
@@ -844,8 +1143,11 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
|
|
|
844
1143
|
const ref = ensureRef(args.ref);
|
|
845
1144
|
const frame = parseOptionalFrame(args.frame);
|
|
846
1145
|
await applyFrameTarget(effectiveSessionId, frame);
|
|
847
|
-
const result = await runAgentBrowser(effectiveSessionId, 'click', [
|
|
848
|
-
|
|
1146
|
+
const result = await runAgentBrowser(effectiveSessionId, 'click', [
|
|
1147
|
+
ref,
|
|
1148
|
+
]);
|
|
1149
|
+
if (!result.success)
|
|
1150
|
+
return failure(result.error || `failed to click ${ref}`);
|
|
849
1151
|
return success({
|
|
850
1152
|
clicked: ref,
|
|
851
1153
|
...(frame ? { frame: frame.raw } : {}),
|
|
@@ -858,8 +1160,12 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
|
|
|
858
1160
|
if (!text) return failure('text is required');
|
|
859
1161
|
const frame = parseOptionalFrame(args.frame);
|
|
860
1162
|
await applyFrameTarget(effectiveSessionId, frame);
|
|
861
|
-
const result = await runAgentBrowser(effectiveSessionId, 'fill', [
|
|
862
|
-
|
|
1163
|
+
const result = await runAgentBrowser(effectiveSessionId, 'fill', [
|
|
1164
|
+
ref,
|
|
1165
|
+
text,
|
|
1166
|
+
]);
|
|
1167
|
+
if (!result.success)
|
|
1168
|
+
return failure(result.error || `failed to fill ${ref}`);
|
|
863
1169
|
return success({
|
|
864
1170
|
element: ref,
|
|
865
1171
|
typed_chars: text.length,
|
|
@@ -867,13 +1173,69 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
|
|
|
867
1173
|
});
|
|
868
1174
|
}
|
|
869
1175
|
|
|
1176
|
+
case 'browser_upload': {
|
|
1177
|
+
const target = resolveUploadTarget(args);
|
|
1178
|
+
const filePaths = resolveUploadPaths(args);
|
|
1179
|
+
const frame = parseOptionalFrame(args.frame);
|
|
1180
|
+
await applyFrameTarget(effectiveSessionId, frame);
|
|
1181
|
+
const result = await runAgentBrowser(effectiveSessionId, 'upload', [
|
|
1182
|
+
target.raw,
|
|
1183
|
+
...filePaths,
|
|
1184
|
+
]);
|
|
1185
|
+
if (
|
|
1186
|
+
!result.success &&
|
|
1187
|
+
target.source === 'ref' &&
|
|
1188
|
+
isUploadTypeMismatchError(result.error || '')
|
|
1189
|
+
) {
|
|
1190
|
+
const selectorEval = await runBrowserEval(
|
|
1191
|
+
effectiveSessionId,
|
|
1192
|
+
FIND_FILE_INPUT_SELECTORS_SCRIPT,
|
|
1193
|
+
15_000,
|
|
1194
|
+
);
|
|
1195
|
+
const selectors = selectorEval.success
|
|
1196
|
+
? normalizeStringList(selectorEval.result, 10)
|
|
1197
|
+
: [];
|
|
1198
|
+
for (const selector of selectors) {
|
|
1199
|
+
const retry = await runAgentBrowser(effectiveSessionId, 'upload', [
|
|
1200
|
+
selector,
|
|
1201
|
+
...filePaths,
|
|
1202
|
+
]);
|
|
1203
|
+
if (!retry.success) continue;
|
|
1204
|
+
return success({
|
|
1205
|
+
element: target.raw,
|
|
1206
|
+
selector,
|
|
1207
|
+
target: selector,
|
|
1208
|
+
uploaded_count: filePaths.length,
|
|
1209
|
+
files: filePaths,
|
|
1210
|
+
fallback_from_ref: true,
|
|
1211
|
+
...(frame ? { frame: frame.raw } : {}),
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (!result.success) {
|
|
1216
|
+
return failure(result.error || `failed to upload via ${target.raw}`);
|
|
1217
|
+
}
|
|
1218
|
+
return success({
|
|
1219
|
+
target: target.raw,
|
|
1220
|
+
...(target.source === 'ref'
|
|
1221
|
+
? { element: target.raw }
|
|
1222
|
+
: { selector: target.raw }),
|
|
1223
|
+
uploaded_count: filePaths.length,
|
|
1224
|
+
files: filePaths,
|
|
1225
|
+
...(frame ? { frame: frame.raw } : {}),
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
|
|
870
1229
|
case 'browser_press': {
|
|
871
1230
|
const key = String(args.key || '').trim();
|
|
872
1231
|
if (!key) return failure('key is required');
|
|
873
1232
|
const frame = parseOptionalFrame(args.frame);
|
|
874
1233
|
await applyFrameTarget(effectiveSessionId, frame);
|
|
875
|
-
const result = await runAgentBrowser(effectiveSessionId, 'press', [
|
|
876
|
-
|
|
1234
|
+
const result = await runAgentBrowser(effectiveSessionId, 'press', [
|
|
1235
|
+
key,
|
|
1236
|
+
]);
|
|
1237
|
+
if (!result.success)
|
|
1238
|
+
return failure(result.error || `failed to press ${key}`);
|
|
877
1239
|
return success({
|
|
878
1240
|
key,
|
|
879
1241
|
...(frame ? { frame: frame.raw } : {}),
|
|
@@ -881,20 +1243,30 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
|
|
|
881
1243
|
}
|
|
882
1244
|
|
|
883
1245
|
case 'browser_scroll': {
|
|
884
|
-
const direction = String(args.direction || '')
|
|
1246
|
+
const direction = String(args.direction || '')
|
|
1247
|
+
.trim()
|
|
1248
|
+
.toLowerCase();
|
|
885
1249
|
if (direction !== 'up' && direction !== 'down') {
|
|
886
1250
|
return failure('direction must be "up" or "down"');
|
|
887
1251
|
}
|
|
888
1252
|
const pixelsRaw = Number(args.pixels);
|
|
889
|
-
const pixels =
|
|
890
|
-
|
|
891
|
-
|
|
1253
|
+
const pixels =
|
|
1254
|
+
Number.isFinite(pixelsRaw) && pixelsRaw > 0
|
|
1255
|
+
? Math.floor(pixelsRaw)
|
|
1256
|
+
: 800;
|
|
1257
|
+
const result = await runAgentBrowser(effectiveSessionId, 'scroll', [
|
|
1258
|
+
direction,
|
|
1259
|
+
String(pixels),
|
|
1260
|
+
]);
|
|
1261
|
+
if (!result.success)
|
|
1262
|
+
return failure(result.error || `failed to scroll ${direction}`);
|
|
892
1263
|
return success({ direction, pixels });
|
|
893
1264
|
}
|
|
894
1265
|
|
|
895
1266
|
case 'browser_back': {
|
|
896
1267
|
const result = await runAgentBrowser(effectiveSessionId, 'back', []);
|
|
897
|
-
if (!result.success)
|
|
1268
|
+
if (!result.success)
|
|
1269
|
+
return failure(result.error || 'failed to navigate back');
|
|
898
1270
|
const data = (result.data || {}) as Record<string, unknown>;
|
|
899
1271
|
return success({ url: data.url || '' });
|
|
900
1272
|
}
|
|
@@ -903,8 +1275,14 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
|
|
|
903
1275
|
const outPath = resolveOutputPath(args.path, 'png');
|
|
904
1276
|
const fullPage = args.fullPage === true;
|
|
905
1277
|
const commandArgs = fullPage ? ['--full', outPath] : [outPath];
|
|
906
|
-
const result = await runAgentBrowser(
|
|
907
|
-
|
|
1278
|
+
const result = await runAgentBrowser(
|
|
1279
|
+
effectiveSessionId,
|
|
1280
|
+
'screenshot',
|
|
1281
|
+
commandArgs,
|
|
1282
|
+
{ timeoutMs: 60_000 },
|
|
1283
|
+
);
|
|
1284
|
+
if (!result.success)
|
|
1285
|
+
return failure(result.error || 'failed to capture screenshot');
|
|
908
1286
|
return success({
|
|
909
1287
|
path: path.relative(WORKSPACE_ROOT, outPath),
|
|
910
1288
|
full_page: fullPage,
|
|
@@ -913,8 +1291,14 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
|
|
|
913
1291
|
|
|
914
1292
|
case 'browser_pdf': {
|
|
915
1293
|
const outPath = resolveOutputPath(args.path, 'pdf');
|
|
916
|
-
const result = await runAgentBrowser(
|
|
917
|
-
|
|
1294
|
+
const result = await runAgentBrowser(
|
|
1295
|
+
effectiveSessionId,
|
|
1296
|
+
'pdf',
|
|
1297
|
+
[outPath],
|
|
1298
|
+
{ timeoutMs: 60_000 },
|
|
1299
|
+
);
|
|
1300
|
+
if (!result.success)
|
|
1301
|
+
return failure(result.error || 'failed to generate pdf');
|
|
918
1302
|
return success({ path: path.relative(WORKSPACE_ROOT, outPath) });
|
|
919
1303
|
}
|
|
920
1304
|
|
|
@@ -924,11 +1308,19 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
|
|
|
924
1308
|
|
|
925
1309
|
const tempPath = createTempScreenshotPath('browser-vision');
|
|
926
1310
|
try {
|
|
927
|
-
const screenshotResult = await runAgentBrowser(
|
|
928
|
-
|
|
929
|
-
|
|
1311
|
+
const screenshotResult = await runAgentBrowser(
|
|
1312
|
+
effectiveSessionId,
|
|
1313
|
+
'screenshot',
|
|
1314
|
+
[tempPath],
|
|
1315
|
+
{
|
|
1316
|
+
timeoutMs: 60_000,
|
|
1317
|
+
},
|
|
1318
|
+
);
|
|
930
1319
|
if (!screenshotResult.success) {
|
|
931
|
-
return failure(
|
|
1320
|
+
return failure(
|
|
1321
|
+
screenshotResult.error ||
|
|
1322
|
+
'failed to capture screenshot for vision analysis',
|
|
1323
|
+
);
|
|
932
1324
|
}
|
|
933
1325
|
|
|
934
1326
|
const imageBuffer = await fs.promises.readFile(tempPath);
|
|
@@ -944,8 +1336,13 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
|
|
|
944
1336
|
}
|
|
945
1337
|
|
|
946
1338
|
case 'browser_get_images': {
|
|
947
|
-
const evalResult = await runBrowserEval(
|
|
948
|
-
|
|
1339
|
+
const evalResult = await runBrowserEval(
|
|
1340
|
+
effectiveSessionId,
|
|
1341
|
+
EXTRACT_IMAGES_SCRIPT,
|
|
1342
|
+
20_000,
|
|
1343
|
+
);
|
|
1344
|
+
if (!evalResult.success)
|
|
1345
|
+
return failure(evalResult.error || 'failed to extract images');
|
|
949
1346
|
const images = normalizeImageList(evalResult.result);
|
|
950
1347
|
return success({ count: images.length, images });
|
|
951
1348
|
}
|
|
@@ -953,8 +1350,14 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
|
|
|
953
1350
|
case 'browser_console': {
|
|
954
1351
|
const clear = args.clear === true;
|
|
955
1352
|
const commandArgs = clear ? ['--clear'] : [];
|
|
956
|
-
const result = await runAgentBrowser(
|
|
957
|
-
|
|
1353
|
+
const result = await runAgentBrowser(
|
|
1354
|
+
effectiveSessionId,
|
|
1355
|
+
'console',
|
|
1356
|
+
commandArgs,
|
|
1357
|
+
{ timeoutMs: 20_000 },
|
|
1358
|
+
);
|
|
1359
|
+
if (!result.success)
|
|
1360
|
+
return failure(result.error || 'failed to read console logs');
|
|
958
1361
|
const data = asRecord(result.data) || {};
|
|
959
1362
|
if (clear) {
|
|
960
1363
|
return success({ cleared: true, count: 0, messages: [] });
|
|
@@ -967,11 +1370,22 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
|
|
|
967
1370
|
const text = typeof entry.text === 'string' ? entry.text : '';
|
|
968
1371
|
const level = typeof entry.type === 'string' ? entry.type : 'log';
|
|
969
1372
|
const timestamp =
|
|
970
|
-
typeof entry.timestamp === 'number' &&
|
|
1373
|
+
typeof entry.timestamp === 'number' &&
|
|
1374
|
+
Number.isFinite(entry.timestamp)
|
|
1375
|
+
? entry.timestamp
|
|
1376
|
+
: null;
|
|
971
1377
|
if (!text) return null;
|
|
972
1378
|
return { level, text, timestamp };
|
|
973
1379
|
})
|
|
974
|
-
.filter(
|
|
1380
|
+
.filter(
|
|
1381
|
+
(
|
|
1382
|
+
item,
|
|
1383
|
+
): item is {
|
|
1384
|
+
level: string;
|
|
1385
|
+
text: string;
|
|
1386
|
+
timestamp: number | null;
|
|
1387
|
+
} => item !== null,
|
|
1388
|
+
);
|
|
975
1389
|
return success({
|
|
976
1390
|
messages,
|
|
977
1391
|
count: messages.length,
|
|
@@ -983,39 +1397,70 @@ export async function executeBrowserTool(name: string, args: Record<string, unkn
|
|
|
983
1397
|
const clear = args.clear === true;
|
|
984
1398
|
const filter = String(args.filter || '').trim();
|
|
985
1399
|
if (clear) {
|
|
986
|
-
const clearRequestsResult = await runAgentBrowser(
|
|
987
|
-
|
|
988
|
-
|
|
1400
|
+
const clearRequestsResult = await runAgentBrowser(
|
|
1401
|
+
effectiveSessionId,
|
|
1402
|
+
'network',
|
|
1403
|
+
['requests', '--clear'],
|
|
1404
|
+
{
|
|
1405
|
+
timeoutMs: 20_000,
|
|
1406
|
+
},
|
|
1407
|
+
);
|
|
989
1408
|
if (!clearRequestsResult.success) {
|
|
990
|
-
return failure(
|
|
1409
|
+
return failure(
|
|
1410
|
+
clearRequestsResult.error ||
|
|
1411
|
+
'failed to clear network request history',
|
|
1412
|
+
);
|
|
991
1413
|
}
|
|
992
|
-
await runBrowserEval(
|
|
1414
|
+
await runBrowserEval(
|
|
1415
|
+
effectiveSessionId,
|
|
1416
|
+
CLEAR_NETWORK_TIMINGS_SCRIPT,
|
|
1417
|
+
10_000,
|
|
1418
|
+
).catch(() => undefined);
|
|
993
1419
|
return success({ cleared: true, count: 0, requests: [] });
|
|
994
1420
|
}
|
|
995
1421
|
|
|
996
1422
|
const networkArgs = ['requests'];
|
|
997
1423
|
if (filter) networkArgs.push('--filter', filter);
|
|
998
|
-
const trackedResult = await runAgentBrowser(
|
|
1424
|
+
const trackedResult = await runAgentBrowser(
|
|
1425
|
+
effectiveSessionId,
|
|
1426
|
+
'network',
|
|
1427
|
+
networkArgs,
|
|
1428
|
+
{ timeoutMs: 20_000 },
|
|
1429
|
+
);
|
|
999
1430
|
const trackedData = asRecord(trackedResult.data);
|
|
1000
|
-
const trackedRequests = trackedResult.success
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1431
|
+
const trackedRequests = trackedResult.success
|
|
1432
|
+
? normalizeTrackedRequests(trackedData?.requests)
|
|
1433
|
+
: [];
|
|
1434
|
+
|
|
1435
|
+
const timingsEval = await runBrowserEval(
|
|
1436
|
+
effectiveSessionId,
|
|
1437
|
+
NETWORK_TIMINGS_SCRIPT,
|
|
1438
|
+
20_000,
|
|
1439
|
+
);
|
|
1440
|
+
const perfRequests = timingsEval.success
|
|
1441
|
+
? normalizePerformanceRequests(timingsEval.result, filter)
|
|
1442
|
+
: [];
|
|
1004
1443
|
|
|
1005
1444
|
if (!trackedResult.success && !timingsEval.success) {
|
|
1006
|
-
return failure(
|
|
1445
|
+
return failure(
|
|
1446
|
+
trackedResult.error ||
|
|
1447
|
+
timingsEval.error ||
|
|
1448
|
+
'failed to read network requests',
|
|
1449
|
+
);
|
|
1007
1450
|
}
|
|
1008
1451
|
|
|
1009
1452
|
const dedupe = new Set<string>();
|
|
1010
|
-
const requests = [...trackedRequests, ...perfRequests].filter(
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1453
|
+
const requests = [...trackedRequests, ...perfRequests].filter(
|
|
1454
|
+
(entry) => {
|
|
1455
|
+
const url = typeof entry.url === 'string' ? entry.url : '';
|
|
1456
|
+
const method = typeof entry.method === 'string' ? entry.method : '';
|
|
1457
|
+
const type = typeof entry.type === 'string' ? entry.type : '';
|
|
1458
|
+
const key = `${method}|${type}|${url}`;
|
|
1459
|
+
if (!url || dedupe.has(key)) return false;
|
|
1460
|
+
dedupe.add(key);
|
|
1461
|
+
return true;
|
|
1462
|
+
},
|
|
1463
|
+
);
|
|
1019
1464
|
|
|
1020
1465
|
return success({
|
|
1021
1466
|
count: requests.length,
|
|
@@ -1054,7 +1499,10 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1054
1499
|
parameters: {
|
|
1055
1500
|
type: 'object',
|
|
1056
1501
|
properties: {
|
|
1057
|
-
url: {
|
|
1502
|
+
url: {
|
|
1503
|
+
type: 'string',
|
|
1504
|
+
description: 'URL to open (http:// or https://)',
|
|
1505
|
+
},
|
|
1058
1506
|
},
|
|
1059
1507
|
required: ['url'],
|
|
1060
1508
|
},
|
|
@@ -1069,7 +1517,11 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1069
1517
|
parameters: {
|
|
1070
1518
|
type: 'object',
|
|
1071
1519
|
properties: {
|
|
1072
|
-
full: {
|
|
1520
|
+
full: {
|
|
1521
|
+
type: 'boolean',
|
|
1522
|
+
description:
|
|
1523
|
+
'If true, request fuller snapshot output (default: false).',
|
|
1524
|
+
},
|
|
1073
1525
|
mode: {
|
|
1074
1526
|
type: 'string',
|
|
1075
1527
|
enum: ['default', 'interactive', 'full'],
|
|
@@ -1089,10 +1541,14 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1089
1541
|
parameters: {
|
|
1090
1542
|
type: 'object',
|
|
1091
1543
|
properties: {
|
|
1092
|
-
ref: {
|
|
1544
|
+
ref: {
|
|
1545
|
+
type: 'string',
|
|
1546
|
+
description: 'Element reference from browser_snapshot.',
|
|
1547
|
+
},
|
|
1093
1548
|
frame: {
|
|
1094
1549
|
type: 'string',
|
|
1095
|
-
description:
|
|
1550
|
+
description:
|
|
1551
|
+
'Optional frame selector. Use "main" to target the main document again.',
|
|
1096
1552
|
},
|
|
1097
1553
|
},
|
|
1098
1554
|
required: ['ref'],
|
|
@@ -1103,33 +1559,80 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1103
1559
|
type: 'function',
|
|
1104
1560
|
function: {
|
|
1105
1561
|
name: 'browser_type',
|
|
1106
|
-
description:
|
|
1562
|
+
description:
|
|
1563
|
+
'Type text into an input element by snapshot ref (clears then fills).',
|
|
1107
1564
|
parameters: {
|
|
1108
1565
|
type: 'object',
|
|
1109
1566
|
properties: {
|
|
1110
|
-
ref: {
|
|
1567
|
+
ref: {
|
|
1568
|
+
type: 'string',
|
|
1569
|
+
description: 'Element reference from browser_snapshot.',
|
|
1570
|
+
},
|
|
1111
1571
|
text: { type: 'string', description: 'Text to type.' },
|
|
1112
1572
|
frame: {
|
|
1113
1573
|
type: 'string',
|
|
1114
|
-
description:
|
|
1574
|
+
description:
|
|
1575
|
+
'Optional frame selector. Use "main" to target the main document again.',
|
|
1115
1576
|
},
|
|
1116
1577
|
},
|
|
1117
1578
|
required: ['ref', 'text'],
|
|
1118
1579
|
},
|
|
1119
1580
|
},
|
|
1120
1581
|
},
|
|
1582
|
+
{
|
|
1583
|
+
type: 'function',
|
|
1584
|
+
function: {
|
|
1585
|
+
name: 'browser_upload',
|
|
1586
|
+
description:
|
|
1587
|
+
'Upload one or more local files to a file input. Prefer a snapshot ref (for example "@e12"); if that ref points to a wrapper (like a span/button), provide selector for the underlying input[type=file].',
|
|
1588
|
+
parameters: {
|
|
1589
|
+
type: 'object',
|
|
1590
|
+
properties: {
|
|
1591
|
+
ref: {
|
|
1592
|
+
type: 'string',
|
|
1593
|
+
description:
|
|
1594
|
+
'Optional element reference from browser_snapshot (for example "@e12").',
|
|
1595
|
+
},
|
|
1596
|
+
selector: {
|
|
1597
|
+
type: 'string',
|
|
1598
|
+
description:
|
|
1599
|
+
'Optional CSS selector for the actual file input (for example input[type="file"]).',
|
|
1600
|
+
},
|
|
1601
|
+
path: {
|
|
1602
|
+
type: 'string',
|
|
1603
|
+
description:
|
|
1604
|
+
'Primary local file path to upload (relative to /workspace or absolute /discord-media-cache path).',
|
|
1605
|
+
},
|
|
1606
|
+
files: {
|
|
1607
|
+
type: 'array',
|
|
1608
|
+
items: { type: 'string' },
|
|
1609
|
+
description:
|
|
1610
|
+
'Optional additional local file paths for multi-file inputs.',
|
|
1611
|
+
},
|
|
1612
|
+
frame: {
|
|
1613
|
+
type: 'string',
|
|
1614
|
+
description:
|
|
1615
|
+
'Optional frame selector. Use "main" to target the main document again.',
|
|
1616
|
+
},
|
|
1617
|
+
},
|
|
1618
|
+
required: ['path'],
|
|
1619
|
+
},
|
|
1620
|
+
},
|
|
1621
|
+
},
|
|
1121
1622
|
{
|
|
1122
1623
|
type: 'function',
|
|
1123
1624
|
function: {
|
|
1124
1625
|
name: 'browser_press',
|
|
1125
|
-
description:
|
|
1626
|
+
description:
|
|
1627
|
+
'Press a keyboard key in the active page (Enter, Tab, Escape, etc.).',
|
|
1126
1628
|
parameters: {
|
|
1127
1629
|
type: 'object',
|
|
1128
1630
|
properties: {
|
|
1129
1631
|
key: { type: 'string', description: 'Keyboard key name.' },
|
|
1130
1632
|
frame: {
|
|
1131
1633
|
type: 'string',
|
|
1132
|
-
description:
|
|
1634
|
+
description:
|
|
1635
|
+
'Optional frame selector. Use "main" to target the main document again.',
|
|
1133
1636
|
},
|
|
1134
1637
|
},
|
|
1135
1638
|
required: ['key'],
|
|
@@ -1144,8 +1647,14 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1144
1647
|
parameters: {
|
|
1145
1648
|
type: 'object',
|
|
1146
1649
|
properties: {
|
|
1147
|
-
direction: {
|
|
1148
|
-
|
|
1650
|
+
direction: {
|
|
1651
|
+
type: 'string',
|
|
1652
|
+
description: 'Scroll direction: "up" or "down".',
|
|
1653
|
+
},
|
|
1654
|
+
pixels: {
|
|
1655
|
+
type: 'number',
|
|
1656
|
+
description: 'Optional pixel amount (default: 800).',
|
|
1657
|
+
},
|
|
1149
1658
|
},
|
|
1150
1659
|
required: ['direction'],
|
|
1151
1660
|
},
|
|
@@ -1172,8 +1681,15 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1172
1681
|
parameters: {
|
|
1173
1682
|
type: 'object',
|
|
1174
1683
|
properties: {
|
|
1175
|
-
path: {
|
|
1176
|
-
|
|
1684
|
+
path: {
|
|
1685
|
+
type: 'string',
|
|
1686
|
+
description:
|
|
1687
|
+
'Optional relative output path under .browser-artifacts.',
|
|
1688
|
+
},
|
|
1689
|
+
fullPage: {
|
|
1690
|
+
type: 'boolean',
|
|
1691
|
+
description: 'Capture full page when true.',
|
|
1692
|
+
},
|
|
1177
1693
|
},
|
|
1178
1694
|
required: [],
|
|
1179
1695
|
},
|
|
@@ -1188,7 +1704,11 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1188
1704
|
parameters: {
|
|
1189
1705
|
type: 'object',
|
|
1190
1706
|
properties: {
|
|
1191
|
-
path: {
|
|
1707
|
+
path: {
|
|
1708
|
+
type: 'string',
|
|
1709
|
+
description:
|
|
1710
|
+
'Optional relative output path under .browser-artifacts.',
|
|
1711
|
+
},
|
|
1192
1712
|
},
|
|
1193
1713
|
required: [],
|
|
1194
1714
|
},
|
|
@@ -1203,7 +1723,10 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1203
1723
|
parameters: {
|
|
1204
1724
|
type: 'object',
|
|
1205
1725
|
properties: {
|
|
1206
|
-
question: {
|
|
1726
|
+
question: {
|
|
1727
|
+
type: 'string',
|
|
1728
|
+
description: 'Question to ask about the current page screenshot.',
|
|
1729
|
+
},
|
|
1207
1730
|
},
|
|
1208
1731
|
required: ['question'],
|
|
1209
1732
|
},
|
|
@@ -1225,11 +1748,16 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1225
1748
|
type: 'function',
|
|
1226
1749
|
function: {
|
|
1227
1750
|
name: 'browser_console',
|
|
1228
|
-
description:
|
|
1751
|
+
description:
|
|
1752
|
+
'Return console messages captured from the current page; optionally clear them.',
|
|
1229
1753
|
parameters: {
|
|
1230
1754
|
type: 'object',
|
|
1231
1755
|
properties: {
|
|
1232
|
-
clear: {
|
|
1756
|
+
clear: {
|
|
1757
|
+
type: 'boolean',
|
|
1758
|
+
description:
|
|
1759
|
+
'When true, clear stored console messages before returning.',
|
|
1760
|
+
},
|
|
1233
1761
|
},
|
|
1234
1762
|
required: [],
|
|
1235
1763
|
},
|
|
@@ -1244,8 +1772,15 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1244
1772
|
parameters: {
|
|
1245
1773
|
type: 'object',
|
|
1246
1774
|
properties: {
|
|
1247
|
-
filter: {
|
|
1248
|
-
|
|
1775
|
+
filter: {
|
|
1776
|
+
type: 'string',
|
|
1777
|
+
description: 'Optional URL substring filter.',
|
|
1778
|
+
},
|
|
1779
|
+
clear: {
|
|
1780
|
+
type: 'boolean',
|
|
1781
|
+
description:
|
|
1782
|
+
'When true, clear recorded network request history first.',
|
|
1783
|
+
},
|
|
1249
1784
|
},
|
|
1250
1785
|
required: [],
|
|
1251
1786
|
},
|
|
@@ -1255,7 +1790,8 @@ export const BROWSER_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1255
1790
|
type: 'function',
|
|
1256
1791
|
function: {
|
|
1257
1792
|
name: 'browser_close',
|
|
1258
|
-
description:
|
|
1793
|
+
description:
|
|
1794
|
+
'Close the current browser session and release associated resources.',
|
|
1259
1795
|
parameters: {
|
|
1260
1796
|
type: 'object',
|
|
1261
1797
|
properties: {},
|