@web-auto/camo 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/autoscript/action-providers/xhs/comments.mjs +38 -2
- package/src/autoscript/action-providers/xhs/interaction.mjs +47 -2
- package/src/commands/browser.mjs +127 -26
- package/src/container/runtime-core/operations/selector-scripts.mjs +71 -4
- package/src/utils/help.mjs +2 -1
package/README.md
CHANGED
|
@@ -39,6 +39,9 @@ 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
|
+
|
|
42
45
|
# Navigate
|
|
43
46
|
camo goto https://www.xiaohongshu.com
|
|
44
47
|
|
|
@@ -71,7 +74,7 @@ camo create fingerprint --os <os> --region <region>
|
|
|
71
74
|
### Browser Control
|
|
72
75
|
|
|
73
76
|
```bash
|
|
74
|
-
camo start [profileId] [--url <url>] [--headless] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]
|
|
77
|
+
camo start [profileId] [--url <url>] [--headless] [--devtools] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]
|
|
75
78
|
camo stop [profileId]
|
|
76
79
|
camo stop --id <instanceId>
|
|
77
80
|
camo stop --alias <alias>
|
|
@@ -85,6 +88,7 @@ camo shutdown # Shutdown browser-service (all sessions)
|
|
|
85
88
|
If no saved size exists, it defaults to near-fullscreen (full width, slight vertical reserve).
|
|
86
89
|
Use `--width/--height` to override and update the saved profile size.
|
|
87
90
|
For headless sessions, default idle timeout is `30m` (auto-stop on inactivity). Use `--idle-timeout` (e.g. `45m`, `1800s`, `0`) to customize.
|
|
91
|
+
Use `--devtools` to open browser developer tools in headed mode (cannot be combined with `--headless`).
|
|
88
92
|
|
|
89
93
|
### Lifecycle & Cleanup
|
|
90
94
|
|
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/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
|
}
|
|
@@ -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
|
|
@@ -97,6 +97,7 @@ EXAMPLES:
|
|
|
97
97
|
camo profile default myprofile
|
|
98
98
|
camo start --url https://example.com --alias main
|
|
99
99
|
camo start worker-1 --headless --alias shard1 --idle-timeout 45m
|
|
100
|
+
camo start worker-1 --devtools
|
|
100
101
|
camo start myprofile --width 1920 --height 1020
|
|
101
102
|
camo stop --id inst_xxxxxxxx
|
|
102
103
|
camo stop --alias shard1
|