@hera-al/browser-server 1.0.0
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/LICENSE +21 -0
- package/README.md +212 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.js +74 -0
- package/dist/core/cdp.d.ts +124 -0
- package/dist/core/cdp.helpers.d.ts +14 -0
- package/dist/core/cdp.helpers.js +148 -0
- package/dist/core/cdp.js +309 -0
- package/dist/core/chrome.d.ts +21 -0
- package/dist/core/chrome.executables.d.ts +10 -0
- package/dist/core/chrome.executables.js +559 -0
- package/dist/core/chrome.js +257 -0
- package/dist/core/chrome.profile-decoration.d.ts +11 -0
- package/dist/core/chrome.profile-decoration.js +148 -0
- package/dist/core/constants.d.ts +9 -0
- package/dist/core/constants.js +9 -0
- package/dist/core/profiles.d.ts +31 -0
- package/dist/core/profiles.js +99 -0
- package/dist/core/target-id.d.ts +12 -0
- package/dist/core/target-id.js +21 -0
- package/dist/data-dir.d.ts +2 -0
- package/dist/data-dir.js +6 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.js +125 -0
- package/dist/playwright/pw-role-snapshot.d.ts +32 -0
- package/dist/playwright/pw-role-snapshot.js +337 -0
- package/dist/playwright/pw-session.d.ts +119 -0
- package/dist/playwright/pw-session.js +530 -0
- package/dist/playwright/pw-tools-core.activity.d.ts +22 -0
- package/dist/playwright/pw-tools-core.activity.js +47 -0
- package/dist/playwright/pw-tools-core.d.ts +9 -0
- package/dist/playwright/pw-tools-core.downloads.d.ts +35 -0
- package/dist/playwright/pw-tools-core.downloads.js +186 -0
- package/dist/playwright/pw-tools-core.interactions.d.ts +104 -0
- package/dist/playwright/pw-tools-core.interactions.js +404 -0
- package/dist/playwright/pw-tools-core.js +9 -0
- package/dist/playwright/pw-tools-core.responses.d.ts +14 -0
- package/dist/playwright/pw-tools-core.responses.js +91 -0
- package/dist/playwright/pw-tools-core.shared.d.ts +7 -0
- package/dist/playwright/pw-tools-core.shared.js +50 -0
- package/dist/playwright/pw-tools-core.snapshot.d.ts +65 -0
- package/dist/playwright/pw-tools-core.snapshot.js +144 -0
- package/dist/playwright/pw-tools-core.state.d.ts +47 -0
- package/dist/playwright/pw-tools-core.state.js +154 -0
- package/dist/playwright/pw-tools-core.storage.d.ts +48 -0
- package/dist/playwright/pw-tools-core.storage.js +76 -0
- package/dist/playwright/pw-tools-core.trace.d.ts +13 -0
- package/dist/playwright/pw-tools-core.trace.js +26 -0
- package/dist/server/browser-context.d.ts +29 -0
- package/dist/server/browser-context.js +137 -0
- package/dist/server/browser-server.d.ts +7 -0
- package/dist/server/browser-server.js +49 -0
- package/dist/server/routes/act.d.ts +4 -0
- package/dist/server/routes/act.js +176 -0
- package/dist/server/routes/basic.d.ts +4 -0
- package/dist/server/routes/basic.js +36 -0
- package/dist/server/routes/index.d.ts +4 -0
- package/dist/server/routes/index.js +16 -0
- package/dist/server/routes/snapshot.d.ts +4 -0
- package/dist/server/routes/snapshot.js +143 -0
- package/dist/server/routes/storage.d.ts +4 -0
- package/dist/server/routes/storage.js +117 -0
- package/dist/server/routes/tabs.d.ts +4 -0
- package/dist/server/routes/tabs.js +51 -0
- package/dist/server/standalone.d.ts +9 -0
- package/dist/server/standalone.js +42 -0
- package/dist/utils.d.ts +18 -0
- package/dist/utils.js +58 -0
- package/package.json +66 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import WebSocket from "ws";
|
|
6
|
+
import { ensurePortAvailable } from "../utils.js";
|
|
7
|
+
import { getDataDir } from "../data-dir.js";
|
|
8
|
+
import { appendCdpPath } from "./cdp.helpers.js";
|
|
9
|
+
import { getHeadersWithAuth, normalizeCdpWsUrl } from "./cdp.js";
|
|
10
|
+
import { resolveBrowserExecutableForPlatform, } from "./chrome.executables.js";
|
|
11
|
+
import { decorateOpenClawProfile, ensureProfileCleanExit, isProfileDecorated, } from "./chrome.profile-decoration.js";
|
|
12
|
+
import { DEFAULT_OPENCLAW_BROWSER_COLOR, } from "./constants.js";
|
|
13
|
+
import { createLogger } from "../logger.js";
|
|
14
|
+
const log = createLogger("Browser:Chrome");
|
|
15
|
+
export { findChromeExecutableLinux, findChromeExecutableMac, findChromeExecutableWindows, resolveBrowserExecutableForPlatform, } from "./chrome.executables.js";
|
|
16
|
+
export { decorateOpenClawProfile, ensureProfileCleanExit, isProfileDecorated, } from "./chrome.profile-decoration.js";
|
|
17
|
+
function exists(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
return fs.existsSync(filePath);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function resolveBrowserExecutable(resolved) {
|
|
26
|
+
return resolveBrowserExecutableForPlatform(resolved, process.platform);
|
|
27
|
+
}
|
|
28
|
+
export function resolveUserDataDir(profileName = "default") {
|
|
29
|
+
return path.join(getDataDir(), "browser", profileName, "user-data");
|
|
30
|
+
}
|
|
31
|
+
function cdpUrlForPort(cdpPort) {
|
|
32
|
+
return `http://127.0.0.1:${cdpPort}`;
|
|
33
|
+
}
|
|
34
|
+
export async function isChromeReachable(cdpUrl, timeoutMs = 500) {
|
|
35
|
+
const version = await fetchChromeVersion(cdpUrl, timeoutMs);
|
|
36
|
+
return Boolean(version);
|
|
37
|
+
}
|
|
38
|
+
async function fetchChromeVersion(cdpUrl, timeoutMs = 500) {
|
|
39
|
+
const ctrl = new AbortController();
|
|
40
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
41
|
+
try {
|
|
42
|
+
const versionUrl = appendCdpPath(cdpUrl, "/json/version");
|
|
43
|
+
const res = await fetch(versionUrl, {
|
|
44
|
+
signal: ctrl.signal,
|
|
45
|
+
headers: getHeadersWithAuth(versionUrl),
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const data = (await res.json());
|
|
51
|
+
if (!data || typeof data !== "object") {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return data;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
clearTimeout(t);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500) {
|
|
64
|
+
const version = await fetchChromeVersion(cdpUrl, timeoutMs);
|
|
65
|
+
const wsUrl = String(version?.webSocketDebuggerUrl ?? "").trim();
|
|
66
|
+
if (!wsUrl) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return normalizeCdpWsUrl(wsUrl, cdpUrl);
|
|
70
|
+
}
|
|
71
|
+
async function canOpenWebSocket(wsUrl, timeoutMs = 800) {
|
|
72
|
+
return await new Promise((resolve) => {
|
|
73
|
+
const headers = getHeadersWithAuth(wsUrl);
|
|
74
|
+
const ws = new WebSocket(wsUrl, {
|
|
75
|
+
handshakeTimeout: timeoutMs,
|
|
76
|
+
...(Object.keys(headers).length ? { headers } : {}),
|
|
77
|
+
});
|
|
78
|
+
const timer = setTimeout(() => {
|
|
79
|
+
try {
|
|
80
|
+
ws.terminate();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// ignore
|
|
84
|
+
}
|
|
85
|
+
resolve(false);
|
|
86
|
+
}, Math.max(50, timeoutMs + 25));
|
|
87
|
+
ws.once("open", () => {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
try {
|
|
90
|
+
ws.close();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// ignore
|
|
94
|
+
}
|
|
95
|
+
resolve(true);
|
|
96
|
+
});
|
|
97
|
+
ws.once("error", () => {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
resolve(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export async function isChromeCdpReady(cdpUrl, timeoutMs = 500, handshakeTimeoutMs = 800) {
|
|
104
|
+
const wsUrl = await getChromeWebSocketUrl(cdpUrl, timeoutMs);
|
|
105
|
+
if (!wsUrl) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return await canOpenWebSocket(wsUrl, handshakeTimeoutMs);
|
|
109
|
+
}
|
|
110
|
+
export async function launchChrome(resolved, profile) {
|
|
111
|
+
if (!profile.cdpIsLoopback) {
|
|
112
|
+
throw new Error(`Profile "${profile.name}" is remote; cannot launch local Chrome.`);
|
|
113
|
+
}
|
|
114
|
+
await ensurePortAvailable(profile.cdpPort);
|
|
115
|
+
const exe = resolveBrowserExecutable(resolved);
|
|
116
|
+
if (!exe) {
|
|
117
|
+
throw new Error("No supported browser found (Chrome/Brave/Edge/Chromium on macOS, Linux, or Windows).");
|
|
118
|
+
}
|
|
119
|
+
const userDataDir = resolveUserDataDir(profile.name);
|
|
120
|
+
fs.mkdirSync(userDataDir, { recursive: true });
|
|
121
|
+
const needsDecorate = !isProfileDecorated(userDataDir, profile.name, (profile.color ?? DEFAULT_OPENCLAW_BROWSER_COLOR).toUpperCase());
|
|
122
|
+
const spawnOnce = () => {
|
|
123
|
+
const args = [
|
|
124
|
+
`--remote-debugging-port=${profile.cdpPort}`,
|
|
125
|
+
`--user-data-dir=${userDataDir}`,
|
|
126
|
+
"--no-first-run",
|
|
127
|
+
"--no-default-browser-check",
|
|
128
|
+
"--disable-sync",
|
|
129
|
+
"--disable-background-networking",
|
|
130
|
+
"--disable-component-update",
|
|
131
|
+
"--disable-features=Translate,MediaRouter",
|
|
132
|
+
"--disable-session-crashed-bubble",
|
|
133
|
+
"--hide-crash-restore-bubble",
|
|
134
|
+
"--password-store=basic",
|
|
135
|
+
];
|
|
136
|
+
if (resolved.headless) {
|
|
137
|
+
args.push("--headless=new");
|
|
138
|
+
args.push("--disable-gpu");
|
|
139
|
+
}
|
|
140
|
+
if (resolved.noSandbox) {
|
|
141
|
+
args.push("--no-sandbox");
|
|
142
|
+
args.push("--disable-setuid-sandbox");
|
|
143
|
+
}
|
|
144
|
+
if (process.platform === "linux") {
|
|
145
|
+
args.push("--disable-dev-shm-usage");
|
|
146
|
+
}
|
|
147
|
+
args.push("about:blank");
|
|
148
|
+
return spawn(exe.path, args, {
|
|
149
|
+
stdio: "pipe",
|
|
150
|
+
env: {
|
|
151
|
+
...process.env,
|
|
152
|
+
HOME: os.homedir(),
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
const startedAt = Date.now();
|
|
157
|
+
const localStatePath = path.join(userDataDir, "Local State");
|
|
158
|
+
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
|
|
159
|
+
const needsBootstrap = !exists(localStatePath) || !exists(preferencesPath);
|
|
160
|
+
if (needsBootstrap) {
|
|
161
|
+
const bootstrap = spawnOnce();
|
|
162
|
+
const deadline = Date.now() + 10_000;
|
|
163
|
+
while (Date.now() < deadline) {
|
|
164
|
+
if (exists(localStatePath) && exists(preferencesPath)) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
bootstrap.kill("SIGTERM");
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// ignore
|
|
174
|
+
}
|
|
175
|
+
const exitDeadline = Date.now() + 5000;
|
|
176
|
+
while (Date.now() < exitDeadline) {
|
|
177
|
+
if (bootstrap.exitCode != null) {
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (needsDecorate) {
|
|
184
|
+
try {
|
|
185
|
+
decorateOpenClawProfile(userDataDir, {
|
|
186
|
+
name: profile.name,
|
|
187
|
+
color: profile.color,
|
|
188
|
+
});
|
|
189
|
+
log.info(`Browser profile decorated (${profile.color})`);
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
log.warn(`Browser profile decoration failed: ${String(err)}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
ensureProfileCleanExit(userDataDir);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
log.warn(`Browser clean-exit prefs failed: ${String(err)}`);
|
|
200
|
+
}
|
|
201
|
+
const proc = spawnOnce();
|
|
202
|
+
const readyDeadline = Date.now() + 15_000;
|
|
203
|
+
while (Date.now() < readyDeadline) {
|
|
204
|
+
if (await isChromeReachable(profile.cdpUrl, 500)) {
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
208
|
+
}
|
|
209
|
+
if (!(await isChromeReachable(profile.cdpUrl, 500))) {
|
|
210
|
+
try {
|
|
211
|
+
proc.kill("SIGKILL");
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// ignore
|
|
215
|
+
}
|
|
216
|
+
throw new Error(`Failed to start Chrome CDP on port ${profile.cdpPort} for profile "${profile.name}".`);
|
|
217
|
+
}
|
|
218
|
+
const pid = proc.pid ?? -1;
|
|
219
|
+
log.info(`Browser started (${exe.kind}) profile "${profile.name}" on 127.0.0.1:${profile.cdpPort} (pid ${pid})`);
|
|
220
|
+
return {
|
|
221
|
+
pid,
|
|
222
|
+
exe,
|
|
223
|
+
userDataDir,
|
|
224
|
+
cdpPort: profile.cdpPort,
|
|
225
|
+
startedAt,
|
|
226
|
+
proc,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
export async function stopChrome(running, timeoutMs = 2500) {
|
|
230
|
+
const proc = running.proc;
|
|
231
|
+
if (proc.killed) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
proc.kill("SIGTERM");
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// ignore
|
|
239
|
+
}
|
|
240
|
+
const start = Date.now();
|
|
241
|
+
while (Date.now() - start < timeoutMs) {
|
|
242
|
+
if (!proc.exitCode && proc.killed) {
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
if (!(await isChromeReachable(cdpUrlForPort(running.cdpPort), 200))) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
proc.kill("SIGKILL");
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// ignore
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=chrome.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function isProfileDecorated(userDataDir: string, desiredName: string, desiredColorHex: string): boolean;
|
|
2
|
+
/**
|
|
3
|
+
* Best-effort profile decoration (name + lobster-orange). Chrome preference keys
|
|
4
|
+
* vary by version; we keep this conservative and idempotent.
|
|
5
|
+
*/
|
|
6
|
+
export declare function decorateOpenClawProfile(userDataDir: string, opts?: {
|
|
7
|
+
name?: string;
|
|
8
|
+
color?: string;
|
|
9
|
+
}): void;
|
|
10
|
+
export declare function ensureProfileCleanExit(userDataDir: string): void;
|
|
11
|
+
//# sourceMappingURL=chrome.profile-decoration.d.ts.map
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DEFAULT_OPENCLAW_BROWSER_COLOR, DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME, } from "./constants.js";
|
|
4
|
+
function decoratedMarkerPath(userDataDir) {
|
|
5
|
+
return path.join(userDataDir, ".openclaw-profile-decorated");
|
|
6
|
+
}
|
|
7
|
+
function safeReadJson(filePath) {
|
|
8
|
+
try {
|
|
9
|
+
if (!fs.existsSync(filePath)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function safeWriteJson(filePath, data) {
|
|
24
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
25
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
26
|
+
}
|
|
27
|
+
function setDeep(obj, keys, value) {
|
|
28
|
+
let node = obj;
|
|
29
|
+
for (const key of keys.slice(0, -1)) {
|
|
30
|
+
const next = node[key];
|
|
31
|
+
if (typeof next !== "object" || next === null || Array.isArray(next)) {
|
|
32
|
+
node[key] = {};
|
|
33
|
+
}
|
|
34
|
+
node = node[key];
|
|
35
|
+
}
|
|
36
|
+
node[keys[keys.length - 1] ?? ""] = value;
|
|
37
|
+
}
|
|
38
|
+
function parseHexRgbToSignedArgbInt(hex) {
|
|
39
|
+
const cleaned = hex.trim().replace(/^#/, "");
|
|
40
|
+
if (!/^[0-9a-fA-F]{6}$/.test(cleaned)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const rgb = Number.parseInt(cleaned, 16);
|
|
44
|
+
const argbUnsigned = (0xff << 24) | rgb;
|
|
45
|
+
// Chrome stores colors as signed 32-bit ints (SkColor).
|
|
46
|
+
return argbUnsigned > 0x7fffffff ? argbUnsigned - 0x1_0000_0000 : argbUnsigned;
|
|
47
|
+
}
|
|
48
|
+
export function isProfileDecorated(userDataDir, desiredName, desiredColorHex) {
|
|
49
|
+
const desiredColorInt = parseHexRgbToSignedArgbInt(desiredColorHex);
|
|
50
|
+
const localStatePath = path.join(userDataDir, "Local State");
|
|
51
|
+
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
|
|
52
|
+
const localState = safeReadJson(localStatePath);
|
|
53
|
+
const profile = localState?.profile;
|
|
54
|
+
const infoCache = typeof profile === "object" && profile !== null && !Array.isArray(profile)
|
|
55
|
+
? profile.info_cache
|
|
56
|
+
: null;
|
|
57
|
+
const info = typeof infoCache === "object" &&
|
|
58
|
+
infoCache !== null &&
|
|
59
|
+
!Array.isArray(infoCache) &&
|
|
60
|
+
typeof infoCache.Default === "object" &&
|
|
61
|
+
infoCache.Default !== null &&
|
|
62
|
+
!Array.isArray(infoCache.Default)
|
|
63
|
+
? infoCache.Default
|
|
64
|
+
: null;
|
|
65
|
+
const prefs = safeReadJson(preferencesPath);
|
|
66
|
+
const browserTheme = (() => {
|
|
67
|
+
const browser = prefs?.browser;
|
|
68
|
+
const theme = typeof browser === "object" && browser !== null && !Array.isArray(browser)
|
|
69
|
+
? browser.theme
|
|
70
|
+
: null;
|
|
71
|
+
return typeof theme === "object" && theme !== null && !Array.isArray(theme)
|
|
72
|
+
? theme
|
|
73
|
+
: null;
|
|
74
|
+
})();
|
|
75
|
+
const autogeneratedTheme = (() => {
|
|
76
|
+
const autogenerated = prefs?.autogenerated;
|
|
77
|
+
const theme = typeof autogenerated === "object" && autogenerated !== null && !Array.isArray(autogenerated)
|
|
78
|
+
? autogenerated.theme
|
|
79
|
+
: null;
|
|
80
|
+
return typeof theme === "object" && theme !== null && !Array.isArray(theme)
|
|
81
|
+
? theme
|
|
82
|
+
: null;
|
|
83
|
+
})();
|
|
84
|
+
const nameOk = typeof info?.name === "string" ? info.name === desiredName : true;
|
|
85
|
+
if (desiredColorInt == null) {
|
|
86
|
+
// If the user provided a non-#RRGGBB value, we can only do best-effort.
|
|
87
|
+
return nameOk;
|
|
88
|
+
}
|
|
89
|
+
const localSeedOk = typeof info?.profile_color_seed === "number"
|
|
90
|
+
? info.profile_color_seed === desiredColorInt
|
|
91
|
+
: false;
|
|
92
|
+
const prefOk = (typeof browserTheme?.user_color2 === "number" &&
|
|
93
|
+
browserTheme.user_color2 === desiredColorInt) ||
|
|
94
|
+
(typeof autogeneratedTheme?.color === "number" && autogeneratedTheme.color === desiredColorInt);
|
|
95
|
+
return nameOk && localSeedOk && prefOk;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Best-effort profile decoration (name + lobster-orange). Chrome preference keys
|
|
99
|
+
* vary by version; we keep this conservative and idempotent.
|
|
100
|
+
*/
|
|
101
|
+
export function decorateOpenClawProfile(userDataDir, opts) {
|
|
102
|
+
const desiredName = opts?.name ?? DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME;
|
|
103
|
+
const desiredColor = (opts?.color ?? DEFAULT_OPENCLAW_BROWSER_COLOR).toUpperCase();
|
|
104
|
+
const desiredColorInt = parseHexRgbToSignedArgbInt(desiredColor);
|
|
105
|
+
const localStatePath = path.join(userDataDir, "Local State");
|
|
106
|
+
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
|
|
107
|
+
const localState = safeReadJson(localStatePath) ?? {};
|
|
108
|
+
// Common-ish shape: profile.info_cache.Default
|
|
109
|
+
setDeep(localState, ["profile", "info_cache", "Default", "name"], desiredName);
|
|
110
|
+
setDeep(localState, ["profile", "info_cache", "Default", "shortcut_name"], desiredName);
|
|
111
|
+
setDeep(localState, ["profile", "info_cache", "Default", "user_name"], desiredName);
|
|
112
|
+
// Color keys are best-effort (Chrome changes these frequently).
|
|
113
|
+
setDeep(localState, ["profile", "info_cache", "Default", "profile_color"], desiredColor);
|
|
114
|
+
setDeep(localState, ["profile", "info_cache", "Default", "user_color"], desiredColor);
|
|
115
|
+
if (desiredColorInt != null) {
|
|
116
|
+
// These are the fields Chrome actually uses for profile/avatar tinting.
|
|
117
|
+
setDeep(localState, ["profile", "info_cache", "Default", "profile_color_seed"], desiredColorInt);
|
|
118
|
+
setDeep(localState, ["profile", "info_cache", "Default", "profile_highlight_color"], desiredColorInt);
|
|
119
|
+
setDeep(localState, ["profile", "info_cache", "Default", "default_avatar_fill_color"], desiredColorInt);
|
|
120
|
+
setDeep(localState, ["profile", "info_cache", "Default", "default_avatar_stroke_color"], desiredColorInt);
|
|
121
|
+
}
|
|
122
|
+
safeWriteJson(localStatePath, localState);
|
|
123
|
+
const prefs = safeReadJson(preferencesPath) ?? {};
|
|
124
|
+
setDeep(prefs, ["profile", "name"], desiredName);
|
|
125
|
+
setDeep(prefs, ["profile", "profile_color"], desiredColor);
|
|
126
|
+
setDeep(prefs, ["profile", "user_color"], desiredColor);
|
|
127
|
+
if (desiredColorInt != null) {
|
|
128
|
+
// Chrome refresh stores the autogenerated theme in these prefs (SkColor ints).
|
|
129
|
+
setDeep(prefs, ["autogenerated", "theme", "color"], desiredColorInt);
|
|
130
|
+
// User-selected browser theme color (pref name: browser.theme.user_color2).
|
|
131
|
+
setDeep(prefs, ["browser", "theme", "user_color2"], desiredColorInt);
|
|
132
|
+
}
|
|
133
|
+
safeWriteJson(preferencesPath, prefs);
|
|
134
|
+
try {
|
|
135
|
+
fs.writeFileSync(decoratedMarkerPath(userDataDir), `${Date.now()}\n`, "utf-8");
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// ignore
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export function ensureProfileCleanExit(userDataDir) {
|
|
142
|
+
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
|
|
143
|
+
const prefs = safeReadJson(preferencesPath) ?? {};
|
|
144
|
+
setDeep(prefs, ["exit_type"], "Normal");
|
|
145
|
+
setDeep(prefs, ["exited_cleanly"], true);
|
|
146
|
+
safeWriteJson(preferencesPath, prefs);
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=chrome.profile-decoration.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const DEFAULT_OPENCLAW_BROWSER_ENABLED = true;
|
|
2
|
+
export declare const DEFAULT_BROWSER_EVALUATE_ENABLED = true;
|
|
3
|
+
export declare const DEFAULT_OPENCLAW_BROWSER_COLOR = "#FF4500";
|
|
4
|
+
export declare const DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME = "openclaw";
|
|
5
|
+
export declare const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "chrome";
|
|
6
|
+
export declare const DEFAULT_AI_SNAPSHOT_MAX_CHARS = 80000;
|
|
7
|
+
export declare const DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS = 10000;
|
|
8
|
+
export declare const DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH = 6;
|
|
9
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const DEFAULT_OPENCLAW_BROWSER_ENABLED = true;
|
|
2
|
+
export const DEFAULT_BROWSER_EVALUATE_ENABLED = true;
|
|
3
|
+
export const DEFAULT_OPENCLAW_BROWSER_COLOR = "#FF4500";
|
|
4
|
+
export const DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME = "openclaw";
|
|
5
|
+
export const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "chrome";
|
|
6
|
+
export const DEFAULT_AI_SNAPSHOT_MAX_CHARS = 80_000;
|
|
7
|
+
export const DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS = 10_000;
|
|
8
|
+
export const DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH = 6;
|
|
9
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP port allocation for browser profiles.
|
|
3
|
+
*
|
|
4
|
+
* Default port range: 18800-18899 (100 profiles max)
|
|
5
|
+
* Ports are allocated once at profile creation and persisted in config.
|
|
6
|
+
* Multi-instance: callers may pass an explicit range to avoid collisions.
|
|
7
|
+
*
|
|
8
|
+
* Reserved ports (do not use for CDP):
|
|
9
|
+
* 18789 - Gateway WebSocket
|
|
10
|
+
* 18790 - Bridge
|
|
11
|
+
* 18791 - Browser control server
|
|
12
|
+
* 18792-18799 - Reserved for future one-off services (canvas at 18793)
|
|
13
|
+
*/
|
|
14
|
+
export declare const CDP_PORT_RANGE_START = 18800;
|
|
15
|
+
export declare const CDP_PORT_RANGE_END = 18899;
|
|
16
|
+
export declare const PROFILE_NAME_REGEX: RegExp;
|
|
17
|
+
export declare function isValidProfileName(name: string): boolean;
|
|
18
|
+
export declare function allocateCdpPort(usedPorts: Set<number>, range?: {
|
|
19
|
+
start: number;
|
|
20
|
+
end: number;
|
|
21
|
+
}): number | null;
|
|
22
|
+
export declare function getUsedPorts(profiles: Record<string, {
|
|
23
|
+
cdpPort?: number;
|
|
24
|
+
cdpUrl?: string;
|
|
25
|
+
}> | undefined): Set<number>;
|
|
26
|
+
export declare const PROFILE_COLORS: string[];
|
|
27
|
+
export declare function allocateColor(usedColors: Set<string>): string;
|
|
28
|
+
export declare function getUsedColors(profiles: Record<string, {
|
|
29
|
+
color: string;
|
|
30
|
+
}> | undefined): Set<string>;
|
|
31
|
+
//# sourceMappingURL=profiles.d.ts.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP port allocation for browser profiles.
|
|
3
|
+
*
|
|
4
|
+
* Default port range: 18800-18899 (100 profiles max)
|
|
5
|
+
* Ports are allocated once at profile creation and persisted in config.
|
|
6
|
+
* Multi-instance: callers may pass an explicit range to avoid collisions.
|
|
7
|
+
*
|
|
8
|
+
* Reserved ports (do not use for CDP):
|
|
9
|
+
* 18789 - Gateway WebSocket
|
|
10
|
+
* 18790 - Bridge
|
|
11
|
+
* 18791 - Browser control server
|
|
12
|
+
* 18792-18799 - Reserved for future one-off services (canvas at 18793)
|
|
13
|
+
*/
|
|
14
|
+
export const CDP_PORT_RANGE_START = 18800;
|
|
15
|
+
export const CDP_PORT_RANGE_END = 18899;
|
|
16
|
+
export const PROFILE_NAME_REGEX = /^[a-z0-9][a-z0-9-]*$/;
|
|
17
|
+
export function isValidProfileName(name) {
|
|
18
|
+
if (!name || name.length > 64) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return PROFILE_NAME_REGEX.test(name);
|
|
22
|
+
}
|
|
23
|
+
export function allocateCdpPort(usedPorts, range) {
|
|
24
|
+
const start = range?.start ?? CDP_PORT_RANGE_START;
|
|
25
|
+
const end = range?.end ?? CDP_PORT_RANGE_END;
|
|
26
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || start <= 0 || end <= 0) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
if (start > end) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
for (let port = start; port <= end; port++) {
|
|
33
|
+
if (!usedPorts.has(port)) {
|
|
34
|
+
return port;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
export function getUsedPorts(profiles) {
|
|
40
|
+
if (!profiles) {
|
|
41
|
+
return new Set();
|
|
42
|
+
}
|
|
43
|
+
const used = new Set();
|
|
44
|
+
for (const profile of Object.values(profiles)) {
|
|
45
|
+
if (typeof profile.cdpPort === "number") {
|
|
46
|
+
used.add(profile.cdpPort);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const rawUrl = profile.cdpUrl?.trim();
|
|
50
|
+
if (!rawUrl) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const parsed = new URL(rawUrl);
|
|
55
|
+
const port = parsed.port && Number.parseInt(parsed.port, 10) > 0
|
|
56
|
+
? Number.parseInt(parsed.port, 10)
|
|
57
|
+
: parsed.protocol === "https:"
|
|
58
|
+
? 443
|
|
59
|
+
: 80;
|
|
60
|
+
if (!Number.isNaN(port) && port > 0 && port <= 65535) {
|
|
61
|
+
used.add(port);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// ignore invalid URLs
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return used;
|
|
69
|
+
}
|
|
70
|
+
export const PROFILE_COLORS = [
|
|
71
|
+
"#FF4500", // Orange-red (openclaw default)
|
|
72
|
+
"#0066CC", // Blue
|
|
73
|
+
"#00AA00", // Green
|
|
74
|
+
"#9933FF", // Purple
|
|
75
|
+
"#FF6699", // Pink
|
|
76
|
+
"#00CCCC", // Cyan
|
|
77
|
+
"#FF9900", // Orange
|
|
78
|
+
"#6666FF", // Indigo
|
|
79
|
+
"#CC3366", // Magenta
|
|
80
|
+
"#339966", // Teal
|
|
81
|
+
];
|
|
82
|
+
export function allocateColor(usedColors) {
|
|
83
|
+
// Find first unused color from palette
|
|
84
|
+
for (const color of PROFILE_COLORS) {
|
|
85
|
+
if (!usedColors.has(color.toUpperCase())) {
|
|
86
|
+
return color;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// All colors used, cycle based on count
|
|
90
|
+
const index = usedColors.size % PROFILE_COLORS.length;
|
|
91
|
+
return PROFILE_COLORS[index] ?? PROFILE_COLORS[0];
|
|
92
|
+
}
|
|
93
|
+
export function getUsedColors(profiles) {
|
|
94
|
+
if (!profiles) {
|
|
95
|
+
return new Set();
|
|
96
|
+
}
|
|
97
|
+
return new Set(Object.values(profiles).map((p) => p.color.toUpperCase()));
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=profiles.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type TargetIdResolution = {
|
|
2
|
+
ok: true;
|
|
3
|
+
targetId: string;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
reason: "not_found" | "ambiguous";
|
|
7
|
+
matches?: string[];
|
|
8
|
+
};
|
|
9
|
+
export declare function resolveTargetIdFromTabs(input: string, tabs: Array<{
|
|
10
|
+
targetId: string;
|
|
11
|
+
}>): TargetIdResolution;
|
|
12
|
+
//# sourceMappingURL=target-id.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function resolveTargetIdFromTabs(input, tabs) {
|
|
2
|
+
const needle = input.trim();
|
|
3
|
+
if (!needle) {
|
|
4
|
+
return { ok: false, reason: "not_found" };
|
|
5
|
+
}
|
|
6
|
+
const exact = tabs.find((t) => t.targetId === needle);
|
|
7
|
+
if (exact) {
|
|
8
|
+
return { ok: true, targetId: exact.targetId };
|
|
9
|
+
}
|
|
10
|
+
const lower = needle.toLowerCase();
|
|
11
|
+
const matches = tabs.map((t) => t.targetId).filter((id) => id.toLowerCase().startsWith(lower));
|
|
12
|
+
const only = matches.length === 1 ? matches[0] : undefined;
|
|
13
|
+
if (only) {
|
|
14
|
+
return { ok: true, targetId: only };
|
|
15
|
+
}
|
|
16
|
+
if (matches.length === 0) {
|
|
17
|
+
return { ok: false, reason: "not_found" };
|
|
18
|
+
}
|
|
19
|
+
return { ok: false, reason: "ambiguous", matches };
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=target-id.js.map
|
package/dist/data-dir.js
ADDED
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
export declare function setLogLevel(level: LogLevel): void;
|
|
3
|
+
export declare function getLogLevel(): LogLevel;
|
|
4
|
+
/** Initialize file logging. Call once at startup. */
|
|
5
|
+
export declare function initLogFile(dir: string): void;
|
|
6
|
+
/** Returns the logs directory path (empty string if not initialized). */
|
|
7
|
+
export declare function getLogsDir(): string;
|
|
8
|
+
export declare function createLogger(tag: string): {
|
|
9
|
+
debug: (msg: string, ...args: unknown[]) => void;
|
|
10
|
+
info: (msg: string, ...args: unknown[]) => void;
|
|
11
|
+
warn: (msg: string, ...args: unknown[]) => void;
|
|
12
|
+
error: (msg: string, ...args: unknown[]) => void;
|
|
13
|
+
};
|
|
14
|
+
export type Logger = ReturnType<typeof createLogger>;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=logger.d.ts.map
|