@reconcrap/boss-recommend-mcp 2.0.47 → 2.0.49

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 (55) hide show
  1. package/bin/boss-recommend-mcp.js +4 -4
  2. package/config/screening-config.example.json +27 -27
  3. package/package.json +1 -1
  4. package/scripts/postinstall.cjs +44 -44
  5. package/skills/boss-chat/README.md +39 -39
  6. package/skills/boss-chat/SKILL.md +93 -93
  7. package/skills/boss-recommend-pipeline/README.md +12 -12
  8. package/skills/boss-recommend-pipeline/SKILL.md +180 -180
  9. package/skills/boss-recruit-pipeline/README.md +17 -17
  10. package/skills/boss-recruit-pipeline/SKILL.md +58 -58
  11. package/src/chat-mcp.js +1780 -1780
  12. package/src/chat-runtime-config.js +749 -749
  13. package/src/cli.js +3054 -3054
  14. package/src/core/boss-cards/index.js +199 -199
  15. package/src/core/browser/index.js +1586 -1453
  16. package/src/core/capture/index.js +1201 -1201
  17. package/src/core/cv-acquisition/index.js +238 -238
  18. package/src/core/cv-capture-target/index.js +299 -299
  19. package/src/core/greet-quota/index.js +54 -54
  20. package/src/core/infinite-list/index.js +1326 -1326
  21. package/src/core/reporting/legacy-csv.js +341 -341
  22. package/src/core/run/timing.js +33 -33
  23. package/src/core/self-heal/index.js +973 -973
  24. package/src/core/self-heal/viewport.js +564 -564
  25. package/src/domains/chat/cards.js +137 -137
  26. package/src/domains/chat/constants.js +221 -221
  27. package/src/domains/chat/detail.js +1668 -1668
  28. package/src/domains/chat/index.js +7 -7
  29. package/src/domains/chat/jobs.js +592 -592
  30. package/src/domains/chat/page-guard.js +98 -98
  31. package/src/domains/chat/roots.js +56 -56
  32. package/src/domains/chat/run-service.js +1977 -1977
  33. package/src/domains/recommend/actions.js +457 -457
  34. package/src/domains/recommend/cards.js +243 -243
  35. package/src/domains/recommend/constants.js +165 -165
  36. package/src/domains/recommend/detail.js +1 -1
  37. package/src/domains/recommend/filters.js +610 -610
  38. package/src/domains/recommend/index.js +10 -10
  39. package/src/domains/recommend/jobs.js +378 -316
  40. package/src/domains/recommend/refresh.js +491 -472
  41. package/src/domains/recommend/roots.js +80 -80
  42. package/src/domains/recommend/run-service.js +50 -29
  43. package/src/domains/recommend/scopes.js +246 -246
  44. package/src/domains/recruit/actions.js +277 -277
  45. package/src/domains/recruit/cards.js +74 -74
  46. package/src/domains/recruit/constants.js +167 -167
  47. package/src/domains/recruit/detail.js +461 -461
  48. package/src/domains/recruit/index.js +9 -9
  49. package/src/domains/recruit/instruction-parser.js +451 -451
  50. package/src/domains/recruit/refresh.js +44 -44
  51. package/src/domains/recruit/roots.js +68 -68
  52. package/src/domains/recruit/run-service.js +1207 -1207
  53. package/src/domains/recruit/search.js +1202 -1202
  54. package/src/recommend-mcp.js +22 -22
  55. package/src/recruit-mcp.js +1338 -1338
@@ -1,1453 +1,1586 @@
1
- import { spawn } from "node:child_process";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import CDP from "chrome-remote-interface";
6
-
7
- export const DEFAULT_CHROME_HOST = "127.0.0.1";
8
- export const DEFAULT_CHROME_PORT = 9222;
9
- export const BOSS_LOGIN_URL = "https://www.zhipin.com/web/user/?ka=bticket";
10
- export const LID_CLOSED_SAFE_CHROME_ARGS = [
11
- "--disable-backgrounding-occluded-windows",
12
- "--disable-background-timer-throttling",
13
- "--disable-renderer-backgrounding",
14
- "--disable-features=CalculateNativeWinOcclusion"
15
- ];
16
-
17
- export const ALLOWED_CDP_DOMAINS = new Set([
18
- "Accessibility",
19
- "Browser",
20
- "DOM",
21
- "Input",
22
- "Network",
23
- "Page",
24
- "Target"
25
- ]);
26
-
27
- export const FORBIDDEN_CDP_DOMAINS = new Set(["Runtime"]);
28
-
29
- const BOSS_LOGIN_URL_PATTERN = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com|login\.zhipin\.com)/i;
30
- const BOSS_LOGIN_TEXT_PATTERN = /扫码登录|验证码登录|密码登录|登录后|请登录|登录BOSS直聘|Boss登录|BOSS登录/i;
31
- const CHROME_DEBUG_UNAVAILABLE_PATTERN = /ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|connect|socket hang up/i;
32
- const BOSS_LOGIN_DOM_SELECTORS = [
33
- ".login-box",
34
- ".login-form",
35
- ".login-dialog",
36
- ".sign-form",
37
- ".qrcode-box",
38
- ".user-login",
39
- "input[name='phone']",
40
- "input[placeholder*='手机号']",
41
- "input[placeholder*='验证码']"
42
- ];
43
- const HUMAN_INTERACTION_CONFIG = new WeakMap();
44
- const DEFAULT_HUMAN_BEHAVIOR_PROFILE = "paced_with_rests";
45
- export const DETERMINISTIC_CLICK_OPTIONS = Object.freeze({
46
- humanRestEnabled: false
47
- });
48
- const HUMAN_BEHAVIOR_PROFILES = Object.freeze({
49
- baseline: Object.freeze({
50
- enabled: false,
51
- clickMovement: false,
52
- textEntry: false,
53
- listScrollJitter: false,
54
- shortRest: false,
55
- batchRest: false,
56
- actionCooldown: false
57
- }),
58
- paced: Object.freeze({
59
- enabled: true,
60
- clickMovement: true,
61
- textEntry: true,
62
- listScrollJitter: true,
63
- shortRest: false,
64
- batchRest: false,
65
- actionCooldown: true
66
- }),
67
- paced_with_rests: Object.freeze({
68
- enabled: true,
69
- clickMovement: true,
70
- textEntry: true,
71
- listScrollJitter: true,
72
- shortRest: true,
73
- batchRest: true,
74
- actionCooldown: true
75
- })
76
- });
77
- const HUMAN_BEHAVIOR_PROFILE_ALIASES = Object.freeze({
78
- off: "baseline",
79
- disabled: "baseline",
80
- deterministic: "baseline",
81
- safe: "paced",
82
- safe_pacing: "paced",
83
- paced_with_rest: "paced_with_rests",
84
- rests: "paced_with_rests",
85
- rest: "paced_with_rests"
86
- });
87
-
88
- function clampNumber(value, min, max) {
89
- const number = Number(value);
90
- if (!Number.isFinite(number)) return min;
91
- return Math.min(max, Math.max(min, number));
92
- }
93
-
94
- function randomBetween(random, min, max) {
95
- const lower = Number(min) || 0;
96
- const upper = Number(max) || lower;
97
- if (upper <= lower) return lower;
98
- return lower + random() * (upper - lower);
99
- }
100
-
101
- function randomIntegerBetween(random, min, max) {
102
- return Math.floor(randomBetween(random, min, max + 1));
103
- }
104
-
105
- function normalizePoint(point) {
106
- const x = Number(point?.x);
107
- const y = Number(point?.y);
108
- if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
109
- return { x, y };
110
- }
111
-
112
- function normalizeRandom(random) {
113
- return typeof random === "function" ? random : Math.random;
114
- }
115
-
116
- function getHumanInteractionConfig(client) {
117
- return HUMAN_INTERACTION_CONFIG.get(client) || null;
118
- }
119
-
120
- function normalizeBooleanOption(raw, fallback = null) {
121
- if (typeof raw === "boolean") return raw;
122
- if (typeof raw === "number" && Number.isFinite(raw)) return raw !== 0;
123
- const normalized = String(raw ?? "").trim().toLowerCase();
124
- if (!normalized) return fallback;
125
- if (["true", "1", "yes", "y", "on", "enabled"].includes(normalized)) return true;
126
- if (["false", "0", "no", "n", "off", "disabled"].includes(normalized)) return false;
127
- return fallback;
128
- }
129
-
130
- function readFirstOption(source, keys = []) {
131
- if (!source || typeof source !== "object") return undefined;
132
- for (const key of keys) {
133
- if (Object.prototype.hasOwnProperty.call(source, key)) return source[key];
134
- }
135
- return undefined;
136
- }
137
-
138
- function normalizeFeatureBoolean(raw, fallback) {
139
- if (raw && typeof raw === "object" && !Array.isArray(raw)) {
140
- return normalizeBooleanOption(readFirstOption(raw, ["enabled", "enable"]), fallback);
141
- }
142
- return normalizeBooleanOption(raw, fallback);
143
- }
144
-
145
- export function normalizeHumanBehaviorProfile(raw, fallback = "baseline") {
146
- const normalized = String(raw || "").trim().toLowerCase().replace(/[\s-]+/g, "_");
147
- const profile = HUMAN_BEHAVIOR_PROFILE_ALIASES[normalized] || normalized;
148
- return Object.prototype.hasOwnProperty.call(HUMAN_BEHAVIOR_PROFILES, profile)
149
- ? profile
150
- : fallback;
151
- }
152
-
153
- export function normalizeHumanBehaviorOptions(raw = null, {
154
- legacyEnabled = false,
155
- safePacing = null,
156
- batchRestEnabled = null
157
- } = {}) {
158
- const safePacingFlag = normalizeBooleanOption(safePacing, null);
159
- const batchRestFlag = normalizeBooleanOption(batchRestEnabled, null);
160
- let source = "default";
161
- let rawObject = {};
162
- if (typeof raw === "boolean") {
163
- rawObject = { enabled: raw };
164
- source = "boolean";
165
- } else if (typeof raw === "string") {
166
- rawObject = { profile: raw };
167
- source = "profile";
168
- } else if (raw && typeof raw === "object" && !Array.isArray(raw)) {
169
- rawObject = raw;
170
- source = "object";
171
- }
172
-
173
- const explicitProfile = readFirstOption(rawObject, ["profile", "mode", "behaviorProfile", "behavior_profile"]);
174
- const enabledRaw = readFirstOption(rawObject, ["enabled", "enable", "human_behavior_enabled"]);
175
- const explicitEnabled = normalizeBooleanOption(enabledRaw, null);
176
- const inferredProfile = (raw === true || explicitEnabled === true) && legacyEnabled !== true && batchRestFlag !== true
177
- ? "paced"
178
- : legacyEnabled === true || batchRestFlag === true
179
- ? "paced_with_rests"
180
- : safePacingFlag === true
181
- ? "paced"
182
- : DEFAULT_HUMAN_BEHAVIOR_PROFILE;
183
- const profile = normalizeHumanBehaviorProfile(explicitProfile, inferredProfile);
184
- const profileDefaults = {
185
- ...HUMAN_BEHAVIOR_PROFILES[profile]
186
- };
187
- if (legacyEnabled === true && !explicitProfile) {
188
- Object.assign(profileDefaults, HUMAN_BEHAVIOR_PROFILES.paced_with_rests);
189
- } else if (safePacingFlag === true && !explicitProfile) {
190
- Object.assign(profileDefaults, HUMAN_BEHAVIOR_PROFILES.paced);
191
- }
192
- if (batchRestFlag === true && !explicitProfile) {
193
- Object.assign(profileDefaults, HUMAN_BEHAVIOR_PROFILES.paced_with_rests);
194
- }
195
-
196
- const hasExplicitEnabled = enabledRaw !== undefined;
197
- if (hasExplicitEnabled) {
198
- profileDefaults.enabled = normalizeBooleanOption(enabledRaw, profileDefaults.enabled);
199
- }
200
- if (!hasExplicitEnabled && (safePacingFlag === false || batchRestFlag === false) && !explicitProfile && legacyEnabled !== true) {
201
- profileDefaults.enabled = false;
202
- }
203
- if (!hasExplicitEnabled && (safePacingFlag === true || batchRestFlag === true || legacyEnabled === true)) {
204
- profileDefaults.enabled = true;
205
- }
206
-
207
- const enabled = profileDefaults.enabled === true;
208
- const clickMovement = normalizeFeatureBoolean(
209
- readFirstOption(rawObject, ["clickMovement", "click_movement", "click_movement_enabled"]),
210
- profileDefaults.clickMovement
211
- );
212
- const textEntry = normalizeFeatureBoolean(
213
- readFirstOption(rawObject, ["textEntry", "text_entry", "text_entry_enabled"]),
214
- profileDefaults.textEntry
215
- );
216
- const listScrollJitter = normalizeFeatureBoolean(
217
- readFirstOption(rawObject, ["listScrollJitter", "list_scroll_jitter", "scrollJitter", "scroll_jitter"]),
218
- profileDefaults.listScrollJitter
219
- );
220
- const actionCooldown = normalizeFeatureBoolean(
221
- readFirstOption(rawObject, ["actionCooldown", "action_cooldown", "readPause", "read_pause"]),
222
- profileDefaults.actionCooldown
223
- );
224
- let shortRest = normalizeFeatureBoolean(
225
- readFirstOption(rawObject, ["shortRest", "short_rest", "randomRest", "random_rest"]),
226
- profileDefaults.shortRest
227
- );
228
- let batchRest = normalizeFeatureBoolean(
229
- readFirstOption(rawObject, ["batchRest", "batch_rest", "batchRestEnabled", "batch_rest_enabled"]),
230
- profileDefaults.batchRest
231
- );
232
- if (batchRestFlag !== null) {
233
- batchRest = batchRestFlag;
234
- if (batchRestFlag === true && readFirstOption(rawObject, ["shortRest", "short_rest", "randomRest", "random_rest"]) === undefined) {
235
- shortRest = true;
236
- }
237
- }
238
-
239
- return {
240
- enabled,
241
- profile,
242
- source,
243
- clickMovement: enabled && clickMovement === true,
244
- textEntry: enabled && textEntry === true,
245
- listScrollJitter: enabled && listScrollJitter === true,
246
- shortRest: enabled && shortRest === true,
247
- batchRest: enabled && batchRest === true,
248
- actionCooldown: enabled && actionCooldown === true,
249
- restEnabled: enabled && (shortRest === true || batchRest === true)
250
- };
251
- }
252
-
253
- function nowIso() {
254
- return new Date().toISOString();
255
- }
256
-
257
- function normalizeTargetMatcher({ targetUrlIncludes, targetPredicate } = {}) {
258
- if (typeof targetPredicate === "function") return targetPredicate;
259
- if (targetUrlIncludes) {
260
- return (target) => String(target?.url || "").includes(targetUrlIncludes);
261
- }
262
- return (target) => target?.type === "page";
263
- }
264
-
265
- function isForbiddenMethod(methodName) {
266
- const [domain] = String(methodName || "").split(".");
267
- return FORBIDDEN_CDP_DOMAINS.has(domain);
268
- }
269
-
270
- function methodName(domain, method) {
271
- return `${String(domain)}.${String(method)}`;
272
- }
273
-
274
- function recordMethod(methodLog, method) {
275
- if (Array.isArray(methodLog)) {
276
- methodLog.push({ method, at: nowIso() });
277
- }
278
- }
279
-
280
- export function assertNoForbiddenCdpCalls(methodLog = []) {
281
- const forbidden = methodLog.filter((entry) => isForbiddenMethod(entry?.method));
282
- if (forbidden.length > 0) {
283
- const methods = forbidden.map((entry) => entry.method).join(", ");
284
- throw new Error(`Forbidden CDP methods were used: ${methods}`);
285
- }
286
- }
287
-
288
- export function humanDelay(baseMs, varianceMs, {
289
- minMs = 100,
290
- maxMs = 60000,
291
- random = Math.random
292
- } = {}) {
293
- const nextRandom = normalizeRandom(random);
294
- const base = Math.max(0, Number(baseMs) || 0);
295
- const variance = Math.max(0, Number(varianceMs) || 0);
296
- const lower = Math.max(0, Number(minMs) || 0);
297
- const upper = Math.max(lower, Number(maxMs) || lower);
298
- if (variance <= 0) return Math.round(clampNumber(base, lower, upper));
299
- const u1 = Math.max(Number.EPSILON, Math.min(1 - Number.EPSILON, nextRandom()));
300
- const u2 = Math.max(Number.EPSILON, Math.min(1 - Number.EPSILON, nextRandom()));
301
- const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
302
- return Math.round(clampNumber(base + z * variance, lower, upper));
303
- }
304
-
305
- export function generateBezierPath(start, end, {
306
- steps = 18,
307
- random = Math.random,
308
- controlJitterX = 100,
309
- controlJitterY = 60
310
- } = {}) {
311
- const startPoint = normalizePoint(start);
312
- const endPoint = normalizePoint(end);
313
- if (!startPoint || !endPoint) {
314
- throw new Error("generateBezierPath requires finite start and end points");
315
- }
316
- const nextRandom = normalizeRandom(random);
317
- const safeSteps = Math.max(1, Math.floor(Number(steps) || 18));
318
- const midX = (startPoint.x + endPoint.x) / 2 + (nextRandom() - 0.5) * Math.max(0, Number(controlJitterX) || 0);
319
- const midY = (startPoint.y + endPoint.y) / 2 + (nextRandom() - 0.5) * Math.max(0, Number(controlJitterY) || 0);
320
- const path = [];
321
- for (let index = 0; index <= safeSteps; index += 1) {
322
- const t = index / safeSteps;
323
- const inverse = 1 - t;
324
- path.push({
325
- x: inverse * inverse * startPoint.x + 2 * inverse * t * midX + t * t * endPoint.x,
326
- y: inverse * inverse * startPoint.y + 2 * inverse * t * midY + t * t * endPoint.y
327
- });
328
- }
329
- return path;
330
- }
331
-
332
- export function configureHumanInteraction(client, {
333
- enabled = false,
334
- clickMovementEnabled = null,
335
- textEntryEnabled = null,
336
- safeClickPointEnabled = null,
337
- actionCooldownEnabled = null,
338
- random = Math.random,
339
- sleepFn = null,
340
- moveSteps = 18,
341
- moveJitterPx = 3,
342
- hoverJitterPx = 5,
343
- moveDelayMinMs = 5,
344
- moveDelayMaxMs = 23,
345
- hoverDelayMinMs = 10,
346
- hoverDelayMaxMs = 30,
347
- prePressBaseMs = 260,
348
- prePressVarianceMs = 80,
349
- holdVarianceMs = 30,
350
- safeClickMinWidth = 44,
351
- safeClickMinHeight = 28,
352
- safeClickInsetRatio = 0.22,
353
- safeClickMinInsetPx = 4,
354
- safeClickMaxInsetPx = 18,
355
- textChunkMinLength = 1,
356
- textChunkMaxLength = 5,
357
- textChunkDelayBaseMs = 55,
358
- textChunkDelayVarianceMs = 30
359
- } = {}) {
360
- const previous = getHumanInteractionConfig(client);
361
- const normalizedEnabled = enabled === true;
362
- HUMAN_INTERACTION_CONFIG.set(client, {
363
- enabled: normalizedEnabled,
364
- clickMovementEnabled: normalizedEnabled && clickMovementEnabled !== false,
365
- textEntryEnabled: normalizedEnabled && textEntryEnabled !== false,
366
- safeClickPointEnabled: normalizedEnabled && safeClickPointEnabled !== false,
367
- actionCooldownEnabled: normalizedEnabled && actionCooldownEnabled !== false,
368
- random: normalizeRandom(random),
369
- sleepFn: typeof sleepFn === "function" ? sleepFn : sleep,
370
- moveSteps: Math.max(1, Math.floor(Number(moveSteps) || 18)),
371
- moveJitterPx: Math.max(0, Number(moveJitterPx) || 0),
372
- hoverJitterPx: Math.max(0, Number(hoverJitterPx) || 0),
373
- moveDelayMinMs: Math.max(0, Number(moveDelayMinMs) || 0),
374
- moveDelayMaxMs: Math.max(0, Number(moveDelayMaxMs) || 0),
375
- hoverDelayMinMs: Math.max(0, Number(hoverDelayMinMs) || 0),
376
- hoverDelayMaxMs: Math.max(0, Number(hoverDelayMaxMs) || 0),
377
- prePressBaseMs: Math.max(0, Number(prePressBaseMs) || 0),
378
- prePressVarianceMs: Math.max(0, Number(prePressVarianceMs) || 0),
379
- holdVarianceMs: Math.max(0, Number(holdVarianceMs) || 0),
380
- safeClickMinWidth: Math.max(1, Number(safeClickMinWidth) || 44),
381
- safeClickMinHeight: Math.max(1, Number(safeClickMinHeight) || 28),
382
- safeClickInsetRatio: clampNumber(safeClickInsetRatio, 0.05, 0.45),
383
- safeClickMinInsetPx: Math.max(0, Number(safeClickMinInsetPx) || 0),
384
- safeClickMaxInsetPx: Math.max(0, Number(safeClickMaxInsetPx) || 0),
385
- textChunkMinLength: Math.max(1, Math.floor(Number(textChunkMinLength) || 1)),
386
- textChunkMaxLength: Math.max(1, Math.floor(Number(textChunkMaxLength) || 5)),
387
- textChunkDelayBaseMs: Math.max(0, Number(textChunkDelayBaseMs) || 0),
388
- textChunkDelayVarianceMs: Math.max(0, Number(textChunkDelayVarianceMs) || 0),
389
- lastMousePoint: previous?.lastMousePoint || null
390
- });
391
- return () => {
392
- if (previous) {
393
- HUMAN_INTERACTION_CONFIG.set(client, previous);
394
- } else {
395
- HUMAN_INTERACTION_CONFIG.delete(client);
396
- }
397
- };
398
- }
399
-
400
- export function createHumanRestController({
401
- enabled = false,
402
- shortRestEnabled = true,
403
- batchRestEnabled = true,
404
- random = Math.random,
405
- shortRestProbability = 0.08,
406
- shortRestMinMs = 3000,
407
- shortRestMaxMs = 7000,
408
- batchThresholdBase = 25,
409
- batchThresholdJitter = 8,
410
- batchRestMinMs = 15000,
411
- batchRestMaxMs = 30000
412
- } = {}) {
413
- const nextRandom = normalizeRandom(random);
414
- const state = {
415
- enabled: enabled === true,
416
- short_rest_enabled: enabled === true && shortRestEnabled !== false,
417
- batch_rest_enabled: enabled === true && batchRestEnabled !== false,
418
- rest_counter: 0,
419
- rest_threshold: Math.max(1, Math.floor(Number(batchThresholdBase) || 25) + Math.floor(nextRandom() * Math.max(1, Number(batchThresholdJitter) || 1))),
420
- rest_count: 0,
421
- total_rest_ms: 0
422
- };
423
-
424
- function resetThreshold() {
425
- state.rest_threshold = Math.max(1, Math.floor(Number(batchThresholdBase) || 25) + Math.floor(nextRandom() * Math.max(1, Number(batchThresholdJitter) || 1)));
426
- }
427
-
428
- async function takeBreakIfNeeded({ sleepFn = sleep } = {}) {
429
- if (!state.enabled) {
430
- return {
431
- enabled: false,
432
- rested: false,
433
- rest_counter: state.rest_counter,
434
- rest_threshold: state.rest_threshold,
435
- events: []
436
- };
437
- }
438
- const sleeper = typeof sleepFn === "function" ? sleepFn : sleep;
439
- state.rest_counter += 1;
440
- const events = [];
441
- if (state.short_rest_enabled && nextRandom() < Math.max(0, Number(shortRestProbability) || 0)) {
442
- const pauseMs = Math.round(randomBetween(nextRandom, shortRestMinMs, shortRestMaxMs));
443
- await sleeper(pauseMs);
444
- events.push({ kind: "random_rest", pause_ms: pauseMs });
445
- }
446
- if (state.batch_rest_enabled && state.rest_counter >= state.rest_threshold) {
447
- const pauseMs = Math.round(randomBetween(nextRandom, batchRestMinMs, batchRestMaxMs));
448
- await sleeper(pauseMs);
449
- events.push({
450
- kind: "batch_rest",
451
- pause_ms: pauseMs,
452
- processed_since_last_batch_rest: state.rest_counter
453
- });
454
- state.rest_counter = 0;
455
- resetThreshold();
456
- }
457
- const pauseMs = events.reduce((sum, event) => sum + event.pause_ms, 0);
458
- if (pauseMs > 0) {
459
- state.rest_count += events.length;
460
- state.total_rest_ms += pauseMs;
461
- }
462
- return {
463
- enabled: true,
464
- rested: events.length > 0,
465
- pause_ms: pauseMs,
466
- rest_counter: state.rest_counter,
467
- rest_threshold: state.rest_threshold,
468
- rest_count: state.rest_count,
469
- total_rest_ms: state.total_rest_ms,
470
- events
471
- };
472
- }
473
-
474
- return {
475
- takeBreakIfNeeded,
476
- getState() {
477
- return { ...state };
478
- }
479
- };
480
- }
481
-
482
- export function isBossLoginUrl(url) {
483
- return BOSS_LOGIN_URL_PATTERN.test(String(url || ""));
484
- }
485
-
486
- export function createBossLoginRequiredError({
487
- domain = "boss",
488
- currentUrl = "",
489
- targetUrl = "",
490
- loginUrl = BOSS_LOGIN_URL,
491
- loginDetection = null,
492
- chrome = null
493
- } = {}) {
494
- const error = new Error(`Boss login is required before starting the ${domain} run.`);
495
- error.code = "BOSS_LOGIN_REQUIRED";
496
- error.requires_login = true;
497
- error.current_url = currentUrl || null;
498
- error.target_url = targetUrl || null;
499
- error.login_url = loginUrl;
500
- error.login_detection = loginDetection || null;
501
- error.chrome = chrome || null;
502
- error.retryable = true;
503
- return error;
504
- }
505
-
506
- export async function detectBossLoginState(client, { currentUrl = "" } = {}) {
507
- const inspectedUrl = currentUrl || await getMainFrameUrl(client).catch(() => "");
508
- if (isBossLoginUrl(inspectedUrl)) {
509
- return {
510
- requires_login: true,
511
- reason: "url",
512
- current_url: inspectedUrl,
513
- matched_selectors: []
514
- };
515
- }
516
-
517
- let root = null;
518
- try {
519
- root = await getDocumentRoot(client, { depth: 1, pierce: true });
520
- } catch (error) {
521
- return {
522
- requires_login: false,
523
- reason: "dom_unavailable",
524
- current_url: inspectedUrl,
525
- error: error?.message || String(error || "")
526
- };
527
- }
528
-
529
- const matchedSelectors = [];
530
- for (const selector of BOSS_LOGIN_DOM_SELECTORS) {
531
- const nodeId = await querySelector(client, root.nodeId, selector).catch(() => 0);
532
- if (nodeId) matchedSelectors.push(selector);
533
- }
534
-
535
- if (matchedSelectors.length === 0) {
536
- return {
537
- requires_login: false,
538
- reason: "no_login_dom",
539
- current_url: inspectedUrl,
540
- matched_selectors: []
541
- };
542
- }
543
-
544
- const html = await getOuterHTML(client, root.nodeId).catch(() => "");
545
- const looksLikeLogin = BOSS_LOGIN_TEXT_PATTERN.test(html);
546
- return {
547
- requires_login: looksLikeLogin,
548
- reason: looksLikeLogin ? "dom" : "login_selector_without_login_text",
549
- current_url: inspectedUrl,
550
- matched_selectors: matchedSelectors
551
- };
552
- }
553
-
554
- export function isChromeDebugUnavailableError(error) {
555
- return CHROME_DEBUG_UNAVAILABLE_PATTERN.test(String(error?.message || error || ""));
556
- }
557
-
558
- function pathExists(targetPath) {
559
- try {
560
- return Boolean(targetPath) && fs.existsSync(targetPath);
561
- } catch {
562
- return false;
563
- }
564
- }
565
-
566
- function ensureDir(targetPath) {
567
- fs.mkdirSync(targetPath, { recursive: true });
568
- }
569
-
570
- function isLocalChromeHost(host) {
571
- const normalized = String(host || "").trim().toLowerCase();
572
- return !normalized || normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
573
- }
574
-
575
- function getCodexHome() {
576
- return process.env.CODEX_HOME
577
- ? path.resolve(process.env.CODEX_HOME)
578
- : path.join(os.homedir(), ".codex");
579
- }
580
-
581
- function getDefaultChromeExecutableCandidates() {
582
- const candidates = [
583
- process.env.BOSS_MCP_CHROME_PATH,
584
- process.env.BOSS_RECOMMEND_CHROME_PATH
585
- ].filter(Boolean);
586
- if (process.platform === "win32") {
587
- candidates.push(
588
- path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
589
- path.join(process.env.ProgramFiles || "", "Google", "Chrome", "Application", "chrome.exe"),
590
- path.join(process.env["ProgramFiles(x86)"] || "", "Google", "Chrome", "Application", "chrome.exe")
591
- );
592
- } else if (process.platform === "darwin") {
593
- candidates.push(
594
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
595
- path.join(os.homedir(), "Applications", "Google Chrome.app", "Contents", "MacOS", "Google Chrome"),
596
- "/Applications/Chromium.app/Contents/MacOS/Chromium"
597
- );
598
- } else {
599
- candidates.push(
600
- "/usr/bin/google-chrome",
601
- "/usr/bin/google-chrome-stable",
602
- "/usr/bin/chromium-browser",
603
- "/usr/bin/chromium",
604
- "/snap/bin/chromium"
605
- );
606
- }
607
- return Array.from(new Set(candidates.filter(Boolean)));
608
- }
609
-
610
- export function getChromeExecutable() {
611
- return getDefaultChromeExecutableCandidates().find((candidate) => pathExists(candidate)) || null;
612
- }
613
-
614
- export function getBossChromeUserDataDir(port = DEFAULT_CHROME_PORT) {
615
- const sharedPath = path.join(getCodexHome(), "boss-mcp", `chrome-profile-${port}`);
616
- ensureDir(sharedPath);
617
- return sharedPath;
618
- }
619
-
620
- function parseExtraChromeArgs(value = "") {
621
- return String(value || "")
622
- .split(/\s+/)
623
- .map((item) => item.trim())
624
- .filter(Boolean);
625
- }
626
-
627
- export function buildBossChromeLaunchArgs({
628
- port = DEFAULT_CHROME_PORT,
629
- userDataDir = "",
630
- url = "about:blank",
631
- extraArgs = []
632
- } = {}) {
633
- const args = [
634
- `--remote-debugging-port=${port}`,
635
- `--user-data-dir=${userDataDir}`,
636
- "--no-first-run",
637
- "--no-default-browser-check",
638
- ...LID_CLOSED_SAFE_CHROME_ARGS,
639
- ...parseExtraChromeArgs(process.env.BOSS_MCP_EXTRA_CHROME_ARGS),
640
- ...extraArgs,
641
- "--new-window",
642
- url
643
- ];
644
- return Array.from(new Set(args.filter(Boolean)));
645
- }
646
-
647
- export async function waitForChromeDebugPort({
648
- host = DEFAULT_CHROME_HOST,
649
- port = DEFAULT_CHROME_PORT,
650
- timeoutMs = 8000,
651
- intervalMs = 300
652
- } = {}) {
653
- const started = Date.now();
654
- let lastError = null;
655
- while (Date.now() - started <= timeoutMs) {
656
- try {
657
- const targets = await listChromeTargets({ host, port });
658
- return {
659
- ok: true,
660
- elapsed_ms: Date.now() - started,
661
- targets
662
- };
663
- } catch (error) {
664
- lastError = error;
665
- await sleep(intervalMs);
666
- }
667
- }
668
- return {
669
- ok: false,
670
- elapsed_ms: Date.now() - started,
671
- error: lastError?.message || String(lastError || "Chrome debug port did not become ready")
672
- };
673
- }
674
-
675
- export async function launchChromeDebugInstance({
676
- host = DEFAULT_CHROME_HOST,
677
- port = DEFAULT_CHROME_PORT,
678
- url = "about:blank",
679
- slowLive = false
680
- } = {}) {
681
- if (!isLocalChromeHost(host)) {
682
- throw new Error(`Cannot auto-launch Chrome for non-local debug host: ${host}`);
683
- }
684
- const chromePath = getChromeExecutable();
685
- if (!chromePath) {
686
- throw new Error("Chrome executable not found. Set BOSS_MCP_CHROME_PATH or BOSS_RECOMMEND_CHROME_PATH.");
687
- }
688
- const userDataDir = getBossChromeUserDataDir(port);
689
- const args = buildBossChromeLaunchArgs({ port, userDataDir, url });
690
- const child = spawn(chromePath, args, {
691
- detached: true,
692
- stdio: "ignore",
693
- windowsHide: false
694
- });
695
- child.unref();
696
- const readiness = await waitForChromeDebugPort({
697
- host,
698
- port,
699
- timeoutMs: slowLive ? 30000 : 12000,
700
- intervalMs: slowLive ? 700 : 300
701
- });
702
- if (!readiness.ok) {
703
- throw new Error(`Chrome launched but DevTools port ${port} did not become reachable: ${readiness.error}`);
704
- }
705
- return {
706
- launched: true,
707
- chrome_path: chromePath,
708
- user_data_dir: userDataDir,
709
- launch_args: args,
710
- port,
711
- url,
712
- readiness: {
713
- elapsed_ms: readiness.elapsed_ms,
714
- target_count: readiness.targets.length
715
- }
716
- };
717
- }
718
-
719
- export async function ensureChromeDebugPort({
720
- host = DEFAULT_CHROME_HOST,
721
- port = DEFAULT_CHROME_PORT,
722
- url = "about:blank",
723
- slowLive = false,
724
- launchIfMissing = true
725
- } = {}) {
726
- try {
727
- const targets = await listChromeTargets({ host, port });
728
- return {
729
- launched: false,
730
- reused: true,
731
- port,
732
- target_count: targets.length
733
- };
734
- } catch (error) {
735
- if (!launchIfMissing || !isChromeDebugUnavailableError(error)) {
736
- throw error;
737
- }
738
- return launchChromeDebugInstance({ host, port, url, slowLive });
739
- }
740
- }
741
-
742
- export async function openChromeTarget({
743
- host = DEFAULT_CHROME_HOST,
744
- port = DEFAULT_CHROME_PORT,
745
- url
746
- } = {}) {
747
- const encodedUrl = encodeURIComponent(url || "about:blank");
748
- const endpoint = `http://${host}:${port}/json/new?${encodedUrl}`;
749
- const methods = ["PUT", "GET"];
750
- let lastError = null;
751
- for (const method of methods) {
752
- try {
753
- const response = await fetch(endpoint, { method });
754
- if (response.ok) {
755
- let payload = null;
756
- try {
757
- payload = await response.json();
758
- } catch {}
759
- return {
760
- ok: true,
761
- method,
762
- target_id: payload?.id || null,
763
- url: payload?.url || url || null
764
- };
765
- }
766
- lastError = new Error(`DevTools /json/new returned ${response.status}`);
767
- } catch (error) {
768
- lastError = error;
769
- }
770
- }
771
- return {
772
- ok: false,
773
- error: lastError?.message || "Failed to open Chrome target"
774
- };
775
- }
776
-
777
- export async function connectToChromeTargetOrOpen({
778
- host = DEFAULT_CHROME_HOST,
779
- port = DEFAULT_CHROME_PORT,
780
- targetUrlIncludes,
781
- targetPredicate,
782
- fallbackTargetPredicate,
783
- targetUrl,
784
- allowNavigate = true,
785
- slowLive = false,
786
- launchIfMissing = true
787
- } = {}) {
788
- let chrome = null;
789
- if (allowNavigate && targetUrl) {
790
- chrome = await ensureChromeDebugPort({
791
- host,
792
- port,
793
- url: targetUrl,
794
- slowLive,
795
- launchIfMissing
796
- });
797
- }
798
-
799
- try {
800
- const session = await connectToChromeTarget({
801
- host,
802
- port,
803
- targetUrlIncludes,
804
- targetPredicate
805
- });
806
- return {
807
- ...session,
808
- chrome: {
809
- ...(chrome || { launched: false, reused: true, port }),
810
- target_created: false
811
- }
812
- };
813
- } catch (primaryError) {
814
- if (!allowNavigate) throw primaryError;
815
-
816
- if (typeof fallbackTargetPredicate === "function") {
817
- try {
818
- const session = await connectToChromeTarget({
819
- host,
820
- port,
821
- targetPredicate: fallbackTargetPredicate
822
- });
823
- return {
824
- ...session,
825
- chrome: {
826
- ...(chrome || { launched: false, reused: true, port }),
827
- target_created: false,
828
- fallback_target: true
829
- }
830
- };
831
- } catch {}
832
- }
833
-
834
- let openAttempt = null;
835
- if (targetUrl) {
836
- openAttempt = await openChromeTarget({ host, port, url: targetUrl });
837
- if (openAttempt.ok) {
838
- const session = await connectToChromeTarget({
839
- host,
840
- port,
841
- targetPredicate: (target) => (
842
- (openAttempt.target_id && target?.id === openAttempt.target_id)
843
- || String(target?.url || "").includes(targetUrlIncludes || targetUrl)
844
- || (targetUrl.includes("zhipin.com") && String(target?.url || "").includes("zhipin.com"))
845
- )
846
- });
847
- return {
848
- ...session,
849
- chrome: {
850
- ...(chrome || { launched: false, reused: true, port }),
851
- target_created: true,
852
- open_attempt: openAttempt
853
- }
854
- };
855
- }
856
- }
857
-
858
- const session = await connectToChromeTarget({
859
- host,
860
- port,
861
- targetPredicate: (target) => target?.type === "page"
862
- });
863
- return {
864
- ...session,
865
- chrome: {
866
- ...(chrome || { launched: false, reused: true, port }),
867
- target_created: false,
868
- open_attempt: openAttempt,
869
- fallback_any_page: true
870
- }
871
- };
872
- }
873
- }
874
-
875
- export function createGuardedCdpClient(client, { methodLog = [] } = {}) {
876
- return new Proxy(client, {
877
- get(target, property, receiver) {
878
- if (property === "send") {
879
- return async (method, params = {}) => {
880
- if (isForbiddenMethod(method)) {
881
- throw new Error(`Forbidden CDP method blocked: ${method}`);
882
- }
883
- recordMethod(methodLog, method);
884
- return target.send(method, params);
885
- };
886
- }
887
-
888
- const value = Reflect.get(target, property, receiver);
889
- if (!value || typeof value !== "object") return value;
890
-
891
- return new Proxy(value, {
892
- get(domainTarget, method, domainReceiver) {
893
- const domainValue = Reflect.get(domainTarget, method, domainReceiver);
894
- if (typeof domainValue !== "function") return domainValue;
895
-
896
- return async (params = {}) => {
897
- const fullMethod = methodName(property, method);
898
- if (isForbiddenMethod(fullMethod)) {
899
- throw new Error(`Forbidden CDP method blocked: ${fullMethod}`);
900
- }
901
- recordMethod(methodLog, fullMethod);
902
- return domainValue.call(domainTarget, params);
903
- };
904
- }
905
- });
906
- }
907
- });
908
- }
909
-
910
- export async function listChromeTargets({
911
- host = DEFAULT_CHROME_HOST,
912
- port = DEFAULT_CHROME_PORT
913
- } = {}) {
914
- return CDP.List({ host, port });
915
- }
916
-
917
- export async function connectToChromeTarget({
918
- host = DEFAULT_CHROME_HOST,
919
- port = DEFAULT_CHROME_PORT,
920
- targetUrlIncludes,
921
- targetPredicate
922
- } = {}) {
923
- const targets = await listChromeTargets({ host, port });
924
- const matcher = normalizeTargetMatcher({ targetUrlIncludes, targetPredicate });
925
- const target = targets.find(matcher);
926
- if (!target) {
927
- const urls = targets.map((item) => item.url).filter(Boolean).join("\n");
928
- throw new Error(`No matching Chrome target found on ${host}:${port}.\nAvailable targets:\n${urls}`);
929
- }
930
-
931
- const rawClient = await CDP({ host, port, target });
932
- const methodLog = [];
933
- const client = createGuardedCdpClient(rawClient, { methodLog });
934
-
935
- return {
936
- client,
937
- rawClient,
938
- target,
939
- methodLog,
940
- async close() {
941
- await rawClient.close();
942
- }
943
- };
944
- }
945
-
946
- export async function assertRuntimeEvaluateBlocked(client) {
947
- try {
948
- await client.Runtime.evaluate({ expression: "1" });
949
- } catch (error) {
950
- if (/Forbidden CDP method blocked: Runtime\.evaluate/.test(String(error?.message || ""))) {
951
- return { blocked: true, message: error.message };
952
- }
953
- throw error;
954
- }
955
- throw new Error("Runtime.evaluate was not blocked by the CDP guard");
956
- }
957
-
958
- export async function enableDomains(client, domains = ["Page", "DOM", "Input"]) {
959
- for (const domain of domains) {
960
- if (!ALLOWED_CDP_DOMAINS.has(domain)) {
961
- throw new Error(`CDP domain is not allowed by the CDP-only contract: ${domain}`);
962
- }
963
- if (typeof client?.[domain]?.enable === "function") {
964
- await client[domain].enable();
965
- }
966
- }
967
- }
968
-
969
- export async function bringPageToFront(client) {
970
- if (typeof client?.Page?.bringToFront === "function") {
971
- await client.Page.bringToFront();
972
- }
973
- }
974
-
975
- export async function getPageFrameTree(client) {
976
- const result = await client.Page.getFrameTree();
977
- return result.frameTree || null;
978
- }
979
-
980
- export async function getMainFrame(client) {
981
- const frameTree = await getPageFrameTree(client);
982
- return frameTree?.frame || null;
983
- }
984
-
985
- export async function getMainFrameUrl(client) {
986
- const frame = await getMainFrame(client);
987
- return frame?.url || "";
988
- }
989
-
990
- export async function waitForMainFrameUrl(client, predicate, {
991
- timeoutMs = 10000,
992
- intervalMs = 250
993
- } = {}) {
994
- const started = Date.now();
995
- let lastUrl = "";
996
- while (Date.now() - started <= timeoutMs) {
997
- lastUrl = await getMainFrameUrl(client);
998
- if (predicate(lastUrl)) {
999
- return {
1000
- ok: true,
1001
- elapsed_ms: Date.now() - started,
1002
- url: lastUrl
1003
- };
1004
- }
1005
- await sleep(intervalMs);
1006
- }
1007
- return {
1008
- ok: false,
1009
- elapsed_ms: Date.now() - started,
1010
- url: lastUrl
1011
- };
1012
- }
1013
-
1014
- export async function getDocumentRoot(client, { depth = 1, pierce = true } = {}) {
1015
- const result = await client.DOM.getDocument({ depth, pierce });
1016
- return result.root;
1017
- }
1018
-
1019
- export async function querySelector(client, nodeId, selector) {
1020
- const result = await client.DOM.querySelector({ nodeId, selector });
1021
- return result.nodeId || 0;
1022
- }
1023
-
1024
- export async function querySelectorAll(client, nodeId, selector) {
1025
- const result = await client.DOM.querySelectorAll({ nodeId, selector });
1026
- return result.nodeIds || [];
1027
- }
1028
-
1029
- export async function findFirstNode(client, rootNodeId, selectors = []) {
1030
- for (const selector of selectors) {
1031
- const nodeId = await querySelector(client, rootNodeId, selector);
1032
- if (nodeId) return { selector, nodeId };
1033
- }
1034
- return null;
1035
- }
1036
-
1037
- export async function describeNode(client, nodeId, { depth = 1, pierce = true } = {}) {
1038
- const result = await client.DOM.describeNode({ nodeId, depth, pierce });
1039
- return result.node;
1040
- }
1041
-
1042
- export async function getFrameDocumentNodeId(client, iframeNodeId) {
1043
- const node = await describeNode(client, iframeNodeId, { depth: 1, pierce: true });
1044
- const documentNodeId = node?.contentDocument?.nodeId;
1045
- if (!documentNodeId) {
1046
- throw new Error(`Node ${iframeNodeId} does not expose a contentDocument node`);
1047
- }
1048
- return documentNodeId;
1049
- }
1050
-
1051
- export async function findIframeDocument(client, rootNodeId, selectors = []) {
1052
- const iframe = await findFirstNode(client, rootNodeId, selectors);
1053
- if (!iframe) return null;
1054
- const documentNodeId = await getFrameDocumentNodeId(client, iframe.nodeId);
1055
- return { ...iframe, documentNodeId };
1056
- }
1057
-
1058
- export async function getAttributesMap(client, nodeId) {
1059
- const result = await client.DOM.getAttributes({ nodeId });
1060
- const attributes = {};
1061
- const raw = result.attributes || [];
1062
- for (let index = 0; index < raw.length; index += 2) {
1063
- attributes[raw[index]] = raw[index + 1] || "";
1064
- }
1065
- return attributes;
1066
- }
1067
-
1068
- export async function getOuterHTML(client, nodeId) {
1069
- const result = await client.DOM.getOuterHTML({ nodeId });
1070
- return result.outerHTML || "";
1071
- }
1072
-
1073
- export async function getNodeBox(client, nodeId) {
1074
- const result = await client.DOM.getBoxModel({ nodeId });
1075
- const model = result.model;
1076
- const quad = model.border?.length ? model.border : model.content;
1077
- const xs = [quad[0], quad[2], quad[4], quad[6]];
1078
- const ys = [quad[1], quad[3], quad[5], quad[7]];
1079
- const minX = Math.min(...xs);
1080
- const maxX = Math.max(...xs);
1081
- const minY = Math.min(...ys);
1082
- const maxY = Math.max(...ys);
1083
- return {
1084
- model,
1085
- center: {
1086
- x: (minX + maxX) / 2,
1087
- y: (minY + maxY) / 2
1088
- },
1089
- rect: {
1090
- x: minX,
1091
- y: minY,
1092
- width: maxX - minX,
1093
- height: maxY - minY
1094
- }
1095
- };
1096
- }
1097
-
1098
- export async function simulateHumanClick(client, targetX, targetY, {
1099
- button = "left",
1100
- clickCount = 1,
1101
- delayMs = 80,
1102
- random = Math.random,
1103
- sleepFn = sleep,
1104
- moveSteps = 18,
1105
- moveJitterPx = 3,
1106
- hoverJitterPx = 5,
1107
- moveDelayMinMs = 5,
1108
- moveDelayMaxMs = 23,
1109
- hoverDelayMinMs = 10,
1110
- hoverDelayMaxMs = 30,
1111
- prePressBaseMs = 260,
1112
- prePressVarianceMs = 80,
1113
- holdVarianceMs = 30,
1114
- startPoint = null
1115
- } = {}) {
1116
- const target = normalizePoint({ x: targetX, y: targetY });
1117
- if (!target) throw new Error("simulateHumanClick requires finite target coordinates");
1118
- const nextRandom = normalizeRandom(random);
1119
- const interactionConfig = getHumanInteractionConfig(client) || {};
1120
- const start = normalizePoint(startPoint)
1121
- || normalizePoint(interactionConfig.lastMousePoint)
1122
- || {
1123
- x: Math.max(0, target.x + randomBetween(nextRandom, -140, 140)),
1124
- y: Math.max(0, target.y + randomBetween(nextRandom, -90, 90))
1125
- };
1126
- const path = generateBezierPath(start, target, {
1127
- steps: moveSteps,
1128
- random: nextRandom
1129
- });
1130
- const sleeper = typeof sleepFn === "function" ? sleepFn : sleep;
1131
- const moveDelayMin = Math.min(moveDelayMinMs, moveDelayMaxMs);
1132
- const moveDelayMax = Math.max(moveDelayMinMs, moveDelayMaxMs);
1133
- const hoverDelayMin = Math.min(hoverDelayMinMs, hoverDelayMaxMs);
1134
- const hoverDelayMax = Math.max(hoverDelayMinMs, hoverDelayMaxMs);
1135
- for (const point of path) {
1136
- await client.Input.dispatchMouseEvent({
1137
- type: "mouseMoved",
1138
- x: Math.round(point.x + randomBetween(nextRandom, -moveJitterPx / 2, moveJitterPx / 2)),
1139
- y: Math.round(point.y + randomBetween(nextRandom, -moveJitterPx / 2, moveJitterPx / 2)),
1140
- button: "none"
1141
- });
1142
- const pauseMs = Math.round(randomBetween(nextRandom, moveDelayMin, moveDelayMax));
1143
- if (pauseMs > 0) await sleeper(pauseMs);
1144
- }
1145
- const hoverSteps = randomIntegerBetween(nextRandom, 3, 6);
1146
- for (let index = 0; index < hoverSteps; index += 1) {
1147
- await client.Input.dispatchMouseEvent({
1148
- type: "mouseMoved",
1149
- x: Math.round(target.x + randomBetween(nextRandom, -hoverJitterPx / 2, hoverJitterPx / 2)),
1150
- y: Math.round(target.y + randomBetween(nextRandom, -hoverJitterPx / 2, hoverJitterPx / 2)),
1151
- button: "none"
1152
- });
1153
- const pauseMs = Math.round(randomBetween(nextRandom, hoverDelayMin, hoverDelayMax));
1154
- if (pauseMs > 0) await sleeper(pauseMs);
1155
- }
1156
- const prePressMs = humanDelay(prePressBaseMs, prePressVarianceMs, {
1157
- minMs: 0,
1158
- maxMs: Math.max(prePressBaseMs + prePressVarianceMs * 4, prePressBaseMs),
1159
- random: nextRandom
1160
- });
1161
- if (prePressMs > 0) await sleeper(prePressMs);
1162
- await client.Input.dispatchMouseEvent({ type: "mousePressed", x: target.x, y: target.y, button, clickCount });
1163
- const holdMs = humanDelay(delayMs, holdVarianceMs, {
1164
- minMs: 0,
1165
- maxMs: Math.max(delayMs + holdVarianceMs * 4, delayMs),
1166
- random: nextRandom
1167
- });
1168
- if (holdMs > 0) await sleeper(holdMs);
1169
- await client.Input.dispatchMouseEvent({ type: "mouseReleased", x: target.x, y: target.y, button, clickCount });
1170
- const latestConfig = getHumanInteractionConfig(client);
1171
- if (latestConfig) latestConfig.lastMousePoint = target;
1172
- return {
1173
- mode: "human",
1174
- path_points: path.length,
1175
- hover_steps: hoverSteps,
1176
- pre_press_ms: prePressMs,
1177
- hold_ms: holdMs
1178
- };
1179
- }
1180
-
1181
- export function resolveHumanClickPointForBox(box, {
1182
- enabled = true,
1183
- safeClickPointEnabled = true,
1184
- random = Math.random,
1185
- safeClickMinWidth = 44,
1186
- safeClickMinHeight = 28,
1187
- safeClickInsetRatio = 0.22,
1188
- safeClickMinInsetPx = 4,
1189
- safeClickMaxInsetPx = 18
1190
- } = {}) {
1191
- const center = normalizePoint(box?.center);
1192
- if (!center) throw new Error("resolveHumanClickPointForBox requires a box center");
1193
- const rect = box?.rect || {};
1194
- const width = Number(rect.width);
1195
- const height = Number(rect.height);
1196
- const originX = Number(rect.x);
1197
- const originY = Number(rect.y);
1198
- if (
1199
- enabled !== true
1200
- || safeClickPointEnabled === false
1201
- || !Number.isFinite(width)
1202
- || !Number.isFinite(height)
1203
- || !Number.isFinite(originX)
1204
- || !Number.isFinite(originY)
1205
- || width < Math.max(1, Number(safeClickMinWidth) || 44)
1206
- || height < Math.max(1, Number(safeClickMinHeight) || 28)
1207
- ) {
1208
- return {
1209
- x: center.x,
1210
- y: center.y,
1211
- mode: "center",
1212
- reason: "small_or_disabled"
1213
- };
1214
- }
1215
-
1216
- const nextRandom = normalizeRandom(random);
1217
- const insetRatio = clampNumber(safeClickInsetRatio, 0.05, 0.45);
1218
- const minInset = Math.max(0, Number(safeClickMinInsetPx) || 0);
1219
- const maxInset = Math.max(minInset, Number(safeClickMaxInsetPx) || minInset);
1220
- const insetX = Math.min(width / 2 - 1, Math.max(minInset, Math.min(maxInset, width * insetRatio)));
1221
- const insetY = Math.min(height / 2 - 1, Math.max(minInset, Math.min(maxInset, height * insetRatio)));
1222
- const usableWidth = Math.max(0, width - insetX * 2);
1223
- const usableHeight = Math.max(0, height - insetY * 2);
1224
- if (usableWidth <= 0 || usableHeight <= 0) {
1225
- return {
1226
- x: center.x,
1227
- y: center.y,
1228
- mode: "center",
1229
- reason: "insufficient_safe_area"
1230
- };
1231
- }
1232
- return {
1233
- x: originX + insetX + nextRandom() * usableWidth,
1234
- y: originY + insetY + nextRandom() * usableHeight,
1235
- mode: "safe_inset",
1236
- inset_x: insetX,
1237
- inset_y: insetY
1238
- };
1239
- }
1240
-
1241
- export async function clickPoint(client, x, y, {
1242
- button = "left",
1243
- clickCount = 1,
1244
- delayMs = 80,
1245
- humanRestEnabled = null,
1246
- humanInteraction = null
1247
- } = {}) {
1248
- const configured = getHumanInteractionConfig(client);
1249
- const mergedHumanInteraction = {
1250
- ...(configured || {}),
1251
- ...(humanInteraction || {})
1252
- };
1253
- const humanEnabled = humanRestEnabled === true
1254
- || humanInteraction?.enabled === true
1255
- || (humanRestEnabled !== false && configured?.enabled === true);
1256
- if (humanEnabled && mergedHumanInteraction.clickMovementEnabled !== false) {
1257
- return simulateHumanClick(client, x, y, {
1258
- ...mergedHumanInteraction,
1259
- button,
1260
- clickCount,
1261
- delayMs
1262
- });
1263
- }
1264
- await client.Input.dispatchMouseEvent({ type: "mouseMoved", x, y, button: "none" });
1265
- await client.Input.dispatchMouseEvent({ type: "mousePressed", x, y, button, clickCount });
1266
- if (delayMs > 0) await sleep(delayMs);
1267
- await client.Input.dispatchMouseEvent({ type: "mouseReleased", x, y, button, clickCount });
1268
- return {
1269
- mode: "direct"
1270
- };
1271
- }
1272
-
1273
- export async function scrollNodeIntoView(client, nodeId) {
1274
- await client.DOM.scrollIntoViewIfNeeded({ nodeId });
1275
- }
1276
-
1277
- export async function clickNodeCenter(client, nodeId, {
1278
- scrollIntoView = false,
1279
- ...clickOptions
1280
- } = {}) {
1281
- if (scrollIntoView) {
1282
- await scrollNodeIntoView(client, nodeId);
1283
- await sleep(150);
1284
- }
1285
- const box = await getNodeBox(client, nodeId);
1286
- const configured = getHumanInteractionConfig(client);
1287
- const mergedHumanInteraction = {
1288
- ...(configured || {}),
1289
- ...(clickOptions.humanInteraction || {})
1290
- };
1291
- const humanClickPointEnabled = (
1292
- clickOptions.humanRestEnabled === true
1293
- || clickOptions.humanInteraction?.enabled === true
1294
- || (clickOptions.humanRestEnabled !== false && configured?.enabled === true)
1295
- ) && mergedHumanInteraction.safeClickPointEnabled !== false;
1296
- const clickPointTarget = humanClickPointEnabled
1297
- ? resolveHumanClickPointForBox(box, mergedHumanInteraction)
1298
- : { ...box.center, mode: "center" };
1299
- const clickResult = await clickPoint(client, clickPointTarget.x, clickPointTarget.y, clickOptions);
1300
- return {
1301
- ...box,
1302
- click_target: clickPointTarget,
1303
- click_result: clickResult
1304
- };
1305
- }
1306
-
1307
- export async function pressKey(client, key, {
1308
- code = key,
1309
- windowsVirtualKeyCode,
1310
- nativeVirtualKeyCode = windowsVirtualKeyCode,
1311
- text = "",
1312
- modifiers = 0
1313
- } = {}) {
1314
- await client.Input.dispatchKeyEvent({
1315
- type: "keyDown",
1316
- key,
1317
- code,
1318
- windowsVirtualKeyCode,
1319
- nativeVirtualKeyCode,
1320
- text,
1321
- modifiers
1322
- });
1323
- await client.Input.dispatchKeyEvent({
1324
- type: "keyUp",
1325
- key,
1326
- code,
1327
- windowsVirtualKeyCode,
1328
- nativeVirtualKeyCode,
1329
- modifiers
1330
- });
1331
- }
1332
-
1333
- export function chunkHumanText(text, {
1334
- random = Math.random,
1335
- minLength = 1,
1336
- maxLength = 5
1337
- } = {}) {
1338
- const chars = Array.from(String(text || ""));
1339
- const min = Math.max(1, Math.floor(Number(minLength) || 1));
1340
- const max = Math.max(min, Math.floor(Number(maxLength) || min));
1341
- const nextRandom = normalizeRandom(random);
1342
- const chunks = [];
1343
- let index = 0;
1344
- while (index < chars.length) {
1345
- const remaining = chars.length - index;
1346
- const size = Math.min(remaining, randomIntegerBetween(nextRandom, min, max));
1347
- chunks.push(chars.slice(index, index + size).join(""));
1348
- index += size;
1349
- }
1350
- return chunks;
1351
- }
1352
-
1353
- export async function insertText(client, text, {
1354
- humanTextEntryEnabled = null,
1355
- humanInteraction = null
1356
- } = {}) {
1357
- const value = String(text || "");
1358
- const configured = getHumanInteractionConfig(client);
1359
- const mergedHumanInteraction = {
1360
- ...(configured || {}),
1361
- ...(humanInteraction || {})
1362
- };
1363
- const textEntryEnabled = humanTextEntryEnabled === true
1364
- || humanInteraction?.textEntryEnabled === true
1365
- || (humanTextEntryEnabled !== false
1366
- && configured?.enabled === true
1367
- && configured?.textEntryEnabled !== false);
1368
- if (!textEntryEnabled || value.length <= 1) {
1369
- await client.Input.insertText({ text: value });
1370
- return {
1371
- mode: "direct",
1372
- chunk_count: value ? 1 : 0
1373
- };
1374
- }
1375
- const chunks = chunkHumanText(value, {
1376
- random: mergedHumanInteraction.random,
1377
- minLength: mergedHumanInteraction.textChunkMinLength,
1378
- maxLength: mergedHumanInteraction.textChunkMaxLength
1379
- });
1380
- const sleeper = typeof mergedHumanInteraction.sleepFn === "function"
1381
- ? mergedHumanInteraction.sleepFn
1382
- : sleep;
1383
- for (let index = 0; index < chunks.length; index += 1) {
1384
- await client.Input.insertText({ text: chunks[index] });
1385
- if (index < chunks.length - 1) {
1386
- const pauseMs = humanDelay(
1387
- mergedHumanInteraction.textChunkDelayBaseMs,
1388
- mergedHumanInteraction.textChunkDelayVarianceMs,
1389
- {
1390
- minMs: 0,
1391
- maxMs: Math.max(
1392
- mergedHumanInteraction.textChunkDelayBaseMs + mergedHumanInteraction.textChunkDelayVarianceMs * 4,
1393
- mergedHumanInteraction.textChunkDelayBaseMs
1394
- ),
1395
- random: mergedHumanInteraction.random
1396
- }
1397
- );
1398
- if (pauseMs > 0) await sleeper(pauseMs);
1399
- }
1400
- }
1401
- return {
1402
- mode: "chunked",
1403
- chunk_count: chunks.length,
1404
- chunks
1405
- };
1406
- }
1407
-
1408
- export async function selectAllFocusedText(client) {
1409
- await pressKey(client, "a", {
1410
- code: "KeyA",
1411
- windowsVirtualKeyCode: 65,
1412
- nativeVirtualKeyCode: 65,
1413
- modifiers: 2
1414
- });
1415
- }
1416
-
1417
- export async function clearFocusedInput(client) {
1418
- await selectAllFocusedText(client);
1419
- await pressKey(client, "Backspace", {
1420
- code: "Backspace",
1421
- windowsVirtualKeyCode: 8,
1422
- nativeVirtualKeyCode: 8
1423
- });
1424
- }
1425
-
1426
- export async function waitForSelector(client, nodeId, selector, {
1427
- timeoutMs = 5000,
1428
- intervalMs = 150
1429
- } = {}) {
1430
- const started = Date.now();
1431
- while (Date.now() - started <= timeoutMs) {
1432
- const foundNodeId = await querySelector(client, nodeId, selector);
1433
- if (foundNodeId) return foundNodeId;
1434
- await sleep(intervalMs);
1435
- }
1436
- return 0;
1437
- }
1438
-
1439
- export async function countSelectors(client, nodeId, selectors = {}) {
1440
- const counts = {};
1441
- for (const [name, selector] of Object.entries(selectors)) {
1442
- counts[name] = (await querySelectorAll(client, nodeId, selector)).length;
1443
- }
1444
- return counts;
1445
- }
1446
-
1447
- export async function getAccessibilityTree(client, options = {}) {
1448
- return client.Accessibility.getFullAXTree(options);
1449
- }
1450
-
1451
- export async function sleep(ms) {
1452
- await new Promise((resolve) => setTimeout(resolve, ms));
1453
- }
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import CDP from "chrome-remote-interface";
6
+
7
+ export const DEFAULT_CHROME_HOST = "127.0.0.1";
8
+ export const DEFAULT_CHROME_PORT = 9222;
9
+ export const BOSS_LOGIN_URL = "https://www.zhipin.com/web/user/?ka=bticket";
10
+ export const LID_CLOSED_SAFE_CHROME_ARGS = [
11
+ "--disable-backgrounding-occluded-windows",
12
+ "--disable-background-timer-throttling",
13
+ "--disable-renderer-backgrounding",
14
+ "--disable-features=CalculateNativeWinOcclusion"
15
+ ];
16
+
17
+ export const ALLOWED_CDP_DOMAINS = new Set([
18
+ "Accessibility",
19
+ "Browser",
20
+ "DOM",
21
+ "Input",
22
+ "Network",
23
+ "Page",
24
+ "Target"
25
+ ]);
26
+
27
+ export const FORBIDDEN_CDP_DOMAINS = new Set(["Runtime"]);
28
+
29
+ const BOSS_LOGIN_URL_PATTERN = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com|login\.zhipin\.com)/i;
30
+ const BOSS_LOGIN_TEXT_PATTERN = /扫码登录|验证码登录|密码登录|登录后|请登录|登录BOSS直聘|Boss登录|BOSS登录/i;
31
+ const CHROME_DEBUG_UNAVAILABLE_PATTERN = /ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|connect|socket hang up/i;
32
+ const CDP_CLOSED_TRANSPORT_PATTERN = /WebSocket is not open|readyState\s+\d+\s+\(CLOSED\)|ECONNRESET|socket hang up|Target closed|Session closed|Connection closed/i;
33
+ const BOSS_LOGIN_DOM_SELECTORS = [
34
+ ".login-box",
35
+ ".login-form",
36
+ ".login-dialog",
37
+ ".sign-form",
38
+ ".qrcode-box",
39
+ ".user-login",
40
+ "input[name='phone']",
41
+ "input[placeholder*='手机号']",
42
+ "input[placeholder*='验证码']"
43
+ ];
44
+ const HUMAN_INTERACTION_CONFIG = new WeakMap();
45
+ const DEFAULT_HUMAN_BEHAVIOR_PROFILE = "paced_with_rests";
46
+ export const DETERMINISTIC_CLICK_OPTIONS = Object.freeze({
47
+ humanRestEnabled: false
48
+ });
49
+ const HUMAN_BEHAVIOR_PROFILES = Object.freeze({
50
+ baseline: Object.freeze({
51
+ enabled: false,
52
+ clickMovement: false,
53
+ textEntry: false,
54
+ listScrollJitter: false,
55
+ shortRest: false,
56
+ batchRest: false,
57
+ actionCooldown: false
58
+ }),
59
+ paced: Object.freeze({
60
+ enabled: true,
61
+ clickMovement: true,
62
+ textEntry: true,
63
+ listScrollJitter: true,
64
+ shortRest: false,
65
+ batchRest: false,
66
+ actionCooldown: true
67
+ }),
68
+ paced_with_rests: Object.freeze({
69
+ enabled: true,
70
+ clickMovement: true,
71
+ textEntry: true,
72
+ listScrollJitter: true,
73
+ shortRest: true,
74
+ batchRest: true,
75
+ actionCooldown: true
76
+ })
77
+ });
78
+ const HUMAN_BEHAVIOR_PROFILE_ALIASES = Object.freeze({
79
+ off: "baseline",
80
+ disabled: "baseline",
81
+ deterministic: "baseline",
82
+ safe: "paced",
83
+ safe_pacing: "paced",
84
+ paced_with_rest: "paced_with_rests",
85
+ rests: "paced_with_rests",
86
+ rest: "paced_with_rests"
87
+ });
88
+
89
+ function clampNumber(value, min, max) {
90
+ const number = Number(value);
91
+ if (!Number.isFinite(number)) return min;
92
+ return Math.min(max, Math.max(min, number));
93
+ }
94
+
95
+ function randomBetween(random, min, max) {
96
+ const lower = Number(min) || 0;
97
+ const upper = Number(max) || lower;
98
+ if (upper <= lower) return lower;
99
+ return lower + random() * (upper - lower);
100
+ }
101
+
102
+ function randomIntegerBetween(random, min, max) {
103
+ return Math.floor(randomBetween(random, min, max + 1));
104
+ }
105
+
106
+ function normalizePoint(point) {
107
+ const x = Number(point?.x);
108
+ const y = Number(point?.y);
109
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
110
+ return { x, y };
111
+ }
112
+
113
+ function normalizeRandom(random) {
114
+ return typeof random === "function" ? random : Math.random;
115
+ }
116
+
117
+ function getHumanInteractionConfig(client) {
118
+ return HUMAN_INTERACTION_CONFIG.get(client) || null;
119
+ }
120
+
121
+ function normalizeBooleanOption(raw, fallback = null) {
122
+ if (typeof raw === "boolean") return raw;
123
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw !== 0;
124
+ const normalized = String(raw ?? "").trim().toLowerCase();
125
+ if (!normalized) return fallback;
126
+ if (["true", "1", "yes", "y", "on", "enabled"].includes(normalized)) return true;
127
+ if (["false", "0", "no", "n", "off", "disabled"].includes(normalized)) return false;
128
+ return fallback;
129
+ }
130
+
131
+ function readFirstOption(source, keys = []) {
132
+ if (!source || typeof source !== "object") return undefined;
133
+ for (const key of keys) {
134
+ if (Object.prototype.hasOwnProperty.call(source, key)) return source[key];
135
+ }
136
+ return undefined;
137
+ }
138
+
139
+ function normalizeFeatureBoolean(raw, fallback) {
140
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
141
+ return normalizeBooleanOption(readFirstOption(raw, ["enabled", "enable"]), fallback);
142
+ }
143
+ return normalizeBooleanOption(raw, fallback);
144
+ }
145
+
146
+ export function normalizeHumanBehaviorProfile(raw, fallback = "baseline") {
147
+ const normalized = String(raw || "").trim().toLowerCase().replace(/[\s-]+/g, "_");
148
+ const profile = HUMAN_BEHAVIOR_PROFILE_ALIASES[normalized] || normalized;
149
+ return Object.prototype.hasOwnProperty.call(HUMAN_BEHAVIOR_PROFILES, profile)
150
+ ? profile
151
+ : fallback;
152
+ }
153
+
154
+ export function normalizeHumanBehaviorOptions(raw = null, {
155
+ legacyEnabled = false,
156
+ safePacing = null,
157
+ batchRestEnabled = null
158
+ } = {}) {
159
+ const safePacingFlag = normalizeBooleanOption(safePacing, null);
160
+ const batchRestFlag = normalizeBooleanOption(batchRestEnabled, null);
161
+ let source = "default";
162
+ let rawObject = {};
163
+ if (typeof raw === "boolean") {
164
+ rawObject = { enabled: raw };
165
+ source = "boolean";
166
+ } else if (typeof raw === "string") {
167
+ rawObject = { profile: raw };
168
+ source = "profile";
169
+ } else if (raw && typeof raw === "object" && !Array.isArray(raw)) {
170
+ rawObject = raw;
171
+ source = "object";
172
+ }
173
+
174
+ const explicitProfile = readFirstOption(rawObject, ["profile", "mode", "behaviorProfile", "behavior_profile"]);
175
+ const enabledRaw = readFirstOption(rawObject, ["enabled", "enable", "human_behavior_enabled"]);
176
+ const explicitEnabled = normalizeBooleanOption(enabledRaw, null);
177
+ const inferredProfile = (raw === true || explicitEnabled === true) && legacyEnabled !== true && batchRestFlag !== true
178
+ ? "paced"
179
+ : legacyEnabled === true || batchRestFlag === true
180
+ ? "paced_with_rests"
181
+ : safePacingFlag === true
182
+ ? "paced"
183
+ : DEFAULT_HUMAN_BEHAVIOR_PROFILE;
184
+ const profile = normalizeHumanBehaviorProfile(explicitProfile, inferredProfile);
185
+ const profileDefaults = {
186
+ ...HUMAN_BEHAVIOR_PROFILES[profile]
187
+ };
188
+ if (legacyEnabled === true && !explicitProfile) {
189
+ Object.assign(profileDefaults, HUMAN_BEHAVIOR_PROFILES.paced_with_rests);
190
+ } else if (safePacingFlag === true && !explicitProfile) {
191
+ Object.assign(profileDefaults, HUMAN_BEHAVIOR_PROFILES.paced);
192
+ }
193
+ if (batchRestFlag === true && !explicitProfile) {
194
+ Object.assign(profileDefaults, HUMAN_BEHAVIOR_PROFILES.paced_with_rests);
195
+ }
196
+
197
+ const hasExplicitEnabled = enabledRaw !== undefined;
198
+ if (hasExplicitEnabled) {
199
+ profileDefaults.enabled = normalizeBooleanOption(enabledRaw, profileDefaults.enabled);
200
+ }
201
+ if (!hasExplicitEnabled && (safePacingFlag === false || batchRestFlag === false) && !explicitProfile && legacyEnabled !== true) {
202
+ profileDefaults.enabled = false;
203
+ }
204
+ if (!hasExplicitEnabled && (safePacingFlag === true || batchRestFlag === true || legacyEnabled === true)) {
205
+ profileDefaults.enabled = true;
206
+ }
207
+
208
+ const enabled = profileDefaults.enabled === true;
209
+ const clickMovement = normalizeFeatureBoolean(
210
+ readFirstOption(rawObject, ["clickMovement", "click_movement", "click_movement_enabled"]),
211
+ profileDefaults.clickMovement
212
+ );
213
+ const textEntry = normalizeFeatureBoolean(
214
+ readFirstOption(rawObject, ["textEntry", "text_entry", "text_entry_enabled"]),
215
+ profileDefaults.textEntry
216
+ );
217
+ const listScrollJitter = normalizeFeatureBoolean(
218
+ readFirstOption(rawObject, ["listScrollJitter", "list_scroll_jitter", "scrollJitter", "scroll_jitter"]),
219
+ profileDefaults.listScrollJitter
220
+ );
221
+ const actionCooldown = normalizeFeatureBoolean(
222
+ readFirstOption(rawObject, ["actionCooldown", "action_cooldown", "readPause", "read_pause"]),
223
+ profileDefaults.actionCooldown
224
+ );
225
+ let shortRest = normalizeFeatureBoolean(
226
+ readFirstOption(rawObject, ["shortRest", "short_rest", "randomRest", "random_rest"]),
227
+ profileDefaults.shortRest
228
+ );
229
+ let batchRest = normalizeFeatureBoolean(
230
+ readFirstOption(rawObject, ["batchRest", "batch_rest", "batchRestEnabled", "batch_rest_enabled"]),
231
+ profileDefaults.batchRest
232
+ );
233
+ if (batchRestFlag !== null) {
234
+ batchRest = batchRestFlag;
235
+ if (batchRestFlag === true && readFirstOption(rawObject, ["shortRest", "short_rest", "randomRest", "random_rest"]) === undefined) {
236
+ shortRest = true;
237
+ }
238
+ }
239
+
240
+ return {
241
+ enabled,
242
+ profile,
243
+ source,
244
+ clickMovement: enabled && clickMovement === true,
245
+ textEntry: enabled && textEntry === true,
246
+ listScrollJitter: enabled && listScrollJitter === true,
247
+ shortRest: enabled && shortRest === true,
248
+ batchRest: enabled && batchRest === true,
249
+ actionCooldown: enabled && actionCooldown === true,
250
+ restEnabled: enabled && (shortRest === true || batchRest === true)
251
+ };
252
+ }
253
+
254
+ function nowIso() {
255
+ return new Date().toISOString();
256
+ }
257
+
258
+ function normalizeTargetMatcher({ targetUrlIncludes, targetPredicate } = {}) {
259
+ if (typeof targetPredicate === "function") return targetPredicate;
260
+ if (targetUrlIncludes) {
261
+ return (target) => String(target?.url || "").includes(targetUrlIncludes);
262
+ }
263
+ return (target) => target?.type === "page";
264
+ }
265
+
266
+ function isForbiddenMethod(methodName) {
267
+ const [domain] = String(methodName || "").split(".");
268
+ return FORBIDDEN_CDP_DOMAINS.has(domain);
269
+ }
270
+
271
+ function methodName(domain, method) {
272
+ return `${String(domain)}.${String(method)}`;
273
+ }
274
+
275
+ function recordMethod(methodLog, method) {
276
+ if (Array.isArray(methodLog)) {
277
+ methodLog.push({ method, at: nowIso() });
278
+ }
279
+ }
280
+
281
+ export function assertNoForbiddenCdpCalls(methodLog = []) {
282
+ const forbidden = methodLog.filter((entry) => isForbiddenMethod(entry?.method));
283
+ if (forbidden.length > 0) {
284
+ const methods = forbidden.map((entry) => entry.method).join(", ");
285
+ throw new Error(`Forbidden CDP methods were used: ${methods}`);
286
+ }
287
+ }
288
+
289
+ export function humanDelay(baseMs, varianceMs, {
290
+ minMs = 100,
291
+ maxMs = 60000,
292
+ random = Math.random
293
+ } = {}) {
294
+ const nextRandom = normalizeRandom(random);
295
+ const base = Math.max(0, Number(baseMs) || 0);
296
+ const variance = Math.max(0, Number(varianceMs) || 0);
297
+ const lower = Math.max(0, Number(minMs) || 0);
298
+ const upper = Math.max(lower, Number(maxMs) || lower);
299
+ if (variance <= 0) return Math.round(clampNumber(base, lower, upper));
300
+ const u1 = Math.max(Number.EPSILON, Math.min(1 - Number.EPSILON, nextRandom()));
301
+ const u2 = Math.max(Number.EPSILON, Math.min(1 - Number.EPSILON, nextRandom()));
302
+ const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
303
+ return Math.round(clampNumber(base + z * variance, lower, upper));
304
+ }
305
+
306
+ export function generateBezierPath(start, end, {
307
+ steps = 18,
308
+ random = Math.random,
309
+ controlJitterX = 100,
310
+ controlJitterY = 60
311
+ } = {}) {
312
+ const startPoint = normalizePoint(start);
313
+ const endPoint = normalizePoint(end);
314
+ if (!startPoint || !endPoint) {
315
+ throw new Error("generateBezierPath requires finite start and end points");
316
+ }
317
+ const nextRandom = normalizeRandom(random);
318
+ const safeSteps = Math.max(1, Math.floor(Number(steps) || 18));
319
+ const midX = (startPoint.x + endPoint.x) / 2 + (nextRandom() - 0.5) * Math.max(0, Number(controlJitterX) || 0);
320
+ const midY = (startPoint.y + endPoint.y) / 2 + (nextRandom() - 0.5) * Math.max(0, Number(controlJitterY) || 0);
321
+ const path = [];
322
+ for (let index = 0; index <= safeSteps; index += 1) {
323
+ const t = index / safeSteps;
324
+ const inverse = 1 - t;
325
+ path.push({
326
+ x: inverse * inverse * startPoint.x + 2 * inverse * t * midX + t * t * endPoint.x,
327
+ y: inverse * inverse * startPoint.y + 2 * inverse * t * midY + t * t * endPoint.y
328
+ });
329
+ }
330
+ return path;
331
+ }
332
+
333
+ export function configureHumanInteraction(client, {
334
+ enabled = false,
335
+ clickMovementEnabled = null,
336
+ textEntryEnabled = null,
337
+ safeClickPointEnabled = null,
338
+ actionCooldownEnabled = null,
339
+ random = Math.random,
340
+ sleepFn = null,
341
+ moveSteps = 18,
342
+ moveJitterPx = 3,
343
+ hoverJitterPx = 5,
344
+ moveDelayMinMs = 5,
345
+ moveDelayMaxMs = 23,
346
+ hoverDelayMinMs = 10,
347
+ hoverDelayMaxMs = 30,
348
+ prePressBaseMs = 260,
349
+ prePressVarianceMs = 80,
350
+ holdVarianceMs = 30,
351
+ safeClickMinWidth = 44,
352
+ safeClickMinHeight = 28,
353
+ safeClickInsetRatio = 0.22,
354
+ safeClickMinInsetPx = 4,
355
+ safeClickMaxInsetPx = 18,
356
+ textChunkMinLength = 1,
357
+ textChunkMaxLength = 5,
358
+ textChunkDelayBaseMs = 55,
359
+ textChunkDelayVarianceMs = 30
360
+ } = {}) {
361
+ const previous = getHumanInteractionConfig(client);
362
+ const normalizedEnabled = enabled === true;
363
+ HUMAN_INTERACTION_CONFIG.set(client, {
364
+ enabled: normalizedEnabled,
365
+ clickMovementEnabled: normalizedEnabled && clickMovementEnabled !== false,
366
+ textEntryEnabled: normalizedEnabled && textEntryEnabled !== false,
367
+ safeClickPointEnabled: normalizedEnabled && safeClickPointEnabled !== false,
368
+ actionCooldownEnabled: normalizedEnabled && actionCooldownEnabled !== false,
369
+ random: normalizeRandom(random),
370
+ sleepFn: typeof sleepFn === "function" ? sleepFn : sleep,
371
+ moveSteps: Math.max(1, Math.floor(Number(moveSteps) || 18)),
372
+ moveJitterPx: Math.max(0, Number(moveJitterPx) || 0),
373
+ hoverJitterPx: Math.max(0, Number(hoverJitterPx) || 0),
374
+ moveDelayMinMs: Math.max(0, Number(moveDelayMinMs) || 0),
375
+ moveDelayMaxMs: Math.max(0, Number(moveDelayMaxMs) || 0),
376
+ hoverDelayMinMs: Math.max(0, Number(hoverDelayMinMs) || 0),
377
+ hoverDelayMaxMs: Math.max(0, Number(hoverDelayMaxMs) || 0),
378
+ prePressBaseMs: Math.max(0, Number(prePressBaseMs) || 0),
379
+ prePressVarianceMs: Math.max(0, Number(prePressVarianceMs) || 0),
380
+ holdVarianceMs: Math.max(0, Number(holdVarianceMs) || 0),
381
+ safeClickMinWidth: Math.max(1, Number(safeClickMinWidth) || 44),
382
+ safeClickMinHeight: Math.max(1, Number(safeClickMinHeight) || 28),
383
+ safeClickInsetRatio: clampNumber(safeClickInsetRatio, 0.05, 0.45),
384
+ safeClickMinInsetPx: Math.max(0, Number(safeClickMinInsetPx) || 0),
385
+ safeClickMaxInsetPx: Math.max(0, Number(safeClickMaxInsetPx) || 0),
386
+ textChunkMinLength: Math.max(1, Math.floor(Number(textChunkMinLength) || 1)),
387
+ textChunkMaxLength: Math.max(1, Math.floor(Number(textChunkMaxLength) || 5)),
388
+ textChunkDelayBaseMs: Math.max(0, Number(textChunkDelayBaseMs) || 0),
389
+ textChunkDelayVarianceMs: Math.max(0, Number(textChunkDelayVarianceMs) || 0),
390
+ lastMousePoint: previous?.lastMousePoint || null
391
+ });
392
+ return () => {
393
+ if (previous) {
394
+ HUMAN_INTERACTION_CONFIG.set(client, previous);
395
+ } else {
396
+ HUMAN_INTERACTION_CONFIG.delete(client);
397
+ }
398
+ };
399
+ }
400
+
401
+ export function createHumanRestController({
402
+ enabled = false,
403
+ shortRestEnabled = true,
404
+ batchRestEnabled = true,
405
+ random = Math.random,
406
+ shortRestProbability = 0.08,
407
+ shortRestMinMs = 3000,
408
+ shortRestMaxMs = 7000,
409
+ batchThresholdBase = 25,
410
+ batchThresholdJitter = 8,
411
+ batchRestMinMs = 15000,
412
+ batchRestMaxMs = 30000
413
+ } = {}) {
414
+ const nextRandom = normalizeRandom(random);
415
+ const state = {
416
+ enabled: enabled === true,
417
+ short_rest_enabled: enabled === true && shortRestEnabled !== false,
418
+ batch_rest_enabled: enabled === true && batchRestEnabled !== false,
419
+ rest_counter: 0,
420
+ rest_threshold: Math.max(1, Math.floor(Number(batchThresholdBase) || 25) + Math.floor(nextRandom() * Math.max(1, Number(batchThresholdJitter) || 1))),
421
+ rest_count: 0,
422
+ total_rest_ms: 0
423
+ };
424
+
425
+ function resetThreshold() {
426
+ state.rest_threshold = Math.max(1, Math.floor(Number(batchThresholdBase) || 25) + Math.floor(nextRandom() * Math.max(1, Number(batchThresholdJitter) || 1)));
427
+ }
428
+
429
+ async function takeBreakIfNeeded({ sleepFn = sleep } = {}) {
430
+ if (!state.enabled) {
431
+ return {
432
+ enabled: false,
433
+ rested: false,
434
+ rest_counter: state.rest_counter,
435
+ rest_threshold: state.rest_threshold,
436
+ events: []
437
+ };
438
+ }
439
+ const sleeper = typeof sleepFn === "function" ? sleepFn : sleep;
440
+ state.rest_counter += 1;
441
+ const events = [];
442
+ if (state.short_rest_enabled && nextRandom() < Math.max(0, Number(shortRestProbability) || 0)) {
443
+ const pauseMs = Math.round(randomBetween(nextRandom, shortRestMinMs, shortRestMaxMs));
444
+ await sleeper(pauseMs);
445
+ events.push({ kind: "random_rest", pause_ms: pauseMs });
446
+ }
447
+ if (state.batch_rest_enabled && state.rest_counter >= state.rest_threshold) {
448
+ const pauseMs = Math.round(randomBetween(nextRandom, batchRestMinMs, batchRestMaxMs));
449
+ await sleeper(pauseMs);
450
+ events.push({
451
+ kind: "batch_rest",
452
+ pause_ms: pauseMs,
453
+ processed_since_last_batch_rest: state.rest_counter
454
+ });
455
+ state.rest_counter = 0;
456
+ resetThreshold();
457
+ }
458
+ const pauseMs = events.reduce((sum, event) => sum + event.pause_ms, 0);
459
+ if (pauseMs > 0) {
460
+ state.rest_count += events.length;
461
+ state.total_rest_ms += pauseMs;
462
+ }
463
+ return {
464
+ enabled: true,
465
+ rested: events.length > 0,
466
+ pause_ms: pauseMs,
467
+ rest_counter: state.rest_counter,
468
+ rest_threshold: state.rest_threshold,
469
+ rest_count: state.rest_count,
470
+ total_rest_ms: state.total_rest_ms,
471
+ events
472
+ };
473
+ }
474
+
475
+ return {
476
+ takeBreakIfNeeded,
477
+ getState() {
478
+ return { ...state };
479
+ }
480
+ };
481
+ }
482
+
483
+ export function isBossLoginUrl(url) {
484
+ return BOSS_LOGIN_URL_PATTERN.test(String(url || ""));
485
+ }
486
+
487
+ export function createBossLoginRequiredError({
488
+ domain = "boss",
489
+ currentUrl = "",
490
+ targetUrl = "",
491
+ loginUrl = BOSS_LOGIN_URL,
492
+ loginDetection = null,
493
+ chrome = null
494
+ } = {}) {
495
+ const error = new Error(`Boss login is required before starting the ${domain} run.`);
496
+ error.code = "BOSS_LOGIN_REQUIRED";
497
+ error.requires_login = true;
498
+ error.current_url = currentUrl || null;
499
+ error.target_url = targetUrl || null;
500
+ error.login_url = loginUrl;
501
+ error.login_detection = loginDetection || null;
502
+ error.chrome = chrome || null;
503
+ error.retryable = true;
504
+ return error;
505
+ }
506
+
507
+ export async function detectBossLoginState(client, { currentUrl = "" } = {}) {
508
+ const inspectedUrl = currentUrl || await getMainFrameUrl(client).catch(() => "");
509
+ if (isBossLoginUrl(inspectedUrl)) {
510
+ return {
511
+ requires_login: true,
512
+ reason: "url",
513
+ current_url: inspectedUrl,
514
+ matched_selectors: []
515
+ };
516
+ }
517
+
518
+ let root = null;
519
+ try {
520
+ root = await getDocumentRoot(client, { depth: 1, pierce: true });
521
+ } catch (error) {
522
+ return {
523
+ requires_login: false,
524
+ reason: "dom_unavailable",
525
+ current_url: inspectedUrl,
526
+ error: error?.message || String(error || "")
527
+ };
528
+ }
529
+
530
+ const matchedSelectors = [];
531
+ for (const selector of BOSS_LOGIN_DOM_SELECTORS) {
532
+ const nodeId = await querySelector(client, root.nodeId, selector).catch(() => 0);
533
+ if (nodeId) matchedSelectors.push(selector);
534
+ }
535
+
536
+ if (matchedSelectors.length === 0) {
537
+ return {
538
+ requires_login: false,
539
+ reason: "no_login_dom",
540
+ current_url: inspectedUrl,
541
+ matched_selectors: []
542
+ };
543
+ }
544
+
545
+ const html = await getOuterHTML(client, root.nodeId).catch(() => "");
546
+ const looksLikeLogin = BOSS_LOGIN_TEXT_PATTERN.test(html);
547
+ return {
548
+ requires_login: looksLikeLogin,
549
+ reason: looksLikeLogin ? "dom" : "login_selector_without_login_text",
550
+ current_url: inspectedUrl,
551
+ matched_selectors: matchedSelectors
552
+ };
553
+ }
554
+
555
+ export function isChromeDebugUnavailableError(error) {
556
+ return CHROME_DEBUG_UNAVAILABLE_PATTERN.test(String(error?.message || error || ""));
557
+ }
558
+
559
+ function pathExists(targetPath) {
560
+ try {
561
+ return Boolean(targetPath) && fs.existsSync(targetPath);
562
+ } catch {
563
+ return false;
564
+ }
565
+ }
566
+
567
+ function ensureDir(targetPath) {
568
+ fs.mkdirSync(targetPath, { recursive: true });
569
+ }
570
+
571
+ function isLocalChromeHost(host) {
572
+ const normalized = String(host || "").trim().toLowerCase();
573
+ return !normalized || normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
574
+ }
575
+
576
+ function getCodexHome() {
577
+ return process.env.CODEX_HOME
578
+ ? path.resolve(process.env.CODEX_HOME)
579
+ : path.join(os.homedir(), ".codex");
580
+ }
581
+
582
+ function getDefaultChromeExecutableCandidates() {
583
+ const candidates = [
584
+ process.env.BOSS_MCP_CHROME_PATH,
585
+ process.env.BOSS_RECOMMEND_CHROME_PATH
586
+ ].filter(Boolean);
587
+ if (process.platform === "win32") {
588
+ candidates.push(
589
+ path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
590
+ path.join(process.env.ProgramFiles || "", "Google", "Chrome", "Application", "chrome.exe"),
591
+ path.join(process.env["ProgramFiles(x86)"] || "", "Google", "Chrome", "Application", "chrome.exe")
592
+ );
593
+ } else if (process.platform === "darwin") {
594
+ candidates.push(
595
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
596
+ path.join(os.homedir(), "Applications", "Google Chrome.app", "Contents", "MacOS", "Google Chrome"),
597
+ "/Applications/Chromium.app/Contents/MacOS/Chromium"
598
+ );
599
+ } else {
600
+ candidates.push(
601
+ "/usr/bin/google-chrome",
602
+ "/usr/bin/google-chrome-stable",
603
+ "/usr/bin/chromium-browser",
604
+ "/usr/bin/chromium",
605
+ "/snap/bin/chromium"
606
+ );
607
+ }
608
+ return Array.from(new Set(candidates.filter(Boolean)));
609
+ }
610
+
611
+ export function getChromeExecutable() {
612
+ return getDefaultChromeExecutableCandidates().find((candidate) => pathExists(candidate)) || null;
613
+ }
614
+
615
+ export function getBossChromeUserDataDir(port = DEFAULT_CHROME_PORT) {
616
+ const sharedPath = path.join(getCodexHome(), "boss-mcp", `chrome-profile-${port}`);
617
+ ensureDir(sharedPath);
618
+ return sharedPath;
619
+ }
620
+
621
+ function parseExtraChromeArgs(value = "") {
622
+ return String(value || "")
623
+ .split(/\s+/)
624
+ .map((item) => item.trim())
625
+ .filter(Boolean);
626
+ }
627
+
628
+ export function buildBossChromeLaunchArgs({
629
+ port = DEFAULT_CHROME_PORT,
630
+ userDataDir = "",
631
+ url = "about:blank",
632
+ extraArgs = []
633
+ } = {}) {
634
+ const args = [
635
+ `--remote-debugging-port=${port}`,
636
+ `--user-data-dir=${userDataDir}`,
637
+ "--no-first-run",
638
+ "--no-default-browser-check",
639
+ ...LID_CLOSED_SAFE_CHROME_ARGS,
640
+ ...parseExtraChromeArgs(process.env.BOSS_MCP_EXTRA_CHROME_ARGS),
641
+ ...extraArgs,
642
+ "--new-window",
643
+ url
644
+ ];
645
+ return Array.from(new Set(args.filter(Boolean)));
646
+ }
647
+
648
+ export async function waitForChromeDebugPort({
649
+ host = DEFAULT_CHROME_HOST,
650
+ port = DEFAULT_CHROME_PORT,
651
+ timeoutMs = 8000,
652
+ intervalMs = 300
653
+ } = {}) {
654
+ const started = Date.now();
655
+ let lastError = null;
656
+ while (Date.now() - started <= timeoutMs) {
657
+ try {
658
+ const targets = await listChromeTargets({ host, port });
659
+ return {
660
+ ok: true,
661
+ elapsed_ms: Date.now() - started,
662
+ targets
663
+ };
664
+ } catch (error) {
665
+ lastError = error;
666
+ await sleep(intervalMs);
667
+ }
668
+ }
669
+ return {
670
+ ok: false,
671
+ elapsed_ms: Date.now() - started,
672
+ error: lastError?.message || String(lastError || "Chrome debug port did not become ready")
673
+ };
674
+ }
675
+
676
+ export async function launchChromeDebugInstance({
677
+ host = DEFAULT_CHROME_HOST,
678
+ port = DEFAULT_CHROME_PORT,
679
+ url = "about:blank",
680
+ slowLive = false
681
+ } = {}) {
682
+ if (!isLocalChromeHost(host)) {
683
+ throw new Error(`Cannot auto-launch Chrome for non-local debug host: ${host}`);
684
+ }
685
+ const chromePath = getChromeExecutable();
686
+ if (!chromePath) {
687
+ throw new Error("Chrome executable not found. Set BOSS_MCP_CHROME_PATH or BOSS_RECOMMEND_CHROME_PATH.");
688
+ }
689
+ const userDataDir = getBossChromeUserDataDir(port);
690
+ const args = buildBossChromeLaunchArgs({ port, userDataDir, url });
691
+ const child = spawn(chromePath, args, {
692
+ detached: true,
693
+ stdio: "ignore",
694
+ windowsHide: false
695
+ });
696
+ child.unref();
697
+ const readiness = await waitForChromeDebugPort({
698
+ host,
699
+ port,
700
+ timeoutMs: slowLive ? 30000 : 12000,
701
+ intervalMs: slowLive ? 700 : 300
702
+ });
703
+ if (!readiness.ok) {
704
+ throw new Error(`Chrome launched but DevTools port ${port} did not become reachable: ${readiness.error}`);
705
+ }
706
+ return {
707
+ launched: true,
708
+ chrome_path: chromePath,
709
+ user_data_dir: userDataDir,
710
+ launch_args: args,
711
+ port,
712
+ url,
713
+ readiness: {
714
+ elapsed_ms: readiness.elapsed_ms,
715
+ target_count: readiness.targets.length
716
+ }
717
+ };
718
+ }
719
+
720
+ export async function ensureChromeDebugPort({
721
+ host = DEFAULT_CHROME_HOST,
722
+ port = DEFAULT_CHROME_PORT,
723
+ url = "about:blank",
724
+ slowLive = false,
725
+ launchIfMissing = true
726
+ } = {}) {
727
+ try {
728
+ const targets = await listChromeTargets({ host, port });
729
+ return {
730
+ launched: false,
731
+ reused: true,
732
+ port,
733
+ target_count: targets.length
734
+ };
735
+ } catch (error) {
736
+ if (!launchIfMissing || !isChromeDebugUnavailableError(error)) {
737
+ throw error;
738
+ }
739
+ return launchChromeDebugInstance({ host, port, url, slowLive });
740
+ }
741
+ }
742
+
743
+ export async function openChromeTarget({
744
+ host = DEFAULT_CHROME_HOST,
745
+ port = DEFAULT_CHROME_PORT,
746
+ url
747
+ } = {}) {
748
+ const encodedUrl = encodeURIComponent(url || "about:blank");
749
+ const endpoint = `http://${host}:${port}/json/new?${encodedUrl}`;
750
+ const methods = ["PUT", "GET"];
751
+ let lastError = null;
752
+ for (const method of methods) {
753
+ try {
754
+ const response = await fetch(endpoint, { method });
755
+ if (response.ok) {
756
+ let payload = null;
757
+ try {
758
+ payload = await response.json();
759
+ } catch {}
760
+ return {
761
+ ok: true,
762
+ method,
763
+ target_id: payload?.id || null,
764
+ url: payload?.url || url || null
765
+ };
766
+ }
767
+ lastError = new Error(`DevTools /json/new returned ${response.status}`);
768
+ } catch (error) {
769
+ lastError = error;
770
+ }
771
+ }
772
+ return {
773
+ ok: false,
774
+ error: lastError?.message || "Failed to open Chrome target"
775
+ };
776
+ }
777
+
778
+ export async function connectToChromeTargetOrOpen({
779
+ host = DEFAULT_CHROME_HOST,
780
+ port = DEFAULT_CHROME_PORT,
781
+ targetUrlIncludes,
782
+ targetPredicate,
783
+ fallbackTargetPredicate,
784
+ targetUrl,
785
+ allowNavigate = true,
786
+ slowLive = false,
787
+ launchIfMissing = true
788
+ } = {}) {
789
+ let chrome = null;
790
+ if (allowNavigate && targetUrl) {
791
+ chrome = await ensureChromeDebugPort({
792
+ host,
793
+ port,
794
+ url: targetUrl,
795
+ slowLive,
796
+ launchIfMissing
797
+ });
798
+ }
799
+
800
+ try {
801
+ const session = await connectToChromeTarget({
802
+ host,
803
+ port,
804
+ targetUrlIncludes,
805
+ targetPredicate
806
+ });
807
+ return {
808
+ ...session,
809
+ chrome: {
810
+ ...(chrome || { launched: false, reused: true, port }),
811
+ target_created: false
812
+ }
813
+ };
814
+ } catch (primaryError) {
815
+ if (!allowNavigate) throw primaryError;
816
+
817
+ if (typeof fallbackTargetPredicate === "function") {
818
+ try {
819
+ const session = await connectToChromeTarget({
820
+ host,
821
+ port,
822
+ targetPredicate: fallbackTargetPredicate
823
+ });
824
+ return {
825
+ ...session,
826
+ chrome: {
827
+ ...(chrome || { launched: false, reused: true, port }),
828
+ target_created: false,
829
+ fallback_target: true
830
+ }
831
+ };
832
+ } catch {}
833
+ }
834
+
835
+ let openAttempt = null;
836
+ if (targetUrl) {
837
+ openAttempt = await openChromeTarget({ host, port, url: targetUrl });
838
+ if (openAttempt.ok) {
839
+ const session = await connectToChromeTarget({
840
+ host,
841
+ port,
842
+ targetPredicate: (target) => (
843
+ (openAttempt.target_id && target?.id === openAttempt.target_id)
844
+ || String(target?.url || "").includes(targetUrlIncludes || targetUrl)
845
+ || (targetUrl.includes("zhipin.com") && String(target?.url || "").includes("zhipin.com"))
846
+ )
847
+ });
848
+ return {
849
+ ...session,
850
+ chrome: {
851
+ ...(chrome || { launched: false, reused: true, port }),
852
+ target_created: true,
853
+ open_attempt: openAttempt
854
+ }
855
+ };
856
+ }
857
+ }
858
+
859
+ const session = await connectToChromeTarget({
860
+ host,
861
+ port,
862
+ targetPredicate: (target) => target?.type === "page"
863
+ });
864
+ return {
865
+ ...session,
866
+ chrome: {
867
+ ...(chrome || { launched: false, reused: true, port }),
868
+ target_created: false,
869
+ open_attempt: openAttempt,
870
+ fallback_any_page: true
871
+ }
872
+ };
873
+ }
874
+ }
875
+
876
+ export function isClosedCdpTransportError(error) {
877
+ return CDP_CLOSED_TRANSPORT_PATTERN.test(String(error?.message || error || ""));
878
+ }
879
+
880
+ function cloneCdpParams(params = {}) {
881
+ if (!params || typeof params !== "object" || typeof params === "function") return params;
882
+ try {
883
+ return JSON.parse(JSON.stringify(params));
884
+ } catch {
885
+ return { ...params };
886
+ }
887
+ }
888
+
889
+ function shouldReplayCdpSetupCall(domain, method) {
890
+ return method === "enable"
891
+ || (domain === "Network" && method === "setCacheDisabled")
892
+ || (domain === "Page" && method === "bringToFront");
893
+ }
894
+
895
+ export function createGuardedCdpClient(client, { methodLog = [], reconnect = null } = {}) {
896
+ let currentClient = client;
897
+ let reconnectInFlight = null;
898
+ const setupCalls = [];
899
+ const eventSubscriptions = [];
900
+
901
+ async function replaySessionSetup(nextClient) {
902
+ for (const call of setupCalls) {
903
+ const fn = nextClient?.[call.domain]?.[call.method];
904
+ if (typeof fn === "function") {
905
+ await fn.call(nextClient[call.domain], cloneCdpParams(call.params));
906
+ }
907
+ }
908
+ for (const subscription of eventSubscriptions) {
909
+ const fn = nextClient?.[subscription.domain]?.[subscription.event];
910
+ if (typeof fn === "function") {
911
+ fn.call(nextClient[subscription.domain], subscription.listener);
912
+ }
913
+ }
914
+ }
915
+
916
+ async function reconnectClient() {
917
+ if (typeof reconnect !== "function") return null;
918
+ if (!reconnectInFlight) {
919
+ reconnectInFlight = Promise.resolve()
920
+ .then(() => reconnect())
921
+ .then(async (nextClient) => {
922
+ if (!nextClient) throw new Error("CDP reconnect returned no client");
923
+ currentClient = nextClient;
924
+ await replaySessionSetup(nextClient);
925
+ return nextClient;
926
+ })
927
+ .finally(() => {
928
+ reconnectInFlight = null;
929
+ });
930
+ }
931
+ return reconnectInFlight;
932
+ }
933
+
934
+ async function invokeWithReconnect({
935
+ methodNameForLog,
936
+ invoke,
937
+ retryable = true
938
+ }) {
939
+ recordMethod(methodLog, methodNameForLog);
940
+ try {
941
+ return await invoke(currentClient);
942
+ } catch (error) {
943
+ if (!retryable || !isClosedCdpTransportError(error) || typeof reconnect !== "function") {
944
+ throw error;
945
+ }
946
+ await reconnectClient();
947
+ recordMethod(methodLog, `${methodNameForLog}:retry_after_reconnect`);
948
+ return invoke(currentClient);
949
+ }
950
+ }
951
+
952
+ return new Proxy({}, {
953
+ get(_target, property, receiver) {
954
+ if (property === "send") {
955
+ return async (method, params = {}) => {
956
+ if (isForbiddenMethod(method)) {
957
+ throw new Error(`Forbidden CDP method blocked: ${method}`);
958
+ }
959
+ return invokeWithReconnect({
960
+ methodNameForLog: method,
961
+ invoke: (activeClient) => activeClient.send(method, params)
962
+ });
963
+ };
964
+ }
965
+
966
+ if (property === "close") {
967
+ return async () => currentClient?.close?.();
968
+ }
969
+
970
+ if (property === "__rawClient") return currentClient;
971
+
972
+ const value = Reflect.get(currentClient, property, receiver);
973
+ if (!value || typeof value !== "object") return value;
974
+
975
+ return new Proxy({}, {
976
+ get(_domainTarget, method, domainReceiver) {
977
+ const domainTarget = Reflect.get(currentClient, property, receiver);
978
+ const domainValue = Reflect.get(domainTarget, method, domainReceiver);
979
+ if (typeof domainValue !== "function") return domainValue;
980
+
981
+ return (params = {}) => {
982
+ const fullMethod = methodName(property, method);
983
+ if (isForbiddenMethod(fullMethod)) {
984
+ throw new Error(`Forbidden CDP method blocked: ${fullMethod}`);
985
+ }
986
+ if (typeof params === "function") {
987
+ eventSubscriptions.push({
988
+ domain: property,
989
+ event: method,
990
+ listener: params
991
+ });
992
+ recordMethod(methodLog, fullMethod);
993
+ return domainValue.call(domainTarget, params);
994
+ }
995
+ if (shouldReplayCdpSetupCall(property, method)) {
996
+ setupCalls.push({
997
+ domain: property,
998
+ method,
999
+ params: cloneCdpParams(params)
1000
+ });
1001
+ }
1002
+ return invokeWithReconnect({
1003
+ methodNameForLog: fullMethod,
1004
+ invoke: (activeClient) => {
1005
+ const activeDomain = activeClient?.[property];
1006
+ const activeMethod = activeDomain?.[method];
1007
+ if (typeof activeMethod !== "function") {
1008
+ throw new Error(`CDP method is unavailable after reconnect: ${fullMethod}`);
1009
+ }
1010
+ return activeMethod.call(activeDomain, params);
1011
+ }
1012
+ });
1013
+ };
1014
+ }
1015
+ });
1016
+ }
1017
+ });
1018
+ }
1019
+
1020
+ export async function listChromeTargets({
1021
+ host = DEFAULT_CHROME_HOST,
1022
+ port = DEFAULT_CHROME_PORT
1023
+ } = {}) {
1024
+ return CDP.List({ host, port });
1025
+ }
1026
+
1027
+ export async function connectToChromeTarget({
1028
+ host = DEFAULT_CHROME_HOST,
1029
+ port = DEFAULT_CHROME_PORT,
1030
+ targetUrlIncludes,
1031
+ targetPredicate
1032
+ } = {}) {
1033
+ const targets = await listChromeTargets({ host, port });
1034
+ const matcher = normalizeTargetMatcher({ targetUrlIncludes, targetPredicate });
1035
+ const target = targets.find(matcher);
1036
+ if (!target) {
1037
+ const urls = targets.map((item) => item.url).filter(Boolean).join("\n");
1038
+ throw new Error(`No matching Chrome target found on ${host}:${port}.\nAvailable targets:\n${urls}`);
1039
+ }
1040
+
1041
+ let rawClient = await CDP({ host, port, target });
1042
+ let activeTarget = target;
1043
+ const methodLog = [];
1044
+ const client = createGuardedCdpClient(rawClient, {
1045
+ methodLog,
1046
+ reconnect: async () => {
1047
+ const latestTargets = await listChromeTargets({ host, port });
1048
+ const nextTarget = activeTarget?.id
1049
+ ? latestTargets.find((item) => item?.id === activeTarget.id)
1050
+ : latestTargets.find(matcher);
1051
+ if (!nextTarget) {
1052
+ const urls = latestTargets.map((item) => item.url).filter(Boolean).join("\n");
1053
+ throw new Error(`No matching Chrome target found while reconnecting to ${host}:${port}.\nAvailable targets:\n${urls}`);
1054
+ }
1055
+ try {
1056
+ await rawClient.close();
1057
+ } catch {}
1058
+ rawClient = await CDP({ host, port, target: nextTarget });
1059
+ activeTarget = nextTarget;
1060
+ return rawClient;
1061
+ }
1062
+ });
1063
+
1064
+ return {
1065
+ client,
1066
+ get rawClient() {
1067
+ return rawClient;
1068
+ },
1069
+ get target() {
1070
+ return activeTarget;
1071
+ },
1072
+ methodLog,
1073
+ async close() {
1074
+ await rawClient.close();
1075
+ }
1076
+ };
1077
+ }
1078
+
1079
+ export async function assertRuntimeEvaluateBlocked(client) {
1080
+ try {
1081
+ await client.Runtime.evaluate({ expression: "1" });
1082
+ } catch (error) {
1083
+ if (/Forbidden CDP method blocked: Runtime\.evaluate/.test(String(error?.message || ""))) {
1084
+ return { blocked: true, message: error.message };
1085
+ }
1086
+ throw error;
1087
+ }
1088
+ throw new Error("Runtime.evaluate was not blocked by the CDP guard");
1089
+ }
1090
+
1091
+ export async function enableDomains(client, domains = ["Page", "DOM", "Input"]) {
1092
+ for (const domain of domains) {
1093
+ if (!ALLOWED_CDP_DOMAINS.has(domain)) {
1094
+ throw new Error(`CDP domain is not allowed by the CDP-only contract: ${domain}`);
1095
+ }
1096
+ if (typeof client?.[domain]?.enable === "function") {
1097
+ await client[domain].enable();
1098
+ }
1099
+ }
1100
+ }
1101
+
1102
+ export async function bringPageToFront(client) {
1103
+ if (typeof client?.Page?.bringToFront === "function") {
1104
+ await client.Page.bringToFront();
1105
+ }
1106
+ }
1107
+
1108
+ export async function getPageFrameTree(client) {
1109
+ const result = await client.Page.getFrameTree();
1110
+ return result.frameTree || null;
1111
+ }
1112
+
1113
+ export async function getMainFrame(client) {
1114
+ const frameTree = await getPageFrameTree(client);
1115
+ return frameTree?.frame || null;
1116
+ }
1117
+
1118
+ export async function getMainFrameUrl(client) {
1119
+ const frame = await getMainFrame(client);
1120
+ return frame?.url || "";
1121
+ }
1122
+
1123
+ export async function waitForMainFrameUrl(client, predicate, {
1124
+ timeoutMs = 10000,
1125
+ intervalMs = 250
1126
+ } = {}) {
1127
+ const started = Date.now();
1128
+ let lastUrl = "";
1129
+ while (Date.now() - started <= timeoutMs) {
1130
+ lastUrl = await getMainFrameUrl(client);
1131
+ if (predicate(lastUrl)) {
1132
+ return {
1133
+ ok: true,
1134
+ elapsed_ms: Date.now() - started,
1135
+ url: lastUrl
1136
+ };
1137
+ }
1138
+ await sleep(intervalMs);
1139
+ }
1140
+ return {
1141
+ ok: false,
1142
+ elapsed_ms: Date.now() - started,
1143
+ url: lastUrl
1144
+ };
1145
+ }
1146
+
1147
+ export async function getDocumentRoot(client, { depth = 1, pierce = true } = {}) {
1148
+ const result = await client.DOM.getDocument({ depth, pierce });
1149
+ return result.root;
1150
+ }
1151
+
1152
+ export async function querySelector(client, nodeId, selector) {
1153
+ const result = await client.DOM.querySelector({ nodeId, selector });
1154
+ return result.nodeId || 0;
1155
+ }
1156
+
1157
+ export async function querySelectorAll(client, nodeId, selector) {
1158
+ const result = await client.DOM.querySelectorAll({ nodeId, selector });
1159
+ return result.nodeIds || [];
1160
+ }
1161
+
1162
+ export async function findFirstNode(client, rootNodeId, selectors = []) {
1163
+ for (const selector of selectors) {
1164
+ const nodeId = await querySelector(client, rootNodeId, selector);
1165
+ if (nodeId) return { selector, nodeId };
1166
+ }
1167
+ return null;
1168
+ }
1169
+
1170
+ export async function describeNode(client, nodeId, { depth = 1, pierce = true } = {}) {
1171
+ const result = await client.DOM.describeNode({ nodeId, depth, pierce });
1172
+ return result.node;
1173
+ }
1174
+
1175
+ export async function getFrameDocumentNodeId(client, iframeNodeId) {
1176
+ const node = await describeNode(client, iframeNodeId, { depth: 1, pierce: true });
1177
+ const documentNodeId = node?.contentDocument?.nodeId;
1178
+ if (!documentNodeId) {
1179
+ throw new Error(`Node ${iframeNodeId} does not expose a contentDocument node`);
1180
+ }
1181
+ return documentNodeId;
1182
+ }
1183
+
1184
+ export async function findIframeDocument(client, rootNodeId, selectors = []) {
1185
+ const iframe = await findFirstNode(client, rootNodeId, selectors);
1186
+ if (!iframe) return null;
1187
+ const documentNodeId = await getFrameDocumentNodeId(client, iframe.nodeId);
1188
+ return { ...iframe, documentNodeId };
1189
+ }
1190
+
1191
+ export async function getAttributesMap(client, nodeId) {
1192
+ const result = await client.DOM.getAttributes({ nodeId });
1193
+ const attributes = {};
1194
+ const raw = result.attributes || [];
1195
+ for (let index = 0; index < raw.length; index += 2) {
1196
+ attributes[raw[index]] = raw[index + 1] || "";
1197
+ }
1198
+ return attributes;
1199
+ }
1200
+
1201
+ export async function getOuterHTML(client, nodeId) {
1202
+ const result = await client.DOM.getOuterHTML({ nodeId });
1203
+ return result.outerHTML || "";
1204
+ }
1205
+
1206
+ export async function getNodeBox(client, nodeId) {
1207
+ const result = await client.DOM.getBoxModel({ nodeId });
1208
+ const model = result.model;
1209
+ const quad = model.border?.length ? model.border : model.content;
1210
+ const xs = [quad[0], quad[2], quad[4], quad[6]];
1211
+ const ys = [quad[1], quad[3], quad[5], quad[7]];
1212
+ const minX = Math.min(...xs);
1213
+ const maxX = Math.max(...xs);
1214
+ const minY = Math.min(...ys);
1215
+ const maxY = Math.max(...ys);
1216
+ return {
1217
+ model,
1218
+ center: {
1219
+ x: (minX + maxX) / 2,
1220
+ y: (minY + maxY) / 2
1221
+ },
1222
+ rect: {
1223
+ x: minX,
1224
+ y: minY,
1225
+ width: maxX - minX,
1226
+ height: maxY - minY
1227
+ }
1228
+ };
1229
+ }
1230
+
1231
+ export async function simulateHumanClick(client, targetX, targetY, {
1232
+ button = "left",
1233
+ clickCount = 1,
1234
+ delayMs = 80,
1235
+ random = Math.random,
1236
+ sleepFn = sleep,
1237
+ moveSteps = 18,
1238
+ moveJitterPx = 3,
1239
+ hoverJitterPx = 5,
1240
+ moveDelayMinMs = 5,
1241
+ moveDelayMaxMs = 23,
1242
+ hoverDelayMinMs = 10,
1243
+ hoverDelayMaxMs = 30,
1244
+ prePressBaseMs = 260,
1245
+ prePressVarianceMs = 80,
1246
+ holdVarianceMs = 30,
1247
+ startPoint = null
1248
+ } = {}) {
1249
+ const target = normalizePoint({ x: targetX, y: targetY });
1250
+ if (!target) throw new Error("simulateHumanClick requires finite target coordinates");
1251
+ const nextRandom = normalizeRandom(random);
1252
+ const interactionConfig = getHumanInteractionConfig(client) || {};
1253
+ const start = normalizePoint(startPoint)
1254
+ || normalizePoint(interactionConfig.lastMousePoint)
1255
+ || {
1256
+ x: Math.max(0, target.x + randomBetween(nextRandom, -140, 140)),
1257
+ y: Math.max(0, target.y + randomBetween(nextRandom, -90, 90))
1258
+ };
1259
+ const path = generateBezierPath(start, target, {
1260
+ steps: moveSteps,
1261
+ random: nextRandom
1262
+ });
1263
+ const sleeper = typeof sleepFn === "function" ? sleepFn : sleep;
1264
+ const moveDelayMin = Math.min(moveDelayMinMs, moveDelayMaxMs);
1265
+ const moveDelayMax = Math.max(moveDelayMinMs, moveDelayMaxMs);
1266
+ const hoverDelayMin = Math.min(hoverDelayMinMs, hoverDelayMaxMs);
1267
+ const hoverDelayMax = Math.max(hoverDelayMinMs, hoverDelayMaxMs);
1268
+ for (const point of path) {
1269
+ await client.Input.dispatchMouseEvent({
1270
+ type: "mouseMoved",
1271
+ x: Math.round(point.x + randomBetween(nextRandom, -moveJitterPx / 2, moveJitterPx / 2)),
1272
+ y: Math.round(point.y + randomBetween(nextRandom, -moveJitterPx / 2, moveJitterPx / 2)),
1273
+ button: "none"
1274
+ });
1275
+ const pauseMs = Math.round(randomBetween(nextRandom, moveDelayMin, moveDelayMax));
1276
+ if (pauseMs > 0) await sleeper(pauseMs);
1277
+ }
1278
+ const hoverSteps = randomIntegerBetween(nextRandom, 3, 6);
1279
+ for (let index = 0; index < hoverSteps; index += 1) {
1280
+ await client.Input.dispatchMouseEvent({
1281
+ type: "mouseMoved",
1282
+ x: Math.round(target.x + randomBetween(nextRandom, -hoverJitterPx / 2, hoverJitterPx / 2)),
1283
+ y: Math.round(target.y + randomBetween(nextRandom, -hoverJitterPx / 2, hoverJitterPx / 2)),
1284
+ button: "none"
1285
+ });
1286
+ const pauseMs = Math.round(randomBetween(nextRandom, hoverDelayMin, hoverDelayMax));
1287
+ if (pauseMs > 0) await sleeper(pauseMs);
1288
+ }
1289
+ const prePressMs = humanDelay(prePressBaseMs, prePressVarianceMs, {
1290
+ minMs: 0,
1291
+ maxMs: Math.max(prePressBaseMs + prePressVarianceMs * 4, prePressBaseMs),
1292
+ random: nextRandom
1293
+ });
1294
+ if (prePressMs > 0) await sleeper(prePressMs);
1295
+ await client.Input.dispatchMouseEvent({ type: "mousePressed", x: target.x, y: target.y, button, clickCount });
1296
+ const holdMs = humanDelay(delayMs, holdVarianceMs, {
1297
+ minMs: 0,
1298
+ maxMs: Math.max(delayMs + holdVarianceMs * 4, delayMs),
1299
+ random: nextRandom
1300
+ });
1301
+ if (holdMs > 0) await sleeper(holdMs);
1302
+ await client.Input.dispatchMouseEvent({ type: "mouseReleased", x: target.x, y: target.y, button, clickCount });
1303
+ const latestConfig = getHumanInteractionConfig(client);
1304
+ if (latestConfig) latestConfig.lastMousePoint = target;
1305
+ return {
1306
+ mode: "human",
1307
+ path_points: path.length,
1308
+ hover_steps: hoverSteps,
1309
+ pre_press_ms: prePressMs,
1310
+ hold_ms: holdMs
1311
+ };
1312
+ }
1313
+
1314
+ export function resolveHumanClickPointForBox(box, {
1315
+ enabled = true,
1316
+ safeClickPointEnabled = true,
1317
+ random = Math.random,
1318
+ safeClickMinWidth = 44,
1319
+ safeClickMinHeight = 28,
1320
+ safeClickInsetRatio = 0.22,
1321
+ safeClickMinInsetPx = 4,
1322
+ safeClickMaxInsetPx = 18
1323
+ } = {}) {
1324
+ const center = normalizePoint(box?.center);
1325
+ if (!center) throw new Error("resolveHumanClickPointForBox requires a box center");
1326
+ const rect = box?.rect || {};
1327
+ const width = Number(rect.width);
1328
+ const height = Number(rect.height);
1329
+ const originX = Number(rect.x);
1330
+ const originY = Number(rect.y);
1331
+ if (
1332
+ enabled !== true
1333
+ || safeClickPointEnabled === false
1334
+ || !Number.isFinite(width)
1335
+ || !Number.isFinite(height)
1336
+ || !Number.isFinite(originX)
1337
+ || !Number.isFinite(originY)
1338
+ || width < Math.max(1, Number(safeClickMinWidth) || 44)
1339
+ || height < Math.max(1, Number(safeClickMinHeight) || 28)
1340
+ ) {
1341
+ return {
1342
+ x: center.x,
1343
+ y: center.y,
1344
+ mode: "center",
1345
+ reason: "small_or_disabled"
1346
+ };
1347
+ }
1348
+
1349
+ const nextRandom = normalizeRandom(random);
1350
+ const insetRatio = clampNumber(safeClickInsetRatio, 0.05, 0.45);
1351
+ const minInset = Math.max(0, Number(safeClickMinInsetPx) || 0);
1352
+ const maxInset = Math.max(minInset, Number(safeClickMaxInsetPx) || minInset);
1353
+ const insetX = Math.min(width / 2 - 1, Math.max(minInset, Math.min(maxInset, width * insetRatio)));
1354
+ const insetY = Math.min(height / 2 - 1, Math.max(minInset, Math.min(maxInset, height * insetRatio)));
1355
+ const usableWidth = Math.max(0, width - insetX * 2);
1356
+ const usableHeight = Math.max(0, height - insetY * 2);
1357
+ if (usableWidth <= 0 || usableHeight <= 0) {
1358
+ return {
1359
+ x: center.x,
1360
+ y: center.y,
1361
+ mode: "center",
1362
+ reason: "insufficient_safe_area"
1363
+ };
1364
+ }
1365
+ return {
1366
+ x: originX + insetX + nextRandom() * usableWidth,
1367
+ y: originY + insetY + nextRandom() * usableHeight,
1368
+ mode: "safe_inset",
1369
+ inset_x: insetX,
1370
+ inset_y: insetY
1371
+ };
1372
+ }
1373
+
1374
+ export async function clickPoint(client, x, y, {
1375
+ button = "left",
1376
+ clickCount = 1,
1377
+ delayMs = 80,
1378
+ humanRestEnabled = null,
1379
+ humanInteraction = null
1380
+ } = {}) {
1381
+ const configured = getHumanInteractionConfig(client);
1382
+ const mergedHumanInteraction = {
1383
+ ...(configured || {}),
1384
+ ...(humanInteraction || {})
1385
+ };
1386
+ const humanEnabled = humanRestEnabled === true
1387
+ || humanInteraction?.enabled === true
1388
+ || (humanRestEnabled !== false && configured?.enabled === true);
1389
+ if (humanEnabled && mergedHumanInteraction.clickMovementEnabled !== false) {
1390
+ return simulateHumanClick(client, x, y, {
1391
+ ...mergedHumanInteraction,
1392
+ button,
1393
+ clickCount,
1394
+ delayMs
1395
+ });
1396
+ }
1397
+ await client.Input.dispatchMouseEvent({ type: "mouseMoved", x, y, button: "none" });
1398
+ await client.Input.dispatchMouseEvent({ type: "mousePressed", x, y, button, clickCount });
1399
+ if (delayMs > 0) await sleep(delayMs);
1400
+ await client.Input.dispatchMouseEvent({ type: "mouseReleased", x, y, button, clickCount });
1401
+ return {
1402
+ mode: "direct"
1403
+ };
1404
+ }
1405
+
1406
+ export async function scrollNodeIntoView(client, nodeId) {
1407
+ await client.DOM.scrollIntoViewIfNeeded({ nodeId });
1408
+ }
1409
+
1410
+ export async function clickNodeCenter(client, nodeId, {
1411
+ scrollIntoView = false,
1412
+ ...clickOptions
1413
+ } = {}) {
1414
+ if (scrollIntoView) {
1415
+ await scrollNodeIntoView(client, nodeId);
1416
+ await sleep(150);
1417
+ }
1418
+ const box = await getNodeBox(client, nodeId);
1419
+ const configured = getHumanInteractionConfig(client);
1420
+ const mergedHumanInteraction = {
1421
+ ...(configured || {}),
1422
+ ...(clickOptions.humanInteraction || {})
1423
+ };
1424
+ const humanClickPointEnabled = (
1425
+ clickOptions.humanRestEnabled === true
1426
+ || clickOptions.humanInteraction?.enabled === true
1427
+ || (clickOptions.humanRestEnabled !== false && configured?.enabled === true)
1428
+ ) && mergedHumanInteraction.safeClickPointEnabled !== false;
1429
+ const clickPointTarget = humanClickPointEnabled
1430
+ ? resolveHumanClickPointForBox(box, mergedHumanInteraction)
1431
+ : { ...box.center, mode: "center" };
1432
+ const clickResult = await clickPoint(client, clickPointTarget.x, clickPointTarget.y, clickOptions);
1433
+ return {
1434
+ ...box,
1435
+ click_target: clickPointTarget,
1436
+ click_result: clickResult
1437
+ };
1438
+ }
1439
+
1440
+ export async function pressKey(client, key, {
1441
+ code = key,
1442
+ windowsVirtualKeyCode,
1443
+ nativeVirtualKeyCode = windowsVirtualKeyCode,
1444
+ text = "",
1445
+ modifiers = 0
1446
+ } = {}) {
1447
+ await client.Input.dispatchKeyEvent({
1448
+ type: "keyDown",
1449
+ key,
1450
+ code,
1451
+ windowsVirtualKeyCode,
1452
+ nativeVirtualKeyCode,
1453
+ text,
1454
+ modifiers
1455
+ });
1456
+ await client.Input.dispatchKeyEvent({
1457
+ type: "keyUp",
1458
+ key,
1459
+ code,
1460
+ windowsVirtualKeyCode,
1461
+ nativeVirtualKeyCode,
1462
+ modifiers
1463
+ });
1464
+ }
1465
+
1466
+ export function chunkHumanText(text, {
1467
+ random = Math.random,
1468
+ minLength = 1,
1469
+ maxLength = 5
1470
+ } = {}) {
1471
+ const chars = Array.from(String(text || ""));
1472
+ const min = Math.max(1, Math.floor(Number(minLength) || 1));
1473
+ const max = Math.max(min, Math.floor(Number(maxLength) || min));
1474
+ const nextRandom = normalizeRandom(random);
1475
+ const chunks = [];
1476
+ let index = 0;
1477
+ while (index < chars.length) {
1478
+ const remaining = chars.length - index;
1479
+ const size = Math.min(remaining, randomIntegerBetween(nextRandom, min, max));
1480
+ chunks.push(chars.slice(index, index + size).join(""));
1481
+ index += size;
1482
+ }
1483
+ return chunks;
1484
+ }
1485
+
1486
+ export async function insertText(client, text, {
1487
+ humanTextEntryEnabled = null,
1488
+ humanInteraction = null
1489
+ } = {}) {
1490
+ const value = String(text || "");
1491
+ const configured = getHumanInteractionConfig(client);
1492
+ const mergedHumanInteraction = {
1493
+ ...(configured || {}),
1494
+ ...(humanInteraction || {})
1495
+ };
1496
+ const textEntryEnabled = humanTextEntryEnabled === true
1497
+ || humanInteraction?.textEntryEnabled === true
1498
+ || (humanTextEntryEnabled !== false
1499
+ && configured?.enabled === true
1500
+ && configured?.textEntryEnabled !== false);
1501
+ if (!textEntryEnabled || value.length <= 1) {
1502
+ await client.Input.insertText({ text: value });
1503
+ return {
1504
+ mode: "direct",
1505
+ chunk_count: value ? 1 : 0
1506
+ };
1507
+ }
1508
+ const chunks = chunkHumanText(value, {
1509
+ random: mergedHumanInteraction.random,
1510
+ minLength: mergedHumanInteraction.textChunkMinLength,
1511
+ maxLength: mergedHumanInteraction.textChunkMaxLength
1512
+ });
1513
+ const sleeper = typeof mergedHumanInteraction.sleepFn === "function"
1514
+ ? mergedHumanInteraction.sleepFn
1515
+ : sleep;
1516
+ for (let index = 0; index < chunks.length; index += 1) {
1517
+ await client.Input.insertText({ text: chunks[index] });
1518
+ if (index < chunks.length - 1) {
1519
+ const pauseMs = humanDelay(
1520
+ mergedHumanInteraction.textChunkDelayBaseMs,
1521
+ mergedHumanInteraction.textChunkDelayVarianceMs,
1522
+ {
1523
+ minMs: 0,
1524
+ maxMs: Math.max(
1525
+ mergedHumanInteraction.textChunkDelayBaseMs + mergedHumanInteraction.textChunkDelayVarianceMs * 4,
1526
+ mergedHumanInteraction.textChunkDelayBaseMs
1527
+ ),
1528
+ random: mergedHumanInteraction.random
1529
+ }
1530
+ );
1531
+ if (pauseMs > 0) await sleeper(pauseMs);
1532
+ }
1533
+ }
1534
+ return {
1535
+ mode: "chunked",
1536
+ chunk_count: chunks.length,
1537
+ chunks
1538
+ };
1539
+ }
1540
+
1541
+ export async function selectAllFocusedText(client) {
1542
+ await pressKey(client, "a", {
1543
+ code: "KeyA",
1544
+ windowsVirtualKeyCode: 65,
1545
+ nativeVirtualKeyCode: 65,
1546
+ modifiers: 2
1547
+ });
1548
+ }
1549
+
1550
+ export async function clearFocusedInput(client) {
1551
+ await selectAllFocusedText(client);
1552
+ await pressKey(client, "Backspace", {
1553
+ code: "Backspace",
1554
+ windowsVirtualKeyCode: 8,
1555
+ nativeVirtualKeyCode: 8
1556
+ });
1557
+ }
1558
+
1559
+ export async function waitForSelector(client, nodeId, selector, {
1560
+ timeoutMs = 5000,
1561
+ intervalMs = 150
1562
+ } = {}) {
1563
+ const started = Date.now();
1564
+ while (Date.now() - started <= timeoutMs) {
1565
+ const foundNodeId = await querySelector(client, nodeId, selector);
1566
+ if (foundNodeId) return foundNodeId;
1567
+ await sleep(intervalMs);
1568
+ }
1569
+ return 0;
1570
+ }
1571
+
1572
+ export async function countSelectors(client, nodeId, selectors = {}) {
1573
+ const counts = {};
1574
+ for (const [name, selector] of Object.entries(selectors)) {
1575
+ counts[name] = (await querySelectorAll(client, nodeId, selector)).length;
1576
+ }
1577
+ return counts;
1578
+ }
1579
+
1580
+ export async function getAccessibilityTree(client, options = {}) {
1581
+ return client.Accessibility.getFullAXTree(options);
1582
+ }
1583
+
1584
+ export async function sleep(ms) {
1585
+ await new Promise((resolve) => setTimeout(resolve, ms));
1586
+ }