@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
package/bin/webauto.mjs CHANGED
@@ -163,6 +163,7 @@ Core Commands:
163
163
  webauto schedule <list|get|add|update|delete|import|export|run|run-due|daemon> [options]
164
164
  webauto deps <check|auto|install|uninstall|reinstall> [options]
165
165
  webauto ui console [--build] [--install] [--check]
166
+ webauto ui restart [--build] [--install] [--timeout <ms>] [--reason <text>]
166
167
  webauto ui cli <action> [options]
167
168
  webauto xhs install [--download-browser] [--download-geoip] [--ensure-backend]
168
169
  webauto xhs unified [xhs options...]
@@ -193,6 +194,7 @@ Examples (standard):
193
194
  webauto ui console --check
194
195
  webauto ui console --build
195
196
  webauto ui console --install
197
+ webauto ui restart --build --reason "reload-after-pull"
196
198
  webauto ui cli start --build
197
199
  webauto ui cli tab --tab 配置
198
200
  webauto ui cli input --selector "#keyword-input" --value "seedance2.0"
@@ -214,6 +216,7 @@ function printUiConsoleHelp() {
214
216
 
215
217
  Usage:
216
218
  webauto ui console [--build] [--install] [--check] [--no-daemon]
219
+ webauto ui restart [--build] [--install] [--timeout <ms>] [--reason <text>]
217
220
  webauto ui cli <action> [options]
218
221
 
219
222
  Options:
@@ -224,6 +227,7 @@ Options:
224
227
 
225
228
  CLI Actions:
226
229
  start 启动 UI(可配合 --build/--install)
230
+ restart 广播重启消息,应用自停并自启(可配合 --reason)
227
231
  status 获取 UI 运行状态(含 runId/错误计数)
228
232
  snapshot 获取 UI 快照(含当前 tab 与关键控件值)
229
233
  tab 切换 tab(--tab config 或 --label 配置)
@@ -250,6 +254,7 @@ Examples:
250
254
  webauto ui console --check
251
255
  webauto ui console --build
252
256
  webauto ui console --install
257
+ webauto ui restart --reason "git pull"
253
258
  webauto ui cli start --build
254
259
  webauto ui cli status --json
255
260
  webauto ui cli tab --tab 配置
@@ -485,11 +490,14 @@ async function runInDir(dir, cmd, args) {
485
490
  }
486
491
 
487
492
  function checkDesktopConsoleDeps() {
488
- const electronName = process.platform === 'win32' ? 'electron.exe' : 'electron';
489
- const candidates = [
490
- path.join(ROOT, 'node_modules', 'electron', 'dist', electronName),
491
- path.join(ROOT, 'apps', 'desktop-console', 'node_modules', 'electron', 'dist', electronName),
493
+ const electronNames = process.platform === 'win32'
494
+ ? ['electron.exe']
495
+ : (process.platform === 'darwin' ? ['electron', path.join('Electron.app', 'Contents', 'MacOS', 'Electron')] : ['electron']);
496
+ const roots = [
497
+ path.join(ROOT, 'node_modules', 'electron', 'dist'),
498
+ path.join(ROOT, 'apps', 'desktop-console', 'node_modules', 'electron', 'dist'),
492
499
  ];
500
+ const candidates = roots.flatMap((base) => electronNames.map((name) => path.join(base, name)));
493
501
  return candidates.some((p) => exists(p));
494
502
  }
495
503
 
@@ -775,6 +783,16 @@ async function main() {
775
783
  return;
776
784
  }
777
785
 
786
+ if (cmd === 'ui' && sub === 'restart') {
787
+ await ensureUiRuntimeReady({
788
+ build: args.build === true,
789
+ install: args.install === true,
790
+ });
791
+ const script = path.join(ROOT, 'apps', 'desktop-console', 'entry', 'ui-cli.mjs');
792
+ await run(process.execPath, [script, 'restart', ...rawArgv.slice(2)]);
793
+ return;
794
+ }
795
+
778
796
  if (cmd === 'ui' && sub === 'cli') {
779
797
  await ensureUiRuntimeReady({
780
798
  build: args.build === true,
@@ -17,6 +17,12 @@ function readNumber(value) {
17
17
  return null;
18
18
  return parsed;
19
19
  }
20
+ function readPositiveNumber(value) {
21
+ const parsed = Number(value);
22
+ if (!Number.isFinite(parsed) || parsed <= 0)
23
+ return null;
24
+ return parsed;
25
+ }
20
26
  function getDisplayMetrics() {
21
27
  const envWidth = readNumber(process.env.WEBAUTO_SCREEN_WIDTH);
22
28
  const envHeight = readNumber(process.env.WEBAUTO_SCREEN_HEIGHT);
@@ -120,6 +126,31 @@ function getDisplayMetrics() {
120
126
  return null;
121
127
  }
122
128
  }
129
+ function resolveStartViewport(args) {
130
+ const explicitWidth = readPositiveNumber(args?.width ?? args?.viewportWidth ?? args?.viewport?.width);
131
+ const explicitHeight = readPositiveNumber(args?.height ?? args?.viewportHeight ?? args?.viewport?.height);
132
+ if (explicitWidth && explicitHeight) {
133
+ return {
134
+ width: Math.floor(explicitWidth),
135
+ height: Math.floor(explicitHeight),
136
+ };
137
+ }
138
+ if (Boolean(args?.headless))
139
+ return null;
140
+ const display = getDisplayMetrics();
141
+ if (!display)
142
+ return null;
143
+ const reserveRaw = Number(process.env.WEBAUTO_WINDOW_VERTICAL_RESERVE ?? 0);
144
+ const reserve = Number.isFinite(reserveRaw) ? Math.max(0, Math.min(240, Math.floor(reserveRaw))) : 0;
145
+ const baseWidth = readPositiveNumber(display.workWidth) || readPositiveNumber(display.width);
146
+ const baseHeight = readPositiveNumber(display.workHeight) || readPositiveNumber(display.height);
147
+ if (!baseWidth || !baseHeight)
148
+ return null;
149
+ return {
150
+ width: Math.max(960, Math.floor(baseWidth)),
151
+ height: Math.max(700, Math.floor(baseHeight - reserve)),
152
+ };
153
+ }
123
154
  export async function startBrowserService(opts = {}) {
124
155
  const { logEvent } = installServiceProcessLogger({ serviceName: 'browser-service' });
125
156
  const host = opts.host || '127.0.0.1';
@@ -260,6 +291,7 @@ async function handleCommand(payload, manager, wsServer, options = {}) {
260
291
  const args = payload.args ?? payload;
261
292
  switch (action) {
262
293
  case 'start': {
294
+ const startViewport = resolveStartViewport(args);
263
295
  const opts = {
264
296
  profileId: args.profileId || 'default',
265
297
  sessionName: args.profileId || 'default',
@@ -267,6 +299,7 @@ async function handleCommand(payload, manager, wsServer, options = {}) {
267
299
  initialUrl: args.url,
268
300
  engine: args.engine || 'camoufox',
269
301
  fingerprintPlatform: args.fingerprintPlatform || null,
302
+ ...(startViewport ? { viewport: startViewport } : {}),
270
303
  ...(args.ownerPid ? { ownerPid: args.ownerPid } : {}),
271
304
  };
272
305
  const res = await manager.createSession(opts);
@@ -10,6 +10,32 @@ import { loadOrGenerateFingerprint, applyFingerprint } from './fingerprint.js';
10
10
  import { launchEngineContext } from './engine-manager.js';
11
11
  import { resolveCookiesRoot, resolveProfilesRoot, resolveRecordsRoot } from './storage-paths.js';
12
12
  const stateBus = getStateBus();
13
+ function resolveInputActionTimeoutMs() {
14
+ const raw = Number(process.env.CAMO_INPUT_ACTION_TIMEOUT_MS ?? process.env.CAMO_API_TIMEOUT_MS ?? 30000);
15
+ return Math.max(1000, Number.isFinite(raw) ? raw : 30000);
16
+ }
17
+ function resolveNavigationWaitUntil() {
18
+ const raw = String(process.env.CAMO_NAV_WAIT_UNTIL ?? 'commit').trim().toLowerCase();
19
+ if (raw === 'load')
20
+ return 'load';
21
+ if (raw === 'domcontentloaded' || raw === 'dom')
22
+ return 'domcontentloaded';
23
+ if (raw === 'networkidle')
24
+ return 'networkidle';
25
+ return 'commit';
26
+ }
27
+ function resolveInputActionMaxAttempts() {
28
+ const raw = Number(process.env.CAMO_INPUT_ACTION_MAX_ATTEMPTS ?? 2);
29
+ return Math.max(1, Math.min(3, Number.isFinite(raw) ? Math.floor(raw) : 2));
30
+ }
31
+ function resolveInputRecoveryDelayMs() {
32
+ const raw = Number(process.env.CAMO_INPUT_RECOVERY_DELAY_MS ?? 120);
33
+ return Math.max(0, Number.isFinite(raw) ? Math.floor(raw) : 120);
34
+ }
35
+ function resolveInputRecoveryBringToFrontTimeoutMs() {
36
+ const raw = Number(process.env.CAMO_INPUT_RECOVERY_BRING_TO_FRONT_TIMEOUT_MS ?? 800);
37
+ return Math.max(100, Number.isFinite(raw) ? Math.floor(raw) : 800);
38
+ }
13
39
  export class BrowserSession {
14
40
  options;
15
41
  browser;
@@ -25,6 +51,8 @@ export class BrowserSession {
25
51
  bridgedPages = new WeakSet();
26
52
  recorderBridgePages = new WeakSet();
27
53
  lastViewport = null;
54
+ followWindowViewport = false;
55
+ inputActionTail = Promise.resolve();
28
56
  fingerprint = null;
29
57
  recordingStream = null;
30
58
  recording = {
@@ -86,14 +114,25 @@ export class BrowserSession {
86
114
  platform: fingerprint.platform,
87
115
  userAgent: fingerprint.userAgent?.substring(0, 50) + '...',
88
116
  });
89
- const viewport = this.options.viewport || { width: 3840, height: 2046 };
117
+ const fallbackViewport = { width: 1440, height: 1100 };
118
+ const explicitViewport = this.options.viewport
119
+ && Number(this.options.viewport.width) > 0
120
+ && Number(this.options.viewport.height) > 0
121
+ ? {
122
+ width: Math.floor(Number(this.options.viewport.width)),
123
+ height: Math.floor(Number(this.options.viewport.height)),
124
+ }
125
+ : null;
126
+ const viewport = explicitViewport || fingerprint?.viewport || fallbackViewport;
127
+ const headless = !!this.options.headless;
128
+ this.followWindowViewport = !headless;
90
129
  const deviceScaleFactor = this.resolveDeviceScaleFactor();
91
130
  // 使用 EngineManager 启动上下文(Chromium 已移除,仅支持 Camoufox)
92
131
  this.context = await launchEngineContext({
93
132
  engine,
94
- headless: !!this.options.headless,
133
+ headless,
95
134
  profileDir: this.profileDir,
96
- viewport: fingerprint?.viewport || viewport,
135
+ viewport,
97
136
  userAgent: fingerprint?.userAgent,
98
137
  locale: 'zh-CN',
99
138
  timezoneId: fingerprint?.timezoneId || 'Asia/Shanghai',
@@ -101,7 +140,9 @@ export class BrowserSession {
101
140
  // 应用指纹到上下文(Playwright JS 注入)
102
141
  await applyFingerprint(this.context, fingerprint);
103
142
  // NOTE: deviceScaleFactor override was Chromium-only (CDP). Chromium removed.
104
- this.lastViewport = { width: viewport.width, height: viewport.height };
143
+ this.lastViewport = this.followWindowViewport
144
+ ? null
145
+ : { width: viewport.width, height: viewport.height };
105
146
  this.browser = this.context.browser();
106
147
  this.browser.on('disconnected', () => this.notifyExit());
107
148
  this.context.on('close', () => this.notifyExit());
@@ -109,6 +150,9 @@ export class BrowserSession {
109
150
  this.page = existing.length ? existing[0] : await this.context.newPage();
110
151
  this.setupPageHooks(this.page);
111
152
  this.context.on('page', (p) => this.setupPageHooks(p));
153
+ if (this.followWindowViewport) {
154
+ await this.refreshViewportFromWindow(this.page).catch(() => { });
155
+ }
112
156
  if (initialUrl) {
113
157
  await this.goto(initialUrl);
114
158
  }
@@ -782,6 +826,10 @@ export class BrowserSession {
782
826
  }
783
827
  }
784
828
  async ensurePageViewport(page) {
829
+ if (this.followWindowViewport) {
830
+ await this.refreshViewportFromWindow(page).catch(() => { });
831
+ return;
832
+ }
785
833
  if (!this.lastViewport)
786
834
  return;
787
835
  const current = page.viewportSize();
@@ -795,6 +843,33 @@ export class BrowserSession {
795
843
  await this.syncWindowBounds(page, { ...this.lastViewport });
796
844
  await this.syncDeviceScaleFactor(page, { ...this.lastViewport });
797
845
  }
846
+ async readWindowInnerSize(page) {
847
+ try {
848
+ const metrics = await page.evaluate(() => ({
849
+ width: Math.floor(Number(window.innerWidth || 0)),
850
+ height: Math.floor(Number(window.innerHeight || 0)),
851
+ }));
852
+ const width = Number(metrics?.width);
853
+ const height = Number(metrics?.height);
854
+ if (!Number.isFinite(width) || !Number.isFinite(height))
855
+ return null;
856
+ if (width < 300 || height < 200)
857
+ return null;
858
+ return { width, height };
859
+ }
860
+ catch {
861
+ return null;
862
+ }
863
+ }
864
+ async refreshViewportFromWindow(page) {
865
+ const inner = await this.readWindowInnerSize(page);
866
+ if (!inner)
867
+ return;
868
+ this.lastViewport = {
869
+ width: Math.max(800, Math.floor(inner.width)),
870
+ height: Math.max(700, Math.floor(inner.height)),
871
+ };
872
+ }
798
873
  ensureContext() {
799
874
  if (!this.context) {
800
875
  throw new Error('browser context not ready');
@@ -805,6 +880,12 @@ export class BrowserSession {
805
880
  const ctx = this.ensureContext();
806
881
  const existing = this.getActivePage();
807
882
  if (existing) {
883
+ try {
884
+ await this.ensurePageViewport(existing);
885
+ }
886
+ catch {
887
+ /* ignore */
888
+ }
808
889
  return existing;
809
890
  }
810
891
  this.page = await ctx.newPage();
@@ -830,9 +911,10 @@ export class BrowserSession {
830
911
  }
831
912
  async goBack() {
832
913
  const page = await this.ensurePrimaryPage();
914
+ const waitUntil = resolveNavigationWaitUntil();
833
915
  try {
834
916
  const res = await page
835
- .goBack({ waitUntil: 'domcontentloaded' })
917
+ .goBack({ waitUntil })
836
918
  .catch(() => null);
837
919
  await ensurePageRuntime(page, true).catch(() => { });
838
920
  this.lastKnownUrl = page.url();
@@ -919,7 +1001,7 @@ export class BrowserSession {
919
1001
  /* ignore */
920
1002
  }
921
1003
  if (url) {
922
- await page.goto(url, { waitUntil: 'domcontentloaded' });
1004
+ await page.goto(url, { waitUntil: resolveNavigationWaitUntil() });
923
1005
  await ensurePageRuntime(page);
924
1006
  this.lastKnownUrl = url;
925
1007
  }
@@ -1063,7 +1145,7 @@ export class BrowserSession {
1063
1145
  }
1064
1146
  async goto(url) {
1065
1147
  const page = await this.ensurePrimaryPage();
1066
- await page.goto(url, { waitUntil: 'domcontentloaded' });
1148
+ await page.goto(url, { waitUntil: resolveNavigationWaitUntil() });
1067
1149
  await ensurePageRuntime(page);
1068
1150
  this.lastKnownUrl = url;
1069
1151
  }
@@ -1071,19 +1153,9 @@ export class BrowserSession {
1071
1153
  const page = await this.ensurePrimaryPage();
1072
1154
  return page.screenshot({ fullPage });
1073
1155
  }
1074
- async click(selector) {
1075
- const page = await this.ensurePrimaryPage();
1076
- await page.click(selector, { timeout: 20000 });
1077
- }
1078
- async fill(selector, text) {
1079
- const page = await this.ensurePrimaryPage();
1080
- await page.fill(selector, text, { timeout: 20000 });
1081
- }
1082
1156
  async ensureInputReady(page) {
1083
1157
  if (this.options.headless)
1084
1158
  return;
1085
- if (os.platform() !== 'win32')
1086
- return;
1087
1159
  let needsFocus = false;
1088
1160
  try {
1089
1161
  const state = await page.evaluate(() => ({
@@ -1107,28 +1179,118 @@ export class BrowserSession {
1107
1179
  // Keep best-effort behavior and do not block input flow on platform quirks.
1108
1180
  }
1109
1181
  }
1182
+ async withInputActionTimeout(label, run) {
1183
+ const timeoutMs = resolveInputActionTimeoutMs();
1184
+ let timer = null;
1185
+ try {
1186
+ return await Promise.race([
1187
+ run(),
1188
+ new Promise((_resolve, reject) => {
1189
+ timer = setTimeout(() => {
1190
+ reject(new Error(`${label} timed out after ${timeoutMs}ms`));
1191
+ }, timeoutMs);
1192
+ }),
1193
+ ]);
1194
+ }
1195
+ finally {
1196
+ if (timer)
1197
+ clearTimeout(timer);
1198
+ }
1199
+ }
1200
+ async recoverInputPipeline(page) {
1201
+ const bringToFrontTimeoutMs = resolveInputRecoveryBringToFrontTimeoutMs();
1202
+ let bringToFrontTimer = null;
1203
+ try {
1204
+ await Promise.race([
1205
+ page.bringToFront(),
1206
+ new Promise((_resolve, reject) => {
1207
+ bringToFrontTimer = setTimeout(() => {
1208
+ reject(new Error(`input recovery bringToFront timed out after ${bringToFrontTimeoutMs}ms`));
1209
+ }, bringToFrontTimeoutMs);
1210
+ }),
1211
+ ]);
1212
+ }
1213
+ catch {
1214
+ // Best-effort recovery only.
1215
+ }
1216
+ finally {
1217
+ if (bringToFrontTimer)
1218
+ clearTimeout(bringToFrontTimer);
1219
+ }
1220
+ const delayMs = resolveInputRecoveryDelayMs();
1221
+ if (delayMs > 0) {
1222
+ try {
1223
+ await page.waitForTimeout(delayMs);
1224
+ }
1225
+ catch {
1226
+ // Best-effort recovery only.
1227
+ }
1228
+ }
1229
+ return page;
1230
+ }
1231
+ async runInputAction(page, label, run) {
1232
+ const maxAttempts = resolveInputActionMaxAttempts();
1233
+ let lastError = null;
1234
+ let activePage = page;
1235
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1236
+ try {
1237
+ return await this.withInputActionTimeout(`${label} (attempt ${attempt}/${maxAttempts})`, () => run(activePage));
1238
+ }
1239
+ catch (error) {
1240
+ lastError = error;
1241
+ if (attempt >= maxAttempts)
1242
+ break;
1243
+ activePage = await this.recoverInputPipeline(activePage);
1244
+ }
1245
+ }
1246
+ if (lastError instanceof Error)
1247
+ throw lastError;
1248
+ throw new Error(`${label} failed`);
1249
+ }
1250
+ async withInputActionLock(run) {
1251
+ const previous = this.inputActionTail;
1252
+ let release = null;
1253
+ this.inputActionTail = new Promise((resolve) => {
1254
+ release = resolve;
1255
+ });
1256
+ await previous.catch(() => { });
1257
+ try {
1258
+ return await run();
1259
+ }
1260
+ finally {
1261
+ if (release)
1262
+ release();
1263
+ }
1264
+ }
1110
1265
  /**
1111
1266
  * 基于屏幕坐标的系统级鼠标点击(Playwright 原生)
1112
1267
  * @param opts 屏幕坐标及点击选项
1113
1268
  */
1114
1269
  async mouseClick(opts) {
1115
1270
  const page = await this.ensurePrimaryPage();
1116
- await this.ensureInputReady(page);
1117
- const { x, y, button = 'left', clicks = 1, delay = 50 } = opts;
1118
- // 移动鼠标到目标位置(模拟轨迹)
1119
- await page.mouse.move(x, y, { steps: 3 });
1120
- // 执行点击(支持多次、间隔随机抖动)
1121
- for (let i = 0; i < clicks; i++) {
1122
- if (i > 0) {
1123
- // 多次点击间隔 100-200ms
1124
- await new Promise(r => setTimeout(r, 100 + Math.random() * 100));
1271
+ await this.withInputActionLock(async () => {
1272
+ await this.runInputAction(page, 'input:ready', (activePage) => this.ensureInputReady(activePage));
1273
+ const { x, y, button = 'left', clicks = 1, delay = 50 } = opts;
1274
+ // Avoid page.mouse.click() composite hangs by using explicit down/up sequence.
1275
+ for (let i = 0; i < clicks; i++) {
1276
+ if (i > 0) {
1277
+ // 多次点击间隔 100-200ms
1278
+ await new Promise(r => setTimeout(r, 100 + Math.random() * 100));
1279
+ }
1280
+ await this.runInputAction(page, 'mouse:move(pre-click)', (activePage) => activePage.mouse.move(x, y, { steps: 1 }));
1281
+ await this.runInputAction(page, 'mouse:down', (activePage) => activePage.mouse.down({
1282
+ button,
1283
+ clickCount: 1,
1284
+ }));
1285
+ if (delay > 0) {
1286
+ await page.waitForTimeout(delay);
1287
+ }
1288
+ await this.runInputAction(page, 'mouse:up', (activePage) => activePage.mouse.up({
1289
+ button,
1290
+ clickCount: 1,
1291
+ }));
1125
1292
  }
1126
- await page.mouse.click(x, y, {
1127
- button,
1128
- clickCount: 1,
1129
- delay // 按键间隔
1130
- });
1131
- }
1293
+ });
1132
1294
  }
1133
1295
  /**
1134
1296
  * 基于屏幕坐标的鼠标移动(Playwright 原生)
@@ -1136,29 +1298,35 @@ export class BrowserSession {
1136
1298
  */
1137
1299
  async mouseMove(opts) {
1138
1300
  const page = await this.ensurePrimaryPage();
1139
- await this.ensureInputReady(page);
1140
- const { x, y, steps = 3 } = opts;
1141
- await page.mouse.move(x, y, { steps });
1301
+ await this.withInputActionLock(async () => {
1302
+ await this.runInputAction(page, 'input:ready', (activePage) => this.ensureInputReady(activePage));
1303
+ const { x, y, steps = 3 } = opts;
1304
+ await this.runInputAction(page, 'mouse:move', (activePage) => activePage.mouse.move(x, y, { steps }));
1305
+ });
1142
1306
  }
1143
1307
  /**
1144
1308
  * 基于键盘的系统输入(Playwright keyboard)
1145
1309
  */
1146
1310
  async keyboardType(opts) {
1147
1311
  const page = await this.ensurePrimaryPage();
1148
- await this.ensureInputReady(page);
1149
- const { text, delay = 80, submit } = opts;
1150
- if (text && text.length > 0) {
1151
- await page.keyboard.type(text, { delay });
1152
- }
1153
- if (submit) {
1154
- await page.keyboard.press('Enter');
1155
- }
1312
+ await this.withInputActionLock(async () => {
1313
+ await this.runInputAction(page, 'input:ready', (activePage) => this.ensureInputReady(activePage));
1314
+ const { text, delay = 80, submit } = opts;
1315
+ if (text && text.length > 0) {
1316
+ await this.runInputAction(page, 'keyboard:type', (activePage) => activePage.keyboard.type(text, { delay }));
1317
+ }
1318
+ if (submit) {
1319
+ await this.runInputAction(page, 'keyboard:press', (activePage) => activePage.keyboard.press('Enter'));
1320
+ }
1321
+ });
1156
1322
  }
1157
1323
  async keyboardPress(opts) {
1158
1324
  const page = await this.ensurePrimaryPage();
1159
- await this.ensureInputReady(page);
1160
- const { key, delay } = opts;
1161
- await page.keyboard.press(key, typeof delay === 'number' ? { delay } : undefined);
1325
+ await this.withInputActionLock(async () => {
1326
+ await this.runInputAction(page, 'input:ready', (activePage) => this.ensureInputReady(activePage));
1327
+ const { key, delay } = opts;
1328
+ await this.runInputAction(page, 'keyboard:press', (activePage) => activePage.keyboard.press(key, typeof delay === 'number' ? { delay } : undefined));
1329
+ });
1162
1330
  }
1163
1331
  /**
1164
1332
  * 基于鼠标滚轮的系统滚动(Playwright mouse.wheel)
@@ -1166,9 +1334,11 @@ export class BrowserSession {
1166
1334
  */
1167
1335
  async mouseWheel(opts) {
1168
1336
  const page = await this.ensurePrimaryPage();
1169
- await this.ensureInputReady(page);
1170
- const { deltaX = 0, deltaY } = opts;
1171
- await page.mouse.wheel(Number(deltaX) || 0, Number(deltaY) || 0);
1337
+ await this.withInputActionLock(async () => {
1338
+ await this.runInputAction(page, 'input:ready', (activePage) => this.ensureInputReady(activePage));
1339
+ const { deltaX = 0, deltaY } = opts;
1340
+ await this.runInputAction(page, 'mouse:wheel', (activePage) => activePage.mouse.wheel(Number(deltaX) || 0, Number(deltaY) || 0));
1341
+ });
1172
1342
  }
1173
1343
  async syncWindowBounds(page, viewport) {
1174
1344
  const engine = String(this.options.engine ?? 'camoufox');
@@ -1286,6 +1456,19 @@ export class BrowserSession {
1286
1456
  if (!width || !height) {
1287
1457
  throw new Error('invalid_viewport_size');
1288
1458
  }
1459
+ if (this.followWindowViewport && !this.options.headless) {
1460
+ await page.evaluate(({ w, h }) => {
1461
+ try {
1462
+ window.resizeTo(w, h);
1463
+ }
1464
+ catch { }
1465
+ }, { w: width, h: height });
1466
+ await page.waitForTimeout(150);
1467
+ await this.refreshViewportFromWindow(page).catch(() => { });
1468
+ const next = this.lastViewport || { width, height };
1469
+ await this.maybeCenterWindow(page, next).catch(() => { });
1470
+ return next;
1471
+ }
1289
1472
  await page.setViewportSize({ width, height });
1290
1473
  await this.syncWindowBounds(page, { width, height });
1291
1474
  await this.syncDeviceScaleFactor(page, { width, height });
@@ -158,15 +158,13 @@ export async function launchEngineContext(opts) {
158
158
  const physicalH = Number(dm?.height || 2304);
159
159
  const workW = Number(dm?.workWidth || physicalW || 4096);
160
160
  const workH = Number(dm?.workHeight || physicalH || 2190);
161
- // Use provided viewport as MAXIMUM (e.g., from fingerprint or explicit opt), default to full work area
162
- const maxViewportW = Number(opts.viewport?.width || workW);
163
- const maxViewportH = Number(opts.viewport?.height || workH);
164
- // Target: fill work area, but don't exceed explicit viewport caps if provided
165
- const targetW = maxViewportW > 0 ? Math.min(maxViewportW, workW) : workW;
166
- const targetH = maxViewportH > 0 ? Math.min(maxViewportH, workH) : workH;
167
- // Use targetW/targetH as viewport (even in headless) to match headful size
168
- const viewportW = Math.max(1440, Math.floor(Number(targetW) || 4096));
169
- const viewportH = Math.max(900, Math.floor(Number(targetH) || 2190));
161
+ // Base viewport target:
162
+ // - prefer explicit/fingerprint viewport if provided
163
+ // - otherwise use a deterministic default for stable first-run UX
164
+ const requestedW = Number(opts.viewport?.width || process.env.WEBAUTO_VIEWPORT_WIDTH || 1440);
165
+ const requestedH = Number(opts.viewport?.height || process.env.WEBAUTO_VIEWPORT_HEIGHT || 1100);
166
+ const viewportW = Math.max(900, Math.floor(Number(requestedW) || 1440));
167
+ const viewportH = Math.max(700, Math.floor(Number(requestedH) || 1100));
170
168
  const envHeadlessW = Number(process.env.WEBAUTO_HEADLESS_WIDTH || 0);
171
169
  const envHeadlessH = Number(process.env.WEBAUTO_HEADLESS_HEIGHT || 0);
172
170
  const headlessW = envHeadlessW > 0 ? envHeadlessW : viewportW;
@@ -175,9 +173,12 @@ export async function launchEngineContext(opts) {
175
173
  throw new Error('headless viewport invalid');
176
174
  }
177
175
  const headless = Boolean(opts.headless);
178
- // Final window size: maximize to fill work area (leave small margin for chrome/window decorations)
179
- const winW = headless ? headlessW : Math.max(1440, Math.floor(workW * 0.95));
180
- const winH = headless ? headlessH : Math.max(900, Math.floor(workH - 80));
176
+ // Final headful window size:
177
+ // align window with viewport by default and keep it inside available work area.
178
+ const maxHeadfulW = workW > 0 ? Math.max(900, workW - 40) : 1920;
179
+ const maxHeadfulH = workH > 0 ? Math.max(700, workH - 80) : 1200;
180
+ const winW = headless ? headlessW : Math.min(viewportW, maxHeadfulW);
181
+ const winH = headless ? headlessH : Math.min(viewportH, maxHeadfulH);
181
182
  // Ensure integer types for Camoufox
182
183
  const screenW = Math.floor(physicalW) | 0;
183
184
  const screenH = Math.floor(physicalH) | 0;
@@ -219,7 +220,7 @@ export async function launchEngineContext(opts) {
219
220
  headless,
220
221
  os: targetOS,
221
222
  window: [intWinW, intWinH],
222
- viewport: { width: headless ? headlessW : viewportW, height: headless ? headlessH : viewportH },
223
+ viewport: headless ? { width: headlessW, height: headlessH } : null,
223
224
  firefox_user_prefs,
224
225
  config,
225
226
  data_dir: opts.profileDir,
@@ -25,6 +25,18 @@ function appendLog(target, event, payload = {}) {
25
25
  }
26
26
  const appendDomPickerLog = (event, payload = {}) => appendLog(domPickerLogPath, event, payload);
27
27
  const appendHighlightLog = (event, payload = {}) => appendLog(highlightLogPath, event, payload);
28
+ function legacySelectorActionDisabled(source, action) {
29
+ return {
30
+ success: false,
31
+ code: 'LEGACY_ACTION_DISABLED',
32
+ error: `[${source}] legacy selector action "${action}" is disabled; use mouse:* and keyboard:* protocol actions instead`,
33
+ data: {
34
+ source,
35
+ action,
36
+ replacements: ['mouse:move', 'mouse:click', 'mouse:wheel', 'keyboard:press', 'keyboard:type'],
37
+ },
38
+ };
39
+ }
28
40
  export class BrowserWsServer {
29
41
  options;
30
42
  wss;
@@ -607,10 +619,10 @@ export class BrowserWsServer {
607
619
  if (!op)
608
620
  throw new Error('operation_type required');
609
621
  if (op === 'click') {
610
- return this.handleNodeExecute(sessionId, { command_type: 'node_execute', node_type: 'click', parameters: parameters.target || parameters });
622
+ return legacySelectorActionDisabled('user_action.operation', 'click');
611
623
  }
612
624
  if (op === 'type') {
613
- return this.handleNodeExecute(sessionId, { command_type: 'node_execute', node_type: 'type', parameters: parameters.target || parameters });
625
+ return legacySelectorActionDisabled('user_action.operation', 'type');
614
626
  }
615
627
  if (op === 'scroll') {
616
628
  const session = this.options.sessionManager.getSession(sessionId);
@@ -902,25 +914,10 @@ export class BrowserWsServer {
902
914
  };
903
915
  }
904
916
  case 'click': {
905
- const selector = parameters.selector;
906
- if (!selector)
907
- throw new Error('Click node requires selector');
908
- await session.click(selector);
909
- return {
910
- success: true,
911
- data: { action: 'clicked', selector },
912
- };
917
+ return legacySelectorActionDisabled('node_execute', 'click');
913
918
  }
914
919
  case 'type': {
915
- const selector = parameters.selector;
916
- if (!selector)
917
- throw new Error('Type node requires selector');
918
- const text = parameters.text ?? parameters.value ?? '';
919
- await session.fill(selector, String(text));
920
- return {
921
- success: true,
922
- data: { action: 'typed', selector, text },
923
- };
920
+ return legacySelectorActionDisabled('node_execute', 'type');
924
921
  }
925
922
  case 'screenshot': {
926
923
  const filename = parameters.filename || `screenshot_${Date.now()}.png`;