@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.
- package/README.md +122 -53
- package/apps/desktop-console/dist/main/index.mjs +229 -14
- package/apps/desktop-console/dist/renderer/index.js +237 -8
- package/apps/desktop-console/entry/ui-cli.mjs +290 -21
- 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
|
@@ -42,6 +42,36 @@ interface RecordingState {
|
|
|
42
42
|
lastError: string | null;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
type NavigationWaitUntil = 'load' | 'domcontentloaded' | 'networkidle' | 'commit';
|
|
46
|
+
|
|
47
|
+
function resolveInputActionTimeoutMs(): number {
|
|
48
|
+
const raw = Number(process.env.CAMO_INPUT_ACTION_TIMEOUT_MS ?? process.env.CAMO_API_TIMEOUT_MS ?? 30000);
|
|
49
|
+
return Math.max(1000, Number.isFinite(raw) ? raw : 30000);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveNavigationWaitUntil(): NavigationWaitUntil {
|
|
53
|
+
const raw = String(process.env.CAMO_NAV_WAIT_UNTIL ?? 'commit').trim().toLowerCase();
|
|
54
|
+
if (raw === 'load') return 'load';
|
|
55
|
+
if (raw === 'domcontentloaded' || raw === 'dom') return 'domcontentloaded';
|
|
56
|
+
if (raw === 'networkidle') return 'networkidle';
|
|
57
|
+
return 'commit';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveInputActionMaxAttempts(): number {
|
|
61
|
+
const raw = Number(process.env.CAMO_INPUT_ACTION_MAX_ATTEMPTS ?? 2);
|
|
62
|
+
return Math.max(1, Math.min(3, Number.isFinite(raw) ? Math.floor(raw) : 2));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveInputRecoveryDelayMs(): number {
|
|
66
|
+
const raw = Number(process.env.CAMO_INPUT_RECOVERY_DELAY_MS ?? 120);
|
|
67
|
+
return Math.max(0, Number.isFinite(raw) ? Math.floor(raw) : 120);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolveInputRecoveryBringToFrontTimeoutMs(): number {
|
|
71
|
+
const raw = Number(process.env.CAMO_INPUT_RECOVERY_BRING_TO_FRONT_TIMEOUT_MS ?? 800);
|
|
72
|
+
return Math.max(100, Number.isFinite(raw) ? Math.floor(raw) : 800);
|
|
73
|
+
}
|
|
74
|
+
|
|
45
75
|
export class BrowserSession {
|
|
46
76
|
private browser?: Browser;
|
|
47
77
|
private context?: BrowserContext;
|
|
@@ -56,6 +86,8 @@ export class BrowserSession {
|
|
|
56
86
|
private bridgedPages = new WeakSet<Page>();
|
|
57
87
|
private recorderBridgePages = new WeakSet<Page>();
|
|
58
88
|
private lastViewport: { width: number; height: number } | null = null;
|
|
89
|
+
private followWindowViewport = false;
|
|
90
|
+
private inputActionTail: Promise<void> = Promise.resolve();
|
|
59
91
|
private fingerprint: any = null;
|
|
60
92
|
private recordingStream: fs.WriteStream | null = null;
|
|
61
93
|
private recording: RecordingState = {
|
|
@@ -129,15 +161,26 @@ export class BrowserSession {
|
|
|
129
161
|
userAgent: fingerprint.userAgent?.substring(0, 50) + '...',
|
|
130
162
|
});
|
|
131
163
|
|
|
132
|
-
const
|
|
164
|
+
const fallbackViewport = { width: 1440, height: 1100 };
|
|
165
|
+
const explicitViewport = this.options.viewport
|
|
166
|
+
&& Number(this.options.viewport.width) > 0
|
|
167
|
+
&& Number(this.options.viewport.height) > 0
|
|
168
|
+
? {
|
|
169
|
+
width: Math.floor(Number(this.options.viewport.width)),
|
|
170
|
+
height: Math.floor(Number(this.options.viewport.height)),
|
|
171
|
+
}
|
|
172
|
+
: null;
|
|
173
|
+
const viewport = explicitViewport || fingerprint?.viewport || fallbackViewport;
|
|
174
|
+
const headless = !!this.options.headless;
|
|
175
|
+
this.followWindowViewport = !headless;
|
|
133
176
|
const deviceScaleFactor = this.resolveDeviceScaleFactor();
|
|
134
177
|
|
|
135
178
|
// 使用 EngineManager 启动上下文(Chromium 已移除,仅支持 Camoufox)
|
|
136
179
|
this.context = await launchEngineContext({
|
|
137
180
|
engine,
|
|
138
|
-
headless
|
|
181
|
+
headless,
|
|
139
182
|
profileDir: this.profileDir,
|
|
140
|
-
viewport
|
|
183
|
+
viewport,
|
|
141
184
|
userAgent: fingerprint?.userAgent,
|
|
142
185
|
locale: 'zh-CN',
|
|
143
186
|
timezoneId: fingerprint?.timezoneId || 'Asia/Shanghai',
|
|
@@ -148,7 +191,9 @@ export class BrowserSession {
|
|
|
148
191
|
|
|
149
192
|
// NOTE: deviceScaleFactor override was Chromium-only (CDP). Chromium removed.
|
|
150
193
|
|
|
151
|
-
this.lastViewport =
|
|
194
|
+
this.lastViewport = this.followWindowViewport
|
|
195
|
+
? null
|
|
196
|
+
: { width: viewport.width, height: viewport.height };
|
|
152
197
|
this.browser = this.context.browser();
|
|
153
198
|
this.browser.on('disconnected', () => this.notifyExit());
|
|
154
199
|
this.context.on('close', () => this.notifyExit());
|
|
@@ -158,6 +203,9 @@ export class BrowserSession {
|
|
|
158
203
|
|
|
159
204
|
this.setupPageHooks(this.page);
|
|
160
205
|
this.context.on('page', (p) => this.setupPageHooks(p));
|
|
206
|
+
if (this.followWindowViewport) {
|
|
207
|
+
await this.refreshViewportFromWindow(this.page).catch(() => {});
|
|
208
|
+
}
|
|
161
209
|
|
|
162
210
|
if (initialUrl) {
|
|
163
211
|
await this.goto(initialUrl);
|
|
@@ -863,6 +911,10 @@ export class BrowserSession {
|
|
|
863
911
|
}
|
|
864
912
|
|
|
865
913
|
private async ensurePageViewport(page: Page): Promise<void> {
|
|
914
|
+
if (this.followWindowViewport) {
|
|
915
|
+
await this.refreshViewportFromWindow(page).catch(() => {});
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
866
918
|
if (!this.lastViewport) return;
|
|
867
919
|
const current = page.viewportSize();
|
|
868
920
|
if (current && current.width === this.lastViewport.width && current.height === this.lastViewport.height) {
|
|
@@ -876,6 +928,31 @@ export class BrowserSession {
|
|
|
876
928
|
await this.syncDeviceScaleFactor(page, { ...this.lastViewport });
|
|
877
929
|
}
|
|
878
930
|
|
|
931
|
+
private async readWindowInnerSize(page: Page): Promise<{ width: number; height: number } | null> {
|
|
932
|
+
try {
|
|
933
|
+
const metrics = await page.evaluate(() => ({
|
|
934
|
+
width: Math.floor(Number(window.innerWidth || 0)),
|
|
935
|
+
height: Math.floor(Number(window.innerHeight || 0)),
|
|
936
|
+
}));
|
|
937
|
+
const width = Number(metrics?.width);
|
|
938
|
+
const height = Number(metrics?.height);
|
|
939
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
|
|
940
|
+
if (width < 300 || height < 200) return null;
|
|
941
|
+
return { width, height };
|
|
942
|
+
} catch {
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
private async refreshViewportFromWindow(page: Page): Promise<void> {
|
|
948
|
+
const inner = await this.readWindowInnerSize(page);
|
|
949
|
+
if (!inner) return;
|
|
950
|
+
this.lastViewport = {
|
|
951
|
+
width: Math.max(800, Math.floor(inner.width)),
|
|
952
|
+
height: Math.max(700, Math.floor(inner.height)),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
879
956
|
private ensureContext(): BrowserContext {
|
|
880
957
|
if (!this.context) {
|
|
881
958
|
throw new Error('browser context not ready');
|
|
@@ -887,6 +964,11 @@ export class BrowserSession {
|
|
|
887
964
|
const ctx = this.ensureContext();
|
|
888
965
|
const existing = this.getActivePage();
|
|
889
966
|
if (existing) {
|
|
967
|
+
try {
|
|
968
|
+
await this.ensurePageViewport(existing);
|
|
969
|
+
} catch {
|
|
970
|
+
/* ignore */
|
|
971
|
+
}
|
|
890
972
|
return existing;
|
|
891
973
|
}
|
|
892
974
|
this.page = await ctx.newPage();
|
|
@@ -913,9 +995,10 @@ export class BrowserSession {
|
|
|
913
995
|
|
|
914
996
|
async goBack(): Promise<{ ok: boolean; url: string }> {
|
|
915
997
|
const page = await this.ensurePrimaryPage();
|
|
998
|
+
const waitUntil = resolveNavigationWaitUntil();
|
|
916
999
|
try {
|
|
917
1000
|
const res = await page
|
|
918
|
-
.goBack({ waitUntil
|
|
1001
|
+
.goBack({ waitUntil })
|
|
919
1002
|
.catch((): null => null);
|
|
920
1003
|
await ensurePageRuntime(page, true).catch(() => {});
|
|
921
1004
|
this.lastKnownUrl = page.url();
|
|
@@ -1001,7 +1084,7 @@ export class BrowserSession {
|
|
|
1001
1084
|
/* ignore */
|
|
1002
1085
|
}
|
|
1003
1086
|
if (url) {
|
|
1004
|
-
await page.goto(url, { waitUntil:
|
|
1087
|
+
await page.goto(url, { waitUntil: resolveNavigationWaitUntil() });
|
|
1005
1088
|
await ensurePageRuntime(page);
|
|
1006
1089
|
this.lastKnownUrl = url;
|
|
1007
1090
|
}
|
|
@@ -1149,7 +1232,7 @@ export class BrowserSession {
|
|
|
1149
1232
|
|
|
1150
1233
|
async goto(url: string): Promise<void> {
|
|
1151
1234
|
const page = await this.ensurePrimaryPage();
|
|
1152
|
-
await page.goto(url, { waitUntil:
|
|
1235
|
+
await page.goto(url, { waitUntil: resolveNavigationWaitUntil() });
|
|
1153
1236
|
await ensurePageRuntime(page);
|
|
1154
1237
|
this.lastKnownUrl = url;
|
|
1155
1238
|
}
|
|
@@ -1159,19 +1242,8 @@ export class BrowserSession {
|
|
|
1159
1242
|
return page.screenshot({ fullPage });
|
|
1160
1243
|
}
|
|
1161
1244
|
|
|
1162
|
-
async click(selector: string): Promise<void> {
|
|
1163
|
-
const page = await this.ensurePrimaryPage();
|
|
1164
|
-
await page.click(selector, { timeout: 20000 });
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
async fill(selector: string, text: string): Promise<void> {
|
|
1168
|
-
const page = await this.ensurePrimaryPage();
|
|
1169
|
-
await page.fill(selector, text, { timeout: 20000 });
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
1245
|
private async ensureInputReady(page: Page): Promise<void> {
|
|
1173
1246
|
if (this.options.headless) return;
|
|
1174
|
-
if (os.platform() !== 'win32') return;
|
|
1175
1247
|
let needsFocus = false;
|
|
1176
1248
|
try {
|
|
1177
1249
|
const state = await page.evaluate(() => ({
|
|
@@ -1193,30 +1265,112 @@ export class BrowserSession {
|
|
|
1193
1265
|
}
|
|
1194
1266
|
}
|
|
1195
1267
|
|
|
1268
|
+
private async withInputActionTimeout<T>(label: string, run: () => Promise<T>): Promise<T> {
|
|
1269
|
+
const timeoutMs = resolveInputActionTimeoutMs();
|
|
1270
|
+
let timer: NodeJS.Timeout | null = null;
|
|
1271
|
+
try {
|
|
1272
|
+
return await Promise.race<T>([
|
|
1273
|
+
run(),
|
|
1274
|
+
new Promise<T>((_resolve, reject) => {
|
|
1275
|
+
timer = setTimeout(() => {
|
|
1276
|
+
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
1277
|
+
}, timeoutMs);
|
|
1278
|
+
}),
|
|
1279
|
+
]);
|
|
1280
|
+
} finally {
|
|
1281
|
+
if (timer) clearTimeout(timer);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
private async recoverInputPipeline(page: Page): Promise<Page> {
|
|
1286
|
+
const bringToFrontTimeoutMs = resolveInputRecoveryBringToFrontTimeoutMs();
|
|
1287
|
+
let bringToFrontTimer: NodeJS.Timeout | null = null;
|
|
1288
|
+
try {
|
|
1289
|
+
await Promise.race<void>([
|
|
1290
|
+
page.bringToFront(),
|
|
1291
|
+
new Promise<void>((_resolve, reject) => {
|
|
1292
|
+
bringToFrontTimer = setTimeout(() => {
|
|
1293
|
+
reject(new Error(`input recovery bringToFront timed out after ${bringToFrontTimeoutMs}ms`));
|
|
1294
|
+
}, bringToFrontTimeoutMs);
|
|
1295
|
+
}),
|
|
1296
|
+
]);
|
|
1297
|
+
} catch {
|
|
1298
|
+
// Best-effort recovery only.
|
|
1299
|
+
} finally {
|
|
1300
|
+
if (bringToFrontTimer) clearTimeout(bringToFrontTimer);
|
|
1301
|
+
}
|
|
1302
|
+
const delayMs = resolveInputRecoveryDelayMs();
|
|
1303
|
+
if (delayMs > 0) {
|
|
1304
|
+
try {
|
|
1305
|
+
await page.waitForTimeout(delayMs);
|
|
1306
|
+
} catch {
|
|
1307
|
+
// Best-effort recovery only.
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
return page;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
private async runInputAction<T>(page: Page, label: string, run: (activePage: Page) => Promise<T>): Promise<T> {
|
|
1314
|
+
const maxAttempts = resolveInputActionMaxAttempts();
|
|
1315
|
+
let lastError: unknown = null;
|
|
1316
|
+
let activePage = page;
|
|
1317
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
1318
|
+
try {
|
|
1319
|
+
return await this.withInputActionTimeout(`${label} (attempt ${attempt}/${maxAttempts})`, () => run(activePage));
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
lastError = error;
|
|
1322
|
+
if (attempt >= maxAttempts) break;
|
|
1323
|
+
activePage = await this.recoverInputPipeline(activePage);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
if (lastError instanceof Error) throw lastError;
|
|
1327
|
+
throw new Error(`${label} failed`);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
private async withInputActionLock<T>(run: () => Promise<T>): Promise<T> {
|
|
1331
|
+
const previous = this.inputActionTail;
|
|
1332
|
+
let release: (() => void) | null = null;
|
|
1333
|
+
this.inputActionTail = new Promise<void>((resolve) => {
|
|
1334
|
+
release = resolve;
|
|
1335
|
+
});
|
|
1336
|
+
await previous.catch(() => {});
|
|
1337
|
+
try {
|
|
1338
|
+
return await run();
|
|
1339
|
+
} finally {
|
|
1340
|
+
if (release) release();
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1196
1344
|
/**
|
|
1197
1345
|
* 基于屏幕坐标的系统级鼠标点击(Playwright 原生)
|
|
1198
1346
|
* @param opts 屏幕坐标及点击选项
|
|
1199
1347
|
*/
|
|
1200
1348
|
async mouseClick(opts: { x: number; y: number; button?: 'left' | 'right' | 'middle'; clicks?: number; delay?: number }): Promise<void> {
|
|
1201
1349
|
const page = await this.ensurePrimaryPage();
|
|
1202
|
-
await this.
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
await
|
|
1350
|
+
await this.withInputActionLock(async () => {
|
|
1351
|
+
await this.runInputAction(page, 'input:ready', (activePage) => this.ensureInputReady(activePage));
|
|
1352
|
+
const { x, y, button = 'left', clicks = 1, delay = 50 } = opts;
|
|
1353
|
+
|
|
1354
|
+
// Avoid page.mouse.click() composite hangs by using explicit down/up sequence.
|
|
1355
|
+
for (let i = 0; i < clicks; i++) {
|
|
1356
|
+
if (i > 0) {
|
|
1357
|
+
// 多次点击间隔 100-200ms
|
|
1358
|
+
await new Promise(r => setTimeout(r, 100 + Math.random() * 100));
|
|
1359
|
+
}
|
|
1360
|
+
await this.runInputAction(page, 'mouse:move(pre-click)', (activePage) => activePage.mouse.move(x, y, { steps: 1 }));
|
|
1361
|
+
await this.runInputAction(page, 'mouse:down', (activePage) => activePage.mouse.down({
|
|
1362
|
+
button,
|
|
1363
|
+
clickCount: 1,
|
|
1364
|
+
}));
|
|
1365
|
+
if (delay > 0) {
|
|
1366
|
+
await page.waitForTimeout(delay);
|
|
1367
|
+
}
|
|
1368
|
+
await this.runInputAction(page, 'mouse:up', (activePage) => activePage.mouse.up({
|
|
1369
|
+
button,
|
|
1370
|
+
clickCount: 1,
|
|
1371
|
+
}));
|
|
1213
1372
|
}
|
|
1214
|
-
|
|
1215
|
-
button,
|
|
1216
|
-
clickCount: 1,
|
|
1217
|
-
delay // 按键间隔
|
|
1218
|
-
});
|
|
1219
|
-
}
|
|
1373
|
+
});
|
|
1220
1374
|
}
|
|
1221
1375
|
|
|
1222
1376
|
/**
|
|
@@ -1225,10 +1379,11 @@ export class BrowserSession {
|
|
|
1225
1379
|
*/
|
|
1226
1380
|
async mouseMove(opts: { x: number; y: number; steps?: number }): Promise<void> {
|
|
1227
1381
|
const page = await this.ensurePrimaryPage();
|
|
1228
|
-
await this.
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1382
|
+
await this.withInputActionLock(async () => {
|
|
1383
|
+
await this.runInputAction(page, 'input:ready', (activePage) => this.ensureInputReady(activePage));
|
|
1384
|
+
const { x, y, steps = 3 } = opts;
|
|
1385
|
+
await this.runInputAction(page, 'mouse:move', (activePage) => activePage.mouse.move(x, y, { steps }));
|
|
1386
|
+
});
|
|
1232
1387
|
}
|
|
1233
1388
|
|
|
1234
1389
|
/**
|
|
@@ -1236,23 +1391,27 @@ export class BrowserSession {
|
|
|
1236
1391
|
*/
|
|
1237
1392
|
async keyboardType(opts: { text: string; delay?: number; submit?: boolean }): Promise<void> {
|
|
1238
1393
|
const page = await this.ensurePrimaryPage();
|
|
1239
|
-
await this.
|
|
1240
|
-
|
|
1394
|
+
await this.withInputActionLock(async () => {
|
|
1395
|
+
await this.runInputAction(page, 'input:ready', (activePage) => this.ensureInputReady(activePage));
|
|
1396
|
+
const { text, delay = 80, submit } = opts;
|
|
1241
1397
|
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1398
|
+
if (text && text.length > 0) {
|
|
1399
|
+
await this.runInputAction(page, 'keyboard:type', (activePage) => activePage.keyboard.type(text, { delay }));
|
|
1400
|
+
}
|
|
1245
1401
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1402
|
+
if (submit) {
|
|
1403
|
+
await this.runInputAction(page, 'keyboard:press', (activePage) => activePage.keyboard.press('Enter'));
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1249
1406
|
}
|
|
1250
1407
|
|
|
1251
1408
|
async keyboardPress(opts: { key: string; delay?: number }): Promise<void> {
|
|
1252
1409
|
const page = await this.ensurePrimaryPage();
|
|
1253
|
-
await this.
|
|
1254
|
-
|
|
1255
|
-
|
|
1410
|
+
await this.withInputActionLock(async () => {
|
|
1411
|
+
await this.runInputAction(page, 'input:ready', (activePage) => this.ensureInputReady(activePage));
|
|
1412
|
+
const { key, delay } = opts;
|
|
1413
|
+
await this.runInputAction(page, 'keyboard:press', (activePage) => activePage.keyboard.press(key, typeof delay === 'number' ? { delay } : undefined));
|
|
1414
|
+
});
|
|
1256
1415
|
}
|
|
1257
1416
|
|
|
1258
1417
|
/**
|
|
@@ -1261,9 +1420,11 @@ export class BrowserSession {
|
|
|
1261
1420
|
*/
|
|
1262
1421
|
async mouseWheel(opts: { deltaY: number; deltaX?: number }): Promise<void> {
|
|
1263
1422
|
const page = await this.ensurePrimaryPage();
|
|
1264
|
-
await this.
|
|
1265
|
-
|
|
1266
|
-
|
|
1423
|
+
await this.withInputActionLock(async () => {
|
|
1424
|
+
await this.runInputAction(page, 'input:ready', (activePage) => this.ensureInputReady(activePage));
|
|
1425
|
+
const { deltaX = 0, deltaY } = opts;
|
|
1426
|
+
await this.runInputAction(page, 'mouse:wheel', (activePage) => activePage.mouse.wheel(Number(deltaX) || 0, Number(deltaY) || 0));
|
|
1427
|
+
});
|
|
1267
1428
|
}
|
|
1268
1429
|
|
|
1269
1430
|
private async syncWindowBounds(
|
|
@@ -1384,6 +1545,16 @@ export class BrowserSession {
|
|
|
1384
1545
|
if (!width || !height) {
|
|
1385
1546
|
throw new Error('invalid_viewport_size');
|
|
1386
1547
|
}
|
|
1548
|
+
if (this.followWindowViewport && !this.options.headless) {
|
|
1549
|
+
await page.evaluate(({ w, h }) => {
|
|
1550
|
+
try { window.resizeTo(w, h); } catch {}
|
|
1551
|
+
}, { w: width, h: height });
|
|
1552
|
+
await page.waitForTimeout(150);
|
|
1553
|
+
await this.refreshViewportFromWindow(page).catch(() => {});
|
|
1554
|
+
const next = this.lastViewport || { width, height };
|
|
1555
|
+
await this.maybeCenterWindow(page, next).catch(() => {});
|
|
1556
|
+
return next;
|
|
1557
|
+
}
|
|
1387
1558
|
await page.setViewportSize({ width, height });
|
|
1388
1559
|
await this.syncWindowBounds(page, { width, height });
|
|
1389
1560
|
await this.syncDeviceScaleFactor(page, { width, height });
|
|
@@ -176,17 +176,13 @@ export async function launchEngineContext(opts: EngineLaunchOptions): Promise<Br
|
|
|
176
176
|
const workW = Number(dm?.workWidth || physicalW || 4096);
|
|
177
177
|
const workH = Number(dm?.workHeight || physicalH || 2190);
|
|
178
178
|
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
// Use targetW/targetH as viewport (even in headless) to match headful size
|
|
188
|
-
const viewportW = Math.max(1440, Math.floor(Number(targetW) || 4096));
|
|
189
|
-
const viewportH = Math.max(900, Math.floor(Number(targetH) || 2190));
|
|
179
|
+
// Base viewport target:
|
|
180
|
+
// - prefer explicit/fingerprint viewport if provided
|
|
181
|
+
// - otherwise use a deterministic default for stable first-run UX
|
|
182
|
+
const requestedW = Number(opts.viewport?.width || process.env.WEBAUTO_VIEWPORT_WIDTH || 1440);
|
|
183
|
+
const requestedH = Number(opts.viewport?.height || process.env.WEBAUTO_VIEWPORT_HEIGHT || 1100);
|
|
184
|
+
const viewportW = Math.max(900, Math.floor(Number(requestedW) || 1440));
|
|
185
|
+
const viewportH = Math.max(700, Math.floor(Number(requestedH) || 1100));
|
|
190
186
|
|
|
191
187
|
const envHeadlessW = Number(process.env.WEBAUTO_HEADLESS_WIDTH || 0);
|
|
192
188
|
const envHeadlessH = Number(process.env.WEBAUTO_HEADLESS_HEIGHT || 0);
|
|
@@ -198,9 +194,12 @@ export async function launchEngineContext(opts: EngineLaunchOptions): Promise<Br
|
|
|
198
194
|
|
|
199
195
|
const headless = Boolean(opts.headless);
|
|
200
196
|
|
|
201
|
-
// Final window size:
|
|
202
|
-
|
|
203
|
-
const
|
|
197
|
+
// Final headful window size:
|
|
198
|
+
// align window with viewport by default and keep it inside available work area.
|
|
199
|
+
const maxHeadfulW = workW > 0 ? Math.max(900, workW - 40) : 1920;
|
|
200
|
+
const maxHeadfulH = workH > 0 ? Math.max(700, workH - 80) : 1200;
|
|
201
|
+
const winW = headless ? headlessW : Math.min(viewportW, maxHeadfulW);
|
|
202
|
+
const winH = headless ? headlessH : Math.min(viewportH, maxHeadfulH);
|
|
204
203
|
|
|
205
204
|
// Ensure integer types for Camoufox
|
|
206
205
|
const screenW = Math.floor(physicalW) | 0;
|
|
@@ -247,7 +246,7 @@ export async function launchEngineContext(opts: EngineLaunchOptions): Promise<Br
|
|
|
247
246
|
headless,
|
|
248
247
|
os: targetOS,
|
|
249
248
|
window: [intWinW, intWinH],
|
|
250
|
-
viewport:
|
|
249
|
+
viewport: headless ? { width: headlessW, height: headlessH } : null,
|
|
251
250
|
firefox_user_prefs,
|
|
252
251
|
config,
|
|
253
252
|
data_dir: opts.profileDir,
|
|
@@ -40,6 +40,19 @@ function appendLog(target: string, event: string, payload: Record<string, any> =
|
|
|
40
40
|
const appendDomPickerLog = (event: string, payload: Record<string, any> = {}) => appendLog(domPickerLogPath, event, payload);
|
|
41
41
|
const appendHighlightLog = (event: string, payload: Record<string, any> = {}) => appendLog(highlightLogPath, event, payload);
|
|
42
42
|
|
|
43
|
+
function legacySelectorActionDisabled(source: string, action: 'click' | 'type') {
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
code: 'LEGACY_ACTION_DISABLED',
|
|
47
|
+
error: `[${source}] legacy selector action "${action}" is disabled; use mouse:* and keyboard:* protocol actions instead`,
|
|
48
|
+
data: {
|
|
49
|
+
source,
|
|
50
|
+
action,
|
|
51
|
+
replacements: ['mouse:move', 'mouse:click', 'mouse:wheel', 'keyboard:press', 'keyboard:type'],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
export class BrowserWsServer {
|
|
44
57
|
private wss?: WebSocketServer;
|
|
45
58
|
private matcher = new ContainerMatcher();
|
|
@@ -687,10 +700,10 @@ export class BrowserWsServer {
|
|
|
687
700
|
const op = parameters.operation_type;
|
|
688
701
|
if (!op) throw new Error('operation_type required');
|
|
689
702
|
if (op === 'click') {
|
|
690
|
-
return
|
|
703
|
+
return legacySelectorActionDisabled('user_action.operation', 'click');
|
|
691
704
|
}
|
|
692
705
|
if (op === 'type') {
|
|
693
|
-
return
|
|
706
|
+
return legacySelectorActionDisabled('user_action.operation', 'type');
|
|
694
707
|
}
|
|
695
708
|
if (op === 'scroll') {
|
|
696
709
|
const session = this.options.sessionManager.getSession(sessionId);
|
|
@@ -1000,23 +1013,10 @@ export class BrowserWsServer {
|
|
|
1000
1013
|
};
|
|
1001
1014
|
}
|
|
1002
1015
|
case 'click': {
|
|
1003
|
-
|
|
1004
|
-
if (!selector) throw new Error('Click node requires selector');
|
|
1005
|
-
await session.click(selector);
|
|
1006
|
-
return {
|
|
1007
|
-
success: true,
|
|
1008
|
-
data: { action: 'clicked', selector },
|
|
1009
|
-
};
|
|
1016
|
+
return legacySelectorActionDisabled('node_execute', 'click');
|
|
1010
1017
|
}
|
|
1011
1018
|
case 'type': {
|
|
1012
|
-
|
|
1013
|
-
if (!selector) throw new Error('Type node requires selector');
|
|
1014
|
-
const text = parameters.text ?? parameters.value ?? '';
|
|
1015
|
-
await session.fill(selector, String(text));
|
|
1016
|
-
return {
|
|
1017
|
-
success: true,
|
|
1018
|
-
data: { action: 'typed', selector, text },
|
|
1019
|
-
};
|
|
1019
|
+
return legacySelectorActionDisabled('node_execute', 'type');
|
|
1020
1020
|
}
|
|
1021
1021
|
case 'screenshot': {
|
|
1022
1022
|
const filename = parameters.filename || `screenshot_${Date.now()}.png`;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { callAPI } from '../../../utils/browser-service.mjs';
|
|
2
|
+
import { assertNoForbiddenJsAction } from '../../../utils/js-policy.mjs';
|
|
2
3
|
|
|
3
4
|
export function withOperationHighlight(script, color = '#ff7a00') {
|
|
4
5
|
return `(() => {
|
|
@@ -37,8 +38,17 @@ export function withOperationHighlight(script, color = '#ff7a00') {
|
|
|
37
38
|
})()`;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
export async function runEvaluateScript({
|
|
41
|
-
|
|
41
|
+
export async function runEvaluateScript({
|
|
42
|
+
profileId,
|
|
43
|
+
script,
|
|
44
|
+
highlight = true,
|
|
45
|
+
allowUnsafeJs = false,
|
|
46
|
+
}) {
|
|
47
|
+
const sourceScript = String(script || '');
|
|
48
|
+
if (!allowUnsafeJs) {
|
|
49
|
+
assertNoForbiddenJsAction(sourceScript, 'xhs provider evaluate');
|
|
50
|
+
}
|
|
51
|
+
const wrappedScript = highlight && allowUnsafeJs ? withOperationHighlight(sourceScript) : sourceScript;
|
|
42
52
|
return callAPI('evaluate', { profileId, script: wrappedScript });
|
|
43
53
|
}
|
|
44
54
|
|
|
@@ -39,6 +39,7 @@ export function resolveXhsOutputContext({
|
|
|
39
39
|
noteId: note,
|
|
40
40
|
keywordDir,
|
|
41
41
|
noteDir,
|
|
42
|
+
linksPath: path.join(keywordDir, 'links.collected.jsonl'),
|
|
42
43
|
commentsPath: path.join(noteDir, 'comments.jsonl'),
|
|
43
44
|
likeStatePath: path.join(keywordDir, '.like-state.jsonl'),
|
|
44
45
|
likeEvidenceDir: path.join(keywordDir, 'like-evidence', note),
|
|
@@ -93,6 +94,38 @@ function commentDedupKey(row) {
|
|
|
93
94
|
return `${String(row?.userId || '')}:${String(row?.content || '')}`;
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
function readXsecToken(url) {
|
|
98
|
+
const href = String(url || '').trim();
|
|
99
|
+
if (!href) return '';
|
|
100
|
+
try {
|
|
101
|
+
const parsed = new URL(href);
|
|
102
|
+
return String(parsed.searchParams.get('xsec_token') || '').trim();
|
|
103
|
+
} catch {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeLinkRow(row) {
|
|
109
|
+
const noteId = String(row?.noteId || '').trim();
|
|
110
|
+
const noteUrl = String(row?.noteUrl || row?.url || '').trim();
|
|
111
|
+
const listUrl = String(row?.listUrl || '').trim();
|
|
112
|
+
const xsecToken = String(row?.xsecToken || readXsecToken(noteUrl)).trim();
|
|
113
|
+
return {
|
|
114
|
+
noteId,
|
|
115
|
+
noteUrl,
|
|
116
|
+
listUrl,
|
|
117
|
+
xsecToken,
|
|
118
|
+
ts: new Date().toISOString(),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function linkDedupKey(row) {
|
|
123
|
+
const noteId = String(row?.noteId || '').trim();
|
|
124
|
+
if (noteId) return `note:${noteId}`;
|
|
125
|
+
const noteUrl = String(row?.noteUrl || '').trim();
|
|
126
|
+
return noteUrl ? `url:${noteUrl}` : '';
|
|
127
|
+
}
|
|
128
|
+
|
|
96
129
|
export async function mergeCommentsJsonl({ filePath, noteId, comments = [] }) {
|
|
97
130
|
const existing = await readJsonlRows(filePath);
|
|
98
131
|
const seen = new Set(
|
|
@@ -122,6 +155,30 @@ export async function mergeCommentsJsonl({ filePath, noteId, comments = [] }) {
|
|
|
122
155
|
};
|
|
123
156
|
}
|
|
124
157
|
|
|
158
|
+
export async function mergeLinksJsonl({ filePath, links = [] }) {
|
|
159
|
+
const existing = await readJsonlRows(filePath);
|
|
160
|
+
const seen = new Set(existing.map((row) => linkDedupKey(row)).filter(Boolean));
|
|
161
|
+
|
|
162
|
+
const added = [];
|
|
163
|
+
for (const row of links) {
|
|
164
|
+
const normalized = normalizeLinkRow(row);
|
|
165
|
+
if (!normalized.noteUrl || !normalized.xsecToken) continue;
|
|
166
|
+
const key = linkDedupKey(normalized);
|
|
167
|
+
if (!key || seen.has(key)) continue;
|
|
168
|
+
seen.add(key);
|
|
169
|
+
added.push(normalized);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await appendJsonlRows(filePath, added);
|
|
173
|
+
return {
|
|
174
|
+
filePath,
|
|
175
|
+
added: added.length,
|
|
176
|
+
existing: existing.length,
|
|
177
|
+
total: existing.length + added.length,
|
|
178
|
+
rowsAdded: added,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
125
182
|
export function makeLikeSignature({ noteId, userId = '', userName = '', text = '' }) {
|
|
126
183
|
const normalizedText = String(text || '').replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
127
184
|
return [
|