@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
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
|
|
489
|
-
|
|
490
|
-
path.join(
|
|
491
|
-
|
|
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
|
|
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
|
|
133
|
+
headless,
|
|
95
134
|
profileDir: this.profileDir,
|
|
96
|
-
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 =
|
|
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
|
|
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:
|
|
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:
|
|
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.
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
-
|
|
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.
|
|
1140
|
-
|
|
1141
|
-
|
|
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.
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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.
|
|
1160
|
-
|
|
1161
|
-
|
|
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.
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
|
|
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:
|
|
179
|
-
|
|
180
|
-
const
|
|
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:
|
|
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
|
|
622
|
+
return legacySelectorActionDisabled('user_action.operation', 'click');
|
|
611
623
|
}
|
|
612
624
|
if (op === 'type') {
|
|
613
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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`;
|