@web-auto/camo 0.1.4 → 0.1.6
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 +17 -4
- package/package.json +1 -1
- package/src/cli.mjs +13 -2
- package/src/commands/browser.mjs +445 -24
- package/src/commands/lifecycle.mjs +75 -34
- package/src/commands/window.mjs +81 -20
- package/src/lifecycle/session-registry.mjs +99 -3
- package/src/lifecycle/session-watchdog.mjs +65 -4
- package/src/utils/browser-service.mjs +14 -0
- package/src/utils/config.mjs +58 -2
- package/src/utils/help.mjs +14 -2
package/README.md
CHANGED
|
@@ -33,8 +33,11 @@ camo profile create myprofile
|
|
|
33
33
|
# Set as default
|
|
34
34
|
camo profile default myprofile
|
|
35
35
|
|
|
36
|
-
# Start browser
|
|
37
|
-
camo start --url https://example.com
|
|
36
|
+
# Start browser (with alias)
|
|
37
|
+
camo start --url https://example.com --alias main
|
|
38
|
+
|
|
39
|
+
# Start headless worker (auto-kill after idle timeout)
|
|
40
|
+
camo start worker-1 --headless --alias shard1 --idle-timeout 30m
|
|
38
41
|
|
|
39
42
|
# Navigate
|
|
40
43
|
camo goto https://www.xiaohongshu.com
|
|
@@ -68,15 +71,25 @@ camo create fingerprint --os <os> --region <region>
|
|
|
68
71
|
### Browser Control
|
|
69
72
|
|
|
70
73
|
```bash
|
|
71
|
-
camo start [profileId] [--url <url>] [--headless]
|
|
74
|
+
camo start [profileId] [--url <url>] [--headless] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]
|
|
72
75
|
camo stop [profileId]
|
|
76
|
+
camo stop --id <instanceId>
|
|
77
|
+
camo stop --alias <alias>
|
|
78
|
+
camo stop idle
|
|
79
|
+
camo stop all
|
|
73
80
|
camo status [profileId]
|
|
74
81
|
camo shutdown # Shutdown browser-service (all sessions)
|
|
75
82
|
```
|
|
76
83
|
|
|
84
|
+
`camo start` in headful mode now persists window size per profile and reuses that size on next start.
|
|
85
|
+
If no saved size exists, it defaults to near-fullscreen (full width, slight vertical reserve).
|
|
86
|
+
Use `--width/--height` to override and update the saved profile size.
|
|
87
|
+
For headless sessions, default idle timeout is `30m` (auto-stop on inactivity). Use `--idle-timeout` (e.g. `45m`, `1800s`, `0`) to customize.
|
|
88
|
+
|
|
77
89
|
### Lifecycle & Cleanup
|
|
78
90
|
|
|
79
91
|
```bash
|
|
92
|
+
camo instances # List global camoufox instances (live + orphaned + idle state)
|
|
80
93
|
camo sessions # List active browser sessions
|
|
81
94
|
camo cleanup [profileId] # Cleanup session (release lock + stop)
|
|
82
95
|
camo cleanup all # Cleanup all active sessions
|
|
@@ -328,7 +341,7 @@ Condition types:
|
|
|
328
341
|
Camo CLI persists session information locally:
|
|
329
342
|
|
|
330
343
|
- Sessions are registered in `~/.webauto/sessions/`
|
|
331
|
-
- On restart, `camo sessions` shows
|
|
344
|
+
- On restart, `camo sessions` / `camo instances` shows live + orphaned sessions
|
|
332
345
|
- Use `camo recover <profileId>` to reconnect or cleanup orphaned sessions
|
|
333
346
|
- Stale sessions (>7 days) are automatically cleaned up
|
|
334
347
|
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { listProfiles, getDefaultProfile, loadConfig, hasStartScript, setRepoRoot } from './utils/config.mjs';
|
|
5
|
-
import { ensureCamoufox, ensureBrowserService } from './utils/browser-service.mjs';
|
|
6
5
|
import { printHelp, printProfilesAndHint } from './utils/help.mjs';
|
|
7
6
|
import { handleProfileCommand } from './commands/profile.mjs';
|
|
8
7
|
import { handleInitCommand } from './commands/init.mjs';
|
|
@@ -24,7 +23,7 @@ import {
|
|
|
24
23
|
} from './commands/browser.mjs';
|
|
25
24
|
import {
|
|
26
25
|
handleCleanupCommand, handleForceStopCommand, handleLockCommand,
|
|
27
|
-
handleUnlockCommand, handleSessionsCommand
|
|
26
|
+
handleUnlockCommand, handleSessionsCommand, handleInstancesCommand
|
|
28
27
|
} from './commands/lifecycle.mjs';
|
|
29
28
|
import { handleSessionWatchdogCommand } from './lifecycle/session-watchdog.mjs';
|
|
30
29
|
import { safeAppendProgressEvent } from './events/progress-log.mjs';
|
|
@@ -55,6 +54,13 @@ function inferProfileId(cmd, args) {
|
|
|
55
54
|
'new-page', 'close-page', 'switch-page', 'list-pages',
|
|
56
55
|
'cleanup', 'force-stop', 'lock', 'unlock', 'sessions',
|
|
57
56
|
].includes(cmd)) {
|
|
57
|
+
if ((cmd === 'stop' || cmd === 'close') && (args.includes('--id') || args.includes('--alias'))) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const first = positionals[0] || null;
|
|
61
|
+
if ((cmd === 'stop' || cmd === 'close') && (first === 'all' || first === 'idle')) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
58
64
|
return positionals[0] || null;
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -212,6 +218,11 @@ async function main() {
|
|
|
212
218
|
return;
|
|
213
219
|
}
|
|
214
220
|
|
|
221
|
+
if (cmd === 'instances') {
|
|
222
|
+
await runTrackedCommand(cmd, args, () => handleInstancesCommand(args));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
215
226
|
if (cmd === 'container') {
|
|
216
227
|
await runTrackedCommand(cmd, args, () => handleContainerCommand(args));
|
|
217
228
|
return;
|
package/src/commands/browser.mjs
CHANGED
|
@@ -1,11 +1,238 @@
|
|
|
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
|
-
import { acquireLock, releaseLock,
|
|
6
|
-
import {
|
|
10
|
+
import { acquireLock, releaseLock, cleanupStaleLocks } from '../lifecycle/lock.mjs';
|
|
11
|
+
import {
|
|
12
|
+
registerSession,
|
|
13
|
+
updateSession,
|
|
14
|
+
getSessionInfo,
|
|
15
|
+
unregisterSession,
|
|
16
|
+
listRegisteredSessions,
|
|
17
|
+
markSessionClosed,
|
|
18
|
+
cleanupStaleSessions,
|
|
19
|
+
resolveSessionTarget,
|
|
20
|
+
isSessionAliasTaken,
|
|
21
|
+
} from '../lifecycle/session-registry.mjs';
|
|
7
22
|
import { startSessionWatchdog, stopAllSessionWatchdogs, stopSessionWatchdog } from '../lifecycle/session-watchdog.mjs';
|
|
8
23
|
|
|
24
|
+
const START_WINDOW_MIN_WIDTH = 960;
|
|
25
|
+
const START_WINDOW_MIN_HEIGHT = 700;
|
|
26
|
+
const START_WINDOW_MAX_RESERVE = 240;
|
|
27
|
+
const START_WINDOW_DEFAULT_RESERVE = 72;
|
|
28
|
+
const DEFAULT_HEADLESS_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
29
|
+
|
|
30
|
+
function sleep(ms) {
|
|
31
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseNumber(value, fallback = 0) {
|
|
35
|
+
const parsed = Number(value);
|
|
36
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clamp(value, min, max) {
|
|
40
|
+
return Math.min(Math.max(value, min), max);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readFlagValue(args, names) {
|
|
44
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
45
|
+
if (!names.includes(args[i])) continue;
|
|
46
|
+
const value = args[i + 1];
|
|
47
|
+
if (!value || String(value).startsWith('-')) return null;
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseDurationMs(raw, fallbackMs) {
|
|
54
|
+
if (raw === undefined || raw === null || String(raw).trim() === '') return fallbackMs;
|
|
55
|
+
const text = String(raw).trim().toLowerCase();
|
|
56
|
+
if (text === '0' || text === 'off' || text === 'none' || text === 'disable' || text === 'disabled') return 0;
|
|
57
|
+
const matched = text.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
|
|
58
|
+
if (!matched) {
|
|
59
|
+
throw new Error('Invalid --idle-timeout. Use forms like 30m, 1800s, 5000ms, 1h, 0');
|
|
60
|
+
}
|
|
61
|
+
const value = Number(matched[1]);
|
|
62
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
63
|
+
throw new Error('Invalid --idle-timeout value');
|
|
64
|
+
}
|
|
65
|
+
const unit = matched[2] || 'm';
|
|
66
|
+
const factor = unit === 'h' ? 3600000 : unit === 'm' ? 60000 : unit === 's' ? 1000 : 1;
|
|
67
|
+
return Math.floor(value * factor);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateAlias(alias) {
|
|
71
|
+
const text = String(alias || '').trim();
|
|
72
|
+
if (!text) return null;
|
|
73
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(text)) {
|
|
74
|
+
throw new Error('Invalid --alias. Use only letters, numbers, dot, underscore, dash.');
|
|
75
|
+
}
|
|
76
|
+
return text.slice(0, 64);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatDurationMs(ms) {
|
|
80
|
+
const value = Number(ms);
|
|
81
|
+
if (!Number.isFinite(value) || value <= 0) return 'disabled';
|
|
82
|
+
if (value % 3600000 === 0) return `${value / 3600000}h`;
|
|
83
|
+
if (value % 60000 === 0) return `${value / 60000}m`;
|
|
84
|
+
if (value % 1000 === 0) return `${value / 1000}s`;
|
|
85
|
+
return `${value}ms`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function computeIdleState(session, now = Date.now()) {
|
|
89
|
+
const headless = session?.headless === true;
|
|
90
|
+
const timeoutMs = headless
|
|
91
|
+
? (Number.isFinite(Number(session?.idleTimeoutMs)) ? Math.max(0, Number(session.idleTimeoutMs)) : DEFAULT_HEADLESS_IDLE_TIMEOUT_MS)
|
|
92
|
+
: 0;
|
|
93
|
+
const lastAt = Number(session?.lastActivityAt || session?.lastSeen || session?.startTime || now);
|
|
94
|
+
const idleMs = Math.max(0, now - (Number.isFinite(lastAt) ? lastAt : now));
|
|
95
|
+
const idle = headless && timeoutMs > 0 && idleMs >= timeoutMs;
|
|
96
|
+
return { headless, timeoutMs, idleMs, idle };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function stopAndCleanupProfile(profileId, options = {}) {
|
|
100
|
+
const id = String(profileId || '').trim();
|
|
101
|
+
if (!id) return { profileId: id, ok: false, error: 'profile_required' };
|
|
102
|
+
const force = options.force === true;
|
|
103
|
+
const serviceUp = options.serviceUp === true;
|
|
104
|
+
let result = null;
|
|
105
|
+
let error = null;
|
|
106
|
+
if (serviceUp) {
|
|
107
|
+
try {
|
|
108
|
+
result = await callAPI('stop', force ? { profileId: id, force: true } : { profileId: id });
|
|
109
|
+
} catch (err) {
|
|
110
|
+
error = err;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
stopSessionWatchdog(id);
|
|
114
|
+
releaseLock(id);
|
|
115
|
+
markSessionClosed(id);
|
|
116
|
+
return {
|
|
117
|
+
profileId: id,
|
|
118
|
+
ok: !error,
|
|
119
|
+
serviceUp,
|
|
120
|
+
result,
|
|
121
|
+
error: error ? (error.message || String(error)) : null,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function computeTargetViewportFromWindowMetrics(measured) {
|
|
126
|
+
const innerWidth = Math.max(320, parseNumber(measured?.innerWidth, 0));
|
|
127
|
+
const innerHeight = Math.max(240, parseNumber(measured?.innerHeight, 0));
|
|
128
|
+
const outerWidth = Math.max(320, parseNumber(measured?.outerWidth, innerWidth));
|
|
129
|
+
const outerHeight = Math.max(240, parseNumber(measured?.outerHeight, innerHeight));
|
|
130
|
+
|
|
131
|
+
const rawDeltaW = Math.max(0, outerWidth - innerWidth);
|
|
132
|
+
const rawDeltaH = Math.max(0, outerHeight - innerHeight);
|
|
133
|
+
const frameW = rawDeltaW > 400 ? 16 : Math.min(rawDeltaW, 120);
|
|
134
|
+
const frameH = rawDeltaH > 400 ? 88 : Math.min(rawDeltaH, 180);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
width: Math.max(320, outerWidth - frameW),
|
|
138
|
+
height: Math.max(240, outerHeight - frameH),
|
|
139
|
+
frameW,
|
|
140
|
+
frameH,
|
|
141
|
+
innerWidth,
|
|
142
|
+
innerHeight,
|
|
143
|
+
outerWidth,
|
|
144
|
+
outerHeight,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function computeStartWindowSize(metrics, options = {}) {
|
|
149
|
+
const display = metrics?.metrics || metrics || {};
|
|
150
|
+
const reserveFromEnv = parseNumber(process.env.CAMO_DEFAULT_WINDOW_VERTICAL_RESERVE, START_WINDOW_DEFAULT_RESERVE);
|
|
151
|
+
const reserve = clamp(
|
|
152
|
+
parseNumber(options.reservePx, reserveFromEnv),
|
|
153
|
+
0,
|
|
154
|
+
START_WINDOW_MAX_RESERVE,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const workWidth = parseNumber(display.workWidth, 0);
|
|
158
|
+
const workHeight = parseNumber(display.workHeight, 0);
|
|
159
|
+
const width = parseNumber(display.width, 0);
|
|
160
|
+
const height = parseNumber(display.height, 0);
|
|
161
|
+
const baseW = Math.floor(workWidth > 0 ? workWidth : width);
|
|
162
|
+
const baseH = Math.floor(workHeight > 0 ? workHeight : height);
|
|
163
|
+
|
|
164
|
+
if (baseW <= 0 || baseH <= 0) {
|
|
165
|
+
return {
|
|
166
|
+
width: 1920,
|
|
167
|
+
height: 1000,
|
|
168
|
+
reservePx: reserve,
|
|
169
|
+
source: 'fallback',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
width: Math.max(START_WINDOW_MIN_WIDTH, baseW),
|
|
175
|
+
height: Math.max(START_WINDOW_MIN_HEIGHT, baseH - reserve),
|
|
176
|
+
reservePx: reserve,
|
|
177
|
+
source: workWidth > 0 || workHeight > 0 ? 'workArea' : 'screen',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function probeWindowMetrics(profileId) {
|
|
182
|
+
const measured = await callAPI('evaluate', {
|
|
183
|
+
profileId,
|
|
184
|
+
script: '({ innerWidth: window.innerWidth, innerHeight: window.innerHeight, outerWidth: window.outerWidth, outerHeight: window.outerHeight })',
|
|
185
|
+
});
|
|
186
|
+
return measured?.result || {};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function syncWindowViewportAfterResize(profileId, width, height, options = {}) {
|
|
190
|
+
const settleMs = Math.max(40, parseNumber(options.settleMs, 120));
|
|
191
|
+
const attempts = Math.max(1, Math.min(8, Math.floor(parseNumber(options.attempts, 4))));
|
|
192
|
+
const tolerancePx = Math.max(0, parseNumber(options.tolerancePx, 3));
|
|
193
|
+
|
|
194
|
+
const windowResult = await callAPI('window:resize', { profileId, width, height });
|
|
195
|
+
await sleep(settleMs);
|
|
196
|
+
|
|
197
|
+
let measured = {};
|
|
198
|
+
let verified = {};
|
|
199
|
+
let viewport = null;
|
|
200
|
+
let matched = false;
|
|
201
|
+
let target = { width: 1280, height: 720, frameW: 16, frameH: 88 };
|
|
202
|
+
|
|
203
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
204
|
+
measured = await probeWindowMetrics(profileId);
|
|
205
|
+
target = computeTargetViewportFromWindowMetrics(measured);
|
|
206
|
+
viewport = await callAPI('page:setViewport', {
|
|
207
|
+
profileId,
|
|
208
|
+
width: target.width,
|
|
209
|
+
height: target.height,
|
|
210
|
+
});
|
|
211
|
+
await sleep(settleMs);
|
|
212
|
+
verified = await probeWindowMetrics(profileId);
|
|
213
|
+
const dw = Math.abs(parseNumber(verified?.innerWidth, 0) - target.width);
|
|
214
|
+
const dh = Math.abs(parseNumber(verified?.innerHeight, 0) - target.height);
|
|
215
|
+
if (dw <= tolerancePx && dh <= tolerancePx) {
|
|
216
|
+
matched = true;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
window: windowResult,
|
|
223
|
+
measured,
|
|
224
|
+
verified,
|
|
225
|
+
targetViewport: {
|
|
226
|
+
width: target.width,
|
|
227
|
+
height: target.height,
|
|
228
|
+
frameW: target.frameW,
|
|
229
|
+
frameH: target.frameH,
|
|
230
|
+
matched,
|
|
231
|
+
},
|
|
232
|
+
viewport,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
9
236
|
export async function handleStartCommand(args) {
|
|
10
237
|
ensureCamoufox();
|
|
11
238
|
await ensureBrowserService();
|
|
@@ -14,6 +241,22 @@ export async function handleStartCommand(args) {
|
|
|
14
241
|
|
|
15
242
|
const urlIdx = args.indexOf('--url');
|
|
16
243
|
const explicitUrl = urlIdx >= 0 ? args[urlIdx + 1] : undefined;
|
|
244
|
+
const widthIdx = args.indexOf('--width');
|
|
245
|
+
const heightIdx = args.indexOf('--height');
|
|
246
|
+
const explicitWidth = widthIdx >= 0 ? parseNumber(args[widthIdx + 1], NaN) : NaN;
|
|
247
|
+
const explicitHeight = heightIdx >= 0 ? parseNumber(args[heightIdx + 1], NaN) : NaN;
|
|
248
|
+
const hasExplicitWidth = Number.isFinite(explicitWidth);
|
|
249
|
+
const hasExplicitHeight = Number.isFinite(explicitHeight);
|
|
250
|
+
const alias = validateAlias(readFlagValue(args, ['--alias']));
|
|
251
|
+
const idleTimeoutRaw = readFlagValue(args, ['--idle-timeout']);
|
|
252
|
+
const parsedIdleTimeoutMs = parseDurationMs(idleTimeoutRaw, DEFAULT_HEADLESS_IDLE_TIMEOUT_MS);
|
|
253
|
+
if (hasExplicitWidth !== hasExplicitHeight) {
|
|
254
|
+
throw new Error('Usage: camo start [profileId] [--url <url>] [--headless] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]');
|
|
255
|
+
}
|
|
256
|
+
if ((hasExplicitWidth && explicitWidth < START_WINDOW_MIN_WIDTH) || (hasExplicitHeight && explicitHeight < START_WINDOW_MIN_HEIGHT)) {
|
|
257
|
+
throw new Error(`Window size too small. Minimum is ${START_WINDOW_MIN_WIDTH}x${START_WINDOW_MIN_HEIGHT}`);
|
|
258
|
+
}
|
|
259
|
+
const hasExplicitWindowSize = hasExplicitWidth && hasExplicitHeight;
|
|
17
260
|
const profileSet = new Set(listProfiles());
|
|
18
261
|
let implicitUrl;
|
|
19
262
|
|
|
@@ -21,6 +264,8 @@ export async function handleStartCommand(args) {
|
|
|
21
264
|
for (let i = 1; i < args.length; i++) {
|
|
22
265
|
const arg = args[i];
|
|
23
266
|
if (arg === '--url') { i++; continue; }
|
|
267
|
+
if (arg === '--width' || arg === '--height') { i++; continue; }
|
|
268
|
+
if (arg === '--alias' || arg === '--idle-timeout') { i++; continue; }
|
|
24
269
|
if (arg === '--headless') continue;
|
|
25
270
|
if (arg.startsWith('--')) continue;
|
|
26
271
|
|
|
@@ -39,23 +284,45 @@ export async function handleStartCommand(args) {
|
|
|
39
284
|
throw new Error('No default profile set. Run: camo profile default <profileId>');
|
|
40
285
|
}
|
|
41
286
|
}
|
|
287
|
+
if (alias && isSessionAliasTaken(alias, profileId)) {
|
|
288
|
+
throw new Error(`Alias is already in use: ${alias}`);
|
|
289
|
+
}
|
|
42
290
|
|
|
43
291
|
// Check for existing session in browser service
|
|
44
292
|
const existing = await getSessionByProfile(profileId);
|
|
45
293
|
if (existing) {
|
|
46
294
|
// Session exists in browser service - update registry and lock
|
|
47
295
|
acquireLock(profileId, { sessionId: existing.session_id || existing.profileId });
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
296
|
+
const saved = getSessionInfo(profileId);
|
|
297
|
+
const record = saved
|
|
298
|
+
? updateSession(profileId, {
|
|
299
|
+
sessionId: existing.session_id || existing.profileId,
|
|
300
|
+
url: existing.current_url,
|
|
301
|
+
mode: existing.mode,
|
|
302
|
+
alias: alias || saved.alias || null,
|
|
303
|
+
})
|
|
304
|
+
: registerSession(profileId, {
|
|
305
|
+
sessionId: existing.session_id || existing.profileId,
|
|
306
|
+
url: existing.current_url,
|
|
307
|
+
mode: existing.mode,
|
|
308
|
+
alias: alias || null,
|
|
309
|
+
});
|
|
310
|
+
const idleState = computeIdleState(record);
|
|
53
311
|
console.log(JSON.stringify({
|
|
54
312
|
ok: true,
|
|
55
313
|
sessionId: existing.session_id || existing.profileId,
|
|
314
|
+
instanceId: record.instanceId,
|
|
56
315
|
profileId,
|
|
57
316
|
message: 'Session already running',
|
|
58
317
|
url: existing.current_url,
|
|
318
|
+
alias: record.alias || null,
|
|
319
|
+
idleTimeoutMs: idleState.timeoutMs,
|
|
320
|
+
idleTimeout: formatDurationMs(idleState.timeoutMs),
|
|
321
|
+
closeHint: {
|
|
322
|
+
byProfile: `camo stop ${profileId}`,
|
|
323
|
+
byId: `camo stop --id ${record.instanceId}`,
|
|
324
|
+
byAlias: record.alias ? `camo stop --alias ${record.alias}` : null,
|
|
325
|
+
},
|
|
59
326
|
}, null, 2));
|
|
60
327
|
startSessionWatchdog(profileId);
|
|
61
328
|
return;
|
|
@@ -71,6 +338,7 @@ export async function handleStartCommand(args) {
|
|
|
71
338
|
}
|
|
72
339
|
|
|
73
340
|
const headless = args.includes('--headless');
|
|
341
|
+
const idleTimeoutMs = headless ? parsedIdleTimeoutMs : 0;
|
|
74
342
|
const targetUrl = explicitUrl || implicitUrl;
|
|
75
343
|
const result = await callAPI('start', {
|
|
76
344
|
profileId,
|
|
@@ -81,35 +349,188 @@ export async function handleStartCommand(args) {
|
|
|
81
349
|
if (result?.ok) {
|
|
82
350
|
const sessionId = result.sessionId || result.profileId || profileId;
|
|
83
351
|
acquireLock(profileId, { sessionId });
|
|
84
|
-
registerSession(profileId, {
|
|
352
|
+
const record = registerSession(profileId, {
|
|
85
353
|
sessionId,
|
|
86
354
|
url: targetUrl,
|
|
87
355
|
headless,
|
|
356
|
+
alias,
|
|
357
|
+
idleTimeoutMs,
|
|
358
|
+
lastAction: 'start',
|
|
88
359
|
});
|
|
89
360
|
startSessionWatchdog(profileId);
|
|
361
|
+
result.instanceId = record.instanceId;
|
|
362
|
+
result.alias = record.alias || null;
|
|
363
|
+
result.idleTimeoutMs = idleTimeoutMs;
|
|
364
|
+
result.idleTimeout = formatDurationMs(idleTimeoutMs);
|
|
365
|
+
result.closeHint = {
|
|
366
|
+
byProfile: `camo stop ${profileId}`,
|
|
367
|
+
byId: `camo stop --id ${record.instanceId}`,
|
|
368
|
+
byAlias: record.alias ? `camo stop --alias ${record.alias}` : null,
|
|
369
|
+
all: 'camo close all',
|
|
370
|
+
};
|
|
371
|
+
result.message = headless
|
|
372
|
+
? `Started headless session. Idle timeout: ${formatDurationMs(idleTimeoutMs)}`
|
|
373
|
+
: 'Started session. Remember to stop it when finished.';
|
|
374
|
+
|
|
375
|
+
if (!headless) {
|
|
376
|
+
let windowTarget = null;
|
|
377
|
+
if (hasExplicitWindowSize) {
|
|
378
|
+
windowTarget = {
|
|
379
|
+
width: Math.floor(explicitWidth),
|
|
380
|
+
height: Math.floor(explicitHeight),
|
|
381
|
+
source: 'explicit',
|
|
382
|
+
};
|
|
383
|
+
} else {
|
|
384
|
+
const rememberedWindow = getProfileWindowSize(profileId);
|
|
385
|
+
if (rememberedWindow) {
|
|
386
|
+
windowTarget = {
|
|
387
|
+
width: rememberedWindow.width,
|
|
388
|
+
height: rememberedWindow.height,
|
|
389
|
+
source: 'profile',
|
|
390
|
+
updatedAt: rememberedWindow.updatedAt,
|
|
391
|
+
};
|
|
392
|
+
} else {
|
|
393
|
+
const display = await callAPI('system:display', {}).catch(() => null);
|
|
394
|
+
windowTarget = computeStartWindowSize(display);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
result.startWindow = {
|
|
399
|
+
width: windowTarget.width,
|
|
400
|
+
height: windowTarget.height,
|
|
401
|
+
source: windowTarget.source,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const syncResult = await syncWindowViewportAfterResize(
|
|
405
|
+
profileId,
|
|
406
|
+
windowTarget.width,
|
|
407
|
+
windowTarget.height,
|
|
408
|
+
).catch((err) => ({ error: err?.message || String(err) }));
|
|
409
|
+
result.windowSync = syncResult;
|
|
410
|
+
|
|
411
|
+
const measuredOuterWidth = Number(syncResult?.verified?.outerWidth);
|
|
412
|
+
const measuredOuterHeight = Number(syncResult?.verified?.outerHeight);
|
|
413
|
+
const savedWindow = setProfileWindowSize(
|
|
414
|
+
profileId,
|
|
415
|
+
Number.isFinite(measuredOuterWidth) ? measuredOuterWidth : windowTarget.width,
|
|
416
|
+
Number.isFinite(measuredOuterHeight) ? measuredOuterHeight : windowTarget.height,
|
|
417
|
+
);
|
|
418
|
+
result.profileWindow = savedWindow?.window || null;
|
|
419
|
+
}
|
|
90
420
|
}
|
|
91
421
|
console.log(JSON.stringify(result, null, 2));
|
|
92
422
|
}
|
|
93
423
|
|
|
94
424
|
export async function handleStopCommand(args) {
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
425
|
+
const rawTarget = String(args[1] || '').trim();
|
|
426
|
+
const target = rawTarget.toLowerCase();
|
|
427
|
+
const idTarget = readFlagValue(args, ['--id']);
|
|
428
|
+
const aliasTarget = readFlagValue(args, ['--alias']);
|
|
429
|
+
const stopIdle = target === 'idle' || args.includes('--idle');
|
|
430
|
+
const stopAll = target === 'all';
|
|
431
|
+
const serviceUp = await checkBrowserService();
|
|
98
432
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
433
|
+
if (stopAll) {
|
|
434
|
+
let liveSessions = [];
|
|
435
|
+
if (serviceUp) {
|
|
436
|
+
try {
|
|
437
|
+
const status = await callAPI('getStatus', {});
|
|
438
|
+
liveSessions = Array.isArray(status?.sessions) ? status.sessions : [];
|
|
439
|
+
} catch {
|
|
440
|
+
// Ignore and fallback to local registry.
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const profileSet = new Set(liveSessions.map((item) => String(item?.profileId || '').trim()).filter(Boolean));
|
|
444
|
+
for (const session of listRegisteredSessions()) {
|
|
445
|
+
if (String(session?.status || '').trim() === 'closed') continue;
|
|
446
|
+
const profileId = String(session?.profileId || '').trim();
|
|
447
|
+
if (profileId) profileSet.add(profileId);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const results = [];
|
|
451
|
+
for (const profileId of profileSet) {
|
|
452
|
+
// eslint-disable-next-line no-await-in-loop
|
|
453
|
+
results.push(await stopAndCleanupProfile(profileId, { serviceUp }));
|
|
454
|
+
}
|
|
455
|
+
console.log(JSON.stringify({
|
|
456
|
+
ok: true,
|
|
457
|
+
mode: 'all',
|
|
458
|
+
serviceUp,
|
|
459
|
+
closed: results.filter((item) => item.ok).length,
|
|
460
|
+
failed: results.filter((item) => !item.ok).length,
|
|
461
|
+
results,
|
|
462
|
+
}, null, 2));
|
|
463
|
+
return;
|
|
109
464
|
}
|
|
110
465
|
|
|
111
|
-
if (
|
|
112
|
-
|
|
466
|
+
if (stopIdle) {
|
|
467
|
+
const now = Date.now();
|
|
468
|
+
const idleTargets = listRegisteredSessions()
|
|
469
|
+
.filter((item) => String(item?.status || '').trim() === 'active')
|
|
470
|
+
.map((item) => ({ session: item, idle: computeIdleState(item, now) }))
|
|
471
|
+
.filter((item) => item.idle.idle)
|
|
472
|
+
.map((item) => item.session.profileId);
|
|
473
|
+
const results = [];
|
|
474
|
+
for (const profileId of idleTargets) {
|
|
475
|
+
// eslint-disable-next-line no-await-in-loop
|
|
476
|
+
results.push(await stopAndCleanupProfile(profileId, { serviceUp }));
|
|
477
|
+
}
|
|
478
|
+
console.log(JSON.stringify({
|
|
479
|
+
ok: true,
|
|
480
|
+
mode: 'idle',
|
|
481
|
+
serviceUp,
|
|
482
|
+
targetCount: idleTargets.length,
|
|
483
|
+
closed: results.filter((item) => item.ok).length,
|
|
484
|
+
failed: results.filter((item) => !item.ok).length,
|
|
485
|
+
results,
|
|
486
|
+
}, null, 2));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
let profileId = null;
|
|
491
|
+
let resolvedBy = 'profile';
|
|
492
|
+
if (idTarget) {
|
|
493
|
+
const resolved = resolveSessionTarget(idTarget);
|
|
494
|
+
if (!resolved) throw new Error(`No session found for instance id: ${idTarget}`);
|
|
495
|
+
profileId = resolved.profileId;
|
|
496
|
+
resolvedBy = resolved.reason;
|
|
497
|
+
} else if (aliasTarget) {
|
|
498
|
+
const resolved = resolveSessionTarget(aliasTarget);
|
|
499
|
+
if (!resolved) throw new Error(`No session found for alias: ${aliasTarget}`);
|
|
500
|
+
profileId = resolved.profileId;
|
|
501
|
+
resolvedBy = resolved.reason;
|
|
502
|
+
} else {
|
|
503
|
+
const positional = args.slice(1).find((arg) => arg && !String(arg).startsWith('--')) || null;
|
|
504
|
+
if (positional) {
|
|
505
|
+
const resolved = resolveSessionTarget(positional);
|
|
506
|
+
if (resolved) {
|
|
507
|
+
profileId = resolved.profileId;
|
|
508
|
+
resolvedBy = resolved.reason;
|
|
509
|
+
} else {
|
|
510
|
+
profileId = positional;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (!profileId) {
|
|
516
|
+
profileId = getDefaultProfile();
|
|
517
|
+
}
|
|
518
|
+
if (!profileId) {
|
|
519
|
+
throw new Error('Usage: camo stop [profileId] | camo stop --id <instanceId> | camo stop --alias <alias> | camo stop all | camo stop idle');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const result = await stopAndCleanupProfile(profileId, { serviceUp });
|
|
523
|
+
if (!result.ok && serviceUp) {
|
|
524
|
+
throw new Error(result.error || `stop failed for profile: ${profileId}`);
|
|
525
|
+
}
|
|
526
|
+
console.log(JSON.stringify({
|
|
527
|
+
ok: true,
|
|
528
|
+
profileId,
|
|
529
|
+
resolvedBy,
|
|
530
|
+
serviceUp,
|
|
531
|
+
warning: (!serviceUp && !result.ok) ? result.error : null,
|
|
532
|
+
result: result.result || null,
|
|
533
|
+
}, null, 2));
|
|
113
534
|
}
|
|
114
535
|
|
|
115
536
|
export async function handleStatusCommand(args) {
|
|
@@ -5,13 +5,81 @@
|
|
|
5
5
|
import { getDefaultProfile } from '../utils/config.mjs';
|
|
6
6
|
import { callAPI, ensureBrowserService, checkBrowserService } from '../utils/browser-service.mjs';
|
|
7
7
|
import { resolveProfileId } from '../utils/args.mjs';
|
|
8
|
-
import { acquireLock, getLockInfo, releaseLock,
|
|
8
|
+
import { acquireLock, getLockInfo, releaseLock, cleanupStaleLocks, listActiveLocks } from '../lifecycle/lock.mjs';
|
|
9
9
|
import {
|
|
10
10
|
getSessionInfo, unregisterSession, markSessionClosed, cleanupStaleSessions,
|
|
11
|
-
listRegisteredSessions,
|
|
11
|
+
listRegisteredSessions, updateSession
|
|
12
12
|
} from '../lifecycle/session-registry.mjs';
|
|
13
13
|
import { stopAllSessionWatchdogs, stopSessionWatchdog } from '../lifecycle/session-watchdog.mjs';
|
|
14
14
|
|
|
15
|
+
const DEFAULT_HEADLESS_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
16
|
+
|
|
17
|
+
function computeIdleState(session, now = Date.now()) {
|
|
18
|
+
const headless = session?.headless === true;
|
|
19
|
+
const timeoutMs = headless
|
|
20
|
+
? (Number.isFinite(Number(session?.idleTimeoutMs)) ? Math.max(0, Number(session.idleTimeoutMs)) : DEFAULT_HEADLESS_IDLE_TIMEOUT_MS)
|
|
21
|
+
: 0;
|
|
22
|
+
const lastAt = Number(session?.lastActivityAt || session?.lastSeen || session?.startTime || now);
|
|
23
|
+
const idleMs = Math.max(0, now - (Number.isFinite(lastAt) ? lastAt : now));
|
|
24
|
+
const idle = headless && timeoutMs > 0 && idleMs >= timeoutMs;
|
|
25
|
+
return { headless, timeoutMs, idleMs, idle };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildMergedSessionRows(liveSessions, registeredSessions) {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const regMap = new Map(registeredSessions.map((item) => [item.profileId, item]));
|
|
31
|
+
const rows = [];
|
|
32
|
+
const liveMap = new Map(liveSessions.map((item) => [item.profileId, item]));
|
|
33
|
+
|
|
34
|
+
for (const live of liveSessions) {
|
|
35
|
+
const reg = regMap.get(live.profileId);
|
|
36
|
+
const idle = computeIdleState(reg || {}, now);
|
|
37
|
+
rows.push({
|
|
38
|
+
profileId: live.profileId,
|
|
39
|
+
sessionId: live.session_id || live.profileId,
|
|
40
|
+
instanceId: reg?.instanceId || live.session_id || live.profileId,
|
|
41
|
+
alias: reg?.alias || null,
|
|
42
|
+
url: live.current_url,
|
|
43
|
+
mode: live.mode,
|
|
44
|
+
headless: idle.headless,
|
|
45
|
+
idleTimeoutMs: idle.timeoutMs,
|
|
46
|
+
idleMs: idle.idleMs,
|
|
47
|
+
idle: idle.idle,
|
|
48
|
+
live: true,
|
|
49
|
+
registered: !!reg,
|
|
50
|
+
registryStatus: reg?.status,
|
|
51
|
+
lastSeen: reg?.lastSeen || null,
|
|
52
|
+
lastActivityAt: reg?.lastActivityAt || null,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const reg of registeredSessions) {
|
|
57
|
+
if (liveMap.has(reg.profileId)) continue;
|
|
58
|
+
if (reg.status === 'closed') continue;
|
|
59
|
+
const idle = computeIdleState(reg, now);
|
|
60
|
+
rows.push({
|
|
61
|
+
profileId: reg.profileId,
|
|
62
|
+
sessionId: reg.sessionId || reg.profileId,
|
|
63
|
+
instanceId: reg.instanceId || reg.sessionId || reg.profileId,
|
|
64
|
+
alias: reg.alias || null,
|
|
65
|
+
url: reg.url || null,
|
|
66
|
+
mode: reg.mode || null,
|
|
67
|
+
headless: idle.headless,
|
|
68
|
+
idleTimeoutMs: idle.timeoutMs,
|
|
69
|
+
idleMs: idle.idleMs,
|
|
70
|
+
idle: idle.idle,
|
|
71
|
+
live: false,
|
|
72
|
+
orphaned: true,
|
|
73
|
+
needsRecovery: reg.status === 'active',
|
|
74
|
+
registered: true,
|
|
75
|
+
registryStatus: reg.status,
|
|
76
|
+
lastSeen: reg.lastSeen || null,
|
|
77
|
+
lastActivityAt: reg.lastActivityAt || null,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return rows;
|
|
81
|
+
}
|
|
82
|
+
|
|
15
83
|
export async function handleCleanupCommand(args) {
|
|
16
84
|
const sub = args[1];
|
|
17
85
|
|
|
@@ -158,38 +226,7 @@ export async function handleSessionsCommand(args) {
|
|
|
158
226
|
} catch {}
|
|
159
227
|
}
|
|
160
228
|
|
|
161
|
-
|
|
162
|
-
const liveMap = new Map(liveSessions.map(s => [s.profileId, s]));
|
|
163
|
-
|
|
164
|
-
// Merge live and registered sessions
|
|
165
|
-
const merged = [];
|
|
166
|
-
|
|
167
|
-
// Add all live sessions
|
|
168
|
-
for (const live of liveSessions) {
|
|
169
|
-
const reg = getSessionInfo(live.profileId);
|
|
170
|
-
merged.push({
|
|
171
|
-
profileId: live.profileId,
|
|
172
|
-
sessionId: live.session_id || live.profileId,
|
|
173
|
-
url: live.current_url,
|
|
174
|
-
mode: live.mode,
|
|
175
|
-
live: true,
|
|
176
|
-
registered: !!reg,
|
|
177
|
-
registryStatus: reg?.status,
|
|
178
|
-
lastSeen: reg?.lastSeen,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Add registered sessions that are not live (orphaned)
|
|
183
|
-
for (const reg of registeredSessions) {
|
|
184
|
-
if (!liveMap.has(reg.profileId) && reg.status !== 'closed') {
|
|
185
|
-
merged.push({
|
|
186
|
-
...reg,
|
|
187
|
-
live: false,
|
|
188
|
-
orphaned: true,
|
|
189
|
-
needsRecovery: reg.status === 'active',
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
}
|
|
229
|
+
const merged = buildMergedSessionRows(liveSessions, registeredSessions);
|
|
193
230
|
|
|
194
231
|
console.log(JSON.stringify({
|
|
195
232
|
ok: true,
|
|
@@ -202,6 +239,10 @@ export async function handleSessionsCommand(args) {
|
|
|
202
239
|
}, null, 2));
|
|
203
240
|
}
|
|
204
241
|
|
|
242
|
+
export async function handleInstancesCommand(args) {
|
|
243
|
+
await handleSessionsCommand(args);
|
|
244
|
+
}
|
|
245
|
+
|
|
205
246
|
export async function handleRecoverCommand(args) {
|
|
206
247
|
const profileId = resolveProfileId(args, 1, getDefaultProfile);
|
|
207
248
|
if (!profileId) throw new Error('Usage: camo recover [profileId]');
|
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 {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import os from 'node:os';
|
|
9
|
+
import crypto from 'node:crypto';
|
|
9
10
|
|
|
10
11
|
const SESSION_DIR = path.join(os.homedir(), '.webauto', 'sessions');
|
|
11
12
|
|
|
@@ -15,6 +16,25 @@ function ensureSessionDir() {
|
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
function normalizeAlias(value) {
|
|
20
|
+
const text = String(value || '').trim();
|
|
21
|
+
if (!text) return null;
|
|
22
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(text)) {
|
|
23
|
+
throw new Error('Invalid alias. Use only letters, numbers, dot, underscore, dash.');
|
|
24
|
+
}
|
|
25
|
+
return text.slice(0, 64);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeTimeoutMs(value) {
|
|
29
|
+
const ms = Number(value);
|
|
30
|
+
if (!Number.isFinite(ms) || ms < 0) return null;
|
|
31
|
+
return Math.floor(ms);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function generateInstanceId() {
|
|
35
|
+
return `inst_${crypto.randomUUID().replace(/-/g, '').slice(0, 12)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
export function getSessionFile(profileId) {
|
|
19
39
|
return path.join(SESSION_DIR, `${profileId}.json`);
|
|
20
40
|
}
|
|
@@ -22,14 +42,24 @@ export function getSessionFile(profileId) {
|
|
|
22
42
|
export function registerSession(profileId, sessionInfo = {}) {
|
|
23
43
|
ensureSessionDir();
|
|
24
44
|
const sessionFile = getSessionFile(profileId);
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const alias = normalizeAlias(sessionInfo.alias);
|
|
47
|
+
const idleTimeoutMs = normalizeTimeoutMs(sessionInfo.idleTimeoutMs);
|
|
48
|
+
const instanceId = String(sessionInfo.instanceId || sessionInfo.sessionId || generateInstanceId()).trim();
|
|
25
49
|
|
|
26
50
|
const sessionData = {
|
|
27
51
|
profileId,
|
|
28
52
|
pid: process.pid,
|
|
29
|
-
|
|
30
|
-
|
|
53
|
+
instanceId,
|
|
54
|
+
alias,
|
|
55
|
+
startTime: now,
|
|
56
|
+
lastSeen: now,
|
|
57
|
+
lastActivityAt: now,
|
|
31
58
|
status: 'active',
|
|
32
59
|
...sessionInfo,
|
|
60
|
+
instanceId,
|
|
61
|
+
alias,
|
|
62
|
+
idleTimeoutMs,
|
|
33
63
|
};
|
|
34
64
|
|
|
35
65
|
fs.writeFileSync(sessionFile, JSON.stringify(sessionData, null, 2));
|
|
@@ -44,12 +74,30 @@ export function updateSession(profileId, updates = {}) {
|
|
|
44
74
|
|
|
45
75
|
try {
|
|
46
76
|
const existing = JSON.parse(fs.readFileSync(sessionFile, 'utf-8'));
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
const mergedAlias = Object.prototype.hasOwnProperty.call(updates, 'alias')
|
|
79
|
+
? normalizeAlias(updates.alias)
|
|
80
|
+
: normalizeAlias(existing.alias);
|
|
81
|
+
const mergedIdleTimeoutMs = Object.prototype.hasOwnProperty.call(updates, 'idleTimeoutMs')
|
|
82
|
+
? normalizeTimeoutMs(updates.idleTimeoutMs)
|
|
83
|
+
: normalizeTimeoutMs(existing.idleTimeoutMs);
|
|
84
|
+
const touchActivity = updates.touchActivity === true;
|
|
85
|
+
const nextActivityAt = touchActivity
|
|
86
|
+
? now
|
|
87
|
+
: (Object.prototype.hasOwnProperty.call(updates, 'lastActivityAt')
|
|
88
|
+
? Number(updates.lastActivityAt) || now
|
|
89
|
+
: Number(existing.lastActivityAt) || now);
|
|
47
90
|
const updated = {
|
|
48
91
|
...existing,
|
|
49
92
|
...updates,
|
|
50
|
-
|
|
93
|
+
instanceId: String(updates.instanceId || existing.instanceId || updates.sessionId || existing.sessionId || generateInstanceId()).trim(),
|
|
94
|
+
alias: mergedAlias,
|
|
95
|
+
idleTimeoutMs: mergedIdleTimeoutMs,
|
|
96
|
+
lastSeen: now,
|
|
97
|
+
lastActivityAt: nextActivityAt,
|
|
51
98
|
profileId,
|
|
52
99
|
};
|
|
100
|
+
delete updated.touchActivity;
|
|
53
101
|
fs.writeFileSync(sessionFile, JSON.stringify(updated, null, 2));
|
|
54
102
|
return updated;
|
|
55
103
|
} catch {
|
|
@@ -93,6 +141,54 @@ export function listRegisteredSessions() {
|
|
|
93
141
|
return sessions;
|
|
94
142
|
}
|
|
95
143
|
|
|
144
|
+
export function findSessionByAlias(alias) {
|
|
145
|
+
const target = normalizeAlias(alias);
|
|
146
|
+
if (!target) return null;
|
|
147
|
+
return listRegisteredSessions().find((item) => normalizeAlias(item?.alias) === target) || null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function findSessionByInstanceId(instanceId) {
|
|
151
|
+
const target = String(instanceId || '').trim();
|
|
152
|
+
if (!target) return null;
|
|
153
|
+
return listRegisteredSessions().find((item) => String(item?.instanceId || '').trim() === target) || null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function resolveSessionTarget(target) {
|
|
157
|
+
const value = String(target || '').trim();
|
|
158
|
+
if (!value) return null;
|
|
159
|
+
const sessions = listRegisteredSessions();
|
|
160
|
+
const byProfile = sessions.find((item) => String(item?.profileId || '').trim() === value);
|
|
161
|
+
if (byProfile) return { profileId: byProfile.profileId, reason: 'profile', session: byProfile };
|
|
162
|
+
const byInstanceId = sessions.find((item) => String(item?.instanceId || '').trim() === value);
|
|
163
|
+
if (byInstanceId) return { profileId: byInstanceId.profileId, reason: 'instanceId', session: byInstanceId };
|
|
164
|
+
const byAlias = sessions.find((item) => normalizeAlias(item?.alias) === normalizeAlias(value));
|
|
165
|
+
if (byAlias) return { profileId: byAlias.profileId, reason: 'alias', session: byAlias };
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function isSessionAliasTaken(alias, exceptProfileId = '') {
|
|
170
|
+
const target = normalizeAlias(alias);
|
|
171
|
+
if (!target) return false;
|
|
172
|
+
const except = String(exceptProfileId || '').trim();
|
|
173
|
+
return listRegisteredSessions().some((item) => {
|
|
174
|
+
if (!item) return false;
|
|
175
|
+
if (except && String(item.profileId || '').trim() === except) return false;
|
|
176
|
+
if (String(item.status || '').trim() !== 'active') return false;
|
|
177
|
+
return normalizeAlias(item.alias) === target;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function touchSessionActivity(profileId, updates = {}) {
|
|
182
|
+
const id = String(profileId || '').trim();
|
|
183
|
+
if (!id) return null;
|
|
184
|
+
const existing = getSessionInfo(id);
|
|
185
|
+
if (!existing) return null;
|
|
186
|
+
return updateSession(id, {
|
|
187
|
+
...updates,
|
|
188
|
+
touchActivity: true,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
96
192
|
export function markSessionReconnecting(profileId) {
|
|
97
193
|
return updateSession(profileId, { status: 'reconnecting', reconnectAttempt: Date.now() });
|
|
98
194
|
}
|
|
@@ -9,6 +9,7 @@ import { releaseLock } from './lock.mjs';
|
|
|
9
9
|
import { getSessionInfo, markSessionClosed } from './session-registry.mjs';
|
|
10
10
|
|
|
11
11
|
const WATCHDOG_DIR = path.join(os.homedir(), '.webauto', 'run', 'camo-watchdogs');
|
|
12
|
+
const DEFAULT_HEADLESS_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
12
13
|
|
|
13
14
|
function ensureWatchdogDir() {
|
|
14
15
|
if (!fs.existsSync(WATCHDOG_DIR)) {
|
|
@@ -63,9 +64,50 @@ function isBlankUrl(url) {
|
|
|
63
64
|
return text === '' || text === 'about:blank' || text === 'about:blank#blocked';
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
function
|
|
67
|
-
const
|
|
68
|
-
|
|
67
|
+
function computeTargetViewportFromWindow(measured) {
|
|
68
|
+
const innerWidth = Math.max(320, Number(measured?.innerWidth || 0) || 0);
|
|
69
|
+
const innerHeight = Math.max(240, Number(measured?.innerHeight || 0) || 0);
|
|
70
|
+
const outerWidth = Math.max(320, Number(measured?.outerWidth || 0) || innerWidth);
|
|
71
|
+
const outerHeight = Math.max(240, Number(measured?.outerHeight || 0) || innerHeight);
|
|
72
|
+
const rawDeltaW = Math.max(0, outerWidth - innerWidth);
|
|
73
|
+
const rawDeltaH = Math.max(0, outerHeight - innerHeight);
|
|
74
|
+
const frameW = rawDeltaW > 400 ? 16 : Math.min(rawDeltaW, 120);
|
|
75
|
+
const frameH = rawDeltaH > 400 ? 88 : Math.min(rawDeltaH, 180);
|
|
76
|
+
return {
|
|
77
|
+
width: Math.max(320, outerWidth - frameW),
|
|
78
|
+
height: Math.max(240, outerHeight - frameH),
|
|
79
|
+
innerWidth,
|
|
80
|
+
innerHeight,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function probeWindowMetrics(profileId) {
|
|
85
|
+
const measured = await callAPI('evaluate', {
|
|
86
|
+
profileId,
|
|
87
|
+
script: '({ innerWidth: window.innerWidth, innerHeight: window.innerHeight, outerWidth: window.outerWidth, outerHeight: window.outerHeight })',
|
|
88
|
+
});
|
|
89
|
+
return measured?.result || {};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function syncViewportWithWindow(profileId, tolerancePx = 3) {
|
|
93
|
+
const measured = await probeWindowMetrics(profileId);
|
|
94
|
+
const target = computeTargetViewportFromWindow(measured);
|
|
95
|
+
const dw = Math.abs(target.innerWidth - target.width);
|
|
96
|
+
const dh = Math.abs(target.innerHeight - target.height);
|
|
97
|
+
if (dw <= tolerancePx && dh <= tolerancePx) return false;
|
|
98
|
+
await callAPI('page:setViewport', {
|
|
99
|
+
profileId,
|
|
100
|
+
width: target.width,
|
|
101
|
+
height: target.height,
|
|
102
|
+
});
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveIdleTimeoutMs(sessionInfo) {
|
|
107
|
+
if (!sessionInfo || sessionInfo.headless !== true) return 0;
|
|
108
|
+
const raw = Number(sessionInfo.idleTimeoutMs);
|
|
109
|
+
if (Number.isFinite(raw) && raw >= 0) return Math.floor(raw);
|
|
110
|
+
return DEFAULT_HEADLESS_IDLE_TIMEOUT_MS;
|
|
69
111
|
}
|
|
70
112
|
|
|
71
113
|
async function cleanupSession(profileId) {
|
|
@@ -78,6 +120,8 @@ async function runMonitor(profileId, options = {}) {
|
|
|
78
120
|
const intervalMs = Math.max(500, Number(options.intervalMs) || 1200);
|
|
79
121
|
const emptyThreshold = Math.max(1, Number(options.emptyThreshold) || 2);
|
|
80
122
|
const blankThreshold = Math.max(1, Number(options.blankThreshold) || 3);
|
|
123
|
+
const viewportSyncIntervalMs = Math.max(500, Number(options.viewportSyncIntervalMs) || 1500);
|
|
124
|
+
let lastViewportSyncAt = 0;
|
|
81
125
|
|
|
82
126
|
let seenAnyPage = false;
|
|
83
127
|
let seenNonBlankPage = false;
|
|
@@ -85,7 +129,18 @@ async function runMonitor(profileId, options = {}) {
|
|
|
85
129
|
let blankOnlyStreak = 0;
|
|
86
130
|
|
|
87
131
|
while (true) {
|
|
88
|
-
|
|
132
|
+
const localInfo = getSessionInfo(profileId);
|
|
133
|
+
if (!localInfo || localInfo.status !== 'active') return;
|
|
134
|
+
|
|
135
|
+
const idleTimeoutMs = resolveIdleTimeoutMs(localInfo);
|
|
136
|
+
if (idleTimeoutMs > 0) {
|
|
137
|
+
const lastActivityAt = Number(localInfo.lastActivityAt || localInfo.lastSeen || localInfo.startTime || Date.now());
|
|
138
|
+
const idleMs = Date.now() - lastActivityAt;
|
|
139
|
+
if (idleMs >= idleTimeoutMs) {
|
|
140
|
+
await cleanupSession(profileId);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
89
144
|
|
|
90
145
|
let sessions = [];
|
|
91
146
|
try {
|
|
@@ -140,6 +195,12 @@ async function runMonitor(profileId, options = {}) {
|
|
|
140
195
|
return;
|
|
141
196
|
}
|
|
142
197
|
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
if (pages.length > 0 && now - lastViewportSyncAt >= viewportSyncIntervalMs) {
|
|
200
|
+
lastViewportSyncAt = now;
|
|
201
|
+
await syncViewportWithWindow(profileId).catch(() => {});
|
|
202
|
+
}
|
|
203
|
+
|
|
143
204
|
await sleep(intervalMs);
|
|
144
205
|
}
|
|
145
206
|
}
|
|
@@ -4,6 +4,14 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import { BROWSER_SERVICE_URL, loadConfig, setRepoRoot } from './config.mjs';
|
|
7
|
+
import { touchSessionActivity } from '../lifecycle/session-registry.mjs';
|
|
8
|
+
|
|
9
|
+
function shouldTrackSessionActivity(action, payload) {
|
|
10
|
+
const profileId = String(payload?.profileId || '').trim();
|
|
11
|
+
if (!profileId) return false;
|
|
12
|
+
if (action === 'getStatus' || action === 'service:shutdown' || action === 'stop') return false;
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
7
15
|
|
|
8
16
|
export async function callAPI(action, payload = {}) {
|
|
9
17
|
const r = await fetch(`${BROWSER_SERVICE_URL}/command`, {
|
|
@@ -21,6 +29,12 @@ export async function callAPI(action, payload = {}) {
|
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
if (!r.ok) throw new Error(body?.error || `HTTP ${r.status}`);
|
|
32
|
+
if (shouldTrackSessionActivity(action, payload)) {
|
|
33
|
+
touchSessionActivity(payload.profileId, {
|
|
34
|
+
lastAction: String(action || '').trim() || null,
|
|
35
|
+
lastActionAt: Date.now(),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
24
38
|
return body;
|
|
25
39
|
}
|
|
26
40
|
|
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,12 +24,17 @@ 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] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]
|
|
28
28
|
stop [profileId]
|
|
29
|
+
stop --id <instanceId> Stop by instance id
|
|
30
|
+
stop --alias <alias> Stop by alias
|
|
31
|
+
stop idle Stop all idle sessions
|
|
32
|
+
stop all Stop all sessions
|
|
29
33
|
status [profileId]
|
|
30
34
|
list Alias of status
|
|
31
35
|
|
|
32
36
|
LIFECYCLE & CLEANUP:
|
|
37
|
+
instances List global camoufox instances (live + registered + idle state)
|
|
33
38
|
sessions List active browser sessions
|
|
34
39
|
cleanup [profileId] Cleanup session (release lock + stop)
|
|
35
40
|
cleanup all Cleanup all active sessions
|
|
@@ -90,7 +95,13 @@ EXAMPLES:
|
|
|
90
95
|
camo create fingerprint --os windows --region uk
|
|
91
96
|
camo profile create myprofile
|
|
92
97
|
camo profile default myprofile
|
|
93
|
-
camo start --url https://example.com
|
|
98
|
+
camo start --url https://example.com --alias main
|
|
99
|
+
camo start worker-1 --headless --alias shard1 --idle-timeout 45m
|
|
100
|
+
camo start myprofile --width 1920 --height 1020
|
|
101
|
+
camo stop --id inst_xxxxxxxx
|
|
102
|
+
camo stop --alias shard1
|
|
103
|
+
camo stop idle
|
|
104
|
+
camo close all
|
|
94
105
|
camo goto https://www.xiaohongshu.com
|
|
95
106
|
camo scroll --down --amount 500
|
|
96
107
|
camo click "#search-input"
|
|
@@ -108,6 +119,7 @@ EXAMPLES:
|
|
|
108
119
|
camo mouse wheel --deltay -300
|
|
109
120
|
camo system display
|
|
110
121
|
camo sessions
|
|
122
|
+
camo instances
|
|
111
123
|
camo cleanup myprofile
|
|
112
124
|
camo force-stop myprofile
|
|
113
125
|
camo lock list
|