@web-auto/camo 0.1.15 → 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/package.json +1 -1
- package/src/autoscript/runtime.mjs +1 -0
- package/src/autoscript/schema.mjs +6 -0
- package/src/cli.mjs +5 -1
- package/src/commands/autoscript.mjs +15 -1
- package/src/commands/browser.mjs +247 -19
- package/src/commands/mouse.mjs +9 -3
- package/src/container/runtime-core/operations/index.mjs +392 -38
- package/src/container/runtime-core/subscription.mjs +79 -7
- package/src/utils/browser-service.mjs +41 -6
- package/src/utils/js-policy.mjs +13 -0
package/package.json
CHANGED
|
@@ -970,6 +970,7 @@ export class AutoscriptRunner {
|
|
|
970
970
|
profileId: this.profileId,
|
|
971
971
|
subscriptions: this.script.subscriptions,
|
|
972
972
|
throttle: this.script.throttle,
|
|
973
|
+
filterMode: this.script?.defaults?.filterMode || 'strict',
|
|
973
974
|
onEvent: async (event) => {
|
|
974
975
|
this.log('autoscript:event', {
|
|
975
976
|
type: event.type,
|
|
@@ -127,9 +127,14 @@ function normalizeSubscription(item, index, defaults) {
|
|
|
127
127
|
const selector = toTrimmedString(item.selector);
|
|
128
128
|
if (!selector) return null;
|
|
129
129
|
const events = toArray(item.events).map((name) => toTrimmedString(name)).filter(Boolean);
|
|
130
|
+
const pageUrlIncludes = toArray(item.pageUrlIncludes).map((token) => toTrimmedString(token)).filter(Boolean);
|
|
131
|
+
const pageUrlExcludes = toArray(item.pageUrlExcludes).map((token) => toTrimmedString(token)).filter(Boolean);
|
|
130
132
|
return {
|
|
131
133
|
id,
|
|
132
134
|
selector,
|
|
135
|
+
visible: item.visible !== false,
|
|
136
|
+
pageUrlIncludes,
|
|
137
|
+
pageUrlExcludes,
|
|
133
138
|
events: events.length > 0 ? events : ['appear', 'exist', 'disappear', 'change'],
|
|
134
139
|
dependsOn: toArray(item.dependsOn).map((x) => toTrimmedString(x)).filter(Boolean),
|
|
135
140
|
retry: normalizeRetry(item.retry, defaults.retry),
|
|
@@ -243,6 +248,7 @@ export function normalizeAutoscript(raw, sourcePath = null) {
|
|
|
243
248
|
retry: normalizeRetry(defaults.retry, {}),
|
|
244
249
|
impact: toTrimmedString(defaults.impact || 'op') || 'op',
|
|
245
250
|
onFailure: toTrimmedString(defaults.onFailure || 'chain_stop') || 'chain_stop',
|
|
251
|
+
filterMode: toTrimmedString(defaults.filterMode || 'strict') || 'strict',
|
|
246
252
|
validationMode: toTrimmedString(defaults.validationMode || 'none') || 'none',
|
|
247
253
|
recovery: normalizeRecovery(defaults.recovery, {}),
|
|
248
254
|
pacing: normalizePacing(defaults.pacing, {}),
|
package/src/cli.mjs
CHANGED
|
@@ -160,7 +160,11 @@ async function handleConfigCommand(args) {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
async function main() {
|
|
163
|
-
const
|
|
163
|
+
const rawArgs = process.argv.slice(2);
|
|
164
|
+
const jsEnabled = rawArgs.includes('--js');
|
|
165
|
+
if (jsEnabled) process.env.CAMO_ALLOW_JS = '1';
|
|
166
|
+
else delete process.env.CAMO_ALLOW_JS;
|
|
167
|
+
const args = rawArgs.filter((arg) => arg !== '--js');
|
|
164
168
|
const cmd = args[0];
|
|
165
169
|
|
|
166
170
|
if (!cmd) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { getDefaultProfile } from '../utils/config.mjs';
|
|
3
|
+
import { getDefaultProfile, listProfiles } from '../utils/config.mjs';
|
|
4
4
|
import { explainAutoscript, loadAndValidateAutoscript } from '../autoscript/schema.mjs';
|
|
5
5
|
import { AutoscriptRunner } from '../autoscript/runtime.mjs';
|
|
6
6
|
import { safeAppendProgressEvent } from '../events/progress-log.mjs';
|
|
@@ -30,6 +30,18 @@ function collectPositionals(args, startIndex = 2, valueFlags = new Set(['--profi
|
|
|
30
30
|
return out;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function assertExistingProfile(profileId) {
|
|
34
|
+
const id = String(profileId || '').trim();
|
|
35
|
+
if (!id) {
|
|
36
|
+
throw new Error('profileId is required');
|
|
37
|
+
}
|
|
38
|
+
const known = new Set(listProfiles());
|
|
39
|
+
if (!known.has(id)) {
|
|
40
|
+
throw new Error(`profile not found: ${id}. create it first with "camo profile create ${id}"`);
|
|
41
|
+
}
|
|
42
|
+
return id;
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
function appendJsonLine(filePath, payload) {
|
|
34
46
|
if (!filePath) return;
|
|
35
47
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -783,6 +795,7 @@ async function handleRun(args) {
|
|
|
783
795
|
if (!profileId) {
|
|
784
796
|
throw new Error('profileId is required. Set in script or pass --profile <id>');
|
|
785
797
|
}
|
|
798
|
+
assertExistingProfile(profileId);
|
|
786
799
|
|
|
787
800
|
await executeAutoscriptRuntime({
|
|
788
801
|
commandName: 'autoscript.run',
|
|
@@ -896,6 +909,7 @@ async function handleResume(args) {
|
|
|
896
909
|
if (!profileId) {
|
|
897
910
|
throw new Error('profileId is required. Set in script or pass --profile <id>');
|
|
898
911
|
}
|
|
912
|
+
assertExistingProfile(profileId);
|
|
899
913
|
const resumeState = buildResumeStateFromSnapshot(script, snapshot, fromNode || null);
|
|
900
914
|
await executeAutoscriptRuntime({
|
|
901
915
|
commandName: 'autoscript.resume',
|
package/src/commands/browser.mjs
CHANGED
|
@@ -9,11 +9,10 @@ import {
|
|
|
9
9
|
} from '../utils/config.mjs';
|
|
10
10
|
import { callAPI, ensureCamoufox, ensureBrowserService, getSessionByProfile, checkBrowserService } from '../utils/browser-service.mjs';
|
|
11
11
|
import { resolveProfileId, ensureUrlScheme, looksLikeUrlToken, getPositionals } from '../utils/args.mjs';
|
|
12
|
+
import { ensureJsExecutionEnabled } from '../utils/js-policy.mjs';
|
|
12
13
|
import { acquireLock, releaseLock, cleanupStaleLocks } from '../lifecycle/lock.mjs';
|
|
13
14
|
import {
|
|
14
|
-
buildSelectorClickScript,
|
|
15
15
|
buildScrollTargetScript,
|
|
16
|
-
buildSelectorTypeScript,
|
|
17
16
|
} from '../container/runtime-core/operations/selector-scripts.mjs';
|
|
18
17
|
import {
|
|
19
18
|
registerSession,
|
|
@@ -31,11 +30,15 @@ import { startSessionWatchdog, stopAllSessionWatchdogs, stopSessionWatchdog } fr
|
|
|
31
30
|
const START_WINDOW_MIN_WIDTH = 960;
|
|
32
31
|
const START_WINDOW_MIN_HEIGHT = 700;
|
|
33
32
|
const START_WINDOW_MAX_RESERVE = 240;
|
|
34
|
-
const START_WINDOW_DEFAULT_RESERVE =
|
|
33
|
+
const START_WINDOW_DEFAULT_RESERVE = 0;
|
|
35
34
|
const DEFAULT_HEADLESS_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
36
35
|
const DEVTOOLS_SHORTCUTS = process.platform === 'darwin'
|
|
37
36
|
? ['Meta+Alt+I', 'F12']
|
|
38
37
|
: ['F12', 'Control+Shift+I'];
|
|
38
|
+
const INPUT_ACTION_TIMEOUT_MS = Math.max(
|
|
39
|
+
1000,
|
|
40
|
+
parseNumber(process.env.CAMO_INPUT_ACTION_TIMEOUT_MS, 30000),
|
|
41
|
+
);
|
|
39
42
|
|
|
40
43
|
function sleep(ms) {
|
|
41
44
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -77,6 +80,167 @@ function parseDurationMs(raw, fallbackMs) {
|
|
|
77
80
|
return Math.floor(value * factor);
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
function assertExistingProfile(profileId, profileSet = null) {
|
|
84
|
+
const id = String(profileId || '').trim();
|
|
85
|
+
if (!id) throw new Error('profileId is required');
|
|
86
|
+
const known = profileSet || new Set(listProfiles());
|
|
87
|
+
if (!known.has(id)) {
|
|
88
|
+
throw new Error(`profile not found: ${id}. create it first with "camo profile create ${id}"`);
|
|
89
|
+
}
|
|
90
|
+
return id;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function resolveVisibleTargetPoint(profileId, selector, options = {}) {
|
|
94
|
+
const selectorLiteral = JSON.stringify(String(selector || '').trim());
|
|
95
|
+
const highlight = options.highlight === true;
|
|
96
|
+
const payload = await callAPI('evaluate', {
|
|
97
|
+
profileId,
|
|
98
|
+
script: `(() => {
|
|
99
|
+
const selector = ${selectorLiteral};
|
|
100
|
+
const highlight = ${highlight ? 'true' : 'false'};
|
|
101
|
+
const nodes = Array.from(document.querySelectorAll(selector));
|
|
102
|
+
const isVisible = (node) => {
|
|
103
|
+
if (!(node instanceof Element)) return false;
|
|
104
|
+
const rect = node.getBoundingClientRect?.();
|
|
105
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) return false;
|
|
106
|
+
try {
|
|
107
|
+
const style = window.getComputedStyle(node);
|
|
108
|
+
if (!style) return false;
|
|
109
|
+
if (style.display === 'none') return false;
|
|
110
|
+
if (style.visibility === 'hidden' || style.visibility === 'collapse') return false;
|
|
111
|
+
const opacity = Number.parseFloat(String(style.opacity || '1'));
|
|
112
|
+
if (Number.isFinite(opacity) && opacity <= 0.01) return false;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
};
|
|
118
|
+
const hitVisible = (node) => {
|
|
119
|
+
if (!(node instanceof Element)) return false;
|
|
120
|
+
const rect = node.getBoundingClientRect?.();
|
|
121
|
+
if (!rect) return false;
|
|
122
|
+
const x = Math.max(0, Math.min((window.innerWidth || 1) - 1, Math.round(rect.left + rect.width / 2)));
|
|
123
|
+
const y = Math.max(0, Math.min((window.innerHeight || 1) - 1, Math.round(rect.top + rect.height / 2)));
|
|
124
|
+
const top = document.elementFromPoint(x, y);
|
|
125
|
+
if (!top) return false;
|
|
126
|
+
return top === node || node.contains(top) || top.contains(node);
|
|
127
|
+
};
|
|
128
|
+
const target = nodes.find((item) => isVisible(item) && hitVisible(item))
|
|
129
|
+
|| nodes.find((item) => isVisible(item))
|
|
130
|
+
|| nodes[0]
|
|
131
|
+
|| null;
|
|
132
|
+
if (!target) {
|
|
133
|
+
return { ok: false, error: 'selector_not_found', selector };
|
|
134
|
+
}
|
|
135
|
+
const rect = target.getBoundingClientRect?.() || { left: 0, top: 0, width: 1, height: 1 };
|
|
136
|
+
const center = {
|
|
137
|
+
x: Math.max(1, Math.min((window.innerWidth || 1) - 1, Math.round(rect.left + Math.max(1, rect.width / 2)))),
|
|
138
|
+
y: Math.max(1, Math.min((window.innerHeight || 1) - 1, Math.round(rect.top + Math.max(1, rect.height / 2)))),
|
|
139
|
+
};
|
|
140
|
+
if (highlight) {
|
|
141
|
+
try {
|
|
142
|
+
const id = 'webauto-action-highlight-overlay';
|
|
143
|
+
const old = document.getElementById(id);
|
|
144
|
+
if (old) old.remove();
|
|
145
|
+
const overlay = document.createElement('div');
|
|
146
|
+
overlay.id = id;
|
|
147
|
+
overlay.style.position = 'fixed';
|
|
148
|
+
overlay.style.left = rect.left + 'px';
|
|
149
|
+
overlay.style.top = rect.top + 'px';
|
|
150
|
+
overlay.style.width = rect.width + 'px';
|
|
151
|
+
overlay.style.height = rect.height + 'px';
|
|
152
|
+
overlay.style.border = '2px solid #00A8FF';
|
|
153
|
+
overlay.style.borderRadius = '8px';
|
|
154
|
+
overlay.style.background = 'rgba(0,168,255,0.12)';
|
|
155
|
+
overlay.style.pointerEvents = 'none';
|
|
156
|
+
overlay.style.zIndex = '2147483647';
|
|
157
|
+
overlay.style.transition = 'opacity 120ms ease';
|
|
158
|
+
overlay.style.opacity = '1';
|
|
159
|
+
document.documentElement.appendChild(overlay);
|
|
160
|
+
setTimeout(() => {
|
|
161
|
+
overlay.style.opacity = '0';
|
|
162
|
+
setTimeout(() => overlay.remove(), 180);
|
|
163
|
+
}, 260);
|
|
164
|
+
} catch {}
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
ok: true,
|
|
168
|
+
selector,
|
|
169
|
+
center,
|
|
170
|
+
rect: {
|
|
171
|
+
left: rect.left,
|
|
172
|
+
top: rect.top,
|
|
173
|
+
width: rect.width,
|
|
174
|
+
height: rect.height,
|
|
175
|
+
},
|
|
176
|
+
viewport: {
|
|
177
|
+
width: Number(window.innerWidth || 0),
|
|
178
|
+
height: Number(window.innerHeight || 0),
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
})()`,
|
|
182
|
+
}, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
|
|
183
|
+
const result = payload?.result || payload?.data?.result || payload?.data || payload || null;
|
|
184
|
+
if (!result || result.ok !== true || !result.center) {
|
|
185
|
+
throw new Error(`Element not found: ${selector}`);
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isTargetFullyInViewport(target, margin = 6) {
|
|
191
|
+
const rect = target?.rect && typeof target.rect === 'object' ? target.rect : null;
|
|
192
|
+
const viewport = target?.viewport && typeof target.viewport === 'object' ? target.viewport : null;
|
|
193
|
+
if (!rect || !viewport) return true;
|
|
194
|
+
const vw = Number(viewport.width || 0);
|
|
195
|
+
const vh = Number(viewport.height || 0);
|
|
196
|
+
if (!Number.isFinite(vw) || !Number.isFinite(vh) || vw <= 0 || vh <= 0) return true;
|
|
197
|
+
const left = Number(rect.left || 0);
|
|
198
|
+
const top = Number(rect.top || 0);
|
|
199
|
+
const width = Math.max(0, Number(rect.width || 0));
|
|
200
|
+
const height = Math.max(0, Number(rect.height || 0));
|
|
201
|
+
const right = left + width;
|
|
202
|
+
const bottom = top + height;
|
|
203
|
+
const m = Math.max(0, Number(margin) || 0);
|
|
204
|
+
return left >= m && top >= m && right <= (vw - m) && bottom <= (vh - m);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function ensureClickTargetInViewport(profileId, selector, initialTarget, options = {}) {
|
|
208
|
+
let target = initialTarget;
|
|
209
|
+
const maxSteps = Math.max(0, Number(options.maxAutoScrollSteps ?? 8) || 8);
|
|
210
|
+
const settleMs = Math.max(0, Number(options.autoScrollSettleMs ?? 140) || 140);
|
|
211
|
+
let autoScrolled = 0;
|
|
212
|
+
|
|
213
|
+
while (autoScrolled < maxSteps && !isTargetFullyInViewport(target)) {
|
|
214
|
+
const rect = target?.rect && typeof target.rect === 'object' ? target.rect : {};
|
|
215
|
+
const viewport = target?.viewport && typeof target.viewport === 'object' ? target.viewport : {};
|
|
216
|
+
const vw = Math.max(1, Number(viewport.width || 1));
|
|
217
|
+
const vh = Math.max(1, Number(viewport.height || 1));
|
|
218
|
+
const rawCenterY = Number(rect.top || 0) + Math.max(1, Number(rect.height || 0)) / 2;
|
|
219
|
+
const desiredCenterY = clamp(Math.round(vh * 0.45), 80, Math.max(80, vh - 80));
|
|
220
|
+
let deltaY = Math.round(rawCenterY - desiredCenterY);
|
|
221
|
+
deltaY = clamp(deltaY, -900, 900);
|
|
222
|
+
if (Math.abs(deltaY) < 100) {
|
|
223
|
+
deltaY = deltaY >= 0 ? 120 : -120;
|
|
224
|
+
}
|
|
225
|
+
const anchorX = clamp(Math.round(vw / 2), 1, Math.max(1, vw - 1));
|
|
226
|
+
const anchorY = clamp(Math.round(vh / 2), 1, Math.max(1, vh - 1));
|
|
227
|
+
|
|
228
|
+
await callAPI('mouse:move', { profileId, x: anchorX, y: anchorY, steps: 1 }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
|
|
229
|
+
await callAPI('mouse:wheel', { profileId, deltaX: 0, deltaY }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
|
|
230
|
+
autoScrolled += 1;
|
|
231
|
+
if (settleMs > 0) {
|
|
232
|
+
await sleep(settleMs);
|
|
233
|
+
}
|
|
234
|
+
target = await resolveVisibleTargetPoint(profileId, selector, { highlight: false });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
target,
|
|
239
|
+
autoScrolled,
|
|
240
|
+
targetFullyVisible: isTargetFullyInViewport(target),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
80
244
|
function validateAlias(alias) {
|
|
81
245
|
const text = String(alias || '').trim();
|
|
82
246
|
if (!text) return null;
|
|
@@ -378,6 +542,7 @@ export async function handleStartCommand(args) {
|
|
|
378
542
|
throw new Error('No default profile set. Run: camo profile default <profileId>');
|
|
379
543
|
}
|
|
380
544
|
}
|
|
545
|
+
assertExistingProfile(profileId, profileSet);
|
|
381
546
|
if (alias && isSessionAliasTaken(alias, profileId)) {
|
|
382
547
|
throw new Error(`Alias is already in use: ${alias}`);
|
|
383
548
|
}
|
|
@@ -497,17 +662,28 @@ export async function handleStartCommand(args) {
|
|
|
497
662
|
source: 'explicit',
|
|
498
663
|
};
|
|
499
664
|
} else {
|
|
665
|
+
const display = await callAPI('system:display', {}).catch(() => null);
|
|
666
|
+
const displayTarget = computeStartWindowSize(display);
|
|
500
667
|
const rememberedWindow = getProfileWindowSize(profileId);
|
|
501
668
|
if (rememberedWindow) {
|
|
502
|
-
|
|
669
|
+
const rememberedTarget = {
|
|
503
670
|
width: rememberedWindow.width,
|
|
504
671
|
height: rememberedWindow.height,
|
|
505
672
|
source: 'profile',
|
|
506
673
|
updatedAt: rememberedWindow.updatedAt,
|
|
507
674
|
};
|
|
675
|
+
const canTrustDisplayTarget = displayTarget?.source && displayTarget.source !== 'fallback';
|
|
676
|
+
const refreshFromDisplay = canTrustDisplayTarget
|
|
677
|
+
&& (
|
|
678
|
+
rememberedTarget.height < Math.floor(displayTarget.height * 0.92)
|
|
679
|
+
|| rememberedTarget.width < Math.floor(displayTarget.width * 0.92)
|
|
680
|
+
);
|
|
681
|
+
windowTarget = refreshFromDisplay ? {
|
|
682
|
+
...displayTarget,
|
|
683
|
+
source: 'display',
|
|
684
|
+
} : rememberedTarget;
|
|
508
685
|
} else {
|
|
509
|
-
|
|
510
|
-
windowTarget = computeStartWindowSize(display);
|
|
686
|
+
windowTarget = displayTarget;
|
|
511
687
|
}
|
|
512
688
|
}
|
|
513
689
|
|
|
@@ -704,6 +880,7 @@ export async function handleStatusCommand(args) {
|
|
|
704
880
|
export async function handleGotoCommand(args) {
|
|
705
881
|
await ensureBrowserService();
|
|
706
882
|
const positionals = getPositionals(args);
|
|
883
|
+
const profileSet = new Set(listProfiles());
|
|
707
884
|
|
|
708
885
|
let profileId;
|
|
709
886
|
let url;
|
|
@@ -718,6 +895,13 @@ export async function handleGotoCommand(args) {
|
|
|
718
895
|
|
|
719
896
|
if (!profileId) throw new Error('Usage: camo goto [profileId] <url> (or set default profile first)');
|
|
720
897
|
if (!url) throw new Error('Usage: camo goto [profileId] <url>');
|
|
898
|
+
assertExistingProfile(profileId, profileSet);
|
|
899
|
+
const active = await getSessionByProfile(profileId);
|
|
900
|
+
if (!active) {
|
|
901
|
+
throw new Error(
|
|
902
|
+
`No active session for profile: ${profileId}. Start via "camo start ${profileId}" (or UI CLI startup) before goto.`,
|
|
903
|
+
);
|
|
904
|
+
}
|
|
721
905
|
|
|
722
906
|
const result = await callAPI('goto', { profileId, url: ensureUrlScheme(url) });
|
|
723
907
|
updateSession(profileId, { url: ensureUrlScheme(url), lastSeen: Date.now() });
|
|
@@ -768,7 +952,8 @@ export async function handleScrollCommand(args) {
|
|
|
768
952
|
const isFlag = (arg) => arg?.startsWith('--');
|
|
769
953
|
const selectorIdx = args.indexOf('--selector');
|
|
770
954
|
const selector = selectorIdx >= 0 ? String(args[selectorIdx + 1] || '').trim() : null;
|
|
771
|
-
const
|
|
955
|
+
const highlightRequested = resolveHighlightEnabled(args);
|
|
956
|
+
const highlight = highlightRequested;
|
|
772
957
|
|
|
773
958
|
let profileId = null;
|
|
774
959
|
for (let i = 1; i < args.length; i++) {
|
|
@@ -794,16 +979,15 @@ export async function handleScrollCommand(args) {
|
|
|
794
979
|
const target = await callAPI('evaluate', {
|
|
795
980
|
profileId,
|
|
796
981
|
script: buildScrollTargetScript({ selector, highlight }),
|
|
797
|
-
});
|
|
982
|
+
}, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
|
|
798
983
|
const centerX = Number(target?.result?.center?.x);
|
|
799
984
|
const centerY = Number(target?.result?.center?.y);
|
|
800
985
|
if (Number.isFinite(centerX) && Number.isFinite(centerY)) {
|
|
801
|
-
await callAPI('mouse:move', { profileId, x: centerX, y: centerY, steps: 2 });
|
|
986
|
+
await callAPI('mouse:move', { profileId, x: centerX, y: centerY, steps: 2 }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
|
|
802
987
|
}
|
|
803
|
-
|
|
804
988
|
const deltaX = direction === 'left' ? -amount : direction === 'right' ? amount : 0;
|
|
805
989
|
const deltaY = direction === 'up' ? -amount : direction === 'down' ? amount : 0;
|
|
806
|
-
const result = await callAPI('mouse:wheel', { profileId, deltaX, deltaY });
|
|
990
|
+
const result = await callAPI('mouse:wheel', { profileId, deltaX, deltaY }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
|
|
807
991
|
console.log(JSON.stringify({
|
|
808
992
|
...result,
|
|
809
993
|
scrollTarget: target?.result || null,
|
|
@@ -829,11 +1013,33 @@ export async function handleClickCommand(args) {
|
|
|
829
1013
|
if (!profileId) throw new Error('Usage: camo click [profileId] <selector> [--highlight|--no-highlight]');
|
|
830
1014
|
if (!selector) throw new Error('Usage: camo click [profileId] <selector> [--highlight|--no-highlight]');
|
|
831
1015
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1016
|
+
let target = await resolveVisibleTargetPoint(profileId, selector, { highlight });
|
|
1017
|
+
const ensured = await ensureClickTargetInViewport(profileId, selector, target, {
|
|
1018
|
+
maxAutoScrollSteps: 3,
|
|
835
1019
|
});
|
|
836
|
-
|
|
1020
|
+
if (!ensured.targetFullyVisible) {
|
|
1021
|
+
throw new Error(`Click target not fully visible after ${ensured.autoScrolled} auto-scroll attempts: ${selector}`);
|
|
1022
|
+
}
|
|
1023
|
+
target = ensured.target;
|
|
1024
|
+
if (highlight) {
|
|
1025
|
+
target = await resolveVisibleTargetPoint(profileId, selector, { highlight: true });
|
|
1026
|
+
}
|
|
1027
|
+
await callAPI('mouse:move', { profileId, x: target.center.x, y: target.center.y, steps: 2 }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
|
|
1028
|
+
const result = await callAPI('mouse:click', {
|
|
1029
|
+
profileId,
|
|
1030
|
+
x: target.center.x,
|
|
1031
|
+
y: target.center.y,
|
|
1032
|
+
button: 'left',
|
|
1033
|
+
clicks: 1,
|
|
1034
|
+
}, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
|
|
1035
|
+
console.log(JSON.stringify({
|
|
1036
|
+
...result,
|
|
1037
|
+
selector,
|
|
1038
|
+
highlight,
|
|
1039
|
+
autoScrolled: ensured.autoScrolled,
|
|
1040
|
+
targetFullyVisible: ensured.targetFullyVisible,
|
|
1041
|
+
target,
|
|
1042
|
+
}, null, 2));
|
|
837
1043
|
}
|
|
838
1044
|
|
|
839
1045
|
export async function handleTypeCommand(args) {
|
|
@@ -857,15 +1063,36 @@ export async function handleTypeCommand(args) {
|
|
|
857
1063
|
if (!profileId) throw new Error('Usage: camo type [profileId] <selector> <text> [--highlight|--no-highlight]');
|
|
858
1064
|
if (!selector || text === undefined) throw new Error('Usage: camo type [profileId] <selector> <text> [--highlight|--no-highlight]');
|
|
859
1065
|
|
|
860
|
-
const
|
|
1066
|
+
const target = await resolveVisibleTargetPoint(profileId, selector, { highlight });
|
|
1067
|
+
await callAPI('mouse:move', { profileId, x: target.center.x, y: target.center.y, steps: 2 }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
|
|
1068
|
+
await callAPI('mouse:click', {
|
|
861
1069
|
profileId,
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1070
|
+
x: target.center.x,
|
|
1071
|
+
y: target.center.y,
|
|
1072
|
+
button: 'left',
|
|
1073
|
+
clicks: 1,
|
|
1074
|
+
}, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
|
|
1075
|
+
await callAPI('keyboard:press', {
|
|
1076
|
+
profileId,
|
|
1077
|
+
key: process.platform === 'darwin' ? 'Meta+A' : 'Control+A',
|
|
1078
|
+
}, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
|
|
1079
|
+
await callAPI('keyboard:press', { profileId, key: 'Backspace' }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
|
|
1080
|
+
const result = await callAPI('keyboard:type', {
|
|
1081
|
+
profileId,
|
|
1082
|
+
text: String(text),
|
|
1083
|
+
}, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
|
|
1084
|
+
console.log(JSON.stringify({
|
|
1085
|
+
...result,
|
|
1086
|
+
selector,
|
|
1087
|
+
typed: String(text).length,
|
|
1088
|
+
highlight,
|
|
1089
|
+
target,
|
|
1090
|
+
}, null, 2));
|
|
865
1091
|
}
|
|
866
1092
|
|
|
867
1093
|
export async function handleHighlightCommand(args) {
|
|
868
1094
|
await ensureBrowserService();
|
|
1095
|
+
ensureJsExecutionEnabled('highlight command');
|
|
869
1096
|
const positionals = getPositionals(args);
|
|
870
1097
|
let profileId;
|
|
871
1098
|
let selector;
|
|
@@ -898,6 +1125,7 @@ export async function handleHighlightCommand(args) {
|
|
|
898
1125
|
|
|
899
1126
|
export async function handleClearHighlightCommand(args) {
|
|
900
1127
|
await ensureBrowserService();
|
|
1128
|
+
ensureJsExecutionEnabled('clear-highlight command');
|
|
901
1129
|
const profileId = resolveProfileId(args, 1, getDefaultProfile);
|
|
902
1130
|
if (!profileId) throw new Error('Usage: camo clear-highlight [profileId]');
|
|
903
1131
|
|
package/src/commands/mouse.mjs
CHANGED
|
@@ -2,6 +2,12 @@ import { getDefaultProfile } from '../utils/config.mjs';
|
|
|
2
2
|
import { callAPI, ensureBrowserService } from '../utils/browser-service.mjs';
|
|
3
3
|
import { getPositionals } from '../utils/args.mjs';
|
|
4
4
|
|
|
5
|
+
const INPUT_ACTION_TIMEOUT_MS = (() => {
|
|
6
|
+
const parsed = Number(process.env.CAMO_INPUT_ACTION_TIMEOUT_MS);
|
|
7
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return 30000;
|
|
8
|
+
return Math.max(1000, Math.floor(parsed));
|
|
9
|
+
})();
|
|
10
|
+
|
|
5
11
|
export async function handleMouseCommand(args) {
|
|
6
12
|
await ensureBrowserService();
|
|
7
13
|
|
|
@@ -17,7 +23,7 @@ export async function handleMouseCommand(args) {
|
|
|
17
23
|
const x = parseInt(args[xIdx + 1]);
|
|
18
24
|
const y = parseInt(args[yIdx + 1]);
|
|
19
25
|
const steps = stepsIdx >= 0 ? parseInt(args[stepsIdx + 1]) : undefined;
|
|
20
|
-
const result = await callAPI('mouse:move', { profileId, x, y, steps });
|
|
26
|
+
const result = await callAPI('mouse:move', { profileId, x, y, steps }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
|
|
21
27
|
console.log(JSON.stringify(result, null, 2));
|
|
22
28
|
} else if (sub === 'click') {
|
|
23
29
|
// Use existing click command? We already have click command for element clicking.
|
|
@@ -33,7 +39,7 @@ export async function handleMouseCommand(args) {
|
|
|
33
39
|
const button = buttonIdx >= 0 ? args[buttonIdx + 1] : 'left';
|
|
34
40
|
const clicks = clicksIdx >= 0 ? parseInt(args[clicksIdx + 1]) : 1;
|
|
35
41
|
const delay = delayIdx >= 0 ? parseInt(args[delayIdx + 1]) : undefined;
|
|
36
|
-
const result = await callAPI('mouse:click', { profileId, x, y, button, clicks, delay });
|
|
42
|
+
const result = await callAPI('mouse:click', { profileId, x, y, button, clicks, delay }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
|
|
37
43
|
console.log(JSON.stringify(result, null, 2));
|
|
38
44
|
} else if (sub === 'wheel') {
|
|
39
45
|
const deltaXIdx = args.indexOf('--deltax');
|
|
@@ -41,7 +47,7 @@ export async function handleMouseCommand(args) {
|
|
|
41
47
|
if (deltaXIdx === -1 && deltaYIdx === -1) throw new Error('Usage: camo mouse wheel [profileId] [--deltax <px>] [--deltay <px>]');
|
|
42
48
|
const deltaX = deltaXIdx >= 0 ? parseInt(args[deltaXIdx + 1]) : 0;
|
|
43
49
|
const deltaY = deltaYIdx >= 0 ? parseInt(args[deltaYIdx + 1]) : 0;
|
|
44
|
-
const result = await callAPI('mouse:wheel', { profileId, deltaX, deltaY });
|
|
50
|
+
const result = await callAPI('mouse:wheel', { profileId, deltaX, deltaY }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
|
|
45
51
|
console.log(JSON.stringify(result, null, 2));
|
|
46
52
|
} else {
|
|
47
53
|
throw new Error('Usage: camo mouse <move|click|wheel> [profileId] [options]');
|
|
@@ -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,6 +9,31 @@ import { BROWSER_SERVICE_URL, loadConfig, setRepoRoot } from './config.mjs';
|
|
|
9
9
|
import { touchSessionActivity } from '../lifecycle/session-registry.mjs';
|
|
10
10
|
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
|
+
const DEFAULT_API_TIMEOUT_MS = 30000;
|
|
13
|
+
|
|
14
|
+
function resolveApiTimeoutMs(options = {}) {
|
|
15
|
+
const optionValue = Number(options?.timeoutMs);
|
|
16
|
+
if (Number.isFinite(optionValue) && optionValue > 0) {
|
|
17
|
+
return Math.max(1000, Math.floor(optionValue));
|
|
18
|
+
}
|
|
19
|
+
const envValue = Number(process.env.CAMO_API_TIMEOUT_MS);
|
|
20
|
+
if (Number.isFinite(envValue) && envValue > 0) {
|
|
21
|
+
return Math.max(1000, Math.floor(envValue));
|
|
22
|
+
}
|
|
23
|
+
return DEFAULT_API_TIMEOUT_MS;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isTimeoutError(error) {
|
|
27
|
+
const name = String(error?.name || '').toLowerCase();
|
|
28
|
+
const message = String(error?.message || '').toLowerCase();
|
|
29
|
+
return (
|
|
30
|
+
name.includes('timeout')
|
|
31
|
+
|| name.includes('abort')
|
|
32
|
+
|| message.includes('timeout')
|
|
33
|
+
|| message.includes('timed out')
|
|
34
|
+
|| message.includes('aborted')
|
|
35
|
+
);
|
|
36
|
+
}
|
|
12
37
|
|
|
13
38
|
function shouldTrackSessionActivity(action, payload) {
|
|
14
39
|
const profileId = String(payload?.profileId || '').trim();
|
|
@@ -17,12 +42,22 @@ function shouldTrackSessionActivity(action, payload) {
|
|
|
17
42
|
return true;
|
|
18
43
|
}
|
|
19
44
|
|
|
20
|
-
export async function callAPI(action, payload = {}) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
45
|
+
export async function callAPI(action, payload = {}, options = {}) {
|
|
46
|
+
const timeoutMs = resolveApiTimeoutMs(options);
|
|
47
|
+
let r;
|
|
48
|
+
try {
|
|
49
|
+
r = await fetch(`${BROWSER_SERVICE_URL}/command`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({ action, args: payload }),
|
|
53
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (isTimeoutError(error)) {
|
|
57
|
+
throw new Error(`browser-service timeout after ${timeoutMs}ms: ${action}`);
|
|
58
|
+
}
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
26
61
|
|
|
27
62
|
let body;
|
|
28
63
|
try {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function isTruthy(value) {
|
|
2
|
+
const text = String(value || '').trim().toLowerCase();
|
|
3
|
+
return ['1', 'true', 'yes', 'on'].includes(text);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function isJsExecutionEnabled() {
|
|
7
|
+
return isTruthy(process.env.CAMO_ALLOW_JS);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ensureJsExecutionEnabled(scope = 'JavaScript execution') {
|
|
11
|
+
if (isJsExecutionEnabled()) return;
|
|
12
|
+
throw new Error(`${scope} is disabled by default. Re-run with --js to enable.`);
|
|
13
|
+
}
|