@web-auto/webauto 0.1.18 → 0.1.19
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 +122 -53
- package/apps/desktop-console/dist/main/index.mjs +227 -12
- package/apps/desktop-console/dist/renderer/index.js +237 -8
- package/apps/desktop-console/entry/ui-cli.mjs +282 -16
- package/apps/desktop-console/entry/ui-console.mjs +46 -15
- package/apps/webauto/entry/account.mjs +126 -27
- package/apps/webauto/entry/lib/account-detect.mjs +399 -9
- package/apps/webauto/entry/lib/account-store.mjs +201 -109
- package/apps/webauto/entry/lib/iflow-reply.mjs +194 -0
- package/apps/webauto/entry/lib/profile-policy.mjs +48 -0
- package/apps/webauto/entry/lib/profilepool.mjs +12 -0
- package/apps/webauto/entry/lib/schedule-store.mjs +29 -2
- package/apps/webauto/entry/lib/session-init.mjs +227 -0
- package/apps/webauto/entry/lib/upgrade-check.mjs +269 -0
- package/apps/webauto/entry/lib/xhs-unified-blocks.mjs +160 -0
- package/apps/webauto/entry/lib/xhs-unified-output-blocks.mjs +83 -0
- package/apps/webauto/entry/lib/xhs-unified-plan-blocks.mjs +55 -0
- package/apps/webauto/entry/lib/xhs-unified-profile-blocks.mjs +542 -0
- package/apps/webauto/entry/lib/xhs-unified-runtime-blocks.mjs +436 -0
- package/apps/webauto/entry/profilepool.mjs +56 -9
- package/apps/webauto/entry/smart-reply-cli.mjs +267 -0
- package/apps/webauto/entry/weibo-unified.mjs +84 -11
- package/apps/webauto/entry/xhs-orchestrate.mjs +43 -1
- package/apps/webauto/entry/xhs-unified.mjs +92 -997
- package/bin/webauto.mjs +22 -4
- package/dist/modules/camo-backend/src/index.js +33 -0
- package/dist/modules/camo-backend/src/internal/BrowserSession.js +232 -49
- package/dist/modules/camo-backend/src/internal/engine-manager.js +14 -13
- package/dist/modules/camo-backend/src/internal/ws-server.js +16 -19
- package/dist/modules/camo-runtime/src/utils/browser-service.mjs +38 -6
- package/dist/modules/workflow/blocks/EnsureSession.js +0 -8
- package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +78 -6
- package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +266 -192
- package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +2 -0
- package/dist/modules/workflow/src/runner.js +2 -0
- package/dist/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +150 -37
- package/dist/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.js +491 -0
- package/modules/camo-backend/src/index.ts +31 -0
- package/modules/camo-backend/src/internal/BrowserSession.ts +224 -53
- package/modules/camo-backend/src/internal/engine-manager.ts +14 -15
- package/modules/camo-backend/src/internal/ws-server.ts +17 -17
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/common.mjs +12 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/persistence.mjs +57 -0
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +2475 -243
- package/modules/camo-runtime/src/autoscript/runtime.mjs +35 -30
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +80 -443
- package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +39 -6
- package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +206 -39
- package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +0 -79
- package/modules/camo-runtime/src/container/runtime-core/operations/viewport.mjs +46 -0
- package/modules/camo-runtime/src/utils/browser-service.mjs +41 -6
- package/modules/camo-runtime/src/utils/js-policy.mjs +28 -0
- package/modules/workflow/blocks/EnsureSession.ts +0 -4
- package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +81 -6
- package/modules/workflow/blocks/WeiboCollectSearchLinksBlock.ts +316 -0
- package/modules/workflow/definitions/weibo-search-workflow-v1.ts +2 -0
- package/modules/workflow/src/runner.ts +2 -0
- package/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.ts +198 -53
- package/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.ts +706 -0
- package/package.json +2 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +0 -498
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/detail.mjs +0 -181
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +0 -691
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +0 -388
- package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +0 -135
|
@@ -261,15 +261,48 @@ export async function restoreCheckpoint({
|
|
|
261
261
|
actionResult = { selector: effectiveSelector, count: matches.length };
|
|
262
262
|
} else if (action === 'scroll_into_view') {
|
|
263
263
|
if (!effectiveSelector) return asErrorPayload('CONTAINER_NOT_FOUND', 'Selector is required for scroll_into_view');
|
|
264
|
-
|
|
264
|
+
const resolved = await callAPI('evaluate', {
|
|
265
265
|
profileId: resolvedProfile,
|
|
266
|
-
script: `(
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
266
|
+
script: `(() => {
|
|
267
|
+
const selector = ${JSON.stringify(effectiveSelector)};
|
|
268
|
+
const nodes = Array.from(document.querySelectorAll(selector));
|
|
269
|
+
const target = nodes.find((node) => {
|
|
270
|
+
if (!(node instanceof Element)) return false;
|
|
271
|
+
const rect = node.getBoundingClientRect?.();
|
|
272
|
+
return Boolean(rect && rect.width > 0 && rect.height > 0);
|
|
273
|
+
}) || nodes[0] || null;
|
|
274
|
+
if (!target) return { ok: false, selector };
|
|
275
|
+
const rect = target.getBoundingClientRect?.() || { left: 0, top: 0, width: 1, height: 1 };
|
|
276
|
+
const rawCenterY = Number(rect.top) + Math.max(1, Number(rect.height) / 2);
|
|
277
|
+
return {
|
|
278
|
+
ok: true,
|
|
279
|
+
selector,
|
|
280
|
+
center: {
|
|
281
|
+
x: Math.max(1, Math.min((window.innerWidth || 1) - 1, Math.round(Number(rect.left) + Math.max(1, Number(rect.width) / 2)))),
|
|
282
|
+
y: Math.max(1, Math.min((window.innerHeight || 1) - 1, Math.round(rawCenterY))),
|
|
283
|
+
},
|
|
284
|
+
rawCenterY,
|
|
285
|
+
viewportHeight: Number(window.innerHeight || 0),
|
|
286
|
+
};
|
|
271
287
|
})()`,
|
|
272
288
|
});
|
|
289
|
+
const target = resolved?.result || resolved?.data?.result || resolved?.data || resolved || null;
|
|
290
|
+
if (!target || target.ok !== true) {
|
|
291
|
+
return asErrorPayload('CONTAINER_NOT_FOUND', `Container selector not found: ${effectiveSelector}`);
|
|
292
|
+
}
|
|
293
|
+
const viewportHeight = Number(target.viewportHeight || 0);
|
|
294
|
+
const rawCenterY = Number(target.rawCenterY || 0);
|
|
295
|
+
if (Number.isFinite(viewportHeight) && viewportHeight > 0 && Number.isFinite(rawCenterY)) {
|
|
296
|
+
const deltaY = Math.round(rawCenterY - viewportHeight / 2);
|
|
297
|
+
if (Math.abs(deltaY) > 48) {
|
|
298
|
+
await callAPI('mouse:wheel', {
|
|
299
|
+
profileId: resolvedProfile,
|
|
300
|
+
deltaX: 0,
|
|
301
|
+
deltaY: Math.max(-900, Math.min(900, deltaY)),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
actionResult = { selector: effectiveSelector, center: target.center || null };
|
|
273
306
|
} else if (action === 'page_back') {
|
|
274
307
|
actionResult = await callAPI('page:back', { profileId: resolvedProfile });
|
|
275
308
|
} else if (action === 'goto_checkpoint_url') {
|
|
@@ -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,
|
|
@@ -54,6 +50,7 @@ async function executeExternalOperationIfAny({
|
|
|
54
50
|
}
|
|
55
51
|
|
|
56
52
|
async function flashOperationViewport(profileId, params = {}) {
|
|
53
|
+
if (!isJsExecutionEnabled()) return;
|
|
57
54
|
if (params.highlight === false) return;
|
|
58
55
|
try {
|
|
59
56
|
await callAPI('evaluate', {
|
|
@@ -77,6 +74,150 @@ async function flashOperationViewport(profileId, params = {}) {
|
|
|
77
74
|
}
|
|
78
75
|
}
|
|
79
76
|
|
|
77
|
+
function sleep(ms) {
|
|
78
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function clamp(value, min, max) {
|
|
82
|
+
return Math.min(Math.max(value, min), max);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isTargetFullyInViewport(target, margin = 6) {
|
|
86
|
+
const rect = target?.rect && typeof target.rect === 'object' ? target.rect : null;
|
|
87
|
+
const viewport = target?.viewport && typeof target.viewport === 'object' ? target.viewport : null;
|
|
88
|
+
if (!rect || !viewport) return true;
|
|
89
|
+
const vw = Number(viewport.width || 0);
|
|
90
|
+
const vh = Number(viewport.height || 0);
|
|
91
|
+
if (!Number.isFinite(vw) || !Number.isFinite(vh) || vw <= 0 || vh <= 0) return true;
|
|
92
|
+
const left = Number(rect.left || 0);
|
|
93
|
+
const top = Number(rect.top || 0);
|
|
94
|
+
const width = Math.max(0, Number(rect.width || 0));
|
|
95
|
+
const height = Math.max(0, Number(rect.height || 0));
|
|
96
|
+
const right = left + width;
|
|
97
|
+
const bottom = top + height;
|
|
98
|
+
const m = Math.max(0, Number(margin) || 0);
|
|
99
|
+
return left >= m && top >= m && right <= (vw - m) && bottom <= (vh - m);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolveScrollDeltaY(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 0;
|
|
106
|
+
const vh = Number(viewport.height || 0);
|
|
107
|
+
if (!Number.isFinite(vh) || vh <= 0) return 0;
|
|
108
|
+
const top = Number(rect.top || 0);
|
|
109
|
+
const height = Math.max(0, Number(rect.height || 0));
|
|
110
|
+
const bottom = top + height;
|
|
111
|
+
const m = Math.max(0, Number(margin) || 0);
|
|
112
|
+
if (top < m) {
|
|
113
|
+
return clamp(Math.round(top - m - 24), -900, -80);
|
|
114
|
+
}
|
|
115
|
+
if (bottom > (vh - m)) {
|
|
116
|
+
return clamp(Math.round(bottom - (vh - m) + 24), 80, 900);
|
|
117
|
+
}
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function resolveSelectorTarget(profileId, selector) {
|
|
122
|
+
const selectorLiteral = JSON.stringify(String(selector || '').trim());
|
|
123
|
+
const payload = await callAPI('evaluate', {
|
|
124
|
+
profileId,
|
|
125
|
+
script: `(() => {
|
|
126
|
+
const selector = ${selectorLiteral};
|
|
127
|
+
const nodes = Array.from(document.querySelectorAll(selector));
|
|
128
|
+
const isVisible = (node) => {
|
|
129
|
+
if (!(node instanceof Element)) return false;
|
|
130
|
+
const rect = node.getBoundingClientRect?.();
|
|
131
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) return false;
|
|
132
|
+
try {
|
|
133
|
+
const style = window.getComputedStyle(node);
|
|
134
|
+
if (!style) return false;
|
|
135
|
+
if (style.display === 'none') return false;
|
|
136
|
+
if (style.visibility === 'hidden' || style.visibility === 'collapse') return false;
|
|
137
|
+
const opacity = Number.parseFloat(String(style.opacity || '1'));
|
|
138
|
+
if (Number.isFinite(opacity) && opacity <= 0.01) return false;
|
|
139
|
+
} catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
};
|
|
144
|
+
const hitVisible = (node) => {
|
|
145
|
+
if (!(node instanceof Element)) return false;
|
|
146
|
+
const rect = node.getBoundingClientRect?.();
|
|
147
|
+
if (!rect) return false;
|
|
148
|
+
const x = Math.max(0, Math.min((window.innerWidth || 1) - 1, rect.left + rect.width / 2));
|
|
149
|
+
const y = Math.max(0, Math.min((window.innerHeight || 1) - 1, rect.top + rect.height / 2));
|
|
150
|
+
const top = document.elementFromPoint(x, y);
|
|
151
|
+
if (!top) return false;
|
|
152
|
+
return top === node || node.contains(top) || top.contains(node);
|
|
153
|
+
};
|
|
154
|
+
const target = nodes.find((item) => isVisible(item) && hitVisible(item))
|
|
155
|
+
|| nodes.find((item) => isVisible(item))
|
|
156
|
+
|| nodes[0]
|
|
157
|
+
|| null;
|
|
158
|
+
if (!target) {
|
|
159
|
+
return { ok: false, error: 'selector_not_found', selector };
|
|
160
|
+
}
|
|
161
|
+
const rect = target.getBoundingClientRect?.() || { left: 0, top: 0, width: 1, height: 1 };
|
|
162
|
+
const rawCenterX = Number(rect.left) + Math.max(1, Number(rect.width) / 2);
|
|
163
|
+
const rawCenterY = Number(rect.top) + Math.max(1, Number(rect.height) / 2);
|
|
164
|
+
const viewport = {
|
|
165
|
+
width: Number(window.innerWidth || 0),
|
|
166
|
+
height: Number(window.innerHeight || 0),
|
|
167
|
+
};
|
|
168
|
+
const center = {
|
|
169
|
+
x: Math.max(1, Math.min((viewport.width || 1) - 1, Math.round(rawCenterX))),
|
|
170
|
+
y: Math.max(1, Math.min((viewport.height || 1) - 1, Math.round(rawCenterY))),
|
|
171
|
+
};
|
|
172
|
+
return {
|
|
173
|
+
ok: true,
|
|
174
|
+
selector,
|
|
175
|
+
matchedIndex: Math.max(0, nodes.indexOf(target)),
|
|
176
|
+
center,
|
|
177
|
+
rawCenter: {
|
|
178
|
+
x: rawCenterX,
|
|
179
|
+
y: rawCenterY,
|
|
180
|
+
},
|
|
181
|
+
rect: {
|
|
182
|
+
left: Number(rect.left || 0),
|
|
183
|
+
top: Number(rect.top || 0),
|
|
184
|
+
width: Number(rect.width || 0),
|
|
185
|
+
height: Number(rect.height || 0),
|
|
186
|
+
},
|
|
187
|
+
viewport,
|
|
188
|
+
};
|
|
189
|
+
})()`,
|
|
190
|
+
});
|
|
191
|
+
const result = payload?.result || payload?.data?.result || payload?.data || payload || null;
|
|
192
|
+
if (!result || result.ok !== true || !result.center) {
|
|
193
|
+
throw new Error(`Element not found: ${selector}`);
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function scrollTargetIntoViewport(profileId, selector, initialTarget, params = {}) {
|
|
199
|
+
let target = initialTarget;
|
|
200
|
+
const maxSteps = Math.max(0, Math.min(8, Number(params.maxScrollSteps ?? 3) || 3));
|
|
201
|
+
const settleMs = Math.max(0, Number(params.scrollSettleMs ?? 0) || 0);
|
|
202
|
+
const margin = Math.max(0, Number(params.viewportMargin ?? 6) || 6);
|
|
203
|
+
for (let i = 0; i < maxSteps; i += 1) {
|
|
204
|
+
if (isTargetFullyInViewport(target, margin)) break;
|
|
205
|
+
const deltaY = resolveScrollDeltaY(target, margin);
|
|
206
|
+
if (!Number.isFinite(deltaY) || Math.abs(deltaY) < 1) break;
|
|
207
|
+
const vw = Number(target?.viewport?.width || 0);
|
|
208
|
+
const vh = Number(target?.viewport?.height || 0);
|
|
209
|
+
if (Number.isFinite(vw) && vw > 2 && Number.isFinite(vh) && vh > 2) {
|
|
210
|
+
const anchorX = clamp(Math.round(vw / 2), 1, Math.max(1, vw - 1));
|
|
211
|
+
const anchorY = clamp(Math.round(vh / 2), 1, Math.max(1, vh - 1));
|
|
212
|
+
await callAPI('mouse:move', { profileId, x: anchorX, y: anchorY, steps: 1 });
|
|
213
|
+
}
|
|
214
|
+
await callAPI('mouse:wheel', { profileId, deltaX: 0, deltaY });
|
|
215
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
216
|
+
target = await resolveSelectorTarget(profileId, selector);
|
|
217
|
+
}
|
|
218
|
+
return target;
|
|
219
|
+
}
|
|
220
|
+
|
|
80
221
|
async function executeSelectorOperation({ profileId, action, operation, params }) {
|
|
81
222
|
const selector = maybeSelector({
|
|
82
223
|
profileId,
|
|
@@ -85,22 +226,66 @@ async function executeSelectorOperation({ profileId, action, operation, params }
|
|
|
85
226
|
});
|
|
86
227
|
if (!selector) return asErrorPayload('CONTAINER_NOT_FOUND', `${action} requires selector/containerId`);
|
|
87
228
|
|
|
88
|
-
|
|
229
|
+
let target = await resolveSelectorTarget(profileId, selector);
|
|
230
|
+
target = await scrollTargetIntoViewport(profileId, selector, target, params);
|
|
231
|
+
|
|
89
232
|
if (action === 'scroll_into_view') {
|
|
90
|
-
|
|
91
|
-
|
|
233
|
+
await callAPI('mouse:move', { profileId, x: target.center.x, y: target.center.y, steps: 2 });
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
code: 'OPERATION_DONE',
|
|
237
|
+
message: 'scroll_into_view done',
|
|
238
|
+
data: { selector, target },
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (action === 'click') {
|
|
243
|
+
const button = String(params.button || 'left').trim() || 'left';
|
|
244
|
+
const clicks = Math.max(1, Number(params.clicks ?? 1) || 1);
|
|
245
|
+
const delay = Number(params.delay);
|
|
246
|
+
const result = await callAPI('mouse:click', {
|
|
92
247
|
profileId,
|
|
93
|
-
|
|
248
|
+
x: target.center.x,
|
|
249
|
+
y: target.center.y,
|
|
250
|
+
button,
|
|
251
|
+
clicks,
|
|
252
|
+
...(Number.isFinite(delay) && delay >= 0 ? { delay } : {}),
|
|
94
253
|
});
|
|
95
|
-
return { ok: true, code: 'OPERATION_DONE', message: '
|
|
254
|
+
return { ok: true, code: 'OPERATION_DONE', message: 'click done', data: { selector, target, result } };
|
|
96
255
|
}
|
|
97
256
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
:
|
|
102
|
-
|
|
103
|
-
|
|
257
|
+
const text = String(params.text ?? params.value ?? '');
|
|
258
|
+
await callAPI('mouse:click', {
|
|
259
|
+
profileId,
|
|
260
|
+
x: target.center.x,
|
|
261
|
+
y: target.center.y,
|
|
262
|
+
button: 'left',
|
|
263
|
+
clicks: 1,
|
|
264
|
+
delay: 30,
|
|
265
|
+
});
|
|
266
|
+
const clearBeforeType = params.clear !== false;
|
|
267
|
+
if (clearBeforeType) {
|
|
268
|
+
await callAPI('keyboard:press', {
|
|
269
|
+
profileId,
|
|
270
|
+
key: process.platform === 'darwin' ? 'Meta+A' : 'Control+A',
|
|
271
|
+
});
|
|
272
|
+
await callAPI('keyboard:press', { profileId, key: 'Backspace' });
|
|
273
|
+
}
|
|
274
|
+
const delay = Number(params.keyDelayMs ?? params.delay);
|
|
275
|
+
await callAPI('keyboard:type', {
|
|
276
|
+
profileId,
|
|
277
|
+
text,
|
|
278
|
+
...(Number.isFinite(delay) && delay >= 0 ? { delay } : {}),
|
|
279
|
+
});
|
|
280
|
+
if (params.pressEnter === true) {
|
|
281
|
+
await callAPI('keyboard:press', { profileId, key: 'Enter' });
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
ok: true,
|
|
285
|
+
code: 'OPERATION_DONE',
|
|
286
|
+
message: 'type done',
|
|
287
|
+
data: { selector, target, length: text.length },
|
|
288
|
+
};
|
|
104
289
|
}
|
|
105
290
|
|
|
106
291
|
async function executeVerifySubscriptions({ profileId, params }) {
|
|
@@ -365,26 +550,11 @@ export async function executeOperation({ profileId, operation, context = {} }) {
|
|
|
365
550
|
if (action === 'press_key') {
|
|
366
551
|
const key = String(params.key || params.value || '').trim();
|
|
367
552
|
if (!key) return asErrorPayload('OPERATION_FAILED', 'press_key requires params.key');
|
|
368
|
-
const
|
|
553
|
+
const delay = Number(params.delay);
|
|
554
|
+
const result = await callAPI('keyboard:press', {
|
|
369
555
|
profileId: resolvedProfile,
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const key = ${JSON.stringify(key)};
|
|
373
|
-
const code = key.length === 1 ? 'Key' + key.toUpperCase() : key;
|
|
374
|
-
const opts = { key, code, bubbles: true, cancelable: true };
|
|
375
|
-
target.dispatchEvent(new KeyboardEvent('keydown', opts));
|
|
376
|
-
target.dispatchEvent(new KeyboardEvent('keypress', opts));
|
|
377
|
-
target.dispatchEvent(new KeyboardEvent('keyup', opts));
|
|
378
|
-
if (key === 'Escape') {
|
|
379
|
-
const closeButton = document.querySelector('.note-detail-mask .close-box, .note-detail-mask .close-circle');
|
|
380
|
-
if (closeButton instanceof HTMLElement) closeButton.click();
|
|
381
|
-
}
|
|
382
|
-
if (key === 'Enter' && target instanceof HTMLInputElement && target.form) {
|
|
383
|
-
if (typeof target.form.requestSubmit === 'function') target.form.requestSubmit();
|
|
384
|
-
else target.form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
385
|
-
}
|
|
386
|
-
return { key, targetTag: target?.tagName || null };
|
|
387
|
-
})()`,
|
|
556
|
+
key,
|
|
557
|
+
...(Number.isFinite(delay) && delay >= 0 ? { delay } : {}),
|
|
388
558
|
});
|
|
389
559
|
return { ok: true, code: 'OPERATION_DONE', message: 'press_key done', data: result };
|
|
390
560
|
}
|
|
@@ -394,10 +564,7 @@ export async function executeOperation({ profileId, operation, context = {} }) {
|
|
|
394
564
|
}
|
|
395
565
|
|
|
396
566
|
if (action === 'evaluate') {
|
|
397
|
-
|
|
398
|
-
if (!script) return asErrorPayload('OPERATION_FAILED', 'evaluate requires params.script');
|
|
399
|
-
const result = await callAPI('evaluate', { profileId: resolvedProfile, script });
|
|
400
|
-
return { ok: true, code: 'OPERATION_DONE', message: 'evaluate done', data: result };
|
|
567
|
+
return asErrorPayload('JS_DISABLED', 'evaluate is disabled in webauto runtime');
|
|
401
568
|
}
|
|
402
569
|
|
|
403
570
|
if (action === 'click' || action === 'type' || action === 'scroll_into_view') {
|
|
@@ -242,34 +242,6 @@ async function waitForTabCountIncrease({
|
|
|
242
242
|
};
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
async function tryOpenTabWithAnchor(profileId, seedUrl, timeoutMs) {
|
|
246
|
-
try {
|
|
247
|
-
const popupResult = await callApiWithTimeout('evaluate', {
|
|
248
|
-
profileId,
|
|
249
|
-
script: `(() => {
|
|
250
|
-
const href = ${JSON.stringify(seedUrl || 'about:blank')};
|
|
251
|
-
const anchor = document.createElement('a');
|
|
252
|
-
anchor.href = href;
|
|
253
|
-
anchor.target = '_blank';
|
|
254
|
-
anchor.rel = 'noopener noreferrer';
|
|
255
|
-
anchor.style.position = 'fixed';
|
|
256
|
-
anchor.style.left = '-9999px';
|
|
257
|
-
anchor.style.top = '-9999px';
|
|
258
|
-
document.body.appendChild(anchor);
|
|
259
|
-
const evt = new MouseEvent('click', { bubbles: true, cancelable: true, view: window });
|
|
260
|
-
const dispatched = anchor.dispatchEvent(evt);
|
|
261
|
-
anchor.click();
|
|
262
|
-
anchor.remove();
|
|
263
|
-
return { opened: true, dispatched };
|
|
264
|
-
})()`,
|
|
265
|
-
}, timeoutMs);
|
|
266
|
-
const popupData = popupResult?.result || popupResult || {};
|
|
267
|
-
return { ok: Boolean(popupData?.opened || popupData?.ok), error: null };
|
|
268
|
-
} catch (err) {
|
|
269
|
-
return { ok: false, error: err };
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
245
|
async function openTabBestEffort({
|
|
274
246
|
profileId,
|
|
275
247
|
seedUrl,
|
|
@@ -335,57 +307,6 @@ async function openTabBestEffort({
|
|
|
335
307
|
openError = err;
|
|
336
308
|
}
|
|
337
309
|
|
|
338
|
-
try {
|
|
339
|
-
const popupResult = await callApiWithTimeout('evaluate', {
|
|
340
|
-
profileId,
|
|
341
|
-
script: `(() => {
|
|
342
|
-
const popup = window.open(${JSON.stringify(seedUrl || 'about:blank')}, '_blank');
|
|
343
|
-
return { opened: !!popup };
|
|
344
|
-
})()`,
|
|
345
|
-
}, apiTimeoutMs);
|
|
346
|
-
const popupData = popupResult?.result || popupResult || {};
|
|
347
|
-
if (Boolean(popupData?.opened || popupData?.ok)) {
|
|
348
|
-
await settle();
|
|
349
|
-
const popupOpened = await waitForTab();
|
|
350
|
-
if (popupOpened.ok) {
|
|
351
|
-
await seedNewestTabIfNeeded({
|
|
352
|
-
profileId,
|
|
353
|
-
seedUrl,
|
|
354
|
-
openDelayMs,
|
|
355
|
-
apiTimeoutMs,
|
|
356
|
-
navigationTimeoutMs,
|
|
357
|
-
syncConfig,
|
|
358
|
-
});
|
|
359
|
-
return { ok: true, mode: 'window.open', error: null };
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
} catch (err) {
|
|
363
|
-
openError = err;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const anchorResult = await tryOpenTabWithAnchor(
|
|
367
|
-
profileId,
|
|
368
|
-
seedUrl,
|
|
369
|
-
Math.max(1200, Math.min(apiTimeoutMs, 5000)),
|
|
370
|
-
);
|
|
371
|
-
if (anchorResult.ok) {
|
|
372
|
-
await settle();
|
|
373
|
-
const anchorOpened = await waitForTab();
|
|
374
|
-
if (anchorOpened.ok) {
|
|
375
|
-
await seedNewestTabIfNeeded({
|
|
376
|
-
profileId,
|
|
377
|
-
seedUrl,
|
|
378
|
-
openDelayMs,
|
|
379
|
-
apiTimeoutMs,
|
|
380
|
-
navigationTimeoutMs,
|
|
381
|
-
syncConfig,
|
|
382
|
-
});
|
|
383
|
-
return { ok: true, mode: 'anchor_click', error: null };
|
|
384
|
-
}
|
|
385
|
-
} else {
|
|
386
|
-
openError = anchorResult.error || openError;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
310
|
return { ok: false, mode: null, error: openError };
|
|
390
311
|
}
|
|
391
312
|
|
|
@@ -41,6 +41,7 @@ export async function executeViewportOperation({ profileId, action, params = {}
|
|
|
41
41
|
const width = hasTargetViewport ? Math.max(320, rawWidth) : null;
|
|
42
42
|
const height = hasTargetViewport ? Math.max(240, rawHeight) : null;
|
|
43
43
|
const followWindow = params.followWindow !== false;
|
|
44
|
+
const fitDisplayWindow = params.fitDisplayWindow !== false;
|
|
44
45
|
const settleMs = Math.max(0, Number(params.settleMs ?? 180) || 180);
|
|
45
46
|
const attempts = Math.max(1, Number(params.attempts ?? 3) || 3);
|
|
46
47
|
const tolerance = Math.max(0, Number(params.tolerancePx ?? 3) || 3);
|
|
@@ -56,6 +57,49 @@ export async function executeViewportOperation({ profileId, action, params = {}
|
|
|
56
57
|
|
|
57
58
|
let measured = await probeWindow();
|
|
58
59
|
if (followWindow && !hasTargetViewport) {
|
|
60
|
+
let resizedWindow = false;
|
|
61
|
+
let windowTarget = null;
|
|
62
|
+
if (fitDisplayWindow) {
|
|
63
|
+
try {
|
|
64
|
+
const displayPayload = await callWithTimeout('system:display', {}, apiTimeoutMs);
|
|
65
|
+
const display = displayPayload?.metrics || displayPayload || {};
|
|
66
|
+
const reserveFromEnv = Number(process.env.CAMO_DEFAULT_WINDOW_VERTICAL_RESERVE ?? 0);
|
|
67
|
+
const reservePx = Number.isFinite(reserveFromEnv) ? Math.max(0, Math.min(240, Math.floor(reserveFromEnv))) : 0;
|
|
68
|
+
const workWidth = Number(display.workWidth || 0);
|
|
69
|
+
const workHeight = Number(display.workHeight || 0);
|
|
70
|
+
const width = Number(display.width || 0);
|
|
71
|
+
const height = Number(display.height || 0);
|
|
72
|
+
const baseW = Math.floor(workWidth > 0 ? workWidth : width);
|
|
73
|
+
const baseH = Math.floor(workHeight > 0 ? workHeight : height);
|
|
74
|
+
if (baseW > 0 && baseH > 0) {
|
|
75
|
+
windowTarget = {
|
|
76
|
+
width: Math.max(960, baseW),
|
|
77
|
+
height: Math.max(700, baseH - reservePx),
|
|
78
|
+
};
|
|
79
|
+
const currentOuterWidth = Number(measured.outerWidth || 0);
|
|
80
|
+
const currentOuterHeight = Number(measured.outerHeight || 0);
|
|
81
|
+
const shouldResize = (
|
|
82
|
+
!Number.isFinite(currentOuterWidth)
|
|
83
|
+
|| !Number.isFinite(currentOuterHeight)
|
|
84
|
+
|| currentOuterWidth < Math.floor(windowTarget.width * 0.92)
|
|
85
|
+
|| currentOuterHeight < Math.floor(windowTarget.height * 0.92)
|
|
86
|
+
);
|
|
87
|
+
if (shouldResize) {
|
|
88
|
+
await callWithTimeout('window:resize', {
|
|
89
|
+
profileId,
|
|
90
|
+
width: windowTarget.width,
|
|
91
|
+
height: windowTarget.height,
|
|
92
|
+
}, apiTimeoutMs);
|
|
93
|
+
if (settleMs > 0) await new Promise((resolve) => setTimeout(resolve, settleMs));
|
|
94
|
+
measured = await probeWindow();
|
|
95
|
+
resizedWindow = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// display probing is best-effort and must not block follow-window sync
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
59
103
|
const innerWidth = Math.max(320, Number(measured.innerWidth || 0) || 1280);
|
|
60
104
|
const innerHeight = Math.max(240, Number(measured.innerHeight || 0) || 720);
|
|
61
105
|
const outerWidth = Math.max(320, Number(measured.outerWidth || 0) || innerWidth);
|
|
@@ -77,6 +121,8 @@ export async function executeViewportOperation({ profileId, action, params = {}
|
|
|
77
121
|
followWindow: true,
|
|
78
122
|
viewport: { width: followWidth, height: followHeight },
|
|
79
123
|
frame: { width: frameW, height: frameH },
|
|
124
|
+
resizedWindow,
|
|
125
|
+
windowTarget,
|
|
80
126
|
measured: synced,
|
|
81
127
|
},
|
|
82
128
|
};
|
|
@@ -9,6 +9,7 @@ import { BROWSER_SERVICE_URL, loadConfig, setRepoRoot } from './config.mjs';
|
|
|
9
9
|
|
|
10
10
|
const requireFromHere = createRequire(import.meta.url);
|
|
11
11
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const DEFAULT_API_TIMEOUT_MS = 30000;
|
|
12
13
|
|
|
13
14
|
function resolveNodeBin() {
|
|
14
15
|
const explicit = String(process.env.WEBAUTO_NODE_BIN || '').trim();
|
|
@@ -54,12 +55,46 @@ function runCamoCli(args = [], options = {}) {
|
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
function resolveApiTimeoutMs(options = {}) {
|
|
59
|
+
const optionValue = Number(options?.timeoutMs);
|
|
60
|
+
if (Number.isFinite(optionValue) && optionValue > 0) {
|
|
61
|
+
return Math.max(1000, Math.floor(optionValue));
|
|
62
|
+
}
|
|
63
|
+
const envValue = Number(process.env.CAMO_API_TIMEOUT_MS);
|
|
64
|
+
if (Number.isFinite(envValue) && envValue > 0) {
|
|
65
|
+
return Math.max(1000, Math.floor(envValue));
|
|
66
|
+
}
|
|
67
|
+
return DEFAULT_API_TIMEOUT_MS;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isTimeoutError(error) {
|
|
71
|
+
const name = String(error?.name || '').toLowerCase();
|
|
72
|
+
const message = String(error?.message || '').toLowerCase();
|
|
73
|
+
return (
|
|
74
|
+
name.includes('timeout')
|
|
75
|
+
|| name.includes('abort')
|
|
76
|
+
|| message.includes('timeout')
|
|
77
|
+
|| message.includes('timed out')
|
|
78
|
+
|| message.includes('aborted')
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function callAPI(action, payload = {}, options = {}) {
|
|
83
|
+
const timeoutMs = resolveApiTimeoutMs(options);
|
|
84
|
+
let r;
|
|
85
|
+
try {
|
|
86
|
+
r = await fetch(`${BROWSER_SERVICE_URL}/command`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({ action, args: payload }),
|
|
90
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (isTimeoutError(error)) {
|
|
94
|
+
throw new Error(`browser-service timeout after ${timeoutMs}ms: ${action}`);
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
63
98
|
|
|
64
99
|
let body;
|
|
65
100
|
try {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const FORBIDDEN_JS_ACTION_RULES = [
|
|
2
|
+
{ code: 'dom_click', re: /\.click\s*\(/i },
|
|
3
|
+
{ code: 'dispatch_event', re: /\.dispatchEvent\s*\(/i },
|
|
4
|
+
{ code: 'js_scroll_by', re: /\bscrollBy\s*\(/i },
|
|
5
|
+
{ code: 'js_scroll_to', re: /\bscrollTo\s*\(/i },
|
|
6
|
+
{ code: 'js_scroll_into_view', re: /\bscrollIntoView\s*\(/i },
|
|
7
|
+
{ code: 'keyboard_event_ctor', re: /\bKeyboardEvent\s*\(/i },
|
|
8
|
+
{ code: 'input_event_ctor', re: /\bInputEvent\s*\(/i },
|
|
9
|
+
{ code: 'dom_value_assign', re: /\.value\s*=/i },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export function detectForbiddenJsAction(script = '') {
|
|
13
|
+
const source = String(script || '');
|
|
14
|
+
for (const rule of FORBIDDEN_JS_ACTION_RULES) {
|
|
15
|
+
if (rule.re.test(source)) return rule.code;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function assertNoForbiddenJsAction(script = '', scope = 'evaluate') {
|
|
21
|
+
const hit = detectForbiddenJsAction(script);
|
|
22
|
+
if (!hit) return;
|
|
23
|
+
throw new Error(`${scope} blocked: forbidden_js_action(${hit})`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isJsExecutionEnabled() {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
@@ -207,7 +207,6 @@ export async function execute(input: EnsureSessionInput): Promise<EnsureSessionO
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
async function clearCssZoom(): Promise<void> {
|
|
210
|
-
if (!profileId.startsWith('xiaohongshu_')) return;
|
|
211
210
|
try {
|
|
212
211
|
await fetch(statusUrl, {
|
|
213
212
|
method: 'POST',
|
|
@@ -230,7 +229,6 @@ export async function execute(input: EnsureSessionInput): Promise<EnsureSessionO
|
|
|
230
229
|
}
|
|
231
230
|
|
|
232
231
|
async function applyZoom(): Promise<void> {
|
|
233
|
-
if (!profileId.startsWith('xiaohongshu_')) return;
|
|
234
232
|
const metrics = await loadMetrics();
|
|
235
233
|
const zoom = resolveZoom(metrics);
|
|
236
234
|
if (zoom === null) {
|
|
@@ -281,7 +279,6 @@ export async function execute(input: EnsureSessionInput): Promise<EnsureSessionO
|
|
|
281
279
|
}
|
|
282
280
|
|
|
283
281
|
async function applyBrowserZoom(): Promise<void> {
|
|
284
|
-
if (!profileId.startsWith('xiaohongshu_')) return;
|
|
285
282
|
const target = resolveBrowserZoomTarget();
|
|
286
283
|
if (!target || Math.abs(target - 1) < 0.01) return;
|
|
287
284
|
const zoomOutSteps = [0.9, 0.8, 0.67, 0.5, 0.33, 0.25];
|
|
@@ -327,7 +324,6 @@ export async function execute(input: EnsureSessionInput): Promise<EnsureSessionO
|
|
|
327
324
|
}
|
|
328
325
|
|
|
329
326
|
async function resetBrowserZoom(): Promise<void> {
|
|
330
|
-
if (!profileId.startsWith('xiaohongshu_')) return;
|
|
331
327
|
if (process.env.WEBAUTO_RESET_BROWSER_ZOOM === '0') return;
|
|
332
328
|
const key = os.platform() === 'darwin' ? 'Meta+0' : 'Control+0';
|
|
333
329
|
try {
|