@reconcrap/boss-recommend-mcp 1.2.5 → 1.2.7
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 +7 -0
- package/package.json +2 -1
- package/src/adapters.js +402 -95
- package/src/index.js +97 -0
- package/src/pipeline.js +34 -2
- package/src/recommend-healing-config.js +131 -0
- package/src/recommend-healing-rules.json +261 -0
- package/src/self-heal.js +2237 -0
- package/src/test-pipeline.js +67 -0
- package/src/test-self-heal.js +224 -0
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +259 -153
- package/vendor/boss-recommend-search-cli/src/cli.js +98 -50
package/README.md
CHANGED
|
@@ -14,6 +14,12 @@ MCP 工具:
|
|
|
14
14
|
- `cancel_recommend_pipeline_run`(取消运行中任务)
|
|
15
15
|
- `pause_recommend_pipeline_run`(请求暂停 run;会在当前候选人处理完成后进入 paused)
|
|
16
16
|
- `resume_recommend_pipeline_run`(继续 paused run;沿用同 run_id 与同 CSV)
|
|
17
|
+
- `run_recommend_self_heal`(手动运维工具;扫描 Boss recommend 的 selector / network 规则漂移,并在确认后应用高置信度修复)
|
|
18
|
+
- `validation_profile=safe`:只做非破坏性扫描与被动 network 观察
|
|
19
|
+
- `validation_profile=full`:会主动打开候选人详情,并执行收藏往返校验与一次打招呼校验;若完整交互没跑通,会明确返回验证异常而不是静默跳过
|
|
20
|
+
- 扫描会主动覆盖 recommend/latest/featured 三个 tab 的详情链路(详情打开、详情内关键 selector、popup 关闭)
|
|
21
|
+
- 搜索链路 selector 会在状态触发后验证:职位下拉、职位搜索输入、职位 label、筛选面板、筛选分组(school/degree/gender/recentNotView)、筛选滚动容器与筛选项激活态
|
|
22
|
+
- 对关闭弹层相关 selector,会同时验证 close 按钮与 fallback close 候选 selector
|
|
17
23
|
|
|
18
24
|
状态机:
|
|
19
25
|
|
|
@@ -52,6 +58,7 @@ MCP 工具:
|
|
|
52
58
|
- 不会对每位候选人重复确认
|
|
53
59
|
- 推荐页详情处理完成后,会强制关闭详情页并确认已关闭
|
|
54
60
|
- 简历提取采用“分段滚动截图 + 拼成长图”的方式,再交给多模态模型判断
|
|
61
|
+
- 提供显式运维自愈工具:只在手动调用 `run_recommend_self_heal` 时运行,不会接入正常 run / doctor / preflight 自动链路
|
|
55
62
|
- 运行前会自动做依赖体检(Node.js、Python、Pillow、`chrome-remote-interface`、`ws`),缺失时会在 `doctor` 与流水线失败诊断中明确提示
|
|
56
63
|
- 若 preflight 失败,返回 `diagnostics.recovery`(含有序修复步骤与 `agent_prompt`),可直接交给 AI agent 自动按顺序安装依赖
|
|
57
64
|
- 不依赖 PowerShell;Windows / macOS 均可运行(命令提示会按平台给出)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reconcrap/boss-recommend-mcp",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"boss",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"test:run-state": "node src/test-run-state.js",
|
|
26
26
|
"test:runtime": "node src/test-adapters-runtime.js",
|
|
27
27
|
"test:async": "node src/test-index-async.js",
|
|
28
|
+
"test:self-heal": "node src/test-self-heal.js",
|
|
28
29
|
"test:search-job": "node vendor/boss-recommend-search-cli/src/test-job-selection.js",
|
|
29
30
|
"test:screen-runtime": "node vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs"
|
|
30
31
|
},
|
package/src/adapters.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import CDP from "chrome-remote-interface";
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import CDP from "chrome-remote-interface";
|
|
7
|
+
import {
|
|
8
|
+
buildFirstSelectorLookupExpression,
|
|
9
|
+
getRecommendSelectorRule
|
|
10
|
+
} from "./recommend-healing-config.js";
|
|
7
11
|
|
|
8
12
|
const currentFilePath = fileURLToPath(import.meta.url);
|
|
9
13
|
const packagedMcpDir = path.resolve(path.dirname(currentFilePath), "..");
|
|
@@ -23,6 +27,44 @@ const PAGE_SCOPE_TO_TAB_STATUS = {
|
|
|
23
27
|
latest: "1",
|
|
24
28
|
featured: "3"
|
|
25
29
|
};
|
|
30
|
+
const RECOMMEND_IFRAME_SELECTORS = getRecommendSelectorRule(
|
|
31
|
+
["top", "recommend_iframe"],
|
|
32
|
+
['iframe[name="recommendFrame"]', 'iframe[src*="/web/frame/recommend/"]', "iframe"]
|
|
33
|
+
);
|
|
34
|
+
const RECOMMEND_TAB_SELECTORS = getRecommendSelectorRule(
|
|
35
|
+
["frame", "tab_items"],
|
|
36
|
+
['li.tab-item[data-status]', 'li[data-status][class*="tab"]']
|
|
37
|
+
);
|
|
38
|
+
const RECOMMEND_CARD_SELECTORS = getRecommendSelectorRule(["frame", "recommend_cards"], ['ul.card-list > li.card-item']);
|
|
39
|
+
const FEATURED_CARD_SELECTORS = getRecommendSelectorRule(["frame", "featured_cards"], ['li.geek-info-card']);
|
|
40
|
+
const LATEST_CARD_SELECTORS = getRecommendSelectorRule(["frame", "latest_cards"], ['.candidate-card-wrap .card-inner[data-geek]', '.candidate-card-wrap [data-geek]']);
|
|
41
|
+
const DETAIL_POPUP_SELECTORS = getRecommendSelectorRule(
|
|
42
|
+
["detail", "popup"],
|
|
43
|
+
[
|
|
44
|
+
".dialog-wrap.active",
|
|
45
|
+
".boss-popup__wrapper",
|
|
46
|
+
".boss-popup_wrapper",
|
|
47
|
+
".boss-dialog_wrapper",
|
|
48
|
+
".boss-dialog",
|
|
49
|
+
".resume-item-detail",
|
|
50
|
+
".geek-detail-modal",
|
|
51
|
+
'[class*="popup"][class*="wrapper"]',
|
|
52
|
+
'[class*="dialog"][class*="wrapper"]'
|
|
53
|
+
]
|
|
54
|
+
);
|
|
55
|
+
const DETAIL_RESUME_IFRAME_SELECTORS = getRecommendSelectorRule(
|
|
56
|
+
["detail", "resume_iframe"],
|
|
57
|
+
['iframe[src*="/web/frame/c-resume/"]', 'iframe[name*="resume"]']
|
|
58
|
+
);
|
|
59
|
+
const REFRESH_FINISHED_WRAP_SELECTORS = getRecommendSelectorRule(["frame", "refresh_finished_wrap"], [".finished-wrap"]);
|
|
60
|
+
const REFRESH_BUTTON_SELECTORS = getRecommendSelectorRule(
|
|
61
|
+
["frame", "refresh_button"],
|
|
62
|
+
[".finished-wrap .btn.btn-refresh", ".finished-wrap .btn-refresh", ".no-data-refresh .btn-refresh"]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
function buildRecommendIframeLookupExpression(rootExpr = "document") {
|
|
66
|
+
return buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS, rootExpr);
|
|
67
|
+
}
|
|
26
68
|
|
|
27
69
|
function getCodexHome() {
|
|
28
70
|
return process.env.CODEX_HOME
|
|
@@ -1343,15 +1385,21 @@ function findChromeOnboardingUrl(tabs) {
|
|
|
1343
1385
|
return null;
|
|
1344
1386
|
}
|
|
1345
1387
|
|
|
1346
|
-
function isBossLoginTab(tab) {
|
|
1347
|
-
const url = String(tab?.url || "");
|
|
1348
|
-
const title = String(tab?.title || "");
|
|
1349
|
-
return (
|
|
1350
|
-
url === bossLoginUrl
|
|
1351
|
-
|| bossLoginUrlPattern.test(url)
|
|
1352
|
-
|| bossLoginTitlePattern.test(title)
|
|
1353
|
-
);
|
|
1354
|
-
}
|
|
1388
|
+
function isBossLoginTab(tab) {
|
|
1389
|
+
const url = String(tab?.url || "");
|
|
1390
|
+
const title = String(tab?.title || "");
|
|
1391
|
+
return (
|
|
1392
|
+
url === bossLoginUrl
|
|
1393
|
+
|| bossLoginUrlPattern.test(url)
|
|
1394
|
+
|| bossLoginTitlePattern.test(title)
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function findRecommendTab(tabs = []) {
|
|
1399
|
+
return tabs.find(
|
|
1400
|
+
(tab) => typeof tab?.url === "string" && tab.url.includes("/web/chat/recommend")
|
|
1401
|
+
) || null;
|
|
1402
|
+
}
|
|
1355
1403
|
|
|
1356
1404
|
export async function inspectBossRecommendPageState(port, options = {}) {
|
|
1357
1405
|
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 6000;
|
|
@@ -1365,9 +1413,7 @@ export async function inspectBossRecommendPageState(port, options = {}) {
|
|
|
1365
1413
|
try {
|
|
1366
1414
|
const tabs = await listChromeTabs(port);
|
|
1367
1415
|
lastTabs = tabs;
|
|
1368
|
-
const exactTab = tabs
|
|
1369
|
-
(tab) => typeof tab?.url === "string" && tab.url.includes("/web/chat/recommend")
|
|
1370
|
-
);
|
|
1416
|
+
const exactTab = findRecommendTab(tabs);
|
|
1371
1417
|
if (exactTab) {
|
|
1372
1418
|
if (isBossLoginTab(exactTab)) {
|
|
1373
1419
|
return buildBossPageState({
|
|
@@ -1498,36 +1544,285 @@ async function openBossRecommendTab(port) {
|
|
|
1498
1544
|
};
|
|
1499
1545
|
}
|
|
1500
1546
|
|
|
1501
|
-
async function verifyRecommendPageStable(port, options = {}) {
|
|
1502
|
-
const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 1500;
|
|
1503
|
-
const recheckTimeoutMs = Number.isFinite(options.recheckTimeoutMs) ? options.recheckTimeoutMs : 2500;
|
|
1504
|
-
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 600;
|
|
1547
|
+
async function verifyRecommendPageStable(port, options = {}) {
|
|
1548
|
+
const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 1500;
|
|
1549
|
+
const recheckTimeoutMs = Number.isFinite(options.recheckTimeoutMs) ? options.recheckTimeoutMs : 2500;
|
|
1550
|
+
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 600;
|
|
1505
1551
|
|
|
1506
1552
|
await sleep(settleMs);
|
|
1507
|
-
const recheck = await inspectBossRecommendPageState(port, {
|
|
1508
|
-
timeoutMs: recheckTimeoutMs,
|
|
1509
|
-
pollMs
|
|
1510
|
-
});
|
|
1511
|
-
if (recheck.state === "RECOMMEND_READY") {
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1553
|
+
const recheck = await inspectBossRecommendPageState(port, {
|
|
1554
|
+
timeoutMs: recheckTimeoutMs,
|
|
1555
|
+
pollMs
|
|
1556
|
+
});
|
|
1557
|
+
if (recheck.state === "RECOMMEND_READY") {
|
|
1558
|
+
const iframeState = await waitForRecommendIframeReady(port, {
|
|
1559
|
+
timeoutMs: recheckTimeoutMs,
|
|
1560
|
+
pollMs
|
|
1561
|
+
});
|
|
1562
|
+
if (iframeState.state === "RECOMMEND_IFRAME_READY") {
|
|
1563
|
+
return buildBossPageState({
|
|
1564
|
+
...recheck,
|
|
1565
|
+
ok: true,
|
|
1566
|
+
state: "RECOMMEND_READY",
|
|
1567
|
+
frame_url: iframeState.frame_url || null,
|
|
1568
|
+
iframe_state: iframeState
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
if (iframeState.state === "LOGIN_REQUIRED") {
|
|
1572
|
+
return buildBossPageState({
|
|
1573
|
+
...iframeState,
|
|
1574
|
+
state: "LOGIN_REQUIRED_AFTER_REDIRECT",
|
|
1575
|
+
message: "Boss 页面曾进入 recommend 但 iframe 不可用且出现登录态特征,通常表示登录态失效。"
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
if (iframeState.state === "NO_RECOMMEND_IFRAME") {
|
|
1579
|
+
return buildBossPageState({
|
|
1580
|
+
...recheck,
|
|
1581
|
+
ok: false,
|
|
1582
|
+
state: "NO_RECOMMEND_IFRAME",
|
|
1583
|
+
current_url: iframeState.current_url || recheck.current_url || null,
|
|
1584
|
+
title: iframeState.title || recheck.title || null,
|
|
1585
|
+
frame_url: iframeState.frame_url || null,
|
|
1586
|
+
iframe_state: iframeState,
|
|
1587
|
+
message: "Boss recommend 页面已打开,但 recommend iframe 尚未就绪。"
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
return iframeState;
|
|
1591
|
+
}
|
|
1592
|
+
if (recheck.state === "LOGIN_REQUIRED") {
|
|
1593
|
+
return buildBossPageState({
|
|
1594
|
+
...recheck,
|
|
1595
|
+
state: "LOGIN_REQUIRED_AFTER_REDIRECT",
|
|
1518
1596
|
message: "Boss 页面曾进入 recommend 但随后跳转到其他页面,通常表示登录态失效。"
|
|
1519
1597
|
});
|
|
1520
|
-
}
|
|
1521
|
-
return recheck;
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
function
|
|
1525
|
-
return
|
|
1526
|
-
(
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
}
|
|
1598
|
+
}
|
|
1599
|
+
return recheck;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function buildRecommendIframeProbeExpression() {
|
|
1603
|
+
return `(() => {
|
|
1604
|
+
const currentUrl = (() => {
|
|
1605
|
+
try { return String(window.location.href || ""); } catch { return ""; }
|
|
1606
|
+
})();
|
|
1607
|
+
const title = (() => {
|
|
1608
|
+
try { return String(document.title || ""); } catch { return ""; }
|
|
1609
|
+
})();
|
|
1610
|
+
const isLogin = ${bossLoginUrlPattern}.test(currentUrl)
|
|
1611
|
+
|| ${bossLoginTitlePattern}.test(title);
|
|
1612
|
+
if (isLogin) {
|
|
1613
|
+
return {
|
|
1614
|
+
ok: false,
|
|
1615
|
+
error: "LOGIN_REQUIRED",
|
|
1616
|
+
current_url: currentUrl || ${JSON.stringify(bossLoginUrl)},
|
|
1617
|
+
title
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
const frame = ${buildRecommendIframeLookupExpression()};
|
|
1621
|
+
const frameUrl = (() => {
|
|
1622
|
+
try {
|
|
1623
|
+
return String(frame?.contentWindow?.location?.href || frame?.src || "");
|
|
1624
|
+
} catch {
|
|
1625
|
+
return String(frame?.src || "");
|
|
1626
|
+
}
|
|
1627
|
+
})();
|
|
1628
|
+
const iframeCount = document.querySelectorAll("iframe").length;
|
|
1629
|
+
if (!frame || !frame.contentDocument) {
|
|
1630
|
+
return {
|
|
1631
|
+
ok: false,
|
|
1632
|
+
error: "NO_RECOMMEND_IFRAME",
|
|
1633
|
+
current_url: currentUrl,
|
|
1634
|
+
title,
|
|
1635
|
+
frame_url: frameUrl,
|
|
1636
|
+
frame_present: Boolean(frame),
|
|
1637
|
+
iframe_count: iframeCount
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
return {
|
|
1641
|
+
ok: true,
|
|
1642
|
+
current_url: currentUrl,
|
|
1643
|
+
title,
|
|
1644
|
+
frame_url: frameUrl,
|
|
1645
|
+
frame_present: true,
|
|
1646
|
+
iframe_count: iframeCount
|
|
1647
|
+
};
|
|
1648
|
+
})()`;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
async function probeRecommendIframeState(port, options = {}) {
|
|
1652
|
+
const expectedUrl = options.expectedUrl || bossRecommendUrl;
|
|
1653
|
+
let client = null;
|
|
1654
|
+
try {
|
|
1655
|
+
const tabs = await listChromeTabs(port);
|
|
1656
|
+
const exactTab = findRecommendTab(tabs);
|
|
1657
|
+
if (!exactTab) {
|
|
1658
|
+
const loginTab = tabs.find((tab) => isBossLoginTab(tab));
|
|
1659
|
+
if (loginTab) {
|
|
1660
|
+
return buildBossPageState({
|
|
1661
|
+
ok: false,
|
|
1662
|
+
state: "LOGIN_REQUIRED",
|
|
1663
|
+
path: loginTab.url || bossLoginUrl,
|
|
1664
|
+
current_url: loginTab.url || bossLoginUrl,
|
|
1665
|
+
title: loginTab.title || null,
|
|
1666
|
+
requires_login: true,
|
|
1667
|
+
expected_url: expectedUrl,
|
|
1668
|
+
login_url: bossLoginUrl,
|
|
1669
|
+
message: "Boss 页面未登录,需先完成登录后再进入 recommend 页面。"
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
const bossTab = tabs.find(
|
|
1673
|
+
(tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")
|
|
1674
|
+
);
|
|
1675
|
+
if (bossTab) {
|
|
1676
|
+
return buildBossPageState({
|
|
1677
|
+
ok: false,
|
|
1678
|
+
state: "BOSS_NOT_ON_RECOMMEND",
|
|
1679
|
+
path: bossTab.url,
|
|
1680
|
+
current_url: bossTab.url,
|
|
1681
|
+
title: bossTab.title || null,
|
|
1682
|
+
requires_login: false,
|
|
1683
|
+
expected_url: expectedUrl,
|
|
1684
|
+
message: "Boss 已登录但当前不在 recommend 页面。"
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
return buildBossPageState({
|
|
1688
|
+
ok: false,
|
|
1689
|
+
state: "BOSS_TAB_NOT_FOUND",
|
|
1690
|
+
path: expectedUrl,
|
|
1691
|
+
current_url: null,
|
|
1692
|
+
title: null,
|
|
1693
|
+
requires_login: false,
|
|
1694
|
+
expected_url: expectedUrl,
|
|
1695
|
+
message: "未检测到 Boss 推荐页标签页。",
|
|
1696
|
+
sample_urls: extractSampleUrls(tabs)
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
if (isBossLoginTab(exactTab)) {
|
|
1700
|
+
return buildBossPageState({
|
|
1701
|
+
ok: false,
|
|
1702
|
+
state: "LOGIN_REQUIRED",
|
|
1703
|
+
path: exactTab.url || bossLoginUrl,
|
|
1704
|
+
current_url: exactTab.url || bossLoginUrl,
|
|
1705
|
+
title: exactTab.title || null,
|
|
1706
|
+
requires_login: true,
|
|
1707
|
+
expected_url: expectedUrl,
|
|
1708
|
+
login_url: bossLoginUrl,
|
|
1709
|
+
message: "当前标签页虽在 recommend 路径,但检测到登录态页面特征,请先完成 Boss 登录。"
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
client = await CDP({ port, target: exactTab });
|
|
1714
|
+
const { Runtime, Page } = client;
|
|
1715
|
+
if (Runtime && typeof Runtime.enable === "function") {
|
|
1716
|
+
await Runtime.enable();
|
|
1717
|
+
}
|
|
1718
|
+
if (Page && typeof Page.enable === "function") {
|
|
1719
|
+
await Page.enable();
|
|
1720
|
+
}
|
|
1721
|
+
const frameProbe = await evaluateCdpExpression(client, buildRecommendIframeProbeExpression());
|
|
1722
|
+
if (frameProbe?.ok) {
|
|
1723
|
+
return buildBossPageState({
|
|
1724
|
+
ok: true,
|
|
1725
|
+
state: "RECOMMEND_IFRAME_READY",
|
|
1726
|
+
path: frameProbe.current_url || exactTab.url || expectedUrl,
|
|
1727
|
+
current_url: frameProbe.current_url || exactTab.url || null,
|
|
1728
|
+
title: frameProbe.title || exactTab.title || null,
|
|
1729
|
+
expected_url: expectedUrl,
|
|
1730
|
+
frame_url: frameProbe.frame_url || null,
|
|
1731
|
+
frame_present: frameProbe.frame_present === true,
|
|
1732
|
+
iframe_count: Number.isFinite(Number(frameProbe.iframe_count))
|
|
1733
|
+
? Number(frameProbe.iframe_count)
|
|
1734
|
+
: null,
|
|
1735
|
+
message: "Boss 推荐页 iframe 已就绪。"
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
if (frameProbe?.error === "LOGIN_REQUIRED") {
|
|
1739
|
+
return buildBossPageState({
|
|
1740
|
+
ok: false,
|
|
1741
|
+
state: "LOGIN_REQUIRED",
|
|
1742
|
+
path: frameProbe.current_url || exactTab.url || bossLoginUrl,
|
|
1743
|
+
current_url: frameProbe.current_url || exactTab.url || bossLoginUrl,
|
|
1744
|
+
title: frameProbe.title || exactTab.title || null,
|
|
1745
|
+
requires_login: true,
|
|
1746
|
+
expected_url: expectedUrl,
|
|
1747
|
+
login_url: bossLoginUrl,
|
|
1748
|
+
message: "页面检测到登录态特征,请先完成 Boss 登录。"
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
return buildBossPageState({
|
|
1752
|
+
ok: false,
|
|
1753
|
+
state: frameProbe?.error || "NO_RECOMMEND_IFRAME",
|
|
1754
|
+
path: frameProbe?.current_url || exactTab.url || expectedUrl,
|
|
1755
|
+
current_url: frameProbe?.current_url || exactTab.url || null,
|
|
1756
|
+
title: frameProbe?.title || exactTab.title || null,
|
|
1757
|
+
expected_url: expectedUrl,
|
|
1758
|
+
frame_url: frameProbe?.frame_url || null,
|
|
1759
|
+
frame_present: frameProbe?.frame_present === true,
|
|
1760
|
+
iframe_count: Number.isFinite(Number(frameProbe?.iframe_count))
|
|
1761
|
+
? Number(frameProbe.iframe_count)
|
|
1762
|
+
: null,
|
|
1763
|
+
message: "recommend iframe 暂不可用。"
|
|
1764
|
+
});
|
|
1765
|
+
} catch (error) {
|
|
1766
|
+
return buildBossPageState({
|
|
1767
|
+
ok: false,
|
|
1768
|
+
state: "DEBUG_PORT_UNREACHABLE",
|
|
1769
|
+
path: `http://127.0.0.1:${port}`,
|
|
1770
|
+
current_url: null,
|
|
1771
|
+
title: null,
|
|
1772
|
+
requires_login: false,
|
|
1773
|
+
expected_url: expectedUrl,
|
|
1774
|
+
message: `无法连接到 Chrome DevTools 端口 ${port}。请确认 Chrome 已以远程调试模式启动。`,
|
|
1775
|
+
error: error?.message || String(error)
|
|
1776
|
+
});
|
|
1777
|
+
} finally {
|
|
1778
|
+
if (client) {
|
|
1779
|
+
try {
|
|
1780
|
+
await client.close();
|
|
1781
|
+
} catch {}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
async function waitForRecommendIframeReady(port, options = {}) {
|
|
1787
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 3000;
|
|
1788
|
+
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 600;
|
|
1789
|
+
const expectedUrl = options.expectedUrl || bossRecommendUrl;
|
|
1790
|
+
const deadline = Date.now() + timeoutMs;
|
|
1791
|
+
let lastState = null;
|
|
1792
|
+
|
|
1793
|
+
while (Date.now() < deadline) {
|
|
1794
|
+
lastState = await probeRecommendIframeState(port, { expectedUrl });
|
|
1795
|
+
if (lastState?.state === "RECOMMEND_IFRAME_READY") {
|
|
1796
|
+
return lastState;
|
|
1797
|
+
}
|
|
1798
|
+
if (
|
|
1799
|
+
lastState?.state === "LOGIN_REQUIRED"
|
|
1800
|
+
|| lastState?.state === "LOGIN_REQUIRED_AFTER_REDIRECT"
|
|
1801
|
+
|| lastState?.state === "DEBUG_PORT_UNREACHABLE"
|
|
1802
|
+
|| lastState?.state === "BOSS_TAB_NOT_FOUND"
|
|
1803
|
+
|| lastState?.state === "BOSS_NOT_ON_RECOMMEND"
|
|
1804
|
+
) {
|
|
1805
|
+
return lastState;
|
|
1806
|
+
}
|
|
1807
|
+
await sleep(pollMs);
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
return lastState || buildBossPageState({
|
|
1811
|
+
ok: false,
|
|
1812
|
+
state: "NO_RECOMMEND_IFRAME",
|
|
1813
|
+
path: expectedUrl,
|
|
1814
|
+
current_url: null,
|
|
1815
|
+
title: null,
|
|
1816
|
+
expected_url: expectedUrl,
|
|
1817
|
+
message: "recommend iframe 尚未就绪。"
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
function pickBossRecommendReloadTarget(tabs = []) {
|
|
1822
|
+
return findRecommendTab(tabs) || tabs.find(
|
|
1823
|
+
(tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")
|
|
1824
|
+
) || null;
|
|
1825
|
+
}
|
|
1531
1826
|
|
|
1532
1827
|
async function evaluateCdpExpression(client, expression) {
|
|
1533
1828
|
const result = await client.Runtime.evaluate({
|
|
@@ -1543,15 +1838,16 @@ async function evaluateCdpExpression(client, expression) {
|
|
|
1543
1838
|
|
|
1544
1839
|
function buildRecommendTabStateExpression() {
|
|
1545
1840
|
return `(() => {
|
|
1546
|
-
const frame =
|
|
1547
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1548
|
-
|| document.querySelector('iframe');
|
|
1841
|
+
const frame = ${buildRecommendIframeLookupExpression()};
|
|
1549
1842
|
if (!frame || !frame.contentDocument) {
|
|
1550
1843
|
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1551
1844
|
}
|
|
1552
1845
|
const doc = frame.contentDocument;
|
|
1553
1846
|
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
1554
|
-
const tabs = Array.from(
|
|
1847
|
+
const tabs = Array.from(new Set(${JSON.stringify(RECOMMEND_TAB_SELECTORS)}
|
|
1848
|
+
.flatMap((selector) => {
|
|
1849
|
+
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
1850
|
+
}))).map((node) => {
|
|
1555
1851
|
const status = normalize(node.getAttribute('data-status'));
|
|
1556
1852
|
const className = normalize(node.className);
|
|
1557
1853
|
const active = (
|
|
@@ -1568,9 +1864,18 @@ function buildRecommendTabStateExpression() {
|
|
|
1568
1864
|
};
|
|
1569
1865
|
});
|
|
1570
1866
|
const activeTab = tabs.find((item) => item.active && item.status) || null;
|
|
1571
|
-
const featuredCount =
|
|
1572
|
-
|
|
1573
|
-
|
|
1867
|
+
const featuredCount = ${JSON.stringify(FEATURED_CARD_SELECTORS)}
|
|
1868
|
+
.reduce((sum, selector) => {
|
|
1869
|
+
try { return sum + doc.querySelectorAll(selector).length; } catch { return sum; }
|
|
1870
|
+
}, 0);
|
|
1871
|
+
const recommendCount = ${JSON.stringify(RECOMMEND_CARD_SELECTORS)}
|
|
1872
|
+
.reduce((sum, selector) => {
|
|
1873
|
+
try { return sum + doc.querySelectorAll(selector).length; } catch { return sum; }
|
|
1874
|
+
}, 0);
|
|
1875
|
+
const latestCount = ${JSON.stringify(LATEST_CARD_SELECTORS)}
|
|
1876
|
+
.reduce((sum, selector) => {
|
|
1877
|
+
try { return sum + doc.querySelectorAll(selector).length; } catch { return sum; }
|
|
1878
|
+
}, 0);
|
|
1574
1879
|
let inferredStatus = activeTab?.status || null;
|
|
1575
1880
|
if (!inferredStatus) {
|
|
1576
1881
|
if (featuredCount > 0 && recommendCount === 0 && latestCount === 0) inferredStatus = '3';
|
|
@@ -1592,15 +1897,16 @@ function buildRecommendTabStateExpression() {
|
|
|
1592
1897
|
|
|
1593
1898
|
function buildRecommendTabSwitchExpression(targetStatus) {
|
|
1594
1899
|
return `((targetStatus) => {
|
|
1595
|
-
const frame =
|
|
1596
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1597
|
-
|| document.querySelector('iframe');
|
|
1900
|
+
const frame = ${buildRecommendIframeLookupExpression()};
|
|
1598
1901
|
if (!frame || !frame.contentDocument) {
|
|
1599
1902
|
return { ok: false, state: 'NO_RECOMMEND_IFRAME' };
|
|
1600
1903
|
}
|
|
1601
1904
|
const doc = frame.contentDocument;
|
|
1602
1905
|
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
1603
|
-
const tabs = Array.from(
|
|
1906
|
+
const tabs = Array.from(new Set(${JSON.stringify(RECOMMEND_TAB_SELECTORS)}
|
|
1907
|
+
.flatMap((selector) => {
|
|
1908
|
+
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
1909
|
+
})));
|
|
1604
1910
|
const target = tabs.find((node) => normalize(node.getAttribute('data-status')) === String(targetStatus)) || null;
|
|
1605
1911
|
if (!target) {
|
|
1606
1912
|
return { ok: false, state: 'TAB_NOT_FOUND', target_status: String(targetStatus) };
|
|
@@ -1628,19 +1934,7 @@ function buildRecommendTabSwitchExpression(targetStatus) {
|
|
|
1628
1934
|
|
|
1629
1935
|
function buildRecommendDetailStateExpression() {
|
|
1630
1936
|
return `(() => {
|
|
1631
|
-
const selectors = [
|
|
1632
|
-
'.dialog-wrap.active',
|
|
1633
|
-
'.boss-popup__wrapper',
|
|
1634
|
-
'.boss-popup_wrapper',
|
|
1635
|
-
'.boss-dialog_wrapper',
|
|
1636
|
-
'.boss-dialog',
|
|
1637
|
-
'.resume-item-detail',
|
|
1638
|
-
'.geek-detail-modal',
|
|
1639
|
-
'iframe[src*="/web/frame/c-resume/"]',
|
|
1640
|
-
'iframe[name*="resume"]',
|
|
1641
|
-
'[class*="popup"][class*="wrapper"]',
|
|
1642
|
-
'[class*="dialog"][class*="wrapper"]'
|
|
1643
|
-
];
|
|
1937
|
+
const selectors = ${JSON.stringify([...DETAIL_POPUP_SELECTORS, ...DETAIL_RESUME_IFRAME_SELECTORS])};
|
|
1644
1938
|
const isVisible = (node) => {
|
|
1645
1939
|
if (!node || !node.getBoundingClientRect) return false;
|
|
1646
1940
|
const rect = node.getBoundingClientRect();
|
|
@@ -1674,9 +1968,7 @@ function buildRecommendDetailStateExpression() {
|
|
|
1674
1968
|
const topDetail = findVisibleDetail(document, 'top');
|
|
1675
1969
|
if (topDetail) return topDetail;
|
|
1676
1970
|
|
|
1677
|
-
const frame =
|
|
1678
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1679
|
-
|| null;
|
|
1971
|
+
const frame = ${buildRecommendIframeLookupExpression()} || null;
|
|
1680
1972
|
if (!frame || !frame.contentDocument) {
|
|
1681
1973
|
return { ok: true, open: false, reason: 'NO_RECOMMEND_IFRAME' };
|
|
1682
1974
|
}
|
|
@@ -1940,14 +2232,12 @@ export async function switchRecommendTab(workspaceRoot, options = {}) {
|
|
|
1940
2232
|
}
|
|
1941
2233
|
}
|
|
1942
2234
|
|
|
1943
|
-
function buildRecommendRefreshStateExpression() {
|
|
1944
|
-
return `(() => {
|
|
1945
|
-
const frame =
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1950
|
-
}
|
|
2235
|
+
function buildRecommendRefreshStateExpression() {
|
|
2236
|
+
return `(() => {
|
|
2237
|
+
const frame = ${buildRecommendIframeLookupExpression()};
|
|
2238
|
+
if (!frame || !frame.contentDocument) {
|
|
2239
|
+
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
2240
|
+
}
|
|
1951
2241
|
const doc = frame.contentDocument;
|
|
1952
2242
|
const isVisible = (el) => {
|
|
1953
2243
|
if (!el) return false;
|
|
@@ -1960,13 +2250,29 @@ function buildRecommendRefreshStateExpression() {
|
|
|
1960
2250
|
const rect = el.getBoundingClientRect();
|
|
1961
2251
|
return rect.width > 2 && rect.height > 2 && el.offsetParent !== null;
|
|
1962
2252
|
};
|
|
1963
|
-
const finishedWrap =
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2253
|
+
const finishedWrap = ${JSON.stringify(REFRESH_FINISHED_WRAP_SELECTORS)}
|
|
2254
|
+
.flatMap((selector) => {
|
|
2255
|
+
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
2256
|
+
})
|
|
2257
|
+
.find((el) => isVisible(el)) || null;
|
|
2258
|
+
const refreshButton = ${JSON.stringify(REFRESH_BUTTON_SELECTORS)}
|
|
2259
|
+
.flatMap((selector) => {
|
|
2260
|
+
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
2261
|
+
})
|
|
2262
|
+
.find((el) => isVisible(el)) || null;
|
|
2263
|
+
const cards = ${JSON.stringify(RECOMMEND_CARD_SELECTORS)}
|
|
2264
|
+
.flatMap((selector) => {
|
|
2265
|
+
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
2266
|
+
});
|
|
1967
2267
|
const candidateCards = cards.filter((card) => card.querySelector('.card-inner[data-geekid]'));
|
|
1968
|
-
const latestCards =
|
|
1969
|
-
|
|
2268
|
+
const latestCards = ${JSON.stringify(LATEST_CARD_SELECTORS)}
|
|
2269
|
+
.flatMap((selector) => {
|
|
2270
|
+
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
2271
|
+
});
|
|
2272
|
+
const tabs = ${JSON.stringify(RECOMMEND_TAB_SELECTORS)}
|
|
2273
|
+
.flatMap((selector) => {
|
|
2274
|
+
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
2275
|
+
});
|
|
1970
2276
|
const activeTab = tabs.find((node) => /(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(String(node.className || ''))) || null;
|
|
1971
2277
|
const activeStatus = activeTab ? String(activeTab.getAttribute('data-status') || '') : '';
|
|
1972
2278
|
const inferredStatus = activeStatus
|
|
@@ -1992,14 +2298,12 @@ function buildRecommendRefreshStateExpression() {
|
|
|
1992
2298
|
})()`;
|
|
1993
2299
|
}
|
|
1994
2300
|
|
|
1995
|
-
function buildRecommendRefreshClickExpression() {
|
|
1996
|
-
return `(() => {
|
|
1997
|
-
const frame =
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
return { ok: false, state: 'NO_RECOMMEND_IFRAME' };
|
|
2002
|
-
}
|
|
2301
|
+
function buildRecommendRefreshClickExpression() {
|
|
2302
|
+
return `(() => {
|
|
2303
|
+
const frame = ${buildRecommendIframeLookupExpression()};
|
|
2304
|
+
if (!frame || !frame.contentDocument) {
|
|
2305
|
+
return { ok: false, state: 'NO_RECOMMEND_IFRAME' };
|
|
2306
|
+
}
|
|
2003
2307
|
const doc = frame.contentDocument;
|
|
2004
2308
|
const isVisible = (el) => {
|
|
2005
2309
|
if (!el) return false;
|
|
@@ -2012,8 +2316,11 @@ function buildRecommendRefreshClickExpression() {
|
|
|
2012
2316
|
const rect = el.getBoundingClientRect();
|
|
2013
2317
|
return rect.width > 2 && rect.height > 2 && el.offsetParent !== null;
|
|
2014
2318
|
};
|
|
2015
|
-
const refreshButton =
|
|
2016
|
-
.
|
|
2319
|
+
const refreshButton = ${JSON.stringify(REFRESH_BUTTON_SELECTORS)}
|
|
2320
|
+
.flatMap((selector) => {
|
|
2321
|
+
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
2322
|
+
})
|
|
2323
|
+
.find((el) => isVisible(el)) || null;
|
|
2017
2324
|
if (!refreshButton) {
|
|
2018
2325
|
return { ok: false, state: 'REFRESH_BUTTON_NOT_FOUND' };
|
|
2019
2326
|
}
|