@web-auto/camo 0.1.18 → 0.1.20
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/README.md +18 -19
- package/bin/browser-service.mjs +11 -0
- package/package.json +7 -2
- package/scripts/install.mjs +3 -3
- package/src/cli.mjs +8 -5
- package/src/commands/attach.mjs +141 -0
- package/src/commands/browser.mjs +5 -16
- package/src/commands/mouse.mjs +2 -12
- package/src/container/runtime-core/operations/index.mjs +6 -15
- package/src/container/subscription-registry.mjs +6 -6
- package/src/core/actions.mjs +0 -12
- package/src/core/index.mjs +0 -1
- package/src/lifecycle/lock.mjs +7 -3
- package/src/services/browser-service/index.js +651 -0
- package/src/services/browser-service/index.js.map +1 -0
- package/src/services/browser-service/internal/BrowserSession.input.test.js +322 -0
- package/src/services/browser-service/internal/BrowserSession.input.test.js.map +1 -0
- package/src/services/browser-service/internal/BrowserSession.js +304 -0
- package/src/services/browser-service/internal/BrowserSession.js.map +1 -0
- package/src/services/browser-service/internal/ElementRegistry.js +61 -0
- package/src/services/browser-service/internal/ElementRegistry.js.map +1 -0
- package/src/services/browser-service/internal/ProfileLock.js +85 -0
- package/src/services/browser-service/internal/ProfileLock.js.map +1 -0
- package/src/services/browser-service/internal/SessionManager.js +184 -0
- package/src/services/browser-service/internal/SessionManager.js.map +1 -0
- package/src/services/browser-service/internal/SessionManager.test.js +40 -0
- package/src/services/browser-service/internal/SessionManager.test.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/cookies.js +145 -0
- package/src/services/browser-service/internal/browser-session/cookies.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/input-ops.js +127 -0
- package/src/services/browser-service/internal/browser-session/input-ops.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/input-pipeline.js +133 -0
- package/src/services/browser-service/internal/browser-session/input-pipeline.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/logging.js +46 -0
- package/src/services/browser-service/internal/browser-session/navigation.js +39 -0
- package/src/services/browser-service/internal/browser-session/navigation.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/page-hooks.js +443 -0
- package/src/services/browser-service/internal/browser-session/page-hooks.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/page-management.js +212 -0
- package/src/services/browser-service/internal/browser-session/page-management.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/recording.js +199 -0
- package/src/services/browser-service/internal/browser-session/recording.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/runtime-events.js +62 -0
- package/src/services/browser-service/internal/browser-session/runtime-events.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/session-core.js +85 -0
- package/src/services/browser-service/internal/browser-session/session-core.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/session-state.js +39 -0
- package/src/services/browser-service/internal/browser-session/session-state.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/types.js +15 -0
- package/src/services/browser-service/internal/browser-session/types.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/utils.js +69 -0
- package/src/services/browser-service/internal/browser-session/utils.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/viewport-manager.js +47 -0
- package/src/services/browser-service/internal/browser-session/viewport-manager.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/viewport.js +216 -0
- package/src/services/browser-service/internal/browser-session/viewport.js.map +1 -0
- package/src/services/browser-service/internal/container-matcher.js +852 -0
- package/src/services/browser-service/internal/container-matcher.js.map +1 -0
- package/src/services/browser-service/internal/container-registry.js +182 -0
- package/src/services/browser-service/internal/engine-manager.js +259 -0
- package/src/services/browser-service/internal/engine-manager.js.map +1 -0
- package/src/services/browser-service/internal/fingerprint.js +203 -0
- package/src/services/browser-service/internal/fingerprint.js.map +1 -0
- package/src/services/browser-service/internal/heartbeat.js +137 -0
- package/src/services/browser-service/internal/logging.js +46 -0
- package/src/services/browser-service/internal/page-runtime/runtime.js +1317 -0
- package/src/services/browser-service/internal/pageRuntime.js +29 -0
- package/src/services/browser-service/internal/pageRuntime.js.map +1 -0
- package/src/services/browser-service/internal/runtimeInjector.js +31 -0
- package/src/services/browser-service/internal/runtimeInjector.js.map +1 -0
- package/src/services/browser-service/internal/service-process-logger.js +140 -0
- package/src/services/browser-service/internal/state-bus.js +46 -0
- package/src/services/browser-service/internal/state-bus.js.map +1 -0
- package/src/services/browser-service/internal/storage-paths.js +42 -0
- package/src/services/browser-service/internal/storage-paths.js.map +1 -0
- package/src/services/browser-service/internal/ws-server.js +1194 -0
- package/src/services/browser-service/internal/ws-server.js.map +1 -0
- package/src/services/browser-service/internal/ws-server.test.js +59 -0
- package/src/services/browser-service/internal/ws-server.test.js.map +1 -0
- package/src/services/browser-service/server.mjs +6 -0
- package/src/services/controller/cli-bridge.js +93 -0
- package/src/services/controller/container-index.js +50 -0
- package/src/services/controller/container-storage.js +36 -0
- package/src/services/controller/controller-actions.js +207 -0
- package/src/services/controller/controller.js +1138 -0
- package/src/services/controller/selectors.js +54 -0
- package/src/services/controller/transport.js +118 -0
- package/src/utils/browser-service.mjs +100 -125
- package/src/utils/config.mjs +22 -21
- package/src/utils/help.mjs +11 -9
- package/src/utils/ws-client.mjs +30 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function normalizeSelectors(rawSelectors) {
|
|
2
|
+
const out = [];
|
|
3
|
+
const pushCss = (css, extra = {}) => {
|
|
4
|
+
if (typeof css !== 'string' || !css.trim()) return;
|
|
5
|
+
out.push({
|
|
6
|
+
css: css.trim(),
|
|
7
|
+
...(typeof extra.variant === 'string' ? { variant: extra.variant } : {}),
|
|
8
|
+
...(Number.isFinite(Number(extra.score)) ? { score: Number(extra.score) } : {}),
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const splitLegacySelector = (rawSelector) => {
|
|
13
|
+
if (typeof rawSelector !== 'string') return [];
|
|
14
|
+
return rawSelector
|
|
15
|
+
.split(',')
|
|
16
|
+
.map((item) => item.trim())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (Array.isArray(rawSelectors)) {
|
|
21
|
+
for (const item of rawSelectors) {
|
|
22
|
+
if (typeof item === 'string') {
|
|
23
|
+
for (const css of splitLegacySelector(item)) pushCss(css);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (item && typeof item === 'object') {
|
|
27
|
+
if (typeof item.css === 'string') {
|
|
28
|
+
pushCss(item.css, item);
|
|
29
|
+
} else if (typeof item.selector === 'string') {
|
|
30
|
+
for (const css of splitLegacySelector(item.selector)) pushCss(css, item);
|
|
31
|
+
} else if (typeof item.id === 'string' && item.id) {
|
|
32
|
+
pushCss(`#${item.id}`, item);
|
|
33
|
+
} else if (Array.isArray(item.classes) && item.classes.length > 0) {
|
|
34
|
+
pushCss(`.${item.classes.filter(Boolean).join('.')}`, item);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} else if (typeof rawSelectors === 'string') {
|
|
39
|
+
for (const css of splitLegacySelector(rawSelectors)) pushCss(css);
|
|
40
|
+
} else if (rawSelectors && typeof rawSelectors === 'object') {
|
|
41
|
+
if (typeof rawSelectors.css === 'string') {
|
|
42
|
+
pushCss(rawSelectors.css, rawSelectors);
|
|
43
|
+
} else if (typeof rawSelectors.selector === 'string') {
|
|
44
|
+
for (const css of splitLegacySelector(rawSelectors.selector)) pushCss(css, rawSelectors);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const dedup = new Map();
|
|
49
|
+
for (const item of out) {
|
|
50
|
+
const key = `${item.css}::${item.variant || ''}::${item.score || ''}`;
|
|
51
|
+
if (!dedup.has(key)) dedup.set(key, item);
|
|
52
|
+
}
|
|
53
|
+
return Array.from(dedup.values());
|
|
54
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
|
|
3
|
+
export function createTransport({ env = process.env, defaults = {}, debugLog = null } = {}) {
|
|
4
|
+
const getBrowserWsUrl = () => {
|
|
5
|
+
if (env.CAMO_WS_URL) return env.CAMO_WS_URL;
|
|
6
|
+
const host = env.CAMO_WS_HOST || defaults.wsHost || '127.0.0.1';
|
|
7
|
+
const port = Number(env.CAMO_WS_PORT || defaults.wsPort || 8765);
|
|
8
|
+
return `ws://${host}:${port}`;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const getBrowserHttpBase = () => {
|
|
12
|
+
if (env.CAMO_BROWSER_HTTP_BASE) return env.CAMO_BROWSER_HTTP_BASE.replace(/\/$/, '');
|
|
13
|
+
const host = env.CAMO_BROWSER_HTTP_HOST || defaults.httpHost || '127.0.0.1';
|
|
14
|
+
const port = Number(env.CAMO_BROWSER_HTTP_PORT || defaults.httpPort || 7704);
|
|
15
|
+
const protocol = env.CAMO_BROWSER_HTTP_PROTO || defaults.httpProtocol || 'http';
|
|
16
|
+
return `${protocol}://${host}:${port}`;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const browserServiceCommand = async (action, args, options = {}) => {
|
|
20
|
+
const timeoutMs = typeof options.timeoutMs === 'number' && options.timeoutMs > 0
|
|
21
|
+
? options.timeoutMs
|
|
22
|
+
: 20000;
|
|
23
|
+
const profileId = (args?.profileId || args?.profile || args?.sessionId || '').toString();
|
|
24
|
+
debugLog?.('browserServiceCommand:start', { action, profileId, timeoutMs });
|
|
25
|
+
const res = await fetch(`${getBrowserHttpBase()}/command`, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
body: JSON.stringify({ action, args }),
|
|
29
|
+
signal: AbortSignal.timeout ? AbortSignal.timeout(timeoutMs) : undefined,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const raw = await res.text();
|
|
33
|
+
let data = {};
|
|
34
|
+
try {
|
|
35
|
+
data = raw ? JSON.parse(raw) : {};
|
|
36
|
+
} catch {
|
|
37
|
+
data = { raw };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
debugLog?.('browserServiceCommand:http_err', { action, profileId, status: res.status, raw: raw?.slice?.(0, 200) });
|
|
42
|
+
throw new Error(data?.error || data?.body?.error || `browser-service command "${action}" HTTP ${res.status}`);
|
|
43
|
+
}
|
|
44
|
+
if (data && data.ok === false) {
|
|
45
|
+
debugLog?.('browserServiceCommand:ok_false', { action, profileId, error: data.error });
|
|
46
|
+
throw new Error(data.error || `browser-service command "${action}" failed`);
|
|
47
|
+
}
|
|
48
|
+
if (data && data.error) {
|
|
49
|
+
debugLog?.('browserServiceCommand:body_err', { action, profileId, error: data.error });
|
|
50
|
+
throw new Error(data.error);
|
|
51
|
+
}
|
|
52
|
+
debugLog?.('browserServiceCommand:ok', { action, profileId });
|
|
53
|
+
return data.body ?? data;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const sendWsCommand = (wsUrl, payload, timeoutMs = 15000) => new Promise((resolve, reject) => {
|
|
57
|
+
const socket = new WebSocket(wsUrl);
|
|
58
|
+
let settled = false;
|
|
59
|
+
const timeout = setTimeout(() => {
|
|
60
|
+
if (settled) return;
|
|
61
|
+
settled = true;
|
|
62
|
+
socket.terminate();
|
|
63
|
+
reject(new Error('WebSocket command timeout'));
|
|
64
|
+
}, timeoutMs);
|
|
65
|
+
|
|
66
|
+
const cleanup = () => {
|
|
67
|
+
clearTimeout(timeout);
|
|
68
|
+
socket.removeAllListeners();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
socket.once('open', () => {
|
|
72
|
+
try {
|
|
73
|
+
socket.send(JSON.stringify(payload));
|
|
74
|
+
} catch (err) {
|
|
75
|
+
cleanup();
|
|
76
|
+
if (!settled) {
|
|
77
|
+
settled = true;
|
|
78
|
+
reject(err);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
socket.once('message', (data) => {
|
|
84
|
+
cleanup();
|
|
85
|
+
if (settled) return;
|
|
86
|
+
settled = true;
|
|
87
|
+
try {
|
|
88
|
+
resolve(JSON.parse(data.toString('utf-8')));
|
|
89
|
+
} catch (err) {
|
|
90
|
+
reject(err);
|
|
91
|
+
} finally {
|
|
92
|
+
socket.close();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
socket.once('error', (err) => {
|
|
97
|
+
cleanup();
|
|
98
|
+
if (settled) return;
|
|
99
|
+
settled = true;
|
|
100
|
+
reject(err);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
socket.once('close', () => {
|
|
104
|
+
cleanup();
|
|
105
|
+
if (!settled) {
|
|
106
|
+
settled = true;
|
|
107
|
+
reject(new Error('WebSocket closed before response'));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
getBrowserWsUrl,
|
|
114
|
+
getBrowserHttpBase,
|
|
115
|
+
browserServiceCommand,
|
|
116
|
+
sendWsCommand,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -9,7 +9,7 @@ import { BROWSER_SERVICE_URL, loadConfig, setRepoRoot } from './config.mjs';
|
|
|
9
9
|
import { touchSessionActivity } from '../lifecycle/session-registry.mjs';
|
|
10
10
|
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
|
-
const DEFAULT_API_TIMEOUT_MS =
|
|
12
|
+
const DEFAULT_API_TIMEOUT_MS = 90000;
|
|
13
13
|
|
|
14
14
|
function resolveApiTimeoutMs(options = {}) {
|
|
15
15
|
const optionValue = Number(options?.timeoutMs);
|
|
@@ -23,6 +23,78 @@ function resolveApiTimeoutMs(options = {}) {
|
|
|
23
23
|
return DEFAULT_API_TIMEOUT_MS;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function resolveWsUrl() {
|
|
27
|
+
const cfg = loadConfig();
|
|
28
|
+
const explicit = String(process.env.CAMO_WS_URL || '').trim();
|
|
29
|
+
if (explicit) return explicit;
|
|
30
|
+
const host = String(process.env.CAMO_WS_HOST || '127.0.0.1').trim() || '127.0.0.1';
|
|
31
|
+
const port = Number(process.env.CAMO_WS_PORT || 8765) || 8765;
|
|
32
|
+
return `ws://${host}:${port}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function openWs() {
|
|
36
|
+
if (typeof WebSocket !== 'function') {
|
|
37
|
+
throw new Error('Global WebSocket is unavailable in this Node runtime');
|
|
38
|
+
}
|
|
39
|
+
const wsUrl = resolveWsUrl();
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const socket = new WebSocket(wsUrl);
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
try { socket.close(); } catch {}
|
|
44
|
+
reject(new Error(`WebSocket connect timeout: ${wsUrl}`));
|
|
45
|
+
}, 8000);
|
|
46
|
+
socket.addEventListener('open', () => {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
resolve(socket);
|
|
49
|
+
});
|
|
50
|
+
socket.addEventListener('error', (err) => {
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
reject(new Error(`WebSocket connect failed: ${err?.message || String(err)}`));
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function callWS(action, payload = {}, options = {}) {
|
|
58
|
+
const timeoutMs = resolveApiTimeoutMs(options);
|
|
59
|
+
const socket = await openWs();
|
|
60
|
+
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
61
|
+
const sessionId = String(payload?.profileId || payload?.sessionId || payload?.profile || '').trim();
|
|
62
|
+
const message = {
|
|
63
|
+
type: 'command',
|
|
64
|
+
request_id: requestId,
|
|
65
|
+
session_id: sessionId,
|
|
66
|
+
data: { command_type: 'dev_command', action, parameters: payload },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
try { socket.close(); } catch {}
|
|
72
|
+
reject(new Error(`browser-service ws timeout after ${timeoutMs}ms: ${action}`));
|
|
73
|
+
}, timeoutMs);
|
|
74
|
+
|
|
75
|
+
socket.addEventListener('message', (event) => {
|
|
76
|
+
try {
|
|
77
|
+
const data = typeof event.data === 'string' ? JSON.parse(event.data) : JSON.parse(String(event.data));
|
|
78
|
+
if (data?.type === 'response' && data.request_id === requestId) {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
try { socket.close(); } catch {}
|
|
81
|
+
resolve(data?.data ?? data);
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
try { socket.close(); } catch {}
|
|
86
|
+
reject(err);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
socket.send(JSON.stringify(message));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function findRepoRootCandidate() {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
26
98
|
function isTimeoutError(error) {
|
|
27
99
|
const name = String(error?.name || '').toLowerCase();
|
|
28
100
|
const message = String(error?.message || '').toLowerCase();
|
|
@@ -342,64 +414,17 @@ export function ensureCamoufox() {
|
|
|
342
414
|
console.log('Camoufox installed.');
|
|
343
415
|
}
|
|
344
416
|
|
|
345
|
-
const
|
|
346
|
-
const CONTROLLER_SERVER_REL = path.join('services', 'controller', 'src', 'server.mjs');
|
|
417
|
+
const CONTROLLER_SERVER_REL = path.join('src', 'services', 'browser-service', 'index.js');
|
|
347
418
|
|
|
348
|
-
function hasStartScript(root) {
|
|
349
|
-
if (!root) return false;
|
|
350
|
-
return fs.existsSync(path.join(root, START_SCRIPT_REL));
|
|
351
|
-
}
|
|
352
419
|
|
|
353
420
|
function hasControllerServer(root) {
|
|
354
421
|
if (!root) return false;
|
|
355
422
|
return fs.existsSync(path.join(root, CONTROLLER_SERVER_REL));
|
|
356
423
|
}
|
|
357
424
|
|
|
358
|
-
function walkUpForRepoRoot(startDir) {
|
|
359
|
-
if (!startDir) return null;
|
|
360
|
-
let cursor = path.resolve(startDir);
|
|
361
|
-
for (;;) {
|
|
362
|
-
if (hasStartScript(cursor)) return cursor;
|
|
363
|
-
const parent = path.dirname(cursor);
|
|
364
|
-
if (parent === cursor) return null;
|
|
365
|
-
cursor = parent;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
425
|
|
|
369
|
-
function scanCommonRepoRoots() {
|
|
370
|
-
const home = os.homedir();
|
|
371
|
-
const roots = [
|
|
372
|
-
path.join(home, 'Documents', 'github'),
|
|
373
|
-
path.join(home, 'github'),
|
|
374
|
-
path.join(home, 'code'),
|
|
375
|
-
path.join(home, 'projects'),
|
|
376
|
-
path.join('/Volumes', 'extension', 'code'),
|
|
377
|
-
path.join('C:', 'code'),
|
|
378
|
-
path.join('D:', 'code'),
|
|
379
|
-
path.join('C:', 'projects'),
|
|
380
|
-
path.join('D:', 'projects'),
|
|
381
|
-
path.join('C:', 'Users', os.userInfo().username, 'code'),
|
|
382
|
-
path.join('C:', 'Users', os.userInfo().username, 'projects'),
|
|
383
|
-
path.join('C:', 'Users', os.userInfo().username, 'Documents', 'github'),
|
|
384
|
-
].filter(Boolean);
|
|
385
426
|
|
|
386
|
-
for (const root of roots) {
|
|
387
|
-
if (!fs.existsSync(root)) continue;
|
|
388
|
-
try {
|
|
389
|
-
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
390
|
-
for (const entry of entries) {
|
|
391
|
-
if (!entry.isDirectory()) continue;
|
|
392
|
-
if (!entry.name.toLowerCase().includes('webauto')) continue;
|
|
393
|
-
const candidate = path.join(root, entry.name);
|
|
394
|
-
if (hasStartScript(candidate)) return candidate;
|
|
395
|
-
}
|
|
396
|
-
} catch {
|
|
397
|
-
// ignore scanning errors and continue
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
427
|
|
|
401
|
-
return null;
|
|
402
|
-
}
|
|
403
428
|
|
|
404
429
|
function scanCommonInstallRoots() {
|
|
405
430
|
const home = os.homedir();
|
|
@@ -416,75 +441,32 @@ function scanCommonInstallRoots() {
|
|
|
416
441
|
].filter(Boolean);
|
|
417
442
|
|
|
418
443
|
for (const root of nodeModuleRoots) {
|
|
419
|
-
const candidate = path.join(root, '@web-auto', '
|
|
444
|
+
const candidate = path.join(root, '@web-auto', 'camo');
|
|
420
445
|
if (hasControllerServer(candidate)) return candidate;
|
|
421
446
|
}
|
|
422
447
|
return null;
|
|
423
448
|
}
|
|
424
449
|
|
|
425
|
-
export function findRepoRootCandidate() {
|
|
426
|
-
const cfg = loadConfig();
|
|
427
|
-
const candidates = [
|
|
428
|
-
process.env.WEBAUTO_REPO_ROOT,
|
|
429
|
-
cfg.repoRoot,
|
|
430
|
-
process.cwd(),
|
|
431
|
-
path.join('/Volumes', 'extension', 'code', 'webauto'),
|
|
432
|
-
path.join('/Volumes', 'extension', 'code', 'WebAuto'),
|
|
433
|
-
path.join(os.homedir(), 'Documents', 'github', 'webauto'),
|
|
434
|
-
path.join(os.homedir(), 'Documents', 'github', 'WebAuto'),
|
|
435
|
-
path.join(os.homedir(), 'github', 'webauto'),
|
|
436
|
-
path.join(os.homedir(), 'github', 'WebAuto'),
|
|
437
|
-
path.join('C:', 'code', 'webauto'),
|
|
438
|
-
path.join('C:', 'code', 'WebAuto'),
|
|
439
|
-
path.join('C:', 'Users', os.userInfo().username, 'code', 'webauto'),
|
|
440
|
-
path.join('C:', 'Users', os.userInfo().username, 'code', 'WebAuto'),
|
|
441
|
-
].filter(Boolean);
|
|
442
450
|
|
|
443
|
-
for (const root of candidates) {
|
|
444
|
-
if (hasStartScript(root)) {
|
|
445
|
-
if (cfg.repoRoot !== root) {
|
|
446
|
-
setRepoRoot(root);
|
|
447
|
-
}
|
|
448
|
-
return root;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
451
|
|
|
452
|
-
for (const startDir of [process.cwd()]) {
|
|
453
|
-
const found = walkUpForRepoRoot(startDir);
|
|
454
|
-
if (found) {
|
|
455
|
-
if (cfg.repoRoot !== found) {
|
|
456
|
-
setRepoRoot(found);
|
|
457
|
-
}
|
|
458
|
-
return found;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
452
|
|
|
462
|
-
const scanned = scanCommonRepoRoots();
|
|
463
|
-
if (scanned) {
|
|
464
|
-
if (cfg.repoRoot !== scanned) {
|
|
465
|
-
setRepoRoot(scanned);
|
|
466
|
-
}
|
|
467
|
-
return scanned;
|
|
468
|
-
}
|
|
469
453
|
|
|
470
|
-
return null;
|
|
471
|
-
}
|
|
472
454
|
|
|
473
455
|
export function findInstallRootCandidate() {
|
|
474
456
|
const cfg = loadConfig();
|
|
475
457
|
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
476
|
-
const siblingInScopedNodeModules = path.resolve(currentDir, '..', '..', '..', '
|
|
458
|
+
const siblingInScopedNodeModules = path.resolve(currentDir, '..', '..', '..', 'camo');
|
|
477
459
|
const candidates = [
|
|
478
|
-
process.env.
|
|
479
|
-
process.env.
|
|
480
|
-
process.env.
|
|
460
|
+
process.env.CAMO_INSTALL_DIR,
|
|
461
|
+
process.env.CAMO_PACKAGE_ROOT,
|
|
462
|
+
process.env.CAMO_REPO_ROOT,
|
|
481
463
|
cfg.repoRoot,
|
|
482
464
|
siblingInScopedNodeModules,
|
|
483
465
|
process.cwd(),
|
|
484
466
|
].filter(Boolean);
|
|
485
467
|
|
|
486
468
|
try {
|
|
487
|
-
const pkgPath = require.resolve('@web-auto/
|
|
469
|
+
const pkgPath = require.resolve('@web-auto/camo/package.json');
|
|
488
470
|
candidates.push(path.dirname(pkgPath));
|
|
489
471
|
} catch {
|
|
490
472
|
// ignore resolution failures in npx-only environments
|
|
@@ -504,34 +486,27 @@ export function findInstallRootCandidate() {
|
|
|
504
486
|
export async function ensureBrowserService() {
|
|
505
487
|
if (await checkBrowserService()) return;
|
|
506
488
|
|
|
507
|
-
const
|
|
508
|
-
if (
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const installRoot = findInstallRootCandidate();
|
|
514
|
-
if (!installRoot) {
|
|
515
|
-
throw new Error(
|
|
516
|
-
`Cannot locate browser-service launcher (${START_SCRIPT_REL} or ${CONTROLLER_SERVER_REL}). ` +
|
|
517
|
-
'Set WEBAUTO_INSTALL_DIR=<@web-auto/webauto install dir> or WEBAUTO_REPO_ROOT=<repo root>.',
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
const scriptPath = path.join(installRoot, CONTROLLER_SERVER_REL);
|
|
521
|
-
const env = {
|
|
522
|
-
...process.env,
|
|
523
|
-
WEBAUTO_REPO_ROOT: String(process.env.WEBAUTO_REPO_ROOT || '').trim() || installRoot,
|
|
524
|
-
};
|
|
525
|
-
const child = spawn(process.execPath, [scriptPath], {
|
|
526
|
-
cwd: installRoot,
|
|
527
|
-
detached: true,
|
|
528
|
-
stdio: 'ignore',
|
|
529
|
-
windowsHide: true,
|
|
530
|
-
env,
|
|
531
|
-
});
|
|
532
|
-
child.unref();
|
|
533
|
-
console.log(`Starting browser-service daemon (packaged webauto, pid=${child.pid || 'unknown'})...`);
|
|
489
|
+
const installRoot = findInstallRootCandidate();
|
|
490
|
+
if (!installRoot) {
|
|
491
|
+
throw new Error(
|
|
492
|
+
`Cannot locate browser-service launcher (${CONTROLLER_SERVER_REL}). ` +
|
|
493
|
+
'Ensure @web-auto/camo is installed or set CAMO_INSTALL_DIR.',
|
|
494
|
+
);
|
|
534
495
|
}
|
|
496
|
+
const scriptPath = path.join(installRoot, CONTROLLER_SERVER_REL);
|
|
497
|
+
const env = {
|
|
498
|
+
...process.env,
|
|
499
|
+
CAMO_REPO_ROOT: String(process.env.CAMO_REPO_ROOT || '').trim() || installRoot,
|
|
500
|
+
};
|
|
501
|
+
const child = spawn(process.execPath, [scriptPath], {
|
|
502
|
+
cwd: installRoot,
|
|
503
|
+
detached: true,
|
|
504
|
+
stdio: 'ignore',
|
|
505
|
+
windowsHide: true,
|
|
506
|
+
env,
|
|
507
|
+
});
|
|
508
|
+
child.unref();
|
|
509
|
+
console.log(`Starting browser-service daemon (pid=${child.pid || 'unknown'})...`);
|
|
535
510
|
|
|
536
511
|
for (let i = 0; i < 20; i += 1) {
|
|
537
512
|
await new Promise((r) => setTimeout(r, 400));
|
package/src/utils/config.mjs
CHANGED
|
@@ -19,50 +19,58 @@ function normalizePathForPlatform(input, platform = process.platform) {
|
|
|
19
19
|
return isWinPath ? pathApi.normalize(raw) : path.resolve(raw);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function
|
|
22
|
+
function normalizeLegacyCamoRoot(input, platform = process.platform) {
|
|
23
23
|
const pathApi = platform === 'win32' ? path.win32 : path;
|
|
24
24
|
const resolved = normalizePathForPlatform(input, platform);
|
|
25
25
|
const base = pathApi.basename(resolved).toLowerCase();
|
|
26
|
-
if (base === '.
|
|
27
|
-
return pathApi.join(resolved, '.
|
|
26
|
+
if (base === '.camo' || base === 'camo') return resolved;
|
|
27
|
+
return pathApi.join(resolved, '.camo');
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export function
|
|
30
|
+
export function resolveCamoRoot(options = {}) {
|
|
31
31
|
const env = options.env || process.env;
|
|
32
32
|
const platform = String(options.platform || process.platform);
|
|
33
33
|
const pathApi = platform === 'win32' ? path.win32 : path;
|
|
34
34
|
const homeDir = String(options.homeDir || os.homedir());
|
|
35
|
-
const explicitDataRoot = String(env.
|
|
35
|
+
const explicitDataRoot = String(env.CAMO_DATA_ROOT || env.CAMO_HOME || '').trim();
|
|
36
36
|
if (explicitDataRoot) return normalizePathForPlatform(explicitDataRoot, platform);
|
|
37
37
|
|
|
38
|
-
const legacyRoot = String(env.
|
|
39
|
-
if (legacyRoot) return
|
|
38
|
+
const legacyRoot = String(env.CAMO_ROOT || env.CAMO_PORTABLE_ROOT || '').trim();
|
|
39
|
+
if (legacyRoot) return normalizeLegacyCamoRoot(legacyRoot, platform);
|
|
40
40
|
|
|
41
41
|
const dDriveExists = typeof options.hasDDrive === 'boolean'
|
|
42
42
|
? options.hasDDrive
|
|
43
43
|
: hasDrive('D');
|
|
44
44
|
if (platform === 'win32') {
|
|
45
|
-
return dDriveExists ? 'D:\\
|
|
45
|
+
return dDriveExists ? 'D:\\camo' : pathApi.join(homeDir, '.camo');
|
|
46
46
|
}
|
|
47
|
-
return pathApi.join(homeDir, '.
|
|
47
|
+
return pathApi.join(homeDir, '.camo');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Backward-compatible export name; camo no longer uses legacy paths.
|
|
51
|
+
export function resolveLegacyRoot(options = {}) {
|
|
52
|
+
return resolveCamoRoot(options);
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
export function resolveProfilesDir(options = {}) {
|
|
51
56
|
const env = options.env || process.env;
|
|
52
57
|
const platform = String(options.platform || process.platform);
|
|
53
|
-
const explicitProfileRoot = String(env.
|
|
58
|
+
const explicitProfileRoot = String(env.CAMO_PROFILE_ROOT || env.CAMO_PATHS_PROFILES || '').trim();
|
|
54
59
|
if (explicitProfileRoot) {
|
|
55
60
|
return normalizePathForPlatform(explicitProfileRoot, platform);
|
|
56
61
|
}
|
|
57
62
|
const pathApi = platform === 'win32' ? path.win32 : path;
|
|
58
|
-
return pathApi.join(
|
|
63
|
+
return pathApi.join(resolveCamoRoot(options), 'profiles');
|
|
59
64
|
}
|
|
60
65
|
|
|
61
|
-
export const CONFIG_DIR =
|
|
66
|
+
export const CONFIG_DIR = resolveCamoRoot();
|
|
62
67
|
export const PROFILES_DIR = resolveProfilesDir();
|
|
63
68
|
export const CONFIG_FILE = path.join(CONFIG_DIR, 'camo-cli.json');
|
|
64
69
|
export const PROFILE_META_FILE = 'camo-profile.json';
|
|
65
|
-
export const BROWSER_SERVICE_URL = process.env.
|
|
70
|
+
export const BROWSER_SERVICE_URL = process.env.CAMO_BROWSER_URL
|
|
71
|
+
|| process.env.CAMO_BROWSER_HTTP_URL
|
|
72
|
+
|| process.env.CAMO_BROWSER_HOST
|
|
73
|
+
|| 'http://127.0.0.1:7704';
|
|
66
74
|
|
|
67
75
|
export function ensureDir(p) {
|
|
68
76
|
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
|
|
@@ -202,12 +210,5 @@ export function setProfileWindowSize(profileId, width, height) {
|
|
|
202
210
|
height: Math.floor(parsedHeight),
|
|
203
211
|
updatedAt: now,
|
|
204
212
|
},
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const START_SCRIPT_REL = path.join('runtime', 'infra', 'utils', 'scripts', 'service', 'start-browser-service.mjs');
|
|
209
|
-
|
|
210
|
-
export function hasStartScript(root) {
|
|
211
|
-
if (!root) return false;
|
|
212
|
-
return fs.existsSync(path.join(root, START_SCRIPT_REL));
|
|
213
|
+
});
|
|
213
214
|
}
|
package/src/utils/help.mjs
CHANGED
|
@@ -20,8 +20,9 @@ INITIALIZATION:
|
|
|
20
20
|
create profile <profileId> Create a new profile
|
|
21
21
|
|
|
22
22
|
CONFIG:
|
|
23
|
-
config repo-root [path] Get or set persisted
|
|
23
|
+
config repo-root [path] Get or set persisted camo repo root
|
|
24
24
|
highlight-mode [status|on|off] Global highlight mode for click/type/scroll (default: on)
|
|
25
|
+
attach <profileId|sessionId> Attach to session via WS; read JSON commands from stdin
|
|
25
26
|
|
|
26
27
|
BROWSER CONTROL:
|
|
27
28
|
init Ensure camoufox + ensure browser-service daemon
|
|
@@ -87,7 +88,6 @@ WINDOW:
|
|
|
87
88
|
window resize [profileId] --width <w> --height <h> Resize browser window
|
|
88
89
|
|
|
89
90
|
MOUSE:
|
|
90
|
-
mouse move [profileId] --x <x> --y <y> [--steps <n>] Move mouse to coordinates
|
|
91
91
|
mouse click [profileId] --x <x> --y <y> [--button left|right|middle] [--clicks <n>] [--delay <ms>] Click at coordinates
|
|
92
92
|
mouse wheel [profileId] [--deltax <px>] [--deltay <px>] Scroll wheel
|
|
93
93
|
|
|
@@ -134,7 +134,6 @@ EXAMPLES:
|
|
|
134
134
|
camo cookies auto start --interval 5000
|
|
135
135
|
camo window move --x 100 --y 100
|
|
136
136
|
camo window resize --width 1920 --height 1080
|
|
137
|
-
camo mouse move --x 500 --y 300
|
|
138
137
|
camo mouse click --x 500 --y 300 --button left
|
|
139
138
|
camo mouse wheel --deltay -300
|
|
140
139
|
camo system display
|
|
@@ -145,6 +144,7 @@ EXAMPLES:
|
|
|
145
144
|
camo lock list
|
|
146
145
|
camo unlock myprofile
|
|
147
146
|
camo stop
|
|
147
|
+
echo '{"action":"stop","args":{"profileId":"myprofile"}}' | camo attach myprofile
|
|
148
148
|
|
|
149
149
|
CONTAINER FILTER & SUBSCRIPTION:
|
|
150
150
|
container init [--source <dir>] [--force] Initialize subscription dir + migrate container sets
|
|
@@ -172,12 +172,14 @@ PROGRESS EVENTS:
|
|
|
172
172
|
(non-events commands auto-start daemon by default)
|
|
173
173
|
|
|
174
174
|
ENV:
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
175
|
+
CAMO_BROWSER_URL Default: http://127.0.0.1:7704
|
|
176
|
+
CAMO_INSTALL_DIR Optional @web-auto/camo install dir
|
|
177
|
+
CAMO_REPO_ROOT Optional camo repo root (dev mode)
|
|
178
|
+
CAMO_DATA_ROOT / CAMO_HOME Optional data root (Windows default D:\\camo)
|
|
179
|
+
CAMO_PROFILE_ROOT Optional profile dir override
|
|
180
|
+
CAMO_ROOT Legacy data root (auto-appends .camo if needed)
|
|
181
|
+
CAMO_WS_URL Optional ws://host:port override
|
|
182
|
+
CAMO_WS_HOST / CAMO_WS_PORT WS host/port for browser-service
|
|
181
183
|
CAMO_PROGRESS_EVENTS_FILE Optional path override for progress jsonl
|
|
182
184
|
CAMO_PROGRESS_WS_HOST / CAMO_PROGRESS_WS_PORT Progress daemon host/port (defaults: 127.0.0.1:7788)
|
|
183
185
|
`);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function resolveWsUrl() {
|
|
2
|
+
const explicit = String(process.env.CAMO_WS_URL || '').trim();
|
|
3
|
+
if (explicit) return explicit;
|
|
4
|
+
const host = String(process.env.CAMO_WS_HOST || '127.0.0.1').trim() || '127.0.0.1';
|
|
5
|
+
const port = Number(process.env.CAMO_WS_PORT || 8765) || 8765;
|
|
6
|
+
return `ws://${host}:${port}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function ensureWsClient(wsUrl = resolveWsUrl()) {
|
|
10
|
+
if (typeof WebSocket !== 'function') {
|
|
11
|
+
throw new Error('Global WebSocket is unavailable in this Node runtime');
|
|
12
|
+
}
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const socket = new WebSocket(wsUrl);
|
|
15
|
+
const timer = setTimeout(() => {
|
|
16
|
+
try { socket.close(); } catch {}
|
|
17
|
+
reject(new Error(`WebSocket connect timeout: ${wsUrl}`));
|
|
18
|
+
}, 8000);
|
|
19
|
+
|
|
20
|
+
socket.addEventListener('open', () => {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
resolve(socket);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
socket.addEventListener('error', (err) => {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
reject(new Error(`WebSocket connect failed: ${err?.message || String(err)}`));
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|