@web-auto/camo 0.1.14 → 0.1.16
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 -37
- package/package.json +1 -1
- package/src/autoscript/action-providers/index.mjs +3 -6
- package/src/autoscript/runtime.mjs +14 -12
- package/src/autoscript/schema.mjs +6 -0
- package/src/cli.mjs +5 -1
- package/src/commands/autoscript.mjs +14 -103
- package/src/commands/browser.mjs +247 -19
- package/src/commands/mouse.mjs +9 -3
- package/src/container/runtime-core/checkpoint.mjs +21 -7
- package/src/container/runtime-core/operations/index.mjs +392 -38
- package/src/container/runtime-core/subscription.mjs +79 -7
- package/src/container/runtime-core/validation.mjs +2 -2
- package/src/utils/browser-service.mjs +41 -6
- package/src/utils/help.mjs +0 -1
- package/src/utils/js-policy.mjs +13 -0
- package/src/autoscript/action-providers/xhs/comments.mjs +0 -412
- package/src/autoscript/action-providers/xhs/common.mjs +0 -77
- package/src/autoscript/action-providers/xhs/detail.mjs +0 -181
- package/src/autoscript/action-providers/xhs/interaction.mjs +0 -466
- package/src/autoscript/action-providers/xhs/like-rules.mjs +0 -57
- package/src/autoscript/action-providers/xhs/persistence.mjs +0 -167
- package/src/autoscript/action-providers/xhs/search.mjs +0 -174
- package/src/autoscript/action-providers/xhs.mjs +0 -133
- package/src/autoscript/xhs-unified-template.mjs +0 -934
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { callAPI, getDomSnapshotByProfile } from '../../../utils/browser-service.mjs';
|
|
2
|
+
import { isJsExecutionEnabled } from '../../../utils/js-policy.mjs';
|
|
2
3
|
import { executeTabPoolOperation } from './tab-pool.mjs';
|
|
3
|
-
import {
|
|
4
|
-
buildSelectorClickScript,
|
|
5
|
-
buildSelectorScrollIntoViewScript,
|
|
6
|
-
buildSelectorTypeScript,
|
|
7
|
-
} from './selector-scripts.mjs';
|
|
8
4
|
import { executeViewportOperation } from './viewport.mjs';
|
|
9
5
|
import {
|
|
10
6
|
asErrorPayload,
|
|
@@ -27,6 +23,23 @@ const VIEWPORT_ACTIONS = new Set([
|
|
|
27
23
|
'get_current_url',
|
|
28
24
|
]);
|
|
29
25
|
|
|
26
|
+
const DEFAULT_MODAL_SELECTORS = [
|
|
27
|
+
'[aria-modal="true"]',
|
|
28
|
+
'[role="dialog"]',
|
|
29
|
+
'.modal',
|
|
30
|
+
'.dialog',
|
|
31
|
+
'.note-detail-mask',
|
|
32
|
+
'.note-detail-page',
|
|
33
|
+
'.note-detail-dialog',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function resolveFilterMode(input) {
|
|
37
|
+
const text = String(input || process.env.CAMO_FILTER_MODE || 'strict').trim().toLowerCase();
|
|
38
|
+
if (!text) return 'strict';
|
|
39
|
+
if (text === 'legacy') return 'legacy';
|
|
40
|
+
return 'strict';
|
|
41
|
+
}
|
|
42
|
+
|
|
30
43
|
async function executeExternalOperationIfAny({
|
|
31
44
|
profileId,
|
|
32
45
|
action,
|
|
@@ -54,6 +67,7 @@ async function executeExternalOperationIfAny({
|
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
async function flashOperationViewport(profileId, params = {}) {
|
|
70
|
+
if (!isJsExecutionEnabled()) return;
|
|
57
71
|
if (params.highlight === false) return;
|
|
58
72
|
try {
|
|
59
73
|
await callAPI('evaluate', {
|
|
@@ -77,7 +91,272 @@ async function flashOperationViewport(profileId, params = {}) {
|
|
|
77
91
|
}
|
|
78
92
|
}
|
|
79
93
|
|
|
80
|
-
|
|
94
|
+
function sleep(ms) {
|
|
95
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function clamp(value, min, max) {
|
|
99
|
+
return Math.min(Math.max(value, min), max);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isTargetFullyInViewport(target, margin = 6) {
|
|
103
|
+
const rect = target?.rect && typeof target.rect === 'object' ? target.rect : null;
|
|
104
|
+
const viewport = target?.viewport && typeof target.viewport === 'object' ? target.viewport : null;
|
|
105
|
+
if (!rect || !viewport) return true;
|
|
106
|
+
const vw = Number(viewport.width || 0);
|
|
107
|
+
const vh = Number(viewport.height || 0);
|
|
108
|
+
if (!Number.isFinite(vw) || !Number.isFinite(vh) || vw <= 0 || vh <= 0) return true;
|
|
109
|
+
const left = Number(rect.left || 0);
|
|
110
|
+
const top = Number(rect.top || 0);
|
|
111
|
+
const width = Math.max(0, Number(rect.width || 0));
|
|
112
|
+
const height = Math.max(0, Number(rect.height || 0));
|
|
113
|
+
const right = left + width;
|
|
114
|
+
const bottom = top + height;
|
|
115
|
+
const m = Math.max(0, Number(margin) || 0);
|
|
116
|
+
return left >= m && top >= m && right <= (vw - m) && bottom <= (vh - m);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveViewportScrollDelta(target, margin = 6) {
|
|
120
|
+
const rect = target?.rect && typeof target.rect === 'object' ? target.rect : null;
|
|
121
|
+
const viewport = target?.viewport && typeof target.viewport === 'object' ? target.viewport : null;
|
|
122
|
+
if (!rect || !viewport) return { deltaX: 0, deltaY: 0 };
|
|
123
|
+
const vw = Number(viewport.width || 0);
|
|
124
|
+
const vh = Number(viewport.height || 0);
|
|
125
|
+
if (!Number.isFinite(vw) || !Number.isFinite(vh) || vw <= 0 || vh <= 0) return { deltaX: 0, deltaY: 0 };
|
|
126
|
+
const left = Number(rect.left || 0);
|
|
127
|
+
const top = Number(rect.top || 0);
|
|
128
|
+
const width = Math.max(0, Number(rect.width || 0));
|
|
129
|
+
const height = Math.max(0, Number(rect.height || 0));
|
|
130
|
+
const right = left + width;
|
|
131
|
+
const bottom = top + height;
|
|
132
|
+
const m = Math.max(0, Number(margin) || 0);
|
|
133
|
+
|
|
134
|
+
let deltaX = 0;
|
|
135
|
+
let deltaY = 0;
|
|
136
|
+
|
|
137
|
+
if (left < m) {
|
|
138
|
+
deltaX = Math.round(left - m);
|
|
139
|
+
} else if (right > (vw - m)) {
|
|
140
|
+
deltaX = Math.round(right - (vw - m));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (top < m) {
|
|
144
|
+
deltaY = Math.round(top - m);
|
|
145
|
+
} else if (bottom > (vh - m)) {
|
|
146
|
+
deltaY = Math.round(bottom - (vh - m));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (Math.abs(deltaY) < 80 && !isTargetFullyInViewport(target, m)) {
|
|
150
|
+
deltaY = deltaY >= 0 ? 120 : -120;
|
|
151
|
+
}
|
|
152
|
+
if (Math.abs(deltaX) < 40 && (left < m || right > (vw - m))) {
|
|
153
|
+
deltaX = deltaX >= 0 ? 60 : -60;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
deltaX: clamp(deltaX, -900, 900),
|
|
158
|
+
deltaY: clamp(deltaY, -900, 900),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function normalizeRect(node) {
|
|
163
|
+
const rect = node?.rect && typeof node.rect === 'object' ? node.rect : null;
|
|
164
|
+
if (!rect) return null;
|
|
165
|
+
const left = Number(rect.left ?? rect.x ?? 0);
|
|
166
|
+
const top = Number(rect.top ?? rect.y ?? 0);
|
|
167
|
+
const width = Number(rect.width ?? 0);
|
|
168
|
+
const height = Number(rect.height ?? 0);
|
|
169
|
+
if (!Number.isFinite(left) || !Number.isFinite(top) || !Number.isFinite(width) || !Number.isFinite(height)) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
if (width <= 0 || height <= 0) return null;
|
|
173
|
+
return { left, top, width, height };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function nodeArea(node) {
|
|
177
|
+
const rect = normalizeRect(node);
|
|
178
|
+
if (!rect) return 0;
|
|
179
|
+
return Number(rect.width || 0) * Number(rect.height || 0);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function nodeCenter(node, viewport = null) {
|
|
183
|
+
const rect = normalizeRect(node);
|
|
184
|
+
const vw = Number(viewport?.width || 0);
|
|
185
|
+
const vh = Number(viewport?.height || 0);
|
|
186
|
+
if (!rect) return null;
|
|
187
|
+
const rawX = rect.left + Math.max(1, rect.width / 2);
|
|
188
|
+
const rawY = rect.top + Math.max(1, rect.height / 2);
|
|
189
|
+
const centerX = vw > 1
|
|
190
|
+
? clamp(Math.round(rawX), 1, Math.max(1, vw - 1))
|
|
191
|
+
: Math.max(1, Math.round(rawX));
|
|
192
|
+
const centerY = vh > 1
|
|
193
|
+
? clamp(Math.round(rawY), 1, Math.max(1, vh - 1))
|
|
194
|
+
: Math.max(1, Math.round(rawY));
|
|
195
|
+
return {
|
|
196
|
+
center: { x: centerX, y: centerY },
|
|
197
|
+
rawCenter: { x: rawX, y: rawY },
|
|
198
|
+
rect,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getSnapshotViewport(snapshot) {
|
|
203
|
+
const width = Number(snapshot?.__viewport?.width || 0);
|
|
204
|
+
const height = Number(snapshot?.__viewport?.height || 0);
|
|
205
|
+
return { width, height };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isPathWithin(path, parentPath) {
|
|
209
|
+
const child = String(path || '').trim();
|
|
210
|
+
const parent = String(parentPath || '').trim();
|
|
211
|
+
if (!child || !parent) return false;
|
|
212
|
+
return child === parent || child.startsWith(`${parent}/`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function resolveActiveModal(snapshot) {
|
|
216
|
+
if (!snapshot) return null;
|
|
217
|
+
const rows = [];
|
|
218
|
+
for (const selector of DEFAULT_MODAL_SELECTORS) {
|
|
219
|
+
const matches = buildSelectorCheck(snapshot, { css: selector, visible: true });
|
|
220
|
+
for (const node of matches) {
|
|
221
|
+
if (nodeArea(node) <= 1) continue;
|
|
222
|
+
rows.push({
|
|
223
|
+
selector,
|
|
224
|
+
path: String(node.path || ''),
|
|
225
|
+
node,
|
|
226
|
+
area: nodeArea(node),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
rows.sort((a, b) => b.area - a.area);
|
|
231
|
+
return rows[0] || null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function resolveSelectorTarget(profileId, selector, options = {}) {
|
|
235
|
+
const filterMode = resolveFilterMode(options.filterMode);
|
|
236
|
+
const strictFilter = filterMode !== 'legacy';
|
|
237
|
+
const normalizedSelector = String(selector || '').trim();
|
|
238
|
+
const snapshot = await getDomSnapshotByProfile(profileId);
|
|
239
|
+
const viewport = getSnapshotViewport(snapshot);
|
|
240
|
+
const modal = strictFilter ? resolveActiveModal(snapshot) : null;
|
|
241
|
+
const visibleMatches = buildSelectorCheck(snapshot, { css: normalizedSelector, visible: true });
|
|
242
|
+
const allMatches = strictFilter
|
|
243
|
+
? visibleMatches
|
|
244
|
+
: buildSelectorCheck(snapshot, { css: normalizedSelector, visible: false });
|
|
245
|
+
const scopedVisible = modal
|
|
246
|
+
? visibleMatches.filter((item) => isPathWithin(item.path, modal.path))
|
|
247
|
+
: visibleMatches;
|
|
248
|
+
const scopedAll = modal
|
|
249
|
+
? allMatches.filter((item) => isPathWithin(item.path, modal.path))
|
|
250
|
+
: allMatches;
|
|
251
|
+
const candidate = strictFilter
|
|
252
|
+
? (scopedVisible[0] || null)
|
|
253
|
+
: (scopedVisible[0] || scopedAll[0] || null);
|
|
254
|
+
if (!candidate) {
|
|
255
|
+
if (modal) {
|
|
256
|
+
throw new Error(`Modal focus locked for selector: ${normalizedSelector}`);
|
|
257
|
+
}
|
|
258
|
+
throw new Error(`Element not found: ${normalizedSelector}`);
|
|
259
|
+
}
|
|
260
|
+
const center = nodeCenter(candidate, viewport);
|
|
261
|
+
if (!center) {
|
|
262
|
+
throw new Error(`Element not found: ${normalizedSelector}`);
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
ok: true,
|
|
266
|
+
selector: normalizedSelector,
|
|
267
|
+
matchedIndex: Math.max(0, scopedAll.indexOf(candidate)),
|
|
268
|
+
center: center.center,
|
|
269
|
+
rawCenter: center.rawCenter,
|
|
270
|
+
rect: center.rect,
|
|
271
|
+
viewport,
|
|
272
|
+
modalLocked: Boolean(modal),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function scrollTargetIntoViewport(profileId, selector, initialTarget, params = {}, options = {}) {
|
|
277
|
+
let target = initialTarget;
|
|
278
|
+
const maxSteps = Math.max(0, Math.min(24, Number(params.maxScrollSteps ?? 8) || 8));
|
|
279
|
+
const settleMs = Math.max(0, Number(params.scrollSettleMs ?? 140) || 140);
|
|
280
|
+
const visibilityMargin = Math.max(0, Number(params.visibilityMargin ?? params.viewportMargin ?? 6) || 6);
|
|
281
|
+
for (let i = 0; i < maxSteps; i += 1) {
|
|
282
|
+
if (isTargetFullyInViewport(target, visibilityMargin)) break;
|
|
283
|
+
const delta = resolveViewportScrollDelta(target, visibilityMargin);
|
|
284
|
+
if (Math.abs(delta.deltaX) < 1 && Math.abs(delta.deltaY) < 1) break;
|
|
285
|
+
const anchorX = clamp(Math.round(Number(target?.center?.x || 0) || 1), 1, Math.max(1, Number(target?.viewport?.width || 1) - 1));
|
|
286
|
+
const anchorY = clamp(Math.round(Number(target?.center?.y || 0) || 1), 1, Math.max(1, Number(target?.viewport?.height || 1) - 1));
|
|
287
|
+
await callAPI('mouse:move', { profileId, x: anchorX, y: anchorY, steps: 1 });
|
|
288
|
+
await callAPI('mouse:wheel', { profileId, deltaX: delta.deltaX, deltaY: delta.deltaY });
|
|
289
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
290
|
+
target = await resolveSelectorTarget(profileId, selector, options);
|
|
291
|
+
}
|
|
292
|
+
return target;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function resolveScrollAnchor(profileId, options = {}) {
|
|
296
|
+
const filterMode = resolveFilterMode(options.filterMode);
|
|
297
|
+
const strictFilter = filterMode !== 'legacy';
|
|
298
|
+
const selector = String(options.selector || '').trim();
|
|
299
|
+
const snapshot = await getDomSnapshotByProfile(profileId);
|
|
300
|
+
const viewport = getSnapshotViewport(snapshot);
|
|
301
|
+
const modal = strictFilter ? resolveActiveModal(snapshot) : null;
|
|
302
|
+
|
|
303
|
+
if (selector) {
|
|
304
|
+
const visibleMatches = buildSelectorCheck(snapshot, { css: selector, visible: true });
|
|
305
|
+
const target = visibleMatches[0] || null;
|
|
306
|
+
if (target) {
|
|
307
|
+
if (modal && !isPathWithin(target.path, modal.path)) {
|
|
308
|
+
const modalCenter = nodeCenter(modal.node, viewport);
|
|
309
|
+
if (modalCenter) {
|
|
310
|
+
return {
|
|
311
|
+
ok: true,
|
|
312
|
+
source: 'modal',
|
|
313
|
+
center: modalCenter.center,
|
|
314
|
+
modalLocked: true,
|
|
315
|
+
modalSelector: modal.selector,
|
|
316
|
+
selectorRejectedByModalLock: true,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
const targetCenter = nodeCenter(target, viewport);
|
|
321
|
+
if (targetCenter) {
|
|
322
|
+
return {
|
|
323
|
+
ok: true,
|
|
324
|
+
source: 'selector',
|
|
325
|
+
center: targetCenter.center,
|
|
326
|
+
modalLocked: Boolean(modal),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (modal) {
|
|
334
|
+
const modalCenter = nodeCenter(modal.node, viewport);
|
|
335
|
+
if (modalCenter) {
|
|
336
|
+
return {
|
|
337
|
+
ok: true,
|
|
338
|
+
source: 'modal',
|
|
339
|
+
center: modalCenter.center,
|
|
340
|
+
modalLocked: true,
|
|
341
|
+
modalSelector: modal.selector,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const width = Number(viewport.width || 0);
|
|
347
|
+
const height = Number(viewport.height || 0);
|
|
348
|
+
return {
|
|
349
|
+
ok: true,
|
|
350
|
+
source: 'document',
|
|
351
|
+
center: {
|
|
352
|
+
x: width > 1 ? Math.round(width / 2) : 1,
|
|
353
|
+
y: height > 1 ? Math.round(height / 2) : 1,
|
|
354
|
+
},
|
|
355
|
+
modalLocked: false,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function executeSelectorOperation({ profileId, action, operation, params, filterMode }) {
|
|
81
360
|
const selector = maybeSelector({
|
|
82
361
|
profileId,
|
|
83
362
|
containerId: params.containerId || operation?.containerId || null,
|
|
@@ -85,22 +364,75 @@ async function executeSelectorOperation({ profileId, action, operation, params }
|
|
|
85
364
|
});
|
|
86
365
|
if (!selector) return asErrorPayload('CONTAINER_NOT_FOUND', `${action} requires selector/containerId`);
|
|
87
366
|
|
|
88
|
-
|
|
367
|
+
let target = await resolveSelectorTarget(profileId, selector, { filterMode });
|
|
368
|
+
target = await scrollTargetIntoViewport(profileId, selector, target, params, { filterMode });
|
|
369
|
+
const visibilityMargin = Math.max(0, Number(params.visibilityMargin ?? params.viewportMargin ?? 6) || 6);
|
|
370
|
+
const targetFullyVisible = isTargetFullyInViewport(target, visibilityMargin);
|
|
371
|
+
if (action === 'click' && !targetFullyVisible) {
|
|
372
|
+
return asErrorPayload('TARGET_NOT_FULLY_VISIBLE', 'click target is not fully visible after auto scroll', {
|
|
373
|
+
selector,
|
|
374
|
+
target,
|
|
375
|
+
visibilityMargin,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
await callAPI('mouse:move', { profileId, x: target.center.x, y: target.center.y, steps: 2 });
|
|
380
|
+
|
|
89
381
|
if (action === 'scroll_into_view') {
|
|
90
|
-
|
|
91
|
-
|
|
382
|
+
return {
|
|
383
|
+
ok: true,
|
|
384
|
+
code: 'OPERATION_DONE',
|
|
385
|
+
message: 'scroll_into_view done',
|
|
386
|
+
data: { selector, target, targetFullyVisible, visibilityMargin },
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (action === 'click') {
|
|
391
|
+
const button = String(params.button || 'left').trim() || 'left';
|
|
392
|
+
const clicks = Math.max(1, Number(params.clicks ?? 1) || 1);
|
|
393
|
+
const delay = Number(params.delay);
|
|
394
|
+
const result = await callAPI('mouse:click', {
|
|
92
395
|
profileId,
|
|
93
|
-
|
|
396
|
+
x: target.center.x,
|
|
397
|
+
y: target.center.y,
|
|
398
|
+
button,
|
|
399
|
+
clicks,
|
|
400
|
+
...(Number.isFinite(delay) && delay >= 0 ? { delay } : {}),
|
|
94
401
|
});
|
|
95
|
-
return { ok: true, code: 'OPERATION_DONE', message: '
|
|
402
|
+
return { ok: true, code: 'OPERATION_DONE', message: 'click done', data: { selector, target, result, targetFullyVisible, visibilityMargin } };
|
|
96
403
|
}
|
|
97
404
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
:
|
|
102
|
-
|
|
103
|
-
|
|
405
|
+
const text = String(params.text ?? params.value ?? '');
|
|
406
|
+
await callAPI('mouse:click', {
|
|
407
|
+
profileId,
|
|
408
|
+
x: target.center.x,
|
|
409
|
+
y: target.center.y,
|
|
410
|
+
button: 'left',
|
|
411
|
+
clicks: 1,
|
|
412
|
+
});
|
|
413
|
+
const clearBeforeType = params.clear !== false;
|
|
414
|
+
if (clearBeforeType) {
|
|
415
|
+
await callAPI('keyboard:press', {
|
|
416
|
+
profileId,
|
|
417
|
+
key: process.platform === 'darwin' ? 'Meta+A' : 'Control+A',
|
|
418
|
+
});
|
|
419
|
+
await callAPI('keyboard:press', { profileId, key: 'Backspace' });
|
|
420
|
+
}
|
|
421
|
+
const delay = Number(params.keyDelayMs ?? params.delay);
|
|
422
|
+
await callAPI('keyboard:type', {
|
|
423
|
+
profileId,
|
|
424
|
+
text,
|
|
425
|
+
...(Number.isFinite(delay) && delay >= 0 ? { delay } : {}),
|
|
426
|
+
});
|
|
427
|
+
if (params.pressEnter === true) {
|
|
428
|
+
await callAPI('keyboard:press', { profileId, key: 'Enter' });
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
ok: true,
|
|
432
|
+
code: 'OPERATION_DONE',
|
|
433
|
+
message: 'type done',
|
|
434
|
+
data: { selector, target, length: text.length },
|
|
435
|
+
};
|
|
104
436
|
}
|
|
105
437
|
|
|
106
438
|
async function executeVerifySubscriptions({ profileId, params }) {
|
|
@@ -200,6 +532,13 @@ export async function executeOperation({ profileId, operation, context = {} }) {
|
|
|
200
532
|
const resolvedProfile = session.profileId || profileId;
|
|
201
533
|
const action = String(operation?.action || '').trim();
|
|
202
534
|
const params = operation?.params || operation?.config || {};
|
|
535
|
+
const filterMode = resolveFilterMode(
|
|
536
|
+
params.filterMode
|
|
537
|
+
|| operation?.filterMode
|
|
538
|
+
|| context?.filterMode
|
|
539
|
+
|| context?.runtime?.filterMode
|
|
540
|
+
|| null,
|
|
541
|
+
);
|
|
203
542
|
|
|
204
543
|
if (!action) {
|
|
205
544
|
return asErrorPayload('OPERATION_FAILED', 'operation.action is required');
|
|
@@ -279,38 +618,49 @@ export async function executeOperation({ profileId, operation, context = {} }) {
|
|
|
279
618
|
deltaX = amount;
|
|
280
619
|
deltaY = 0;
|
|
281
620
|
}
|
|
621
|
+
const anchorSelector = maybeSelector({
|
|
622
|
+
profileId: resolvedProfile,
|
|
623
|
+
containerId: params.containerId || operation?.containerId || null,
|
|
624
|
+
selector: params.selector || operation?.selector || null,
|
|
625
|
+
});
|
|
626
|
+
const anchor = await resolveScrollAnchor(resolvedProfile, {
|
|
627
|
+
selector: anchorSelector,
|
|
628
|
+
filterMode,
|
|
629
|
+
});
|
|
630
|
+
if (anchor?.center?.x && anchor?.center?.y) {
|
|
631
|
+
await callAPI('mouse:move', {
|
|
632
|
+
profileId: resolvedProfile,
|
|
633
|
+
x: Math.max(1, Math.round(Number(anchor.center.x) || 1)),
|
|
634
|
+
y: Math.max(1, Math.round(Number(anchor.center.y) || 1)),
|
|
635
|
+
steps: 2,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
282
638
|
const result = await callAPI('mouse:wheel', { profileId: resolvedProfile, deltaX, deltaY });
|
|
283
639
|
return {
|
|
284
640
|
ok: true,
|
|
285
641
|
code: 'OPERATION_DONE',
|
|
286
642
|
message: 'scroll done',
|
|
287
|
-
data: {
|
|
643
|
+
data: {
|
|
644
|
+
direction,
|
|
645
|
+
amount,
|
|
646
|
+
deltaX,
|
|
647
|
+
deltaY,
|
|
648
|
+
filterMode,
|
|
649
|
+
anchorSource: String(anchor?.source || 'document'),
|
|
650
|
+
modalLocked: anchor?.modalLocked === true,
|
|
651
|
+
result,
|
|
652
|
+
},
|
|
288
653
|
};
|
|
289
654
|
}
|
|
290
655
|
|
|
291
656
|
if (action === 'press_key') {
|
|
292
657
|
const key = String(params.key || params.value || '').trim();
|
|
293
658
|
if (!key) return asErrorPayload('OPERATION_FAILED', 'press_key requires params.key');
|
|
294
|
-
const
|
|
659
|
+
const delay = Number(params.delay);
|
|
660
|
+
const result = await callAPI('keyboard:press', {
|
|
295
661
|
profileId: resolvedProfile,
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const key = ${JSON.stringify(key)};
|
|
299
|
-
const code = key.length === 1 ? 'Key' + key.toUpperCase() : key;
|
|
300
|
-
const opts = { key, code, bubbles: true, cancelable: true };
|
|
301
|
-
target.dispatchEvent(new KeyboardEvent('keydown', opts));
|
|
302
|
-
target.dispatchEvent(new KeyboardEvent('keypress', opts));
|
|
303
|
-
target.dispatchEvent(new KeyboardEvent('keyup', opts));
|
|
304
|
-
if (key === 'Escape') {
|
|
305
|
-
const closeButton = document.querySelector('.note-detail-mask .close-box, .note-detail-mask .close-circle');
|
|
306
|
-
if (closeButton instanceof HTMLElement) closeButton.click();
|
|
307
|
-
}
|
|
308
|
-
if (key === 'Enter' && target instanceof HTMLInputElement && target.form) {
|
|
309
|
-
if (typeof target.form.requestSubmit === 'function') target.form.requestSubmit();
|
|
310
|
-
else target.form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
311
|
-
}
|
|
312
|
-
return { key, targetTag: target?.tagName || null };
|
|
313
|
-
})()`,
|
|
662
|
+
key,
|
|
663
|
+
...(Number.isFinite(delay) && delay >= 0 ? { delay } : {}),
|
|
314
664
|
});
|
|
315
665
|
return { ok: true, code: 'OPERATION_DONE', message: 'press_key done', data: result };
|
|
316
666
|
}
|
|
@@ -320,6 +670,9 @@ export async function executeOperation({ profileId, operation, context = {} }) {
|
|
|
320
670
|
}
|
|
321
671
|
|
|
322
672
|
if (action === 'evaluate') {
|
|
673
|
+
if (!isJsExecutionEnabled()) {
|
|
674
|
+
return asErrorPayload('JS_DISABLED', 'evaluate is disabled by default. Re-run camo command with --js.');
|
|
675
|
+
}
|
|
323
676
|
const script = String(params.script || '').trim();
|
|
324
677
|
if (!script) return asErrorPayload('OPERATION_FAILED', 'evaluate requires params.script');
|
|
325
678
|
const result = await callAPI('evaluate', { profileId: resolvedProfile, script });
|
|
@@ -327,11 +680,12 @@ export async function executeOperation({ profileId, operation, context = {} }) {
|
|
|
327
680
|
}
|
|
328
681
|
|
|
329
682
|
if (action === 'click' || action === 'type' || action === 'scroll_into_view') {
|
|
330
|
-
return executeSelectorOperation({
|
|
683
|
+
return await executeSelectorOperation({
|
|
331
684
|
profileId: resolvedProfile,
|
|
332
685
|
action,
|
|
333
686
|
operation,
|
|
334
687
|
params,
|
|
688
|
+
filterMode,
|
|
335
689
|
});
|
|
336
690
|
}
|
|
337
691
|
|
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
import { getDomSnapshotByProfile } from '../../utils/browser-service.mjs';
|
|
2
2
|
import { ChangeNotifier } from '../change-notifier.mjs';
|
|
3
|
-
import { ensureActiveSession, normalizeArray } from './utils.mjs';
|
|
3
|
+
import { ensureActiveSession, getCurrentUrl, normalizeArray } from './utils.mjs';
|
|
4
|
+
|
|
5
|
+
function resolveFilterMode(input) {
|
|
6
|
+
const text = String(input || process.env.CAMO_FILTER_MODE || 'strict').trim().toLowerCase();
|
|
7
|
+
if (!text) return 'strict';
|
|
8
|
+
if (text === 'legacy') return 'legacy';
|
|
9
|
+
return 'strict';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function urlMatchesFilter(url, item) {
|
|
13
|
+
const href = String(url || '').trim();
|
|
14
|
+
const includes = normalizeArray(item?.pageUrlIncludes).map((token) => String(token || '').trim()).filter(Boolean);
|
|
15
|
+
const excludes = normalizeArray(item?.pageUrlExcludes).map((token) => String(token || '').trim()).filter(Boolean);
|
|
16
|
+
if (includes.length > 0 && !includes.every((token) => href.includes(token))) return false;
|
|
17
|
+
if (excludes.length > 0 && excludes.some((token) => href.includes(token))) return false;
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
4
20
|
|
|
5
21
|
export async function watchSubscriptions({
|
|
6
22
|
profileId,
|
|
7
23
|
subscriptions,
|
|
8
24
|
throttle = 500,
|
|
25
|
+
filterMode = 'strict',
|
|
9
26
|
onEvent = () => {},
|
|
10
27
|
onError = () => {},
|
|
11
28
|
}) {
|
|
12
29
|
const session = await ensureActiveSession(profileId);
|
|
13
30
|
const resolvedProfile = session.profileId || profileId;
|
|
14
31
|
const notifier = new ChangeNotifier();
|
|
32
|
+
const effectiveFilterMode = resolveFilterMode(filterMode);
|
|
33
|
+
const strictFilter = effectiveFilterMode === 'strict';
|
|
15
34
|
const items = normalizeArray(subscriptions)
|
|
16
35
|
.map((item, index) => {
|
|
17
36
|
if (!item || typeof item !== 'object') return null;
|
|
@@ -19,7 +38,16 @@ export async function watchSubscriptions({
|
|
|
19
38
|
const selector = String(item.selector || '').trim();
|
|
20
39
|
if (!selector) return null;
|
|
21
40
|
const events = normalizeArray(item.events).map((name) => String(name).trim()).filter(Boolean);
|
|
22
|
-
|
|
41
|
+
const pageUrlIncludes = normalizeArray(item.pageUrlIncludes).map((token) => String(token || '').trim()).filter(Boolean);
|
|
42
|
+
const pageUrlExcludes = normalizeArray(item.pageUrlExcludes).map((token) => String(token || '').trim()).filter(Boolean);
|
|
43
|
+
return {
|
|
44
|
+
id,
|
|
45
|
+
selector,
|
|
46
|
+
visible: strictFilter ? true : (item.visible !== false),
|
|
47
|
+
pageUrlIncludes,
|
|
48
|
+
pageUrlExcludes,
|
|
49
|
+
events: events.length > 0 ? new Set(events) : null,
|
|
50
|
+
};
|
|
23
51
|
})
|
|
24
52
|
.filter(Boolean);
|
|
25
53
|
|
|
@@ -39,10 +67,14 @@ export async function watchSubscriptions({
|
|
|
39
67
|
if (stopped) return;
|
|
40
68
|
try {
|
|
41
69
|
const snapshot = await getDomSnapshotByProfile(resolvedProfile);
|
|
70
|
+
const currentUrl = await getCurrentUrl(resolvedProfile).catch(() => '');
|
|
42
71
|
const ts = new Date().toISOString();
|
|
43
72
|
for (const item of items) {
|
|
44
73
|
const prev = state.get(item.id) || { exists: false, stateSig: '', appearCount: 0 };
|
|
45
|
-
const
|
|
74
|
+
const urlMatched = urlMatchesFilter(currentUrl, item);
|
|
75
|
+
const elements = urlMatched
|
|
76
|
+
? notifier.findElements(snapshot, { css: item.selector, visible: item.visible })
|
|
77
|
+
: [];
|
|
46
78
|
const exists = elements.length > 0;
|
|
47
79
|
const stateSig = elements.map((node) => node.path).sort().join(',');
|
|
48
80
|
const changed = stateSig !== prev.stateSig;
|
|
@@ -55,16 +87,56 @@ export async function watchSubscriptions({
|
|
|
55
87
|
|
|
56
88
|
const shouldEmit = (type) => !item.events || item.events.has(type);
|
|
57
89
|
if (exists && !prev.exists && shouldEmit('appear')) {
|
|
58
|
-
await emit({
|
|
90
|
+
await emit({
|
|
91
|
+
type: 'appear',
|
|
92
|
+
profileId: resolvedProfile,
|
|
93
|
+
subscriptionId: item.id,
|
|
94
|
+
selector: item.selector,
|
|
95
|
+
count: elements.length,
|
|
96
|
+
elements,
|
|
97
|
+
pageUrl: currentUrl,
|
|
98
|
+
filterMode: effectiveFilterMode,
|
|
99
|
+
timestamp: ts,
|
|
100
|
+
});
|
|
59
101
|
}
|
|
60
102
|
if (!exists && prev.exists && shouldEmit('disappear')) {
|
|
61
|
-
await emit({
|
|
103
|
+
await emit({
|
|
104
|
+
type: 'disappear',
|
|
105
|
+
profileId: resolvedProfile,
|
|
106
|
+
subscriptionId: item.id,
|
|
107
|
+
selector: item.selector,
|
|
108
|
+
count: 0,
|
|
109
|
+
elements: [],
|
|
110
|
+
pageUrl: currentUrl,
|
|
111
|
+
filterMode: effectiveFilterMode,
|
|
112
|
+
timestamp: ts,
|
|
113
|
+
});
|
|
62
114
|
}
|
|
63
115
|
if (exists && shouldEmit('exist')) {
|
|
64
|
-
await emit({
|
|
116
|
+
await emit({
|
|
117
|
+
type: 'exist',
|
|
118
|
+
profileId: resolvedProfile,
|
|
119
|
+
subscriptionId: item.id,
|
|
120
|
+
selector: item.selector,
|
|
121
|
+
count: elements.length,
|
|
122
|
+
elements,
|
|
123
|
+
pageUrl: currentUrl,
|
|
124
|
+
filterMode: effectiveFilterMode,
|
|
125
|
+
timestamp: ts,
|
|
126
|
+
});
|
|
65
127
|
}
|
|
66
128
|
if (changed && shouldEmit('change')) {
|
|
67
|
-
await emit({
|
|
129
|
+
await emit({
|
|
130
|
+
type: 'change',
|
|
131
|
+
profileId: resolvedProfile,
|
|
132
|
+
subscriptionId: item.id,
|
|
133
|
+
selector: item.selector,
|
|
134
|
+
count: elements.length,
|
|
135
|
+
elements,
|
|
136
|
+
pageUrl: currentUrl,
|
|
137
|
+
filterMode: effectiveFilterMode,
|
|
138
|
+
timestamp: ts,
|
|
139
|
+
});
|
|
68
140
|
}
|
|
69
141
|
}
|
|
70
142
|
await emit({ type: 'tick', profileId: resolvedProfile, timestamp: ts });
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
normalizeArray,
|
|
10
10
|
} from './utils.mjs';
|
|
11
11
|
|
|
12
|
-
async function validatePage(profileId, spec = {}, platform = '
|
|
12
|
+
async function validatePage(profileId, spec = {}, platform = 'generic') {
|
|
13
13
|
const url = await getCurrentUrl(profileId);
|
|
14
14
|
const includes = normalizeArray(spec.urlIncludes || []);
|
|
15
15
|
const excludes = normalizeArray(spec.urlExcludes || []);
|
|
@@ -74,7 +74,7 @@ export async function validateOperation({
|
|
|
74
74
|
validationSpec = {},
|
|
75
75
|
phase = 'pre',
|
|
76
76
|
context = {},
|
|
77
|
-
platform = '
|
|
77
|
+
platform = 'generic',
|
|
78
78
|
}) {
|
|
79
79
|
try {
|
|
80
80
|
const mode = String(validationSpec.mode || 'none').toLowerCase();
|