@reconcrap/boss-recommend-mcp 1.1.8 → 1.1.9

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/src/adapters.js CHANGED
@@ -12,12 +12,16 @@ const bossLoginUrl = "https://www.zhipin.com/web/user/?ka=bticket";
12
12
  const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|history-sync|settings\/syncSetup)/i;
13
13
  const bossLoginUrlPattern = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
14
14
  const bossLoginTitlePattern = /登录|signin|扫码登录|BOSS直聘登录/i;
15
- const screenConfigTemplateDefaults = {
16
- baseUrl: "https://api.openai.com/v1",
17
- apiKey: "replace-with-openai-api-key",
18
- model: "gpt-4.1-mini"
19
- };
20
- const DEFAULT_RECOMMEND_SCREEN_TIMEOUT_MS = 24 * 60 * 60 * 1000;
15
+ const screenConfigTemplateDefaults = {
16
+ baseUrl: "https://api.openai.com/v1",
17
+ apiKey: "replace-with-openai-api-key",
18
+ model: "gpt-4.1-mini"
19
+ };
20
+ const DEFAULT_RECOMMEND_SCREEN_TIMEOUT_MS = 24 * 60 * 60 * 1000;
21
+ const PAGE_SCOPE_TO_TAB_STATUS = {
22
+ recommend: "0",
23
+ featured: "3"
24
+ };
21
25
 
22
26
  function getCodexHome() {
23
27
  return process.env.CODEX_HOME
@@ -35,13 +39,17 @@ function getUserConfigPath() {
35
39
  return path.join(getStateHome(), "screening-config.json");
36
40
  }
37
41
 
38
- function getLegacyUserConfigPath() {
39
- return path.join(getCodexHome(), "boss-recommend-mcp", "screening-config.json");
40
- }
41
-
42
- function getDesktopDir() {
43
- return path.join(os.homedir(), "Desktop");
44
- }
42
+ function getLegacyUserConfigPath() {
43
+ return path.join(getCodexHome(), "boss-recommend-mcp", "screening-config.json");
44
+ }
45
+
46
+ function getUserCalibrationPath() {
47
+ return path.join(getCodexHome(), "boss-recommend-mcp", "favorite-calibration.json");
48
+ }
49
+
50
+ function getDesktopDir() {
51
+ return path.join(os.homedir(), "Desktop");
52
+ }
45
53
 
46
54
  function ensureDir(targetPath) {
47
55
  fs.mkdirSync(targetPath, { recursive: true });
@@ -64,6 +72,14 @@ function normalizeText(value) {
64
72
  return String(value || "").replace(/\s+/g, " ").trim();
65
73
  }
66
74
 
75
+ function normalizePageScope(value) {
76
+ const normalized = normalizeText(value).toLowerCase();
77
+ if (!normalized) return null;
78
+ if (["recommend", "推荐", "推荐页", "推荐页面"].includes(normalized)) return "recommend";
79
+ if (["featured", "精选", "精选页", "精选页面", "精选牛人"].includes(normalized)) return "featured";
80
+ return null;
81
+ }
82
+
67
83
  function shouldBringChromeToFront() {
68
84
  const envValue = normalizeText(process.env.BOSS_RECOMMEND_BRING_TO_FRONT || "").toLowerCase();
69
85
  if (envValue) {
@@ -245,11 +261,11 @@ export function getScreenConfigResolution(workspaceRoot) {
245
261
  };
246
262
  }
247
263
 
248
- function readJsonFile(filePath) {
249
- if (!filePath || !pathExists(filePath)) return null;
250
- try {
251
- const raw = fs.readFileSync(filePath, "utf8");
252
- return JSON.parse(raw);
264
+ function readJsonFile(filePath) {
265
+ if (!filePath || !pathExists(filePath)) return null;
266
+ try {
267
+ const raw = fs.readFileSync(filePath, "utf8");
268
+ return JSON.parse(raw);
253
269
  } catch {
254
270
  return null;
255
271
  }
@@ -432,19 +448,83 @@ function resolveRecommendScreenCliEntry(screenDir) {
432
448
  return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
433
449
  }
434
450
 
435
- function resolveRecommendSearchCliEntry(searchDir) {
436
- const candidates = [
437
- path.join(searchDir, "src", "cli.js"),
438
- path.join(searchDir, "src", "cli.cjs")
451
+ function resolveRecommendSearchCliEntry(searchDir) {
452
+ const candidates = [
453
+ path.join(searchDir, "src", "cli.js"),
454
+ path.join(searchDir, "src", "cli.cjs")
439
455
  ];
440
- return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
441
- }
442
-
443
- function safeInvokeCallback(callback, payload) {
444
- if (typeof callback !== "function") return;
445
- try {
446
- callback(payload);
447
- } catch {
456
+ return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
457
+ }
458
+
459
+ function parseKeyValueOutput(text) {
460
+ const result = {};
461
+ for (const line of String(text || "").split(/\r?\n/)) {
462
+ const trimmed = line.trim();
463
+ if (!trimmed || trimmed.startsWith("#")) continue;
464
+ const sep = trimmed.indexOf("=");
465
+ if (sep <= 0) continue;
466
+ const key = trimmed.slice(0, sep).trim();
467
+ const value = trimmed.slice(sep + 1).trim();
468
+ if (!key) continue;
469
+ result[key] = value;
470
+ }
471
+ return result;
472
+ }
473
+
474
+ function runBossRecruitWhere() {
475
+ const direct = runProcessSync({
476
+ command: "boss-recruit-mcp",
477
+ args: ["where"]
478
+ });
479
+ if (direct.ok) {
480
+ return parseKeyValueOutput(direct.stdout);
481
+ }
482
+
483
+ if (process.platform !== "win32") {
484
+ return null;
485
+ }
486
+
487
+ const fallback = runProcessSync({
488
+ command: "cmd.exe",
489
+ args: ["/d", "/s", "/c", "boss-recruit-mcp where"]
490
+ });
491
+ if (!fallback.ok) return null;
492
+ return parseKeyValueOutput(fallback.stdout);
493
+ }
494
+
495
+ function resolveRecruitCalibrationScriptPath(workspaceRoot) {
496
+ const fromEnv = normalizeText(process.env.BOSS_RECOMMEND_RECRUIT_CALIBRATION_SCRIPT || "");
497
+ const fromWhere = runBossRecruitWhere();
498
+ const packageRootFromWhere = normalizeText(fromWhere?.package_root || "");
499
+ const workspaceResolved = path.resolve(String(workspaceRoot || process.cwd()));
500
+ const candidates = [
501
+ fromEnv,
502
+ packageRootFromWhere
503
+ ? path.join(packageRootFromWhere, "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs")
504
+ : null,
505
+ path.join(workspaceResolved, "..", "boss-recruit-mcp-main", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs"),
506
+ path.join(packagedMcpDir, "..", "boss-recruit-mcp-main", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs")
507
+ ].filter(Boolean).map((item) => path.resolve(item));
508
+
509
+ for (const candidate of new Set(candidates)) {
510
+ if (pathExists(candidate)) {
511
+ return candidate;
512
+ }
513
+ }
514
+ return null;
515
+ }
516
+
517
+ function getCalibrationTimeoutMs(raw) {
518
+ const parsed = parsePositiveInteger(raw);
519
+ if (!parsed) return 60000;
520
+ return Math.max(5000, parsed);
521
+ }
522
+
523
+ function safeInvokeCallback(callback, payload) {
524
+ if (typeof callback !== "function") return;
525
+ try {
526
+ callback(payload);
527
+ } catch {
448
528
  // Ignore callback errors to keep pipeline runtime stable.
449
529
  }
450
530
  }
@@ -912,13 +992,139 @@ function loadScreenConfig(configPath) {
912
992
  return { ok: true, config: parsed };
913
993
  }
914
994
 
915
- function localDirHint(workspaceRoot, dirName) {
916
- return path.join(workspaceRoot, dirName);
917
- }
918
-
919
- export function runPipelinePreflight(workspaceRoot) {
920
- const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
921
- const screenDir = resolveRecommendScreenCliDir(workspaceRoot);
995
+ function localDirHint(workspaceRoot, dirName) {
996
+ return path.join(workspaceRoot, dirName);
997
+ }
998
+
999
+ export function getFeaturedCalibrationResolution(workspaceRoot) {
1000
+ const calibration_path = resolveFavoriteCalibrationPath(workspaceRoot);
1001
+ const calibration_exists = pathExists(calibration_path);
1002
+ const calibration_usable = isUsableCalibrationFile(calibration_path);
1003
+ const calibration_script_path = resolveRecruitCalibrationScriptPath(workspaceRoot);
1004
+ return {
1005
+ calibration_path,
1006
+ calibration_exists,
1007
+ calibration_usable,
1008
+ calibration_script_path
1009
+ };
1010
+ }
1011
+
1012
+ export async function runRecommendCalibration(
1013
+ workspaceRoot,
1014
+ options = {}
1015
+ ) {
1016
+ const debugPort = parsePositiveInteger(options.port) || resolveWorkspaceDebugPort(workspaceRoot);
1017
+ const calibrationPath = options.output
1018
+ ? path.resolve(String(options.output))
1019
+ : resolveFavoriteCalibrationPath(workspaceRoot);
1020
+ const timeoutMs = getCalibrationTimeoutMs(options.timeoutMs);
1021
+ const calibrationScriptPath = resolveRecruitCalibrationScriptPath(workspaceRoot);
1022
+
1023
+ if (!calibrationScriptPath) {
1024
+ return {
1025
+ ok: false,
1026
+ stdout: "",
1027
+ stderr: "",
1028
+ calibration_path: calibrationPath,
1029
+ calibration_script_path: null,
1030
+ debug_port: debugPort,
1031
+ error: {
1032
+ code: "CALIBRATION_SCRIPT_MISSING",
1033
+ message: "未找到 boss-recruit-mcp 校准脚本 calibrate-favorite-position-v2.cjs。"
1034
+ }
1035
+ };
1036
+ }
1037
+
1038
+ ensureDir(path.dirname(calibrationPath));
1039
+ const result = await runProcess({
1040
+ command: "node",
1041
+ args: [
1042
+ calibrationScriptPath,
1043
+ "--port",
1044
+ String(debugPort),
1045
+ "--output",
1046
+ calibrationPath,
1047
+ "--timeout-ms",
1048
+ String(timeoutMs)
1049
+ ],
1050
+ cwd: path.dirname(calibrationScriptPath),
1051
+ timeoutMs: timeoutMs + 15_000,
1052
+ heartbeatIntervalMs: options.runtime?.heartbeatIntervalMs,
1053
+ signal: options.runtime?.signal,
1054
+ onOutput: (event) => {
1055
+ safeInvokeCallback(options.runtime?.onOutput, event);
1056
+ },
1057
+ onHeartbeat: (event) => {
1058
+ safeInvokeCallback(options.runtime?.onHeartbeat, event);
1059
+ }
1060
+ });
1061
+
1062
+ const usable = isUsableCalibrationFile(calibrationPath);
1063
+ const ok = result.code === 0 && usable;
1064
+ return {
1065
+ ok,
1066
+ stdout: result.stdout,
1067
+ stderr: result.stderr,
1068
+ calibration_path: calibrationPath,
1069
+ calibration_script_path: calibrationScriptPath,
1070
+ debug_port: debugPort,
1071
+ auto_started: true,
1072
+ error: ok
1073
+ ? null
1074
+ : {
1075
+ code: result.error_code === "ABORTED"
1076
+ ? "CALIBRATION_ABORTED"
1077
+ : result.error_code === "TIMEOUT"
1078
+ ? "CALIBRATION_TIMEOUT"
1079
+ : "CALIBRATION_FAILED",
1080
+ message: usable
1081
+ ? "校准脚本执行异常。"
1082
+ : "校准脚本未生成可用的 favorite-calibration.json。"
1083
+ }
1084
+ };
1085
+ }
1086
+
1087
+ export async function ensureFeaturedCalibrationReady(
1088
+ workspaceRoot,
1089
+ options = {}
1090
+ ) {
1091
+ const calibrationPath = resolveFavoriteCalibrationPath(workspaceRoot);
1092
+ if (isUsableCalibrationFile(calibrationPath)) {
1093
+ return {
1094
+ ok: true,
1095
+ calibration_path: calibrationPath,
1096
+ auto_started: false
1097
+ };
1098
+ }
1099
+ if (options.autoCalibrate === false) {
1100
+ return {
1101
+ ok: false,
1102
+ calibration_path: calibrationPath,
1103
+ auto_started: false,
1104
+ error: {
1105
+ code: "CALIBRATION_REQUIRED",
1106
+ message: "精选页收藏缺少可用校准文件,需先在推荐页精选 tab 完成校准。"
1107
+ }
1108
+ };
1109
+ }
1110
+ const calibrationRun = await runRecommendCalibration(workspaceRoot, options);
1111
+ if (calibrationRun.ok) {
1112
+ return calibrationRun;
1113
+ }
1114
+ return {
1115
+ ...calibrationRun,
1116
+ ok: false,
1117
+ error: {
1118
+ code: "CALIBRATION_REQUIRED",
1119
+ message: calibrationRun.error?.message || "精选页收藏校准失败,请在推荐页精选 tab 重试校准。"
1120
+ }
1121
+ };
1122
+ }
1123
+
1124
+ export function runPipelinePreflight(workspaceRoot, options = {}) {
1125
+ const pageScope = normalizePageScope(options.pageScope) || "recommend";
1126
+ const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
1127
+ const screenDir = resolveRecommendScreenCliDir(workspaceRoot);
922
1128
  const searchDirExists = Boolean(searchDir && pathExists(searchDir));
923
1129
  const searchEntryPath = searchDir
924
1130
  ? resolveRecommendSearchCliEntry(searchDir)
@@ -929,11 +1135,13 @@ export function runPipelinePreflight(workspaceRoot) {
929
1135
  ? resolveRecommendScreenCliEntry(screenDir)
930
1136
  : path.join(localDirHint(workspaceRoot, "boss-recommend-screen-cli"), "boss-recommend-screen-cli.cjs");
931
1137
  const screenEntryExists = Boolean(screenDir && pathExists(screenEntryPath));
932
- const configResolution = getScreenConfigResolution(workspaceRoot);
933
- const screenConfigPath = configResolution.resolved_path;
934
- const screenConfigParsed = readJsonFile(screenConfigPath);
935
- const screenConfigValidation = validateScreenConfig(screenConfigParsed);
936
- const checks = [
1138
+ const configResolution = getScreenConfigResolution(workspaceRoot);
1139
+ const screenConfigPath = configResolution.resolved_path;
1140
+ const screenConfigParsed = readJsonFile(screenConfigPath);
1141
+ const screenConfigValidation = validateScreenConfig(screenConfigParsed);
1142
+ const calibrationPath = resolveFavoriteCalibrationPath(workspaceRoot);
1143
+ const calibrationUsable = isUsableCalibrationFile(calibrationPath);
1144
+ const checks = [
937
1145
  {
938
1146
  key: "recommend_search_cli_dir",
939
1147
  ok: searchDirExists,
@@ -966,23 +1174,50 @@ export function runPipelinePreflight(workspaceRoot) {
966
1174
  ? "boss-recommend-screen-cli 入口文件可用"
967
1175
  : "boss-recommend-screen-cli 入口文件缺失"
968
1176
  },
969
- {
970
- key: "screen_config",
971
- ok: screenConfigValidation.ok,
972
- path: screenConfigPath,
973
- reason: screenConfigValidation.reason || null,
974
- message: screenConfigValidation.ok ? "screening-config.json 可用" : screenConfigValidation.message
975
- }
976
- ];
977
- checks.push(...buildRuntimeDependencyChecks({ searchDir, screenDir }));
978
-
979
- return {
980
- ok: checks.every((item) => item.ok),
981
- checks,
982
- debug_port: resolveWorkspaceDebugPort(workspaceRoot),
983
- config_resolution: configResolution
984
- };
985
- }
1177
+ {
1178
+ key: "screen_config",
1179
+ ok: screenConfigValidation.ok,
1180
+ path: screenConfigPath,
1181
+ reason: screenConfigValidation.reason || null,
1182
+ message: screenConfigValidation.ok ? "screening-config.json 可用" : screenConfigValidation.message
1183
+ },
1184
+ {
1185
+ key: "favorite_calibration",
1186
+ ok: calibrationUsable,
1187
+ path: calibrationPath,
1188
+ optional: pageScope !== "featured",
1189
+ message: calibrationUsable
1190
+ ? "favorite-calibration.json 可用"
1191
+ : "favorite-calibration.json 不存在或无效(精选页收藏仅支持校准坐标点击)"
1192
+ }
1193
+ ];
1194
+ checks.push(...buildRuntimeDependencyChecks({ searchDir, screenDir }));
1195
+
1196
+ const requiredCheckKeys = new Set([
1197
+ "recommend_search_cli_dir",
1198
+ "recommend_search_cli_entry",
1199
+ "recommend_screen_cli_dir",
1200
+ "recommend_screen_cli_entry",
1201
+ "screen_config",
1202
+ "node_cli",
1203
+ "npm_dep_chrome_remote_interface_search",
1204
+ "npm_dep_chrome_remote_interface_screen",
1205
+ "npm_dep_ws",
1206
+ "npm_dep_sharp"
1207
+ ]);
1208
+ if (pageScope === "featured") {
1209
+ requiredCheckKeys.add("favorite_calibration");
1210
+ }
1211
+
1212
+ return {
1213
+ ok: checks.every((item) => !requiredCheckKeys.has(item.key) || item.ok),
1214
+ checks,
1215
+ debug_port: resolveWorkspaceDebugPort(workspaceRoot),
1216
+ config_resolution: configResolution,
1217
+ calibration_path: calibrationPath,
1218
+ page_scope: pageScope
1219
+ };
1220
+ }
986
1221
 
987
1222
  function collectFailedCheckKeys(checks = []) {
988
1223
  return new Set(
@@ -1036,10 +1271,10 @@ function installNpmDependencies(checks, workspaceRoot) {
1036
1271
  };
1037
1272
  }
1038
1273
 
1039
- export function attemptPipelineAutoRepair(workspaceRoot, preflight = {}) {
1040
- const checks = Array.isArray(preflight.checks) ? preflight.checks : [];
1041
- const failed = collectFailedCheckKeys(checks);
1042
- const actions = [];
1274
+ export function attemptPipelineAutoRepair(workspaceRoot, preflight = {}) {
1275
+ const checks = Array.isArray(preflight.checks) ? preflight.checks : [];
1276
+ const failed = collectFailedCheckKeys(checks);
1277
+ const actions = [];
1043
1278
 
1044
1279
  if (
1045
1280
  failed.has("npm_dep_chrome_remote_interface_search")
@@ -1058,13 +1293,15 @@ export function attemptPipelineAutoRepair(workspaceRoot, preflight = {}) {
1058
1293
  });
1059
1294
  }
1060
1295
  }
1061
-
1062
- const attempted = actions.length > 0;
1063
- const nextPreflight = runPipelinePreflight(workspaceRoot);
1064
- return {
1065
- attempted,
1066
- actions,
1067
- preflight: nextPreflight
1296
+
1297
+ const attempted = actions.length > 0;
1298
+ const nextPreflight = runPipelinePreflight(workspaceRoot, {
1299
+ pageScope: preflight?.page_scope
1300
+ });
1301
+ return {
1302
+ attempted,
1303
+ actions,
1304
+ preflight: nextPreflight
1068
1305
  };
1069
1306
  }
1070
1307
 
@@ -1290,17 +1527,413 @@ function pickBossRecommendReloadTarget(tabs = []) {
1290
1527
  ) || null;
1291
1528
  }
1292
1529
 
1293
- async function evaluateCdpExpression(client, expression) {
1294
- const result = await client.Runtime.evaluate({
1295
- expression,
1296
- returnByValue: true,
1297
- awaitPromise: true
1530
+ async function evaluateCdpExpression(client, expression) {
1531
+ const result = await client.Runtime.evaluate({
1532
+ expression,
1533
+ returnByValue: true,
1534
+ awaitPromise: true
1298
1535
  });
1299
1536
  if (result.exceptionDetails) {
1300
1537
  throw new Error(result.exceptionDetails.exception?.description || "Runtime.evaluate failed");
1301
- }
1302
- return result.result?.value;
1303
- }
1538
+ }
1539
+ return result.result?.value;
1540
+ }
1541
+
1542
+ function buildRecommendTabStateExpression() {
1543
+ return `(() => {
1544
+ const frame = document.querySelector('iframe[name="recommendFrame"]')
1545
+ || document.querySelector('iframe[src*="/web/frame/recommend/"]')
1546
+ || document.querySelector('iframe');
1547
+ if (!frame || !frame.contentDocument) {
1548
+ return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
1549
+ }
1550
+ const doc = frame.contentDocument;
1551
+ const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
1552
+ const tabs = Array.from(doc.querySelectorAll('li.tab-item[data-status], li[data-status][class*="tab"]')).map((node) => {
1553
+ const status = normalize(node.getAttribute('data-status'));
1554
+ const className = normalize(node.className);
1555
+ const active = (
1556
+ /(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(className)
1557
+ || normalize(node.getAttribute('aria-selected')) === 'true'
1558
+ || normalize(node.getAttribute('data-selected')) === 'true'
1559
+ );
1560
+ return {
1561
+ status: status || null,
1562
+ title: normalize(node.getAttribute('title')) || null,
1563
+ label: normalize(node.textContent) || null,
1564
+ active,
1565
+ class_name: className || null
1566
+ };
1567
+ });
1568
+ const activeTab = tabs.find((item) => item.active && item.status) || null;
1569
+ const featuredCount = doc.querySelectorAll('li.geek-info-card').length;
1570
+ const recommendCount = doc.querySelectorAll('ul.card-list > li.card-item').length;
1571
+ let inferredStatus = activeTab?.status || null;
1572
+ if (!inferredStatus) {
1573
+ if (featuredCount > 0 && recommendCount === 0) inferredStatus = '3';
1574
+ else if (recommendCount > 0 && featuredCount === 0) inferredStatus = '0';
1575
+ }
1576
+ return {
1577
+ ok: true,
1578
+ active_status: inferredStatus,
1579
+ tabs,
1580
+ layout: {
1581
+ featured_count: featuredCount,
1582
+ recommend_count: recommendCount
1583
+ }
1584
+ };
1585
+ })()`;
1586
+ }
1587
+
1588
+ function buildRecommendTabSwitchExpression(targetStatus) {
1589
+ return `((targetStatus) => {
1590
+ const frame = document.querySelector('iframe[name="recommendFrame"]')
1591
+ || document.querySelector('iframe[src*="/web/frame/recommend/"]')
1592
+ || document.querySelector('iframe');
1593
+ if (!frame || !frame.contentDocument) {
1594
+ return { ok: false, state: 'NO_RECOMMEND_IFRAME' };
1595
+ }
1596
+ const doc = frame.contentDocument;
1597
+ const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
1598
+ const tabs = Array.from(doc.querySelectorAll('li.tab-item[data-status], li[data-status][class*="tab"]'));
1599
+ const target = tabs.find((node) => normalize(node.getAttribute('data-status')) === String(targetStatus)) || null;
1600
+ if (!target) {
1601
+ return { ok: false, state: 'TAB_NOT_FOUND', target_status: String(targetStatus) };
1602
+ }
1603
+ try {
1604
+ target.scrollIntoView({ behavior: 'instant', block: 'center', inline: 'center' });
1605
+ } catch {}
1606
+ try {
1607
+ target.click();
1608
+ } catch (error) {
1609
+ return {
1610
+ ok: false,
1611
+ state: 'TAB_CLICK_FAILED',
1612
+ message: error?.message || String(error),
1613
+ target_status: String(targetStatus)
1614
+ };
1615
+ }
1616
+ return {
1617
+ ok: true,
1618
+ state: 'TAB_CLICKED',
1619
+ target_status: String(targetStatus)
1620
+ };
1621
+ })(${JSON.stringify(String(targetStatus || ""))})`;
1622
+ }
1623
+
1624
+ function buildRecommendDetailStateExpression() {
1625
+ return `(() => {
1626
+ const selectors = [
1627
+ '.dialog-wrap.active',
1628
+ '.boss-popup__wrapper',
1629
+ '.boss-popup_wrapper',
1630
+ '.boss-dialog_wrapper',
1631
+ '.boss-dialog',
1632
+ '.resume-item-detail',
1633
+ '.geek-detail-modal',
1634
+ 'iframe[src*="/web/frame/c-resume/"]',
1635
+ 'iframe[name*="resume"]',
1636
+ '[class*="popup"][class*="wrapper"]',
1637
+ '[class*="dialog"][class*="wrapper"]'
1638
+ ];
1639
+ const isVisible = (node) => {
1640
+ if (!node || !node.getBoundingClientRect) return false;
1641
+ const rect = node.getBoundingClientRect();
1642
+ if (!rect || rect.width < 4 || rect.height < 4) return false;
1643
+ const style = window.getComputedStyle ? window.getComputedStyle(node) : null;
1644
+ if (style) {
1645
+ if (style.display === 'none') return false;
1646
+ if (style.visibility === 'hidden') return false;
1647
+ if (Number(style.opacity || '1') <= 0) return false;
1648
+ }
1649
+ return true;
1650
+ };
1651
+ const findVisibleDetail = (doc, source) => {
1652
+ if (!doc) return null;
1653
+ for (const selector of selectors) {
1654
+ const nodes = doc.querySelectorAll(selector);
1655
+ for (const node of nodes) {
1656
+ if (isVisible(node)) {
1657
+ return {
1658
+ ok: true,
1659
+ open: true,
1660
+ source,
1661
+ selector
1662
+ };
1663
+ }
1664
+ }
1665
+ }
1666
+ return null;
1667
+ };
1668
+
1669
+ const topDetail = findVisibleDetail(document, 'top');
1670
+ if (topDetail) return topDetail;
1671
+
1672
+ const frame = document.querySelector('iframe[name="recommendFrame"]')
1673
+ || document.querySelector('iframe[src*="/web/frame/recommend/"]')
1674
+ || null;
1675
+ if (!frame || !frame.contentDocument) {
1676
+ return { ok: true, open: false, reason: 'NO_RECOMMEND_IFRAME' };
1677
+ }
1678
+ const frameDetail = findVisibleDetail(frame.contentDocument, 'recommendFrame');
1679
+ if (frameDetail) return frameDetail;
1680
+
1681
+ return { ok: true, open: false, reason: 'DETAIL_NOT_VISIBLE' };
1682
+ })()`;
1683
+ }
1684
+
1685
+ export async function waitRecommendFeaturedDetailReady(workspaceRoot, options = {}) {
1686
+ const debugPort = Number.isFinite(options.port)
1687
+ ? options.port
1688
+ : resolveWorkspaceDebugPort(workspaceRoot);
1689
+ const timeoutMs = Number.isFinite(options.timeoutMs)
1690
+ ? Math.max(5000, options.timeoutMs)
1691
+ : 120000;
1692
+ const pollMs = Number.isFinite(options.pollMs)
1693
+ ? Math.max(150, options.pollMs)
1694
+ : 400;
1695
+
1696
+ let client = null;
1697
+ try {
1698
+ const tabs = await listChromeTabs(debugPort);
1699
+ const target = pickBossRecommendReloadTarget(tabs);
1700
+ if (!target) {
1701
+ return {
1702
+ ok: false,
1703
+ debug_port: debugPort,
1704
+ state: "BOSS_TAB_NOT_FOUND",
1705
+ message: "未找到可检测详情页状态的 Boss recommend 标签页。"
1706
+ };
1707
+ }
1708
+ client = await CDP({ port: debugPort, target });
1709
+ const { Runtime, Page } = client;
1710
+ if (Runtime && typeof Runtime.enable === "function") {
1711
+ await Runtime.enable();
1712
+ }
1713
+ if (Page && typeof Page.enable === "function") {
1714
+ await Page.enable();
1715
+ }
1716
+ if (shouldBringChromeToFront() && Page && typeof Page.bringToFront === "function") {
1717
+ await Page.bringToFront();
1718
+ }
1719
+
1720
+ const deadline = Date.now() + timeoutMs;
1721
+ let lastState = null;
1722
+ while (Date.now() < deadline) {
1723
+ lastState = await evaluateCdpExpression(client, buildRecommendDetailStateExpression());
1724
+ if (lastState?.ok && lastState.open) {
1725
+ return {
1726
+ ok: true,
1727
+ debug_port: debugPort,
1728
+ state: "DETAIL_READY",
1729
+ detail_state: lastState
1730
+ };
1731
+ }
1732
+ await sleep(pollMs);
1733
+ }
1734
+
1735
+ return {
1736
+ ok: false,
1737
+ debug_port: debugPort,
1738
+ state: "DETAIL_NOT_READY_TIMEOUT",
1739
+ message: "未在超时内检测到候选人详情页,请先打开精选候选人详情后重试。",
1740
+ detail_state: lastState || null
1741
+ };
1742
+ } catch (error) {
1743
+ return {
1744
+ ok: false,
1745
+ debug_port: debugPort,
1746
+ state: "DETAIL_STATE_CHECK_FAILED",
1747
+ message: error?.message || "检测候选人详情页状态失败。"
1748
+ };
1749
+ } finally {
1750
+ if (client) {
1751
+ try {
1752
+ await client.close();
1753
+ } catch {}
1754
+ }
1755
+ }
1756
+ }
1757
+
1758
+ export async function readRecommendTabState(workspaceRoot, options = {}) {
1759
+ const debugPort = Number.isFinite(options.port)
1760
+ ? options.port
1761
+ : resolveWorkspaceDebugPort(workspaceRoot);
1762
+
1763
+ let client = null;
1764
+ try {
1765
+ const tabs = await listChromeTabs(debugPort);
1766
+ const target = pickBossRecommendReloadTarget(tabs);
1767
+ if (!target) {
1768
+ return {
1769
+ ok: false,
1770
+ debug_port: debugPort,
1771
+ state: "BOSS_TAB_NOT_FOUND",
1772
+ message: "未找到可读取 tab 状态的 Boss recommend 标签页。"
1773
+ };
1774
+ }
1775
+ client = await CDP({ port: debugPort, target });
1776
+ const { Runtime } = client;
1777
+ if (Runtime && typeof Runtime.enable === "function") {
1778
+ await Runtime.enable();
1779
+ }
1780
+ const tabState = await evaluateCdpExpression(client, buildRecommendTabStateExpression());
1781
+ if (!tabState?.ok) {
1782
+ return {
1783
+ ok: false,
1784
+ debug_port: debugPort,
1785
+ state: tabState?.error || "RECOMMEND_TAB_STATE_FAILED",
1786
+ message: "读取 recommend tab 状态失败。",
1787
+ tab_state: tabState || null
1788
+ };
1789
+ }
1790
+ return {
1791
+ ok: true,
1792
+ debug_port: debugPort,
1793
+ active_status: normalizeText(tabState.active_status),
1794
+ tab_state: tabState
1795
+ };
1796
+ } catch (error) {
1797
+ return {
1798
+ ok: false,
1799
+ debug_port: debugPort,
1800
+ state: "RECOMMEND_TAB_STATE_FAILED",
1801
+ message: error?.message || "读取 recommend tab 状态失败。"
1802
+ };
1803
+ } finally {
1804
+ if (client) {
1805
+ try {
1806
+ await client.close();
1807
+ } catch {}
1808
+ }
1809
+ }
1810
+ }
1811
+
1812
+ function isUsableCalibrationFile(filePath) {
1813
+ if (!filePath || !pathExists(filePath)) return false;
1814
+ const parsed = readJsonFile(filePath);
1815
+ return Boolean(
1816
+ parsed
1817
+ && parsed.favoritePosition
1818
+ && Number.isFinite(parsed.favoritePosition.pageX)
1819
+ && Number.isFinite(parsed.favoritePosition.pageY)
1820
+ );
1821
+ }
1822
+
1823
+ function resolveFavoriteCalibrationPath(workspaceRoot) {
1824
+ const fromEnv = normalizeText(process.env.BOSS_RECOMMEND_CALIBRATION_FILE || "");
1825
+ if (fromEnv) return path.resolve(fromEnv);
1826
+
1827
+ const screenConfigPath = resolveScreenConfigPath(workspaceRoot);
1828
+ const screenConfig = readJsonFile(screenConfigPath);
1829
+ const calibrationFile = normalizeText(screenConfig?.calibrationFile || "");
1830
+ if (calibrationFile) {
1831
+ return path.resolve(path.dirname(screenConfigPath), calibrationFile);
1832
+ }
1833
+ return getUserCalibrationPath();
1834
+ }
1835
+
1836
+ export async function switchRecommendTab(workspaceRoot, options = {}) {
1837
+ const debugPort = Number.isFinite(options.port)
1838
+ ? options.port
1839
+ : resolveWorkspaceDebugPort(workspaceRoot);
1840
+ const targetScope = normalizePageScope(options.page_scope);
1841
+ const targetStatus = normalizeText(options.target_status || PAGE_SCOPE_TO_TAB_STATUS[targetScope] || "");
1842
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 8000;
1843
+ const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 350;
1844
+ if (!targetStatus) {
1845
+ return {
1846
+ ok: false,
1847
+ debug_port: debugPort,
1848
+ state: "TAB_STATUS_REQUIRED",
1849
+ message: "切换 recommend tab 失败:缺少 target_status。"
1850
+ };
1851
+ }
1852
+
1853
+ let client = null;
1854
+ try {
1855
+ const tabs = await listChromeTabs(debugPort);
1856
+ const target = pickBossRecommendReloadTarget(tabs);
1857
+ if (!target) {
1858
+ return {
1859
+ ok: false,
1860
+ debug_port: debugPort,
1861
+ state: "BOSS_TAB_NOT_FOUND",
1862
+ message: "未找到可操作的 Boss recommend 标签页。"
1863
+ };
1864
+ }
1865
+ client = await CDP({ port: debugPort, target });
1866
+ const { Runtime, Page } = client;
1867
+ if (Runtime && typeof Runtime.enable === "function") {
1868
+ await Runtime.enable();
1869
+ }
1870
+ if (Page && typeof Page.enable === "function") {
1871
+ await Page.enable();
1872
+ }
1873
+ if (shouldBringChromeToFront() && Page && typeof Page.bringToFront === "function") {
1874
+ await Page.bringToFront();
1875
+ }
1876
+
1877
+ const beforeState = await evaluateCdpExpression(client, buildRecommendTabStateExpression());
1878
+ if (beforeState?.ok && normalizeText(beforeState.active_status) === targetStatus) {
1879
+ return {
1880
+ ok: true,
1881
+ debug_port: debugPort,
1882
+ state: "TAB_ALREADY_ACTIVE",
1883
+ active_status: targetStatus,
1884
+ tab_state: beforeState
1885
+ };
1886
+ }
1887
+
1888
+ const clickResult = await evaluateCdpExpression(client, buildRecommendTabSwitchExpression(targetStatus));
1889
+ if (!clickResult?.ok) {
1890
+ return {
1891
+ ok: false,
1892
+ debug_port: debugPort,
1893
+ state: clickResult?.state || "TAB_CLICK_FAILED",
1894
+ message: clickResult?.message || "点击 tab 失败。",
1895
+ tab_state: beforeState || null
1896
+ };
1897
+ }
1898
+
1899
+ const deadline = Date.now() + timeoutMs;
1900
+ let lastState = beforeState || null;
1901
+ while (Date.now() < deadline) {
1902
+ await sleep(pollMs);
1903
+ lastState = await evaluateCdpExpression(client, buildRecommendTabStateExpression());
1904
+ if (lastState?.ok && normalizeText(lastState.active_status) === targetStatus) {
1905
+ return {
1906
+ ok: true,
1907
+ debug_port: debugPort,
1908
+ state: "TAB_SWITCHED",
1909
+ active_status: targetStatus,
1910
+ tab_state: lastState
1911
+ };
1912
+ }
1913
+ }
1914
+
1915
+ return {
1916
+ ok: false,
1917
+ debug_port: debugPort,
1918
+ state: "TAB_SWITCH_NOT_APPLIED",
1919
+ message: "点击 tab 后未在超时内确认激活状态。",
1920
+ tab_state: lastState || null
1921
+ };
1922
+ } catch (error) {
1923
+ return {
1924
+ ok: false,
1925
+ debug_port: debugPort,
1926
+ state: "RECOMMEND_TAB_SWITCH_FAILED",
1927
+ message: error?.message || "切换 recommend tab 失败。"
1928
+ };
1929
+ } finally {
1930
+ if (client) {
1931
+ try {
1932
+ await client.close();
1933
+ } catch {}
1934
+ }
1935
+ }
1936
+ }
1304
1937
 
1305
1938
  function buildRecommendRefreshStateExpression() {
1306
1939
  return `(() => {
@@ -1698,8 +2331,8 @@ export async function listRecommendJobs({ workspaceRoot, port, runtime = null })
1698
2331
  };
1699
2332
  }
1700
2333
  const cliPath = resolveRecommendSearchCliEntry(searchDir);
1701
- const args = [
1702
- cliPath,
2334
+ const args = [
2335
+ cliPath,
1703
2336
  "--list-jobs",
1704
2337
  "--port",
1705
2338
  String(parsePositiveInteger(port) || resolveWorkspaceDebugPort(workspaceRoot))
@@ -1751,7 +2384,13 @@ export async function listRecommendJobs({ workspaceRoot, port, runtime = null })
1751
2384
  };
1752
2385
  }
1753
2386
 
1754
- export async function runRecommendSearchCli({ workspaceRoot, searchParams, selectedJob, runtime = null }) {
2387
+ export async function runRecommendSearchCli({
2388
+ workspaceRoot,
2389
+ searchParams,
2390
+ selectedJob,
2391
+ pageScope = "recommend",
2392
+ runtime = null
2393
+ }) {
1755
2394
  const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
1756
2395
  if (!searchDir) {
1757
2396
  return {
@@ -1765,7 +2404,7 @@ export async function runRecommendSearchCli({ workspaceRoot, searchParams, selec
1765
2404
  };
1766
2405
  }
1767
2406
  const cliPath = resolveRecommendSearchCliEntry(searchDir);
1768
- const args = [
2407
+ const args = [
1769
2408
  cliPath,
1770
2409
  "--school-tag",
1771
2410
  serializeSchoolTagSelection(searchParams.school_tag),
@@ -1775,13 +2414,18 @@ export async function runRecommendSearchCli({ workspaceRoot, searchParams, selec
1775
2414
  searchParams.gender,
1776
2415
  "--recent-not-view",
1777
2416
  searchParams.recent_not_view,
1778
- "--port",
1779
- String(resolveWorkspaceDebugPort(workspaceRoot))
1780
- ];
1781
- const normalizedSelectedJob = String(selectedJob || "").trim();
1782
- if (normalizedSelectedJob) {
1783
- args.push("--job", normalizedSelectedJob);
1784
- }
2417
+ "--port",
2418
+ String(resolveWorkspaceDebugPort(workspaceRoot))
2419
+ ];
2420
+ const normalizedPageScope = normalizePageScope(pageScope) || "recommend";
2421
+ args.push("--page-scope", normalizedPageScope);
2422
+ if (normalizedPageScope === "featured") {
2423
+ args.push("--calibration", resolveFavoriteCalibrationPath(workspaceRoot));
2424
+ }
2425
+ const normalizedSelectedJob = String(selectedJob || "").trim();
2426
+ if (normalizedSelectedJob) {
2427
+ args.push("--job", normalizedSelectedJob);
2428
+ }
1785
2429
  const result = await runProcess({
1786
2430
  command: "node",
1787
2431
  args,
@@ -1825,7 +2469,13 @@ export async function runRecommendSearchCli({ workspaceRoot, searchParams, selec
1825
2469
  };
1826
2470
  }
1827
2471
 
1828
- export async function runRecommendScreenCli({ workspaceRoot, screenParams, resume = null, runtime = null }) {
2472
+ export async function runRecommendScreenCli({
2473
+ workspaceRoot,
2474
+ screenParams,
2475
+ pageScope = "recommend",
2476
+ resume = null,
2477
+ runtime = null
2478
+ }) {
1829
2479
  const screenDir = resolveRecommendScreenCliDir(workspaceRoot);
1830
2480
  if (!screenDir) {
1831
2481
  return {
@@ -1909,7 +2559,7 @@ export async function runRecommendScreenCli({ workspaceRoot, screenParams, resum
1909
2559
  }
1910
2560
 
1911
2561
  const cliPath = resolveRecommendScreenCliEntry(screenDir);
1912
- const args = [
2562
+ const args = [
1913
2563
  cliPath,
1914
2564
  "--baseurl",
1915
2565
  loaded.config.baseUrl,
@@ -1925,9 +2575,11 @@ export async function runRecommendScreenCli({ workspaceRoot, screenParams, resum
1925
2575
  screenParams.post_action,
1926
2576
  "--post-action-confirmed",
1927
2577
  "true",
1928
- "--output",
1929
- outputPath
1930
- ];
2578
+ "--output",
2579
+ outputPath
2580
+ ];
2581
+ const normalizedPageScope = normalizePageScope(pageScope) || "recommend";
2582
+ args.push("--page-scope", normalizedPageScope);
1931
2583
 
1932
2584
  if (loaded.config.openaiOrganization) {
1933
2585
  args.push("--openai-organization", loaded.config.openaiOrganization);
@@ -2014,10 +2666,13 @@ export async function runRecommendScreenCli({ workspaceRoot, screenParams, resum
2014
2666
  };
2015
2667
  }
2016
2668
 
2017
- export const __testables = {
2018
- runProcess,
2019
- parseJsonOutput,
2020
- parseScreenProgressLine,
2021
- resolveRecommendScreenTimeoutMs,
2022
- buildRecommendScreenProcessError
2023
- };
2669
+ export const __testables = {
2670
+ runProcess,
2671
+ parseJsonOutput,
2672
+ parseScreenProgressLine,
2673
+ resolveRecommendScreenTimeoutMs,
2674
+ buildRecommendScreenProcessError,
2675
+ normalizePageScope,
2676
+ buildRecommendTabStateExpression,
2677
+ buildRecommendTabSwitchExpression
2678
+ };