@reconcrap/boss-recommend-mcp 2.0.2 → 2.0.4
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 +3 -3
- package/config/screening-config.example.json +10 -8
- package/package.json +1 -1
- package/skills/boss-chat/SKILL.md +3 -0
- package/skills/boss-recommend-pipeline/SKILL.md +9 -2
- package/skills/boss-recruit-pipeline/SKILL.md +3 -0
- package/src/chat-mcp.js +160 -41
- package/src/chat-runtime-config.js +28 -1
- package/src/core/browser/index.js +392 -0
- package/src/core/screening/index.js +3 -3
- package/src/index.js +4 -2
- package/src/recommend-mcp.js +142 -33
- package/src/recruit-mcp.js +157 -30
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
1
5
|
import CDP from "chrome-remote-interface";
|
|
2
6
|
|
|
3
7
|
export const DEFAULT_CHROME_HOST = "127.0.0.1";
|
|
4
8
|
export const DEFAULT_CHROME_PORT = 9222;
|
|
9
|
+
export const BOSS_LOGIN_URL = "https://www.zhipin.com/web/user/?ka=bticket";
|
|
5
10
|
|
|
6
11
|
export const ALLOWED_CDP_DOMAINS = new Set([
|
|
7
12
|
"Accessibility",
|
|
@@ -14,6 +19,21 @@ export const ALLOWED_CDP_DOMAINS = new Set([
|
|
|
14
19
|
|
|
15
20
|
export const FORBIDDEN_CDP_DOMAINS = new Set(["Runtime"]);
|
|
16
21
|
|
|
22
|
+
const BOSS_LOGIN_URL_PATTERN = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com|login\.zhipin\.com)/i;
|
|
23
|
+
const BOSS_LOGIN_TEXT_PATTERN = /扫码登录|验证码登录|密码登录|登录后|请登录|登录BOSS直聘|Boss登录|BOSS登录/i;
|
|
24
|
+
const CHROME_DEBUG_UNAVAILABLE_PATTERN = /ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|connect|socket hang up/i;
|
|
25
|
+
const BOSS_LOGIN_DOM_SELECTORS = [
|
|
26
|
+
".login-box",
|
|
27
|
+
".login-form",
|
|
28
|
+
".login-dialog",
|
|
29
|
+
".sign-form",
|
|
30
|
+
".qrcode-box",
|
|
31
|
+
".user-login",
|
|
32
|
+
"input[name='phone']",
|
|
33
|
+
"input[placeholder*='手机号']",
|
|
34
|
+
"input[placeholder*='验证码']"
|
|
35
|
+
];
|
|
36
|
+
|
|
17
37
|
function nowIso() {
|
|
18
38
|
return new Date().toISOString();
|
|
19
39
|
}
|
|
@@ -49,6 +69,378 @@ export function assertNoForbiddenCdpCalls(methodLog = []) {
|
|
|
49
69
|
}
|
|
50
70
|
}
|
|
51
71
|
|
|
72
|
+
export function isBossLoginUrl(url) {
|
|
73
|
+
return BOSS_LOGIN_URL_PATTERN.test(String(url || ""));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createBossLoginRequiredError({
|
|
77
|
+
domain = "boss",
|
|
78
|
+
currentUrl = "",
|
|
79
|
+
targetUrl = "",
|
|
80
|
+
loginUrl = BOSS_LOGIN_URL,
|
|
81
|
+
loginDetection = null,
|
|
82
|
+
chrome = null
|
|
83
|
+
} = {}) {
|
|
84
|
+
const error = new Error(`Boss login is required before starting the ${domain} run.`);
|
|
85
|
+
error.code = "BOSS_LOGIN_REQUIRED";
|
|
86
|
+
error.requires_login = true;
|
|
87
|
+
error.current_url = currentUrl || null;
|
|
88
|
+
error.target_url = targetUrl || null;
|
|
89
|
+
error.login_url = loginUrl;
|
|
90
|
+
error.login_detection = loginDetection || null;
|
|
91
|
+
error.chrome = chrome || null;
|
|
92
|
+
error.retryable = true;
|
|
93
|
+
return error;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function detectBossLoginState(client, { currentUrl = "" } = {}) {
|
|
97
|
+
const inspectedUrl = currentUrl || await getMainFrameUrl(client).catch(() => "");
|
|
98
|
+
if (isBossLoginUrl(inspectedUrl)) {
|
|
99
|
+
return {
|
|
100
|
+
requires_login: true,
|
|
101
|
+
reason: "url",
|
|
102
|
+
current_url: inspectedUrl,
|
|
103
|
+
matched_selectors: []
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let root = null;
|
|
108
|
+
try {
|
|
109
|
+
root = await getDocumentRoot(client, { depth: 1, pierce: true });
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return {
|
|
112
|
+
requires_login: false,
|
|
113
|
+
reason: "dom_unavailable",
|
|
114
|
+
current_url: inspectedUrl,
|
|
115
|
+
error: error?.message || String(error || "")
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const matchedSelectors = [];
|
|
120
|
+
for (const selector of BOSS_LOGIN_DOM_SELECTORS) {
|
|
121
|
+
const nodeId = await querySelector(client, root.nodeId, selector).catch(() => 0);
|
|
122
|
+
if (nodeId) matchedSelectors.push(selector);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (matchedSelectors.length === 0) {
|
|
126
|
+
return {
|
|
127
|
+
requires_login: false,
|
|
128
|
+
reason: "no_login_dom",
|
|
129
|
+
current_url: inspectedUrl,
|
|
130
|
+
matched_selectors: []
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const html = await getOuterHTML(client, root.nodeId).catch(() => "");
|
|
135
|
+
const looksLikeLogin = BOSS_LOGIN_TEXT_PATTERN.test(html);
|
|
136
|
+
return {
|
|
137
|
+
requires_login: looksLikeLogin,
|
|
138
|
+
reason: looksLikeLogin ? "dom" : "login_selector_without_login_text",
|
|
139
|
+
current_url: inspectedUrl,
|
|
140
|
+
matched_selectors: matchedSelectors
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function isChromeDebugUnavailableError(error) {
|
|
145
|
+
return CHROME_DEBUG_UNAVAILABLE_PATTERN.test(String(error?.message || error || ""));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function pathExists(targetPath) {
|
|
149
|
+
try {
|
|
150
|
+
return Boolean(targetPath) && fs.existsSync(targetPath);
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function ensureDir(targetPath) {
|
|
157
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isLocalChromeHost(host) {
|
|
161
|
+
const normalized = String(host || "").trim().toLowerCase();
|
|
162
|
+
return !normalized || normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getCodexHome() {
|
|
166
|
+
return process.env.CODEX_HOME
|
|
167
|
+
? path.resolve(process.env.CODEX_HOME)
|
|
168
|
+
: path.join(os.homedir(), ".codex");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getDefaultChromeExecutableCandidates() {
|
|
172
|
+
const candidates = [
|
|
173
|
+
process.env.BOSS_MCP_CHROME_PATH,
|
|
174
|
+
process.env.BOSS_RECOMMEND_CHROME_PATH
|
|
175
|
+
].filter(Boolean);
|
|
176
|
+
if (process.platform === "win32") {
|
|
177
|
+
candidates.push(
|
|
178
|
+
path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
179
|
+
path.join(process.env.ProgramFiles || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
180
|
+
path.join(process.env["ProgramFiles(x86)"] || "", "Google", "Chrome", "Application", "chrome.exe")
|
|
181
|
+
);
|
|
182
|
+
} else if (process.platform === "darwin") {
|
|
183
|
+
candidates.push(
|
|
184
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
185
|
+
path.join(os.homedir(), "Applications", "Google Chrome.app", "Contents", "MacOS", "Google Chrome"),
|
|
186
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
187
|
+
);
|
|
188
|
+
} else {
|
|
189
|
+
candidates.push(
|
|
190
|
+
"/usr/bin/google-chrome",
|
|
191
|
+
"/usr/bin/google-chrome-stable",
|
|
192
|
+
"/usr/bin/chromium-browser",
|
|
193
|
+
"/usr/bin/chromium",
|
|
194
|
+
"/snap/bin/chromium"
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return Array.from(new Set(candidates.filter(Boolean)));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function getChromeExecutable() {
|
|
201
|
+
return getDefaultChromeExecutableCandidates().find((candidate) => pathExists(candidate)) || null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function getBossChromeUserDataDir(port = DEFAULT_CHROME_PORT) {
|
|
205
|
+
const sharedPath = path.join(getCodexHome(), "boss-mcp", `chrome-profile-${port}`);
|
|
206
|
+
ensureDir(sharedPath);
|
|
207
|
+
return sharedPath;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function waitForChromeDebugPort({
|
|
211
|
+
host = DEFAULT_CHROME_HOST,
|
|
212
|
+
port = DEFAULT_CHROME_PORT,
|
|
213
|
+
timeoutMs = 8000,
|
|
214
|
+
intervalMs = 300
|
|
215
|
+
} = {}) {
|
|
216
|
+
const started = Date.now();
|
|
217
|
+
let lastError = null;
|
|
218
|
+
while (Date.now() - started <= timeoutMs) {
|
|
219
|
+
try {
|
|
220
|
+
const targets = await listChromeTargets({ host, port });
|
|
221
|
+
return {
|
|
222
|
+
ok: true,
|
|
223
|
+
elapsed_ms: Date.now() - started,
|
|
224
|
+
targets
|
|
225
|
+
};
|
|
226
|
+
} catch (error) {
|
|
227
|
+
lastError = error;
|
|
228
|
+
await sleep(intervalMs);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
elapsed_ms: Date.now() - started,
|
|
234
|
+
error: lastError?.message || String(lastError || "Chrome debug port did not become ready")
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function launchChromeDebugInstance({
|
|
239
|
+
host = DEFAULT_CHROME_HOST,
|
|
240
|
+
port = DEFAULT_CHROME_PORT,
|
|
241
|
+
url = "about:blank",
|
|
242
|
+
slowLive = false
|
|
243
|
+
} = {}) {
|
|
244
|
+
if (!isLocalChromeHost(host)) {
|
|
245
|
+
throw new Error(`Cannot auto-launch Chrome for non-local debug host: ${host}`);
|
|
246
|
+
}
|
|
247
|
+
const chromePath = getChromeExecutable();
|
|
248
|
+
if (!chromePath) {
|
|
249
|
+
throw new Error("Chrome executable not found. Set BOSS_MCP_CHROME_PATH or BOSS_RECOMMEND_CHROME_PATH.");
|
|
250
|
+
}
|
|
251
|
+
const userDataDir = getBossChromeUserDataDir(port);
|
|
252
|
+
const args = [
|
|
253
|
+
`--remote-debugging-port=${port}`,
|
|
254
|
+
`--user-data-dir=${userDataDir}`,
|
|
255
|
+
"--no-first-run",
|
|
256
|
+
"--no-default-browser-check",
|
|
257
|
+
"--new-window",
|
|
258
|
+
url
|
|
259
|
+
];
|
|
260
|
+
const child = spawn(chromePath, args, {
|
|
261
|
+
detached: true,
|
|
262
|
+
stdio: "ignore",
|
|
263
|
+
windowsHide: false
|
|
264
|
+
});
|
|
265
|
+
child.unref();
|
|
266
|
+
const readiness = await waitForChromeDebugPort({
|
|
267
|
+
host,
|
|
268
|
+
port,
|
|
269
|
+
timeoutMs: slowLive ? 30000 : 12000,
|
|
270
|
+
intervalMs: slowLive ? 700 : 300
|
|
271
|
+
});
|
|
272
|
+
if (!readiness.ok) {
|
|
273
|
+
throw new Error(`Chrome launched but DevTools port ${port} did not become reachable: ${readiness.error}`);
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
launched: true,
|
|
277
|
+
chrome_path: chromePath,
|
|
278
|
+
user_data_dir: userDataDir,
|
|
279
|
+
port,
|
|
280
|
+
url,
|
|
281
|
+
readiness: {
|
|
282
|
+
elapsed_ms: readiness.elapsed_ms,
|
|
283
|
+
target_count: readiness.targets.length
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function ensureChromeDebugPort({
|
|
289
|
+
host = DEFAULT_CHROME_HOST,
|
|
290
|
+
port = DEFAULT_CHROME_PORT,
|
|
291
|
+
url = "about:blank",
|
|
292
|
+
slowLive = false,
|
|
293
|
+
launchIfMissing = true
|
|
294
|
+
} = {}) {
|
|
295
|
+
try {
|
|
296
|
+
const targets = await listChromeTargets({ host, port });
|
|
297
|
+
return {
|
|
298
|
+
launched: false,
|
|
299
|
+
reused: true,
|
|
300
|
+
port,
|
|
301
|
+
target_count: targets.length
|
|
302
|
+
};
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (!launchIfMissing || !isChromeDebugUnavailableError(error)) {
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
return launchChromeDebugInstance({ host, port, url, slowLive });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export async function openChromeTarget({
|
|
312
|
+
host = DEFAULT_CHROME_HOST,
|
|
313
|
+
port = DEFAULT_CHROME_PORT,
|
|
314
|
+
url
|
|
315
|
+
} = {}) {
|
|
316
|
+
const encodedUrl = encodeURIComponent(url || "about:blank");
|
|
317
|
+
const endpoint = `http://${host}:${port}/json/new?${encodedUrl}`;
|
|
318
|
+
const methods = ["PUT", "GET"];
|
|
319
|
+
let lastError = null;
|
|
320
|
+
for (const method of methods) {
|
|
321
|
+
try {
|
|
322
|
+
const response = await fetch(endpoint, { method });
|
|
323
|
+
if (response.ok) {
|
|
324
|
+
let payload = null;
|
|
325
|
+
try {
|
|
326
|
+
payload = await response.json();
|
|
327
|
+
} catch {}
|
|
328
|
+
return {
|
|
329
|
+
ok: true,
|
|
330
|
+
method,
|
|
331
|
+
target_id: payload?.id || null,
|
|
332
|
+
url: payload?.url || url || null
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
lastError = new Error(`DevTools /json/new returned ${response.status}`);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
lastError = error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
ok: false,
|
|
342
|
+
error: lastError?.message || "Failed to open Chrome target"
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export async function connectToChromeTargetOrOpen({
|
|
347
|
+
host = DEFAULT_CHROME_HOST,
|
|
348
|
+
port = DEFAULT_CHROME_PORT,
|
|
349
|
+
targetUrlIncludes,
|
|
350
|
+
targetPredicate,
|
|
351
|
+
fallbackTargetPredicate,
|
|
352
|
+
targetUrl,
|
|
353
|
+
allowNavigate = true,
|
|
354
|
+
slowLive = false,
|
|
355
|
+
launchIfMissing = true
|
|
356
|
+
} = {}) {
|
|
357
|
+
let chrome = null;
|
|
358
|
+
if (allowNavigate && targetUrl) {
|
|
359
|
+
chrome = await ensureChromeDebugPort({
|
|
360
|
+
host,
|
|
361
|
+
port,
|
|
362
|
+
url: targetUrl,
|
|
363
|
+
slowLive,
|
|
364
|
+
launchIfMissing
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const session = await connectToChromeTarget({
|
|
370
|
+
host,
|
|
371
|
+
port,
|
|
372
|
+
targetUrlIncludes,
|
|
373
|
+
targetPredicate
|
|
374
|
+
});
|
|
375
|
+
return {
|
|
376
|
+
...session,
|
|
377
|
+
chrome: {
|
|
378
|
+
...(chrome || { launched: false, reused: true, port }),
|
|
379
|
+
target_created: false
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
} catch (primaryError) {
|
|
383
|
+
if (!allowNavigate) throw primaryError;
|
|
384
|
+
|
|
385
|
+
if (typeof fallbackTargetPredicate === "function") {
|
|
386
|
+
try {
|
|
387
|
+
const session = await connectToChromeTarget({
|
|
388
|
+
host,
|
|
389
|
+
port,
|
|
390
|
+
targetPredicate: fallbackTargetPredicate
|
|
391
|
+
});
|
|
392
|
+
return {
|
|
393
|
+
...session,
|
|
394
|
+
chrome: {
|
|
395
|
+
...(chrome || { launched: false, reused: true, port }),
|
|
396
|
+
target_created: false,
|
|
397
|
+
fallback_target: true
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
} catch {}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
let openAttempt = null;
|
|
404
|
+
if (targetUrl) {
|
|
405
|
+
openAttempt = await openChromeTarget({ host, port, url: targetUrl });
|
|
406
|
+
if (openAttempt.ok) {
|
|
407
|
+
const session = await connectToChromeTarget({
|
|
408
|
+
host,
|
|
409
|
+
port,
|
|
410
|
+
targetPredicate: (target) => (
|
|
411
|
+
(openAttempt.target_id && target?.id === openAttempt.target_id)
|
|
412
|
+
|| String(target?.url || "").includes(targetUrlIncludes || targetUrl)
|
|
413
|
+
|| (targetUrl.includes("zhipin.com") && String(target?.url || "").includes("zhipin.com"))
|
|
414
|
+
)
|
|
415
|
+
});
|
|
416
|
+
return {
|
|
417
|
+
...session,
|
|
418
|
+
chrome: {
|
|
419
|
+
...(chrome || { launched: false, reused: true, port }),
|
|
420
|
+
target_created: true,
|
|
421
|
+
open_attempt: openAttempt
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const session = await connectToChromeTarget({
|
|
428
|
+
host,
|
|
429
|
+
port,
|
|
430
|
+
targetPredicate: (target) => target?.type === "page"
|
|
431
|
+
});
|
|
432
|
+
return {
|
|
433
|
+
...session,
|
|
434
|
+
chrome: {
|
|
435
|
+
...(chrome || { launched: false, reused: true, port }),
|
|
436
|
+
target_created: false,
|
|
437
|
+
open_attempt: openAttempt,
|
|
438
|
+
fallback_any_page: true
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
52
444
|
export function createGuardedCdpClient(client, { methodLog = [] } = {}) {
|
|
53
445
|
return new Proxy(client, {
|
|
54
446
|
get(target, property, receiver) {
|
|
@@ -37,7 +37,7 @@ const GENDER_CODE_MAP = {
|
|
|
37
37
|
2: "女"
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
const LLM_THINKING_LEVELS = new Set(["off", "low", "medium", "high", "current"]);
|
|
40
|
+
const LLM_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "auto", "current"]);
|
|
41
41
|
|
|
42
42
|
function nowIso() {
|
|
43
43
|
return new Date().toISOString();
|
|
@@ -64,9 +64,9 @@ function isVolcengineModel(baseUrl, model) {
|
|
|
64
64
|
|
|
65
65
|
function applyChatCompletionThinking(payload, { baseUrl = "", model = "", thinkingLevel = "" } = {}) {
|
|
66
66
|
const level = normalizeLlmThinkingLevel(thinkingLevel);
|
|
67
|
-
if (!level || level === "current") return payload;
|
|
67
|
+
if (!level || level === "current" || level === "auto") return payload;
|
|
68
68
|
if (isVolcengineModel(baseUrl, model)) {
|
|
69
|
-
if (level === "off") {
|
|
69
|
+
if (level === "off" || level === "minimal") {
|
|
70
70
|
payload.thinking = { type: "disabled" };
|
|
71
71
|
} else {
|
|
72
72
|
payload.thinking = { type: "enabled" };
|
package/src/index.js
CHANGED
|
@@ -7,7 +7,8 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import {
|
|
8
8
|
getFeaturedCalibrationResolution,
|
|
9
9
|
getBossChatTargetCountValue,
|
|
10
|
-
normalizeTargetCountInput
|
|
10
|
+
normalizeTargetCountInput,
|
|
11
|
+
resolveBossScreeningConfig
|
|
11
12
|
} from "./chat-runtime-config.js";
|
|
12
13
|
import {
|
|
13
14
|
__resetChatMcpStateForTests,
|
|
@@ -1890,7 +1891,8 @@ async function handleRunRecommendSelfHealTool({ workspaceRoot, args }) {
|
|
|
1890
1891
|
}
|
|
1891
1892
|
|
|
1892
1893
|
const host = "127.0.0.1";
|
|
1893
|
-
const
|
|
1894
|
+
const configResolution = resolveBossScreeningConfig(workspaceRoot);
|
|
1895
|
+
const port = parsePositiveInteger(args.port, configResolution.ok ? configResolution.config.debugPort : 9222);
|
|
1894
1896
|
let session = null;
|
|
1895
1897
|
try {
|
|
1896
1898
|
session = await connectToChromeTarget({
|