@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
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]');
|
|
@@ -40,14 +40,28 @@ export const XHS_CHECKPOINTS = {
|
|
|
40
40
|
],
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
export async function detectCheckpoint({ profileId, platform = '
|
|
44
|
-
if (platform !== 'xiaohongshu') {
|
|
45
|
-
return asErrorPayload('UNSUPPORTED_PLATFORM', `Unsupported platform: ${platform}`);
|
|
46
|
-
}
|
|
47
|
-
|
|
43
|
+
export async function detectCheckpoint({ profileId, platform = 'generic' }) {
|
|
48
44
|
try {
|
|
49
45
|
const session = await ensureActiveSession(profileId);
|
|
50
46
|
const resolvedProfile = session.profileId || profileId;
|
|
47
|
+
|
|
48
|
+
if (platform !== 'xiaohongshu') {
|
|
49
|
+
const url = await getCurrentUrl(resolvedProfile);
|
|
50
|
+
return {
|
|
51
|
+
ok: true,
|
|
52
|
+
code: 'CHECKPOINT_DETECTED',
|
|
53
|
+
message: 'Checkpoint detected',
|
|
54
|
+
data: {
|
|
55
|
+
profileId: resolvedProfile,
|
|
56
|
+
platform,
|
|
57
|
+
checkpoint: 'unknown',
|
|
58
|
+
url,
|
|
59
|
+
signals: [],
|
|
60
|
+
selectorHits: {},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
51
65
|
const [url, snapshot] = await Promise.all([
|
|
52
66
|
getCurrentUrl(resolvedProfile),
|
|
53
67
|
getDomSnapshotByProfile(resolvedProfile),
|
|
@@ -103,7 +117,7 @@ export async function captureCheckpoint({
|
|
|
103
117
|
profileId,
|
|
104
118
|
containerId = null,
|
|
105
119
|
selector = null,
|
|
106
|
-
platform = '
|
|
120
|
+
platform = 'generic',
|
|
107
121
|
}) {
|
|
108
122
|
try {
|
|
109
123
|
const session = await ensureActiveSession(profileId);
|
|
@@ -139,7 +153,7 @@ export async function restoreCheckpoint({
|
|
|
139
153
|
containerId = null,
|
|
140
154
|
selector = null,
|
|
141
155
|
targetCheckpoint = null,
|
|
142
|
-
platform = '
|
|
156
|
+
platform = 'generic',
|
|
143
157
|
}) {
|
|
144
158
|
try {
|
|
145
159
|
const session = await ensureActiveSession(profileId);
|