@web-auto/camo 0.1.24 → 0.1.25
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/package.json +1 -1
- package/src/cli.mjs +9 -0
- package/src/commands/browser.mjs +9 -7
- package/src/container/change-notifier.mjs +90 -39
- package/src/container/runtime-core/operations/index.mjs +108 -48
- package/src/container/runtime-core/operations/tab-pool.mjs +301 -99
- package/src/container/runtime-core/operations/tab-pool.mjs.bak +762 -0
- package/src/container/runtime-core/operations/tab-pool.mjs.syntax-error +762 -0
- package/src/container/runtime-core/operations/viewport.mjs +46 -0
- package/src/container/runtime-core/subscription.mjs +72 -7
- package/src/container/runtime-core/validation.mjs +61 -4
- package/src/core/utils.mjs +4 -0
- package/src/services/browser-service/index.js +27 -10
- package/src/services/browser-service/index.js.bak +671 -0
- package/src/services/browser-service/internal/BrowserSession.js +34 -2
- package/src/services/browser-service/internal/browser-session/page-management.js +120 -58
- package/src/services/browser-service/internal/browser-session/page-management.test.js +43 -0
- package/src/services/controller/controller.js +1 -1
- package/src/services/controller/transport.js +8 -1
- package/src/utils/args.mjs +1 -0
- package/src/utils/browser-service.mjs +13 -1
- package/src/utils/command-log.mjs +64 -0
- package/src/utils/help.mjs +3 -3
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import { handleSessionWatchdogCommand } from './lifecycle/session-watchdog.mjs';
|
|
34
34
|
import { safeAppendProgressEvent } from './events/progress-log.mjs';
|
|
35
35
|
import { ensureProgressEventDaemon } from './events/daemon.mjs';
|
|
36
|
+
import { appendCommandLog, buildCommandSenderMeta } from './utils/command-log.mjs';
|
|
36
37
|
|
|
37
38
|
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
38
39
|
const PACKAGE_JSON_PATH = path.resolve(CURRENT_DIR, '..', 'package.json');
|
|
@@ -119,6 +120,14 @@ function inferProfileId(cmd, args) {
|
|
|
119
120
|
async function runTrackedCommand(cmd, args, fn) {
|
|
120
121
|
const startedAt = Date.now();
|
|
121
122
|
const profileId = inferProfileId(cmd, args);
|
|
123
|
+
appendCommandLog({
|
|
124
|
+
action: cmd,
|
|
125
|
+
command: cmd,
|
|
126
|
+
profileId,
|
|
127
|
+
args: args.slice(1),
|
|
128
|
+
payload: { mode: 'cli' },
|
|
129
|
+
meta: buildCommandSenderMeta({ source: 'cli', cwd: process.cwd(), argv: process.argv.slice() }),
|
|
130
|
+
});
|
|
122
131
|
safeAppendProgressEvent({
|
|
123
132
|
source: 'cli.command',
|
|
124
133
|
mode: cmd === 'autoscript' ? 'autoscript' : 'normal',
|
package/src/commands/browser.mjs
CHANGED
|
@@ -496,6 +496,7 @@ export async function handleStartCommand(args) {
|
|
|
496
496
|
const alias = validateAlias(readFlagValue(args, ['--alias']));
|
|
497
497
|
const idleTimeoutRaw = readFlagValue(args, ['--idle-timeout']);
|
|
498
498
|
const parsedIdleTimeoutMs = parseDurationMs(idleTimeoutRaw, DEFAULT_HEADLESS_IDLE_TIMEOUT_MS);
|
|
499
|
+
const maxTabs = Math.max(1, Math.floor(Number(readFlagValue(args, ['--max-tabs']) || 1) || 1));
|
|
499
500
|
const wantsDevtools = args.includes('--devtools');
|
|
500
501
|
const wantsRecord = args.includes('--record');
|
|
501
502
|
const recordName = readFlagValue(args, ['--record-name']);
|
|
@@ -506,7 +507,7 @@ export async function handleStartCommand(args) {
|
|
|
506
507
|
? true
|
|
507
508
|
: null;
|
|
508
509
|
if (hasExplicitWidth !== hasExplicitHeight) {
|
|
509
|
-
throw new Error('Usage: camo start [profileId] [--url <url>] [--headless] [--devtools] [--record] [--record-name <name>] [--record-output <path>] [--record-overlay|--no-record-overlay] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]');
|
|
510
|
+
throw new Error('Usage: camo start [profileId] [--url <url>] [--no-headless|--visible] [--devtools] [--record] [--record-name <name>] [--record-output <path>] [--record-overlay|--no-record-overlay] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h> --max-tabs <n>]');
|
|
510
511
|
}
|
|
511
512
|
if ((hasExplicitWidth && explicitWidth < START_WINDOW_MIN_WIDTH) || (hasExplicitHeight && explicitHeight < START_WINDOW_MIN_HEIGHT)) {
|
|
512
513
|
throw new Error(`Window size too small. Minimum is ${START_WINDOW_MIN_WIDTH}x${START_WINDOW_MIN_HEIGHT}`);
|
|
@@ -527,8 +528,8 @@ export async function handleStartCommand(args) {
|
|
|
527
528
|
const arg = args[i];
|
|
528
529
|
if (arg === '--url') { i++; continue; }
|
|
529
530
|
if (arg === '--width' || arg === '--height') { i++; continue; }
|
|
530
|
-
if (arg === '--alias' || arg === '--idle-timeout' || arg === '--record-name' || arg === '--record-output') { i++; continue; }
|
|
531
|
-
if (arg === '--headless') continue;
|
|
531
|
+
if (arg === '--alias' || arg === '--idle-timeout' || arg === '--record-name' || arg === '--record-output' || arg === '--max-tabs') { i++; continue; }
|
|
532
|
+
if (arg === '--headless' || arg === '--no-headless' || arg === '--visible') continue;
|
|
532
533
|
if (arg === '--record' || arg === '--record-overlay' || arg === '--no-record-overlay') continue;
|
|
533
534
|
if (arg.startsWith('--')) continue;
|
|
534
535
|
|
|
@@ -615,9 +616,9 @@ export async function handleStartCommand(args) {
|
|
|
615
616
|
releaseLock(profileId);
|
|
616
617
|
}
|
|
617
618
|
|
|
618
|
-
const headless = args.includes('--headless');
|
|
619
|
+
const headless = !args.includes('--no-headless') && !args.includes('--visible');
|
|
619
620
|
if (wantsDevtools && headless) {
|
|
620
|
-
throw new Error('--devtools
|
|
621
|
+
throw new Error('--devtools requires --no-headless or --visible mode');
|
|
621
622
|
}
|
|
622
623
|
const idleTimeoutMs = headless ? parsedIdleTimeoutMs : 0;
|
|
623
624
|
const targetUrl = explicitUrl || implicitUrl;
|
|
@@ -627,6 +628,7 @@ export async function handleStartCommand(args) {
|
|
|
627
628
|
headless,
|
|
628
629
|
devtools: wantsDevtools,
|
|
629
630
|
...(wantsRecord ? { record: true } : {}),
|
|
631
|
+
...(Number.isFinite(maxTabs) ? { maxTabs } : {}),
|
|
630
632
|
...(recordName ? { recordName } : {}),
|
|
631
633
|
...(recordOutput ? { recordOutput } : {}),
|
|
632
634
|
...(recordOverlay !== null ? { recordOverlay } : {}),
|
|
@@ -655,8 +657,8 @@ export async function handleStartCommand(args) {
|
|
|
655
657
|
all: 'camo close all',
|
|
656
658
|
};
|
|
657
659
|
result.message = headless
|
|
658
|
-
? `Started
|
|
659
|
-
: 'Started session. Remember to stop it when finished.';
|
|
660
|
+
? `Started session. Idle timeout: ${formatDurationMs(idleTimeoutMs)}`
|
|
661
|
+
: 'Started visible session. Remember to stop it when finished.';
|
|
660
662
|
|
|
661
663
|
if (!headless) {
|
|
662
664
|
let windowTarget = null;
|
|
@@ -26,30 +26,43 @@ function parseCssSelector(css) {
|
|
|
26
26
|
const raw = typeof css === 'string' ? css.trim() : '';
|
|
27
27
|
if (!raw) return [];
|
|
28
28
|
const attrRegex = /\[\s*([^\s~|^$*=\]]+)\s*(\*=|\^=|\$=|=)?\s*(?:"([^"]*)"|'([^']*)'|([^\]\s]+))?\s*\]/g;
|
|
29
|
+
const parseSegment = (item) => {
|
|
30
|
+
const tagMatch = item.match(/^[a-zA-Z][\w-]*/);
|
|
31
|
+
const idMatch = item.match(/#([\w-]+)/);
|
|
32
|
+
const classMatches = item.match(/\.([\w-]+)/g) || [];
|
|
33
|
+
const attrs = [];
|
|
34
|
+
let attrMatch = attrRegex.exec(item);
|
|
35
|
+
while (attrMatch) {
|
|
36
|
+
attrs.push({
|
|
37
|
+
name: String(attrMatch[1] || '').toLowerCase(),
|
|
38
|
+
op: attrMatch[2] || 'exists',
|
|
39
|
+
value: attrMatch[3] ?? attrMatch[4] ?? attrMatch[5] ?? '',
|
|
40
|
+
});
|
|
41
|
+
attrMatch = attrRegex.exec(item);
|
|
42
|
+
}
|
|
43
|
+
attrRegex.lastIndex = 0;
|
|
44
|
+
return {
|
|
45
|
+
raw: item,
|
|
46
|
+
tag: tagMatch ? tagMatch[0].toLowerCase() : null,
|
|
47
|
+
id: idMatch ? idMatch[1] : null,
|
|
48
|
+
classes: classMatches.map((token) => token.slice(1)),
|
|
49
|
+
attrs,
|
|
50
|
+
};
|
|
51
|
+
};
|
|
29
52
|
return raw
|
|
30
53
|
.split(',')
|
|
31
54
|
.map((item) => item.trim())
|
|
32
55
|
.filter(Boolean)
|
|
33
56
|
.map((item) => {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
while (attrMatch) {
|
|
40
|
-
attrs.push({
|
|
41
|
-
name: String(attrMatch[1] || '').toLowerCase(),
|
|
42
|
-
op: attrMatch[2] || 'exists',
|
|
43
|
-
value: attrMatch[3] ?? attrMatch[4] ?? attrMatch[5] ?? '',
|
|
44
|
-
});
|
|
45
|
-
attrMatch = attrRegex.exec(item);
|
|
46
|
-
}
|
|
47
|
-
attrRegex.lastIndex = 0;
|
|
57
|
+
const segments = item
|
|
58
|
+
.split(/\s+/)
|
|
59
|
+
.map((segment) => segment.trim())
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.map((segment) => parseSegment(segment));
|
|
48
62
|
return {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
attrs,
|
|
63
|
+
raw: item,
|
|
64
|
+
segments,
|
|
65
|
+
...parseSegment(item),
|
|
53
66
|
};
|
|
54
67
|
});
|
|
55
68
|
}
|
|
@@ -82,6 +95,50 @@ function matchAttribute(node, attrSpec, nodeId, nodeClasses) {
|
|
|
82
95
|
return false;
|
|
83
96
|
}
|
|
84
97
|
|
|
98
|
+
function nodeMatchesCssSegment(node, cssSegment) {
|
|
99
|
+
const nodeTag = typeof node?.tag === 'string' ? node.tag.toLowerCase() : null;
|
|
100
|
+
const nodeId = typeof node?.id === 'string' ? node.id : null;
|
|
101
|
+
const nodeClasses = new Set(Array.isArray(node?.classes) ? node.classes : []);
|
|
102
|
+
|
|
103
|
+
const hasConstraints = Boolean(
|
|
104
|
+
cssSegment?.tag
|
|
105
|
+
|| cssSegment?.id
|
|
106
|
+
|| (cssSegment?.classes && cssSegment.classes.length > 0)
|
|
107
|
+
|| (cssSegment?.attrs && cssSegment.attrs.length > 0),
|
|
108
|
+
);
|
|
109
|
+
if (!hasConstraints) return false;
|
|
110
|
+
|
|
111
|
+
let matched = true;
|
|
112
|
+
if (cssSegment.tag && nodeTag !== cssSegment.tag) matched = false;
|
|
113
|
+
if (cssSegment.id && nodeId !== cssSegment.id) matched = false;
|
|
114
|
+
if (matched && cssSegment.classes.length > 0) {
|
|
115
|
+
matched = cssSegment.classes.every((className) => nodeClasses.has(className));
|
|
116
|
+
}
|
|
117
|
+
if (matched && cssSegment.attrs.length > 0) {
|
|
118
|
+
matched = cssSegment.attrs.every((attrSpec) => matchAttribute(node, attrSpec, nodeId, nodeClasses));
|
|
119
|
+
}
|
|
120
|
+
return matched;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function matchesAncestorChain(ancestors, segments) {
|
|
124
|
+
if (!Array.isArray(segments) || segments.length === 0) return true;
|
|
125
|
+
if (!Array.isArray(ancestors) || ancestors.length === 0) return false;
|
|
126
|
+
let ancestorIndex = ancestors.length - 1;
|
|
127
|
+
for (let segmentIndex = segments.length - 1; segmentIndex >= 0; segmentIndex -= 1) {
|
|
128
|
+
let found = false;
|
|
129
|
+
while (ancestorIndex >= 0) {
|
|
130
|
+
if (nodeMatchesCssSegment(ancestors[ancestorIndex], segments[segmentIndex])) {
|
|
131
|
+
found = true;
|
|
132
|
+
ancestorIndex -= 1;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
ancestorIndex -= 1;
|
|
136
|
+
}
|
|
137
|
+
if (!found) return false;
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
85
142
|
export class ChangeNotifier {
|
|
86
143
|
constructor() {
|
|
87
144
|
this.subscriptions = new Map(); // topic -> Set<callback>
|
|
@@ -214,17 +271,22 @@ export class ChangeNotifier {
|
|
|
214
271
|
const normalized = normalizeSelector(selector);
|
|
215
272
|
const runtimeContext = context || {
|
|
216
273
|
viewport: node?.__viewport || null,
|
|
274
|
+
ancestors: [],
|
|
217
275
|
};
|
|
218
276
|
|
|
219
277
|
// Check if current node matches
|
|
220
|
-
if (this.nodeMatchesSelector(node, normalized) && this.nodePassesVisibility(node, normalized, runtimeContext.viewport)) {
|
|
278
|
+
if (this.nodeMatchesSelector(node, normalized, runtimeContext.ancestors) && this.nodePassesVisibility(node, normalized, runtimeContext.viewport)) {
|
|
221
279
|
results.push({ ...node, path });
|
|
222
280
|
}
|
|
223
281
|
|
|
224
282
|
// Recurse into children
|
|
225
283
|
if (node.children) {
|
|
284
|
+
const childContext = {
|
|
285
|
+
...runtimeContext,
|
|
286
|
+
ancestors: [...runtimeContext.ancestors, node],
|
|
287
|
+
};
|
|
226
288
|
for (let i = 0; i < node.children.length; i++) {
|
|
227
|
-
const childResults = this.findElements(node.children[i], normalized, `${path}/${i}`,
|
|
289
|
+
const childResults = this.findElements(node.children[i], normalized, `${path}/${i}`, childContext);
|
|
228
290
|
results.push(...childResults);
|
|
229
291
|
}
|
|
230
292
|
}
|
|
@@ -233,7 +295,7 @@ export class ChangeNotifier {
|
|
|
233
295
|
}
|
|
234
296
|
|
|
235
297
|
// Check if node matches selector
|
|
236
|
-
nodeMatchesSelector(node, selector) {
|
|
298
|
+
nodeMatchesSelector(node, selector, ancestors = []) {
|
|
237
299
|
if (!node) return false;
|
|
238
300
|
const normalized = normalizeSelector(selector);
|
|
239
301
|
if (!normalized || typeof normalized !== 'object') return false;
|
|
@@ -248,24 +310,13 @@ export class ChangeNotifier {
|
|
|
248
310
|
const cssVariants = parseCssSelector(normalized.css);
|
|
249
311
|
if (cssVariants.length > 0) {
|
|
250
312
|
for (const cssVariant of cssVariants) {
|
|
251
|
-
const
|
|
252
|
-
cssVariant.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
);
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
let matched = true;
|
|
260
|
-
if (cssVariant.tag && nodeTag !== cssVariant.tag) matched = false;
|
|
261
|
-
if (cssVariant.id && nodeId !== cssVariant.id) matched = false;
|
|
262
|
-
if (matched && cssVariant.classes.length > 0) {
|
|
263
|
-
matched = cssVariant.classes.every((className) => nodeClasses.has(className));
|
|
264
|
-
}
|
|
265
|
-
if (matched && cssVariant.attrs.length > 0) {
|
|
266
|
-
matched = cssVariant.attrs.every((attrSpec) => matchAttribute(node, attrSpec, nodeId, nodeClasses));
|
|
267
|
-
}
|
|
268
|
-
if (matched) return true;
|
|
313
|
+
const segments = Array.isArray(cssVariant.segments) && cssVariant.segments.length > 0
|
|
314
|
+
? cssVariant.segments
|
|
315
|
+
: [cssVariant];
|
|
316
|
+
const targetSegment = segments[segments.length - 1];
|
|
317
|
+
if (!nodeMatchesCssSegment(node, targetSegment)) continue;
|
|
318
|
+
if (segments.length === 1) return true;
|
|
319
|
+
if (matchesAncestorChain(ancestors, segments.slice(0, -1))) return true;
|
|
269
320
|
}
|
|
270
321
|
}
|
|
271
322
|
|
|
@@ -94,6 +94,17 @@ function sleep(ms) {
|
|
|
94
94
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
async function pageScroll(profileId, deltaY, delayMs = 80) {
|
|
98
|
+
const raw = Number(deltaY) || 0;
|
|
99
|
+
if (!Number.isFinite(raw) || raw === 0) return;
|
|
100
|
+
const key = raw >= 0 ? 'PageDown' : 'PageUp';
|
|
101
|
+
const steps = Math.max(1, Math.min(8, Math.round(Math.abs(raw) / 420) || 1));
|
|
102
|
+
for (let step = 0; step < steps; step += 1) {
|
|
103
|
+
await callAPI('keyboard:press', { profileId, key });
|
|
104
|
+
if (delayMs > 0) await sleep(delayMs);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
97
108
|
function clamp(value, min, max) {
|
|
98
109
|
return Math.min(Math.max(value, min), max);
|
|
99
110
|
}
|
|
@@ -281,7 +292,8 @@ async function scrollTargetIntoViewport(profileId, selector, initialTarget, para
|
|
|
281
292
|
if (isTargetFullyInViewport(target, visibilityMargin)) break;
|
|
282
293
|
const delta = resolveViewportScrollDelta(target, visibilityMargin);
|
|
283
294
|
if (Math.abs(delta.deltaX) < 1 && Math.abs(delta.deltaY) < 1) break;
|
|
284
|
-
|
|
295
|
+
const deltaY = delta.deltaY !== 0 ? delta.deltaY : (delta.deltaX !== 0 ? delta.deltaX : 0);
|
|
296
|
+
await pageScroll(profileId, deltaY);
|
|
285
297
|
if (settleMs > 0) await sleep(settleMs);
|
|
286
298
|
target = await resolveSelectorTarget(profileId, selector, options);
|
|
287
299
|
}
|
|
@@ -468,6 +480,56 @@ async function executeVerifySubscriptions({ profileId, params }) {
|
|
|
468
480
|
|
|
469
481
|
const acrossPages = params.acrossPages === true;
|
|
470
482
|
const settleMs = Math.max(0, Number(params.settleMs ?? 280) || 280);
|
|
483
|
+
const pageUrlIncludes = normalizeArray(params.pageUrlIncludes)
|
|
484
|
+
.map((item) => String(item || '').trim())
|
|
485
|
+
.filter(Boolean);
|
|
486
|
+
const pageUrlExcludes = normalizeArray(params.pageUrlExcludes)
|
|
487
|
+
.map((item) => String(item || '').trim())
|
|
488
|
+
.filter(Boolean);
|
|
489
|
+
const pageUrlRegex = String(params.pageUrlRegex || '').trim();
|
|
490
|
+
const pageUrlNotRegex = String(params.pageUrlNotRegex || '').trim();
|
|
491
|
+
const requireMatchedPages = params.requireMatchedPages !== false;
|
|
492
|
+
|
|
493
|
+
let includeRegex = null;
|
|
494
|
+
if (pageUrlRegex) {
|
|
495
|
+
try {
|
|
496
|
+
includeRegex = new RegExp(pageUrlRegex);
|
|
497
|
+
} catch {
|
|
498
|
+
return asErrorPayload('OPERATION_FAILED', `invalid pageUrlRegex: ${pageUrlRegex}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
let excludeRegex = null;
|
|
502
|
+
if (pageUrlNotRegex) {
|
|
503
|
+
try {
|
|
504
|
+
excludeRegex = new RegExp(pageUrlNotRegex);
|
|
505
|
+
} catch {
|
|
506
|
+
return asErrorPayload('OPERATION_FAILED', `invalid pageUrlNotRegex: ${pageUrlNotRegex}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const hasPageFilter = (
|
|
511
|
+
pageUrlIncludes.length > 0
|
|
512
|
+
|| pageUrlExcludes.length > 0
|
|
513
|
+
|| Boolean(includeRegex)
|
|
514
|
+
|| Boolean(excludeRegex)
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
const shouldVerifyPage = (rawUrl) => {
|
|
518
|
+
const url = String(rawUrl || '').trim();
|
|
519
|
+
if (pageUrlIncludes.length > 0 && !pageUrlIncludes.some((part) => url.includes(part))) {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
if (pageUrlExcludes.length > 0 && pageUrlExcludes.some((part) => url.includes(part))) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
if (includeRegex && !includeRegex.test(url)) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
if (excludeRegex && excludeRegex.test(url)) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
return true;
|
|
532
|
+
};
|
|
471
533
|
|
|
472
534
|
const collectForCurrentPage = async () => {
|
|
473
535
|
const snapshot = await getDomSnapshotByProfile(profileId);
|
|
@@ -487,6 +549,8 @@ async function executeVerifySubscriptions({ profileId, params }) {
|
|
|
487
549
|
|
|
488
550
|
let pagesResult = [];
|
|
489
551
|
let overallOk = true;
|
|
552
|
+
let matchedPageCount = 0;
|
|
553
|
+
let activePageIndex = null;
|
|
490
554
|
if (!acrossPages) {
|
|
491
555
|
const current = await collectForCurrentPage();
|
|
492
556
|
overallOk = current.matches.every((item) => item.count >= item.minCount);
|
|
@@ -494,8 +558,19 @@ async function executeVerifySubscriptions({ profileId, params }) {
|
|
|
494
558
|
} else {
|
|
495
559
|
const listed = await callAPI('page:list', { profileId });
|
|
496
560
|
const { pages, activeIndex } = extractPageList(listed);
|
|
561
|
+
activePageIndex = Number.isFinite(activeIndex) ? activeIndex : null;
|
|
497
562
|
for (const page of pages) {
|
|
498
563
|
const pageIndex = Number(page.index);
|
|
564
|
+
const listedUrl = String(page.url || '');
|
|
565
|
+
if (!shouldVerifyPage(listedUrl)) {
|
|
566
|
+
pagesResult.push({
|
|
567
|
+
index: pageIndex,
|
|
568
|
+
url: listedUrl,
|
|
569
|
+
skipped: true,
|
|
570
|
+
ok: true,
|
|
571
|
+
});
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
499
574
|
if (Number.isFinite(activeIndex) && activeIndex !== pageIndex) {
|
|
500
575
|
await callAPI('page:switch', { profileId, index: pageIndex });
|
|
501
576
|
if (settleMs > 0) await new Promise((resolve) => setTimeout(resolve, settleMs));
|
|
@@ -504,15 +579,43 @@ async function executeVerifySubscriptions({ profileId, params }) {
|
|
|
504
579
|
const pageOk = current.matches.every((item) => item.count >= item.minCount);
|
|
505
580
|
overallOk = overallOk && pageOk;
|
|
506
581
|
pagesResult.push({ index: pageIndex, ...current, ok: pageOk });
|
|
582
|
+
matchedPageCount += 1;
|
|
507
583
|
}
|
|
508
584
|
if (Number.isFinite(activeIndex)) {
|
|
509
585
|
await callAPI('page:switch', { profileId, index: activeIndex });
|
|
510
586
|
}
|
|
511
587
|
}
|
|
512
588
|
|
|
589
|
+
if (acrossPages && hasPageFilter && requireMatchedPages && matchedPageCount === 0) {
|
|
590
|
+
const fallback = await collectForCurrentPage();
|
|
591
|
+
const fallbackOk = fallback.matches.every((item) => item.count >= item.minCount);
|
|
592
|
+
if (fallbackOk) {
|
|
593
|
+
matchedPageCount = 1;
|
|
594
|
+
overallOk = true;
|
|
595
|
+
pagesResult.push({
|
|
596
|
+
index: Number.isFinite(activePageIndex) ? activePageIndex : null,
|
|
597
|
+
urlMatched: false,
|
|
598
|
+
fallback: 'dom_match',
|
|
599
|
+
ok: true,
|
|
600
|
+
...fallback,
|
|
601
|
+
});
|
|
602
|
+
} else {
|
|
603
|
+
return asErrorPayload('SUBSCRIPTION_MISMATCH', 'no page matched verify_subscriptions pageUrl filter', {
|
|
604
|
+
acrossPages,
|
|
605
|
+
pageUrlIncludes,
|
|
606
|
+
pageUrlExcludes,
|
|
607
|
+
pageUrlRegex: pageUrlRegex || null,
|
|
608
|
+
pageUrlNotRegex: pageUrlNotRegex || null,
|
|
609
|
+
pages: pagesResult,
|
|
610
|
+
fallback,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
513
615
|
if (!overallOk) {
|
|
514
616
|
return asErrorPayload('SUBSCRIPTION_MISMATCH', 'subscription selectors missing on one or more pages', {
|
|
515
617
|
acrossPages,
|
|
618
|
+
matchedPageCount,
|
|
516
619
|
pages: pagesResult,
|
|
517
620
|
});
|
|
518
621
|
}
|
|
@@ -521,7 +624,7 @@ async function executeVerifySubscriptions({ profileId, params }) {
|
|
|
521
624
|
ok: true,
|
|
522
625
|
code: 'OPERATION_DONE',
|
|
523
626
|
message: 'verify_subscriptions done',
|
|
524
|
-
data: { acrossPages, pages: pagesResult },
|
|
627
|
+
data: { acrossPages, matchedPageCount, pages: pagesResult },
|
|
525
628
|
};
|
|
526
629
|
}
|
|
527
630
|
|
|
@@ -617,48 +720,12 @@ export async function executeOperation({ profileId, operation, context = {} }) {
|
|
|
617
720
|
deltaX = amount;
|
|
618
721
|
deltaY = 0;
|
|
619
722
|
}
|
|
620
|
-
const
|
|
621
|
-
profileId: resolvedProfile,
|
|
622
|
-
containerId: params.containerId || operation?.containerId || null,
|
|
623
|
-
selector: params.selector || operation?.selector || null,
|
|
624
|
-
});
|
|
625
|
-
const anchor = await resolveScrollAnchor(resolvedProfile, {
|
|
626
|
-
selector: anchorSelector,
|
|
627
|
-
filterMode,
|
|
628
|
-
});
|
|
629
|
-
if (!anchor?.ok || !anchor?.center) {
|
|
630
|
-
return asErrorPayload('OPERATION_FAILED', 'visible scroll container not found');
|
|
631
|
-
}
|
|
632
|
-
await callAPI('mouse:click', {
|
|
633
|
-
profileId: resolvedProfile,
|
|
634
|
-
x: anchor.center.x,
|
|
635
|
-
y: anchor.center.y,
|
|
636
|
-
button: 'left',
|
|
637
|
-
clicks: 1,
|
|
638
|
-
delay: 30,
|
|
639
|
-
});
|
|
640
|
-
const result = await callAPI('mouse:wheel', {
|
|
641
|
-
profileId: resolvedProfile,
|
|
642
|
-
deltaX,
|
|
643
|
-
deltaY,
|
|
644
|
-
anchorX: anchor.center.x,
|
|
645
|
-
anchorY: anchor.center.y,
|
|
646
|
-
});
|
|
723
|
+
const result = await pageScroll(resolvedProfile, deltaY);
|
|
647
724
|
return {
|
|
648
725
|
ok: true,
|
|
649
726
|
code: 'OPERATION_DONE',
|
|
650
727
|
message: 'scroll done',
|
|
651
|
-
data: {
|
|
652
|
-
direction,
|
|
653
|
-
amount,
|
|
654
|
-
deltaX,
|
|
655
|
-
deltaY,
|
|
656
|
-
filterMode,
|
|
657
|
-
anchorSource: String(anchor?.source || 'document'),
|
|
658
|
-
anchorCenter: anchor?.center || null,
|
|
659
|
-
modalLocked: anchor?.modalLocked === true,
|
|
660
|
-
result,
|
|
661
|
-
},
|
|
728
|
+
data: { direction, amount, deltaX, deltaY, result },
|
|
662
729
|
};
|
|
663
730
|
}
|
|
664
731
|
|
|
@@ -679,13 +746,7 @@ export async function executeOperation({ profileId, operation, context = {} }) {
|
|
|
679
746
|
}
|
|
680
747
|
|
|
681
748
|
if (action === 'evaluate') {
|
|
682
|
-
|
|
683
|
-
return asErrorPayload('JS_DISABLED', 'evaluate is disabled by default. Re-run camo command with --js.');
|
|
684
|
-
}
|
|
685
|
-
const script = String(params.script || '').trim();
|
|
686
|
-
if (!script) return asErrorPayload('OPERATION_FAILED', 'evaluate requires params.script');
|
|
687
|
-
const result = await callAPI('evaluate', { profileId: resolvedProfile, script });
|
|
688
|
-
return { ok: true, code: 'OPERATION_DONE', message: 'evaluate done', data: result };
|
|
749
|
+
return asErrorPayload('JS_DISABLED', 'evaluate is disabled in camo runtime');
|
|
689
750
|
}
|
|
690
751
|
|
|
691
752
|
if (action === 'click' || action === 'type' || action === 'scroll_into_view') {
|
|
@@ -694,7 +755,6 @@ export async function executeOperation({ profileId, operation, context = {} }) {
|
|
|
694
755
|
action,
|
|
695
756
|
operation,
|
|
696
757
|
params,
|
|
697
|
-
filterMode,
|
|
698
758
|
});
|
|
699
759
|
}
|
|
700
760
|
|