@phi-code-admin/camofox-browser 1.0.0 → 1.0.2
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/AGENTS.md +571 -571
- package/Dockerfile +86 -86
- package/LICENSE +21 -21
- package/README.md +691 -691
- package/camofox.config.json +10 -10
- package/lib/auth.js +134 -134
- package/lib/camoufox-executable.js +189 -189
- package/lib/config.js +153 -153
- package/lib/cookies.js +119 -119
- package/lib/downloads.js +168 -168
- package/lib/extract.js +74 -74
- package/lib/fly.js +54 -54
- package/lib/images.js +88 -88
- package/lib/inflight.js +16 -16
- package/lib/launcher.js +47 -47
- package/lib/macros.js +31 -31
- package/lib/metrics.js +184 -184
- package/lib/openapi.js +105 -105
- package/lib/persistence.js +89 -89
- package/lib/plugins.js +178 -175
- package/lib/proxy.js +277 -277
- package/lib/reporter.js +1102 -1102
- package/lib/request-utils.js +59 -59
- package/lib/resources.js +76 -76
- package/lib/snapshot.js +41 -41
- package/lib/tmp-cleanup.js +108 -108
- package/lib/tracing.js +137 -137
- package/openclaw.plugin.json +268 -268
- package/package.json +148 -148
- package/plugin.ts +758 -758
- package/plugins/persistence/AGENTS.md +37 -37
- package/plugins/persistence/README.md +48 -48
- package/plugins/persistence/index.js +124 -124
- package/plugins/vnc/AGENTS.md +42 -42
- package/plugins/vnc/README.md +165 -165
- package/plugins/vnc/apt.txt +7 -7
- package/plugins/vnc/index.js +142 -142
- package/plugins/vnc/spawn.js +8 -8
- package/plugins/vnc/vnc-launcher.js +64 -64
- package/plugins/vnc/vnc-watcher.sh +82 -82
- package/plugins/youtube/AGENTS.md +25 -25
- package/plugins/youtube/apt.txt +1 -1
- package/plugins/youtube/index.js +206 -206
- package/plugins/youtube/post-install.sh +5 -5
- package/plugins/youtube/youtube.js +301 -301
- package/run.sh +37 -37
- package/scripts/exec.js +8 -8
- package/scripts/generate-openapi.js +24 -24
- package/scripts/install-plugin-deps.sh +63 -63
- package/scripts/plugin.js +342 -342
- package/scripts/sync-version.js +25 -25
- package/server.js +6062 -6059
- package/tsconfig.json +12 -12
package/lib/images.js
CHANGED
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* In-page image extraction via Playwright page.evaluate().
|
|
3
|
-
*
|
|
4
|
-
* Separated from downloads.js to keep file I/O and image extraction concerns apart.
|
|
5
|
-
* (browser-side fetch inside page.evaluate + Node fs reads in same file).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { MAX_DOWNLOAD_INLINE_BYTES } from './downloads.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Extract image metadata (and optionally inline data) from visible <img> elements.
|
|
12
|
-
*/
|
|
13
|
-
async function extractPageImages(page, { includeData = false, maxBytes = MAX_DOWNLOAD_INLINE_BYTES, limit = 8 } = {}) {
|
|
14
|
-
return page.evaluate(
|
|
15
|
-
async ({ includeData, maxBytes, limit }) => {
|
|
16
|
-
const toDataUrl = (blob) =>
|
|
17
|
-
new Promise((resolve, reject) => {
|
|
18
|
-
const reader = new FileReader();
|
|
19
|
-
reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '');
|
|
20
|
-
reader.onerror = () => reject(new Error('file_reader_failed'));
|
|
21
|
-
reader.readAsDataURL(blob);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const nodes = Array.from(document.querySelectorAll('img'));
|
|
25
|
-
const seen = new Set();
|
|
26
|
-
const candidates = [];
|
|
27
|
-
|
|
28
|
-
for (const node of nodes) {
|
|
29
|
-
const src = String(node.currentSrc || node.src || node.getAttribute('src') || '').trim();
|
|
30
|
-
if (!src || seen.has(src)) continue;
|
|
31
|
-
seen.add(src);
|
|
32
|
-
candidates.push({
|
|
33
|
-
src,
|
|
34
|
-
alt: String(node.alt || '').trim(),
|
|
35
|
-
width: Number(node.naturalWidth || node.width || 0) || undefined,
|
|
36
|
-
height: Number(node.naturalHeight || node.height || 0) || undefined,
|
|
37
|
-
});
|
|
38
|
-
if (candidates.length >= limit) break;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const results = [];
|
|
42
|
-
for (const image of candidates) {
|
|
43
|
-
const entry = { src: image.src, alt: image.alt, width: image.width, height: image.height };
|
|
44
|
-
|
|
45
|
-
if (includeData) {
|
|
46
|
-
try {
|
|
47
|
-
if (image.src.startsWith('data:')) {
|
|
48
|
-
const mimeMatch = image.src.match(/^data:([^;,]+)[;,]/i);
|
|
49
|
-
const isBase64 = /;base64,/i.test(image.src);
|
|
50
|
-
const payload = image.src.slice(image.src.indexOf(',') + 1);
|
|
51
|
-
const estimatedBytes = isBase64 ? Math.floor((payload.length * 3) / 4) : payload.length;
|
|
52
|
-
entry.mimeType = mimeMatch ? mimeMatch[1] : 'application/octet-stream';
|
|
53
|
-
entry.bytes = estimatedBytes;
|
|
54
|
-
if (estimatedBytes <= maxBytes) {
|
|
55
|
-
entry.dataUrl = image.src;
|
|
56
|
-
} else {
|
|
57
|
-
entry.dataSkipped = 'max_bytes_exceeded';
|
|
58
|
-
}
|
|
59
|
-
} else {
|
|
60
|
-
const response = await fetch(image.src, { credentials: 'include' });
|
|
61
|
-
if (response.ok) {
|
|
62
|
-
const blob = await response.blob();
|
|
63
|
-
entry.mimeType = blob.type || 'application/octet-stream';
|
|
64
|
-
entry.bytes = blob.size;
|
|
65
|
-
if (blob.size <= maxBytes) {
|
|
66
|
-
entry.dataUrl = await toDataUrl(blob);
|
|
67
|
-
} else {
|
|
68
|
-
entry.dataSkipped = 'max_bytes_exceeded';
|
|
69
|
-
}
|
|
70
|
-
} else {
|
|
71
|
-
entry.fetchError = `http_${response.status}`;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
} catch (err) {
|
|
75
|
-
entry.fetchError = String(err?.message || err || 'image_fetch_failed');
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
results.push(entry);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return results;
|
|
83
|
-
},
|
|
84
|
-
{ includeData, maxBytes, limit },
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export { extractPageImages };
|
|
1
|
+
/**
|
|
2
|
+
* In-page image extraction via Playwright page.evaluate().
|
|
3
|
+
*
|
|
4
|
+
* Separated from downloads.js to keep file I/O and image extraction concerns apart.
|
|
5
|
+
* (browser-side fetch inside page.evaluate + Node fs reads in same file).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { MAX_DOWNLOAD_INLINE_BYTES } from './downloads.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extract image metadata (and optionally inline data) from visible <img> elements.
|
|
12
|
+
*/
|
|
13
|
+
async function extractPageImages(page, { includeData = false, maxBytes = MAX_DOWNLOAD_INLINE_BYTES, limit = 8 } = {}) {
|
|
14
|
+
return page.evaluate(
|
|
15
|
+
async ({ includeData, maxBytes, limit }) => {
|
|
16
|
+
const toDataUrl = (blob) =>
|
|
17
|
+
new Promise((resolve, reject) => {
|
|
18
|
+
const reader = new FileReader();
|
|
19
|
+
reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '');
|
|
20
|
+
reader.onerror = () => reject(new Error('file_reader_failed'));
|
|
21
|
+
reader.readAsDataURL(blob);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const nodes = Array.from(document.querySelectorAll('img'));
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const candidates = [];
|
|
27
|
+
|
|
28
|
+
for (const node of nodes) {
|
|
29
|
+
const src = String(node.currentSrc || node.src || node.getAttribute('src') || '').trim();
|
|
30
|
+
if (!src || seen.has(src)) continue;
|
|
31
|
+
seen.add(src);
|
|
32
|
+
candidates.push({
|
|
33
|
+
src,
|
|
34
|
+
alt: String(node.alt || '').trim(),
|
|
35
|
+
width: Number(node.naturalWidth || node.width || 0) || undefined,
|
|
36
|
+
height: Number(node.naturalHeight || node.height || 0) || undefined,
|
|
37
|
+
});
|
|
38
|
+
if (candidates.length >= limit) break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const results = [];
|
|
42
|
+
for (const image of candidates) {
|
|
43
|
+
const entry = { src: image.src, alt: image.alt, width: image.width, height: image.height };
|
|
44
|
+
|
|
45
|
+
if (includeData) {
|
|
46
|
+
try {
|
|
47
|
+
if (image.src.startsWith('data:')) {
|
|
48
|
+
const mimeMatch = image.src.match(/^data:([^;,]+)[;,]/i);
|
|
49
|
+
const isBase64 = /;base64,/i.test(image.src);
|
|
50
|
+
const payload = image.src.slice(image.src.indexOf(',') + 1);
|
|
51
|
+
const estimatedBytes = isBase64 ? Math.floor((payload.length * 3) / 4) : payload.length;
|
|
52
|
+
entry.mimeType = mimeMatch ? mimeMatch[1] : 'application/octet-stream';
|
|
53
|
+
entry.bytes = estimatedBytes;
|
|
54
|
+
if (estimatedBytes <= maxBytes) {
|
|
55
|
+
entry.dataUrl = image.src;
|
|
56
|
+
} else {
|
|
57
|
+
entry.dataSkipped = 'max_bytes_exceeded';
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
const response = await fetch(image.src, { credentials: 'include' });
|
|
61
|
+
if (response.ok) {
|
|
62
|
+
const blob = await response.blob();
|
|
63
|
+
entry.mimeType = blob.type || 'application/octet-stream';
|
|
64
|
+
entry.bytes = blob.size;
|
|
65
|
+
if (blob.size <= maxBytes) {
|
|
66
|
+
entry.dataUrl = await toDataUrl(blob);
|
|
67
|
+
} else {
|
|
68
|
+
entry.dataSkipped = 'max_bytes_exceeded';
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
entry.fetchError = `http_${response.status}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
entry.fetchError = String(err?.message || err || 'image_fetch_failed');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
results.push(entry);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return results;
|
|
83
|
+
},
|
|
84
|
+
{ includeData, maxBytes, limit },
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { extractPageImages };
|
package/lib/inflight.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
async function coalesceInflight(map, key, factory) {
|
|
2
|
-
const existing = map.get(key);
|
|
3
|
-
if (existing) return existing;
|
|
4
|
-
|
|
5
|
-
const promise = (async () => {
|
|
6
|
-
try {
|
|
7
|
-
return await factory();
|
|
8
|
-
} finally {
|
|
9
|
-
map.delete(key);
|
|
10
|
-
}
|
|
11
|
-
})();
|
|
12
|
-
map.set(key, promise);
|
|
13
|
-
return promise;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export { coalesceInflight };
|
|
1
|
+
async function coalesceInflight(map, key, factory) {
|
|
2
|
+
const existing = map.get(key);
|
|
3
|
+
if (existing) return existing;
|
|
4
|
+
|
|
5
|
+
const promise = (async () => {
|
|
6
|
+
try {
|
|
7
|
+
return await factory();
|
|
8
|
+
} finally {
|
|
9
|
+
map.delete(key);
|
|
10
|
+
}
|
|
11
|
+
})();
|
|
12
|
+
map.set(key, promise);
|
|
13
|
+
return promise;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { coalesceInflight };
|
package/lib/launcher.js
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Server subprocess launcher for camofox-browser.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import cp from 'child_process';
|
|
6
|
-
import { join } from 'path';
|
|
7
|
-
|
|
8
|
-
// Alias for clarity
|
|
9
|
-
const startProcess = cp.spawn;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Start the camofox server as a subprocess.
|
|
13
|
-
* @param {object} opts
|
|
14
|
-
* @param {string} opts.pluginDir - Directory containing server.js
|
|
15
|
-
* @param {number} opts.port - Port number for the server
|
|
16
|
-
* @param {object} opts.env - Environment variables to pass to the subprocess
|
|
17
|
-
* @param {string[]} [opts.nodeArgs] - Extra Node.js CLI flags (e.g. --max-old-space-size=128)
|
|
18
|
-
* @param {{ info: (msg: string) => void, error: (msg: string) => void }} opts.log - Logger
|
|
19
|
-
* @returns {import('child_process').ChildProcess}
|
|
20
|
-
*/
|
|
21
|
-
function launchServer({ pluginDir, port, env, nodeArgs, log }) {
|
|
22
|
-
const serverPath = join(pluginDir, 'server.js');
|
|
23
|
-
const args = [...(nodeArgs || []), serverPath];
|
|
24
|
-
const proc = startProcess('node', args, {
|
|
25
|
-
cwd: pluginDir,
|
|
26
|
-
env: {
|
|
27
|
-
...env,
|
|
28
|
-
CAMOFOX_PORT: String(port),
|
|
29
|
-
},
|
|
30
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
31
|
-
detached: false,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
proc.stdout?.on('data', (data) => {
|
|
35
|
-
const msg = data.toString().trim();
|
|
36
|
-
if (msg) log?.info?.(`[server] ${msg}`);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
proc.stderr?.on('data', (data) => {
|
|
40
|
-
const msg = data.toString().trim();
|
|
41
|
-
if (msg) log?.error?.(`[server] ${msg}`);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
return proc;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export { launchServer };
|
|
1
|
+
/**
|
|
2
|
+
* Server subprocess launcher for camofox-browser.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import cp from 'child_process';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
|
|
8
|
+
// Alias for clarity
|
|
9
|
+
const startProcess = cp.spawn;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Start the camofox server as a subprocess.
|
|
13
|
+
* @param {object} opts
|
|
14
|
+
* @param {string} opts.pluginDir - Directory containing server.js
|
|
15
|
+
* @param {number} opts.port - Port number for the server
|
|
16
|
+
* @param {object} opts.env - Environment variables to pass to the subprocess
|
|
17
|
+
* @param {string[]} [opts.nodeArgs] - Extra Node.js CLI flags (e.g. --max-old-space-size=128)
|
|
18
|
+
* @param {{ info: (msg: string) => void, error: (msg: string) => void }} opts.log - Logger
|
|
19
|
+
* @returns {import('child_process').ChildProcess}
|
|
20
|
+
*/
|
|
21
|
+
function launchServer({ pluginDir, port, env, nodeArgs, log }) {
|
|
22
|
+
const serverPath = join(pluginDir, 'server.js');
|
|
23
|
+
const args = [...(nodeArgs || []), serverPath];
|
|
24
|
+
const proc = startProcess('node', args, {
|
|
25
|
+
cwd: pluginDir,
|
|
26
|
+
env: {
|
|
27
|
+
...env,
|
|
28
|
+
CAMOFOX_PORT: String(port),
|
|
29
|
+
},
|
|
30
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
31
|
+
detached: false,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
proc.stdout?.on('data', (data) => {
|
|
35
|
+
const msg = data.toString().trim();
|
|
36
|
+
if (msg) log?.info?.(`[server] ${msg}`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
proc.stderr?.on('data', (data) => {
|
|
40
|
+
const msg = data.toString().trim();
|
|
41
|
+
if (msg) log?.error?.(`[server] ${msg}`);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return proc;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { launchServer };
|
package/lib/macros.js
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
const MACROS = {
|
|
2
|
-
'@google_search': (query) => `https://www.google.com/search?q=${encodeURIComponent(query || '')}`,
|
|
3
|
-
'@youtube_search': (query) => `https://www.youtube.com/results?search_query=${encodeURIComponent(query || '')}`,
|
|
4
|
-
'@amazon_search': (query) => `https://www.amazon.com/s?k=${encodeURIComponent(query || '')}`,
|
|
5
|
-
'@reddit_search': (query) => `https://www.reddit.com/search.json?q=${encodeURIComponent(query || '')}&limit=25`,
|
|
6
|
-
'@reddit_subreddit': (query) => `https://www.reddit.com/r/${encodeURIComponent(query || 'all')}.json?limit=25`,
|
|
7
|
-
'@wikipedia_search': (query) => `https://en.wikipedia.org/wiki/Special:Search?search=${encodeURIComponent(query || '')}`,
|
|
8
|
-
'@twitter_search': (query) => `https://twitter.com/search?q=${encodeURIComponent(query || '')}`,
|
|
9
|
-
'@yelp_search': (query) => `https://www.yelp.com/search?find_desc=${encodeURIComponent(query || '')}`,
|
|
10
|
-
'@spotify_search': (query) => `https://open.spotify.com/search/${encodeURIComponent(query || '')}`,
|
|
11
|
-
'@netflix_search': (query) => `https://www.netflix.com/search?q=${encodeURIComponent(query || '')}`,
|
|
12
|
-
'@linkedin_search': (query) => `https://www.linkedin.com/search/results/all/?keywords=${encodeURIComponent(query || '')}`,
|
|
13
|
-
'@instagram_search': (query) => `https://www.instagram.com/explore/tags/${encodeURIComponent(query || '')}`,
|
|
14
|
-
'@tiktok_search': (query) => `https://www.tiktok.com/search?q=${encodeURIComponent(query || '')}`,
|
|
15
|
-
'@twitch_search': (query) => `https://www.twitch.tv/search?term=${encodeURIComponent(query || '')}`
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
function expandMacro(macro, query) {
|
|
19
|
-
const macroFn = MACROS[macro];
|
|
20
|
-
return macroFn ? macroFn(query) : null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function getSupportedMacros() {
|
|
24
|
-
return Object.keys(MACROS);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export {
|
|
28
|
-
expandMacro,
|
|
29
|
-
getSupportedMacros,
|
|
30
|
-
MACROS
|
|
31
|
-
};
|
|
1
|
+
const MACROS = {
|
|
2
|
+
'@google_search': (query) => `https://www.google.com/search?q=${encodeURIComponent(query || '')}`,
|
|
3
|
+
'@youtube_search': (query) => `https://www.youtube.com/results?search_query=${encodeURIComponent(query || '')}`,
|
|
4
|
+
'@amazon_search': (query) => `https://www.amazon.com/s?k=${encodeURIComponent(query || '')}`,
|
|
5
|
+
'@reddit_search': (query) => `https://www.reddit.com/search.json?q=${encodeURIComponent(query || '')}&limit=25`,
|
|
6
|
+
'@reddit_subreddit': (query) => `https://www.reddit.com/r/${encodeURIComponent(query || 'all')}.json?limit=25`,
|
|
7
|
+
'@wikipedia_search': (query) => `https://en.wikipedia.org/wiki/Special:Search?search=${encodeURIComponent(query || '')}`,
|
|
8
|
+
'@twitter_search': (query) => `https://twitter.com/search?q=${encodeURIComponent(query || '')}`,
|
|
9
|
+
'@yelp_search': (query) => `https://www.yelp.com/search?find_desc=${encodeURIComponent(query || '')}`,
|
|
10
|
+
'@spotify_search': (query) => `https://open.spotify.com/search/${encodeURIComponent(query || '')}`,
|
|
11
|
+
'@netflix_search': (query) => `https://www.netflix.com/search?q=${encodeURIComponent(query || '')}`,
|
|
12
|
+
'@linkedin_search': (query) => `https://www.linkedin.com/search/results/all/?keywords=${encodeURIComponent(query || '')}`,
|
|
13
|
+
'@instagram_search': (query) => `https://www.instagram.com/explore/tags/${encodeURIComponent(query || '')}`,
|
|
14
|
+
'@tiktok_search': (query) => `https://www.tiktok.com/search?q=${encodeURIComponent(query || '')}`,
|
|
15
|
+
'@twitch_search': (query) => `https://www.twitch.tv/search?term=${encodeURIComponent(query || '')}`
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function expandMacro(macro, query) {
|
|
19
|
+
const macroFn = MACROS[macro];
|
|
20
|
+
return macroFn ? macroFn(query) : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getSupportedMacros() {
|
|
24
|
+
return Object.keys(MACROS);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
expandMacro,
|
|
29
|
+
getSupportedMacros,
|
|
30
|
+
MACROS
|
|
31
|
+
};
|