mirror of
https://github.com/jo-inc/camofox-browser.git
synced 2026-10-02 04:14:41 +08:00
feat: add opt-in interactive desktop browser
This commit is contained in:
@@ -644,6 +644,7 @@ Browser behavior can be tuned in `camofox.config.json`:
|
||||
| `MAX_TABS_PER_SESSION` | Max tabs per session | `10` |
|
||||
| `SESSION_TIMEOUT_MS` | Session inactivity timeout | `1800000` (30min) |
|
||||
| `BROWSER_IDLE_TIMEOUT_MS` | Kill browser when idle (0 = never) | `300000` (5min) |
|
||||
| `CAMOFOX_INTERACTIVE` | Interactive browser mode: `desktop` opens a real local Camoufox window; `off` keeps normal headless behavior | `off` |
|
||||
| `HANDLER_TIMEOUT_MS` | Max time for any handler | `30000` (30s) |
|
||||
| `MAX_CONCURRENT_PER_USER` | Concurrent request cap per user | `3` |
|
||||
| `MAX_OLD_SPACE_SIZE` | Node.js V8 heap limit (MB) | `128` |
|
||||
@@ -666,6 +667,24 @@ Browser behavior can be tuned in `camofox.config.json`:
|
||||
| `VNC_PASSWORD` | Password for VNC access (recommended in production) | - |
|
||||
| `NOVNC_PORT` | noVNC web UI port | `6080` |
|
||||
|
||||
## Interactive desktop browser
|
||||
|
||||
Camofox is headless by default. On a machine with a local graphical desktop, opt in to a visible Camoufox window:
|
||||
|
||||
```bash
|
||||
CAMOFOX_INTERACTIVE=desktop npm start
|
||||
```
|
||||
|
||||
Or set the same preference in `camofox.config.json` and restart the server:
|
||||
|
||||
```json
|
||||
{
|
||||
"interactive": { "mode": "desktop" }
|
||||
}
|
||||
```
|
||||
|
||||
The environment variable overrides the file setting. Set `CAMOFOX_INTERACTIVE=off` to return to normal headless behavior. This mode is intended for a person using the same machine; it does not expose a remote browser-control service.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"name": "Camofox Browser",
|
||||
"version": "1.6.0",
|
||||
"newPageTimeoutMs": 10000,
|
||||
"interactive": { "mode": "off" },
|
||||
"plugins": {
|
||||
"youtube": { "enabled": true },
|
||||
"persistence": { "enabled": true },
|
||||
|
||||
@@ -76,6 +76,11 @@ function camoufoxExecutablePath(env = process.env) {
|
||||
).trim();
|
||||
}
|
||||
|
||||
function normalizeInteractiveMode(value) {
|
||||
const mode = String(value || '').trim().toLowerCase();
|
||||
return ['off', 'desktop', 'novnc', 'auto'].includes(mode) ? mode : 'off';
|
||||
}
|
||||
|
||||
function loadConfig({ configPath = CONFIG_PATH } = {}) {
|
||||
const externalCamoufoxExecutable = camoufoxExecutablePath();
|
||||
const browserIdleTimeoutMs = parseInt(process.env.BROWSER_IDLE_TIMEOUT_MS, 10);
|
||||
@@ -84,6 +89,8 @@ function loadConfig({ configPath = CONFIG_PATH } = {}) {
|
||||
const newPageTimeoutMs = Number.isFinite(configuredNewPageTimeoutMs) && configuredNewPageTimeoutMs > 0
|
||||
? configuredNewPageTimeoutMs
|
||||
: 10000;
|
||||
const configuredInteractiveMode = fileConfig.interactive?.mode;
|
||||
const interactiveMode = normalizeInteractiveMode(process.env.CAMOFOX_INTERACTIVE || configuredInteractiveMode);
|
||||
return {
|
||||
port: parseInt(process.env.CAMOFOX_PORT || process.env.PORT || '9377', 10),
|
||||
bindHost: (process.env.CAMOFOX_BIND_HOST || '').trim(),
|
||||
@@ -118,6 +125,7 @@ function loadConfig({ configPath = CONFIG_PATH } = {}) {
|
||||
camoufoxCacheDir: camoufoxCacheDir(),
|
||||
prometheusEnabled: process.env.PROMETHEUS_ENABLED === '1' || process.env.PROMETHEUS_ENABLED === 'true',
|
||||
disableDefaultAddons: process.env.CAMOFOX_DISABLE_DEFAULT_ADDONS === '1' || process.env.CAMOFOX_DISABLE_DEFAULT_ADDONS === 'true',
|
||||
interactiveMode,
|
||||
proxy: {
|
||||
strategy: inferProxyStrategy(process.env.PROXY_STRATEGY || ''),
|
||||
providerName: process.env.PROXY_PROVIDER || 'decodo',
|
||||
@@ -153,6 +161,7 @@ function loadConfig({ configPath = CONFIG_PATH } = {}) {
|
||||
CAMOFOX_TRACES_MAX_BYTES: process.env.CAMOFOX_TRACES_MAX_BYTES,
|
||||
CAMOFOX_TRACES_TTL_HOURS: process.env.CAMOFOX_TRACES_TTL_HOURS,
|
||||
CAMOFOX_DISABLE_DEFAULT_ADDONS: process.env.CAMOFOX_DISABLE_DEFAULT_ADDONS,
|
||||
CAMOFOX_INTERACTIVE: process.env.CAMOFOX_INTERACTIVE,
|
||||
CAMOUFOX_EXECUTABLE: process.env.CAMOUFOX_EXECUTABLE,
|
||||
CAMOUFOX_EXECUTABLE_PATH: process.env.CAMOUFOX_EXECUTABLE_PATH,
|
||||
CAMOFOX_EXECUTABLE_PATH: process.env.CAMOFOX_EXECUTABLE_PATH,
|
||||
|
||||
@@ -1084,10 +1084,10 @@ async function launchBrowserInstance() {
|
||||
|
||||
let localVirtualDisplay = null;
|
||||
let vdDisplay = undefined;
|
||||
const useDesktopWindow = CONFIG.interactiveMode === 'desktop';
|
||||
let candidateBrowser = null;
|
||||
|
||||
try {
|
||||
if (os.platform() === 'linux') {
|
||||
if (os.platform() === 'linux' && !useDesktopWindow) {
|
||||
localVirtualDisplay = pluginCtx.createVirtualDisplay();
|
||||
vdDisplay = await localVirtualDisplay.get();
|
||||
log('info', 'xvfb virtual display started', { display: vdDisplay, attempt });
|
||||
@@ -1108,6 +1108,7 @@ async function launchBrowserInstance() {
|
||||
proxySession: launchProxy?.sessionId || null,
|
||||
proxyPoolSize: proxyPool?.size || 0,
|
||||
virtualDisplay: useVirtualDisplay,
|
||||
interactiveMode: CONFIG.interactiveMode,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -1118,7 +1119,7 @@ async function launchBrowserInstance() {
|
||||
}
|
||||
const options = await launchOptions({
|
||||
executable_path: externalCamoufox?.executablePath,
|
||||
headless: useVirtualDisplay ? false : true,
|
||||
headless: useVirtualDisplay ? false : !useDesktopWindow,
|
||||
os: hostOS,
|
||||
humanize: true,
|
||||
enable_cache: true,
|
||||
@@ -1177,6 +1178,7 @@ async function launchBrowserInstance() {
|
||||
attempt,
|
||||
maxAttempts,
|
||||
virtualDisplay: useVirtualDisplay,
|
||||
interactiveMode: CONFIG.interactiveMode,
|
||||
proxyMode: proxyPool?.mode || null,
|
||||
proxyServer: launchProxy?.server || null,
|
||||
proxySession: launchProxy?.sessionId || null,
|
||||
|
||||
@@ -82,6 +82,27 @@ describe('loadConfig', () => {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('enables desktop interactive mode from the environment and forwards it to server subprocesses', () => {
|
||||
process.env.CAMOFOX_INTERACTIVE = 'desktop';
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
expect(config.interactiveMode).toBe('desktop');
|
||||
expect(config.serverEnv.CAMOFOX_INTERACTIVE).toBe('desktop');
|
||||
});
|
||||
|
||||
test('reads interactive mode from camofox.config.json and rejects invalid modes', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'camofox-config-'));
|
||||
const configPath = path.join(dir, 'camofox.config.json');
|
||||
|
||||
fs.writeFileSync(configPath, JSON.stringify({ interactive: { mode: 'desktop' } }));
|
||||
expect(loadConfig({ configPath }).interactiveMode).toBe('desktop');
|
||||
|
||||
fs.writeFileSync(configPath, JSON.stringify({ interactive: { mode: 'surprise' } }));
|
||||
expect(loadConfig({ configPath }).interactiveMode).toBe('off');
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('disables default addons when CAMOFOX_DISABLE_DEFAULT_ADDONS is set', () => {
|
||||
delete process.env.CAMOFOX_DISABLE_DEFAULT_ADDONS;
|
||||
expect(loadConfig().disableDefaultAddons).toBe(false);
|
||||
|
||||
@@ -49,6 +49,17 @@ describe('launch compatibility source contract', () => {
|
||||
expect(`${googleProbeOptions}\n${sessionContextOptions}`).not.toMatch(/viewport\s*:\s*\{\s*width\s*:/);
|
||||
});
|
||||
|
||||
test('uses a real desktop window only when interactive desktop mode is explicit', () => {
|
||||
const launch = sourceBetween(
|
||||
'async function launchBrowserInstance()',
|
||||
'async function ensureBrowser()'
|
||||
);
|
||||
|
||||
expect(launch).toContain("const useDesktopWindow = CONFIG.interactiveMode === 'desktop'");
|
||||
expect(launch).toContain("if (os.platform() === 'linux' && !useDesktopWindow)");
|
||||
expect(launch).toContain('headless: useVirtualDisplay ? false : !useDesktopWindow');
|
||||
});
|
||||
|
||||
test('health probe context also uses a null viewport', () => {
|
||||
const healthProbeOptions = sourceBetween(
|
||||
'testContext = await browser.newContext(',
|
||||
|
||||
Reference in New Issue
Block a user