verybot 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of verybot might be problematic. Click here for more details.
- package/dist/brain/agent-registry.d.ts +75 -0
- package/dist/brain/agent-registry.js +124 -0
- package/dist/brain/agent.d.ts +146 -0
- package/dist/brain/agent.js +680 -0
- package/dist/brain/channel-store.d.ts +27 -0
- package/dist/brain/channel-store.js +78 -0
- package/dist/brain/compaction.d.ts +37 -0
- package/dist/brain/compaction.js +214 -0
- package/dist/brain/context.d.ts +33 -0
- package/dist/brain/context.js +77 -0
- package/dist/brain/delegation-store.d.ts +33 -0
- package/dist/brain/delegation-store.js +106 -0
- package/dist/brain/loop.d.ts +21 -0
- package/dist/brain/loop.js +161 -0
- package/dist/brain/mcp-adapter.d.ts +39 -0
- package/dist/brain/mcp-adapter.js +227 -0
- package/dist/brain/memory-extractor.d.ts +26 -0
- package/dist/brain/memory-extractor.js +82 -0
- package/dist/brain/providers.d.ts +10 -0
- package/dist/brain/providers.js +69 -0
- package/dist/brain/queue.d.ts +18 -0
- package/dist/brain/queue.js +84 -0
- package/dist/brain/run-tools.d.ts +47 -0
- package/dist/brain/run-tools.js +84 -0
- package/dist/brain/session-key.d.ts +23 -0
- package/dist/brain/session-key.js +41 -0
- package/dist/brain/session-state.d.ts +36 -0
- package/dist/brain/session-state.js +51 -0
- package/dist/brain/session-store.d.ts +50 -0
- package/dist/brain/session-store.js +207 -0
- package/dist/brain/session.d.ts +32 -0
- package/dist/brain/session.js +75 -0
- package/dist/brain/utils.d.ts +4 -0
- package/dist/brain/utils.js +26 -0
- package/dist/brain/worker-coordinator.d.ts +25 -0
- package/dist/brain/worker-coordinator.js +83 -0
- package/dist/channels/commands.d.ts +35 -0
- package/dist/channels/commands.js +65 -0
- package/dist/channels/discord/channel.d.ts +18 -0
- package/dist/channels/discord/channel.js +154 -0
- package/dist/channels/discord/markdown.d.ts +19 -0
- package/dist/channels/discord/markdown.js +62 -0
- package/dist/channels/manager.d.ts +29 -0
- package/dist/channels/manager.js +100 -0
- package/dist/channels/slack/channel.d.ts +26 -0
- package/dist/channels/slack/channel.js +207 -0
- package/dist/channels/slack/markdown.d.ts +19 -0
- package/dist/channels/slack/markdown.js +62 -0
- package/dist/channels/specs.d.ts +21 -0
- package/dist/channels/specs.js +96 -0
- package/dist/channels/telegram/channel.d.ts +18 -0
- package/dist/channels/telegram/channel.js +156 -0
- package/dist/channels/telegram/markdown.d.ts +17 -0
- package/dist/channels/telegram/markdown.js +66 -0
- package/dist/channels/types.d.ts +26 -0
- package/dist/channels/types.js +1 -0
- package/dist/channels/whatsapp/channel.d.ts +23 -0
- package/dist/channels/whatsapp/channel.js +242 -0
- package/dist/channels/whatsapp/markdown.d.ts +20 -0
- package/dist/channels/whatsapp/markdown.js +51 -0
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.js +78 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +13 -0
- package/dist/computer/browser/actions.d.ts +31 -0
- package/dist/computer/browser/actions.js +148 -0
- package/dist/computer/browser/manager.d.ts +55 -0
- package/dist/computer/browser/manager.js +496 -0
- package/dist/computer/browser/profile-badge.d.ts +13 -0
- package/dist/computer/browser/profile-badge.js +67 -0
- package/dist/computer/browser/screenshot.d.ts +5 -0
- package/dist/computer/browser/screenshot.js +21 -0
- package/dist/computer/browser/snapshot.d.ts +30 -0
- package/dist/computer/browser/snapshot.js +242 -0
- package/dist/computer/browser/tools.d.ts +5 -0
- package/dist/computer/browser/tools.js +167 -0
- package/dist/computer/desktop/adapter.d.ts +25 -0
- package/dist/computer/desktop/adapter.js +11 -0
- package/dist/computer/desktop/macos.d.ts +24 -0
- package/dist/computer/desktop/macos.js +223 -0
- package/dist/computer/desktop/tools.d.ts +25 -0
- package/dist/computer/desktop/tools.js +114 -0
- package/dist/config/agent-config.d.ts +41 -0
- package/dist/config/agent-config.js +14 -0
- package/dist/config/model-catalog.d.ts +22 -0
- package/dist/config/model-catalog.js +99 -0
- package/dist/config/store.d.ts +25 -0
- package/dist/config/store.js +143 -0
- package/dist/config.d.ts +103 -0
- package/dist/config.js +224 -0
- package/dist/control-ui/assets/index-BANXNUyt.js +143 -0
- package/dist/control-ui/assets/index-BSUFrP9R.css +1 -0
- package/dist/control-ui/assets/noto-sans-cyrillic-ext-wght-normal-DSNfmdVt.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-cyrillic-wght-normal-B2hlT84T.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-devanagari-wght-normal-Cv-Vwajv.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-greek-ext-wght-normal-12T8GTDR.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-greek-wght-normal-Ymb6dZNd.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-latin-ext-wght-normal-W1qJv59z.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-latin-wght-normal-BYSzYMf3.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-vietnamese-wght-normal-DLTJy58D.woff2 +0 -0
- package/dist/control-ui/index.html +14 -0
- package/dist/control-ui/vite.svg +1 -0
- package/dist/events.d.ts +2 -0
- package/dist/events.js +11 -0
- package/dist/gateway/broadcast.d.ts +5 -0
- package/dist/gateway/broadcast.js +33 -0
- package/dist/gateway/methods/chat.d.ts +24 -0
- package/dist/gateway/methods/chat.js +19 -0
- package/dist/gateway/methods/config.d.ts +13 -0
- package/dist/gateway/methods/config.js +14 -0
- package/dist/gateway/methods/models.d.ts +10 -0
- package/dist/gateway/methods/models.js +14 -0
- package/dist/gateway/methods/prompt-templates.d.ts +23 -0
- package/dist/gateway/methods/prompt-templates.js +82 -0
- package/dist/gateway/methods/scheduler.d.ts +62 -0
- package/dist/gateway/methods/scheduler.js +129 -0
- package/dist/gateway/methods/sessions.d.ts +26 -0
- package/dist/gateway/methods/sessions.js +54 -0
- package/dist/gateway/methods/skills.d.ts +35 -0
- package/dist/gateway/methods/skills.js +202 -0
- package/dist/gateway/methods/system.d.ts +12 -0
- package/dist/gateway/methods/system.js +39 -0
- package/dist/gateway/methods/tasks.d.ts +21 -0
- package/dist/gateway/methods/tasks.js +46 -0
- package/dist/gateway/methods/teams.d.ts +70 -0
- package/dist/gateway/methods/teams.js +374 -0
- package/dist/gateway/methods/tools.d.ts +6 -0
- package/dist/gateway/methods/tools.js +7 -0
- package/dist/gateway/methods/whatsapp.d.ts +19 -0
- package/dist/gateway/methods/whatsapp.js +35 -0
- package/dist/gateway/rpc.d.ts +38 -0
- package/dist/gateway/rpc.js +75 -0
- package/dist/gateway/server.d.ts +4 -0
- package/dist/gateway/server.js +133 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +212 -0
- package/dist/integrations/github.d.ts +7 -0
- package/dist/integrations/github.js +133 -0
- package/dist/integrations/mcp.d.ts +7 -0
- package/dist/integrations/mcp.js +106 -0
- package/dist/integrations/registry.d.ts +43 -0
- package/dist/integrations/registry.js +258 -0
- package/dist/integrations/scanner.d.ts +10 -0
- package/dist/integrations/scanner.js +122 -0
- package/dist/integrations/twitter.d.ts +10 -0
- package/dist/integrations/twitter.js +120 -0
- package/dist/integrations/types.d.ts +72 -0
- package/dist/integrations/types.js +1 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.js +104 -0
- package/dist/markdown/chunk.d.ts +9 -0
- package/dist/markdown/chunk.js +52 -0
- package/dist/markdown/ir.d.ts +37 -0
- package/dist/markdown/ir.js +529 -0
- package/dist/markdown/render.d.ts +22 -0
- package/dist/markdown/render.js +148 -0
- package/dist/markdown/table-render.d.ts +43 -0
- package/dist/markdown/table-render.js +219 -0
- package/dist/markdown/tables.d.ts +17 -0
- package/dist/markdown/tables.js +27 -0
- package/dist/memory/embedding.d.ts +16 -0
- package/dist/memory/embedding.js +66 -0
- package/dist/memory/extractor.d.ts +6 -0
- package/dist/memory/extractor.js +72 -0
- package/dist/memory/search.d.ts +15 -0
- package/dist/memory/search.js +57 -0
- package/dist/memory/store.d.ts +34 -0
- package/dist/memory/store.js +328 -0
- package/dist/memory/types.d.ts +9 -0
- package/dist/memory/types.js +2 -0
- package/dist/paths.d.ts +20 -0
- package/dist/paths.js +29 -0
- package/dist/prompt-templates/builtins.d.ts +2 -0
- package/dist/prompt-templates/builtins.js +72 -0
- package/dist/prompt-templates/store.d.ts +39 -0
- package/dist/prompt-templates/store.js +174 -0
- package/dist/prompt-templates/types.d.ts +10 -0
- package/dist/prompt-templates/types.js +1 -0
- package/dist/scheduler/connected-channels.d.ts +24 -0
- package/dist/scheduler/connected-channels.js +57 -0
- package/dist/scheduler/scheduler.d.ts +22 -0
- package/dist/scheduler/scheduler.js +132 -0
- package/dist/scheduler/store.d.ts +27 -0
- package/dist/scheduler/store.js +205 -0
- package/dist/scheduler/types.d.ts +29 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/security/command-validator.d.ts +22 -0
- package/dist/security/command-validator.js +160 -0
- package/dist/security/docker-sandbox.d.ts +48 -0
- package/dist/security/docker-sandbox.js +218 -0
- package/dist/security/env-filter.d.ts +8 -0
- package/dist/security/env-filter.js +41 -0
- package/dist/skills/loader.d.ts +33 -0
- package/dist/skills/loader.js +132 -0
- package/dist/skills/prompt.d.ts +6 -0
- package/dist/skills/prompt.js +17 -0
- package/dist/skills/read-tool.d.ts +7 -0
- package/dist/skills/read-tool.js +24 -0
- package/dist/skills/scanner.d.ts +6 -0
- package/dist/skills/scanner.js +73 -0
- package/dist/skills/types.d.ts +15 -0
- package/dist/skills/types.js +1 -0
- package/dist/tasks/store.d.ts +47 -0
- package/dist/tasks/store.js +193 -0
- package/dist/tasks/types.d.ts +75 -0
- package/dist/tasks/types.js +32 -0
- package/dist/teams/store.d.ts +78 -0
- package/dist/teams/store.js +420 -0
- package/dist/teams/types.d.ts +23 -0
- package/dist/teams/types.js +1 -0
- package/dist/tools/bash.d.ts +16 -0
- package/dist/tools/bash.js +62 -0
- package/dist/tools/channel-history.d.ts +10 -0
- package/dist/tools/channel-history.js +43 -0
- package/dist/tools/delegate.d.ts +16 -0
- package/dist/tools/delegate.js +216 -0
- package/dist/tools/fs.d.ts +4 -0
- package/dist/tools/fs.js +335 -0
- package/dist/tools/integration-toggle.d.ts +14 -0
- package/dist/tools/integration-toggle.js +47 -0
- package/dist/tools/memory.d.ts +13 -0
- package/dist/tools/memory.js +65 -0
- package/dist/tools/registry.d.ts +6 -0
- package/dist/tools/registry.js +9 -0
- package/dist/tools/schedule.d.ts +8 -0
- package/dist/tools/schedule.js +219 -0
- package/dist/tools/speak.d.ts +10 -0
- package/dist/tools/speak.js +56 -0
- package/dist/tools/tasks.d.ts +29 -0
- package/dist/tools/tasks.js +92 -0
- package/dist/tools/teams.d.ts +7 -0
- package/dist/tools/teams.js +180 -0
- package/dist/tools/web-fetch.d.ts +3 -0
- package/dist/tools/web-fetch.js +22 -0
- package/dist/tts/edge.d.ts +10 -0
- package/dist/tts/edge.js +60 -0
- package/dist/tts/speak.d.ts +12 -0
- package/dist/tts/speak.js +81 -0
- package/dist/tts/transcribe.d.ts +5 -0
- package/dist/tts/transcribe.js +40 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +22 -0
- package/package.json +90 -0
- package/verybot.js +2 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { chromium } from "playwright";
|
|
5
|
+
import { BROWSER_PROFILE_DIR, BROWSER_PROFILES_DIR } from "../../paths.js";
|
|
6
|
+
import { logger } from "../../logger.js";
|
|
7
|
+
import { buildProfileBadgeScript } from "./profile-badge.js";
|
|
8
|
+
const DEFAULT_PROFILE = "default";
|
|
9
|
+
const PROFILE_NAME_MAX_LENGTH = 50;
|
|
10
|
+
const PROFILE_NAME_PATTERN = /^[a-zA-Z0-9-]+$/;
|
|
11
|
+
/** Chromium flags that reduce headless fingerprinting. */
|
|
12
|
+
const STEALTH_ARGS = [
|
|
13
|
+
"--disable-blink-features=AutomationControlled",
|
|
14
|
+
"--disable-features=AutomationControlled",
|
|
15
|
+
"--no-first-run",
|
|
16
|
+
"--no-default-browser-check",
|
|
17
|
+
"--disable-component-update",
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Init script injected before any page JS to patch common headless tells.
|
|
21
|
+
* Patches are conditional — only applied when the browser's default state
|
|
22
|
+
* would fail a detection check (e.g. plugins only faked if empty).
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* HEADED mode — minimal, top-frame-only patches.
|
|
26
|
+
*
|
|
27
|
+
* Key insight: addInitScript runs in ALL frames including srcdoc iframes.
|
|
28
|
+
* CreepJS creates hidden srcdoc iframes and compares their state to the
|
|
29
|
+
* main page. If both have webdriver=false, it knows a stealth script is
|
|
30
|
+
* running (real extensions only inject in top frames).
|
|
31
|
+
*
|
|
32
|
+
* Strategy: patch webdriver on the PROTOTYPE so all direct checks pass
|
|
33
|
+
* (navigator.webdriver AND Navigator.prototype.webdriver both return false).
|
|
34
|
+
* Then in srcdoc/about:blank iframes — which CreepJS uses for stealth
|
|
35
|
+
* detection — override the INSTANCE back to true. This makes it look like
|
|
36
|
+
* a real browser where extensions don't inject into srcdoc frames.
|
|
37
|
+
*
|
|
38
|
+
* Speech synthesis is also patched (top-frame only) because Playwright's
|
|
39
|
+
* Chromium build doesn't ship platform voices even in headed mode.
|
|
40
|
+
*/
|
|
41
|
+
const STEALTH_HEADED_SCRIPT = `
|
|
42
|
+
// Prototype patch: makes all standard webdriver checks return false
|
|
43
|
+
Object.defineProperty(Navigator.prototype, 'webdriver', {
|
|
44
|
+
get: () => false,
|
|
45
|
+
configurable: true,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// In srcdoc/about:blank iframes, restore webdriver=true on the instance.
|
|
49
|
+
// CreepJS creates these to detect stealth; real browsers show true here
|
|
50
|
+
// since extensions don't inject into srcdoc frames.
|
|
51
|
+
if (window !== window.top) {
|
|
52
|
+
try {
|
|
53
|
+
const url = document.URL || '';
|
|
54
|
+
if (url === 'about:srcdoc' || url === 'about:blank') {
|
|
55
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
56
|
+
get: () => true,
|
|
57
|
+
configurable: true,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Top-frame-only patches
|
|
64
|
+
if (window === window.top) {
|
|
65
|
+
// Speech synthesis: Playwright's Chromium lacks platform voices
|
|
66
|
+
if (window.speechSynthesis && speechSynthesis.getVoices().length === 0) {
|
|
67
|
+
const _origGV = speechSynthesis.getVoices.bind(speechSynthesis);
|
|
68
|
+
const _fakeVoices = [
|
|
69
|
+
{ voiceURI: 'Samantha', name: 'Samantha', lang: 'en-US', localService: true, default: true },
|
|
70
|
+
{ voiceURI: 'Alex', name: 'Alex', lang: 'en-US', localService: true, default: false },
|
|
71
|
+
{ voiceURI: 'Victoria', name: 'Victoria', lang: 'en-US', localService: true, default: false },
|
|
72
|
+
{ voiceURI: 'Karen', name: 'Karen', lang: 'en-AU', localService: true, default: false },
|
|
73
|
+
{ voiceURI: 'Daniel', name: 'Daniel', lang: 'en-GB', localService: true, default: false },
|
|
74
|
+
].map(v => {
|
|
75
|
+
const sv = Object.create(SpeechSynthesisVoice.prototype);
|
|
76
|
+
for (const [k, val] of Object.entries(v))
|
|
77
|
+
Object.defineProperty(sv, k, { value: val, enumerable: true });
|
|
78
|
+
return sv;
|
|
79
|
+
});
|
|
80
|
+
speechSynthesis.getVoices = function getVoices() {
|
|
81
|
+
const real = _origGV();
|
|
82
|
+
return real.length > 0 ? real : _fakeVoices;
|
|
83
|
+
};
|
|
84
|
+
setTimeout(() => speechSynthesis.dispatchEvent(new Event('voiceschanged')), 50);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Playwright globals cleanup
|
|
88
|
+
for (const k of Object.getOwnPropertyNames(window)) {
|
|
89
|
+
if (k.startsWith('__playwright') || k.startsWith('__pw')) {
|
|
90
|
+
try { delete window[k]; } catch {}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
/**
|
|
96
|
+
* HEADLESS mode — full stealth suite. Headless Chromium is missing
|
|
97
|
+
* plugins, voices, chrome.* APIs, and leaks via webdriver, dimensions,
|
|
98
|
+
* hasFocus, etc. We patch everything here since headless detection is
|
|
99
|
+
* the bigger threat than stealth detection.
|
|
100
|
+
*/
|
|
101
|
+
const STEALTH_HEADLESS_SCRIPT = `
|
|
102
|
+
(function() {
|
|
103
|
+
// --- toString camouflage (WeakMap, no enumerable artifacts) ---
|
|
104
|
+
const _ts = Function.prototype.toString;
|
|
105
|
+
const _m = new WeakMap();
|
|
106
|
+
const _mark = (fn, nm) => { _m.set(fn, nm); };
|
|
107
|
+
const _rep = function toString() {
|
|
108
|
+
const nm = _m.get(this);
|
|
109
|
+
if (nm !== undefined) return 'function ' + nm + '() { [native code] }';
|
|
110
|
+
return _ts.call(this);
|
|
111
|
+
};
|
|
112
|
+
_m.set(_rep, 'toString');
|
|
113
|
+
Function.prototype.toString = _rep;
|
|
114
|
+
|
|
115
|
+
// 1. webdriver
|
|
116
|
+
const _wd = function webdriver() { return false; };
|
|
117
|
+
_mark(_wd, 'get webdriver');
|
|
118
|
+
Object.defineProperty(Navigator.prototype, 'webdriver', { get: _wd, configurable: true });
|
|
119
|
+
|
|
120
|
+
// 2. Plugins
|
|
121
|
+
if (navigator.plugins.length === 0) {
|
|
122
|
+
const fp = Object.create(PluginArray.prototype);
|
|
123
|
+
const entries = [
|
|
124
|
+
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 1 },
|
|
125
|
+
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '', length: 1 },
|
|
126
|
+
{ name: 'Native Client', filename: 'internal-nacl-plugin', description: '', length: 1 },
|
|
127
|
+
{ name: 'Chromium PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 1 },
|
|
128
|
+
{ name: 'Chromium PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '', length: 1 },
|
|
129
|
+
];
|
|
130
|
+
for (let i = 0; i < entries.length; i++) {
|
|
131
|
+
const p = Object.create(Plugin.prototype);
|
|
132
|
+
Object.defineProperties(p, {
|
|
133
|
+
name: { value: entries[i].name, enumerable: true },
|
|
134
|
+
filename: { value: entries[i].filename, enumerable: true },
|
|
135
|
+
description: { value: entries[i].description, enumerable: true },
|
|
136
|
+
length: { value: entries[i].length, enumerable: true },
|
|
137
|
+
});
|
|
138
|
+
fp[i] = p;
|
|
139
|
+
}
|
|
140
|
+
Object.defineProperty(fp, 'length', { value: entries.length });
|
|
141
|
+
const _pg = function plugins() { return fp; };
|
|
142
|
+
_mark(_pg, 'get plugins');
|
|
143
|
+
Object.defineProperty(navigator, 'plugins', { get: _pg });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 3. MimeTypes
|
|
147
|
+
if (navigator.mimeTypes.length === 0) {
|
|
148
|
+
const fm = Object.create(MimeTypeArray.prototype);
|
|
149
|
+
const mt = Object.create(MimeType.prototype);
|
|
150
|
+
Object.defineProperties(mt, {
|
|
151
|
+
type: { value: 'application/pdf', enumerable: true },
|
|
152
|
+
suffixes: { value: 'pdf', enumerable: true },
|
|
153
|
+
description: { value: 'Portable Document Format', enumerable: true },
|
|
154
|
+
});
|
|
155
|
+
fm[0] = mt;
|
|
156
|
+
Object.defineProperty(fm, 'length', { value: 1 });
|
|
157
|
+
const _mg = function mimeTypes() { return fm; };
|
|
158
|
+
_mark(_mg, 'get mimeTypes');
|
|
159
|
+
Object.defineProperty(navigator, 'mimeTypes', { get: _mg });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 4. Languages
|
|
163
|
+
if (!navigator.languages || navigator.languages.length === 0) {
|
|
164
|
+
const _lg = function languages() { return ['en-US', 'en']; };
|
|
165
|
+
_mark(_lg, 'get languages');
|
|
166
|
+
Object.defineProperty(navigator, 'languages', { get: _lg });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 5. Chrome objects
|
|
170
|
+
if (!window.chrome) window.chrome = {};
|
|
171
|
+
if (!window.chrome.runtime) window.chrome.runtime = { connect: () => {}, sendMessage: () => {}, id: undefined };
|
|
172
|
+
if (!window.chrome.app) window.chrome.app = {
|
|
173
|
+
isInstalled: false,
|
|
174
|
+
InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
|
|
175
|
+
RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },
|
|
176
|
+
getDetails: () => null, getIsInstalled: () => false,
|
|
177
|
+
};
|
|
178
|
+
if (!window.chrome.csi) window.chrome.csi = () => ({ onloadT: Date.now(), startE: Date.now(), pageT: performance.now(), tran: 15 });
|
|
179
|
+
if (!window.chrome.loadTimes) window.chrome.loadTimes = () => ({
|
|
180
|
+
commitLoadTime: Date.now()/1000, connectionInfo: 'h2', finishDocumentLoadTime: Date.now()/1000,
|
|
181
|
+
finishLoadTime: Date.now()/1000, firstPaintAfterLoadTime: 0, firstPaintTime: Date.now()/1000,
|
|
182
|
+
navigationType: 'Other', npnNegotiatedProtocol: 'h2', requestTime: Date.now()/1000,
|
|
183
|
+
startLoadTime: Date.now()/1000, wasAlternateProtocolAvailable: false, wasFetchedViaSpdy: true, wasNpnNegotiated: true,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// 6. Permissions
|
|
187
|
+
const oq = window.Permissions?.prototype?.query;
|
|
188
|
+
if (oq) {
|
|
189
|
+
const _pq = function query(p) { if (p.name==='notifications') return Promise.resolve({state:Notification.permission}); return oq.call(this,p); };
|
|
190
|
+
_mark(_pq, 'query');
|
|
191
|
+
window.Permissions.prototype.query = _pq;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 7. Window dimensions
|
|
195
|
+
if (window.outerWidth === window.innerWidth && window.outerHeight === window.innerHeight) {
|
|
196
|
+
Object.defineProperty(window, 'outerWidth', { get: () => window.innerWidth + 15 });
|
|
197
|
+
Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight + 85 });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 8. document.hasFocus
|
|
201
|
+
const _hf = function hasFocus() { return true; };
|
|
202
|
+
_mark(_hf, 'hasFocus');
|
|
203
|
+
Document.prototype.hasFocus = _hf;
|
|
204
|
+
|
|
205
|
+
// 9. Speech synthesis
|
|
206
|
+
if (window.speechSynthesis) {
|
|
207
|
+
const _ogv = speechSynthesis.getVoices.bind(speechSynthesis);
|
|
208
|
+
const _mv = [
|
|
209
|
+
{ voiceURI: 'Samantha', name: 'Samantha', lang: 'en-US', localService: true, default: true },
|
|
210
|
+
{ voiceURI: 'Alex', name: 'Alex', lang: 'en-US', localService: true, default: false },
|
|
211
|
+
{ voiceURI: 'Victoria', name: 'Victoria', lang: 'en-US', localService: true, default: false },
|
|
212
|
+
{ voiceURI: 'Karen', name: 'Karen', lang: 'en-AU', localService: true, default: false },
|
|
213
|
+
{ voiceURI: 'Daniel', name: 'Daniel', lang: 'en-GB', localService: true, default: false },
|
|
214
|
+
].map(v => { const sv = Object.create(SpeechSynthesisVoice.prototype); for (const [k,val] of Object.entries(v)) Object.defineProperty(sv,k,{value:val,enumerable:true}); return sv; });
|
|
215
|
+
const _gv = function getVoices() { const r = _ogv(); return r.length > 0 ? r : _mv; };
|
|
216
|
+
_mark(_gv, 'getVoices');
|
|
217
|
+
speechSynthesis.getVoices = _gv;
|
|
218
|
+
if (speechSynthesis.getVoices().length === 0) setTimeout(() => speechSynthesis.dispatchEvent(new Event('voiceschanged')), 50);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 10. Playwright globals cleanup
|
|
222
|
+
(function() {
|
|
223
|
+
const c = () => { for (const k of Object.getOwnPropertyNames(window)) if (k.startsWith('__playwright')||k.startsWith('__pw')) try{delete window[k]}catch{} };
|
|
224
|
+
c(); let n=0; const iv=setInterval(()=>{c();if(++n>60)clearInterval(iv)},50);
|
|
225
|
+
})();
|
|
226
|
+
|
|
227
|
+
// 11. CDP leak mitigation
|
|
228
|
+
if (typeof Error.captureStackTrace === 'function') {
|
|
229
|
+
const op = Error.prepareStackTrace;
|
|
230
|
+
Error.prepareStackTrace = function(e, s) {
|
|
231
|
+
const f = s.filter(fr => { const fn = fr.getFileName()||''; return !fn.includes('devtools')&&!fn.includes('__puppeteer')&&!fn.includes('__playwright'); });
|
|
232
|
+
if (op) return op(e, f);
|
|
233
|
+
return e.toString() + '\\n' + f.map(x => ' at ' + x.toString()).join('\\n');
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 12. Iframe contentWindow
|
|
238
|
+
const oid = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow');
|
|
239
|
+
if (oid?.get) {
|
|
240
|
+
const _ig = function contentWindow() { const w = oid.get.call(this); if (w&&!w.__p) { try{Object.defineProperty(w,'chrome',{value:window.chrome});w.__p=true}catch{} } return w; };
|
|
241
|
+
_mark(_ig, 'get contentWindow');
|
|
242
|
+
Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', { get: _ig });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 13. WebGL renderer — SwiftShader is a dead giveaway for headless/server.
|
|
246
|
+
// Only override when the real renderer contains "SwiftShader"; pick a
|
|
247
|
+
// platform-consistent GPU string so it doesn't clash with navigator.platform.
|
|
248
|
+
const UNMASKED_RENDERER = 0x9246; // WEBGL_debug_renderer_info
|
|
249
|
+
const UNMASKED_VENDOR = 0x9245;
|
|
250
|
+
const _isMacPlatform = /Mac/i.test(navigator.platform);
|
|
251
|
+
const _isWinPlatform = /Win/i.test(navigator.platform);
|
|
252
|
+
const _gpuRenderer = _isMacPlatform
|
|
253
|
+
? 'ANGLE (Apple, ANGLE Metal Renderer: Apple M1 Pro, Unspecified Version)'
|
|
254
|
+
: _isWinPlatform
|
|
255
|
+
? 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 Ti Direct3D11 vs_5_0 ps_5_0, D3D11)'
|
|
256
|
+
: 'ANGLE (Intel, Mesa Intel(R) UHD Graphics 620 (KBL GT2), OpenGL 4.6)';
|
|
257
|
+
const _gpuVendor = _isMacPlatform
|
|
258
|
+
? 'Google Inc. (Apple)'
|
|
259
|
+
: _isWinPlatform
|
|
260
|
+
? 'Google Inc. (NVIDIA)'
|
|
261
|
+
: 'Google Inc. (Intel)';
|
|
262
|
+
|
|
263
|
+
for (const Proto of [WebGLRenderingContext.prototype, WebGL2RenderingContext.prototype]) {
|
|
264
|
+
const _origGP = Proto.getParameter;
|
|
265
|
+
const _gp = function getParameter(pname) {
|
|
266
|
+
const orig = _origGP.call(this, pname);
|
|
267
|
+
if (pname === UNMASKED_RENDERER && typeof orig === 'string' && orig.includes('SwiftShader')) return _gpuRenderer;
|
|
268
|
+
if (pname === UNMASKED_VENDOR && typeof orig === 'string' && orig.includes('SwiftShader')) return _gpuVendor;
|
|
269
|
+
return orig;
|
|
270
|
+
};
|
|
271
|
+
_mark(_gp, 'getParameter');
|
|
272
|
+
Proto.getParameter = _gp;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 14. Video codecs — headless Chromium lacks proprietary codecs (h264, aac)
|
|
276
|
+
const CODEC_LIST = [
|
|
277
|
+
'video/mp4', 'video/mp4; codecs="avc1.42E01E"', 'video/mp4; codecs="avc1.4D401E"',
|
|
278
|
+
'video/mp4; codecs="avc1.64001E"', 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
|
|
279
|
+
'video/webm; codecs="vp8, vorbis"', 'video/webm; codecs="vp9"',
|
|
280
|
+
'audio/mp4; codecs="mp4a.40.2"', 'audio/mpeg', 'audio/ogg; codecs="vorbis"',
|
|
281
|
+
];
|
|
282
|
+
const _normCodec = (s) => s.replace(/\\s/g, '').toLowerCase();
|
|
283
|
+
const _codecSet = new Set(CODEC_LIST.map(_normCodec));
|
|
284
|
+
|
|
285
|
+
const _origCanPlay = HTMLVideoElement.prototype.canPlayType;
|
|
286
|
+
const _cp = function canPlayType(type) {
|
|
287
|
+
const orig = _origCanPlay.call(this, type);
|
|
288
|
+
if (orig) return orig;
|
|
289
|
+
if (!_codecSet.has(_normCodec(type))) return '';
|
|
290
|
+
// Container-only (no codecs=) returns 'maybe'; explicit codec returns 'probably'
|
|
291
|
+
return type.includes('codecs=') ? 'probably' : 'maybe';
|
|
292
|
+
};
|
|
293
|
+
_mark(_cp, 'canPlayType');
|
|
294
|
+
HTMLVideoElement.prototype.canPlayType = _cp;
|
|
295
|
+
|
|
296
|
+
if (typeof MediaSource !== 'undefined' && MediaSource.isTypeSupported) {
|
|
297
|
+
const _origMSTS = MediaSource.isTypeSupported;
|
|
298
|
+
const _ms = function isTypeSupported(type) {
|
|
299
|
+
if (_origMSTS(type)) return true;
|
|
300
|
+
return _codecSet.has(_normCodec(type));
|
|
301
|
+
};
|
|
302
|
+
_mark(_ms, 'isTypeSupported');
|
|
303
|
+
MediaSource.isTypeSupported = _ms;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 15. Screen dimensions — make screen larger than viewport (real browsers
|
|
307
|
+
// have OS chrome/taskbar). Using dynamic getters so innerWidth changes
|
|
308
|
+
// (e.g. resize) don't create a mismatch.
|
|
309
|
+
const WINDOW_FRAME_W = 16; // typical OS window border width
|
|
310
|
+
const TITLEBAR_H = 80; // titlebar + address bar
|
|
311
|
+
const TASKBAR_H = 40; // OS taskbar
|
|
312
|
+
Object.defineProperties(screen, {
|
|
313
|
+
width: { get: () => (window.innerWidth || 1920) + WINDOW_FRAME_W, configurable: true },
|
|
314
|
+
height: { get: () => (window.innerHeight || 1080) + TITLEBAR_H + TASKBAR_H, configurable: true },
|
|
315
|
+
availWidth: { get: () => (window.innerWidth || 1920) + WINDOW_FRAME_W, configurable: true },
|
|
316
|
+
availHeight:{ get: () => (window.innerHeight || 1080) + TITLEBAR_H, configurable: true },
|
|
317
|
+
colorDepth: { get: () => 24, configurable: true },
|
|
318
|
+
pixelDepth: { get: () => 24, configurable: true },
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
})();
|
|
322
|
+
`;
|
|
323
|
+
/** Validate a profile name. Throws on invalid input. */
|
|
324
|
+
export function validateProfileName(name) {
|
|
325
|
+
if (!name || name.length > PROFILE_NAME_MAX_LENGTH) {
|
|
326
|
+
throw new Error(`Profile name must be 1-${PROFILE_NAME_MAX_LENGTH} characters. Got: "${name}"`);
|
|
327
|
+
}
|
|
328
|
+
if (!PROFILE_NAME_PATTERN.test(name)) {
|
|
329
|
+
throw new Error(`Profile name may only contain letters, digits, and hyphens. Got: "${name}"`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
export class BrowserManager {
|
|
333
|
+
context = null;
|
|
334
|
+
config;
|
|
335
|
+
roleRefs = {};
|
|
336
|
+
activeProfile;
|
|
337
|
+
constructor(config) {
|
|
338
|
+
this.config = config;
|
|
339
|
+
const profile = config.profile ?? DEFAULT_PROFILE;
|
|
340
|
+
if (profile !== DEFAULT_PROFILE)
|
|
341
|
+
validateProfileName(profile);
|
|
342
|
+
this.activeProfile = profile;
|
|
343
|
+
}
|
|
344
|
+
async launch() {
|
|
345
|
+
if (this.context) {
|
|
346
|
+
const pages = this.context.pages();
|
|
347
|
+
return pages[pages.length - 1] ?? (await this.context.newPage());
|
|
348
|
+
}
|
|
349
|
+
const isTemp = this.config.profileDir === "temp";
|
|
350
|
+
const profileDir = isTemp
|
|
351
|
+
? mkdtempSync(join(tmpdir(), "verybot-browser-"))
|
|
352
|
+
: this.resolveProfileDir();
|
|
353
|
+
const headless = this.config.headless ?? true;
|
|
354
|
+
const customUA = this.config.userAgent;
|
|
355
|
+
// Determine the User-Agent to use.
|
|
356
|
+
// If the user set a custom UA, use it directly.
|
|
357
|
+
// Otherwise, do a probe launch to read the real browser UA and strip
|
|
358
|
+
// "HeadlessChrome" so the version always matches internal APIs.
|
|
359
|
+
let userAgent;
|
|
360
|
+
if (customUA) {
|
|
361
|
+
userAgent = customUA;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
userAgent = await this.detectCleanUA(profileDir, headless);
|
|
365
|
+
}
|
|
366
|
+
// Retina DPR on macOS, realistic 14" MacBook resolution
|
|
367
|
+
const isMac = process.platform === "darwin";
|
|
368
|
+
const deviceScaleFactor = isMac ? 2 : 1;
|
|
369
|
+
const viewport = { width: isMac ? 1512 : 1920, height: isMac ? 982 : 1080 };
|
|
370
|
+
this.context = await chromium.launchPersistentContext(profileDir, {
|
|
371
|
+
headless,
|
|
372
|
+
viewport,
|
|
373
|
+
deviceScaleFactor,
|
|
374
|
+
userAgent,
|
|
375
|
+
args: STEALTH_ARGS,
|
|
376
|
+
});
|
|
377
|
+
// Headed: minimal patches (only webdriver). Headless: full stealth suite.
|
|
378
|
+
await this.context.addInitScript(headless ? STEALTH_HEADLESS_SCRIPT : STEALTH_HEADED_SCRIPT);
|
|
379
|
+
// Inject profile badge on every page load (skip for temp/worker profiles)
|
|
380
|
+
if (!isTemp) {
|
|
381
|
+
await this.context.addInitScript(buildProfileBadgeScript(this.activeProfile));
|
|
382
|
+
}
|
|
383
|
+
logger.info(`Browser launched (profile: ${this.activeProfile}, ua: Chrome/${userAgent.match(/Chrome\/([\d.]+)/)?.[1] ?? "?"})`);
|
|
384
|
+
const pages = this.context.pages();
|
|
385
|
+
return pages[pages.length - 1] ?? (await this.context.newPage());
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Probe-launch the browser to read its real User-Agent, then close it.
|
|
389
|
+
* Returns a clean UA with "HeadlessChrome" replaced by "Chrome".
|
|
390
|
+
* The result is cached on the class so we only probe once per process.
|
|
391
|
+
*/
|
|
392
|
+
static _cachedCleanUA = null;
|
|
393
|
+
async detectCleanUA(profileDir, headless) {
|
|
394
|
+
if (BrowserManager._cachedCleanUA)
|
|
395
|
+
return BrowserManager._cachedCleanUA;
|
|
396
|
+
const probe = await chromium.launchPersistentContext(profileDir, {
|
|
397
|
+
headless,
|
|
398
|
+
args: ["--no-first-run"],
|
|
399
|
+
});
|
|
400
|
+
try {
|
|
401
|
+
const page = probe.pages()[0] ?? (await probe.newPage());
|
|
402
|
+
const rawUA = await page.evaluate(() => navigator.userAgent);
|
|
403
|
+
// "HeadlessChrome/145.0.xxx" → "Chrome/145.0.xxx"
|
|
404
|
+
const clean = rawUA.replace(/HeadlessChrome/g, "Chrome");
|
|
405
|
+
BrowserManager._cachedCleanUA = clean;
|
|
406
|
+
logger.info(`Detected browser UA: ${clean}`);
|
|
407
|
+
return clean;
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
await probe.close();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/** Resolve the user-data directory for the active named profile. */
|
|
414
|
+
resolveProfileDir() {
|
|
415
|
+
if (this.config.profileDir && this.config.profileDir !== "temp") {
|
|
416
|
+
return this.config.profileDir;
|
|
417
|
+
}
|
|
418
|
+
// Named profile → dedicated subdirectory under BROWSER_PROFILES_DIR
|
|
419
|
+
if (this.activeProfile !== DEFAULT_PROFILE) {
|
|
420
|
+
const dir = join(BROWSER_PROFILES_DIR, this.activeProfile);
|
|
421
|
+
mkdirSync(dir, { recursive: true });
|
|
422
|
+
return dir;
|
|
423
|
+
}
|
|
424
|
+
// Default profile → legacy shared dir
|
|
425
|
+
return BROWSER_PROFILE_DIR;
|
|
426
|
+
}
|
|
427
|
+
/** Close current browser and switch to a different named profile. */
|
|
428
|
+
async switchProfile(name) {
|
|
429
|
+
validateProfileName(name);
|
|
430
|
+
await this.close();
|
|
431
|
+
this.activeProfile = name;
|
|
432
|
+
logger.info(`Switched to browser profile: ${name}`);
|
|
433
|
+
}
|
|
434
|
+
/** Get the currently active profile name. */
|
|
435
|
+
getActiveProfile() {
|
|
436
|
+
return this.activeProfile;
|
|
437
|
+
}
|
|
438
|
+
/** Update config for next browser launch. Closes existing browser if headless mode changed. */
|
|
439
|
+
async updateConfig(config) {
|
|
440
|
+
const headlessChanged = this.context && (config.headless ?? true) !== (this.config.headless ?? true);
|
|
441
|
+
this.config = config;
|
|
442
|
+
if (headlessChanged) {
|
|
443
|
+
logger.info("Headless mode changed — closing browser so next launch uses new setting");
|
|
444
|
+
await this.close();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/** Get the active page, or null if browser is not launched. */
|
|
448
|
+
getPage() {
|
|
449
|
+
if (!this.context)
|
|
450
|
+
return null;
|
|
451
|
+
const pages = this.context.pages();
|
|
452
|
+
return pages[pages.length - 1] ?? null;
|
|
453
|
+
}
|
|
454
|
+
/** Check if the browser is currently launched. */
|
|
455
|
+
isLaunched() {
|
|
456
|
+
return this.context !== null;
|
|
457
|
+
}
|
|
458
|
+
/** Store role refs from the latest snapshot. */
|
|
459
|
+
setRoleRefs(refs) {
|
|
460
|
+
this.roleRefs = refs;
|
|
461
|
+
}
|
|
462
|
+
/** Get stored role refs from the latest snapshot. */
|
|
463
|
+
getRoleRefs() {
|
|
464
|
+
return this.roleRefs;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Resolve a ref string (e.g. "e5") to a Playwright Locator.
|
|
468
|
+
* Ported from main project's pw-session.ts:refLocator.
|
|
469
|
+
*/
|
|
470
|
+
refLocator(ref) {
|
|
471
|
+
const page = this.getPage();
|
|
472
|
+
if (!page)
|
|
473
|
+
throw new Error("Browser not launched. Use browser_navigate first.");
|
|
474
|
+
const normalized = ref.startsWith("@")
|
|
475
|
+
? ref.slice(1)
|
|
476
|
+
: ref.startsWith("ref=")
|
|
477
|
+
? ref.slice(4)
|
|
478
|
+
: ref;
|
|
479
|
+
const info = this.roleRefs[normalized];
|
|
480
|
+
if (!info) {
|
|
481
|
+
throw new Error(`Unknown ref "${normalized}". Take a new snapshot and use a ref from that snapshot.`);
|
|
482
|
+
}
|
|
483
|
+
const locator = info.name
|
|
484
|
+
? page.getByRole(info.role, { name: info.name, exact: true })
|
|
485
|
+
: page.getByRole(info.role);
|
|
486
|
+
return info.nth !== undefined ? locator.nth(info.nth) : locator;
|
|
487
|
+
}
|
|
488
|
+
async close() {
|
|
489
|
+
if (!this.context)
|
|
490
|
+
return;
|
|
491
|
+
await this.context.close();
|
|
492
|
+
this.context = null;
|
|
493
|
+
this.roleRefs = {};
|
|
494
|
+
logger.info("Browser closed");
|
|
495
|
+
}
|
|
496
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injectable JS script that renders a floating profile badge at the top of
|
|
3
|
+
* every page. Injected via `context.addInitScript()` so it runs on every
|
|
4
|
+
* navigation within the persistent browser context.
|
|
5
|
+
*
|
|
6
|
+
* The badge color is deterministic — derived from a simple hash of the profile
|
|
7
|
+
* name so each profile always gets the same hue.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Build the init-script source string for a given profile name.
|
|
11
|
+
* The returned string is plain JS (no imports) safe for `addInitScript`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildProfileBadgeScript(profileName: string): string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injectable JS script that renders a floating profile badge at the top of
|
|
3
|
+
* every page. Injected via `context.addInitScript()` so it runs on every
|
|
4
|
+
* navigation within the persistent browser context.
|
|
5
|
+
*
|
|
6
|
+
* The badge color is deterministic — derived from a simple hash of the profile
|
|
7
|
+
* name so each profile always gets the same hue.
|
|
8
|
+
*/
|
|
9
|
+
const BADGE_ID = "__verybot_profile_badge__";
|
|
10
|
+
/**
|
|
11
|
+
* Build the init-script source string for a given profile name.
|
|
12
|
+
* The returned string is plain JS (no imports) safe for `addInitScript`.
|
|
13
|
+
*/
|
|
14
|
+
export function buildProfileBadgeScript(profileName) {
|
|
15
|
+
// Defense-in-depth: reject anything outside [a-zA-Z0-9-] even if caller validated
|
|
16
|
+
if (!/^[a-zA-Z0-9-]+$/.test(profileName)) {
|
|
17
|
+
throw new Error(`Unsafe profile name for badge script: "${profileName}"`);
|
|
18
|
+
}
|
|
19
|
+
// Deterministic hue from profile name (djb2 hash → 0-360)
|
|
20
|
+
let hash = 5381;
|
|
21
|
+
for (let i = 0; i < profileName.length; i++) {
|
|
22
|
+
hash = ((hash << 5) + hash + profileName.charCodeAt(i)) >>> 0;
|
|
23
|
+
}
|
|
24
|
+
const hue = hash % 360;
|
|
25
|
+
// Also override document.title so the window is identifiable
|
|
26
|
+
const upperName = profileName.toUpperCase();
|
|
27
|
+
return `
|
|
28
|
+
(function() {
|
|
29
|
+
if (document.getElementById("${BADGE_ID}")) return;
|
|
30
|
+
|
|
31
|
+
/* --- title prefix --- */
|
|
32
|
+
var origTitle = document.title;
|
|
33
|
+
document.title = "[${upperName}] " + origTitle;
|
|
34
|
+
new MutationObserver(function() {
|
|
35
|
+
if (!document.title.startsWith("[${upperName}] ")) {
|
|
36
|
+
document.title = "[${upperName}] " + document.title;
|
|
37
|
+
}
|
|
38
|
+
}).observe(document.querySelector("title") || document.head, { childList: true, subtree: true, characterData: true });
|
|
39
|
+
|
|
40
|
+
/* --- badge bar --- */
|
|
41
|
+
var bar = document.createElement("div");
|
|
42
|
+
bar.id = "${BADGE_ID}";
|
|
43
|
+
bar.style.cssText =
|
|
44
|
+
"position:fixed;top:0;left:0;right:0;height:28px;z-index:2147483647;" +
|
|
45
|
+
"background:hsl(${hue},65%,45%);color:#fff;font:bold 13px/28px sans-serif;" +
|
|
46
|
+
"display:flex;align-items:center;padding:0 12px;box-shadow:0 1px 4px rgba(0,0,0,.25);";
|
|
47
|
+
|
|
48
|
+
var dot = document.createElement("span");
|
|
49
|
+
dot.textContent = "\\u25CF ";
|
|
50
|
+
dot.style.marginRight = "6px";
|
|
51
|
+
bar.appendChild(dot);
|
|
52
|
+
|
|
53
|
+
var label = document.createElement("span");
|
|
54
|
+
label.textContent = "${upperName}";
|
|
55
|
+
label.style.flex = "1";
|
|
56
|
+
bar.appendChild(label);
|
|
57
|
+
|
|
58
|
+
var close = document.createElement("span");
|
|
59
|
+
close.textContent = "\\u2715";
|
|
60
|
+
close.style.cssText = "cursor:pointer;padding:0 4px;font-size:15px;opacity:.8;";
|
|
61
|
+
close.addEventListener("click", function() { bar.remove(); });
|
|
62
|
+
bar.appendChild(close);
|
|
63
|
+
|
|
64
|
+
document.documentElement.appendChild(bar);
|
|
65
|
+
})();
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import sharp from "sharp";
|
|
2
|
+
import { logger } from "../../logger.js";
|
|
3
|
+
/** Anthropic API rejects base64 images over 5 MB. Compress PNG -> JPEG to stay under. */
|
|
4
|
+
const MAX_IMAGE_BYTES = 3_500_000;
|
|
5
|
+
const JPEG_QUALITY_START = 80;
|
|
6
|
+
const JPEG_QUALITY_MIN = 40;
|
|
7
|
+
const JPEG_QUALITY_STEP = 10;
|
|
8
|
+
/** Compress a screenshot PNG to JPEG, reducing quality until under the size limit. */
|
|
9
|
+
export async function compressScreenshot(png) {
|
|
10
|
+
let lastJpeg = null;
|
|
11
|
+
for (let quality = JPEG_QUALITY_START; quality >= JPEG_QUALITY_MIN; quality -= JPEG_QUALITY_STEP) {
|
|
12
|
+
lastJpeg = await sharp(png).jpeg({ quality }).toBuffer();
|
|
13
|
+
if (lastJpeg.length <= MAX_IMAGE_BYTES) {
|
|
14
|
+
logger.info(`Browser: screenshot compressed ${png.length} -> ${lastJpeg.length} bytes (JPEG q${quality})`);
|
|
15
|
+
return { base64: lastJpeg.toString("base64"), mediaType: "image/jpeg" };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Last resort: use the already-computed lowest quality buffer
|
|
19
|
+
logger.info(`Browser: screenshot compressed ${png.length} -> ${lastJpeg.length} bytes (JPEG q${JPEG_QUALITY_MIN}, may exceed limit)`);
|
|
20
|
+
return { base64: lastJpeg.toString("base64"), mediaType: "image/jpeg" };
|
|
21
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses Playwright's ariaSnapshot() output,
|
|
3
|
+
* assigns refs (e1, e2…) to interactive/content elements, and returns an
|
|
4
|
+
* annotated snapshot string plus a RoleRefMap for resolving refs back to locators.
|
|
5
|
+
*/
|
|
6
|
+
export type RoleRef = {
|
|
7
|
+
role: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
/** Index used only when role+name duplicates exist. */
|
|
10
|
+
nth?: number;
|
|
11
|
+
};
|
|
12
|
+
export type RoleRefMap = Record<string, RoleRef>;
|
|
13
|
+
export type RoleSnapshotOptions = {
|
|
14
|
+
/** Only include interactive elements (buttons, links, inputs, etc.). */
|
|
15
|
+
interactive?: boolean;
|
|
16
|
+
/** Maximum depth to include (0 = root only). */
|
|
17
|
+
maxDepth?: number;
|
|
18
|
+
/** Remove unnamed structural elements and empty branches. */
|
|
19
|
+
compact?: boolean;
|
|
20
|
+
};
|
|
21
|
+
/** Validate and normalize a ref string like "e5", "@e5", or "ref=e5". Returns null if invalid. */
|
|
22
|
+
export declare function parseRoleRef(raw: string): string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Parse Playwright's `ariaSnapshot()` text, assign refs to interactive/content
|
|
25
|
+
* elements, and return the annotated snapshot + a map from ref → role info.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot: string, options?: RoleSnapshotOptions): {
|
|
28
|
+
snapshot: string;
|
|
29
|
+
refs: RoleRefMap;
|
|
30
|
+
};
|