@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.
@@ -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 port = parsePositiveInteger(args.port, 9222);
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({