@web-auto/webauto 0.1.17 → 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.
Files changed (65) hide show
  1. package/README.md +122 -53
  2. package/apps/desktop-console/dist/main/index.mjs +229 -14
  3. package/apps/desktop-console/dist/renderer/index.js +237 -8
  4. package/apps/desktop-console/entry/ui-cli.mjs +290 -21
  5. package/apps/desktop-console/entry/ui-console.mjs +46 -15
  6. package/apps/webauto/entry/account.mjs +126 -27
  7. package/apps/webauto/entry/lib/account-detect.mjs +399 -9
  8. package/apps/webauto/entry/lib/account-store.mjs +201 -109
  9. package/apps/webauto/entry/lib/iflow-reply.mjs +194 -0
  10. package/apps/webauto/entry/lib/profile-policy.mjs +48 -0
  11. package/apps/webauto/entry/lib/profilepool.mjs +12 -0
  12. package/apps/webauto/entry/lib/schedule-store.mjs +29 -2
  13. package/apps/webauto/entry/lib/session-init.mjs +227 -0
  14. package/apps/webauto/entry/lib/upgrade-check.mjs +269 -0
  15. package/apps/webauto/entry/lib/xhs-unified-blocks.mjs +160 -0
  16. package/apps/webauto/entry/lib/xhs-unified-output-blocks.mjs +83 -0
  17. package/apps/webauto/entry/lib/xhs-unified-plan-blocks.mjs +55 -0
  18. package/apps/webauto/entry/lib/xhs-unified-profile-blocks.mjs +542 -0
  19. package/apps/webauto/entry/lib/xhs-unified-runtime-blocks.mjs +436 -0
  20. package/apps/webauto/entry/profilepool.mjs +56 -9
  21. package/apps/webauto/entry/smart-reply-cli.mjs +267 -0
  22. package/apps/webauto/entry/weibo-unified.mjs +84 -11
  23. package/apps/webauto/entry/xhs-orchestrate.mjs +43 -1
  24. package/apps/webauto/entry/xhs-unified.mjs +92 -997
  25. package/bin/webauto.mjs +22 -4
  26. package/dist/modules/camo-backend/src/index.js +33 -0
  27. package/dist/modules/camo-backend/src/internal/BrowserSession.js +232 -49
  28. package/dist/modules/camo-backend/src/internal/engine-manager.js +14 -13
  29. package/dist/modules/camo-backend/src/internal/ws-server.js +16 -19
  30. package/dist/modules/camo-runtime/src/utils/browser-service.mjs +38 -6
  31. package/dist/modules/workflow/blocks/EnsureSession.js +0 -8
  32. package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +78 -6
  33. package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +266 -192
  34. package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +2 -0
  35. package/dist/modules/workflow/src/runner.js +2 -0
  36. package/dist/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +150 -37
  37. package/dist/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.js +491 -0
  38. package/modules/camo-backend/src/index.ts +31 -0
  39. package/modules/camo-backend/src/internal/BrowserSession.ts +224 -53
  40. package/modules/camo-backend/src/internal/engine-manager.ts +14 -15
  41. package/modules/camo-backend/src/internal/ws-server.ts +17 -17
  42. package/modules/camo-runtime/src/autoscript/action-providers/xhs/common.mjs +12 -2
  43. package/modules/camo-runtime/src/autoscript/action-providers/xhs/persistence.mjs +57 -0
  44. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +2475 -243
  45. package/modules/camo-runtime/src/autoscript/runtime.mjs +35 -30
  46. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +80 -443
  47. package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +39 -6
  48. package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +206 -39
  49. package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +0 -79
  50. package/modules/camo-runtime/src/container/runtime-core/operations/viewport.mjs +46 -0
  51. package/modules/camo-runtime/src/utils/browser-service.mjs +41 -6
  52. package/modules/camo-runtime/src/utils/js-policy.mjs +28 -0
  53. package/modules/workflow/blocks/EnsureSession.ts +0 -4
  54. package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +81 -6
  55. package/modules/workflow/blocks/WeiboCollectSearchLinksBlock.ts +316 -0
  56. package/modules/workflow/definitions/weibo-search-workflow-v1.ts +2 -0
  57. package/modules/workflow/src/runner.ts +2 -0
  58. package/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.ts +198 -53
  59. package/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.ts +706 -0
  60. package/package.json +2 -2
  61. package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +0 -498
  62. package/modules/camo-runtime/src/autoscript/action-providers/xhs/detail.mjs +0 -181
  63. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +0 -691
  64. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +0 -388
  65. 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
- actionResult = await callAPI('evaluate', {
264
+ const resolved = await callAPI('evaluate', {
265
265
  profileId: resolvedProfile,
266
- script: `(async () => {
267
- const el = document.querySelector(${JSON.stringify(effectiveSelector)});
268
- if (!el) throw new Error('Element not found');
269
- el.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' });
270
- return { ok: true, selector: ${JSON.stringify(effectiveSelector)} };
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
- const highlight = params.highlight !== false;
229
+ let target = await resolveSelectorTarget(profileId, selector);
230
+ target = await scrollTargetIntoViewport(profileId, selector, target, params);
231
+
89
232
  if (action === 'scroll_into_view') {
90
- const script = buildSelectorScrollIntoViewScript({ selector, highlight });
91
- const result = await callAPI('evaluate', {
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
- script,
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: 'scroll_into_view done', data: result };
254
+ return { ok: true, code: 'OPERATION_DONE', message: 'click done', data: { selector, target, result } };
96
255
  }
97
256
 
98
- const typeText = String(params.text ?? params.value ?? '');
99
- const script = action === 'click'
100
- ? buildSelectorClickScript({ selector, highlight })
101
- : buildSelectorTypeScript({ selector, highlight, text: typeText });
102
- const result = await callAPI('evaluate', { profileId, script });
103
- return { ok: true, code: 'OPERATION_DONE', message: `${action} done`, data: result };
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 result = await callAPI('evaluate', {
553
+ const delay = Number(params.delay);
554
+ const result = await callAPI('keyboard:press', {
369
555
  profileId: resolvedProfile,
370
- script: `(async () => {
371
- const target = document.activeElement || document.body || document.documentElement;
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
- const script = String(params.script || '').trim();
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
- export async function callAPI(action, payload = {}) {
58
- const r = await fetch(`${BROWSER_SERVICE_URL}/command`, {
59
- method: 'POST',
60
- headers: { 'Content-Type': 'application/json' },
61
- body: JSON.stringify({ action, args: payload }),
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 {