@web-auto/camo 0.1.6 → 0.1.8
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 +19 -1
- package/package.json +1 -1
- package/src/autoscript/action-providers/xhs/comments.mjs +38 -2
- package/src/autoscript/action-providers/xhs/interaction.mjs +47 -2
- package/src/cli.mjs +14 -1
- package/src/commands/browser.mjs +127 -26
- package/src/commands/devtools.mjs +349 -0
- package/src/container/runtime-core/operations/selector-scripts.mjs +71 -4
- package/src/utils/help.mjs +9 -1
package/README.md
CHANGED
|
@@ -39,6 +39,15 @@ camo start --url https://example.com --alias main
|
|
|
39
39
|
# Start headless worker (auto-kill after idle timeout)
|
|
40
40
|
camo start worker-1 --headless --alias shard1 --idle-timeout 30m
|
|
41
41
|
|
|
42
|
+
# Start with devtools (headful only)
|
|
43
|
+
camo start worker-1 --devtools
|
|
44
|
+
|
|
45
|
+
# Evaluate JS (devtools-style input in page context)
|
|
46
|
+
camo devtools eval worker-1 "document.title"
|
|
47
|
+
|
|
48
|
+
# Read captured console entries
|
|
49
|
+
camo devtools logs worker-1 --levels error,warn --limit 50
|
|
50
|
+
|
|
42
51
|
# Navigate
|
|
43
52
|
camo goto https://www.xiaohongshu.com
|
|
44
53
|
|
|
@@ -71,7 +80,7 @@ camo create fingerprint --os <os> --region <region>
|
|
|
71
80
|
### Browser Control
|
|
72
81
|
|
|
73
82
|
```bash
|
|
74
|
-
camo start [profileId] [--url <url>] [--headless] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]
|
|
83
|
+
camo start [profileId] [--url <url>] [--headless] [--devtools] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]
|
|
75
84
|
camo stop [profileId]
|
|
76
85
|
camo stop --id <instanceId>
|
|
77
86
|
camo stop --alias <alias>
|
|
@@ -85,6 +94,7 @@ camo shutdown # Shutdown browser-service (all sessions)
|
|
|
85
94
|
If no saved size exists, it defaults to near-fullscreen (full width, slight vertical reserve).
|
|
86
95
|
Use `--width/--height` to override and update the saved profile size.
|
|
87
96
|
For headless sessions, default idle timeout is `30m` (auto-stop on inactivity). Use `--idle-timeout` (e.g. `45m`, `1800s`, `0`) to customize.
|
|
97
|
+
Use `--devtools` to open browser developer tools in headed mode (cannot be combined with `--headless`).
|
|
88
98
|
|
|
89
99
|
### Lifecycle & Cleanup
|
|
90
100
|
|
|
@@ -118,6 +128,14 @@ camo clear-highlight [profileId] # Clear all highlights
|
|
|
118
128
|
camo viewport [profileId] --width <w> --height <h>
|
|
119
129
|
```
|
|
120
130
|
|
|
131
|
+
### Devtools
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
camo devtools logs [profileId] [--limit 120] [--since <unix_ms>] [--levels error,warn] [--clear]
|
|
135
|
+
camo devtools eval [profileId] <expression> [--profile <id>]
|
|
136
|
+
camo devtools clear [profileId]
|
|
137
|
+
```
|
|
138
|
+
|
|
121
139
|
### Pages
|
|
122
140
|
|
|
123
141
|
```bash
|
package/package.json
CHANGED
|
@@ -86,6 +86,43 @@ export function buildCommentsHarvestScript(params = {}) {
|
|
|
86
86
|
}
|
|
87
87
|
return null;
|
|
88
88
|
};
|
|
89
|
+
const normalizeInlineText = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
90
|
+
const sanitizeAuthorText = (raw, commentText = '') => {
|
|
91
|
+
const text = normalizeInlineText(raw);
|
|
92
|
+
if (!text) return '';
|
|
93
|
+
if (commentText && text === commentText) return '';
|
|
94
|
+
if (text.length > 40) return '';
|
|
95
|
+
if (/^(回复|展开|收起|查看更多|评论|赞|分享|发送)$/.test(text)) return '';
|
|
96
|
+
return text;
|
|
97
|
+
};
|
|
98
|
+
const readAuthor = (item, commentText = '') => {
|
|
99
|
+
const attrNames = ['data-user-name', 'data-username', 'data-user_nickname', 'data-nickname'];
|
|
100
|
+
for (const attr of attrNames) {
|
|
101
|
+
const value = sanitizeAuthorText(item.getAttribute?.(attr), commentText);
|
|
102
|
+
if (value) return value;
|
|
103
|
+
}
|
|
104
|
+
const selectors = [
|
|
105
|
+
'.comment-user .name',
|
|
106
|
+
'.comment-user .username',
|
|
107
|
+
'.comment-user .user-name',
|
|
108
|
+
'.author .name',
|
|
109
|
+
'.author',
|
|
110
|
+
'.user-name',
|
|
111
|
+
'.username',
|
|
112
|
+
'.name',
|
|
113
|
+
'a[href*="/user/profile/"]',
|
|
114
|
+
'a[href*="/user/"]',
|
|
115
|
+
];
|
|
116
|
+
for (const selector of selectors) {
|
|
117
|
+
const node = item.querySelector(selector);
|
|
118
|
+
if (!node) continue;
|
|
119
|
+
const title = sanitizeAuthorText(node.getAttribute?.('title'), commentText);
|
|
120
|
+
if (title) return title;
|
|
121
|
+
const text = sanitizeAuthorText(node.textContent, commentText);
|
|
122
|
+
if (text) return text;
|
|
123
|
+
}
|
|
124
|
+
return '';
|
|
125
|
+
};
|
|
89
126
|
|
|
90
127
|
const scroller = document.querySelector('.note-scroller')
|
|
91
128
|
|| document.querySelector('.comments-el')
|
|
@@ -105,9 +142,8 @@ export function buildCommentsHarvestScript(params = {}) {
|
|
|
105
142
|
const nodes = Array.from(document.querySelectorAll('.comment-item, [class*="comment-item"]'));
|
|
106
143
|
for (const item of nodes) {
|
|
107
144
|
const textNode = item.querySelector('.content, .comment-content, p');
|
|
108
|
-
const authorNode = item.querySelector('.name, .author, .user-name, [class*="author"], [class*="name"]');
|
|
109
145
|
const text = String((textNode && textNode.textContent) || '').trim();
|
|
110
|
-
const author =
|
|
146
|
+
const author = readAuthor(item, text);
|
|
111
147
|
if (!text) continue;
|
|
112
148
|
const key = author + '::' + text;
|
|
113
149
|
if (commentMap.has(key)) continue;
|
|
@@ -74,6 +74,42 @@ function buildCollectLikeTargetsScript() {
|
|
|
74
74
|
}
|
|
75
75
|
return '';
|
|
76
76
|
};
|
|
77
|
+
const sanitizeUserName = (raw, commentText = '') => {
|
|
78
|
+
const text = String(raw || '').replace(/\\s+/g, ' ').trim();
|
|
79
|
+
if (!text) return '';
|
|
80
|
+
if (commentText && text === commentText) return '';
|
|
81
|
+
if (text.length > 40) return '';
|
|
82
|
+
if (/^(回复|展开|收起|查看更多|评论|赞|分享|发送)$/.test(text)) return '';
|
|
83
|
+
return text;
|
|
84
|
+
};
|
|
85
|
+
const readUserName = (item, commentText = '') => {
|
|
86
|
+
const attrNames = ['data-user-name', 'data-username', 'data-user_nickname', 'data-nickname'];
|
|
87
|
+
for (const attr of attrNames) {
|
|
88
|
+
const value = sanitizeUserName(item.getAttribute?.(attr), commentText);
|
|
89
|
+
if (value) return value;
|
|
90
|
+
}
|
|
91
|
+
const selectors = [
|
|
92
|
+
'.comment-user .name',
|
|
93
|
+
'.comment-user .username',
|
|
94
|
+
'.comment-user .user-name',
|
|
95
|
+
'.author .name',
|
|
96
|
+
'.author',
|
|
97
|
+
'.user-name',
|
|
98
|
+
'.username',
|
|
99
|
+
'.name',
|
|
100
|
+
'a[href*="/user/profile/"]',
|
|
101
|
+
'a[href*="/user/"]',
|
|
102
|
+
];
|
|
103
|
+
for (const selector of selectors) {
|
|
104
|
+
const node = item.querySelector(selector);
|
|
105
|
+
if (!node) continue;
|
|
106
|
+
const title = sanitizeUserName(node.getAttribute?.('title'), commentText);
|
|
107
|
+
if (title) return title;
|
|
108
|
+
const text = sanitizeUserName(node.textContent, commentText);
|
|
109
|
+
if (text) return text;
|
|
110
|
+
}
|
|
111
|
+
return '';
|
|
112
|
+
};
|
|
77
113
|
const readAttr = (item, attrNames) => {
|
|
78
114
|
for (const attr of attrNames) {
|
|
79
115
|
const value = String(item.getAttribute?.(attr) || '').trim();
|
|
@@ -81,6 +117,15 @@ function buildCollectLikeTargetsScript() {
|
|
|
81
117
|
}
|
|
82
118
|
return '';
|
|
83
119
|
};
|
|
120
|
+
const readUserId = (item) => {
|
|
121
|
+
const value = readAttr(item, ['data-user-id', 'data-userid', 'data-user_id']);
|
|
122
|
+
if (value) return value;
|
|
123
|
+
const anchor = item.querySelector('a[href*="/user/profile/"], a[href*="/user/"]');
|
|
124
|
+
const href = String(anchor?.getAttribute?.('href') || '').trim();
|
|
125
|
+
if (!href) return '';
|
|
126
|
+
const matched = href.match(/\\/user\\/(?:profile\\/)?([a-zA-Z0-9_-]+)/);
|
|
127
|
+
return matched && matched[1] ? matched[1] : '';
|
|
128
|
+
};
|
|
84
129
|
|
|
85
130
|
const matchedSet = new Set(
|
|
86
131
|
Array.isArray(state.matchedComments)
|
|
@@ -92,8 +137,8 @@ function buildCollectLikeTargetsScript() {
|
|
|
92
137
|
const item = items[index];
|
|
93
138
|
const text = readText(item, ['.content', '.comment-content', 'p']);
|
|
94
139
|
if (!text) continue;
|
|
95
|
-
const userName =
|
|
96
|
-
const userId =
|
|
140
|
+
const userName = readUserName(item, text);
|
|
141
|
+
const userId = readUserId(item);
|
|
97
142
|
const timestamp = readText(item, ['.date', '.time', '.timestamp', '[class*="time"]']);
|
|
98
143
|
const likeControl = findLikeControl(item);
|
|
99
144
|
rows.push({
|
package/src/cli.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { handleSystemCommand } from './commands/system.mjs';
|
|
|
13
13
|
import { handleContainerCommand } from './commands/container.mjs';
|
|
14
14
|
import { handleAutoscriptCommand } from './commands/autoscript.mjs';
|
|
15
15
|
import { handleEventsCommand } from './commands/events.mjs';
|
|
16
|
+
import { handleDevtoolsCommand } from './commands/devtools.mjs';
|
|
16
17
|
import {
|
|
17
18
|
handleStartCommand, handleStopCommand, handleStatusCommand,
|
|
18
19
|
handleGotoCommand, handleBackCommand, handleScreenshotCommand,
|
|
@@ -64,6 +65,13 @@ function inferProfileId(cmd, args) {
|
|
|
64
65
|
return positionals[0] || null;
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
if (cmd === 'devtools') {
|
|
69
|
+
const sub = positionals[0] || null;
|
|
70
|
+
if (sub === 'eval' || sub === 'logs' || sub === 'clear') {
|
|
71
|
+
return positionals[1] || null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
if (cmd === 'autoscript' && positionals[0] === 'run') {
|
|
68
76
|
return explicitProfile || null;
|
|
69
77
|
}
|
|
@@ -192,6 +200,11 @@ async function main() {
|
|
|
192
200
|
return;
|
|
193
201
|
}
|
|
194
202
|
|
|
203
|
+
if (cmd === 'devtools') {
|
|
204
|
+
await runTrackedCommand(cmd, args, () => handleDevtoolsCommand(args));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
195
208
|
// Lifecycle commands
|
|
196
209
|
if (cmd === 'cleanup') {
|
|
197
210
|
await runTrackedCommand(cmd, args, () => handleCleanupCommand(args));
|
|
@@ -237,7 +250,7 @@ async function main() {
|
|
|
237
250
|
'start', 'stop', 'close', 'status', 'list', 'goto', 'navigate', 'back', 'screenshot',
|
|
238
251
|
'new-page', 'close-page', 'switch-page', 'list-pages', 'shutdown',
|
|
239
252
|
'scroll', 'click', 'type', 'highlight', 'clear-highlight', 'viewport',
|
|
240
|
-
'cookies', 'window', 'mouse', 'system', 'container', 'autoscript', 'events',
|
|
253
|
+
'cookies', 'window', 'mouse', 'system', 'container', 'autoscript', 'events', 'devtools',
|
|
241
254
|
]);
|
|
242
255
|
|
|
243
256
|
if (!serviceCommands.has(cmd)) {
|
package/src/commands/browser.mjs
CHANGED
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
import { callAPI, ensureCamoufox, ensureBrowserService, getSessionByProfile, checkBrowserService } from '../utils/browser-service.mjs';
|
|
9
9
|
import { resolveProfileId, ensureUrlScheme, looksLikeUrlToken, getPositionals } from '../utils/args.mjs';
|
|
10
10
|
import { acquireLock, releaseLock, cleanupStaleLocks } from '../lifecycle/lock.mjs';
|
|
11
|
+
import {
|
|
12
|
+
buildSelectorClickScript,
|
|
13
|
+
buildSelectorTypeScript,
|
|
14
|
+
} from '../container/runtime-core/operations/selector-scripts.mjs';
|
|
11
15
|
import {
|
|
12
16
|
registerSession,
|
|
13
17
|
updateSession,
|
|
@@ -26,6 +30,9 @@ const START_WINDOW_MIN_HEIGHT = 700;
|
|
|
26
30
|
const START_WINDOW_MAX_RESERVE = 240;
|
|
27
31
|
const START_WINDOW_DEFAULT_RESERVE = 72;
|
|
28
32
|
const DEFAULT_HEADLESS_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
33
|
+
const DEVTOOLS_SHORTCUTS = process.platform === 'darwin'
|
|
34
|
+
? ['Meta+Alt+I', 'F12']
|
|
35
|
+
: ['F12', 'Control+Shift+I'];
|
|
29
36
|
|
|
30
37
|
function sleep(ms) {
|
|
31
38
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -122,6 +129,67 @@ async function stopAndCleanupProfile(profileId, options = {}) {
|
|
|
122
129
|
};
|
|
123
130
|
}
|
|
124
131
|
|
|
132
|
+
async function probeViewportSize(profileId) {
|
|
133
|
+
try {
|
|
134
|
+
const payload = await callAPI('evaluate', {
|
|
135
|
+
profileId,
|
|
136
|
+
script: '(() => ({ width: Number(window.innerWidth || 0), height: Number(window.innerHeight || 0) }))()',
|
|
137
|
+
});
|
|
138
|
+
const size = payload?.result || payload?.data || payload || {};
|
|
139
|
+
const width = Number(size?.width || 0);
|
|
140
|
+
const height = Number(size?.height || 0);
|
|
141
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
|
|
142
|
+
return { width, height };
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function requestDevtoolsOpen(profileId, options = {}) {
|
|
149
|
+
const id = String(profileId || '').trim();
|
|
150
|
+
if (!id) {
|
|
151
|
+
return { ok: false, requested: false, reason: 'profile_required', shortcuts: [] };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const shortcuts = Array.isArray(options.shortcuts) && options.shortcuts.length
|
|
155
|
+
? options.shortcuts.map((item) => String(item || '').trim()).filter(Boolean)
|
|
156
|
+
: DEVTOOLS_SHORTCUTS;
|
|
157
|
+
const settleMs = Math.max(0, parseNumber(options.settleMs, 180));
|
|
158
|
+
const before = await probeViewportSize(id);
|
|
159
|
+
const attempts = [];
|
|
160
|
+
|
|
161
|
+
for (const key of shortcuts) {
|
|
162
|
+
try {
|
|
163
|
+
await callAPI('keyboard:press', { profileId: id, key });
|
|
164
|
+
attempts.push({ key, ok: true });
|
|
165
|
+
if (settleMs > 0) {
|
|
166
|
+
// Allow browser UI animation to settle after shortcut.
|
|
167
|
+
// eslint-disable-next-line no-await-in-loop
|
|
168
|
+
await sleep(settleMs);
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
attempts.push({ key, ok: false, error: err?.message || String(err) });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const after = await probeViewportSize(id);
|
|
176
|
+
const beforeHeight = Number(before?.height || 0);
|
|
177
|
+
const afterHeight = Number(after?.height || 0);
|
|
178
|
+
const viewportReduced = beforeHeight > 0 && afterHeight > 0 && afterHeight < (beforeHeight - 100);
|
|
179
|
+
const successCount = attempts.filter((item) => item.ok).length;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
ok: successCount > 0,
|
|
183
|
+
requested: true,
|
|
184
|
+
shortcuts,
|
|
185
|
+
attempts,
|
|
186
|
+
before,
|
|
187
|
+
after,
|
|
188
|
+
verified: viewportReduced,
|
|
189
|
+
verification: viewportReduced ? 'viewport_height_reduced' : 'shortcut_dispatched',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
125
193
|
export function computeTargetViewportFromWindowMetrics(measured) {
|
|
126
194
|
const innerWidth = Math.max(320, parseNumber(measured?.innerWidth, 0));
|
|
127
195
|
const innerHeight = Math.max(240, parseNumber(measured?.innerHeight, 0));
|
|
@@ -250,8 +318,9 @@ export async function handleStartCommand(args) {
|
|
|
250
318
|
const alias = validateAlias(readFlagValue(args, ['--alias']));
|
|
251
319
|
const idleTimeoutRaw = readFlagValue(args, ['--idle-timeout']);
|
|
252
320
|
const parsedIdleTimeoutMs = parseDurationMs(idleTimeoutRaw, DEFAULT_HEADLESS_IDLE_TIMEOUT_MS);
|
|
321
|
+
const wantsDevtools = args.includes('--devtools');
|
|
253
322
|
if (hasExplicitWidth !== hasExplicitHeight) {
|
|
254
|
-
throw new Error('Usage: camo start [profileId] [--url <url>] [--headless] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]');
|
|
323
|
+
throw new Error('Usage: camo start [profileId] [--url <url>] [--headless] [--devtools] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]');
|
|
255
324
|
}
|
|
256
325
|
if ((hasExplicitWidth && explicitWidth < START_WINDOW_MIN_WIDTH) || (hasExplicitHeight && explicitHeight < START_WINDOW_MIN_HEIGHT)) {
|
|
257
326
|
throw new Error(`Window size too small. Minimum is ${START_WINDOW_MIN_WIDTH}x${START_WINDOW_MIN_HEIGHT}`);
|
|
@@ -308,7 +377,7 @@ export async function handleStartCommand(args) {
|
|
|
308
377
|
alias: alias || null,
|
|
309
378
|
});
|
|
310
379
|
const idleState = computeIdleState(record);
|
|
311
|
-
|
|
380
|
+
const payload = {
|
|
312
381
|
ok: true,
|
|
313
382
|
sessionId: existing.session_id || existing.profileId,
|
|
314
383
|
instanceId: record.instanceId,
|
|
@@ -323,7 +392,13 @@ export async function handleStartCommand(args) {
|
|
|
323
392
|
byId: `camo stop --id ${record.instanceId}`,
|
|
324
393
|
byAlias: record.alias ? `camo stop --alias ${record.alias}` : null,
|
|
325
394
|
},
|
|
326
|
-
}
|
|
395
|
+
};
|
|
396
|
+
const existingMode = String(existing?.mode || record?.mode || '').toLowerCase();
|
|
397
|
+
const existingHeadless = existing?.headless === true || existingMode.includes('headless');
|
|
398
|
+
if (!existingHeadless && wantsDevtools) {
|
|
399
|
+
payload.devtools = await requestDevtoolsOpen(profileId);
|
|
400
|
+
}
|
|
401
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
327
402
|
startSessionWatchdog(profileId);
|
|
328
403
|
return;
|
|
329
404
|
}
|
|
@@ -338,12 +413,16 @@ export async function handleStartCommand(args) {
|
|
|
338
413
|
}
|
|
339
414
|
|
|
340
415
|
const headless = args.includes('--headless');
|
|
416
|
+
if (wantsDevtools && headless) {
|
|
417
|
+
throw new Error('--devtools is not supported with --headless');
|
|
418
|
+
}
|
|
341
419
|
const idleTimeoutMs = headless ? parsedIdleTimeoutMs : 0;
|
|
342
420
|
const targetUrl = explicitUrl || implicitUrl;
|
|
343
421
|
const result = await callAPI('start', {
|
|
344
422
|
profileId,
|
|
345
423
|
url: targetUrl ? ensureUrlScheme(targetUrl) : undefined,
|
|
346
424
|
headless,
|
|
425
|
+
devtools: wantsDevtools,
|
|
347
426
|
});
|
|
348
427
|
|
|
349
428
|
if (result?.ok) {
|
|
@@ -416,6 +495,9 @@ export async function handleStartCommand(args) {
|
|
|
416
495
|
Number.isFinite(measuredOuterHeight) ? measuredOuterHeight : windowTarget.height,
|
|
417
496
|
);
|
|
418
497
|
result.profileWindow = savedWindow?.window || null;
|
|
498
|
+
if (wantsDevtools) {
|
|
499
|
+
result.devtools = await requestDevtoolsOpen(profileId);
|
|
500
|
+
}
|
|
419
501
|
}
|
|
420
502
|
}
|
|
421
503
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -426,6 +508,12 @@ export async function handleStopCommand(args) {
|
|
|
426
508
|
const target = rawTarget.toLowerCase();
|
|
427
509
|
const idTarget = readFlagValue(args, ['--id']);
|
|
428
510
|
const aliasTarget = readFlagValue(args, ['--alias']);
|
|
511
|
+
if (args.includes('--id') && !idTarget) {
|
|
512
|
+
throw new Error('Usage: camo stop --id <instanceId>');
|
|
513
|
+
}
|
|
514
|
+
if (args.includes('--alias') && !aliasTarget) {
|
|
515
|
+
throw new Error('Usage: camo stop --alias <alias>');
|
|
516
|
+
}
|
|
429
517
|
const stopIdle = target === 'idle' || args.includes('--idle');
|
|
430
518
|
const stopAll = target === 'all';
|
|
431
519
|
const serviceUp = await checkBrowserService();
|
|
@@ -465,11 +553,41 @@ export async function handleStopCommand(args) {
|
|
|
465
553
|
|
|
466
554
|
if (stopIdle) {
|
|
467
555
|
const now = Date.now();
|
|
468
|
-
const
|
|
556
|
+
const registeredSessions = listRegisteredSessions();
|
|
557
|
+
let liveSessions = [];
|
|
558
|
+
if (serviceUp) {
|
|
559
|
+
try {
|
|
560
|
+
const status = await callAPI('getStatus', {});
|
|
561
|
+
liveSessions = Array.isArray(status?.sessions) ? status.sessions : [];
|
|
562
|
+
} catch {
|
|
563
|
+
// Ignore and fallback to local registry.
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const regMap = new Map(
|
|
567
|
+
registeredSessions
|
|
568
|
+
.filter((item) => item && String(item?.status || '').trim() === 'active')
|
|
569
|
+
.map((item) => [String(item.profileId || '').trim(), item]),
|
|
570
|
+
);
|
|
571
|
+
const idleTargets = new Set(
|
|
572
|
+
registeredSessions
|
|
469
573
|
.filter((item) => String(item?.status || '').trim() === 'active')
|
|
470
574
|
.map((item) => ({ session: item, idle: computeIdleState(item, now) }))
|
|
471
575
|
.filter((item) => item.idle.idle)
|
|
472
|
-
.map((item) => item.session.profileId)
|
|
576
|
+
.map((item) => item.session.profileId),
|
|
577
|
+
);
|
|
578
|
+
let orphanLiveHeadlessCount = 0;
|
|
579
|
+
for (const live of liveSessions) {
|
|
580
|
+
const liveProfileId = String(live?.profileId || '').trim();
|
|
581
|
+
if (!liveProfileId) continue;
|
|
582
|
+
if (regMap.has(liveProfileId) || idleTargets.has(liveProfileId)) continue;
|
|
583
|
+
const mode = String(live?.mode || '').toLowerCase();
|
|
584
|
+
const liveHeadless = live?.headless === true || mode.includes('headless');
|
|
585
|
+
// Live but unregistered headless sessions are treated as idle-orphan targets.
|
|
586
|
+
if (liveHeadless) {
|
|
587
|
+
idleTargets.add(liveProfileId);
|
|
588
|
+
orphanLiveHeadlessCount += 1;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
473
591
|
const results = [];
|
|
474
592
|
for (const profileId of idleTargets) {
|
|
475
593
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -479,7 +597,8 @@ export async function handleStopCommand(args) {
|
|
|
479
597
|
ok: true,
|
|
480
598
|
mode: 'idle',
|
|
481
599
|
serviceUp,
|
|
482
|
-
targetCount: idleTargets.
|
|
600
|
+
targetCount: idleTargets.size,
|
|
601
|
+
orphanLiveHeadlessCount,
|
|
483
602
|
closed: results.filter((item) => item.ok).length,
|
|
484
603
|
failed: results.filter((item) => !item.ok).length,
|
|
485
604
|
results,
|
|
@@ -652,14 +771,7 @@ export async function handleClickCommand(args) {
|
|
|
652
771
|
|
|
653
772
|
const result = await callAPI('evaluate', {
|
|
654
773
|
profileId,
|
|
655
|
-
script:
|
|
656
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
657
|
-
if (!el) throw new Error('Element not found: ' + ${JSON.stringify(selector)});
|
|
658
|
-
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
659
|
-
await new Promise(r => setTimeout(r, 200));
|
|
660
|
-
el.click();
|
|
661
|
-
return { clicked: true, selector: ${JSON.stringify(selector)} };
|
|
662
|
-
})()`
|
|
774
|
+
script: buildSelectorClickScript({ selector, highlight: false }),
|
|
663
775
|
});
|
|
664
776
|
console.log(JSON.stringify(result, null, 2));
|
|
665
777
|
}
|
|
@@ -686,18 +798,7 @@ export async function handleTypeCommand(args) {
|
|
|
686
798
|
|
|
687
799
|
const result = await callAPI('evaluate', {
|
|
688
800
|
profileId,
|
|
689
|
-
script:
|
|
690
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
691
|
-
if (!el) throw new Error('Element not found: ' + ${JSON.stringify(selector)});
|
|
692
|
-
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
693
|
-
await new Promise(r => setTimeout(r, 200));
|
|
694
|
-
el.focus();
|
|
695
|
-
el.value = '';
|
|
696
|
-
el.value = ${JSON.stringify(text)};
|
|
697
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
698
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
699
|
-
return { typed: true, selector: ${JSON.stringify(selector)}, length: ${text.length} };
|
|
700
|
-
})()`
|
|
801
|
+
script: buildSelectorTypeScript({ selector, highlight: false, text }),
|
|
701
802
|
});
|
|
702
803
|
console.log(JSON.stringify(result, null, 2));
|
|
703
804
|
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { getDefaultProfile, listProfiles } from '../utils/config.mjs';
|
|
2
|
+
import { callAPI } from '../utils/browser-service.mjs';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_LIMIT = 120;
|
|
5
|
+
const MAX_LIMIT = 1000;
|
|
6
|
+
|
|
7
|
+
function parseNumber(value, fallback) {
|
|
8
|
+
const num = Number(value);
|
|
9
|
+
return Number.isFinite(num) ? num : fallback;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function clamp(value, min, max) {
|
|
13
|
+
return Math.min(Math.max(value, min), max);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readFlagValue(args, names) {
|
|
17
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
18
|
+
if (!names.includes(args[i])) continue;
|
|
19
|
+
const value = args[i + 1];
|
|
20
|
+
if (!value || String(value).startsWith('-')) return null;
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function collectPositionals(args, startIndex = 2) {
|
|
27
|
+
const values = [];
|
|
28
|
+
for (let i = startIndex; i < args.length; i += 1) {
|
|
29
|
+
const token = args[i];
|
|
30
|
+
if (!token || String(token).startsWith('--')) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const prev = args[i - 1];
|
|
34
|
+
if (prev && ['--profile', '-p', '--limit', '-n', '--levels', '--since'].includes(prev)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
values.push(String(token));
|
|
38
|
+
}
|
|
39
|
+
return values;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function pickProfileAndExpression(args, subcommand) {
|
|
43
|
+
const explicitProfile = readFlagValue(args, ['--profile', '-p']);
|
|
44
|
+
const profileSet = new Set(listProfiles());
|
|
45
|
+
const positionals = collectPositionals(args, 2);
|
|
46
|
+
|
|
47
|
+
let profileId = explicitProfile || null;
|
|
48
|
+
let expression = null;
|
|
49
|
+
|
|
50
|
+
if (subcommand === 'eval') {
|
|
51
|
+
if (positionals.length === 0) {
|
|
52
|
+
return { profileId: profileId || getDefaultProfile(), expression: null };
|
|
53
|
+
}
|
|
54
|
+
if (!profileId && positionals.length >= 2) {
|
|
55
|
+
profileId = positionals[0];
|
|
56
|
+
expression = positionals.slice(1).join(' ').trim();
|
|
57
|
+
} else if (!profileId && profileSet.has(positionals[0])) {
|
|
58
|
+
profileId = positionals[0];
|
|
59
|
+
expression = positionals.slice(1).join(' ').trim();
|
|
60
|
+
} else {
|
|
61
|
+
expression = positionals.join(' ').trim();
|
|
62
|
+
}
|
|
63
|
+
return { profileId: profileId || getDefaultProfile(), expression };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (positionals.length > 0) {
|
|
67
|
+
if (!profileId && (profileSet.has(positionals[0]) || subcommand === 'logs' || subcommand === 'clear')) {
|
|
68
|
+
profileId = positionals[0];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { profileId: profileId || getDefaultProfile(), expression: null };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildConsoleInstallScript(maxEntries) {
|
|
75
|
+
return `(function installCamoDevtoolsConsoleCollector() {
|
|
76
|
+
const KEY = '__camo_console_collector_v1__';
|
|
77
|
+
const BUFFER_KEY = '__camo_console_buffer_v1__';
|
|
78
|
+
const MAX = ${Math.max(100, Math.floor(maxEntries || MAX_LIMIT))};
|
|
79
|
+
const now = () => Date.now();
|
|
80
|
+
|
|
81
|
+
const stringify = (value) => {
|
|
82
|
+
if (typeof value === 'string') return value;
|
|
83
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value);
|
|
84
|
+
if (value === null) return 'null';
|
|
85
|
+
if (typeof value === 'undefined') return 'undefined';
|
|
86
|
+
if (typeof value === 'function') return '[function]';
|
|
87
|
+
if (typeof value === 'symbol') return String(value);
|
|
88
|
+
if (value instanceof Error) return value.stack || value.message || String(value);
|
|
89
|
+
try {
|
|
90
|
+
return JSON.stringify(value);
|
|
91
|
+
} catch {
|
|
92
|
+
return Object.prototype.toString.call(value);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const pushEntry = (level, args) => {
|
|
97
|
+
const target = window[BUFFER_KEY];
|
|
98
|
+
if (!Array.isArray(target)) return;
|
|
99
|
+
const text = Array.from(args || []).map(stringify).join(' ');
|
|
100
|
+
target.push({
|
|
101
|
+
ts: now(),
|
|
102
|
+
level,
|
|
103
|
+
text,
|
|
104
|
+
href: String(window.location?.href || ''),
|
|
105
|
+
});
|
|
106
|
+
if (target.length > MAX) {
|
|
107
|
+
target.splice(0, target.length - MAX);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (!Array.isArray(window[BUFFER_KEY])) {
|
|
112
|
+
window[BUFFER_KEY] = [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!window[KEY]) {
|
|
116
|
+
const levels = ['log', 'info', 'warn', 'error', 'debug'];
|
|
117
|
+
const originals = {};
|
|
118
|
+
for (const level of levels) {
|
|
119
|
+
const raw = typeof console[level] === 'function' ? console[level] : console.log;
|
|
120
|
+
originals[level] = raw.bind(console);
|
|
121
|
+
console[level] = (...args) => {
|
|
122
|
+
try {
|
|
123
|
+
pushEntry(level, args);
|
|
124
|
+
} catch {}
|
|
125
|
+
return originals[level](...args);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
window.addEventListener('error', (event) => {
|
|
130
|
+
try {
|
|
131
|
+
const message = event?.message || 'window.error';
|
|
132
|
+
pushEntry('error', [message]);
|
|
133
|
+
} catch {}
|
|
134
|
+
});
|
|
135
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
136
|
+
try {
|
|
137
|
+
const reason = event?.reason instanceof Error
|
|
138
|
+
? (event.reason.stack || event.reason.message)
|
|
139
|
+
: stringify(event?.reason);
|
|
140
|
+
pushEntry('error', ['unhandledrejection', reason]);
|
|
141
|
+
} catch {}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
window[KEY] = { installedAt: now(), max: MAX };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
installed: true,
|
|
150
|
+
entries: Array.isArray(window[BUFFER_KEY]) ? window[BUFFER_KEY].length : 0,
|
|
151
|
+
max: MAX,
|
|
152
|
+
};
|
|
153
|
+
})();`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildConsoleReadScript(options = {}) {
|
|
157
|
+
const limit = clamp(parseNumber(options.limit, DEFAULT_LIMIT), 1, MAX_LIMIT);
|
|
158
|
+
const sinceTs = Math.max(0, parseNumber(options.sinceTs, 0) || 0);
|
|
159
|
+
const clear = options.clear === true;
|
|
160
|
+
const levels = Array.isArray(options.levels)
|
|
161
|
+
? options.levels.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean)
|
|
162
|
+
: [];
|
|
163
|
+
const levelsLiteral = JSON.stringify(levels);
|
|
164
|
+
|
|
165
|
+
return `(function readCamoDevtoolsConsole() {
|
|
166
|
+
const BUFFER_KEY = '__camo_console_buffer_v1__';
|
|
167
|
+
const raw = Array.isArray(window[BUFFER_KEY]) ? window[BUFFER_KEY] : [];
|
|
168
|
+
const levelSet = new Set(${levelsLiteral});
|
|
169
|
+
const list = raw.filter((entry) => {
|
|
170
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
171
|
+
const ts = Number(entry.ts || 0);
|
|
172
|
+
if (ts < ${sinceTs}) return false;
|
|
173
|
+
if (levelSet.size === 0) return true;
|
|
174
|
+
return levelSet.has(String(entry.level || '').toLowerCase());
|
|
175
|
+
});
|
|
176
|
+
const entries = list.slice(Math.max(0, list.length - ${limit}));
|
|
177
|
+
if (${clear ? 'true' : 'false'}) {
|
|
178
|
+
window[BUFFER_KEY] = [];
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
ok: true,
|
|
182
|
+
total: raw.length,
|
|
183
|
+
returned: entries.length,
|
|
184
|
+
sinceTs: ${sinceTs},
|
|
185
|
+
levels: Array.from(levelSet),
|
|
186
|
+
cleared: ${clear ? 'true' : 'false'},
|
|
187
|
+
entries,
|
|
188
|
+
};
|
|
189
|
+
})();`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function buildEvalScript(expression) {
|
|
193
|
+
return `(async function runCamoDevtoolsEval() {
|
|
194
|
+
const expr = ${JSON.stringify(expression || '')};
|
|
195
|
+
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
196
|
+
const resultPayload = { ok: true, mode: 'expression', value: null, valueType: null };
|
|
197
|
+
|
|
198
|
+
const toSerializable = (value, depth = 0, seen = new WeakSet()) => {
|
|
199
|
+
if (value === null) return null;
|
|
200
|
+
if (typeof value === 'undefined') return '[undefined]';
|
|
201
|
+
if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
202
|
+
if (typeof value === 'bigint') return value.toString();
|
|
203
|
+
if (typeof value === 'function') return '[function]';
|
|
204
|
+
if (typeof value === 'symbol') return value.toString();
|
|
205
|
+
if (value instanceof Error) return { name: value.name, message: value.message, stack: value.stack || null };
|
|
206
|
+
if (depth >= 3) return '[max-depth]';
|
|
207
|
+
if (Array.isArray(value)) return value.slice(0, 30).map((item) => toSerializable(item, depth + 1, seen));
|
|
208
|
+
if (typeof value === 'object') {
|
|
209
|
+
if (seen.has(value)) return '[circular]';
|
|
210
|
+
seen.add(value);
|
|
211
|
+
const out = {};
|
|
212
|
+
const keys = Object.keys(value).slice(0, 30);
|
|
213
|
+
for (const key of keys) {
|
|
214
|
+
out[key] = toSerializable(value[key], depth + 1, seen);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
return JSON.parse(JSON.stringify(value));
|
|
220
|
+
} catch {
|
|
221
|
+
return String(value);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const fn = new AsyncFunction('return (' + expr + ')');
|
|
227
|
+
const value = await fn();
|
|
228
|
+
resultPayload.value = toSerializable(value);
|
|
229
|
+
resultPayload.valueType = typeof value;
|
|
230
|
+
return resultPayload;
|
|
231
|
+
} catch (exprError) {
|
|
232
|
+
try {
|
|
233
|
+
const fn = new AsyncFunction(expr);
|
|
234
|
+
const value = await fn();
|
|
235
|
+
resultPayload.mode = 'statement';
|
|
236
|
+
resultPayload.value = toSerializable(value);
|
|
237
|
+
resultPayload.valueType = typeof value;
|
|
238
|
+
return resultPayload;
|
|
239
|
+
} catch (statementError) {
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
mode: 'statement',
|
|
243
|
+
error: {
|
|
244
|
+
message: statementError?.message || String(statementError),
|
|
245
|
+
stack: statementError?.stack || null,
|
|
246
|
+
expressionError: exprError?.message || String(exprError),
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
})();`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function ensureConsoleCollector(profileId, maxEntries = MAX_LIMIT) {
|
|
255
|
+
return callAPI('evaluate', {
|
|
256
|
+
profileId,
|
|
257
|
+
script: buildConsoleInstallScript(maxEntries),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function handleLogs(args) {
|
|
262
|
+
const { profileId } = pickProfileAndExpression(args, 'logs');
|
|
263
|
+
if (!profileId) {
|
|
264
|
+
throw new Error('No default profile set. Run: camo profile default <profileId>');
|
|
265
|
+
}
|
|
266
|
+
const limit = clamp(parseNumber(readFlagValue(args, ['--limit', '-n']), DEFAULT_LIMIT), 1, MAX_LIMIT);
|
|
267
|
+
const sinceTs = Math.max(0, parseNumber(readFlagValue(args, ['--since']), 0) || 0);
|
|
268
|
+
const levelsRaw = readFlagValue(args, ['--levels', '--level']) || '';
|
|
269
|
+
const levels = levelsRaw
|
|
270
|
+
.split(',')
|
|
271
|
+
.map((item) => String(item || '').trim().toLowerCase())
|
|
272
|
+
.filter(Boolean);
|
|
273
|
+
const clear = args.includes('--clear');
|
|
274
|
+
|
|
275
|
+
const install = await ensureConsoleCollector(profileId, MAX_LIMIT);
|
|
276
|
+
const result = await callAPI('evaluate', {
|
|
277
|
+
profileId,
|
|
278
|
+
script: buildConsoleReadScript({ limit, sinceTs, levels, clear }),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
console.log(JSON.stringify({
|
|
282
|
+
ok: true,
|
|
283
|
+
command: 'devtools.logs',
|
|
284
|
+
profileId,
|
|
285
|
+
collector: install?.result || install?.data || install || null,
|
|
286
|
+
result: result?.result || result?.data || result || null,
|
|
287
|
+
}, null, 2));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function handleClear(args) {
|
|
291
|
+
const { profileId } = pickProfileAndExpression(args, 'clear');
|
|
292
|
+
if (!profileId) {
|
|
293
|
+
throw new Error('No default profile set. Run: camo profile default <profileId>');
|
|
294
|
+
}
|
|
295
|
+
await ensureConsoleCollector(profileId, MAX_LIMIT);
|
|
296
|
+
const result = await callAPI('evaluate', {
|
|
297
|
+
profileId,
|
|
298
|
+
script: buildConsoleReadScript({ limit: MAX_LIMIT, sinceTs: 0, clear: true }),
|
|
299
|
+
});
|
|
300
|
+
console.log(JSON.stringify({
|
|
301
|
+
ok: true,
|
|
302
|
+
command: 'devtools.clear',
|
|
303
|
+
profileId,
|
|
304
|
+
result: result?.result || result?.data || result || null,
|
|
305
|
+
}, null, 2));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function handleEval(args) {
|
|
309
|
+
const { profileId, expression } = pickProfileAndExpression(args, 'eval');
|
|
310
|
+
if (!profileId) {
|
|
311
|
+
throw new Error('No default profile set. Run: camo profile default <profileId>');
|
|
312
|
+
}
|
|
313
|
+
if (!expression) {
|
|
314
|
+
throw new Error('Usage: camo devtools eval [profileId] <expression> [--profile <id>]');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
await ensureConsoleCollector(profileId, MAX_LIMIT);
|
|
318
|
+
const result = await callAPI('evaluate', {
|
|
319
|
+
profileId,
|
|
320
|
+
script: buildEvalScript(expression),
|
|
321
|
+
});
|
|
322
|
+
console.log(JSON.stringify({
|
|
323
|
+
ok: true,
|
|
324
|
+
command: 'devtools.eval',
|
|
325
|
+
profileId,
|
|
326
|
+
expression,
|
|
327
|
+
result: result?.result || result?.data || result || null,
|
|
328
|
+
}, null, 2));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export async function handleDevtoolsCommand(args) {
|
|
332
|
+
const sub = String(args[1] || '').trim().toLowerCase();
|
|
333
|
+
switch (sub) {
|
|
334
|
+
case 'logs':
|
|
335
|
+
return handleLogs(args);
|
|
336
|
+
case 'clear':
|
|
337
|
+
return handleClear(args);
|
|
338
|
+
case 'eval':
|
|
339
|
+
return handleEval(args);
|
|
340
|
+
default:
|
|
341
|
+
console.log(`Usage: camo devtools <logs|eval|clear> [options]
|
|
342
|
+
|
|
343
|
+
Commands:
|
|
344
|
+
logs [profileId] [--limit 120] [--since <unix_ms>] [--levels error,warn] [--clear]
|
|
345
|
+
eval [profileId] <expression> [--profile <id>]
|
|
346
|
+
clear [profileId]
|
|
347
|
+
`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -33,7 +33,20 @@ export function buildSelectorClickScript({ selector, highlight }) {
|
|
|
33
33
|
}
|
|
34
34
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
35
35
|
await new Promise((r) => setTimeout(r, 150));
|
|
36
|
-
el
|
|
36
|
+
if (el instanceof HTMLElement) {
|
|
37
|
+
try { el.focus({ preventScroll: true }); } catch {}
|
|
38
|
+
const common = { bubbles: true, cancelable: true, view: window };
|
|
39
|
+
try {
|
|
40
|
+
if (typeof PointerEvent === 'function') {
|
|
41
|
+
el.dispatchEvent(new PointerEvent('pointerdown', { ...common, pointerType: 'mouse', button: 0 }));
|
|
42
|
+
el.dispatchEvent(new PointerEvent('pointerup', { ...common, pointerType: 'mouse', button: 0 }));
|
|
43
|
+
}
|
|
44
|
+
} catch {}
|
|
45
|
+
try { el.dispatchEvent(new MouseEvent('mousedown', { ...common, button: 0 })); } catch {}
|
|
46
|
+
try { el.dispatchEvent(new MouseEvent('mouseup', { ...common, button: 0 })); } catch {}
|
|
47
|
+
}
|
|
48
|
+
if (typeof el.click === 'function') el.click();
|
|
49
|
+
else el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window, button: 0 }));
|
|
37
50
|
if (${highlightLiteral} && el instanceof HTMLElement) {
|
|
38
51
|
setTimeout(() => { el.style.outline = restoreOutline; }, 260);
|
|
39
52
|
}
|
|
@@ -56,9 +69,63 @@ export function buildSelectorTypeScript({ selector, highlight, text }) {
|
|
|
56
69
|
}
|
|
57
70
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
58
71
|
await new Promise((r) => setTimeout(r, 150));
|
|
59
|
-
el
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
if (el instanceof HTMLElement) {
|
|
73
|
+
try { el.focus({ preventScroll: true }); } catch {}
|
|
74
|
+
if (typeof el.click === 'function') el.click();
|
|
75
|
+
}
|
|
76
|
+
const value = ${textLiteral};
|
|
77
|
+
const fireInputEvent = (target, name, init) => {
|
|
78
|
+
try {
|
|
79
|
+
if (typeof InputEvent === 'function') {
|
|
80
|
+
target.dispatchEvent(new InputEvent(name, init));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
} catch {}
|
|
84
|
+
target.dispatchEvent(new Event(name, { bubbles: true, cancelable: init?.cancelable === true }));
|
|
85
|
+
};
|
|
86
|
+
const assignControlValue = (target, next) => {
|
|
87
|
+
if (target instanceof HTMLInputElement) {
|
|
88
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
|
89
|
+
if (setter) setter.call(target, next);
|
|
90
|
+
else target.value = next;
|
|
91
|
+
if (typeof target.setSelectionRange === 'function') {
|
|
92
|
+
const cursor = String(next).length;
|
|
93
|
+
try { target.setSelectionRange(cursor, cursor); } catch {}
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (target instanceof HTMLTextAreaElement) {
|
|
98
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
|
|
99
|
+
if (setter) setter.call(target, next);
|
|
100
|
+
else target.value = next;
|
|
101
|
+
if (typeof target.setSelectionRange === 'function') {
|
|
102
|
+
const cursor = String(next).length;
|
|
103
|
+
try { target.setSelectionRange(cursor, cursor); } catch {}
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
};
|
|
109
|
+
const editableAssigned = assignControlValue(el, value);
|
|
110
|
+
if (!editableAssigned) {
|
|
111
|
+
if (el instanceof HTMLElement && el.isContentEditable) {
|
|
112
|
+
el.textContent = value;
|
|
113
|
+
} else {
|
|
114
|
+
throw new Error('Element not editable: ' + ${selectorLiteral});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
fireInputEvent(el, 'beforeinput', {
|
|
118
|
+
bubbles: true,
|
|
119
|
+
cancelable: true,
|
|
120
|
+
data: value,
|
|
121
|
+
inputType: 'insertText',
|
|
122
|
+
});
|
|
123
|
+
fireInputEvent(el, 'input', {
|
|
124
|
+
bubbles: true,
|
|
125
|
+
cancelable: false,
|
|
126
|
+
data: value,
|
|
127
|
+
inputType: 'insertText',
|
|
128
|
+
});
|
|
62
129
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
63
130
|
if (${highlightLiteral} && el instanceof HTMLElement) {
|
|
64
131
|
setTimeout(() => { el.style.outline = restoreOutline; }, 260);
|
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] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]
|
|
27
|
+
start [profileId] [--url <url>] [--headless] [--devtools] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]
|
|
28
28
|
stop [profileId]
|
|
29
29
|
stop --id <instanceId> Stop by instance id
|
|
30
30
|
stop --alias <alias> Stop by alias
|
|
@@ -63,6 +63,11 @@ PAGES:
|
|
|
63
63
|
switch-page [profileId] <index>
|
|
64
64
|
list-pages [profileId]
|
|
65
65
|
|
|
66
|
+
DEVTOOLS:
|
|
67
|
+
devtools logs [profileId] [--limit 120] [--since <unix_ms>] [--levels error,warn] [--clear]
|
|
68
|
+
devtools eval [profileId] <expression> [--profile <id>]
|
|
69
|
+
devtools clear [profileId]
|
|
70
|
+
|
|
66
71
|
COOKIES:
|
|
67
72
|
cookies get [profileId] Get all cookies for profile
|
|
68
73
|
cookies save [profileId] --path <file> Save cookies to file
|
|
@@ -97,7 +102,10 @@ EXAMPLES:
|
|
|
97
102
|
camo profile default myprofile
|
|
98
103
|
camo start --url https://example.com --alias main
|
|
99
104
|
camo start worker-1 --headless --alias shard1 --idle-timeout 45m
|
|
105
|
+
camo start worker-1 --devtools
|
|
100
106
|
camo start myprofile --width 1920 --height 1020
|
|
107
|
+
camo devtools eval myprofile "document.title"
|
|
108
|
+
camo devtools logs myprofile --levels error,warn --limit 50
|
|
101
109
|
camo stop --id inst_xxxxxxxx
|
|
102
110
|
camo stop --alias shard1
|
|
103
111
|
camo stop idle
|