@reconcrap/boss-recommend-mcp 1.2.6 → 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 +112 -60
- package/src/index.js +97 -0
- 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-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
|
|
@@ -1575,9 +1617,7 @@ function buildRecommendIframeProbeExpression() {
|
|
|
1575
1617
|
title
|
|
1576
1618
|
};
|
|
1577
1619
|
}
|
|
1578
|
-
const frame =
|
|
1579
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1580
|
-
|| document.querySelector('iframe');
|
|
1620
|
+
const frame = ${buildRecommendIframeLookupExpression()};
|
|
1581
1621
|
const frameUrl = (() => {
|
|
1582
1622
|
try {
|
|
1583
1623
|
return String(frame?.contentWindow?.location?.href || frame?.src || "");
|
|
@@ -1798,15 +1838,16 @@ async function evaluateCdpExpression(client, expression) {
|
|
|
1798
1838
|
|
|
1799
1839
|
function buildRecommendTabStateExpression() {
|
|
1800
1840
|
return `(() => {
|
|
1801
|
-
const frame =
|
|
1802
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1803
|
-
|| document.querySelector('iframe');
|
|
1841
|
+
const frame = ${buildRecommendIframeLookupExpression()};
|
|
1804
1842
|
if (!frame || !frame.contentDocument) {
|
|
1805
1843
|
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1806
1844
|
}
|
|
1807
1845
|
const doc = frame.contentDocument;
|
|
1808
1846
|
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
1809
|
-
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) => {
|
|
1810
1851
|
const status = normalize(node.getAttribute('data-status'));
|
|
1811
1852
|
const className = normalize(node.className);
|
|
1812
1853
|
const active = (
|
|
@@ -1823,9 +1864,18 @@ function buildRecommendTabStateExpression() {
|
|
|
1823
1864
|
};
|
|
1824
1865
|
});
|
|
1825
1866
|
const activeTab = tabs.find((item) => item.active && item.status) || null;
|
|
1826
|
-
const featuredCount =
|
|
1827
|
-
|
|
1828
|
-
|
|
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);
|
|
1829
1879
|
let inferredStatus = activeTab?.status || null;
|
|
1830
1880
|
if (!inferredStatus) {
|
|
1831
1881
|
if (featuredCount > 0 && recommendCount === 0 && latestCount === 0) inferredStatus = '3';
|
|
@@ -1847,15 +1897,16 @@ function buildRecommendTabStateExpression() {
|
|
|
1847
1897
|
|
|
1848
1898
|
function buildRecommendTabSwitchExpression(targetStatus) {
|
|
1849
1899
|
return `((targetStatus) => {
|
|
1850
|
-
const frame =
|
|
1851
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1852
|
-
|| document.querySelector('iframe');
|
|
1900
|
+
const frame = ${buildRecommendIframeLookupExpression()};
|
|
1853
1901
|
if (!frame || !frame.contentDocument) {
|
|
1854
1902
|
return { ok: false, state: 'NO_RECOMMEND_IFRAME' };
|
|
1855
1903
|
}
|
|
1856
1904
|
const doc = frame.contentDocument;
|
|
1857
1905
|
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
1858
|
-
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
|
+
})));
|
|
1859
1910
|
const target = tabs.find((node) => normalize(node.getAttribute('data-status')) === String(targetStatus)) || null;
|
|
1860
1911
|
if (!target) {
|
|
1861
1912
|
return { ok: false, state: 'TAB_NOT_FOUND', target_status: String(targetStatus) };
|
|
@@ -1883,19 +1934,7 @@ function buildRecommendTabSwitchExpression(targetStatus) {
|
|
|
1883
1934
|
|
|
1884
1935
|
function buildRecommendDetailStateExpression() {
|
|
1885
1936
|
return `(() => {
|
|
1886
|
-
const selectors = [
|
|
1887
|
-
'.dialog-wrap.active',
|
|
1888
|
-
'.boss-popup__wrapper',
|
|
1889
|
-
'.boss-popup_wrapper',
|
|
1890
|
-
'.boss-dialog_wrapper',
|
|
1891
|
-
'.boss-dialog',
|
|
1892
|
-
'.resume-item-detail',
|
|
1893
|
-
'.geek-detail-modal',
|
|
1894
|
-
'iframe[src*="/web/frame/c-resume/"]',
|
|
1895
|
-
'iframe[name*="resume"]',
|
|
1896
|
-
'[class*="popup"][class*="wrapper"]',
|
|
1897
|
-
'[class*="dialog"][class*="wrapper"]'
|
|
1898
|
-
];
|
|
1937
|
+
const selectors = ${JSON.stringify([...DETAIL_POPUP_SELECTORS, ...DETAIL_RESUME_IFRAME_SELECTORS])};
|
|
1899
1938
|
const isVisible = (node) => {
|
|
1900
1939
|
if (!node || !node.getBoundingClientRect) return false;
|
|
1901
1940
|
const rect = node.getBoundingClientRect();
|
|
@@ -1929,9 +1968,7 @@ function buildRecommendDetailStateExpression() {
|
|
|
1929
1968
|
const topDetail = findVisibleDetail(document, 'top');
|
|
1930
1969
|
if (topDetail) return topDetail;
|
|
1931
1970
|
|
|
1932
|
-
const frame =
|
|
1933
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1934
|
-
|| null;
|
|
1971
|
+
const frame = ${buildRecommendIframeLookupExpression()} || null;
|
|
1935
1972
|
if (!frame || !frame.contentDocument) {
|
|
1936
1973
|
return { ok: true, open: false, reason: 'NO_RECOMMEND_IFRAME' };
|
|
1937
1974
|
}
|
|
@@ -2195,14 +2232,12 @@ export async function switchRecommendTab(workspaceRoot, options = {}) {
|
|
|
2195
2232
|
}
|
|
2196
2233
|
}
|
|
2197
2234
|
|
|
2198
|
-
function buildRecommendRefreshStateExpression() {
|
|
2199
|
-
return `(() => {
|
|
2200
|
-
const frame =
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
2205
|
-
}
|
|
2235
|
+
function buildRecommendRefreshStateExpression() {
|
|
2236
|
+
return `(() => {
|
|
2237
|
+
const frame = ${buildRecommendIframeLookupExpression()};
|
|
2238
|
+
if (!frame || !frame.contentDocument) {
|
|
2239
|
+
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
2240
|
+
}
|
|
2206
2241
|
const doc = frame.contentDocument;
|
|
2207
2242
|
const isVisible = (el) => {
|
|
2208
2243
|
if (!el) return false;
|
|
@@ -2215,13 +2250,29 @@ function buildRecommendRefreshStateExpression() {
|
|
|
2215
2250
|
const rect = el.getBoundingClientRect();
|
|
2216
2251
|
return rect.width > 2 && rect.height > 2 && el.offsetParent !== null;
|
|
2217
2252
|
};
|
|
2218
|
-
const finishedWrap =
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
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
|
+
});
|
|
2222
2267
|
const candidateCards = cards.filter((card) => card.querySelector('.card-inner[data-geekid]'));
|
|
2223
|
-
const latestCards =
|
|
2224
|
-
|
|
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
|
+
});
|
|
2225
2276
|
const activeTab = tabs.find((node) => /(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(String(node.className || ''))) || null;
|
|
2226
2277
|
const activeStatus = activeTab ? String(activeTab.getAttribute('data-status') || '') : '';
|
|
2227
2278
|
const inferredStatus = activeStatus
|
|
@@ -2247,14 +2298,12 @@ function buildRecommendRefreshStateExpression() {
|
|
|
2247
2298
|
})()`;
|
|
2248
2299
|
}
|
|
2249
2300
|
|
|
2250
|
-
function buildRecommendRefreshClickExpression() {
|
|
2251
|
-
return `(() => {
|
|
2252
|
-
const frame =
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
return { ok: false, state: 'NO_RECOMMEND_IFRAME' };
|
|
2257
|
-
}
|
|
2301
|
+
function buildRecommendRefreshClickExpression() {
|
|
2302
|
+
return `(() => {
|
|
2303
|
+
const frame = ${buildRecommendIframeLookupExpression()};
|
|
2304
|
+
if (!frame || !frame.contentDocument) {
|
|
2305
|
+
return { ok: false, state: 'NO_RECOMMEND_IFRAME' };
|
|
2306
|
+
}
|
|
2258
2307
|
const doc = frame.contentDocument;
|
|
2259
2308
|
const isVisible = (el) => {
|
|
2260
2309
|
if (!el) return false;
|
|
@@ -2267,8 +2316,11 @@ function buildRecommendRefreshClickExpression() {
|
|
|
2267
2316
|
const rect = el.getBoundingClientRect();
|
|
2268
2317
|
return rect.width > 2 && rect.height > 2 && el.offsetParent !== null;
|
|
2269
2318
|
};
|
|
2270
|
-
const refreshButton =
|
|
2271
|
-
.
|
|
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;
|
|
2272
2324
|
if (!refreshButton) {
|
|
2273
2325
|
return { ok: false, state: 'REFRESH_BUTTON_NOT_FOUND' };
|
|
2274
2326
|
}
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
runRecommendCalibration
|
|
10
10
|
} from "./adapters.js";
|
|
11
11
|
import { runRecommendPipeline } from "./pipeline.js";
|
|
12
|
+
import { runRecommendSelfHeal } from "./self-heal.js";
|
|
12
13
|
import {
|
|
13
14
|
RUN_MODE_ASYNC,
|
|
14
15
|
RUN_STAGE_PREFLIGHT,
|
|
@@ -39,6 +40,7 @@ const TOOL_PAUSE_RUN = "pause_recommend_pipeline_run";
|
|
|
39
40
|
const TOOL_RESUME_RUN = "resume_recommend_pipeline_run";
|
|
40
41
|
const TOOL_RUN_FEATURED_CALIBRATION = "run_featured_calibration";
|
|
41
42
|
const TOOL_GET_FEATURED_CALIBRATION_STATUS = "get_featured_calibration_status";
|
|
43
|
+
const TOOL_RUN_RECOMMEND_SELF_HEAL = "run_recommend_self_heal";
|
|
42
44
|
|
|
43
45
|
const SERVER_NAME = "boss-recommend-mcp";
|
|
44
46
|
const FRAMING_UNKNOWN = "unknown";
|
|
@@ -49,6 +51,7 @@ const DETACHED_WORKER_RUN_ID_FLAG = "--run-id";
|
|
|
49
51
|
const DETACHED_WORKER_RESUME_FLAG = "--resume";
|
|
50
52
|
|
|
51
53
|
let runPipelineImpl = runRecommendPipeline;
|
|
54
|
+
let runSelfHealImpl = runRecommendSelfHeal;
|
|
52
55
|
let spawnProcessImpl = spawn;
|
|
53
56
|
const TERMINAL_RUN_STATES = new Set([RUN_STATE_COMPLETED, RUN_STATE_FAILED, RUN_STATE_CANCELED]);
|
|
54
57
|
|
|
@@ -321,6 +324,43 @@ function createRunFeaturedCalibrationInputSchema() {
|
|
|
321
324
|
};
|
|
322
325
|
}
|
|
323
326
|
|
|
327
|
+
function createRunRecommendSelfHealInputSchema() {
|
|
328
|
+
return {
|
|
329
|
+
type: "object",
|
|
330
|
+
properties: {
|
|
331
|
+
mode: {
|
|
332
|
+
type: "string",
|
|
333
|
+
enum: ["scan", "apply"],
|
|
334
|
+
description: "scan=扫描漂移;apply=按 repair_session_id 应用高置信度修复"
|
|
335
|
+
},
|
|
336
|
+
scope: {
|
|
337
|
+
type: "string",
|
|
338
|
+
enum: ["full", "search_screen", "selectors_only"],
|
|
339
|
+
description: "扫描范围,默认 full"
|
|
340
|
+
},
|
|
341
|
+
validation_profile: {
|
|
342
|
+
type: "string",
|
|
343
|
+
enum: ["safe", "full"],
|
|
344
|
+
description: "校验强度,默认 full"
|
|
345
|
+
},
|
|
346
|
+
port: {
|
|
347
|
+
type: "integer",
|
|
348
|
+
minimum: 1,
|
|
349
|
+
description: "可选,Boss Chrome 远程调试端口"
|
|
350
|
+
},
|
|
351
|
+
repair_session_id: {
|
|
352
|
+
type: "string",
|
|
353
|
+
description: "apply 模式必填,来自 scan 返回值"
|
|
354
|
+
},
|
|
355
|
+
confirm_apply: {
|
|
356
|
+
type: "boolean",
|
|
357
|
+
description: "apply 模式必填,必须显式传 true"
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
additionalProperties: false
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
324
364
|
function createToolsSchema() {
|
|
325
365
|
return [
|
|
326
366
|
{
|
|
@@ -389,6 +429,11 @@ function createToolsSchema() {
|
|
|
389
429
|
properties: {},
|
|
390
430
|
additionalProperties: false
|
|
391
431
|
}
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: TOOL_RUN_RECOMMEND_SELF_HEAL,
|
|
435
|
+
description: "手动运维自愈工具:扫描 Boss recommend 相关 selector / network 规则漂移,并在确认后应用高置信度修复。",
|
|
436
|
+
inputSchema: createRunRecommendSelfHealInputSchema()
|
|
392
437
|
}
|
|
393
438
|
];
|
|
394
439
|
}
|
|
@@ -448,6 +493,42 @@ function validateRunFeaturedCalibrationArgs(args) {
|
|
|
448
493
|
return null;
|
|
449
494
|
}
|
|
450
495
|
|
|
496
|
+
function validateRunRecommendSelfHealArgs(args) {
|
|
497
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) {
|
|
498
|
+
return "arguments must be an object";
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (Object.prototype.hasOwnProperty.call(args, "mode")) {
|
|
502
|
+
const mode = normalizeText(args.mode).toLowerCase();
|
|
503
|
+
if (!["scan", "apply"].includes(mode)) {
|
|
504
|
+
return "mode must be one of: scan, apply";
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (Object.prototype.hasOwnProperty.call(args, "scope")) {
|
|
509
|
+
const scope = normalizeText(args.scope).toLowerCase();
|
|
510
|
+
if (!["full", "search_screen", "selectors_only"].includes(scope)) {
|
|
511
|
+
return "scope must be one of: full, search_screen, selectors_only";
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (Object.prototype.hasOwnProperty.call(args, "validation_profile")) {
|
|
516
|
+
const profile = normalizeText(args.validation_profile).toLowerCase();
|
|
517
|
+
if (!["safe", "full"].includes(profile)) {
|
|
518
|
+
return "validation_profile must be one of: safe, full";
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (Object.prototype.hasOwnProperty.call(args, "port")) {
|
|
523
|
+
const port = Number.parseInt(String(args.port), 10);
|
|
524
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
525
|
+
return "port must be a positive integer";
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
|
|
451
532
|
function getLastOutputLine(text) {
|
|
452
533
|
const lines = String(text || "")
|
|
453
534
|
.split(/\r?\n/)
|
|
@@ -1340,6 +1421,10 @@ async function handleRunFeaturedCalibrationTool({ workspaceRoot, args }) {
|
|
|
1340
1421
|
};
|
|
1341
1422
|
}
|
|
1342
1423
|
|
|
1424
|
+
async function handleRunRecommendSelfHealTool({ workspaceRoot, args }) {
|
|
1425
|
+
return runSelfHealImpl({ workspaceRoot, args });
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1343
1428
|
async function handleRequest(message, workspaceRoot) {
|
|
1344
1429
|
if (!message || message.jsonrpc !== "2.0") {
|
|
1345
1430
|
return createJsonRpcError(null, -32600, "Invalid JSON-RPC request");
|
|
@@ -1396,6 +1481,13 @@ async function handleRequest(message, workspaceRoot) {
|
|
|
1396
1481
|
}
|
|
1397
1482
|
}
|
|
1398
1483
|
|
|
1484
|
+
if (toolName === TOOL_RUN_RECOMMEND_SELF_HEAL) {
|
|
1485
|
+
const inputError = validateRunRecommendSelfHealArgs(args);
|
|
1486
|
+
if (inputError) {
|
|
1487
|
+
return createJsonRpcError(id, -32602, inputError);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1399
1491
|
if ([TOOL_GET_RUN, TOOL_CANCEL_RUN, TOOL_PAUSE_RUN, TOOL_RESUME_RUN].includes(toolName)) {
|
|
1400
1492
|
if (!args || typeof args.run_id !== "string" || !normalizeText(args.run_id)) {
|
|
1401
1493
|
return createJsonRpcError(id, -32602, "run_id is required and must be a string");
|
|
@@ -1418,6 +1510,8 @@ async function handleRequest(message, workspaceRoot) {
|
|
|
1418
1510
|
payload = handleGetFeaturedCalibrationStatusTool(workspaceRoot);
|
|
1419
1511
|
} else if (toolName === TOOL_RUN_FEATURED_CALIBRATION) {
|
|
1420
1512
|
payload = await handleRunFeaturedCalibrationTool({ workspaceRoot, args });
|
|
1513
|
+
} else if (toolName === TOOL_RUN_RECOMMEND_SELF_HEAL) {
|
|
1514
|
+
payload = await handleRunRecommendSelfHealTool({ workspaceRoot, args });
|
|
1421
1515
|
} else {
|
|
1422
1516
|
return createJsonRpcError(id, -32602, `Unknown tool: ${toolName || ""}`);
|
|
1423
1517
|
}
|
|
@@ -1553,6 +1647,9 @@ export const __testables = {
|
|
|
1553
1647
|
},
|
|
1554
1648
|
setRunPipelineImplForTests(nextImpl) {
|
|
1555
1649
|
runPipelineImpl = typeof nextImpl === "function" ? nextImpl : runRecommendPipeline;
|
|
1650
|
+
},
|
|
1651
|
+
setRunSelfHealImplForTests(nextImpl) {
|
|
1652
|
+
runSelfHealImpl = typeof nextImpl === "function" ? nextImpl : runRecommendSelfHeal;
|
|
1556
1653
|
}
|
|
1557
1654
|
};
|
|
1558
1655
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
9
|
+
const defaultRulesPath = path.join(path.dirname(currentFilePath), "recommend-healing-rules.json");
|
|
10
|
+
|
|
11
|
+
let cachedRulesPath = null;
|
|
12
|
+
let cachedRulesMtime = null;
|
|
13
|
+
let cachedRules = null;
|
|
14
|
+
|
|
15
|
+
function clone(value) {
|
|
16
|
+
return JSON.parse(JSON.stringify(value));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizePathLike(value) {
|
|
20
|
+
return String(value || "").trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getNestedValue(root, pathParts = []) {
|
|
24
|
+
let current = root;
|
|
25
|
+
for (const part of pathParts) {
|
|
26
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
current = current[part];
|
|
30
|
+
}
|
|
31
|
+
return current;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getRecommendHealingRulesPath() {
|
|
35
|
+
const fromEnv = normalizePathLike(process.env.BOSS_RECOMMEND_HEALING_RULES_FILE);
|
|
36
|
+
return fromEnv ? path.resolve(fromEnv) : defaultRulesPath;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function loadRecommendHealingRules(options = {}) {
|
|
40
|
+
const fresh = options.fresh === true;
|
|
41
|
+
const rulesPath = getRecommendHealingRulesPath();
|
|
42
|
+
const stats = fs.statSync(rulesPath);
|
|
43
|
+
if (
|
|
44
|
+
!fresh
|
|
45
|
+
&& cachedRules
|
|
46
|
+
&& cachedRulesPath === rulesPath
|
|
47
|
+
&& cachedRulesMtime === Number(stats.mtimeMs)
|
|
48
|
+
) {
|
|
49
|
+
return clone(cachedRules);
|
|
50
|
+
}
|
|
51
|
+
const nextRules = require(rulesPath);
|
|
52
|
+
cachedRulesPath = rulesPath;
|
|
53
|
+
cachedRulesMtime = Number(stats.mtimeMs);
|
|
54
|
+
cachedRules = clone(nextRules);
|
|
55
|
+
return clone(cachedRules);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function saveRecommendHealingRules(nextRules) {
|
|
59
|
+
const rulesPath = getRecommendHealingRulesPath();
|
|
60
|
+
const serialized = `${JSON.stringify(nextRules, null, 2)}\n`;
|
|
61
|
+
fs.writeFileSync(rulesPath, serialized, "utf8");
|
|
62
|
+
cachedRulesPath = rulesPath;
|
|
63
|
+
cachedRulesMtime = Number(fs.statSync(rulesPath).mtimeMs);
|
|
64
|
+
cachedRules = clone(nextRules);
|
|
65
|
+
return rulesPath;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getRecommendSelectorRule(pathParts = [], fallback = []) {
|
|
69
|
+
const value = getNestedValue(loadRecommendHealingRules(), ["selectors", ...pathParts]);
|
|
70
|
+
return Array.isArray(value) && value.length > 0 ? value.map((item) => String(item)) : fallback.slice();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getRecommendNetworkRule(pathParts = [], fallback = null) {
|
|
74
|
+
const value = getNestedValue(loadRecommendHealingRules(), ["network", ...pathParts]);
|
|
75
|
+
if (Array.isArray(value)) return value.map((item) => String(item));
|
|
76
|
+
if (value && typeof value === "object") return clone(value);
|
|
77
|
+
if (typeof value === "string") return value;
|
|
78
|
+
if (Array.isArray(fallback)) return fallback.slice();
|
|
79
|
+
if (fallback && typeof fallback === "object") return clone(fallback);
|
|
80
|
+
return fallback;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function compileRegexList(patterns = []) {
|
|
84
|
+
return (Array.isArray(patterns) ? patterns : [])
|
|
85
|
+
.map((pattern) => {
|
|
86
|
+
try {
|
|
87
|
+
return new RegExp(String(pattern), "i");
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
.filter(Boolean);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function findFirstMatchingPattern(value, patterns = []) {
|
|
96
|
+
const text = String(value || "");
|
|
97
|
+
for (const pattern of compileRegexList(patterns)) {
|
|
98
|
+
if (pattern.test(text)) return pattern.source;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function matchesAnyPattern(value, patterns = []) {
|
|
104
|
+
return Boolean(findFirstMatchingPattern(value, patterns));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function buildFirstSelectorLookupExpression(selectors = [], rootExpr = "document") {
|
|
108
|
+
return `(() => {
|
|
109
|
+
const selectors = ${JSON.stringify(selectors)};
|
|
110
|
+
for (const selector of selectors) {
|
|
111
|
+
try {
|
|
112
|
+
const node = ${rootExpr}.querySelector(selector);
|
|
113
|
+
if (node) return node;
|
|
114
|
+
} catch {}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
})()`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function buildSelectorCollectionExpression(selectors = [], rootExpr = "document") {
|
|
121
|
+
return `(() => {
|
|
122
|
+
const selectors = ${JSON.stringify(selectors)};
|
|
123
|
+
const nodes = [];
|
|
124
|
+
for (const selector of selectors) {
|
|
125
|
+
try {
|
|
126
|
+
nodes.push(...Array.from(${rootExpr}.querySelectorAll(selector)));
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
return Array.from(new Set(nodes));
|
|
130
|
+
})()`;
|
|
131
|
+
}
|