@web-auto/camo 0.1.4 → 0.1.5
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 +5 -1
- package/package.json +1 -1
- package/src/commands/browser.mjs +195 -1
- package/src/commands/window.mjs +81 -20
- package/src/lifecycle/session-watchdog.mjs +47 -0
- package/src/utils/config.mjs +58 -2
- package/src/utils/help.mjs +2 -1
package/README.md
CHANGED
|
@@ -68,12 +68,16 @@ camo create fingerprint --os <os> --region <region>
|
|
|
68
68
|
### Browser Control
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
|
-
camo start [profileId] [--url <url>] [--headless]
|
|
71
|
+
camo start [profileId] [--url <url>] [--headless] [--width <w> --height <h>]
|
|
72
72
|
camo stop [profileId]
|
|
73
73
|
camo status [profileId]
|
|
74
74
|
camo shutdown # Shutdown browser-service (all sessions)
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
`camo start` in headful mode now persists window size per profile and reuses that size on next start.
|
|
78
|
+
If no saved size exists, it defaults to near-fullscreen (full width, slight vertical reserve).
|
|
79
|
+
Use `--width/--height` to override and update the saved profile size.
|
|
80
|
+
|
|
77
81
|
### Lifecycle & Cleanup
|
|
78
82
|
|
|
79
83
|
```bash
|
package/package.json
CHANGED
package/src/commands/browser.mjs
CHANGED
|
@@ -1,11 +1,145 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
listProfiles,
|
|
4
|
+
getDefaultProfile,
|
|
5
|
+
getProfileWindowSize,
|
|
6
|
+
setProfileWindowSize,
|
|
7
|
+
} from '../utils/config.mjs';
|
|
3
8
|
import { callAPI, ensureCamoufox, ensureBrowserService, getSessionByProfile, checkBrowserService } from '../utils/browser-service.mjs';
|
|
4
9
|
import { resolveProfileId, ensureUrlScheme, looksLikeUrlToken, getPositionals } from '../utils/args.mjs';
|
|
5
10
|
import { acquireLock, releaseLock, isLocked, getLockInfo, cleanupStaleLocks } from '../lifecycle/lock.mjs';
|
|
6
11
|
import { registerSession, updateSession, getSessionInfo, unregisterSession, listRegisteredSessions, markSessionClosed, cleanupStaleSessions, recoverSession } from '../lifecycle/session-registry.mjs';
|
|
7
12
|
import { startSessionWatchdog, stopAllSessionWatchdogs, stopSessionWatchdog } from '../lifecycle/session-watchdog.mjs';
|
|
8
13
|
|
|
14
|
+
const START_WINDOW_MIN_WIDTH = 960;
|
|
15
|
+
const START_WINDOW_MIN_HEIGHT = 700;
|
|
16
|
+
const START_WINDOW_MAX_RESERVE = 240;
|
|
17
|
+
const START_WINDOW_DEFAULT_RESERVE = 72;
|
|
18
|
+
|
|
19
|
+
function sleep(ms) {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseNumber(value, fallback = 0) {
|
|
24
|
+
const parsed = Number(value);
|
|
25
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function clamp(value, min, max) {
|
|
29
|
+
return Math.min(Math.max(value, min), max);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function computeTargetViewportFromWindowMetrics(measured) {
|
|
33
|
+
const innerWidth = Math.max(320, parseNumber(measured?.innerWidth, 0));
|
|
34
|
+
const innerHeight = Math.max(240, parseNumber(measured?.innerHeight, 0));
|
|
35
|
+
const outerWidth = Math.max(320, parseNumber(measured?.outerWidth, innerWidth));
|
|
36
|
+
const outerHeight = Math.max(240, parseNumber(measured?.outerHeight, innerHeight));
|
|
37
|
+
|
|
38
|
+
const rawDeltaW = Math.max(0, outerWidth - innerWidth);
|
|
39
|
+
const rawDeltaH = Math.max(0, outerHeight - innerHeight);
|
|
40
|
+
const frameW = rawDeltaW > 400 ? 16 : Math.min(rawDeltaW, 120);
|
|
41
|
+
const frameH = rawDeltaH > 400 ? 88 : Math.min(rawDeltaH, 180);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
width: Math.max(320, outerWidth - frameW),
|
|
45
|
+
height: Math.max(240, outerHeight - frameH),
|
|
46
|
+
frameW,
|
|
47
|
+
frameH,
|
|
48
|
+
innerWidth,
|
|
49
|
+
innerHeight,
|
|
50
|
+
outerWidth,
|
|
51
|
+
outerHeight,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function computeStartWindowSize(metrics, options = {}) {
|
|
56
|
+
const display = metrics?.metrics || metrics || {};
|
|
57
|
+
const reserveFromEnv = parseNumber(process.env.CAMO_DEFAULT_WINDOW_VERTICAL_RESERVE, START_WINDOW_DEFAULT_RESERVE);
|
|
58
|
+
const reserve = clamp(
|
|
59
|
+
parseNumber(options.reservePx, reserveFromEnv),
|
|
60
|
+
0,
|
|
61
|
+
START_WINDOW_MAX_RESERVE,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const workWidth = parseNumber(display.workWidth, 0);
|
|
65
|
+
const workHeight = parseNumber(display.workHeight, 0);
|
|
66
|
+
const width = parseNumber(display.width, 0);
|
|
67
|
+
const height = parseNumber(display.height, 0);
|
|
68
|
+
const baseW = Math.floor(workWidth > 0 ? workWidth : width);
|
|
69
|
+
const baseH = Math.floor(workHeight > 0 ? workHeight : height);
|
|
70
|
+
|
|
71
|
+
if (baseW <= 0 || baseH <= 0) {
|
|
72
|
+
return {
|
|
73
|
+
width: 1920,
|
|
74
|
+
height: 1000,
|
|
75
|
+
reservePx: reserve,
|
|
76
|
+
source: 'fallback',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
width: Math.max(START_WINDOW_MIN_WIDTH, baseW),
|
|
82
|
+
height: Math.max(START_WINDOW_MIN_HEIGHT, baseH - reserve),
|
|
83
|
+
reservePx: reserve,
|
|
84
|
+
source: workWidth > 0 || workHeight > 0 ? 'workArea' : 'screen',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function probeWindowMetrics(profileId) {
|
|
89
|
+
const measured = await callAPI('evaluate', {
|
|
90
|
+
profileId,
|
|
91
|
+
script: '({ innerWidth: window.innerWidth, innerHeight: window.innerHeight, outerWidth: window.outerWidth, outerHeight: window.outerHeight })',
|
|
92
|
+
});
|
|
93
|
+
return measured?.result || {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function syncWindowViewportAfterResize(profileId, width, height, options = {}) {
|
|
97
|
+
const settleMs = Math.max(40, parseNumber(options.settleMs, 120));
|
|
98
|
+
const attempts = Math.max(1, Math.min(8, Math.floor(parseNumber(options.attempts, 4))));
|
|
99
|
+
const tolerancePx = Math.max(0, parseNumber(options.tolerancePx, 3));
|
|
100
|
+
|
|
101
|
+
const windowResult = await callAPI('window:resize', { profileId, width, height });
|
|
102
|
+
await sleep(settleMs);
|
|
103
|
+
|
|
104
|
+
let measured = {};
|
|
105
|
+
let verified = {};
|
|
106
|
+
let viewport = null;
|
|
107
|
+
let matched = false;
|
|
108
|
+
let target = { width: 1280, height: 720, frameW: 16, frameH: 88 };
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
111
|
+
measured = await probeWindowMetrics(profileId);
|
|
112
|
+
target = computeTargetViewportFromWindowMetrics(measured);
|
|
113
|
+
viewport = await callAPI('page:setViewport', {
|
|
114
|
+
profileId,
|
|
115
|
+
width: target.width,
|
|
116
|
+
height: target.height,
|
|
117
|
+
});
|
|
118
|
+
await sleep(settleMs);
|
|
119
|
+
verified = await probeWindowMetrics(profileId);
|
|
120
|
+
const dw = Math.abs(parseNumber(verified?.innerWidth, 0) - target.width);
|
|
121
|
+
const dh = Math.abs(parseNumber(verified?.innerHeight, 0) - target.height);
|
|
122
|
+
if (dw <= tolerancePx && dh <= tolerancePx) {
|
|
123
|
+
matched = true;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
window: windowResult,
|
|
130
|
+
measured,
|
|
131
|
+
verified,
|
|
132
|
+
targetViewport: {
|
|
133
|
+
width: target.width,
|
|
134
|
+
height: target.height,
|
|
135
|
+
frameW: target.frameW,
|
|
136
|
+
frameH: target.frameH,
|
|
137
|
+
matched,
|
|
138
|
+
},
|
|
139
|
+
viewport,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
9
143
|
export async function handleStartCommand(args) {
|
|
10
144
|
ensureCamoufox();
|
|
11
145
|
await ensureBrowserService();
|
|
@@ -14,6 +148,19 @@ export async function handleStartCommand(args) {
|
|
|
14
148
|
|
|
15
149
|
const urlIdx = args.indexOf('--url');
|
|
16
150
|
const explicitUrl = urlIdx >= 0 ? args[urlIdx + 1] : undefined;
|
|
151
|
+
const widthIdx = args.indexOf('--width');
|
|
152
|
+
const heightIdx = args.indexOf('--height');
|
|
153
|
+
const explicitWidth = widthIdx >= 0 ? parseNumber(args[widthIdx + 1], NaN) : NaN;
|
|
154
|
+
const explicitHeight = heightIdx >= 0 ? parseNumber(args[heightIdx + 1], NaN) : NaN;
|
|
155
|
+
const hasExplicitWidth = Number.isFinite(explicitWidth);
|
|
156
|
+
const hasExplicitHeight = Number.isFinite(explicitHeight);
|
|
157
|
+
if (hasExplicitWidth !== hasExplicitHeight) {
|
|
158
|
+
throw new Error('Usage: camo start [profileId] [--url <url>] [--headless] [--width <w> --height <h>]');
|
|
159
|
+
}
|
|
160
|
+
if ((hasExplicitWidth && explicitWidth < START_WINDOW_MIN_WIDTH) || (hasExplicitHeight && explicitHeight < START_WINDOW_MIN_HEIGHT)) {
|
|
161
|
+
throw new Error(`Window size too small. Minimum is ${START_WINDOW_MIN_WIDTH}x${START_WINDOW_MIN_HEIGHT}`);
|
|
162
|
+
}
|
|
163
|
+
const hasExplicitWindowSize = hasExplicitWidth && hasExplicitHeight;
|
|
17
164
|
const profileSet = new Set(listProfiles());
|
|
18
165
|
let implicitUrl;
|
|
19
166
|
|
|
@@ -21,6 +168,7 @@ export async function handleStartCommand(args) {
|
|
|
21
168
|
for (let i = 1; i < args.length; i++) {
|
|
22
169
|
const arg = args[i];
|
|
23
170
|
if (arg === '--url') { i++; continue; }
|
|
171
|
+
if (arg === '--width' || arg === '--height') { i++; continue; }
|
|
24
172
|
if (arg === '--headless') continue;
|
|
25
173
|
if (arg.startsWith('--')) continue;
|
|
26
174
|
|
|
@@ -87,6 +235,52 @@ export async function handleStartCommand(args) {
|
|
|
87
235
|
headless,
|
|
88
236
|
});
|
|
89
237
|
startSessionWatchdog(profileId);
|
|
238
|
+
|
|
239
|
+
if (!headless) {
|
|
240
|
+
let windowTarget = null;
|
|
241
|
+
if (hasExplicitWindowSize) {
|
|
242
|
+
windowTarget = {
|
|
243
|
+
width: Math.floor(explicitWidth),
|
|
244
|
+
height: Math.floor(explicitHeight),
|
|
245
|
+
source: 'explicit',
|
|
246
|
+
};
|
|
247
|
+
} else {
|
|
248
|
+
const rememberedWindow = getProfileWindowSize(profileId);
|
|
249
|
+
if (rememberedWindow) {
|
|
250
|
+
windowTarget = {
|
|
251
|
+
width: rememberedWindow.width,
|
|
252
|
+
height: rememberedWindow.height,
|
|
253
|
+
source: 'profile',
|
|
254
|
+
updatedAt: rememberedWindow.updatedAt,
|
|
255
|
+
};
|
|
256
|
+
} else {
|
|
257
|
+
const display = await callAPI('system:display', {}).catch(() => null);
|
|
258
|
+
windowTarget = computeStartWindowSize(display);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
result.startWindow = {
|
|
263
|
+
width: windowTarget.width,
|
|
264
|
+
height: windowTarget.height,
|
|
265
|
+
source: windowTarget.source,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const syncResult = await syncWindowViewportAfterResize(
|
|
269
|
+
profileId,
|
|
270
|
+
windowTarget.width,
|
|
271
|
+
windowTarget.height,
|
|
272
|
+
).catch((err) => ({ error: err?.message || String(err) }));
|
|
273
|
+
result.windowSync = syncResult;
|
|
274
|
+
|
|
275
|
+
const measuredOuterWidth = Number(syncResult?.verified?.outerWidth);
|
|
276
|
+
const measuredOuterHeight = Number(syncResult?.verified?.outerHeight);
|
|
277
|
+
const savedWindow = setProfileWindowSize(
|
|
278
|
+
profileId,
|
|
279
|
+
Number.isFinite(measuredOuterWidth) ? measuredOuterWidth : windowTarget.width,
|
|
280
|
+
Number.isFinite(measuredOuterHeight) ? measuredOuterHeight : windowTarget.height,
|
|
281
|
+
);
|
|
282
|
+
result.profileWindow = savedWindow?.window || null;
|
|
283
|
+
}
|
|
90
284
|
}
|
|
91
285
|
console.log(JSON.stringify(result, null, 2));
|
|
92
286
|
}
|
package/src/commands/window.mjs
CHANGED
|
@@ -1,7 +1,38 @@
|
|
|
1
|
-
import { getDefaultProfile } from '../utils/config.mjs';
|
|
1
|
+
import { getDefaultProfile, setProfileWindowSize } from '../utils/config.mjs';
|
|
2
2
|
import { callAPI, ensureBrowserService } from '../utils/browser-service.mjs';
|
|
3
3
|
import { getPositionals } from '../utils/args.mjs';
|
|
4
4
|
|
|
5
|
+
function sleep(ms) {
|
|
6
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function computeTargetViewport(measured) {
|
|
10
|
+
const innerWidth = Math.max(320, Number(measured?.innerWidth || 0) || 0);
|
|
11
|
+
const innerHeight = Math.max(240, Number(measured?.innerHeight || 0) || 0);
|
|
12
|
+
const outerWidth = Math.max(320, Number(measured?.outerWidth || 0) || innerWidth);
|
|
13
|
+
const outerHeight = Math.max(240, Number(measured?.outerHeight || 0) || innerHeight);
|
|
14
|
+
|
|
15
|
+
const rawDeltaW = Math.max(0, outerWidth - innerWidth);
|
|
16
|
+
const rawDeltaH = Math.max(0, outerHeight - innerHeight);
|
|
17
|
+
const frameW = rawDeltaW > 400 ? 16 : Math.min(rawDeltaW, 120);
|
|
18
|
+
const frameH = rawDeltaH > 400 ? 88 : Math.min(rawDeltaH, 180);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
width: Math.max(320, outerWidth - frameW),
|
|
22
|
+
height: Math.max(240, outerHeight - frameH),
|
|
23
|
+
frameW,
|
|
24
|
+
frameH,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function probeWindowMetrics(profileId) {
|
|
29
|
+
const measured = await callAPI('evaluate', {
|
|
30
|
+
profileId,
|
|
31
|
+
script: '({ innerWidth: window.innerWidth, innerHeight: window.innerHeight, outerWidth: window.outerWidth, outerHeight: window.outerHeight })',
|
|
32
|
+
});
|
|
33
|
+
return measured?.result || {};
|
|
34
|
+
}
|
|
35
|
+
|
|
5
36
|
export async function handleWindowCommand(args) {
|
|
6
37
|
await ensureBrowserService();
|
|
7
38
|
|
|
@@ -24,36 +55,66 @@ export async function handleWindowCommand(args) {
|
|
|
24
55
|
const width = parseInt(args[widthIdx + 1]);
|
|
25
56
|
const height = parseInt(args[heightIdx + 1]);
|
|
26
57
|
const result = await callAPI('window:resize', { profileId, width, height });
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
|
|
59
|
+
const attempts = 4;
|
|
60
|
+
const settleMs = 120;
|
|
61
|
+
const tolerancePx = 3;
|
|
62
|
+
let measured = {};
|
|
63
|
+
let verified = {};
|
|
64
|
+
let viewport = null;
|
|
65
|
+
let targetViewportWidth = 1280;
|
|
66
|
+
let targetViewportHeight = 720;
|
|
67
|
+
let frameW = 16;
|
|
68
|
+
let frameH = 88;
|
|
69
|
+
let matched = false;
|
|
70
|
+
|
|
71
|
+
await sleep(settleMs);
|
|
72
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
73
|
+
measured = await probeWindowMetrics(profileId);
|
|
74
|
+
const target = computeTargetViewport(measured);
|
|
75
|
+
targetViewportWidth = target.width;
|
|
76
|
+
targetViewportHeight = target.height;
|
|
77
|
+
frameW = target.frameW;
|
|
78
|
+
frameH = target.frameH;
|
|
79
|
+
|
|
80
|
+
viewport = await callAPI('page:setViewport', {
|
|
81
|
+
profileId,
|
|
82
|
+
width: targetViewportWidth,
|
|
83
|
+
height: targetViewportHeight,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await sleep(settleMs);
|
|
87
|
+
verified = await probeWindowMetrics(profileId);
|
|
88
|
+
const dw = Math.abs((Number(verified?.innerWidth || 0)) - targetViewportWidth);
|
|
89
|
+
const dh = Math.abs((Number(verified?.innerHeight || 0)) - targetViewportHeight);
|
|
90
|
+
if (dw <= tolerancePx && dh <= tolerancePx) {
|
|
91
|
+
matched = true;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const measuredOuterWidth = Number(verified?.outerWidth);
|
|
97
|
+
const measuredOuterHeight = Number(verified?.outerHeight);
|
|
98
|
+
const savedWindow = setProfileWindowSize(
|
|
42
99
|
profileId,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
100
|
+
Number.isFinite(measuredOuterWidth) ? measuredOuterWidth : width,
|
|
101
|
+
Number.isFinite(measuredOuterHeight) ? measuredOuterHeight : height,
|
|
102
|
+
);
|
|
103
|
+
|
|
46
104
|
console.log(JSON.stringify({
|
|
47
105
|
ok: true,
|
|
48
106
|
profileId,
|
|
49
107
|
window: result,
|
|
50
|
-
measured: measured
|
|
108
|
+
measured: measured || null,
|
|
109
|
+
verified: verified || null,
|
|
51
110
|
targetViewport: {
|
|
52
111
|
width: targetViewportWidth,
|
|
53
112
|
height: targetViewportHeight,
|
|
54
113
|
frameW,
|
|
55
114
|
frameH,
|
|
115
|
+
matched,
|
|
56
116
|
},
|
|
117
|
+
profileWindow: savedWindow?.window || null,
|
|
57
118
|
viewport,
|
|
58
119
|
}, null, 2));
|
|
59
120
|
} else {
|
|
@@ -63,6 +63,45 @@ function isBlankUrl(url) {
|
|
|
63
63
|
return text === '' || text === 'about:blank' || text === 'about:blank#blocked';
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
function computeTargetViewportFromWindow(measured) {
|
|
67
|
+
const innerWidth = Math.max(320, Number(measured?.innerWidth || 0) || 0);
|
|
68
|
+
const innerHeight = Math.max(240, Number(measured?.innerHeight || 0) || 0);
|
|
69
|
+
const outerWidth = Math.max(320, Number(measured?.outerWidth || 0) || innerWidth);
|
|
70
|
+
const outerHeight = Math.max(240, Number(measured?.outerHeight || 0) || innerHeight);
|
|
71
|
+
const rawDeltaW = Math.max(0, outerWidth - innerWidth);
|
|
72
|
+
const rawDeltaH = Math.max(0, outerHeight - innerHeight);
|
|
73
|
+
const frameW = rawDeltaW > 400 ? 16 : Math.min(rawDeltaW, 120);
|
|
74
|
+
const frameH = rawDeltaH > 400 ? 88 : Math.min(rawDeltaH, 180);
|
|
75
|
+
return {
|
|
76
|
+
width: Math.max(320, outerWidth - frameW),
|
|
77
|
+
height: Math.max(240, outerHeight - frameH),
|
|
78
|
+
innerWidth,
|
|
79
|
+
innerHeight,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function probeWindowMetrics(profileId) {
|
|
84
|
+
const measured = await callAPI('evaluate', {
|
|
85
|
+
profileId,
|
|
86
|
+
script: '({ innerWidth: window.innerWidth, innerHeight: window.innerHeight, outerWidth: window.outerWidth, outerHeight: window.outerHeight })',
|
|
87
|
+
});
|
|
88
|
+
return measured?.result || {};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function syncViewportWithWindow(profileId, tolerancePx = 3) {
|
|
92
|
+
const measured = await probeWindowMetrics(profileId);
|
|
93
|
+
const target = computeTargetViewportFromWindow(measured);
|
|
94
|
+
const dw = Math.abs(target.innerWidth - target.width);
|
|
95
|
+
const dh = Math.abs(target.innerHeight - target.height);
|
|
96
|
+
if (dw <= tolerancePx && dh <= tolerancePx) return false;
|
|
97
|
+
await callAPI('page:setViewport', {
|
|
98
|
+
profileId,
|
|
99
|
+
width: target.width,
|
|
100
|
+
height: target.height,
|
|
101
|
+
});
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
66
105
|
function shouldExitMonitor(profileId) {
|
|
67
106
|
const info = getSessionInfo(profileId);
|
|
68
107
|
return !info || info.status !== 'active';
|
|
@@ -78,6 +117,8 @@ async function runMonitor(profileId, options = {}) {
|
|
|
78
117
|
const intervalMs = Math.max(500, Number(options.intervalMs) || 1200);
|
|
79
118
|
const emptyThreshold = Math.max(1, Number(options.emptyThreshold) || 2);
|
|
80
119
|
const blankThreshold = Math.max(1, Number(options.blankThreshold) || 3);
|
|
120
|
+
const viewportSyncIntervalMs = Math.max(500, Number(options.viewportSyncIntervalMs) || 1500);
|
|
121
|
+
let lastViewportSyncAt = 0;
|
|
81
122
|
|
|
82
123
|
let seenAnyPage = false;
|
|
83
124
|
let seenNonBlankPage = false;
|
|
@@ -140,6 +181,12 @@ async function runMonitor(profileId, options = {}) {
|
|
|
140
181
|
return;
|
|
141
182
|
}
|
|
142
183
|
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
if (pages.length > 0 && now - lastViewportSyncAt >= viewportSyncIntervalMs) {
|
|
186
|
+
lastViewportSyncAt = now;
|
|
187
|
+
await syncViewportWithWindow(profileId).catch(() => {});
|
|
188
|
+
}
|
|
189
|
+
|
|
143
190
|
await sleep(intervalMs);
|
|
144
191
|
}
|
|
145
192
|
}
|
package/src/utils/config.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import os from 'node:os';
|
|
|
6
6
|
export const CONFIG_DIR = path.join(os.homedir(), '.webauto');
|
|
7
7
|
export const PROFILES_DIR = path.join(CONFIG_DIR, 'profiles');
|
|
8
8
|
export const CONFIG_FILE = path.join(CONFIG_DIR, 'camo-cli.json');
|
|
9
|
+
export const PROFILE_META_FILE = 'camo-profile.json';
|
|
9
10
|
export const BROWSER_SERVICE_URL = process.env.WEBAUTO_BROWSER_URL || 'http://127.0.0.1:7704';
|
|
10
11
|
|
|
11
12
|
export function ensureDir(p) {
|
|
@@ -55,13 +56,13 @@ export function createProfile(profileId) {
|
|
|
55
56
|
if (!isValidProfileId(profileId)) {
|
|
56
57
|
throw new Error('Invalid profileId. Use only letters, numbers, dot, underscore, dash.');
|
|
57
58
|
}
|
|
58
|
-
const profileDir =
|
|
59
|
+
const profileDir = getProfileDir(profileId);
|
|
59
60
|
if (fs.existsSync(profileDir)) throw new Error(`Profile already exists: ${profileId}`);
|
|
60
61
|
ensureDir(profileDir);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
export function deleteProfile(profileId) {
|
|
64
|
-
const profileDir =
|
|
65
|
+
const profileDir = getProfileDir(profileId);
|
|
65
66
|
if (!fs.existsSync(profileDir)) throw new Error(`Profile not found: ${profileId}`);
|
|
66
67
|
fs.rmSync(profileDir, { recursive: true, force: true });
|
|
67
68
|
}
|
|
@@ -82,6 +83,61 @@ export function getDefaultProfile() {
|
|
|
82
83
|
return loadConfig().defaultProfile;
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
export function getProfileDir(profileId) {
|
|
87
|
+
return path.join(PROFILES_DIR, String(profileId || '').trim());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getProfileMetaFile(profileId) {
|
|
91
|
+
return path.join(getProfileDir(profileId), PROFILE_META_FILE);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function loadProfileMeta(profileId) {
|
|
95
|
+
if (!isValidProfileId(profileId)) return {};
|
|
96
|
+
return readJson(getProfileMetaFile(profileId)) || {};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function saveProfileMeta(profileId, patch) {
|
|
100
|
+
if (!isValidProfileId(profileId)) return null;
|
|
101
|
+
const profileDir = getProfileDir(profileId);
|
|
102
|
+
ensureDir(profileDir);
|
|
103
|
+
const current = loadProfileMeta(profileId);
|
|
104
|
+
const next = {
|
|
105
|
+
...current,
|
|
106
|
+
...patch,
|
|
107
|
+
updatedAt: Date.now(),
|
|
108
|
+
};
|
|
109
|
+
writeJson(getProfileMetaFile(profileId), next);
|
|
110
|
+
return next;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getProfileWindowSize(profileId) {
|
|
114
|
+
const meta = loadProfileMeta(profileId);
|
|
115
|
+
const width = Number(meta?.window?.width);
|
|
116
|
+
const height = Number(meta?.window?.height);
|
|
117
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
|
|
118
|
+
if (width < 320 || height < 240) return null;
|
|
119
|
+
return {
|
|
120
|
+
width: Math.floor(width),
|
|
121
|
+
height: Math.floor(height),
|
|
122
|
+
updatedAt: Number(meta?.window?.updatedAt) || Number(meta?.updatedAt) || null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function setProfileWindowSize(profileId, width, height) {
|
|
127
|
+
const parsedWidth = Number(width);
|
|
128
|
+
const parsedHeight = Number(height);
|
|
129
|
+
if (!Number.isFinite(parsedWidth) || !Number.isFinite(parsedHeight)) return null;
|
|
130
|
+
if (parsedWidth < 320 || parsedHeight < 240) return null;
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
return saveProfileMeta(profileId, {
|
|
133
|
+
window: {
|
|
134
|
+
width: Math.floor(parsedWidth),
|
|
135
|
+
height: Math.floor(parsedHeight),
|
|
136
|
+
updatedAt: now,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
85
141
|
const START_SCRIPT_REL = path.join('runtime', 'infra', 'utils', 'scripts', 'service', 'start-browser-service.mjs');
|
|
86
142
|
|
|
87
143
|
export function hasStartScript(root) {
|
package/src/utils/help.mjs
CHANGED
|
@@ -24,7 +24,7 @@ CONFIG:
|
|
|
24
24
|
|
|
25
25
|
BROWSER CONTROL:
|
|
26
26
|
init Ensure camoufox + ensure browser-service daemon
|
|
27
|
-
start [profileId] [--url <url>] [--headless]
|
|
27
|
+
start [profileId] [--url <url>] [--headless] [--width <w> --height <h>]
|
|
28
28
|
stop [profileId]
|
|
29
29
|
status [profileId]
|
|
30
30
|
list Alias of status
|
|
@@ -91,6 +91,7 @@ EXAMPLES:
|
|
|
91
91
|
camo profile create myprofile
|
|
92
92
|
camo profile default myprofile
|
|
93
93
|
camo start --url https://example.com
|
|
94
|
+
camo start myprofile --width 1920 --height 1020
|
|
94
95
|
camo goto https://www.xiaohongshu.com
|
|
95
96
|
camo scroll --down --amount 500
|
|
96
97
|
camo click "#search-input"
|