@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/bin/boss-recommend-mcp.js +4 -4
- package/config/screening-config.example.json +33 -33
- package/package.json +8 -8
- package/scripts/install-macos.sh +280 -280
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +42 -42
- package/skills/boss-chat/SKILL.md +106 -106
- package/skills/boss-recommend-pipeline/README.md +13 -13
- package/skills/boss-recommend-pipeline/SKILL.md +219 -214
- package/skills/boss-recruit-pipeline/README.md +19 -19
- package/skills/boss-recruit-pipeline/SKILL.md +89 -89
- package/src/chat-mcp.js +127 -127
- package/src/chat-runtime-config.js +775 -775
- package/src/cli.js +573 -573
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +2419 -2415
- package/src/core/capture/index.js +1201 -1201
- package/src/core/cv-acquisition/index.js +238 -238
- package/src/core/cv-capture-target/index.js +299 -299
- package/src/core/greet-quota/index.js +71 -71
- package/src/core/infinite-list/index.js +1326 -1326
- package/src/core/reporting/legacy-csv.js +334 -332
- package/src/core/run/index.js +32 -32
- package/src/core/run/timing.js +33 -33
- package/src/core/screening/index.js +2135 -2135
- package/src/core/self-heal/index.js +973 -973
- package/src/core/self-heal/viewport.js +564 -564
- package/src/detached-worker.js +99 -99
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +9 -9
- package/src/domains/chat/detail.js +113 -113
- package/src/domains/chat/index.js +7 -7
- package/src/domains/chat/jobs.js +620 -620
- package/src/domains/chat/page-guard.js +122 -122
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +571 -571
- package/src/domains/common/account-rights-panel.js +314 -314
- package/src/domains/common/recovery-settle.js +159 -159
- package/src/domains/recommend/actions.js +472 -472
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/colleague-contact.js +333 -333
- package/src/domains/recommend/constants.js +228 -159
- package/src/domains/recommend/detail.js +650 -650
- package/src/domains/recommend/filters.js +748 -377
- package/src/domains/recommend/index.js +4 -3
- package/src/domains/recommend/jobs.js +542 -542
- package/src/domains/recommend/location.js +736 -0
- package/src/domains/recommend/refresh.js +504 -361
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +987 -854
- package/src/domains/recommend/scopes.js +246 -246
- package/src/domains/recruit/actions.js +277 -277
- package/src/domains/recruit/cards.js +74 -74
- package/src/domains/recruit/constants.js +236 -236
- package/src/domains/recruit/detail.js +588 -588
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +866 -866
- package/src/domains/recruit/refresh.js +45 -45
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1620 -1620
- package/src/domains/recruit/search.js +3229 -3229
- package/src/index.js +13 -0
- package/src/parser.js +376 -8
- package/src/recommend-mcp.js +929 -915
- package/src/recommend-scheduler.js +496 -496
- package/src/recruit-mcp.js +2121 -2121
|
@@ -1,2417 +1,2421 @@
|
|
|
1
|
-
import { execFile, 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
|
-
export const DEFAULT_REQUIRED_CHROME_FLAGS = LID_CLOSED_SAFE_CHROME_ARGS;
|
|
17
|
-
|
|
18
|
-
export const ALLOWED_CDP_DOMAINS = new Set([
|
|
19
|
-
"Accessibility",
|
|
20
|
-
"Browser",
|
|
21
|
-
"DOM",
|
|
22
|
-
"Input",
|
|
23
|
-
"Network",
|
|
24
|
-
"Page",
|
|
25
|
-
"Target"
|
|
26
|
-
]);
|
|
27
|
-
|
|
1
|
+
import { execFile, 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
|
+
export const DEFAULT_REQUIRED_CHROME_FLAGS = LID_CLOSED_SAFE_CHROME_ARGS;
|
|
17
|
+
|
|
18
|
+
export const ALLOWED_CDP_DOMAINS = new Set([
|
|
19
|
+
"Accessibility",
|
|
20
|
+
"Browser",
|
|
21
|
+
"DOM",
|
|
22
|
+
"Input",
|
|
23
|
+
"Network",
|
|
24
|
+
"Page",
|
|
25
|
+
"Target"
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
28
|
export const FORBIDDEN_CDP_DOMAINS = new Set(["Runtime"]);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
".
|
|
39
|
-
".
|
|
40
|
-
".
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
return fallback;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
rawObject = {
|
|
217
|
-
source = "
|
|
218
|
-
} else if (
|
|
219
|
-
rawObject = raw;
|
|
220
|
-
source = "
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
Object.assign(profileDefaults, HUMAN_BEHAVIOR_PROFILES.
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (
|
|
251
|
-
profileDefaults.enabled =
|
|
252
|
-
}
|
|
253
|
-
if (!hasExplicitEnabled && (safePacingFlag ===
|
|
254
|
-
profileDefaults.enabled =
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
29
|
+
export const FORBIDDEN_CDP_METHODS = new Set([
|
|
30
|
+
"Page.addScriptToEvaluateOnNewDocument"
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const BOSS_LOGIN_URL_PATTERN = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com|login\.zhipin\.com)/i;
|
|
34
|
+
const BOSS_LOGIN_TEXT_PATTERN = /扫码登录|验证码登录|密码登录|登录后|请登录|登录BOSS直聘|Boss登录|BOSS登录/i;
|
|
35
|
+
const CHROME_DEBUG_UNAVAILABLE_PATTERN = /ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|connect|socket hang up/i;
|
|
36
|
+
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;
|
|
37
|
+
const BOSS_LOGIN_DOM_SELECTORS = [
|
|
38
|
+
".login-box",
|
|
39
|
+
".login-form",
|
|
40
|
+
".login-dialog",
|
|
41
|
+
".sign-form",
|
|
42
|
+
".qrcode-box",
|
|
43
|
+
".user-login",
|
|
44
|
+
"input[name='phone']",
|
|
45
|
+
"input[placeholder*='手机号']",
|
|
46
|
+
"input[placeholder*='验证码']"
|
|
47
|
+
];
|
|
48
|
+
const HUMAN_INTERACTION_CONFIG = new WeakMap();
|
|
49
|
+
const DEFAULT_HUMAN_BEHAVIOR_PROFILE = "paced_with_rests";
|
|
50
|
+
export const DETERMINISTIC_CLICK_OPTIONS = Object.freeze({
|
|
51
|
+
humanRestEnabled: false
|
|
52
|
+
});
|
|
53
|
+
const HUMAN_BEHAVIOR_PROFILES = Object.freeze({
|
|
54
|
+
baseline: Object.freeze({
|
|
55
|
+
enabled: false,
|
|
56
|
+
clickMovement: false,
|
|
57
|
+
textEntry: false,
|
|
58
|
+
listScrollJitter: false,
|
|
59
|
+
shortRest: false,
|
|
60
|
+
batchRest: false,
|
|
61
|
+
actionCooldown: false
|
|
62
|
+
}),
|
|
63
|
+
paced: Object.freeze({
|
|
64
|
+
enabled: true,
|
|
65
|
+
clickMovement: true,
|
|
66
|
+
textEntry: true,
|
|
67
|
+
listScrollJitter: true,
|
|
68
|
+
shortRest: false,
|
|
69
|
+
batchRest: false,
|
|
70
|
+
actionCooldown: true
|
|
71
|
+
}),
|
|
72
|
+
paced_with_rests: Object.freeze({
|
|
73
|
+
enabled: true,
|
|
74
|
+
clickMovement: true,
|
|
75
|
+
textEntry: true,
|
|
76
|
+
listScrollJitter: true,
|
|
77
|
+
shortRest: true,
|
|
78
|
+
batchRest: true,
|
|
79
|
+
actionCooldown: true
|
|
80
|
+
})
|
|
81
|
+
});
|
|
82
|
+
const HUMAN_BEHAVIOR_PROFILE_ALIASES = Object.freeze({
|
|
83
|
+
off: "baseline",
|
|
84
|
+
disabled: "baseline",
|
|
85
|
+
deterministic: "baseline",
|
|
86
|
+
safe: "paced",
|
|
87
|
+
safe_pacing: "paced",
|
|
88
|
+
paced_with_rest: "paced_with_rests",
|
|
89
|
+
rests: "paced_with_rests",
|
|
90
|
+
rest: "paced_with_rests"
|
|
91
|
+
});
|
|
92
|
+
const DEFAULT_HUMAN_REST_LEVEL = "low";
|
|
93
|
+
const HUMAN_REST_LEVEL_ALIASES = Object.freeze({
|
|
94
|
+
default: "low",
|
|
95
|
+
light: "low",
|
|
96
|
+
normal: "medium",
|
|
97
|
+
med: "medium",
|
|
98
|
+
heavy: "high"
|
|
99
|
+
});
|
|
100
|
+
const HUMAN_REST_LEVEL_PROFILES = Object.freeze({
|
|
101
|
+
medium: Object.freeze({
|
|
102
|
+
targetRestMs: 30 * 60 * 1000,
|
|
103
|
+
targetCandidateCount: 700,
|
|
104
|
+
targetWindowMs: 5 * 60 * 60 * 1000,
|
|
105
|
+
intervalMin: 4,
|
|
106
|
+
intervalMax: 16,
|
|
107
|
+
longRestProbability: 0.22,
|
|
108
|
+
shortRestMinMs: 8000,
|
|
109
|
+
shortRestMaxMs: 45000,
|
|
110
|
+
longRestMinMs: 60000,
|
|
111
|
+
longRestMaxMs: 180000,
|
|
112
|
+
minDebtToRestMs: 8000,
|
|
113
|
+
forceDebtMs: 90000,
|
|
114
|
+
maxOverspendMs: 15000
|
|
115
|
+
}),
|
|
116
|
+
high: Object.freeze({
|
|
117
|
+
targetRestMs: 60 * 60 * 1000,
|
|
118
|
+
targetCandidateCount: 700,
|
|
119
|
+
targetWindowMs: 5 * 60 * 60 * 1000,
|
|
120
|
+
intervalMin: 3,
|
|
121
|
+
intervalMax: 12,
|
|
122
|
+
longRestProbability: 0.28,
|
|
123
|
+
shortRestMinMs: 12000,
|
|
124
|
+
shortRestMaxMs: 75000,
|
|
125
|
+
longRestMinMs: 90000,
|
|
126
|
+
longRestMaxMs: 300000,
|
|
127
|
+
minDebtToRestMs: 12000,
|
|
128
|
+
forceDebtMs: 150000,
|
|
129
|
+
maxOverspendMs: 25000
|
|
130
|
+
})
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
function clampNumber(value, min, max) {
|
|
134
|
+
const number = Number(value);
|
|
135
|
+
if (!Number.isFinite(number)) return min;
|
|
136
|
+
return Math.min(max, Math.max(min, number));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function randomBetween(random, min, max) {
|
|
140
|
+
const lower = Number(min) || 0;
|
|
141
|
+
const upper = Number(max) || lower;
|
|
142
|
+
if (upper <= lower) return lower;
|
|
143
|
+
return lower + random() * (upper - lower);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function randomIntegerBetween(random, min, max) {
|
|
147
|
+
return Math.floor(randomBetween(random, min, max + 1));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizePoint(point) {
|
|
151
|
+
const x = Number(point?.x);
|
|
152
|
+
const y = Number(point?.y);
|
|
153
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
154
|
+
return { x, y };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function normalizeRandom(random) {
|
|
158
|
+
return typeof random === "function" ? random : Math.random;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getHumanInteractionConfig(client) {
|
|
162
|
+
return HUMAN_INTERACTION_CONFIG.get(client) || null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function normalizeBooleanOption(raw, fallback = null) {
|
|
166
|
+
if (typeof raw === "boolean") return raw;
|
|
167
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return raw !== 0;
|
|
168
|
+
const normalized = String(raw ?? "").trim().toLowerCase();
|
|
169
|
+
if (!normalized) return fallback;
|
|
170
|
+
if (["true", "1", "yes", "y", "on", "enabled"].includes(normalized)) return true;
|
|
171
|
+
if (["false", "0", "no", "n", "off", "disabled"].includes(normalized)) return false;
|
|
172
|
+
return fallback;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readFirstOption(source, keys = []) {
|
|
176
|
+
if (!source || typeof source !== "object") return undefined;
|
|
177
|
+
for (const key of keys) {
|
|
178
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) return source[key];
|
|
179
|
+
}
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function normalizeFeatureBoolean(raw, fallback) {
|
|
184
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
185
|
+
return normalizeBooleanOption(readFirstOption(raw, ["enabled", "enable"]), fallback);
|
|
186
|
+
}
|
|
187
|
+
return normalizeBooleanOption(raw, fallback);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function normalizeHumanBehaviorProfile(raw, fallback = "baseline") {
|
|
191
|
+
const normalized = String(raw || "").trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
192
|
+
const profile = HUMAN_BEHAVIOR_PROFILE_ALIASES[normalized] || normalized;
|
|
193
|
+
return Object.prototype.hasOwnProperty.call(HUMAN_BEHAVIOR_PROFILES, profile)
|
|
194
|
+
? profile
|
|
195
|
+
: fallback;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function normalizeHumanRestLevel(raw, fallback = DEFAULT_HUMAN_REST_LEVEL) {
|
|
199
|
+
const normalized = String(raw || "").trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
200
|
+
const level = HUMAN_REST_LEVEL_ALIASES[normalized] || normalized;
|
|
201
|
+
return level === "low" || level === "medium" || level === "high"
|
|
202
|
+
? level
|
|
203
|
+
: fallback;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function normalizeHumanBehaviorOptions(raw = null, {
|
|
207
|
+
legacyEnabled = false,
|
|
208
|
+
safePacing = null,
|
|
209
|
+
batchRestEnabled = null
|
|
210
|
+
} = {}) {
|
|
211
|
+
const safePacingFlag = normalizeBooleanOption(safePacing, null);
|
|
212
|
+
const batchRestFlag = normalizeBooleanOption(batchRestEnabled, null);
|
|
213
|
+
let source = "default";
|
|
214
|
+
let rawObject = {};
|
|
215
|
+
if (typeof raw === "boolean") {
|
|
216
|
+
rawObject = { enabled: raw };
|
|
217
|
+
source = "boolean";
|
|
218
|
+
} else if (typeof raw === "string") {
|
|
219
|
+
rawObject = { profile: raw };
|
|
220
|
+
source = "profile";
|
|
221
|
+
} else if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
222
|
+
rawObject = raw;
|
|
223
|
+
source = "object";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const explicitProfile = readFirstOption(rawObject, ["profile", "mode", "behaviorProfile", "behavior_profile"]);
|
|
227
|
+
const enabledRaw = readFirstOption(rawObject, ["enabled", "enable", "human_behavior_enabled"]);
|
|
228
|
+
const explicitEnabled = normalizeBooleanOption(enabledRaw, null);
|
|
229
|
+
const inferredProfile = (raw === true || explicitEnabled === true) && legacyEnabled !== true && batchRestFlag !== true
|
|
230
|
+
? "paced"
|
|
231
|
+
: legacyEnabled === true || batchRestFlag === true
|
|
232
|
+
? "paced_with_rests"
|
|
233
|
+
: safePacingFlag === true
|
|
234
|
+
? "paced"
|
|
235
|
+
: DEFAULT_HUMAN_BEHAVIOR_PROFILE;
|
|
236
|
+
const profile = normalizeHumanBehaviorProfile(explicitProfile, inferredProfile);
|
|
237
|
+
const profileDefaults = {
|
|
238
|
+
...HUMAN_BEHAVIOR_PROFILES[profile]
|
|
239
|
+
};
|
|
240
|
+
if (legacyEnabled === true && !explicitProfile) {
|
|
241
|
+
Object.assign(profileDefaults, HUMAN_BEHAVIOR_PROFILES.paced_with_rests);
|
|
242
|
+
} else if (safePacingFlag === true && !explicitProfile) {
|
|
243
|
+
Object.assign(profileDefaults, HUMAN_BEHAVIOR_PROFILES.paced);
|
|
244
|
+
}
|
|
245
|
+
if (batchRestFlag === true && !explicitProfile) {
|
|
246
|
+
Object.assign(profileDefaults, HUMAN_BEHAVIOR_PROFILES.paced_with_rests);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const hasExplicitEnabled = enabledRaw !== undefined;
|
|
250
|
+
if (hasExplicitEnabled) {
|
|
251
|
+
profileDefaults.enabled = normalizeBooleanOption(enabledRaw, profileDefaults.enabled);
|
|
252
|
+
}
|
|
253
|
+
if (!hasExplicitEnabled && (safePacingFlag === false || batchRestFlag === false) && !explicitProfile && legacyEnabled !== true) {
|
|
254
|
+
profileDefaults.enabled = false;
|
|
255
|
+
}
|
|
256
|
+
if (!hasExplicitEnabled && (safePacingFlag === true || batchRestFlag === true || legacyEnabled === true)) {
|
|
257
|
+
profileDefaults.enabled = true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const enabled = profileDefaults.enabled === true;
|
|
261
|
+
const clickMovement = normalizeFeatureBoolean(
|
|
262
|
+
readFirstOption(rawObject, ["clickMovement", "click_movement", "click_movement_enabled"]),
|
|
263
|
+
profileDefaults.clickMovement
|
|
264
|
+
);
|
|
265
|
+
const textEntry = normalizeFeatureBoolean(
|
|
266
|
+
readFirstOption(rawObject, ["textEntry", "text_entry", "text_entry_enabled"]),
|
|
267
|
+
profileDefaults.textEntry
|
|
268
|
+
);
|
|
269
|
+
const listScrollJitter = normalizeFeatureBoolean(
|
|
270
|
+
readFirstOption(rawObject, ["listScrollJitter", "list_scroll_jitter", "scrollJitter", "scroll_jitter"]),
|
|
271
|
+
profileDefaults.listScrollJitter
|
|
272
|
+
);
|
|
273
|
+
const actionCooldown = normalizeFeatureBoolean(
|
|
274
|
+
readFirstOption(rawObject, ["actionCooldown", "action_cooldown", "readPause", "read_pause"]),
|
|
275
|
+
profileDefaults.actionCooldown
|
|
276
|
+
);
|
|
277
|
+
let shortRest = normalizeFeatureBoolean(
|
|
278
|
+
readFirstOption(rawObject, ["shortRest", "short_rest", "randomRest", "random_rest"]),
|
|
279
|
+
profileDefaults.shortRest
|
|
280
|
+
);
|
|
281
|
+
let batchRest = normalizeFeatureBoolean(
|
|
282
|
+
readFirstOption(rawObject, ["batchRest", "batch_rest", "batchRestEnabled", "batch_rest_enabled"]),
|
|
283
|
+
profileDefaults.batchRest
|
|
284
|
+
);
|
|
285
|
+
const restLevel = normalizeHumanRestLevel(
|
|
286
|
+
readFirstOption(rawObject, ["restLevel", "rest_level"]),
|
|
287
|
+
DEFAULT_HUMAN_REST_LEVEL
|
|
288
|
+
);
|
|
289
|
+
if (batchRestFlag !== null) {
|
|
290
|
+
batchRest = batchRestFlag;
|
|
291
|
+
if (batchRestFlag === true && readFirstOption(rawObject, ["shortRest", "short_rest", "randomRest", "random_rest"]) === undefined) {
|
|
292
|
+
shortRest = true;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
enabled,
|
|
298
|
+
profile,
|
|
299
|
+
source,
|
|
300
|
+
clickMovement: enabled && clickMovement === true,
|
|
301
|
+
textEntry: enabled && textEntry === true,
|
|
302
|
+
listScrollJitter: enabled && listScrollJitter === true,
|
|
303
|
+
shortRest: enabled && shortRest === true,
|
|
304
|
+
batchRest: enabled && batchRest === true,
|
|
305
|
+
actionCooldown: enabled && actionCooldown === true,
|
|
306
|
+
restLevel,
|
|
307
|
+
restEnabled: enabled && (shortRest === true || batchRest === true)
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function nowIso() {
|
|
312
|
+
return new Date().toISOString();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function normalizeTargetMatcher({ targetUrlIncludes, targetPredicate } = {}) {
|
|
316
|
+
if (typeof targetPredicate === "function") return targetPredicate;
|
|
317
|
+
if (targetUrlIncludes) {
|
|
318
|
+
return (target) => String(target?.url || "").includes(targetUrlIncludes);
|
|
319
|
+
}
|
|
320
|
+
return (target) => target?.type === "page";
|
|
321
|
+
}
|
|
322
|
+
|
|
320
323
|
function isForbiddenMethod(methodName) {
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const
|
|
475
|
-
const
|
|
476
|
-
const
|
|
477
|
-
const
|
|
478
|
-
const
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
const
|
|
528
|
-
const
|
|
529
|
-
const
|
|
530
|
-
const
|
|
531
|
-
const
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
state.
|
|
543
|
-
state.
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
state.
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
state.
|
|
584
|
-
state.
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
state.
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
state.
|
|
593
|
-
state.
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
state.
|
|
621
|
-
state.
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
state.
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
state.
|
|
662
|
-
state.
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
error
|
|
701
|
-
error.
|
|
702
|
-
error.
|
|
703
|
-
error.
|
|
704
|
-
error.
|
|
705
|
-
error.
|
|
706
|
-
error.
|
|
707
|
-
error.
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
process.env.
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
path.join(process.env.
|
|
795
|
-
path.join(process.env
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
"
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
"/usr/bin/google-chrome
|
|
807
|
-
"/usr/bin/
|
|
808
|
-
"/usr/bin/chromium",
|
|
809
|
-
"/
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
.
|
|
829
|
-
.
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
.
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
let
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
current
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
.
|
|
874
|
-
.
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
const
|
|
883
|
-
const
|
|
884
|
-
const
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
return
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
const
|
|
902
|
-
const
|
|
903
|
-
const
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
if (arg
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
result.
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
seen.
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
`--
|
|
944
|
-
|
|
945
|
-
"--no-
|
|
946
|
-
|
|
947
|
-
...
|
|
948
|
-
...
|
|
949
|
-
|
|
950
|
-
"--
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
const
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
.
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
:
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
let
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
"
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
"-
|
|
1062
|
-
"
|
|
1063
|
-
"
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
const
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
let
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1212
|
-
let
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
.
|
|
1242
|
-
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
error
|
|
1301
|
-
error.
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
const
|
|
1392
|
-
const
|
|
1393
|
-
const
|
|
1394
|
-
const
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
const
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
"
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
...
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
const
|
|
1556
|
-
const
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
const
|
|
1598
|
-
const
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
||
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|| (domain === "
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
let
|
|
1708
|
-
|
|
1709
|
-
const
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
if (
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
.then(
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
const
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
const
|
|
1845
|
-
const
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
let
|
|
1853
|
-
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
const
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
}
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
const
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
const
|
|
2004
|
-
const
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
wrapped
|
|
2023
|
-
wrapped.
|
|
2024
|
-
wrapped.
|
|
2025
|
-
wrapped.
|
|
2026
|
-
wrapped.
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
const
|
|
2031
|
-
const
|
|
2032
|
-
const
|
|
2033
|
-
const
|
|
2034
|
-
const
|
|
2035
|
-
const
|
|
2036
|
-
const
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
const
|
|
2074
|
-
const
|
|
2075
|
-
|
|
2076
|
-
||
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
const
|
|
2086
|
-
const
|
|
2087
|
-
const
|
|
2088
|
-
const
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
const
|
|
2149
|
-
const
|
|
2150
|
-
const
|
|
2151
|
-
const
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
||
|
|
2156
|
-
|| !Number.isFinite(
|
|
2157
|
-
|| !Number.isFinite(
|
|
2158
|
-
|| !Number.isFinite(
|
|
2159
|
-
||
|
|
2160
|
-
||
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
const
|
|
2172
|
-
const
|
|
2173
|
-
const
|
|
2174
|
-
const
|
|
2175
|
-
const
|
|
2176
|
-
const
|
|
2177
|
-
const
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
const
|
|
2204
|
-
|
|
2205
|
-
...(
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
||
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
await client.Input.dispatchMouseEvent({ type: "
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
wrapped
|
|
2233
|
-
wrapped.
|
|
2234
|
-
wrapped.
|
|
2235
|
-
wrapped.
|
|
2236
|
-
wrapped.
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
}
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
await
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
const
|
|
2251
|
-
const
|
|
2252
|
-
|
|
2253
|
-
...(
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
||
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
}
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
const
|
|
2304
|
-
const
|
|
2305
|
-
const
|
|
2306
|
-
const
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
const
|
|
2311
|
-
|
|
2312
|
-
index
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
const
|
|
2323
|
-
const
|
|
2324
|
-
|
|
2325
|
-
...(
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
||
|
|
2330
|
-
|
|
2331
|
-
&& configured?.
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
mergedHumanInteraction.
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
mergedHumanInteraction.textChunkDelayBaseMs
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
chunks
|
|
2369
|
-
|
|
2370
|
-
}
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
}
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
await
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
}
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
324
|
+
const canonicalMethod = String(methodName || "").replace(/:retry_after_reconnect$/, "");
|
|
325
|
+
const [domain] = canonicalMethod.split(".");
|
|
326
|
+
return FORBIDDEN_CDP_DOMAINS.has(domain) || FORBIDDEN_CDP_METHODS.has(canonicalMethod);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function methodName(domain, method) {
|
|
330
|
+
return `${String(domain)}.${String(method)}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function recordMethod(methodLog, method) {
|
|
334
|
+
if (Array.isArray(methodLog)) {
|
|
335
|
+
methodLog.push({ method, at: nowIso() });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function assertNoForbiddenCdpCalls(methodLog = []) {
|
|
340
|
+
const forbidden = methodLog.filter((entry) => isForbiddenMethod(entry?.method));
|
|
341
|
+
if (forbidden.length > 0) {
|
|
342
|
+
const methods = forbidden.map((entry) => entry.method).join(", ");
|
|
343
|
+
throw new Error(`Forbidden CDP methods were used: ${methods}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function humanDelay(baseMs, varianceMs, {
|
|
348
|
+
minMs = 100,
|
|
349
|
+
maxMs = 60000,
|
|
350
|
+
random = Math.random
|
|
351
|
+
} = {}) {
|
|
352
|
+
const nextRandom = normalizeRandom(random);
|
|
353
|
+
const base = Math.max(0, Number(baseMs) || 0);
|
|
354
|
+
const variance = Math.max(0, Number(varianceMs) || 0);
|
|
355
|
+
const lower = Math.max(0, Number(minMs) || 0);
|
|
356
|
+
const upper = Math.max(lower, Number(maxMs) || lower);
|
|
357
|
+
if (variance <= 0) return Math.round(clampNumber(base, lower, upper));
|
|
358
|
+
const u1 = Math.max(Number.EPSILON, Math.min(1 - Number.EPSILON, nextRandom()));
|
|
359
|
+
const u2 = Math.max(Number.EPSILON, Math.min(1 - Number.EPSILON, nextRandom()));
|
|
360
|
+
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
361
|
+
return Math.round(clampNumber(base + z * variance, lower, upper));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function generateBezierPath(start, end, {
|
|
365
|
+
steps = 18,
|
|
366
|
+
random = Math.random,
|
|
367
|
+
controlJitterX = 100,
|
|
368
|
+
controlJitterY = 60
|
|
369
|
+
} = {}) {
|
|
370
|
+
const startPoint = normalizePoint(start);
|
|
371
|
+
const endPoint = normalizePoint(end);
|
|
372
|
+
if (!startPoint || !endPoint) {
|
|
373
|
+
throw new Error("generateBezierPath requires finite start and end points");
|
|
374
|
+
}
|
|
375
|
+
const nextRandom = normalizeRandom(random);
|
|
376
|
+
const safeSteps = Math.max(1, Math.floor(Number(steps) || 18));
|
|
377
|
+
const midX = (startPoint.x + endPoint.x) / 2 + (nextRandom() - 0.5) * Math.max(0, Number(controlJitterX) || 0);
|
|
378
|
+
const midY = (startPoint.y + endPoint.y) / 2 + (nextRandom() - 0.5) * Math.max(0, Number(controlJitterY) || 0);
|
|
379
|
+
const path = [];
|
|
380
|
+
for (let index = 0; index <= safeSteps; index += 1) {
|
|
381
|
+
const t = index / safeSteps;
|
|
382
|
+
const inverse = 1 - t;
|
|
383
|
+
path.push({
|
|
384
|
+
x: inverse * inverse * startPoint.x + 2 * inverse * t * midX + t * t * endPoint.x,
|
|
385
|
+
y: inverse * inverse * startPoint.y + 2 * inverse * t * midY + t * t * endPoint.y
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return path;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function configureHumanInteraction(client, {
|
|
392
|
+
enabled = false,
|
|
393
|
+
clickMovementEnabled = null,
|
|
394
|
+
textEntryEnabled = null,
|
|
395
|
+
safeClickPointEnabled = null,
|
|
396
|
+
actionCooldownEnabled = null,
|
|
397
|
+
random = Math.random,
|
|
398
|
+
sleepFn = null,
|
|
399
|
+
moveSteps = 18,
|
|
400
|
+
moveJitterPx = 3,
|
|
401
|
+
hoverJitterPx = 5,
|
|
402
|
+
moveDelayMinMs = 5,
|
|
403
|
+
moveDelayMaxMs = 23,
|
|
404
|
+
hoverDelayMinMs = 10,
|
|
405
|
+
hoverDelayMaxMs = 30,
|
|
406
|
+
prePressBaseMs = 260,
|
|
407
|
+
prePressVarianceMs = 80,
|
|
408
|
+
holdVarianceMs = 30,
|
|
409
|
+
safeClickMinWidth = 44,
|
|
410
|
+
safeClickMinHeight = 28,
|
|
411
|
+
safeClickInsetRatio = 0.22,
|
|
412
|
+
safeClickMinInsetPx = 4,
|
|
413
|
+
safeClickMaxInsetPx = 18,
|
|
414
|
+
textChunkMinLength = 1,
|
|
415
|
+
textChunkMaxLength = 5,
|
|
416
|
+
textChunkDelayBaseMs = 55,
|
|
417
|
+
textChunkDelayVarianceMs = 30
|
|
418
|
+
} = {}) {
|
|
419
|
+
const previous = getHumanInteractionConfig(client);
|
|
420
|
+
const normalizedEnabled = enabled === true;
|
|
421
|
+
HUMAN_INTERACTION_CONFIG.set(client, {
|
|
422
|
+
enabled: normalizedEnabled,
|
|
423
|
+
clickMovementEnabled: normalizedEnabled && clickMovementEnabled !== false,
|
|
424
|
+
textEntryEnabled: normalizedEnabled && textEntryEnabled !== false,
|
|
425
|
+
safeClickPointEnabled: normalizedEnabled && safeClickPointEnabled !== false,
|
|
426
|
+
actionCooldownEnabled: normalizedEnabled && actionCooldownEnabled !== false,
|
|
427
|
+
random: normalizeRandom(random),
|
|
428
|
+
sleepFn: typeof sleepFn === "function" ? sleepFn : sleep,
|
|
429
|
+
moveSteps: Math.max(1, Math.floor(Number(moveSteps) || 18)),
|
|
430
|
+
moveJitterPx: Math.max(0, Number(moveJitterPx) || 0),
|
|
431
|
+
hoverJitterPx: Math.max(0, Number(hoverJitterPx) || 0),
|
|
432
|
+
moveDelayMinMs: Math.max(0, Number(moveDelayMinMs) || 0),
|
|
433
|
+
moveDelayMaxMs: Math.max(0, Number(moveDelayMaxMs) || 0),
|
|
434
|
+
hoverDelayMinMs: Math.max(0, Number(hoverDelayMinMs) || 0),
|
|
435
|
+
hoverDelayMaxMs: Math.max(0, Number(hoverDelayMaxMs) || 0),
|
|
436
|
+
prePressBaseMs: Math.max(0, Number(prePressBaseMs) || 0),
|
|
437
|
+
prePressVarianceMs: Math.max(0, Number(prePressVarianceMs) || 0),
|
|
438
|
+
holdVarianceMs: Math.max(0, Number(holdVarianceMs) || 0),
|
|
439
|
+
safeClickMinWidth: Math.max(1, Number(safeClickMinWidth) || 44),
|
|
440
|
+
safeClickMinHeight: Math.max(1, Number(safeClickMinHeight) || 28),
|
|
441
|
+
safeClickInsetRatio: clampNumber(safeClickInsetRatio, 0.05, 0.45),
|
|
442
|
+
safeClickMinInsetPx: Math.max(0, Number(safeClickMinInsetPx) || 0),
|
|
443
|
+
safeClickMaxInsetPx: Math.max(0, Number(safeClickMaxInsetPx) || 0),
|
|
444
|
+
textChunkMinLength: Math.max(1, Math.floor(Number(textChunkMinLength) || 1)),
|
|
445
|
+
textChunkMaxLength: Math.max(1, Math.floor(Number(textChunkMaxLength) || 5)),
|
|
446
|
+
textChunkDelayBaseMs: Math.max(0, Number(textChunkDelayBaseMs) || 0),
|
|
447
|
+
textChunkDelayVarianceMs: Math.max(0, Number(textChunkDelayVarianceMs) || 0),
|
|
448
|
+
lastMousePoint: previous?.lastMousePoint || null
|
|
449
|
+
});
|
|
450
|
+
return () => {
|
|
451
|
+
if (previous) {
|
|
452
|
+
HUMAN_INTERACTION_CONFIG.set(client, previous);
|
|
453
|
+
} else {
|
|
454
|
+
HUMAN_INTERACTION_CONFIG.delete(client);
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function createHumanRestController({
|
|
460
|
+
enabled = false,
|
|
461
|
+
shortRestEnabled = true,
|
|
462
|
+
batchRestEnabled = true,
|
|
463
|
+
random = Math.random,
|
|
464
|
+
nowFn = Date.now,
|
|
465
|
+
restLevel = DEFAULT_HUMAN_REST_LEVEL,
|
|
466
|
+
shortRestProbability = 0.08,
|
|
467
|
+
shortRestMinMs = 3000,
|
|
468
|
+
shortRestMaxMs = 7000,
|
|
469
|
+
perCandidateRestEnabled = false,
|
|
470
|
+
perCandidateRestMinMs = 0,
|
|
471
|
+
perCandidateRestMaxMs = 0,
|
|
472
|
+
batchThresholdBase = 25,
|
|
473
|
+
batchThresholdJitter = 8,
|
|
474
|
+
batchRestMinMs = 15000,
|
|
475
|
+
batchRestMaxMs = 30000
|
|
476
|
+
} = {}) {
|
|
477
|
+
const nextRandom = normalizeRandom(random);
|
|
478
|
+
const readNow = typeof nowFn === "function" ? nowFn : Date.now;
|
|
479
|
+
const normalizedRestLevel = normalizeHumanRestLevel(restLevel);
|
|
480
|
+
const perCandidateMinMs = Math.max(0, Number(perCandidateRestMinMs) || 0);
|
|
481
|
+
const perCandidateMaxMs = Math.max(perCandidateMinMs, Number(perCandidateRestMaxMs) || perCandidateMinMs);
|
|
482
|
+
const perCandidateEnabled = enabled === true && perCandidateRestEnabled === true && perCandidateMaxMs > 0;
|
|
483
|
+
const budgetProfile = !perCandidateEnabled && (shortRestEnabled !== false || batchRestEnabled !== false)
|
|
484
|
+
? HUMAN_REST_LEVEL_PROFILES[normalizedRestLevel] || null
|
|
485
|
+
: null;
|
|
486
|
+
const nextBudgetRestInterval = () => budgetProfile
|
|
487
|
+
? randomIntegerBetween(nextRandom, budgetProfile.intervalMin, budgetProfile.intervalMax)
|
|
488
|
+
: 0;
|
|
489
|
+
const state = {
|
|
490
|
+
enabled: enabled === true,
|
|
491
|
+
rest_level: normalizedRestLevel,
|
|
492
|
+
per_candidate_rest_enabled: perCandidateEnabled,
|
|
493
|
+
per_candidate_rest_min_ms: perCandidateMinMs,
|
|
494
|
+
per_candidate_rest_max_ms: perCandidateMaxMs,
|
|
495
|
+
short_rest_enabled: enabled === true && shortRestEnabled !== false,
|
|
496
|
+
batch_rest_enabled: enabled === true && batchRestEnabled !== false,
|
|
497
|
+
rest_counter: 0,
|
|
498
|
+
rest_threshold: Math.max(1, Math.floor(Number(batchThresholdBase) || 25) + Math.floor(nextRandom() * Math.max(1, Number(batchThresholdJitter) || 1))),
|
|
499
|
+
processed_count: 0,
|
|
500
|
+
candidates_since_last_rest: 0,
|
|
501
|
+
candidates_until_next_rest: nextBudgetRestInterval(),
|
|
502
|
+
active_elapsed_ms: 0,
|
|
503
|
+
last_active_at_ms: Number(readNow()) || 0,
|
|
504
|
+
rest_count: 0,
|
|
505
|
+
total_rest_ms: 0
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
function resetThreshold() {
|
|
509
|
+
state.rest_threshold = Math.max(1, Math.floor(Number(batchThresholdBase) || 25) + Math.floor(nextRandom() * Math.max(1, Number(batchThresholdJitter) || 1)));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function updateActiveElapsed() {
|
|
513
|
+
const now = Number(readNow()) || 0;
|
|
514
|
+
if (state.last_active_at_ms >= 0 && now >= state.last_active_at_ms) {
|
|
515
|
+
state.active_elapsed_ms += now - state.last_active_at_ms;
|
|
516
|
+
}
|
|
517
|
+
state.last_active_at_ms = now;
|
|
518
|
+
return now;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function getBudgetTargetMs() {
|
|
522
|
+
if (!budgetProfile) return 0;
|
|
523
|
+
const candidateTarget = state.processed_count * (budgetProfile.targetRestMs / budgetProfile.targetCandidateCount);
|
|
524
|
+
const elapsedTarget = state.active_elapsed_ms * (budgetProfile.targetRestMs / budgetProfile.targetWindowMs);
|
|
525
|
+
return Math.max(candidateTarget, elapsedTarget);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function chooseBudgetRestPause(debtMs) {
|
|
529
|
+
const longRest = nextRandom() < budgetProfile.longRestProbability;
|
|
530
|
+
const minMs = longRest ? budgetProfile.longRestMinMs : budgetProfile.shortRestMinMs;
|
|
531
|
+
const maxMs = longRest ? budgetProfile.longRestMaxMs : budgetProfile.shortRestMaxMs;
|
|
532
|
+
const scaleMin = longRest ? 0.75 : 0.38;
|
|
533
|
+
const scaleMax = longRest ? 1.1 : 0.78;
|
|
534
|
+
const desiredMs = debtMs * randomBetween(nextRandom, scaleMin, scaleMax);
|
|
535
|
+
const randomizedMs = randomBetween(nextRandom, minMs, maxMs);
|
|
536
|
+
const blendedMs = Math.max(minMs, Math.min(maxMs, (desiredMs + randomizedMs) / 2));
|
|
537
|
+
const maxAllowedMs = Math.max(minMs, debtMs + budgetProfile.maxOverspendMs);
|
|
538
|
+
return {
|
|
539
|
+
pauseMs: Math.round(Math.min(blendedMs, maxAllowedMs)),
|
|
540
|
+
restSize: longRest ? "long" : "short"
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function takeBudgetBreakIfNeeded(sleeper) {
|
|
545
|
+
state.processed_count += 1;
|
|
546
|
+
state.candidates_since_last_rest += 1;
|
|
547
|
+
state.candidates_until_next_rest -= 1;
|
|
548
|
+
const debtMs = getBudgetTargetMs() - state.total_rest_ms;
|
|
549
|
+
const intervalDue = state.candidates_until_next_rest <= 0;
|
|
550
|
+
const forceDue = debtMs >= budgetProfile.forceDebtMs;
|
|
551
|
+
if (!intervalDue && !forceDue) {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
if (debtMs < budgetProfile.minDebtToRestMs) {
|
|
555
|
+
if (intervalDue) state.candidates_until_next_rest = nextBudgetRestInterval();
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
const { pauseMs, restSize } = chooseBudgetRestPause(debtMs);
|
|
559
|
+
await sleeper(pauseMs);
|
|
560
|
+
const event = {
|
|
561
|
+
kind: "random_rest",
|
|
562
|
+
rest_level: normalizedRestLevel,
|
|
563
|
+
rest_size: restSize,
|
|
564
|
+
pause_ms: pauseMs,
|
|
565
|
+
processed_since_last_rest: state.candidates_since_last_rest,
|
|
566
|
+
rest_budget_debt_ms: Math.round(Math.max(0, debtMs))
|
|
567
|
+
};
|
|
568
|
+
state.candidates_since_last_rest = 0;
|
|
569
|
+
state.candidates_until_next_rest = nextBudgetRestInterval();
|
|
570
|
+
return event;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function takeBreakIfNeeded({ sleepFn = sleep } = {}) {
|
|
574
|
+
if (!state.enabled) {
|
|
575
|
+
return {
|
|
576
|
+
enabled: false,
|
|
577
|
+
rested: false,
|
|
578
|
+
rest_counter: state.rest_counter,
|
|
579
|
+
rest_threshold: state.rest_threshold,
|
|
580
|
+
events: []
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const sleeper = typeof sleepFn === "function" ? sleepFn : sleep;
|
|
584
|
+
updateActiveElapsed();
|
|
585
|
+
if (state.per_candidate_rest_enabled) {
|
|
586
|
+
state.rest_counter += 1;
|
|
587
|
+
state.processed_count += 1;
|
|
588
|
+
state.candidates_since_last_rest += 1;
|
|
589
|
+
const pauseMs = Math.round(randomBetween(
|
|
590
|
+
nextRandom,
|
|
591
|
+
state.per_candidate_rest_min_ms,
|
|
592
|
+
state.per_candidate_rest_max_ms
|
|
593
|
+
));
|
|
594
|
+
await sleeper(pauseMs);
|
|
595
|
+
state.rest_count += 1;
|
|
596
|
+
state.total_rest_ms += pauseMs;
|
|
597
|
+
state.last_active_at_ms = Number(readNow()) || state.last_active_at_ms;
|
|
598
|
+
const event = {
|
|
599
|
+
kind: "per_candidate_rest",
|
|
600
|
+
rest_level: normalizedRestLevel,
|
|
601
|
+
pause_ms: pauseMs,
|
|
602
|
+
processed_since_last_rest: state.candidates_since_last_rest
|
|
603
|
+
};
|
|
604
|
+
state.candidates_since_last_rest = 0;
|
|
605
|
+
return {
|
|
606
|
+
enabled: true,
|
|
607
|
+
rested: true,
|
|
608
|
+
pause_ms: pauseMs,
|
|
609
|
+
rest_level: normalizedRestLevel,
|
|
610
|
+
rest_counter: state.rest_counter,
|
|
611
|
+
rest_threshold: state.rest_threshold,
|
|
612
|
+
processed_count: state.processed_count,
|
|
613
|
+
active_elapsed_ms: state.active_elapsed_ms,
|
|
614
|
+
rest_count: state.rest_count,
|
|
615
|
+
total_rest_ms: state.total_rest_ms,
|
|
616
|
+
events: [event]
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
if (budgetProfile) {
|
|
620
|
+
const budgetEvent = await takeBudgetBreakIfNeeded(sleeper);
|
|
621
|
+
const pauseMs = budgetEvent?.pause_ms || 0;
|
|
622
|
+
if (pauseMs > 0) {
|
|
623
|
+
state.rest_count += 1;
|
|
624
|
+
state.total_rest_ms += pauseMs;
|
|
625
|
+
state.last_active_at_ms = Number(readNow()) || state.last_active_at_ms;
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
enabled: true,
|
|
629
|
+
rested: Boolean(budgetEvent),
|
|
630
|
+
pause_ms: pauseMs,
|
|
631
|
+
rest_level: normalizedRestLevel,
|
|
632
|
+
rest_counter: state.rest_counter,
|
|
633
|
+
rest_threshold: state.rest_threshold,
|
|
634
|
+
processed_count: state.processed_count,
|
|
635
|
+
candidates_until_next_rest: state.candidates_until_next_rest,
|
|
636
|
+
active_elapsed_ms: state.active_elapsed_ms,
|
|
637
|
+
rest_count: state.rest_count,
|
|
638
|
+
total_rest_ms: state.total_rest_ms,
|
|
639
|
+
events: budgetEvent ? [budgetEvent] : []
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
state.rest_counter += 1;
|
|
643
|
+
state.processed_count += 1;
|
|
644
|
+
const events = [];
|
|
645
|
+
if (state.short_rest_enabled && nextRandom() < Math.max(0, Number(shortRestProbability) || 0)) {
|
|
646
|
+
const pauseMs = Math.round(randomBetween(nextRandom, shortRestMinMs, shortRestMaxMs));
|
|
647
|
+
await sleeper(pauseMs);
|
|
648
|
+
events.push({ kind: "random_rest", rest_level: normalizedRestLevel, pause_ms: pauseMs });
|
|
649
|
+
}
|
|
650
|
+
if (state.batch_rest_enabled && state.rest_counter >= state.rest_threshold) {
|
|
651
|
+
const pauseMs = Math.round(randomBetween(nextRandom, batchRestMinMs, batchRestMaxMs));
|
|
652
|
+
await sleeper(pauseMs);
|
|
653
|
+
events.push({
|
|
654
|
+
kind: "batch_rest",
|
|
655
|
+
rest_level: normalizedRestLevel,
|
|
656
|
+
pause_ms: pauseMs,
|
|
657
|
+
processed_since_last_batch_rest: state.rest_counter
|
|
658
|
+
});
|
|
659
|
+
state.rest_counter = 0;
|
|
660
|
+
resetThreshold();
|
|
661
|
+
}
|
|
662
|
+
const pauseMs = events.reduce((sum, event) => sum + event.pause_ms, 0);
|
|
663
|
+
if (pauseMs > 0) {
|
|
664
|
+
state.rest_count += events.length;
|
|
665
|
+
state.total_rest_ms += pauseMs;
|
|
666
|
+
state.last_active_at_ms = Number(readNow()) || state.last_active_at_ms;
|
|
667
|
+
}
|
|
668
|
+
return {
|
|
669
|
+
enabled: true,
|
|
670
|
+
rested: events.length > 0,
|
|
671
|
+
pause_ms: pauseMs,
|
|
672
|
+
rest_level: normalizedRestLevel,
|
|
673
|
+
rest_counter: state.rest_counter,
|
|
674
|
+
rest_threshold: state.rest_threshold,
|
|
675
|
+
processed_count: state.processed_count,
|
|
676
|
+
active_elapsed_ms: state.active_elapsed_ms,
|
|
677
|
+
rest_count: state.rest_count,
|
|
678
|
+
total_rest_ms: state.total_rest_ms,
|
|
679
|
+
events
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return {
|
|
684
|
+
takeBreakIfNeeded,
|
|
685
|
+
getState() {
|
|
686
|
+
return { ...state };
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export function isBossLoginUrl(url) {
|
|
692
|
+
return BOSS_LOGIN_URL_PATTERN.test(String(url || ""));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export function createBossLoginRequiredError({
|
|
696
|
+
domain = "boss",
|
|
697
|
+
currentUrl = "",
|
|
698
|
+
targetUrl = "",
|
|
699
|
+
loginUrl = BOSS_LOGIN_URL,
|
|
700
|
+
loginDetection = null,
|
|
701
|
+
chrome = null
|
|
702
|
+
} = {}) {
|
|
703
|
+
const error = new Error(`Boss login is required before starting the ${domain} run.`);
|
|
704
|
+
error.code = "BOSS_LOGIN_REQUIRED";
|
|
705
|
+
error.requires_login = true;
|
|
706
|
+
error.current_url = currentUrl || null;
|
|
707
|
+
error.target_url = targetUrl || null;
|
|
708
|
+
error.login_url = loginUrl;
|
|
709
|
+
error.login_detection = loginDetection || null;
|
|
710
|
+
error.chrome = chrome || null;
|
|
711
|
+
error.retryable = true;
|
|
712
|
+
return error;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export async function detectBossLoginState(client, { currentUrl = "" } = {}) {
|
|
716
|
+
const inspectedUrl = currentUrl || await getMainFrameUrl(client).catch(() => "");
|
|
717
|
+
if (isBossLoginUrl(inspectedUrl)) {
|
|
718
|
+
return {
|
|
719
|
+
requires_login: true,
|
|
720
|
+
reason: "url",
|
|
721
|
+
current_url: inspectedUrl,
|
|
722
|
+
matched_selectors: []
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
let root = null;
|
|
727
|
+
try {
|
|
728
|
+
root = await getDocumentRoot(client, { depth: 1, pierce: true });
|
|
729
|
+
} catch (error) {
|
|
730
|
+
return {
|
|
731
|
+
requires_login: false,
|
|
732
|
+
reason: "dom_unavailable",
|
|
733
|
+
current_url: inspectedUrl,
|
|
734
|
+
error: error?.message || String(error || "")
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const matchedSelectors = [];
|
|
739
|
+
for (const selector of BOSS_LOGIN_DOM_SELECTORS) {
|
|
740
|
+
const nodeId = await querySelector(client, root.nodeId, selector).catch(() => 0);
|
|
741
|
+
if (nodeId) matchedSelectors.push(selector);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (matchedSelectors.length === 0) {
|
|
745
|
+
return {
|
|
746
|
+
requires_login: false,
|
|
747
|
+
reason: "no_login_dom",
|
|
748
|
+
current_url: inspectedUrl,
|
|
749
|
+
matched_selectors: []
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const html = await getOuterHTML(client, root.nodeId).catch(() => "");
|
|
754
|
+
const looksLikeLogin = BOSS_LOGIN_TEXT_PATTERN.test(html);
|
|
755
|
+
return {
|
|
756
|
+
requires_login: looksLikeLogin,
|
|
757
|
+
reason: looksLikeLogin ? "dom" : "login_selector_without_login_text",
|
|
758
|
+
current_url: inspectedUrl,
|
|
759
|
+
matched_selectors: matchedSelectors
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
export function isChromeDebugUnavailableError(error) {
|
|
764
|
+
return CHROME_DEBUG_UNAVAILABLE_PATTERN.test(String(error?.message || error || ""));
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function pathExists(targetPath) {
|
|
768
|
+
try {
|
|
769
|
+
return Boolean(targetPath) && fs.existsSync(targetPath);
|
|
770
|
+
} catch {
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function ensureDir(targetPath) {
|
|
776
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function isLocalChromeHost(host) {
|
|
780
|
+
const normalized = String(host || "").trim().toLowerCase();
|
|
781
|
+
return !normalized || normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function getCodexHome() {
|
|
785
|
+
return process.env.CODEX_HOME
|
|
786
|
+
? path.resolve(process.env.CODEX_HOME)
|
|
787
|
+
: path.join(os.homedir(), ".codex");
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function getDefaultChromeExecutableCandidates() {
|
|
791
|
+
const candidates = [
|
|
792
|
+
process.env.BOSS_MCP_CHROME_PATH,
|
|
793
|
+
process.env.BOSS_RECOMMEND_CHROME_PATH
|
|
794
|
+
].filter(Boolean);
|
|
795
|
+
if (process.platform === "win32") {
|
|
796
|
+
candidates.push(
|
|
797
|
+
path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
798
|
+
path.join(process.env.ProgramFiles || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
799
|
+
path.join(process.env["ProgramFiles(x86)"] || "", "Google", "Chrome", "Application", "chrome.exe")
|
|
800
|
+
);
|
|
801
|
+
} else if (process.platform === "darwin") {
|
|
802
|
+
candidates.push(
|
|
803
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
804
|
+
path.join(os.homedir(), "Applications", "Google Chrome.app", "Contents", "MacOS", "Google Chrome"),
|
|
805
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
806
|
+
);
|
|
807
|
+
} else {
|
|
808
|
+
candidates.push(
|
|
809
|
+
"/usr/bin/google-chrome",
|
|
810
|
+
"/usr/bin/google-chrome-stable",
|
|
811
|
+
"/usr/bin/chromium-browser",
|
|
812
|
+
"/usr/bin/chromium",
|
|
813
|
+
"/snap/bin/chromium"
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
return Array.from(new Set(candidates.filter(Boolean)));
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export function getChromeExecutable() {
|
|
820
|
+
return getDefaultChromeExecutableCandidates().find((candidate) => pathExists(candidate)) || null;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
export function getBossChromeUserDataDir(port = DEFAULT_CHROME_PORT) {
|
|
824
|
+
const sharedPath = path.join(getCodexHome(), "boss-mcp", `chrome-profile-${port}`);
|
|
825
|
+
ensureDir(sharedPath);
|
|
826
|
+
return sharedPath;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function parseExtraChromeArgs(value = "") {
|
|
830
|
+
return String(value || "")
|
|
831
|
+
.split(/\s+/)
|
|
832
|
+
.map((item) => item.trim())
|
|
833
|
+
.filter(Boolean);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
export function parseChromeCommandLineArgs(commandLineOrArgs = []) {
|
|
837
|
+
if (Array.isArray(commandLineOrArgs)) {
|
|
838
|
+
return commandLineOrArgs
|
|
839
|
+
.map((item) => String(item || "").trim())
|
|
840
|
+
.filter(Boolean);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const text = String(commandLineOrArgs || "").trim();
|
|
844
|
+
if (!text) return [];
|
|
845
|
+
const args = [];
|
|
846
|
+
let current = "";
|
|
847
|
+
let quote = null;
|
|
848
|
+
for (const char of text) {
|
|
849
|
+
if (quote) {
|
|
850
|
+
if (char === quote) {
|
|
851
|
+
quote = null;
|
|
852
|
+
} else {
|
|
853
|
+
current += char;
|
|
854
|
+
}
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
if (char === '"' || char === "'") {
|
|
858
|
+
quote = char;
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
if (/\s/.test(char)) {
|
|
862
|
+
if (current) {
|
|
863
|
+
args.push(current);
|
|
864
|
+
current = "";
|
|
865
|
+
}
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
current += char;
|
|
869
|
+
}
|
|
870
|
+
if (current) args.push(current);
|
|
871
|
+
return args;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function splitChromeFeatureList(value = "") {
|
|
875
|
+
return String(value || "")
|
|
876
|
+
.split(",")
|
|
877
|
+
.map((item) => item.trim())
|
|
878
|
+
.filter(Boolean);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function chromeFlagIsPresent(args, requiredFlag) {
|
|
882
|
+
if (!requiredFlag) return true;
|
|
883
|
+
const disableFeaturesPrefix = "--disable-features=";
|
|
884
|
+
if (requiredFlag.startsWith(disableFeaturesPrefix)) {
|
|
885
|
+
const requiredFeatures = splitChromeFeatureList(requiredFlag.slice(disableFeaturesPrefix.length));
|
|
886
|
+
const disableFeatureArgs = args.filter((arg) => arg.startsWith(disableFeaturesPrefix));
|
|
887
|
+
const lastDisableFeatureArg = disableFeatureArgs[disableFeatureArgs.length - 1] || "";
|
|
888
|
+
const features = splitChromeFeatureList(lastDisableFeatureArg.slice(disableFeaturesPrefix.length));
|
|
889
|
+
return requiredFeatures.every((feature) => features.includes(feature));
|
|
890
|
+
}
|
|
891
|
+
if (args.includes(requiredFlag)) return true;
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
export function getMissingRequiredChromeFlags(
|
|
896
|
+
commandLineOrArgs = [],
|
|
897
|
+
requiredFlags = DEFAULT_REQUIRED_CHROME_FLAGS
|
|
898
|
+
) {
|
|
899
|
+
const args = parseChromeCommandLineArgs(commandLineOrArgs);
|
|
900
|
+
return requiredFlags.filter((flag) => !chromeFlagIsPresent(args, flag));
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function normalizeChromeLaunchArgs(args = []) {
|
|
904
|
+
const disableFeaturesPrefix = "--disable-features=";
|
|
905
|
+
const result = [];
|
|
906
|
+
const seen = new Set();
|
|
907
|
+
const disabledFeatures = [];
|
|
908
|
+
const disabledFeatureSet = new Set();
|
|
909
|
+
let disabledFeatureIndex = -1;
|
|
910
|
+
|
|
911
|
+
for (const rawArg of args) {
|
|
912
|
+
const arg = String(rawArg || "").trim();
|
|
913
|
+
if (!arg) continue;
|
|
914
|
+
if (arg.startsWith(disableFeaturesPrefix)) {
|
|
915
|
+
if (disabledFeatureIndex < 0) {
|
|
916
|
+
disabledFeatureIndex = result.length;
|
|
917
|
+
result.push(null);
|
|
918
|
+
}
|
|
919
|
+
for (const feature of splitChromeFeatureList(arg.slice(disableFeaturesPrefix.length))) {
|
|
920
|
+
if (!disabledFeatureSet.has(feature)) {
|
|
921
|
+
disabledFeatureSet.add(feature);
|
|
922
|
+
disabledFeatures.push(feature);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
if (seen.has(arg)) continue;
|
|
928
|
+
seen.add(arg);
|
|
929
|
+
result.push(arg);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
return result.map((arg) => (
|
|
933
|
+
arg === null
|
|
934
|
+
? `${disableFeaturesPrefix}${disabledFeatures.join(",")}`
|
|
935
|
+
: arg
|
|
936
|
+
));
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
export function buildBossChromeLaunchArgs({
|
|
940
|
+
port = DEFAULT_CHROME_PORT,
|
|
941
|
+
userDataDir = "",
|
|
942
|
+
url = "about:blank",
|
|
943
|
+
extraArgs = []
|
|
944
|
+
} = {}) {
|
|
945
|
+
const args = [
|
|
946
|
+
`--remote-debugging-port=${port}`,
|
|
947
|
+
`--user-data-dir=${userDataDir}`,
|
|
948
|
+
"--no-first-run",
|
|
949
|
+
"--no-default-browser-check",
|
|
950
|
+
...LID_CLOSED_SAFE_CHROME_ARGS,
|
|
951
|
+
...parseExtraChromeArgs(process.env.BOSS_MCP_EXTRA_CHROME_ARGS),
|
|
952
|
+
...extraArgs,
|
|
953
|
+
"--start-maximized",
|
|
954
|
+
"--new-window",
|
|
955
|
+
url
|
|
956
|
+
];
|
|
957
|
+
return normalizeChromeLaunchArgs(args);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function execFileText(file, args = [], { timeoutMs = 5000, maxBuffer = 1024 * 1024 } = {}) {
|
|
961
|
+
return new Promise((resolve) => {
|
|
962
|
+
execFile(file, args, {
|
|
963
|
+
timeout: timeoutMs,
|
|
964
|
+
maxBuffer,
|
|
965
|
+
windowsHide: true
|
|
966
|
+
}, (error, stdout, stderr) => {
|
|
967
|
+
resolve({
|
|
968
|
+
ok: !error,
|
|
969
|
+
stdout: String(stdout || ""),
|
|
970
|
+
stderr: String(stderr || ""),
|
|
971
|
+
error: error?.message || ""
|
|
972
|
+
});
|
|
973
|
+
});
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
async function inspectChromeCommandLineViaCdp({
|
|
978
|
+
host = DEFAULT_CHROME_HOST,
|
|
979
|
+
port = DEFAULT_CHROME_PORT
|
|
980
|
+
} = {}) {
|
|
981
|
+
let client = null;
|
|
982
|
+
try {
|
|
983
|
+
client = await CDP({ host, port });
|
|
984
|
+
const result = await client.Browser.getBrowserCommandLine();
|
|
985
|
+
const args = parseChromeCommandLineArgs(result?.arguments || result?.commandLine || result?.command_line || []);
|
|
986
|
+
if (args.length === 0) {
|
|
987
|
+
return {
|
|
988
|
+
ok: false,
|
|
989
|
+
source: "cdp_browser_command_line",
|
|
990
|
+
arguments: [],
|
|
991
|
+
error: "Browser.getBrowserCommandLine returned no command-line arguments"
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
ok: true,
|
|
996
|
+
source: "cdp_browser_command_line",
|
|
997
|
+
arguments: args
|
|
998
|
+
};
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
return {
|
|
1001
|
+
ok: false,
|
|
1002
|
+
source: "cdp_browser_command_line",
|
|
1003
|
+
arguments: [],
|
|
1004
|
+
error: error?.message || String(error || "")
|
|
1005
|
+
};
|
|
1006
|
+
} finally {
|
|
1007
|
+
if (client) {
|
|
1008
|
+
await client.close().catch(() => {});
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function parseWindowsProcessListJson(text = "") {
|
|
1014
|
+
const trimmed = String(text || "").trim();
|
|
1015
|
+
if (!trimmed) return [];
|
|
1016
|
+
const parsed = JSON.parse(trimmed);
|
|
1017
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
1018
|
+
return items
|
|
1019
|
+
.map((item) => ({
|
|
1020
|
+
pid: Number(item?.ProcessId),
|
|
1021
|
+
command_line: String(item?.CommandLine || "")
|
|
1022
|
+
}))
|
|
1023
|
+
.filter((item) => Number.isFinite(item.pid) && item.command_line);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function parsePosixProcessList(text = "", port = DEFAULT_CHROME_PORT) {
|
|
1027
|
+
const portPattern = new RegExp(`--remote-debugging-port(?:=|\\s+)${port}(?=\\s|$)`);
|
|
1028
|
+
return String(text || "")
|
|
1029
|
+
.split(/\r?\n/)
|
|
1030
|
+
.map((line) => {
|
|
1031
|
+
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
1032
|
+
return match
|
|
1033
|
+
? { pid: Number(match[1]), command_line: match[2] }
|
|
1034
|
+
: null;
|
|
1035
|
+
})
|
|
1036
|
+
.filter((item) => item && Number.isFinite(item.pid) && portPattern.test(item.command_line));
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function summarizeChromeProcesses(processes = []) {
|
|
1040
|
+
return processes
|
|
1041
|
+
.map((item) => ({
|
|
1042
|
+
pid: item.pid,
|
|
1043
|
+
command_line_length: String(item.command_line || "").length
|
|
1044
|
+
}))
|
|
1045
|
+
.filter((item) => Number.isFinite(item.pid));
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
async function inspectChromeCommandLineViaProcessList({
|
|
1049
|
+
port = DEFAULT_CHROME_PORT
|
|
1050
|
+
} = {}) {
|
|
1051
|
+
const portText = String(port);
|
|
1052
|
+
let processes = [];
|
|
1053
|
+
let raw = null;
|
|
1054
|
+
|
|
1055
|
+
if (process.platform === "win32") {
|
|
1056
|
+
const portPattern = `--remote-debugging-port(=|\\s+)${portText}(\\s|$)`;
|
|
1057
|
+
const script = [
|
|
1058
|
+
"$items = Get-CimInstance Win32_Process",
|
|
1059
|
+
`| Where-Object { $_.CommandLine -and $_.CommandLine -match '${portPattern}' }`,
|
|
1060
|
+
"| Select-Object ProcessId,CommandLine;",
|
|
1061
|
+
"$items | ConvertTo-Json -Compress"
|
|
1062
|
+
].join(" ");
|
|
1063
|
+
raw = await execFileText("powershell.exe", [
|
|
1064
|
+
"-NoProfile",
|
|
1065
|
+
"-ExecutionPolicy",
|
|
1066
|
+
"Bypass",
|
|
1067
|
+
"-Command",
|
|
1068
|
+
script
|
|
1069
|
+
], { timeoutMs: 6000 });
|
|
1070
|
+
if (!raw.ok) {
|
|
1071
|
+
return {
|
|
1072
|
+
ok: false,
|
|
1073
|
+
source: "process_list",
|
|
1074
|
+
arguments: [],
|
|
1075
|
+
processes: [],
|
|
1076
|
+
error: raw.error || raw.stderr || "Failed to inspect Windows process list"
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
try {
|
|
1080
|
+
processes = parseWindowsProcessListJson(raw.stdout);
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
return {
|
|
1083
|
+
ok: false,
|
|
1084
|
+
source: "process_list",
|
|
1085
|
+
arguments: [],
|
|
1086
|
+
processes: [],
|
|
1087
|
+
error: `Failed to parse Windows process list: ${error?.message || error}`
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
} else {
|
|
1091
|
+
const psArgs = process.platform === "darwin"
|
|
1092
|
+
? ["-axo", "pid=,command="]
|
|
1093
|
+
: ["-eo", "pid=,args="];
|
|
1094
|
+
raw = await execFileText("ps", psArgs, { timeoutMs: 6000 });
|
|
1095
|
+
if (!raw.ok) {
|
|
1096
|
+
return {
|
|
1097
|
+
ok: false,
|
|
1098
|
+
source: "process_list",
|
|
1099
|
+
arguments: [],
|
|
1100
|
+
processes: [],
|
|
1101
|
+
error: raw.error || raw.stderr || "Failed to inspect process list"
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
processes = parsePosixProcessList(raw.stdout, port);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (processes.length === 0) {
|
|
1108
|
+
return {
|
|
1109
|
+
ok: false,
|
|
1110
|
+
source: "process_list",
|
|
1111
|
+
arguments: [],
|
|
1112
|
+
processes: [],
|
|
1113
|
+
error: `No local process was found for --remote-debugging-port=${port}`
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
const primary = processes[0];
|
|
1117
|
+
return {
|
|
1118
|
+
ok: true,
|
|
1119
|
+
source: "process_list",
|
|
1120
|
+
arguments: parseChromeCommandLineArgs(primary.command_line),
|
|
1121
|
+
process: {
|
|
1122
|
+
pid: primary.pid,
|
|
1123
|
+
command_line_length: primary.command_line.length
|
|
1124
|
+
},
|
|
1125
|
+
processes: summarizeChromeProcesses(processes)
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
export async function inspectChromeDebugCommandLine({
|
|
1130
|
+
host = DEFAULT_CHROME_HOST,
|
|
1131
|
+
port = DEFAULT_CHROME_PORT,
|
|
1132
|
+
_deps = {}
|
|
1133
|
+
} = {}) {
|
|
1134
|
+
const inspectViaCdp = _deps.inspectChromeCommandLineViaCdpImpl || inspectChromeCommandLineViaCdp;
|
|
1135
|
+
const inspectViaProcess = _deps.inspectChromeCommandLineViaProcessListImpl || inspectChromeCommandLineViaProcessList;
|
|
1136
|
+
const cdpResult = await inspectViaCdp({ host, port });
|
|
1137
|
+
if (cdpResult?.ok && cdpResult.arguments?.length) {
|
|
1138
|
+
return cdpResult;
|
|
1139
|
+
}
|
|
1140
|
+
if (!isLocalChromeHost(host)) {
|
|
1141
|
+
return {
|
|
1142
|
+
ok: false,
|
|
1143
|
+
source: cdpResult?.source || "unknown",
|
|
1144
|
+
arguments: [],
|
|
1145
|
+
error: cdpResult?.error || `Cannot inspect process list for non-local Chrome debug host: ${host}`
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
const processResult = await inspectViaProcess({ port });
|
|
1149
|
+
if (processResult?.ok && processResult.arguments?.length) {
|
|
1150
|
+
return {
|
|
1151
|
+
...processResult,
|
|
1152
|
+
cdp_error: cdpResult?.error || null
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
return {
|
|
1156
|
+
ok: false,
|
|
1157
|
+
source: processResult?.source || cdpResult?.source || "unknown",
|
|
1158
|
+
arguments: [],
|
|
1159
|
+
processes: processResult?.processes || [],
|
|
1160
|
+
error: processResult?.error || cdpResult?.error || "Chrome command line could not be inspected"
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
async function waitForChromeDebugPortClosed({
|
|
1165
|
+
host = DEFAULT_CHROME_HOST,
|
|
1166
|
+
port = DEFAULT_CHROME_PORT,
|
|
1167
|
+
timeoutMs = 6000,
|
|
1168
|
+
intervalMs = 300,
|
|
1169
|
+
listChromeTargetsImpl = listChromeTargets
|
|
1170
|
+
} = {}) {
|
|
1171
|
+
const started = Date.now();
|
|
1172
|
+
let lastError = null;
|
|
1173
|
+
let lastTargetCount = 0;
|
|
1174
|
+
while (Date.now() - started <= timeoutMs) {
|
|
1175
|
+
try {
|
|
1176
|
+
const targets = await listChromeTargetsImpl({ host, port });
|
|
1177
|
+
lastTargetCount = Array.isArray(targets) ? targets.length : 0;
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
if (isChromeDebugUnavailableError(error)) {
|
|
1180
|
+
return {
|
|
1181
|
+
ok: true,
|
|
1182
|
+
elapsed_ms: Date.now() - started
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
lastError = error;
|
|
1186
|
+
}
|
|
1187
|
+
await sleep(intervalMs);
|
|
1188
|
+
}
|
|
1189
|
+
return {
|
|
1190
|
+
ok: false,
|
|
1191
|
+
elapsed_ms: Date.now() - started,
|
|
1192
|
+
target_count: lastTargetCount,
|
|
1193
|
+
error: lastError?.message || `Chrome debug port ${port} is still reachable`
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
export async function closeChromeDebugInstance({
|
|
1198
|
+
host = DEFAULT_CHROME_HOST,
|
|
1199
|
+
port = DEFAULT_CHROME_PORT,
|
|
1200
|
+
processes = [],
|
|
1201
|
+
timeoutMs = 8000,
|
|
1202
|
+
intervalMs = 300,
|
|
1203
|
+
_deps = {}
|
|
1204
|
+
} = {}) {
|
|
1205
|
+
if (!isLocalChromeHost(host)) {
|
|
1206
|
+
return {
|
|
1207
|
+
ok: false,
|
|
1208
|
+
method: "none",
|
|
1209
|
+
error: `Refusing to close non-local Chrome debug host: ${host}`
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
const listChromeTargetsImpl = _deps.listChromeTargetsImpl || listChromeTargets;
|
|
1214
|
+
const waitClosed = _deps.waitForChromeDebugPortClosedImpl || waitForChromeDebugPortClosed;
|
|
1215
|
+
let browserCloseAttempted = false;
|
|
1216
|
+
let browserCloseError = null;
|
|
1217
|
+
try {
|
|
1218
|
+
let client = null;
|
|
1219
|
+
try {
|
|
1220
|
+
client = await CDP({ host, port });
|
|
1221
|
+
if (typeof client?.Browser?.close !== "function") {
|
|
1222
|
+
throw new Error("Browser.close is not available");
|
|
1223
|
+
}
|
|
1224
|
+
browserCloseAttempted = true;
|
|
1225
|
+
await client.Browser.close();
|
|
1226
|
+
} finally {
|
|
1227
|
+
if (client) await client.close().catch(() => {});
|
|
1228
|
+
}
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
browserCloseError = error?.message || String(error || "");
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
let closed = await waitClosed({ host, port, timeoutMs, intervalMs, listChromeTargetsImpl });
|
|
1234
|
+
if (closed.ok) {
|
|
1235
|
+
return {
|
|
1236
|
+
ok: true,
|
|
1237
|
+
method: browserCloseAttempted ? "Browser.close" : "port_already_closed",
|
|
1238
|
+
elapsed_ms: closed.elapsed_ms,
|
|
1239
|
+
browser_close_error: browserCloseError
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const pids = Array.from(new Set((processes || [])
|
|
1244
|
+
.map((item) => Number(item?.pid))
|
|
1245
|
+
.filter((pid) => Number.isFinite(pid) && pid > 0 && pid !== process.pid)));
|
|
1246
|
+
const killedPids = [];
|
|
1247
|
+
const processErrors = [];
|
|
1248
|
+
for (const pid of pids) {
|
|
1249
|
+
try {
|
|
1250
|
+
process.kill(pid);
|
|
1251
|
+
killedPids.push(pid);
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
processErrors.push({
|
|
1254
|
+
pid,
|
|
1255
|
+
error: error?.message || String(error || "")
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (killedPids.length > 0) {
|
|
1261
|
+
closed = await waitClosed({ host, port, timeoutMs, intervalMs, listChromeTargetsImpl });
|
|
1262
|
+
if (closed.ok) {
|
|
1263
|
+
return {
|
|
1264
|
+
ok: true,
|
|
1265
|
+
method: browserCloseAttempted ? "Browser.close+process.kill" : "process.kill",
|
|
1266
|
+
elapsed_ms: closed.elapsed_ms,
|
|
1267
|
+
killed_pids: killedPids,
|
|
1268
|
+
browser_close_error: browserCloseError,
|
|
1269
|
+
process_errors: processErrors
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
return {
|
|
1275
|
+
ok: false,
|
|
1276
|
+
method: browserCloseAttempted && killedPids.length > 0
|
|
1277
|
+
? "Browser.close+process.kill"
|
|
1278
|
+
: browserCloseAttempted
|
|
1279
|
+
? "Browser.close"
|
|
1280
|
+
: killedPids.length > 0
|
|
1281
|
+
? "process.kill"
|
|
1282
|
+
: "none",
|
|
1283
|
+
killed_pids: killedPids,
|
|
1284
|
+
browser_close_error: browserCloseError,
|
|
1285
|
+
process_errors: processErrors,
|
|
1286
|
+
wait: closed,
|
|
1287
|
+
error: closed.error || browserCloseError || "Failed to close Chrome debug instance"
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function summarizeRelaunch(result = {}, reason = "") {
|
|
1292
|
+
return {
|
|
1293
|
+
reason,
|
|
1294
|
+
launched: Boolean(result?.launched),
|
|
1295
|
+
chrome_path: result?.chrome_path || null,
|
|
1296
|
+
user_data_dir: result?.user_data_dir || null,
|
|
1297
|
+
launch_args: Array.isArray(result?.launch_args) ? result.launch_args : [],
|
|
1298
|
+
readiness: result?.readiness || null
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function createChromeGuardError(message, code, chromeGuard) {
|
|
1303
|
+
const error = new Error(message);
|
|
1304
|
+
error.code = code;
|
|
1305
|
+
error.chrome_guard = chromeGuard;
|
|
1306
|
+
return error;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
export async function waitForChromeDebugPort({
|
|
1310
|
+
host = DEFAULT_CHROME_HOST,
|
|
1311
|
+
port = DEFAULT_CHROME_PORT,
|
|
1312
|
+
timeoutMs = 8000,
|
|
1313
|
+
intervalMs = 300
|
|
1314
|
+
} = {}) {
|
|
1315
|
+
const started = Date.now();
|
|
1316
|
+
let lastError = null;
|
|
1317
|
+
while (Date.now() - started <= timeoutMs) {
|
|
1318
|
+
try {
|
|
1319
|
+
const targets = await listChromeTargets({ host, port });
|
|
1320
|
+
return {
|
|
1321
|
+
ok: true,
|
|
1322
|
+
elapsed_ms: Date.now() - started,
|
|
1323
|
+
targets
|
|
1324
|
+
};
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
lastError = error;
|
|
1327
|
+
await sleep(intervalMs);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return {
|
|
1331
|
+
ok: false,
|
|
1332
|
+
elapsed_ms: Date.now() - started,
|
|
1333
|
+
error: lastError?.message || String(lastError || "Chrome debug port did not become ready")
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
export async function launchChromeDebugInstance({
|
|
1338
|
+
host = DEFAULT_CHROME_HOST,
|
|
1339
|
+
port = DEFAULT_CHROME_PORT,
|
|
1340
|
+
url = "about:blank",
|
|
1341
|
+
slowLive = false,
|
|
1342
|
+
userDataDir = ""
|
|
1343
|
+
} = {}) {
|
|
1344
|
+
if (!isLocalChromeHost(host)) {
|
|
1345
|
+
throw new Error(`Cannot auto-launch Chrome for non-local debug host: ${host}`);
|
|
1346
|
+
}
|
|
1347
|
+
const chromePath = getChromeExecutable();
|
|
1348
|
+
if (!chromePath) {
|
|
1349
|
+
throw new Error("Chrome executable not found. Set BOSS_MCP_CHROME_PATH or BOSS_RECOMMEND_CHROME_PATH.");
|
|
1350
|
+
}
|
|
1351
|
+
const resolvedUserDataDir = userDataDir || getBossChromeUserDataDir(port);
|
|
1352
|
+
ensureDir(resolvedUserDataDir);
|
|
1353
|
+
const args = buildBossChromeLaunchArgs({ port, userDataDir: resolvedUserDataDir, url });
|
|
1354
|
+
const child = spawn(chromePath, args, {
|
|
1355
|
+
detached: true,
|
|
1356
|
+
stdio: "ignore",
|
|
1357
|
+
windowsHide: false
|
|
1358
|
+
});
|
|
1359
|
+
child.unref();
|
|
1360
|
+
const readiness = await waitForChromeDebugPort({
|
|
1361
|
+
host,
|
|
1362
|
+
port,
|
|
1363
|
+
timeoutMs: slowLive ? 30000 : 12000,
|
|
1364
|
+
intervalMs: slowLive ? 700 : 300
|
|
1365
|
+
});
|
|
1366
|
+
if (!readiness.ok) {
|
|
1367
|
+
throw new Error(`Chrome launched but DevTools port ${port} did not become reachable: ${readiness.error}`);
|
|
1368
|
+
}
|
|
1369
|
+
return {
|
|
1370
|
+
launched: true,
|
|
1371
|
+
chrome_path: chromePath,
|
|
1372
|
+
user_data_dir: resolvedUserDataDir,
|
|
1373
|
+
launch_args: args,
|
|
1374
|
+
port,
|
|
1375
|
+
url,
|
|
1376
|
+
readiness: {
|
|
1377
|
+
elapsed_ms: readiness.elapsed_ms,
|
|
1378
|
+
target_count: readiness.targets.length
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
export async function ensureChromeDebugPort({
|
|
1384
|
+
host = DEFAULT_CHROME_HOST,
|
|
1385
|
+
port = DEFAULT_CHROME_PORT,
|
|
1386
|
+
url = "about:blank",
|
|
1387
|
+
slowLive = false,
|
|
1388
|
+
launchIfMissing = true,
|
|
1389
|
+
userDataDir = "",
|
|
1390
|
+
enforceRequiredFlags = true,
|
|
1391
|
+
requiredFlags = DEFAULT_REQUIRED_CHROME_FLAGS,
|
|
1392
|
+
_deps = {}
|
|
1393
|
+
} = {}) {
|
|
1394
|
+
const listChromeTargetsImpl = _deps.listChromeTargetsImpl || listChromeTargets;
|
|
1395
|
+
const inspectCommandLineImpl = _deps.inspectChromeDebugCommandLineImpl || inspectChromeDebugCommandLine;
|
|
1396
|
+
const closeChromeDebugInstanceImpl = _deps.closeChromeDebugInstanceImpl || closeChromeDebugInstance;
|
|
1397
|
+
const launchChromeDebugInstanceImpl = _deps.launchChromeDebugInstanceImpl || launchChromeDebugInstance;
|
|
1398
|
+
const required = Array.from(new Set((requiredFlags || []).filter(Boolean)));
|
|
1399
|
+
const baseGuard = {
|
|
1400
|
+
guard_checked: Boolean(enforceRequiredFlags),
|
|
1401
|
+
required_flags: required,
|
|
1402
|
+
missing_flags: [],
|
|
1403
|
+
required_flags_ok: !enforceRequiredFlags,
|
|
1404
|
+
replaced: false,
|
|
1405
|
+
close_method: null,
|
|
1406
|
+
relaunch: null,
|
|
1407
|
+
host,
|
|
1408
|
+
port
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
try {
|
|
1412
|
+
const targets = await listChromeTargetsImpl({ host, port });
|
|
1413
|
+
if (!enforceRequiredFlags) {
|
|
1414
|
+
return {
|
|
1415
|
+
launched: false,
|
|
1416
|
+
reused: true,
|
|
1417
|
+
port,
|
|
1418
|
+
target_count: targets.length,
|
|
1419
|
+
...baseGuard
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const commandLine = await inspectCommandLineImpl({ host, port, _deps });
|
|
1424
|
+
const missingFlags = commandLine?.ok
|
|
1425
|
+
? getMissingRequiredChromeFlags(commandLine.arguments, required)
|
|
1426
|
+
: required.slice();
|
|
1427
|
+
const commandLineEvidence = {
|
|
1428
|
+
command_line_source: commandLine?.source || "unknown",
|
|
1429
|
+
command_line_error: commandLine?.ok ? null : (commandLine?.error || "Chrome command line could not be inspected"),
|
|
1430
|
+
command_line_args_count: Array.isArray(commandLine?.arguments) ? commandLine.arguments.length : 0,
|
|
1431
|
+
inspected_process: commandLine?.process || null,
|
|
1432
|
+
inspected_processes: commandLine?.processes || []
|
|
1433
|
+
};
|
|
1434
|
+
if (missingFlags.length === 0) {
|
|
1435
|
+
return {
|
|
1436
|
+
launched: false,
|
|
1437
|
+
reused: true,
|
|
1438
|
+
port,
|
|
1439
|
+
target_count: targets.length,
|
|
1440
|
+
...baseGuard,
|
|
1441
|
+
required_flags_ok: true,
|
|
1442
|
+
...commandLineEvidence
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
const guard = {
|
|
1447
|
+
...baseGuard,
|
|
1448
|
+
required_flags_ok: false,
|
|
1449
|
+
missing_flags: missingFlags,
|
|
1450
|
+
target_count: targets.length,
|
|
1451
|
+
...commandLineEvidence
|
|
1452
|
+
};
|
|
1453
|
+
if (!isLocalChromeHost(host)) {
|
|
1454
|
+
throw createChromeGuardError(
|
|
1455
|
+
`Chrome debug host ${host}:${port} is missing required Chrome flags and is not local, so it will not be auto-closed.`,
|
|
1456
|
+
"CHROME_REQUIRED_FLAGS_MISSING_NON_LOCAL",
|
|
1457
|
+
guard
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
const closeResult = await closeChromeDebugInstanceImpl({
|
|
1462
|
+
host,
|
|
1463
|
+
port,
|
|
1464
|
+
processes: commandLine?.processes || [],
|
|
1465
|
+
_deps
|
|
1466
|
+
});
|
|
1467
|
+
if (!closeResult?.ok) {
|
|
1468
|
+
throw createChromeGuardError(
|
|
1469
|
+
`Chrome debug instance on port ${port} is missing required flags and could not be closed: ${closeResult?.error || "unknown close failure"}`,
|
|
1470
|
+
"CHROME_REQUIRED_FLAGS_REPLACE_FAILED",
|
|
1471
|
+
{
|
|
1472
|
+
...guard,
|
|
1473
|
+
close_method: closeResult?.method || null,
|
|
1474
|
+
close_result: closeResult || null
|
|
1475
|
+
}
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
try {
|
|
1480
|
+
const relaunch = await launchChromeDebugInstanceImpl({
|
|
1481
|
+
host,
|
|
1482
|
+
port,
|
|
1483
|
+
url,
|
|
1484
|
+
slowLive,
|
|
1485
|
+
userDataDir
|
|
1486
|
+
});
|
|
1487
|
+
return {
|
|
1488
|
+
...relaunch,
|
|
1489
|
+
reused: false,
|
|
1490
|
+
...guard,
|
|
1491
|
+
required_flags_ok: true,
|
|
1492
|
+
replaced: true,
|
|
1493
|
+
close_method: closeResult.method || null,
|
|
1494
|
+
close_result: closeResult,
|
|
1495
|
+
relaunch: summarizeRelaunch(relaunch, "missing_required_flags")
|
|
1496
|
+
};
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
throw createChromeGuardError(
|
|
1499
|
+
`Chrome debug instance on port ${port} was closed for missing flags, but relaunch failed: ${error?.message || error}`,
|
|
1500
|
+
"CHROME_REQUIRED_FLAGS_RELAUNCH_FAILED",
|
|
1501
|
+
{
|
|
1502
|
+
...guard,
|
|
1503
|
+
close_method: closeResult.method || null,
|
|
1504
|
+
close_result: closeResult,
|
|
1505
|
+
relaunch: {
|
|
1506
|
+
reason: "missing_required_flags",
|
|
1507
|
+
launched: false,
|
|
1508
|
+
error: error?.message || String(error || "")
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
if (error?.chrome_guard) {
|
|
1515
|
+
throw error;
|
|
1516
|
+
}
|
|
1517
|
+
if (!launchIfMissing || !isChromeDebugUnavailableError(error)) {
|
|
1518
|
+
throw error;
|
|
1519
|
+
}
|
|
1520
|
+
try {
|
|
1521
|
+
const relaunch = await launchChromeDebugInstanceImpl({
|
|
1522
|
+
host,
|
|
1523
|
+
port,
|
|
1524
|
+
url,
|
|
1525
|
+
slowLive,
|
|
1526
|
+
userDataDir
|
|
1527
|
+
});
|
|
1528
|
+
return {
|
|
1529
|
+
...baseGuard,
|
|
1530
|
+
...relaunch,
|
|
1531
|
+
reused: false,
|
|
1532
|
+
required_flags_ok: true,
|
|
1533
|
+
relaunch: summarizeRelaunch(relaunch, "port_unreachable")
|
|
1534
|
+
};
|
|
1535
|
+
} catch (launchError) {
|
|
1536
|
+
throw createChromeGuardError(
|
|
1537
|
+
`Chrome debug port ${port} was unreachable and Chrome relaunch failed: ${launchError?.message || launchError}`,
|
|
1538
|
+
"CHROME_RELAUNCH_FAILED",
|
|
1539
|
+
{
|
|
1540
|
+
...baseGuard,
|
|
1541
|
+
required_flags_ok: false,
|
|
1542
|
+
relaunch: {
|
|
1543
|
+
reason: "port_unreachable",
|
|
1544
|
+
launched: false,
|
|
1545
|
+
error: launchError?.message || String(launchError || "")
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
export async function openChromeTarget({
|
|
1554
|
+
host = DEFAULT_CHROME_HOST,
|
|
1555
|
+
port = DEFAULT_CHROME_PORT,
|
|
1556
|
+
url
|
|
1557
|
+
} = {}) {
|
|
1558
|
+
const encodedUrl = encodeURIComponent(url || "about:blank");
|
|
1559
|
+
const endpoint = `http://${host}:${port}/json/new?${encodedUrl}`;
|
|
1560
|
+
const methods = ["PUT", "GET"];
|
|
1561
|
+
let lastError = null;
|
|
1562
|
+
for (const method of methods) {
|
|
1563
|
+
try {
|
|
1564
|
+
const response = await fetch(endpoint, { method });
|
|
1565
|
+
if (response.ok) {
|
|
1566
|
+
let payload = null;
|
|
1567
|
+
try {
|
|
1568
|
+
payload = await response.json();
|
|
1569
|
+
} catch {}
|
|
1570
|
+
return {
|
|
1571
|
+
ok: true,
|
|
1572
|
+
method,
|
|
1573
|
+
target_id: payload?.id || null,
|
|
1574
|
+
url: payload?.url || url || null
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
lastError = new Error(`DevTools /json/new returned ${response.status}`);
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
lastError = error;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
return {
|
|
1583
|
+
ok: false,
|
|
1584
|
+
error: lastError?.message || "Failed to open Chrome target"
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
export async function connectToChromeTargetOrOpen({
|
|
1589
|
+
host = DEFAULT_CHROME_HOST,
|
|
1590
|
+
port = DEFAULT_CHROME_PORT,
|
|
1591
|
+
targetUrlIncludes,
|
|
1592
|
+
targetPredicate,
|
|
1593
|
+
fallbackTargetPredicate,
|
|
1594
|
+
targetUrl,
|
|
1595
|
+
allowNavigate = true,
|
|
1596
|
+
slowLive = false,
|
|
1597
|
+
launchIfMissing = true,
|
|
1598
|
+
_deps = {}
|
|
1599
|
+
} = {}) {
|
|
1600
|
+
const ensureChromeDebugPortImpl = _deps.ensureChromeDebugPortImpl || ensureChromeDebugPort;
|
|
1601
|
+
const connectToChromeTargetImpl = _deps.connectToChromeTargetImpl || connectToChromeTarget;
|
|
1602
|
+
const openChromeTargetImpl = _deps.openChromeTargetImpl || openChromeTarget;
|
|
1603
|
+
let chrome = null;
|
|
1604
|
+
if (targetUrl) {
|
|
1605
|
+
chrome = await ensureChromeDebugPortImpl({
|
|
1606
|
+
host,
|
|
1607
|
+
port,
|
|
1608
|
+
url: targetUrl,
|
|
1609
|
+
slowLive,
|
|
1610
|
+
launchIfMissing: allowNavigate && launchIfMissing
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
try {
|
|
1615
|
+
const session = await connectToChromeTargetImpl({
|
|
1616
|
+
host,
|
|
1617
|
+
port,
|
|
1618
|
+
targetUrlIncludes,
|
|
1619
|
+
targetPredicate
|
|
1620
|
+
});
|
|
1621
|
+
return {
|
|
1622
|
+
...session,
|
|
1623
|
+
chrome: {
|
|
1624
|
+
...(chrome || { launched: false, reused: true, port }),
|
|
1625
|
+
target_created: false
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
} catch (primaryError) {
|
|
1629
|
+
if (!allowNavigate) throw primaryError;
|
|
1630
|
+
|
|
1631
|
+
if (typeof fallbackTargetPredicate === "function") {
|
|
1632
|
+
try {
|
|
1633
|
+
const session = await connectToChromeTargetImpl({
|
|
1634
|
+
host,
|
|
1635
|
+
port,
|
|
1636
|
+
targetPredicate: fallbackTargetPredicate
|
|
1637
|
+
});
|
|
1638
|
+
return {
|
|
1639
|
+
...session,
|
|
1640
|
+
chrome: {
|
|
1641
|
+
...(chrome || { launched: false, reused: true, port }),
|
|
1642
|
+
target_created: false,
|
|
1643
|
+
fallback_target: true
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
} catch {}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
let openAttempt = null;
|
|
1650
|
+
if (targetUrl) {
|
|
1651
|
+
openAttempt = await openChromeTargetImpl({ host, port, url: targetUrl });
|
|
1652
|
+
if (openAttempt.ok) {
|
|
1653
|
+
const session = await connectToChromeTargetImpl({
|
|
1654
|
+
host,
|
|
1655
|
+
port,
|
|
1656
|
+
targetPredicate: (target) => (
|
|
1657
|
+
(openAttempt.target_id && target?.id === openAttempt.target_id)
|
|
1658
|
+
|| String(target?.url || "").includes(targetUrlIncludes || targetUrl)
|
|
1659
|
+
|| (targetUrl.includes("zhipin.com") && String(target?.url || "").includes("zhipin.com"))
|
|
1660
|
+
)
|
|
1661
|
+
});
|
|
1662
|
+
return {
|
|
1663
|
+
...session,
|
|
1664
|
+
chrome: {
|
|
1665
|
+
...(chrome || { launched: false, reused: true, port }),
|
|
1666
|
+
target_created: true,
|
|
1667
|
+
open_attempt: openAttempt
|
|
1668
|
+
}
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
const session = await connectToChromeTargetImpl({
|
|
1674
|
+
host,
|
|
1675
|
+
port,
|
|
1676
|
+
targetPredicate: (target) => target?.type === "page"
|
|
1677
|
+
});
|
|
1678
|
+
return {
|
|
1679
|
+
...session,
|
|
1680
|
+
chrome: {
|
|
1681
|
+
...(chrome || { launched: false, reused: true, port }),
|
|
1682
|
+
target_created: false,
|
|
1683
|
+
open_attempt: openAttempt,
|
|
1684
|
+
fallback_any_page: true
|
|
1685
|
+
}
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
export function isClosedCdpTransportError(error) {
|
|
1691
|
+
return CDP_CLOSED_TRANSPORT_PATTERN.test(String(error?.message || error || ""));
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
function cloneCdpParams(params = {}) {
|
|
1695
|
+
if (!params || typeof params !== "object" || typeof params === "function") return params;
|
|
1696
|
+
try {
|
|
1697
|
+
return JSON.parse(JSON.stringify(params));
|
|
1698
|
+
} catch {
|
|
1699
|
+
return { ...params };
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function shouldReplayCdpSetupCall(domain, method) {
|
|
1704
|
+
return method === "enable"
|
|
1705
|
+
|| (domain === "Network" && method === "setCacheDisabled")
|
|
1706
|
+
|| (domain === "Page" && method === "bringToFront");
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
export function createGuardedCdpClient(client, { methodLog = [], reconnect = null } = {}) {
|
|
1710
|
+
let currentClient = client;
|
|
1711
|
+
let reconnectInFlight = null;
|
|
1712
|
+
const setupCalls = [];
|
|
1713
|
+
const eventSubscriptions = [];
|
|
1714
|
+
|
|
1715
|
+
async function replaySessionSetup(nextClient) {
|
|
1716
|
+
for (const call of setupCalls) {
|
|
1717
|
+
const fn = nextClient?.[call.domain]?.[call.method];
|
|
1718
|
+
if (typeof fn === "function") {
|
|
1719
|
+
await fn.call(nextClient[call.domain], cloneCdpParams(call.params));
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
for (const subscription of eventSubscriptions) {
|
|
1723
|
+
const fn = nextClient?.[subscription.domain]?.[subscription.event];
|
|
1724
|
+
if (typeof fn === "function") {
|
|
1725
|
+
fn.call(nextClient[subscription.domain], subscription.listener);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
async function reconnectClient() {
|
|
1731
|
+
if (typeof reconnect !== "function") return null;
|
|
1732
|
+
if (!reconnectInFlight) {
|
|
1733
|
+
reconnectInFlight = Promise.resolve()
|
|
1734
|
+
.then(() => reconnect())
|
|
1735
|
+
.then(async (nextClient) => {
|
|
1736
|
+
if (!nextClient) throw new Error("CDP reconnect returned no client");
|
|
1737
|
+
currentClient = nextClient;
|
|
1738
|
+
await replaySessionSetup(nextClient);
|
|
1739
|
+
return nextClient;
|
|
1740
|
+
})
|
|
1741
|
+
.finally(() => {
|
|
1742
|
+
reconnectInFlight = null;
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
return reconnectInFlight;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
async function invokeWithReconnect({
|
|
1749
|
+
methodNameForLog,
|
|
1750
|
+
invoke,
|
|
1751
|
+
retryable = true
|
|
1752
|
+
}) {
|
|
1753
|
+
recordMethod(methodLog, methodNameForLog);
|
|
1754
|
+
try {
|
|
1755
|
+
return await invoke(currentClient);
|
|
1756
|
+
} catch (error) {
|
|
1757
|
+
if (!retryable || !isClosedCdpTransportError(error) || typeof reconnect !== "function") {
|
|
1758
|
+
throw error;
|
|
1759
|
+
}
|
|
1760
|
+
await reconnectClient();
|
|
1761
|
+
recordMethod(methodLog, `${methodNameForLog}:retry_after_reconnect`);
|
|
1762
|
+
return invoke(currentClient);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
return new Proxy({}, {
|
|
1767
|
+
get(_target, property, receiver) {
|
|
1768
|
+
if (property === "send") {
|
|
1769
|
+
return async (method, params = {}) => {
|
|
1770
|
+
if (isForbiddenMethod(method)) {
|
|
1771
|
+
throw new Error(`Forbidden CDP method blocked: ${method}`);
|
|
1772
|
+
}
|
|
1773
|
+
return invokeWithReconnect({
|
|
1774
|
+
methodNameForLog: method,
|
|
1775
|
+
invoke: (activeClient) => activeClient.send(method, params)
|
|
1776
|
+
});
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
if (property === "close") {
|
|
1781
|
+
return async () => currentClient?.close?.();
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
if (property === "__rawClient") return currentClient;
|
|
1785
|
+
|
|
1786
|
+
const value = Reflect.get(currentClient, property, receiver);
|
|
1787
|
+
if (!value || typeof value !== "object") return value;
|
|
1788
|
+
|
|
1789
|
+
return new Proxy({}, {
|
|
1790
|
+
get(_domainTarget, method, domainReceiver) {
|
|
1791
|
+
const domainTarget = Reflect.get(currentClient, property, receiver);
|
|
1792
|
+
const domainValue = Reflect.get(domainTarget, method, domainReceiver);
|
|
1793
|
+
if (typeof domainValue !== "function") return domainValue;
|
|
1794
|
+
|
|
1795
|
+
return (params = {}) => {
|
|
1796
|
+
const fullMethod = methodName(property, method);
|
|
1797
|
+
if (isForbiddenMethod(fullMethod)) {
|
|
1798
|
+
throw new Error(`Forbidden CDP method blocked: ${fullMethod}`);
|
|
1799
|
+
}
|
|
1800
|
+
if (typeof params === "function") {
|
|
1801
|
+
eventSubscriptions.push({
|
|
1802
|
+
domain: property,
|
|
1803
|
+
event: method,
|
|
1804
|
+
listener: params
|
|
1805
|
+
});
|
|
1806
|
+
recordMethod(methodLog, fullMethod);
|
|
1807
|
+
return domainValue.call(domainTarget, params);
|
|
1808
|
+
}
|
|
1809
|
+
if (shouldReplayCdpSetupCall(property, method)) {
|
|
1810
|
+
setupCalls.push({
|
|
1811
|
+
domain: property,
|
|
1812
|
+
method,
|
|
1813
|
+
params: cloneCdpParams(params)
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
return invokeWithReconnect({
|
|
1817
|
+
methodNameForLog: fullMethod,
|
|
1818
|
+
invoke: (activeClient) => {
|
|
1819
|
+
const activeDomain = activeClient?.[property];
|
|
1820
|
+
const activeMethod = activeDomain?.[method];
|
|
1821
|
+
if (typeof activeMethod !== "function") {
|
|
1822
|
+
throw new Error(`CDP method is unavailable after reconnect: ${fullMethod}`);
|
|
1823
|
+
}
|
|
1824
|
+
return activeMethod.call(activeDomain, params);
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
export async function listChromeTargets({
|
|
1835
|
+
host = DEFAULT_CHROME_HOST,
|
|
1836
|
+
port = DEFAULT_CHROME_PORT
|
|
1837
|
+
} = {}) {
|
|
1838
|
+
return CDP.List({ host, port });
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
export async function connectToChromeTarget({
|
|
1842
|
+
host = DEFAULT_CHROME_HOST,
|
|
1843
|
+
port = DEFAULT_CHROME_PORT,
|
|
1844
|
+
targetUrlIncludes,
|
|
1845
|
+
targetPredicate
|
|
1846
|
+
} = {}) {
|
|
1847
|
+
const targets = await listChromeTargets({ host, port });
|
|
1848
|
+
const matcher = normalizeTargetMatcher({ targetUrlIncludes, targetPredicate });
|
|
1849
|
+
const target = targets.find(matcher);
|
|
1850
|
+
if (!target) {
|
|
1851
|
+
const urls = targets.map((item) => item.url).filter(Boolean).join("\n");
|
|
1852
|
+
throw new Error(`No matching Chrome target found on ${host}:${port}.\nAvailable targets:\n${urls}`);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
let rawClient = await CDP({ host, port, target });
|
|
1856
|
+
let activeTarget = target;
|
|
1857
|
+
const methodLog = [];
|
|
1858
|
+
const client = createGuardedCdpClient(rawClient, {
|
|
1859
|
+
methodLog,
|
|
1860
|
+
reconnect: async () => {
|
|
1861
|
+
const latestTargets = await listChromeTargets({ host, port });
|
|
1862
|
+
const nextTarget = activeTarget?.id
|
|
1863
|
+
? latestTargets.find((item) => item?.id === activeTarget.id)
|
|
1864
|
+
: latestTargets.find(matcher);
|
|
1865
|
+
if (!nextTarget) {
|
|
1866
|
+
const urls = latestTargets.map((item) => item.url).filter(Boolean).join("\n");
|
|
1867
|
+
throw new Error(`No matching Chrome target found while reconnecting to ${host}:${port}.\nAvailable targets:\n${urls}`);
|
|
1868
|
+
}
|
|
1869
|
+
try {
|
|
1870
|
+
await rawClient.close();
|
|
1871
|
+
} catch {}
|
|
1872
|
+
rawClient = await CDP({ host, port, target: nextTarget });
|
|
1873
|
+
activeTarget = nextTarget;
|
|
1874
|
+
return rawClient;
|
|
1875
|
+
}
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
return {
|
|
1879
|
+
client,
|
|
1880
|
+
get rawClient() {
|
|
1881
|
+
return rawClient;
|
|
1882
|
+
},
|
|
1883
|
+
get target() {
|
|
1884
|
+
return activeTarget;
|
|
1885
|
+
},
|
|
1886
|
+
methodLog,
|
|
1887
|
+
async close() {
|
|
1888
|
+
await rawClient.close();
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
export async function assertRuntimeEvaluateBlocked(client) {
|
|
1894
|
+
try {
|
|
1895
|
+
await client.Runtime.evaluate({ expression: "1" });
|
|
1896
|
+
} catch (error) {
|
|
1897
|
+
if (/Forbidden CDP method blocked: Runtime\.evaluate/.test(String(error?.message || ""))) {
|
|
1898
|
+
return { blocked: true, message: error.message };
|
|
1899
|
+
}
|
|
1900
|
+
throw error;
|
|
1901
|
+
}
|
|
1902
|
+
throw new Error("Runtime.evaluate was not blocked by the CDP guard");
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
export async function enableDomains(client, domains = ["Page", "DOM", "Input"]) {
|
|
1906
|
+
for (const domain of domains) {
|
|
1907
|
+
if (!ALLOWED_CDP_DOMAINS.has(domain)) {
|
|
1908
|
+
throw new Error(`CDP domain is not allowed by the CDP-only contract: ${domain}`);
|
|
1909
|
+
}
|
|
1910
|
+
if (typeof client?.[domain]?.enable === "function") {
|
|
1911
|
+
await client[domain].enable();
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
export async function bringPageToFront(client) {
|
|
1917
|
+
if (typeof client?.Page?.bringToFront === "function") {
|
|
1918
|
+
await client.Page.bringToFront();
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
export async function getPageFrameTree(client) {
|
|
1923
|
+
const result = await client.Page.getFrameTree();
|
|
1924
|
+
return result.frameTree || null;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
export async function getMainFrame(client) {
|
|
1928
|
+
const frameTree = await getPageFrameTree(client);
|
|
1929
|
+
return frameTree?.frame || null;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
export async function getMainFrameUrl(client) {
|
|
1933
|
+
const frame = await getMainFrame(client);
|
|
1934
|
+
return frame?.url || "";
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
export async function waitForMainFrameUrl(client, predicate, {
|
|
1938
|
+
timeoutMs = 10000,
|
|
1939
|
+
intervalMs = 250
|
|
1940
|
+
} = {}) {
|
|
1941
|
+
const started = Date.now();
|
|
1942
|
+
let lastUrl = "";
|
|
1943
|
+
while (Date.now() - started <= timeoutMs) {
|
|
1944
|
+
lastUrl = await getMainFrameUrl(client);
|
|
1945
|
+
if (predicate(lastUrl)) {
|
|
1946
|
+
return {
|
|
1947
|
+
ok: true,
|
|
1948
|
+
elapsed_ms: Date.now() - started,
|
|
1949
|
+
url: lastUrl
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
await sleep(intervalMs);
|
|
1953
|
+
}
|
|
1954
|
+
return {
|
|
1955
|
+
ok: false,
|
|
1956
|
+
elapsed_ms: Date.now() - started,
|
|
1957
|
+
url: lastUrl
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
export async function getDocumentRoot(client, { depth = 1, pierce = true } = {}) {
|
|
1962
|
+
const result = await client.DOM.getDocument({ depth, pierce });
|
|
1963
|
+
return result.root;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
export async function querySelector(client, nodeId, selector) {
|
|
1967
|
+
const result = await client.DOM.querySelector({ nodeId, selector });
|
|
1968
|
+
return result.nodeId || 0;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
export async function querySelectorAll(client, nodeId, selector) {
|
|
1972
|
+
const result = await client.DOM.querySelectorAll({ nodeId, selector });
|
|
1973
|
+
return result.nodeIds || [];
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
export async function findFirstNode(client, rootNodeId, selectors = []) {
|
|
1977
|
+
for (const selector of selectors) {
|
|
1978
|
+
const nodeId = await querySelector(client, rootNodeId, selector);
|
|
1979
|
+
if (nodeId) return { selector, nodeId };
|
|
1980
|
+
}
|
|
1981
|
+
return null;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
export async function describeNode(client, nodeId, { depth = 1, pierce = true } = {}) {
|
|
1985
|
+
const result = await client.DOM.describeNode({ nodeId, depth, pierce });
|
|
1986
|
+
return result.node;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
export async function getFrameDocumentNodeId(client, iframeNodeId) {
|
|
1990
|
+
const node = await describeNode(client, iframeNodeId, { depth: 1, pierce: true });
|
|
1991
|
+
const documentNodeId = node?.contentDocument?.nodeId;
|
|
1992
|
+
if (!documentNodeId) {
|
|
1993
|
+
throw new Error(`Node ${iframeNodeId} does not expose a contentDocument node`);
|
|
1994
|
+
}
|
|
1995
|
+
return documentNodeId;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
export async function findIframeDocument(client, rootNodeId, selectors = []) {
|
|
1999
|
+
const iframe = await findFirstNode(client, rootNodeId, selectors);
|
|
2000
|
+
if (!iframe) return null;
|
|
2001
|
+
const documentNodeId = await getFrameDocumentNodeId(client, iframe.nodeId);
|
|
2002
|
+
return { ...iframe, documentNodeId };
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
export async function getAttributesMap(client, nodeId) {
|
|
2006
|
+
const result = await client.DOM.getAttributes({ nodeId });
|
|
2007
|
+
const attributes = {};
|
|
2008
|
+
const raw = result.attributes || [];
|
|
2009
|
+
for (let index = 0; index < raw.length; index += 2) {
|
|
2010
|
+
attributes[raw[index]] = raw[index + 1] || "";
|
|
2011
|
+
}
|
|
2012
|
+
return attributes;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
export async function getOuterHTML(client, nodeId) {
|
|
2016
|
+
const result = await client.DOM.getOuterHTML({ nodeId });
|
|
2017
|
+
return result.outerHTML || "";
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
export async function getNodeBox(client, nodeId) {
|
|
2021
|
+
let result;
|
|
2022
|
+
try {
|
|
2023
|
+
result = await client.DOM.getBoxModel({ nodeId });
|
|
2024
|
+
} catch (error) {
|
|
2025
|
+
const wrapped = new Error(error?.message || String(error));
|
|
2026
|
+
wrapped.name = error?.name || "Error";
|
|
2027
|
+
wrapped.node_id = nodeId;
|
|
2028
|
+
wrapped.cdp_method = "DOM.getBoxModel";
|
|
2029
|
+
wrapped.original_stack = error?.stack || "";
|
|
2030
|
+
wrapped.stack = `${new Error(`getNodeBox failed for nodeId=${nodeId}`).stack || wrapped.stack}\nCaused by: ${error?.stack || error}`;
|
|
2031
|
+
throw wrapped;
|
|
2032
|
+
}
|
|
2033
|
+
const model = result.model;
|
|
2034
|
+
const quad = model.border?.length ? model.border : model.content;
|
|
2035
|
+
const xs = [quad[0], quad[2], quad[4], quad[6]];
|
|
2036
|
+
const ys = [quad[1], quad[3], quad[5], quad[7]];
|
|
2037
|
+
const minX = Math.min(...xs);
|
|
2038
|
+
const maxX = Math.max(...xs);
|
|
2039
|
+
const minY = Math.min(...ys);
|
|
2040
|
+
const maxY = Math.max(...ys);
|
|
2041
|
+
return {
|
|
2042
|
+
model,
|
|
2043
|
+
center: {
|
|
2044
|
+
x: (minX + maxX) / 2,
|
|
2045
|
+
y: (minY + maxY) / 2
|
|
2046
|
+
},
|
|
2047
|
+
rect: {
|
|
2048
|
+
x: minX,
|
|
2049
|
+
y: minY,
|
|
2050
|
+
width: maxX - minX,
|
|
2051
|
+
height: maxY - minY
|
|
2052
|
+
}
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
export async function simulateHumanClick(client, targetX, targetY, {
|
|
2057
|
+
button = "left",
|
|
2058
|
+
clickCount = 1,
|
|
2059
|
+
delayMs = 80,
|
|
2060
|
+
random = Math.random,
|
|
2061
|
+
sleepFn = sleep,
|
|
2062
|
+
moveSteps = 18,
|
|
2063
|
+
moveJitterPx = 3,
|
|
2064
|
+
hoverJitterPx = 5,
|
|
2065
|
+
moveDelayMinMs = 5,
|
|
2066
|
+
moveDelayMaxMs = 23,
|
|
2067
|
+
hoverDelayMinMs = 10,
|
|
2068
|
+
hoverDelayMaxMs = 30,
|
|
2069
|
+
prePressBaseMs = 260,
|
|
2070
|
+
prePressVarianceMs = 80,
|
|
2071
|
+
holdVarianceMs = 30,
|
|
2072
|
+
startPoint = null
|
|
2073
|
+
} = {}) {
|
|
2074
|
+
const target = normalizePoint({ x: targetX, y: targetY });
|
|
2075
|
+
if (!target) throw new Error("simulateHumanClick requires finite target coordinates");
|
|
2076
|
+
const nextRandom = normalizeRandom(random);
|
|
2077
|
+
const interactionConfig = getHumanInteractionConfig(client) || {};
|
|
2078
|
+
const start = normalizePoint(startPoint)
|
|
2079
|
+
|| normalizePoint(interactionConfig.lastMousePoint)
|
|
2080
|
+
|| {
|
|
2081
|
+
x: Math.max(0, target.x + randomBetween(nextRandom, -140, 140)),
|
|
2082
|
+
y: Math.max(0, target.y + randomBetween(nextRandom, -90, 90))
|
|
2083
|
+
};
|
|
2084
|
+
const path = generateBezierPath(start, target, {
|
|
2085
|
+
steps: moveSteps,
|
|
2086
|
+
random: nextRandom
|
|
2087
|
+
});
|
|
2088
|
+
const sleeper = typeof sleepFn === "function" ? sleepFn : sleep;
|
|
2089
|
+
const moveDelayMin = Math.min(moveDelayMinMs, moveDelayMaxMs);
|
|
2090
|
+
const moveDelayMax = Math.max(moveDelayMinMs, moveDelayMaxMs);
|
|
2091
|
+
const hoverDelayMin = Math.min(hoverDelayMinMs, hoverDelayMaxMs);
|
|
2092
|
+
const hoverDelayMax = Math.max(hoverDelayMinMs, hoverDelayMaxMs);
|
|
2093
|
+
for (const point of path) {
|
|
2094
|
+
await client.Input.dispatchMouseEvent({
|
|
2095
|
+
type: "mouseMoved",
|
|
2096
|
+
x: Math.round(point.x + randomBetween(nextRandom, -moveJitterPx / 2, moveJitterPx / 2)),
|
|
2097
|
+
y: Math.round(point.y + randomBetween(nextRandom, -moveJitterPx / 2, moveJitterPx / 2)),
|
|
2098
|
+
button: "none"
|
|
2099
|
+
});
|
|
2100
|
+
const pauseMs = Math.round(randomBetween(nextRandom, moveDelayMin, moveDelayMax));
|
|
2101
|
+
if (pauseMs > 0) await sleeper(pauseMs);
|
|
2102
|
+
}
|
|
2103
|
+
const hoverSteps = randomIntegerBetween(nextRandom, 3, 6);
|
|
2104
|
+
for (let index = 0; index < hoverSteps; index += 1) {
|
|
2105
|
+
await client.Input.dispatchMouseEvent({
|
|
2106
|
+
type: "mouseMoved",
|
|
2107
|
+
x: Math.round(target.x + randomBetween(nextRandom, -hoverJitterPx / 2, hoverJitterPx / 2)),
|
|
2108
|
+
y: Math.round(target.y + randomBetween(nextRandom, -hoverJitterPx / 2, hoverJitterPx / 2)),
|
|
2109
|
+
button: "none"
|
|
2110
|
+
});
|
|
2111
|
+
const pauseMs = Math.round(randomBetween(nextRandom, hoverDelayMin, hoverDelayMax));
|
|
2112
|
+
if (pauseMs > 0) await sleeper(pauseMs);
|
|
2113
|
+
}
|
|
2114
|
+
const prePressMs = humanDelay(prePressBaseMs, prePressVarianceMs, {
|
|
2115
|
+
minMs: 0,
|
|
2116
|
+
maxMs: Math.max(prePressBaseMs + prePressVarianceMs * 4, prePressBaseMs),
|
|
2117
|
+
random: nextRandom
|
|
2118
|
+
});
|
|
2119
|
+
if (prePressMs > 0) await sleeper(prePressMs);
|
|
2120
|
+
await client.Input.dispatchMouseEvent({ type: "mousePressed", x: target.x, y: target.y, button, clickCount });
|
|
2121
|
+
const holdMs = humanDelay(delayMs, holdVarianceMs, {
|
|
2122
|
+
minMs: 0,
|
|
2123
|
+
maxMs: Math.max(delayMs + holdVarianceMs * 4, delayMs),
|
|
2124
|
+
random: nextRandom
|
|
2125
|
+
});
|
|
2126
|
+
if (holdMs > 0) await sleeper(holdMs);
|
|
2127
|
+
await client.Input.dispatchMouseEvent({ type: "mouseReleased", x: target.x, y: target.y, button, clickCount });
|
|
2128
|
+
const latestConfig = getHumanInteractionConfig(client);
|
|
2129
|
+
if (latestConfig) latestConfig.lastMousePoint = target;
|
|
2130
|
+
return {
|
|
2131
|
+
mode: "human",
|
|
2132
|
+
path_points: path.length,
|
|
2133
|
+
hover_steps: hoverSteps,
|
|
2134
|
+
pre_press_ms: prePressMs,
|
|
2135
|
+
hold_ms: holdMs
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
export function resolveHumanClickPointForBox(box, {
|
|
2140
|
+
enabled = true,
|
|
2141
|
+
safeClickPointEnabled = true,
|
|
2142
|
+
random = Math.random,
|
|
2143
|
+
safeClickMinWidth = 44,
|
|
2144
|
+
safeClickMinHeight = 28,
|
|
2145
|
+
safeClickInsetRatio = 0.22,
|
|
2146
|
+
safeClickMinInsetPx = 4,
|
|
2147
|
+
safeClickMaxInsetPx = 18
|
|
2148
|
+
} = {}) {
|
|
2149
|
+
const center = normalizePoint(box?.center);
|
|
2150
|
+
if (!center) throw new Error("resolveHumanClickPointForBox requires a box center");
|
|
2151
|
+
const rect = box?.rect || {};
|
|
2152
|
+
const width = Number(rect.width);
|
|
2153
|
+
const height = Number(rect.height);
|
|
2154
|
+
const originX = Number(rect.x);
|
|
2155
|
+
const originY = Number(rect.y);
|
|
2156
|
+
if (
|
|
2157
|
+
enabled !== true
|
|
2158
|
+
|| safeClickPointEnabled === false
|
|
2159
|
+
|| !Number.isFinite(width)
|
|
2160
|
+
|| !Number.isFinite(height)
|
|
2161
|
+
|| !Number.isFinite(originX)
|
|
2162
|
+
|| !Number.isFinite(originY)
|
|
2163
|
+
|| width < Math.max(1, Number(safeClickMinWidth) || 44)
|
|
2164
|
+
|| height < Math.max(1, Number(safeClickMinHeight) || 28)
|
|
2165
|
+
) {
|
|
2166
|
+
return {
|
|
2167
|
+
x: center.x,
|
|
2168
|
+
y: center.y,
|
|
2169
|
+
mode: "center",
|
|
2170
|
+
reason: "small_or_disabled"
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
const nextRandom = normalizeRandom(random);
|
|
2175
|
+
const insetRatio = clampNumber(safeClickInsetRatio, 0.05, 0.45);
|
|
2176
|
+
const minInset = Math.max(0, Number(safeClickMinInsetPx) || 0);
|
|
2177
|
+
const maxInset = Math.max(minInset, Number(safeClickMaxInsetPx) || minInset);
|
|
2178
|
+
const insetX = Math.min(width / 2 - 1, Math.max(minInset, Math.min(maxInset, width * insetRatio)));
|
|
2179
|
+
const insetY = Math.min(height / 2 - 1, Math.max(minInset, Math.min(maxInset, height * insetRatio)));
|
|
2180
|
+
const usableWidth = Math.max(0, width - insetX * 2);
|
|
2181
|
+
const usableHeight = Math.max(0, height - insetY * 2);
|
|
2182
|
+
if (usableWidth <= 0 || usableHeight <= 0) {
|
|
2183
|
+
return {
|
|
2184
|
+
x: center.x,
|
|
2185
|
+
y: center.y,
|
|
2186
|
+
mode: "center",
|
|
2187
|
+
reason: "insufficient_safe_area"
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
return {
|
|
2191
|
+
x: originX + insetX + nextRandom() * usableWidth,
|
|
2192
|
+
y: originY + insetY + nextRandom() * usableHeight,
|
|
2193
|
+
mode: "safe_inset",
|
|
2194
|
+
inset_x: insetX,
|
|
2195
|
+
inset_y: insetY
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
export async function clickPoint(client, x, y, {
|
|
2200
|
+
button = "left",
|
|
2201
|
+
clickCount = 1,
|
|
2202
|
+
delayMs = 80,
|
|
2203
|
+
humanRestEnabled = null,
|
|
2204
|
+
humanInteraction = null
|
|
2205
|
+
} = {}) {
|
|
2206
|
+
const configured = getHumanInteractionConfig(client);
|
|
2207
|
+
const mergedHumanInteraction = {
|
|
2208
|
+
...(configured || {}),
|
|
2209
|
+
...(humanInteraction || {})
|
|
2210
|
+
};
|
|
2211
|
+
const humanEnabled = humanRestEnabled === true
|
|
2212
|
+
|| humanInteraction?.enabled === true
|
|
2213
|
+
|| (humanRestEnabled !== false && configured?.enabled === true);
|
|
2214
|
+
if (humanEnabled && mergedHumanInteraction.clickMovementEnabled !== false) {
|
|
2215
|
+
return simulateHumanClick(client, x, y, {
|
|
2216
|
+
...mergedHumanInteraction,
|
|
2217
|
+
button,
|
|
2218
|
+
clickCount,
|
|
2219
|
+
delayMs
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
await client.Input.dispatchMouseEvent({ type: "mouseMoved", x, y, button: "none" });
|
|
2223
|
+
await client.Input.dispatchMouseEvent({ type: "mousePressed", x, y, button, clickCount });
|
|
2224
|
+
if (delayMs > 0) await sleep(delayMs);
|
|
2225
|
+
await client.Input.dispatchMouseEvent({ type: "mouseReleased", x, y, button, clickCount });
|
|
2226
|
+
return {
|
|
2227
|
+
mode: "direct"
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
export async function scrollNodeIntoView(client, nodeId) {
|
|
2232
|
+
try {
|
|
2233
|
+
await client.DOM.scrollIntoViewIfNeeded({ nodeId });
|
|
2234
|
+
} catch (error) {
|
|
2235
|
+
const wrapped = new Error(error?.message || String(error));
|
|
2236
|
+
wrapped.name = error?.name || "Error";
|
|
2237
|
+
wrapped.node_id = nodeId;
|
|
2238
|
+
wrapped.cdp_method = "DOM.scrollIntoViewIfNeeded";
|
|
2239
|
+
wrapped.original_stack = error?.stack || "";
|
|
2240
|
+
wrapped.stack = `${new Error(`scrollNodeIntoView failed for nodeId=${nodeId}`).stack || wrapped.stack}\nCaused by: ${error?.stack || error}`;
|
|
2241
|
+
throw wrapped;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
export async function clickNodeCenter(client, nodeId, {
|
|
2246
|
+
scrollIntoView = false,
|
|
2247
|
+
...clickOptions
|
|
2248
|
+
} = {}) {
|
|
2249
|
+
if (scrollIntoView) {
|
|
2250
|
+
await scrollNodeIntoView(client, nodeId);
|
|
2251
|
+
await sleep(150);
|
|
2252
|
+
}
|
|
2253
|
+
const box = await getNodeBox(client, nodeId);
|
|
2254
|
+
const configured = getHumanInteractionConfig(client);
|
|
2255
|
+
const mergedHumanInteraction = {
|
|
2256
|
+
...(configured || {}),
|
|
2257
|
+
...(clickOptions.humanInteraction || {})
|
|
2258
|
+
};
|
|
2259
|
+
const humanClickPointEnabled = (
|
|
2260
|
+
clickOptions.humanRestEnabled === true
|
|
2261
|
+
|| clickOptions.humanInteraction?.enabled === true
|
|
2262
|
+
|| (clickOptions.humanRestEnabled !== false && configured?.enabled === true)
|
|
2263
|
+
) && mergedHumanInteraction.safeClickPointEnabled !== false;
|
|
2264
|
+
const clickPointTarget = humanClickPointEnabled
|
|
2265
|
+
? resolveHumanClickPointForBox(box, mergedHumanInteraction)
|
|
2266
|
+
: { ...box.center, mode: "center" };
|
|
2267
|
+
const clickResult = await clickPoint(client, clickPointTarget.x, clickPointTarget.y, clickOptions);
|
|
2268
|
+
return {
|
|
2269
|
+
...box,
|
|
2270
|
+
click_target: clickPointTarget,
|
|
2271
|
+
click_result: clickResult
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
export async function pressKey(client, key, {
|
|
2276
|
+
code = key,
|
|
2277
|
+
windowsVirtualKeyCode,
|
|
2278
|
+
nativeVirtualKeyCode = windowsVirtualKeyCode,
|
|
2279
|
+
text = "",
|
|
2280
|
+
modifiers = 0
|
|
2281
|
+
} = {}) {
|
|
2282
|
+
await client.Input.dispatchKeyEvent({
|
|
2283
|
+
type: "keyDown",
|
|
2284
|
+
key,
|
|
2285
|
+
code,
|
|
2286
|
+
windowsVirtualKeyCode,
|
|
2287
|
+
nativeVirtualKeyCode,
|
|
2288
|
+
text,
|
|
2289
|
+
modifiers
|
|
2290
|
+
});
|
|
2291
|
+
await client.Input.dispatchKeyEvent({
|
|
2292
|
+
type: "keyUp",
|
|
2293
|
+
key,
|
|
2294
|
+
code,
|
|
2295
|
+
windowsVirtualKeyCode,
|
|
2296
|
+
nativeVirtualKeyCode,
|
|
2297
|
+
modifiers
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
export function chunkHumanText(text, {
|
|
2302
|
+
random = Math.random,
|
|
2303
|
+
minLength = 1,
|
|
2304
|
+
maxLength = 5
|
|
2305
|
+
} = {}) {
|
|
2306
|
+
const chars = Array.from(String(text || ""));
|
|
2307
|
+
const min = Math.max(1, Math.floor(Number(minLength) || 1));
|
|
2308
|
+
const max = Math.max(min, Math.floor(Number(maxLength) || min));
|
|
2309
|
+
const nextRandom = normalizeRandom(random);
|
|
2310
|
+
const chunks = [];
|
|
2311
|
+
let index = 0;
|
|
2312
|
+
while (index < chars.length) {
|
|
2313
|
+
const remaining = chars.length - index;
|
|
2314
|
+
const size = Math.min(remaining, randomIntegerBetween(nextRandom, min, max));
|
|
2315
|
+
chunks.push(chars.slice(index, index + size).join(""));
|
|
2316
|
+
index += size;
|
|
2317
|
+
}
|
|
2318
|
+
return chunks;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
export async function insertText(client, text, {
|
|
2322
|
+
humanTextEntryEnabled = null,
|
|
2323
|
+
humanInteraction = null
|
|
2324
|
+
} = {}) {
|
|
2325
|
+
const value = String(text || "");
|
|
2326
|
+
const configured = getHumanInteractionConfig(client);
|
|
2327
|
+
const mergedHumanInteraction = {
|
|
2328
|
+
...(configured || {}),
|
|
2329
|
+
...(humanInteraction || {})
|
|
2330
|
+
};
|
|
2331
|
+
const textEntryEnabled = humanTextEntryEnabled === true
|
|
2332
|
+
|| humanInteraction?.textEntryEnabled === true
|
|
2333
|
+
|| (humanTextEntryEnabled !== false
|
|
2334
|
+
&& configured?.enabled === true
|
|
2335
|
+
&& configured?.textEntryEnabled !== false);
|
|
2336
|
+
if (!textEntryEnabled || value.length <= 1) {
|
|
2337
|
+
await client.Input.insertText({ text: value });
|
|
2338
|
+
return {
|
|
2339
|
+
mode: "direct",
|
|
2340
|
+
chunk_count: value ? 1 : 0
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
const chunks = chunkHumanText(value, {
|
|
2344
|
+
random: mergedHumanInteraction.random,
|
|
2345
|
+
minLength: mergedHumanInteraction.textChunkMinLength,
|
|
2346
|
+
maxLength: mergedHumanInteraction.textChunkMaxLength
|
|
2347
|
+
});
|
|
2348
|
+
const sleeper = typeof mergedHumanInteraction.sleepFn === "function"
|
|
2349
|
+
? mergedHumanInteraction.sleepFn
|
|
2350
|
+
: sleep;
|
|
2351
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
2352
|
+
await client.Input.insertText({ text: chunks[index] });
|
|
2353
|
+
if (index < chunks.length - 1) {
|
|
2354
|
+
const pauseMs = humanDelay(
|
|
2355
|
+
mergedHumanInteraction.textChunkDelayBaseMs,
|
|
2356
|
+
mergedHumanInteraction.textChunkDelayVarianceMs,
|
|
2357
|
+
{
|
|
2358
|
+
minMs: 0,
|
|
2359
|
+
maxMs: Math.max(
|
|
2360
|
+
mergedHumanInteraction.textChunkDelayBaseMs + mergedHumanInteraction.textChunkDelayVarianceMs * 4,
|
|
2361
|
+
mergedHumanInteraction.textChunkDelayBaseMs
|
|
2362
|
+
),
|
|
2363
|
+
random: mergedHumanInteraction.random
|
|
2364
|
+
}
|
|
2365
|
+
);
|
|
2366
|
+
if (pauseMs > 0) await sleeper(pauseMs);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
return {
|
|
2370
|
+
mode: "chunked",
|
|
2371
|
+
chunk_count: chunks.length,
|
|
2372
|
+
chunks
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
export async function selectAllFocusedText(client) {
|
|
2377
|
+
await pressKey(client, "a", {
|
|
2378
|
+
code: "KeyA",
|
|
2379
|
+
windowsVirtualKeyCode: 65,
|
|
2380
|
+
nativeVirtualKeyCode: 65,
|
|
2381
|
+
modifiers: 2
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
export async function clearFocusedInput(client) {
|
|
2386
|
+
await selectAllFocusedText(client);
|
|
2387
|
+
await pressKey(client, "Backspace", {
|
|
2388
|
+
code: "Backspace",
|
|
2389
|
+
windowsVirtualKeyCode: 8,
|
|
2390
|
+
nativeVirtualKeyCode: 8
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
export async function waitForSelector(client, nodeId, selector, {
|
|
2395
|
+
timeoutMs = 5000,
|
|
2396
|
+
intervalMs = 150
|
|
2397
|
+
} = {}) {
|
|
2398
|
+
const started = Date.now();
|
|
2399
|
+
while (Date.now() - started <= timeoutMs) {
|
|
2400
|
+
const foundNodeId = await querySelector(client, nodeId, selector);
|
|
2401
|
+
if (foundNodeId) return foundNodeId;
|
|
2402
|
+
await sleep(intervalMs);
|
|
2403
|
+
}
|
|
2404
|
+
return 0;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
export async function countSelectors(client, nodeId, selectors = {}) {
|
|
2408
|
+
const counts = {};
|
|
2409
|
+
for (const [name, selector] of Object.entries(selectors)) {
|
|
2410
|
+
counts[name] = (await querySelectorAll(client, nodeId, selector)).length;
|
|
2411
|
+
}
|
|
2412
|
+
return counts;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
export async function getAccessibilityTree(client, options = {}) {
|
|
2416
|
+
return client.Accessibility.getFullAXTree(options);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
export async function sleep(ms) {
|
|
2420
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
2421
|
+
}
|