@reconcrap/boss-recommend-mcp 1.2.6 → 1.2.8
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/parser.js +5 -5
- package/src/pipeline.js +51 -1
- 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 +70 -10
- package/src/test-self-heal.js +224 -0
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +570 -189
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +218 -0
- package/vendor/boss-recommend-search-cli/src/cli.js +98 -50
|
@@ -15,6 +15,9 @@ const RESUME_CAPTURE_RETRY_DELAY_MS = 1200;
|
|
|
15
15
|
const MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES = 10;
|
|
16
16
|
const DEFAULT_VISION_MAX_IMAGE_PIXELS = 36000000;
|
|
17
17
|
const DEFAULT_VISION_RETRY_MAX_IMAGE_PIXELS = 30000000;
|
|
18
|
+
const DEFAULT_TEXT_MODEL_CHUNK_SIZE_CHARS = 24000;
|
|
19
|
+
const DEFAULT_TEXT_MODEL_CHUNK_OVERLAP_CHARS = 1200;
|
|
20
|
+
const DEFAULT_TEXT_MODEL_MAX_CHUNKS = 12;
|
|
18
21
|
let visionSharpFactory = null;
|
|
19
22
|
const PAGE_SCOPE_TAB_STATUS = {
|
|
20
23
|
recommend: "0",
|
|
@@ -24,6 +27,204 @@ const PAGE_SCOPE_TAB_STATUS = {
|
|
|
24
27
|
const BOTTOM_HINT_KEYWORDS = ["没有更多", "已显示全部", "已经到底", "暂无更多", "推荐完了", "没有更多人选"];
|
|
25
28
|
const LOAD_MORE_HINT_KEYWORDS = ["滚动加载更多", "下滑加载更多", "继续下滑", "继续滑动", "滑动加载", "正在加载", "加载中"];
|
|
26
29
|
|
|
30
|
+
function getHealingRulesPath() {
|
|
31
|
+
const fromEnv = normalizeText(process.env.BOSS_RECOMMEND_HEALING_RULES_FILE || "");
|
|
32
|
+
return fromEnv
|
|
33
|
+
? path.resolve(fromEnv)
|
|
34
|
+
: path.resolve(__dirname, "..", "..", "src", "recommend-healing-rules.json");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function loadHealingRules() {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(fs.readFileSync(getHealingRulesPath(), "utf8"));
|
|
40
|
+
} catch {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getHealingValue(root, pathParts, fallback) {
|
|
46
|
+
let current = root;
|
|
47
|
+
for (const part of pathParts) {
|
|
48
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
49
|
+
current = undefined;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
current = current[part];
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(current) && current.length > 0) {
|
|
55
|
+
return current.map((item) => String(item));
|
|
56
|
+
}
|
|
57
|
+
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
58
|
+
return JSON.parse(JSON.stringify(current));
|
|
59
|
+
}
|
|
60
|
+
if (typeof current === "string") return current;
|
|
61
|
+
return fallback;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function compilePatternList(patterns = []) {
|
|
65
|
+
return (Array.isArray(patterns) ? patterns : [])
|
|
66
|
+
.map((pattern) => {
|
|
67
|
+
try {
|
|
68
|
+
return new RegExp(String(pattern), "i");
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function firstMatchingPattern(text, patterns = []) {
|
|
77
|
+
const normalized = String(text || "");
|
|
78
|
+
for (const pattern of compilePatternList(patterns)) {
|
|
79
|
+
if (pattern.test(normalized)) return pattern.source;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildFirstSelectorLookupExpression(selectors = [], rootExpr = "document") {
|
|
85
|
+
return `(() => {
|
|
86
|
+
const selectors = ${JSON.stringify(selectors)};
|
|
87
|
+
for (const selector of selectors) {
|
|
88
|
+
try {
|
|
89
|
+
const node = ${rootExpr}.querySelector(selector);
|
|
90
|
+
if (node) return node;
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
})()`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildSelectorCollectionExpression(selectors = [], rootExpr = "document") {
|
|
98
|
+
return `(() => {
|
|
99
|
+
const selectors = ${JSON.stringify(selectors)};
|
|
100
|
+
const nodes = [];
|
|
101
|
+
for (const selector of selectors) {
|
|
102
|
+
try {
|
|
103
|
+
nodes.push(...Array.from(${rootExpr}.querySelectorAll(selector)));
|
|
104
|
+
} catch {}
|
|
105
|
+
}
|
|
106
|
+
return Array.from(new Set(nodes));
|
|
107
|
+
})()`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const HEALING_RULES = loadHealingRules();
|
|
111
|
+
const RECOMMEND_IFRAME_SELECTORS = getHealingValue(
|
|
112
|
+
HEALING_RULES,
|
|
113
|
+
["selectors", "top", "recommend_iframe"],
|
|
114
|
+
['iframe[name="recommendFrame"]', 'iframe[src*="/web/frame/recommend/"]', "iframe"]
|
|
115
|
+
);
|
|
116
|
+
const RECOMMEND_CARD_SELECTORS = getHealingValue(HEALING_RULES, ["selectors", "frame", "recommend_cards"], ["ul.card-list > li.card-item"]);
|
|
117
|
+
const FEATURED_CARD_SELECTORS = getHealingValue(HEALING_RULES, ["selectors", "frame", "featured_cards"], ["li.geek-info-card"]);
|
|
118
|
+
const LATEST_CARD_SELECTORS = getHealingValue(HEALING_RULES, ["selectors", "frame", "latest_cards"], [".candidate-card-wrap"]);
|
|
119
|
+
const RECOMMEND_TAB_SELECTORS = getHealingValue(
|
|
120
|
+
HEALING_RULES,
|
|
121
|
+
["selectors", "frame", "tab_items"],
|
|
122
|
+
["li.tab-item[data-status]", 'li[data-status][class*="tab"]']
|
|
123
|
+
);
|
|
124
|
+
const DETAIL_POPUP_SELECTORS = getHealingValue(
|
|
125
|
+
HEALING_RULES,
|
|
126
|
+
["selectors", "detail", "popup"],
|
|
127
|
+
[
|
|
128
|
+
".boss-popup__wrapper",
|
|
129
|
+
".boss-popup_wrapper",
|
|
130
|
+
".boss-dialog_wrapper",
|
|
131
|
+
".dialog-wrap.active",
|
|
132
|
+
".boss-dialog",
|
|
133
|
+
".geek-detail-modal",
|
|
134
|
+
".resume-item-detail"
|
|
135
|
+
]
|
|
136
|
+
);
|
|
137
|
+
const DETAIL_RESUME_IFRAME_SELECTORS = getHealingValue(
|
|
138
|
+
HEALING_RULES,
|
|
139
|
+
["selectors", "detail", "resume_iframe"],
|
|
140
|
+
['iframe[src*="/web/frame/c-resume/"]', 'iframe[name*="resume"]']
|
|
141
|
+
);
|
|
142
|
+
const DETAIL_CLOSE_SELECTORS = getHealingValue(
|
|
143
|
+
HEALING_RULES,
|
|
144
|
+
["selectors", "detail", "close_button"],
|
|
145
|
+
[
|
|
146
|
+
".boss-popup__close",
|
|
147
|
+
".popup-close",
|
|
148
|
+
".modal-close",
|
|
149
|
+
".dialog-close",
|
|
150
|
+
".close-btn",
|
|
151
|
+
'button[aria-label*="关闭"]',
|
|
152
|
+
'button[title*="关闭"]',
|
|
153
|
+
".icon-close"
|
|
154
|
+
]
|
|
155
|
+
);
|
|
156
|
+
const FAVORITE_BUTTON_SELECTORS = getHealingValue(
|
|
157
|
+
HEALING_RULES,
|
|
158
|
+
["selectors", "detail", "favorite_button"],
|
|
159
|
+
[".like-icon-and-text"]
|
|
160
|
+
);
|
|
161
|
+
const GREET_BUTTON_RECOMMEND_SELECTORS = getHealingValue(
|
|
162
|
+
HEALING_RULES,
|
|
163
|
+
["selectors", "detail", "greet_button_recommend"],
|
|
164
|
+
[
|
|
165
|
+
"button.btn-v2.btn-sure-v2.btn-greet",
|
|
166
|
+
".resume-footer.item-operate button.btn-v2",
|
|
167
|
+
".resume-footer-wrap button.btn-v2",
|
|
168
|
+
".resume-footer.item-operate button",
|
|
169
|
+
".resume-footer-wrap button"
|
|
170
|
+
]
|
|
171
|
+
);
|
|
172
|
+
const GREET_BUTTON_FEATURED_SELECTORS = getHealingValue(
|
|
173
|
+
HEALING_RULES,
|
|
174
|
+
["selectors", "detail", "greet_button_featured"],
|
|
175
|
+
[
|
|
176
|
+
"button.btn-v2.position-rights.btn-sure-v2",
|
|
177
|
+
"button.btn-v2.btn-sure-v2.position-rights",
|
|
178
|
+
".resume-footer.item-operate button.btn-v2",
|
|
179
|
+
".resume-footer-wrap button.btn-v2",
|
|
180
|
+
".resume-footer.item-operate button",
|
|
181
|
+
".resume-footer-wrap button"
|
|
182
|
+
]
|
|
183
|
+
);
|
|
184
|
+
const REFRESH_FINISHED_WRAP_SELECTORS = getHealingValue(HEALING_RULES, ["selectors", "frame", "refresh_finished_wrap"], [".finished-wrap"]);
|
|
185
|
+
const REFRESH_BUTTON_SELECTORS = getHealingValue(
|
|
186
|
+
HEALING_RULES,
|
|
187
|
+
["selectors", "frame", "refresh_button"],
|
|
188
|
+
[".finished-wrap .btn.btn-refresh", ".finished-wrap .btn-refresh", ".no-data-refresh .btn-refresh"]
|
|
189
|
+
);
|
|
190
|
+
const RESUME_INFO_URL_PATTERNS = getHealingValue(
|
|
191
|
+
HEALING_RULES,
|
|
192
|
+
["network", "resume", "info_url_patterns"],
|
|
193
|
+
[
|
|
194
|
+
"\\/wapi\\/zpjob\\/view\\/geek\\/info\\b",
|
|
195
|
+
"\\/wapi\\/zpitem\\/web\\/boss\\/[^?#]*\\/geek\\/info\\b",
|
|
196
|
+
"\\/boss\\/[^?#]*\\/geek\\/info\\b",
|
|
197
|
+
"\\/geek\\/info\\b",
|
|
198
|
+
"[?&](?:geekid|geek_id|encryptgeekid|encryptjid|jid|securityid)="
|
|
199
|
+
]
|
|
200
|
+
);
|
|
201
|
+
const RESUME_RELATED_KEYWORDS = getHealingValue(
|
|
202
|
+
HEALING_RULES,
|
|
203
|
+
["network", "resume", "related_keywords"],
|
|
204
|
+
["geek", "resume", "candidate", "friend"]
|
|
205
|
+
);
|
|
206
|
+
const FAVORITE_ADD_PATTERNS = getHealingValue(
|
|
207
|
+
HEALING_RULES,
|
|
208
|
+
["network", "favorite", "add_patterns"],
|
|
209
|
+
[
|
|
210
|
+
"\\/add(?:\\/|$)|[?&](?:action|op|operation|type)=add\\b|[?&](?:status|p3|favorite|collect|interested)=1\\b",
|
|
211
|
+
"(?:^|[_\\W])(add|favorite|collect|interest(?:ed)?)(?:$|[_\\W])"
|
|
212
|
+
]
|
|
213
|
+
);
|
|
214
|
+
const FAVORITE_REMOVE_PATTERNS = getHealingValue(
|
|
215
|
+
HEALING_RULES,
|
|
216
|
+
["network", "favorite", "remove_patterns"],
|
|
217
|
+
[
|
|
218
|
+
"\\/del(?:\\/|$)|[?&](?:action|op|operation|type)=del\\b|[?&](?:status|p3|favorite|collect|interested)=0\\b",
|
|
219
|
+
"(?:^|[_\\W])(del|delete|remove|cancel|unfavorite|uncollect|uninterest)(?:$|[_\\W])"
|
|
220
|
+
]
|
|
221
|
+
);
|
|
222
|
+
const FAVORITE_ACTIONLOG_NAME = getHealingValue(
|
|
223
|
+
HEALING_RULES,
|
|
224
|
+
["network", "favorite", "actionlog_action_name"],
|
|
225
|
+
"star-interest-click"
|
|
226
|
+
);
|
|
227
|
+
|
|
27
228
|
function classifyFinishedWrapState(finishedWrapText, refreshButtonVisible = false) {
|
|
28
229
|
const normalizedText = normalizeText(finishedWrapText);
|
|
29
230
|
const matchedBottomKeyword = BOTTOM_HINT_KEYWORDS.find((keyword) => normalizedText.includes(keyword)) || null;
|
|
@@ -135,6 +336,60 @@ function isVisionImageSizeLimitMessage(message) {
|
|
|
135
336
|
);
|
|
136
337
|
}
|
|
137
338
|
|
|
339
|
+
function isTextContextLimitMessage(message) {
|
|
340
|
+
const text = normalizeText(message).toLowerCase();
|
|
341
|
+
if (!text) return false;
|
|
342
|
+
return (
|
|
343
|
+
/context length|maximum context|too many tokens|max(?:imum)? token|prompt is too long|input is too long|token limit|上下文|超出.*token|超过.*token|输入过长/i.test(text)
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function toStringArray(value, maxItems = 8) {
|
|
348
|
+
if (!Array.isArray(value)) return [];
|
|
349
|
+
const normalized = [];
|
|
350
|
+
for (const item of value) {
|
|
351
|
+
const text = normalizeText(item);
|
|
352
|
+
if (!text) continue;
|
|
353
|
+
normalized.push(text);
|
|
354
|
+
if (normalized.length >= maxItems) break;
|
|
355
|
+
}
|
|
356
|
+
return normalized;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function splitTextByChunks(text, chunkSize, overlap, maxChunks) {
|
|
360
|
+
const source = String(text || "");
|
|
361
|
+
if (!source) return [];
|
|
362
|
+
|
|
363
|
+
const safeChunkSize = Math.max(1000, parsePositiveInteger(chunkSize) || DEFAULT_TEXT_MODEL_CHUNK_SIZE_CHARS);
|
|
364
|
+
const safeOverlap = Math.max(0, Math.min(safeChunkSize - 1, parsePositiveInteger(overlap) || DEFAULT_TEXT_MODEL_CHUNK_OVERLAP_CHARS));
|
|
365
|
+
const safeMaxChunks = Math.max(1, parsePositiveInteger(maxChunks) || DEFAULT_TEXT_MODEL_MAX_CHUNKS);
|
|
366
|
+
|
|
367
|
+
const chunks = [];
|
|
368
|
+
let start = 0;
|
|
369
|
+
while (start < source.length && chunks.length < safeMaxChunks) {
|
|
370
|
+
const end = Math.min(source.length, start + safeChunkSize);
|
|
371
|
+
chunks.push({
|
|
372
|
+
text: source.slice(start, end),
|
|
373
|
+
start,
|
|
374
|
+
end
|
|
375
|
+
});
|
|
376
|
+
if (end >= source.length) break;
|
|
377
|
+
start = Math.max(0, end - safeOverlap);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (chunks.length > 0) {
|
|
381
|
+
const last = chunks[chunks.length - 1];
|
|
382
|
+
if (last.end < source.length) {
|
|
383
|
+
chunks[chunks.length - 1] = {
|
|
384
|
+
text: source.slice(last.start),
|
|
385
|
+
start: last.start,
|
|
386
|
+
end: source.length
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return chunks;
|
|
391
|
+
}
|
|
392
|
+
|
|
138
393
|
function normalizePostAction(value) {
|
|
139
394
|
const normalized = normalizeText(value).toLowerCase();
|
|
140
395
|
if (!normalized) return null;
|
|
@@ -292,10 +547,10 @@ function parseFavoriteActionFromRequest(url, postData = "") {
|
|
|
292
547
|
const normalizedUrl = normalizeText(url).toLowerCase();
|
|
293
548
|
if (!normalizedUrl) return null;
|
|
294
549
|
|
|
295
|
-
if (
|
|
550
|
+
if (firstMatchingPattern(normalizedUrl, FAVORITE_ADD_PATTERNS)) {
|
|
296
551
|
return "add";
|
|
297
552
|
}
|
|
298
|
-
if (
|
|
553
|
+
if (firstMatchingPattern(normalizedUrl, FAVORITE_REMOVE_PATTERNS)) {
|
|
299
554
|
return "del";
|
|
300
555
|
}
|
|
301
556
|
|
|
@@ -307,14 +562,14 @@ function parseFavoriteActionFromActionLog(postData = "") {
|
|
|
307
562
|
if (!raw) return null;
|
|
308
563
|
try {
|
|
309
564
|
const payload = JSON.parse(raw);
|
|
310
|
-
if (normalizeText(payload?.action).toLowerCase() !==
|
|
565
|
+
if (normalizeText(payload?.action).toLowerCase() !== normalizeText(FAVORITE_ACTIONLOG_NAME).toLowerCase()) return null;
|
|
311
566
|
return parseFavoriteActionValue(payload?.p3);
|
|
312
567
|
} catch {}
|
|
313
568
|
|
|
314
569
|
try {
|
|
315
570
|
const params = new URLSearchParams(raw);
|
|
316
571
|
const actionName = normalizeText(params.get("action")).toLowerCase();
|
|
317
|
-
if (actionName !==
|
|
572
|
+
if (actionName !== normalizeText(FAVORITE_ACTIONLOG_NAME).toLowerCase()) return null;
|
|
318
573
|
return parseFavoriteActionValue(params.get("p3"));
|
|
319
574
|
} catch {}
|
|
320
575
|
return null;
|
|
@@ -345,7 +600,7 @@ function parseFavoriteActionFromWsPayload(payload, depth = 0) {
|
|
|
345
600
|
if (depth > 3 || payload === null || payload === undefined) return null;
|
|
346
601
|
|
|
347
602
|
if (typeof payload === "object") {
|
|
348
|
-
if (normalizeText(payload?.action).toLowerCase() ===
|
|
603
|
+
if (normalizeText(payload?.action).toLowerCase() === normalizeText(FAVORITE_ACTIONLOG_NAME).toLowerCase()) {
|
|
349
604
|
const strictAction = parseFavoriteActionValue(payload?.p3);
|
|
350
605
|
if (strictAction) return strictAction;
|
|
351
606
|
}
|
|
@@ -597,7 +852,7 @@ async function promptMissingInputs(args) {
|
|
|
597
852
|
if (args.targetCount === null) {
|
|
598
853
|
const targetCount = await askWithValidation(
|
|
599
854
|
ask,
|
|
600
|
-
"
|
|
855
|
+
"请输入目标通过人数(--targetCount,可留空表示不设上限): ",
|
|
601
856
|
(value) => parsePositiveInteger(value),
|
|
602
857
|
{ allowEmpty: true }
|
|
603
858
|
);
|
|
@@ -830,23 +1085,13 @@ function extractResumePayloadFromResponseBody(parsedBody) {
|
|
|
830
1085
|
function isResumeInfoRequestUrl(url) {
|
|
831
1086
|
const normalizedUrl = normalizeText(url).toLowerCase();
|
|
832
1087
|
if (!normalizedUrl || !normalizedUrl.includes("/wapi/")) return false;
|
|
833
|
-
|
|
834
|
-
if (/\/wapi\/zpjob\/view\/geek\/info\b/.test(normalizedUrl)) return true;
|
|
835
|
-
if (/\/wapi\/zpitem\/web\/boss\/[^?#]*\/geek\/info\b/.test(normalizedUrl)) return true;
|
|
836
|
-
if (/\/boss\/[^?#]*\/geek\/info\b/.test(normalizedUrl)) return true;
|
|
837
|
-
if (/\/geek\/info\b/.test(normalizedUrl)) return true;
|
|
838
|
-
return /[?&](?:geekid|geek_id|encryptgeekid|encryptjid|jid|securityid)=/.test(normalizedUrl);
|
|
1088
|
+
return Boolean(firstMatchingPattern(normalizedUrl, RESUME_INFO_URL_PATTERNS));
|
|
839
1089
|
}
|
|
840
1090
|
|
|
841
1091
|
function isResumeRelatedWapiUrl(url) {
|
|
842
1092
|
const normalizedUrl = normalizeText(url).toLowerCase();
|
|
843
1093
|
if (!normalizedUrl || !normalizedUrl.includes("/wapi/")) return false;
|
|
844
|
-
return (
|
|
845
|
-
normalizedUrl.includes("geek")
|
|
846
|
-
|| normalizedUrl.includes("resume")
|
|
847
|
-
|| normalizedUrl.includes("candidate")
|
|
848
|
-
|| normalizedUrl.includes("friend")
|
|
849
|
-
);
|
|
1094
|
+
return RESUME_RELATED_KEYWORDS.some((keyword) => normalizedUrl.includes(String(keyword).toLowerCase()));
|
|
850
1095
|
}
|
|
851
1096
|
|
|
852
1097
|
function formatResumeApiData(data) {
|
|
@@ -994,20 +1239,18 @@ async function promptMaxGreetCount() {
|
|
|
994
1239
|
|
|
995
1240
|
function buildListCandidatesExpr(processedKeys) {
|
|
996
1241
|
return `((processedKeys) => {
|
|
997
|
-
const frame =
|
|
998
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
999
|
-
|| document.querySelector('iframe');
|
|
1242
|
+
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
1000
1243
|
if (!frame || !frame.contentDocument) {
|
|
1001
1244
|
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1002
1245
|
}
|
|
1003
1246
|
const doc = frame.contentDocument;
|
|
1004
1247
|
const frameRect = frame.getBoundingClientRect();
|
|
1005
1248
|
const processed = new Set(processedKeys || []);
|
|
1006
|
-
const cards =
|
|
1007
|
-
const featuredCards =
|
|
1008
|
-
const latestCards =
|
|
1249
|
+
const cards = ${buildSelectorCollectionExpression(RECOMMEND_CARD_SELECTORS, "doc")};
|
|
1250
|
+
const featuredCards = ${buildSelectorCollectionExpression(FEATURED_CARD_SELECTORS, "doc")};
|
|
1251
|
+
const latestCards = ${buildSelectorCollectionExpression(LATEST_CARD_SELECTORS, "doc")};
|
|
1009
1252
|
const textOf = (el) => String(el ? el.textContent : '').replace(/\s+/g, ' ').trim();
|
|
1010
|
-
const tabs =
|
|
1253
|
+
const tabs = ${buildSelectorCollectionExpression(RECOMMEND_TAB_SELECTORS, "doc")};
|
|
1011
1254
|
const activeTab = tabs.find((node) => /(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(String(node.className || ''))) || null;
|
|
1012
1255
|
const activeStatus = activeTab ? String(activeTab.getAttribute('data-status') || '') : '';
|
|
1013
1256
|
const recommendCandidates = cards.map((card, index) => {
|
|
@@ -1142,22 +1385,20 @@ function buildListCandidatesExpr(processedKeys) {
|
|
|
1142
1385
|
}
|
|
1143
1386
|
|
|
1144
1387
|
const jsGetListState = `(() => {
|
|
1145
|
-
const frame =
|
|
1146
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1147
|
-
|| document.querySelector('iframe');
|
|
1388
|
+
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
1148
1389
|
if (!frame || !frame.contentDocument) {
|
|
1149
1390
|
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1150
1391
|
}
|
|
1151
1392
|
const doc = frame.contentDocument;
|
|
1152
1393
|
const body = doc.body;
|
|
1153
1394
|
const frameRect = frame.getBoundingClientRect();
|
|
1154
|
-
const cards =
|
|
1395
|
+
const cards = ${buildSelectorCollectionExpression(RECOMMEND_CARD_SELECTORS, "doc")};
|
|
1155
1396
|
const candidateCards = cards.filter((card) => card.querySelector('.card-inner[data-geekid]'));
|
|
1156
|
-
const featuredCards =
|
|
1397
|
+
const featuredCards = ${buildSelectorCollectionExpression(FEATURED_CARD_SELECTORS, "doc")};
|
|
1157
1398
|
const featuredCandidates = featuredCards.filter((card) => card.querySelector('a[data-geekid]'));
|
|
1158
|
-
const latestCards =
|
|
1399
|
+
const latestCards = ${buildSelectorCollectionExpression(LATEST_CARD_SELECTORS, "doc")};
|
|
1159
1400
|
const latestCandidates = latestCards.filter((card) => card.querySelector('.card-inner[data-geek], [data-geek]'));
|
|
1160
|
-
const tabs =
|
|
1401
|
+
const tabs = ${buildSelectorCollectionExpression(RECOMMEND_TAB_SELECTORS, "doc")};
|
|
1161
1402
|
const activeTab = tabs.find((node) => /(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(String(node.className || ''))) || null;
|
|
1162
1403
|
const activeTabStatus = activeTab ? String(activeTab.getAttribute('data-status') || '') : '';
|
|
1163
1404
|
const inferredStatus = activeTabStatus
|
|
@@ -1201,18 +1442,16 @@ const jsGetListState = `(() => {
|
|
|
1201
1442
|
})()`;
|
|
1202
1443
|
|
|
1203
1444
|
const jsScrollList = `(() => {
|
|
1204
|
-
const frame =
|
|
1205
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1206
|
-
|| document.querySelector('iframe');
|
|
1445
|
+
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
1207
1446
|
if (!frame || !frame.contentDocument) {
|
|
1208
1447
|
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1209
1448
|
}
|
|
1210
1449
|
const doc = frame.contentDocument;
|
|
1211
1450
|
const body = doc.body;
|
|
1212
|
-
const recommendCards =
|
|
1213
|
-
const featuredCards =
|
|
1214
|
-
const latestCards =
|
|
1215
|
-
const tabs =
|
|
1451
|
+
const recommendCards = ${buildSelectorCollectionExpression(RECOMMEND_CARD_SELECTORS, "doc")}.filter((card) => card.querySelector('.card-inner[data-geekid]'));
|
|
1452
|
+
const featuredCards = ${buildSelectorCollectionExpression(FEATURED_CARD_SELECTORS, "doc")}.filter((card) => card.querySelector('a[data-geekid]'));
|
|
1453
|
+
const latestCards = ${buildSelectorCollectionExpression(LATEST_CARD_SELECTORS, "doc")}.filter((card) => card.querySelector('.card-inner[data-geek], [data-geek]'));
|
|
1454
|
+
const tabs = ${buildSelectorCollectionExpression(RECOMMEND_TAB_SELECTORS, "doc")};
|
|
1216
1455
|
const activeTab = tabs.find((node) => /(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(String(node.className || ''))) || null;
|
|
1217
1456
|
const activeStatus = activeTab ? String(activeTab.getAttribute('data-status') || '') : '';
|
|
1218
1457
|
const inferredStatus = activeStatus
|
|
@@ -1249,9 +1488,7 @@ const jsScrollList = `(() => {
|
|
|
1249
1488
|
})()`;
|
|
1250
1489
|
|
|
1251
1490
|
const jsDetectBottom = `(() => {
|
|
1252
|
-
const frame =
|
|
1253
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1254
|
-
|| document.querySelector('iframe');
|
|
1491
|
+
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
1255
1492
|
if (!frame || !frame.contentDocument) {
|
|
1256
1493
|
return { isBottom: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1257
1494
|
}
|
|
@@ -1267,8 +1504,9 @@ const jsDetectBottom = `(() => {
|
|
|
1267
1504
|
const rect = el.getBoundingClientRect();
|
|
1268
1505
|
return rect.width > 2 && rect.height > 2 && el.offsetParent !== null;
|
|
1269
1506
|
};
|
|
1270
|
-
const finishedWrap =
|
|
1271
|
-
|
|
1507
|
+
const finishedWrap = ${buildSelectorCollectionExpression(REFRESH_FINISHED_WRAP_SELECTORS, "doc")}
|
|
1508
|
+
.find((el) => isVisible(el)) || null;
|
|
1509
|
+
const refreshButton = ${buildSelectorCollectionExpression(REFRESH_BUTTON_SELECTORS, "doc")}
|
|
1272
1510
|
.find((el) => isVisible(el)) || null;
|
|
1273
1511
|
const keywords = ${JSON.stringify(BOTTOM_HINT_KEYWORDS)};
|
|
1274
1512
|
const loadMoreKeywords = ${JSON.stringify(LOAD_MORE_HINT_KEYWORDS)};
|
|
@@ -1316,13 +1554,7 @@ const jsWaitForDetail = `(() => {
|
|
|
1316
1554
|
const rect = el.getBoundingClientRect();
|
|
1317
1555
|
return rect.width > 2 && rect.height > 2;
|
|
1318
1556
|
};
|
|
1319
|
-
const topSignals = [
|
|
1320
|
-
'.dialog-wrap.active',
|
|
1321
|
-
'.boss-popup__wrapper',
|
|
1322
|
-
'.boss-popup_wrapper',
|
|
1323
|
-
'iframe[src*="/web/frame/c-resume/"]',
|
|
1324
|
-
'iframe[name*="resume"]'
|
|
1325
|
-
];
|
|
1557
|
+
const topSignals = ${JSON.stringify([...DETAIL_POPUP_SELECTORS, ...DETAIL_RESUME_IFRAME_SELECTORS])};
|
|
1326
1558
|
for (const sel of topSignals) {
|
|
1327
1559
|
const nodes = Array.from(document.querySelectorAll(sel));
|
|
1328
1560
|
for (const node of nodes) {
|
|
@@ -1331,9 +1563,7 @@ const jsWaitForDetail = `(() => {
|
|
|
1331
1563
|
}
|
|
1332
1564
|
}
|
|
1333
1565
|
}
|
|
1334
|
-
const frame =
|
|
1335
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1336
|
-
|| document.querySelector('iframe');
|
|
1566
|
+
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
1337
1567
|
if (!frame || !frame.contentDocument) {
|
|
1338
1568
|
return { open: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1339
1569
|
}
|
|
@@ -1356,10 +1586,10 @@ const jsWaitForDetail = `(() => {
|
|
|
1356
1586
|
if (viewportWidth <= 0 || viewportHeight <= 0) return el.offsetParent !== null;
|
|
1357
1587
|
return rect.right > 0 && rect.bottom > 0 && rect.left < viewportWidth && rect.top < viewportHeight;
|
|
1358
1588
|
};
|
|
1359
|
-
const close = doc
|
|
1360
|
-
const favorite = doc
|
|
1361
|
-
const greet = doc
|
|
1362
|
-
const resumeFrame =
|
|
1589
|
+
const close = ${buildFirstSelectorLookupExpression(DETAIL_CLOSE_SELECTORS, "doc")};
|
|
1590
|
+
const favorite = ${buildFirstSelectorLookupExpression(FAVORITE_BUTTON_SELECTORS, "doc")};
|
|
1591
|
+
const greet = ${buildFirstSelectorLookupExpression(GREET_BUTTON_RECOMMEND_SELECTORS, "doc")};
|
|
1592
|
+
const resumeFrame = ${buildFirstSelectorLookupExpression(DETAIL_RESUME_IFRAME_SELECTORS, "doc")};
|
|
1363
1593
|
const open = Boolean(
|
|
1364
1594
|
isVisibleInViewport(close)
|
|
1365
1595
|
|| isVisibleInViewport(favorite)
|
|
@@ -1392,16 +1622,7 @@ const jsCloseDetail = `(() => {
|
|
|
1392
1622
|
const rect = el.getBoundingClientRect();
|
|
1393
1623
|
return rect.width > 2 && rect.height > 2;
|
|
1394
1624
|
};
|
|
1395
|
-
const topCloseSelectors =
|
|
1396
|
-
'.boss-popup__close',
|
|
1397
|
-
'.popup-close',
|
|
1398
|
-
'.modal-close',
|
|
1399
|
-
'.dialog-close',
|
|
1400
|
-
'.close-btn',
|
|
1401
|
-
'button[aria-label*="关闭"]',
|
|
1402
|
-
'button[title*="关闭"]',
|
|
1403
|
-
'.icon-close'
|
|
1404
|
-
];
|
|
1625
|
+
const topCloseSelectors = ${JSON.stringify(DETAIL_CLOSE_SELECTORS)};
|
|
1405
1626
|
for (const sel of topCloseSelectors) {
|
|
1406
1627
|
const nodes = Array.from(document.querySelectorAll(sel));
|
|
1407
1628
|
for (const node of nodes) {
|
|
@@ -1413,9 +1634,7 @@ const jsCloseDetail = `(() => {
|
|
|
1413
1634
|
}
|
|
1414
1635
|
}
|
|
1415
1636
|
|
|
1416
|
-
const frame =
|
|
1417
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1418
|
-
|| document.querySelector('iframe');
|
|
1637
|
+
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
1419
1638
|
if (!frame || !frame.contentDocument) {
|
|
1420
1639
|
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1421
1640
|
}
|
|
@@ -1445,16 +1664,7 @@ const jsCloseDetail = `(() => {
|
|
|
1445
1664
|
return rect.right > 0 && rect.bottom > 0 && rect.left < viewportWidth && rect.top < viewportHeight;
|
|
1446
1665
|
};
|
|
1447
1666
|
|
|
1448
|
-
const directCloseSelectors =
|
|
1449
|
-
'.boss-popup__close',
|
|
1450
|
-
'.popup-close',
|
|
1451
|
-
'.modal-close',
|
|
1452
|
-
'.dialog-close',
|
|
1453
|
-
'.close-btn',
|
|
1454
|
-
'button[aria-label*="关闭"]',
|
|
1455
|
-
'button[title*="关闭"]',
|
|
1456
|
-
'.icon-close'
|
|
1457
|
-
];
|
|
1667
|
+
const directCloseSelectors = ${JSON.stringify(DETAIL_CLOSE_SELECTORS)};
|
|
1458
1668
|
for (const sel of directCloseSelectors) {
|
|
1459
1669
|
const nodes = Array.from(doc.querySelectorAll(sel));
|
|
1460
1670
|
for (const node of nodes) {
|
|
@@ -1532,15 +1742,7 @@ const jsIsDetailClosed = `(() => {
|
|
|
1532
1742
|
return { closed: false, reason: 'top know button visible' };
|
|
1533
1743
|
}
|
|
1534
1744
|
|
|
1535
|
-
const topPopupSelectors =
|
|
1536
|
-
'.boss-popup__wrapper',
|
|
1537
|
-
'.boss-popup_wrapper',
|
|
1538
|
-
'.boss-dialog_wrapper',
|
|
1539
|
-
'.dialog-wrap.active',
|
|
1540
|
-
'.boss-dialog',
|
|
1541
|
-
'[class*="popup"][class*="wrapper"]',
|
|
1542
|
-
'[class*="dialog"][class*="wrapper"]'
|
|
1543
|
-
];
|
|
1745
|
+
const topPopupSelectors = ${JSON.stringify(DETAIL_POPUP_SELECTORS)};
|
|
1544
1746
|
for (const sel of topPopupSelectors) {
|
|
1545
1747
|
const nodes = Array.from(document.querySelectorAll(sel));
|
|
1546
1748
|
for (const node of nodes) {
|
|
@@ -1552,9 +1754,7 @@ const jsIsDetailClosed = `(() => {
|
|
|
1552
1754
|
}
|
|
1553
1755
|
}
|
|
1554
1756
|
|
|
1555
|
-
const frame =
|
|
1556
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1557
|
-
|| document.querySelector('iframe');
|
|
1757
|
+
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
1558
1758
|
if (!frame || !frame.contentDocument) {
|
|
1559
1759
|
return { closed: true, reason: 'NO_RECOMMEND_IFRAME' };
|
|
1560
1760
|
}
|
|
@@ -1579,17 +1779,7 @@ const jsIsDetailClosed = `(() => {
|
|
|
1579
1779
|
return rect.right > 0 && rect.bottom > 0 && rect.left < viewportWidth && rect.top < viewportHeight;
|
|
1580
1780
|
};
|
|
1581
1781
|
|
|
1582
|
-
const popupSelectors =
|
|
1583
|
-
'.boss-popup__wrapper',
|
|
1584
|
-
'.boss-popup_wrapper',
|
|
1585
|
-
'.boss-dialog_wrapper',
|
|
1586
|
-
'.dialog-wrap.active',
|
|
1587
|
-
'.boss-dialog',
|
|
1588
|
-
'[class*="popup"][class*="wrapper"]',
|
|
1589
|
-
'[class*="dialog"][class*="wrapper"]',
|
|
1590
|
-
'.geek-detail-modal',
|
|
1591
|
-
'.resume-item-detail'
|
|
1592
|
-
];
|
|
1782
|
+
const popupSelectors = ${JSON.stringify(DETAIL_POPUP_SELECTORS)};
|
|
1593
1783
|
for (const sel of popupSelectors) {
|
|
1594
1784
|
const nodes = Array.from(doc.querySelectorAll(sel));
|
|
1595
1785
|
for (const node of nodes) {
|
|
@@ -1599,12 +1789,7 @@ const jsIsDetailClosed = `(() => {
|
|
|
1599
1789
|
}
|
|
1600
1790
|
}
|
|
1601
1791
|
|
|
1602
|
-
const detailSignals = [
|
|
1603
|
-
'.like-icon-and-text',
|
|
1604
|
-
'button.btn-v2.btn-sure-v2.btn-greet',
|
|
1605
|
-
'iframe[src*="/web/frame/c-resume/"]',
|
|
1606
|
-
'iframe[name*="resume"]'
|
|
1607
|
-
];
|
|
1792
|
+
const detailSignals = ${JSON.stringify([...FAVORITE_BUTTON_SELECTORS, ...GREET_BUTTON_RECOMMEND_SELECTORS, ...DETAIL_RESUME_IFRAME_SELECTORS])};
|
|
1608
1793
|
for (const sel of detailSignals) {
|
|
1609
1794
|
const node = doc.querySelector(sel);
|
|
1610
1795
|
if (isVisible(node)) {
|
|
@@ -1629,7 +1814,8 @@ const jsGetFavoriteState = `(() => {
|
|
|
1629
1814
|
};
|
|
1630
1815
|
const resolveFavorite = (doc, offsetX, offsetY, scope) => {
|
|
1631
1816
|
if (!doc) return null;
|
|
1632
|
-
const direct =
|
|
1817
|
+
const direct = ${buildSelectorCollectionExpression(FAVORITE_BUTTON_SELECTORS, "doc")}
|
|
1818
|
+
.find((node) => isVisible(doc, node)) || null;
|
|
1633
1819
|
const footer = doc.querySelector('.resume-footer.item-operate, .resume-footer-wrap, .resume-footer');
|
|
1634
1820
|
const fromFooter = footer
|
|
1635
1821
|
? Array.from(footer.querySelectorAll('[class*="collect"], [class*="favorite"], button, .btn, span'))
|
|
@@ -1661,9 +1847,7 @@ const jsGetFavoriteState = `(() => {
|
|
|
1661
1847
|
const topResult = resolveFavorite(document, 0, 0, 'top');
|
|
1662
1848
|
if (topResult) return topResult;
|
|
1663
1849
|
|
|
1664
|
-
const frame =
|
|
1665
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1666
|
-
|| document.querySelector('iframe');
|
|
1850
|
+
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
1667
1851
|
if (!frame || !frame.contentDocument) {
|
|
1668
1852
|
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1669
1853
|
}
|
|
@@ -1674,12 +1858,10 @@ const jsGetFavoriteState = `(() => {
|
|
|
1674
1858
|
})()`;
|
|
1675
1859
|
|
|
1676
1860
|
const jsClickFavoriteFallback = `(() => {
|
|
1677
|
-
const frame =
|
|
1678
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1679
|
-
|| document.querySelector('iframe');
|
|
1861
|
+
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
1680
1862
|
if (!frame || !frame.contentDocument) return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1681
1863
|
const doc = frame.contentDocument;
|
|
1682
|
-
const root = doc
|
|
1864
|
+
const root = ${buildFirstSelectorLookupExpression(FAVORITE_BUTTON_SELECTORS, "doc")};
|
|
1683
1865
|
if (!root || root.offsetParent === null) return { ok: false, error: 'FAVORITE_BUTTON_NOT_FOUND' };
|
|
1684
1866
|
root.click();
|
|
1685
1867
|
return { ok: true };
|
|
@@ -1697,14 +1879,10 @@ const jsGetGreetStateRecommend = `(() => {
|
|
|
1697
1879
|
const rect = el.getBoundingClientRect();
|
|
1698
1880
|
return rect.width > 2 && rect.height > 2;
|
|
1699
1881
|
};
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
...Array.from(doc.querySelectorAll('.resume-footer.item-operate button.btn-v2, .resume-footer-wrap button.btn-v2')),
|
|
1705
|
-
...Array.from(doc.querySelectorAll('.resume-footer.item-operate button, .resume-footer-wrap button'))
|
|
1706
|
-
];
|
|
1707
|
-
const button = candidates.find((item) => isVisible(doc, item) && /沟通|打招呼|聊一聊/.test(normalize(item.textContent))) || null;
|
|
1882
|
+
const resolveGreet = (doc, offsetX, offsetY, scope) => {
|
|
1883
|
+
if (!doc) return null;
|
|
1884
|
+
const candidates = ${buildSelectorCollectionExpression(GREET_BUTTON_RECOMMEND_SELECTORS, "doc")};
|
|
1885
|
+
const button = candidates.find((item) => isVisible(doc, item) && /沟通|打招呼|聊一聊/.test(normalize(item.textContent))) || null;
|
|
1708
1886
|
if (!button) return null;
|
|
1709
1887
|
const rect = button.getBoundingClientRect();
|
|
1710
1888
|
return {
|
|
@@ -1718,9 +1896,7 @@ const jsGetGreetStateRecommend = `(() => {
|
|
|
1718
1896
|
const topResult = resolveGreet(document, 0, 0, 'top');
|
|
1719
1897
|
if (topResult) return topResult;
|
|
1720
1898
|
|
|
1721
|
-
const frame =
|
|
1722
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1723
|
-
|| document.querySelector('iframe');
|
|
1899
|
+
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
1724
1900
|
if (!frame || !frame.contentDocument) {
|
|
1725
1901
|
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1726
1902
|
}
|
|
@@ -1737,12 +1913,10 @@ const jsClickGreetFallbackRecommend = `(() => {
|
|
|
1737
1913
|
topButton.click();
|
|
1738
1914
|
return { ok: true, scope: 'top' };
|
|
1739
1915
|
}
|
|
1740
|
-
const frame =
|
|
1741
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1742
|
-
|| document.querySelector('iframe');
|
|
1916
|
+
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
1743
1917
|
if (!frame || !frame.contentDocument) return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1744
1918
|
const doc = frame.contentDocument;
|
|
1745
|
-
const button = doc
|
|
1919
|
+
const button = ${buildFirstSelectorLookupExpression(GREET_BUTTON_RECOMMEND_SELECTORS, "doc")};
|
|
1746
1920
|
if (!button || button.offsetParent === null) return { ok: false, error: 'GREET_BUTTON_NOT_FOUND' };
|
|
1747
1921
|
button.click();
|
|
1748
1922
|
return { ok: true };
|
|
@@ -1760,15 +1934,10 @@ const jsGetGreetStateFeatured = `(() => {
|
|
|
1760
1934
|
const rect = el.getBoundingClientRect();
|
|
1761
1935
|
return rect.width > 2 && rect.height > 2;
|
|
1762
1936
|
};
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
...Array.from(doc.querySelectorAll('button.btn-v2.btn-sure-v2.position-rights')),
|
|
1768
|
-
...Array.from(doc.querySelectorAll('.resume-footer.item-operate button.btn-v2, .resume-footer-wrap button.btn-v2')),
|
|
1769
|
-
...Array.from(doc.querySelectorAll('.resume-footer.item-operate button, .resume-footer-wrap button'))
|
|
1770
|
-
];
|
|
1771
|
-
const button = candidates.find((item) => isVisible(doc, item) && /立即沟通|沟通|打招呼|聊一聊/.test(normalize(item.textContent))) || null;
|
|
1937
|
+
const resolveGreet = (doc, offsetX, offsetY, scope) => {
|
|
1938
|
+
if (!doc) return null;
|
|
1939
|
+
const candidates = ${buildSelectorCollectionExpression(GREET_BUTTON_FEATURED_SELECTORS, "doc")};
|
|
1940
|
+
const button = candidates.find((item) => isVisible(doc, item) && /立即沟通|沟通|打招呼|聊一聊/.test(normalize(item.textContent))) || null;
|
|
1772
1941
|
if (!button) return null;
|
|
1773
1942
|
const rect = button.getBoundingClientRect();
|
|
1774
1943
|
return {
|
|
@@ -1782,9 +1951,7 @@ const jsGetGreetStateFeatured = `(() => {
|
|
|
1782
1951
|
const topResult = resolveGreet(document, 0, 0, 'top');
|
|
1783
1952
|
if (topResult) return topResult;
|
|
1784
1953
|
|
|
1785
|
-
const frame =
|
|
1786
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1787
|
-
|| document.querySelector('iframe');
|
|
1954
|
+
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
1788
1955
|
if (!frame || !frame.contentDocument) {
|
|
1789
1956
|
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1790
1957
|
}
|
|
@@ -1801,12 +1968,10 @@ const jsClickGreetFallbackFeatured = `(() => {
|
|
|
1801
1968
|
topButton.click();
|
|
1802
1969
|
return { ok: true, scope: 'top' };
|
|
1803
1970
|
}
|
|
1804
|
-
const frame =
|
|
1805
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1806
|
-
|| document.querySelector('iframe');
|
|
1971
|
+
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
1807
1972
|
if (!frame || !frame.contentDocument) return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1808
1973
|
const doc = frame.contentDocument;
|
|
1809
|
-
const button =
|
|
1974
|
+
const button = ${buildFirstSelectorLookupExpression(GREET_BUTTON_FEATURED_SELECTORS, "doc")};
|
|
1810
1975
|
if (!button || button.offsetParent === null) return { ok: false, error: 'GREET_BUTTON_NOT_FOUND' };
|
|
1811
1976
|
button.click();
|
|
1812
1977
|
return { ok: true };
|
|
@@ -3016,9 +3181,7 @@ class RecommendScreenCli {
|
|
|
3016
3181
|
return { ok: false, error: "CANDIDATE_KEY_MISSING" };
|
|
3017
3182
|
}
|
|
3018
3183
|
return this.evaluate(`((candidateKey) => {
|
|
3019
|
-
const frame =
|
|
3020
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
3021
|
-
|| document.querySelector('iframe');
|
|
3184
|
+
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
3022
3185
|
if (!frame || !frame.contentDocument) {
|
|
3023
3186
|
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
3024
3187
|
}
|
|
@@ -3027,11 +3190,11 @@ class RecommendScreenCli {
|
|
|
3027
3190
|
.find((item) => (item.getAttribute('data-geekid') || '') === String(candidateKey)) || null;
|
|
3028
3191
|
const latestInner = recommendInner
|
|
3029
3192
|
? null
|
|
3030
|
-
:
|
|
3193
|
+
: ${buildSelectorCollectionExpression([".candidate-card-wrap .card-inner[data-geek]", ".candidate-card-wrap [data-geek]"], "doc")}
|
|
3031
3194
|
.find((item) => (item.getAttribute('data-geek') || '') === String(candidateKey)) || null;
|
|
3032
3195
|
const featuredAnchor = (recommendInner || latestInner)
|
|
3033
3196
|
? null
|
|
3034
|
-
:
|
|
3197
|
+
: ${buildSelectorCollectionExpression(["li.geek-info-card a[data-geekid]", "a[data-geekid]"], "doc")}
|
|
3035
3198
|
.find((item) => (item.getAttribute('data-geekid') || '') === String(candidateKey)) || null;
|
|
3036
3199
|
const card = recommendInner
|
|
3037
3200
|
? (recommendInner.closest('li.card-item') || recommendInner.closest('.card-item'))
|
|
@@ -3135,9 +3298,9 @@ class RecommendScreenCli {
|
|
|
3135
3298
|
DEFAULT_VISION_MAX_IMAGE_PIXELS
|
|
3136
3299
|
);
|
|
3137
3300
|
const retryLimit = resolveVisionRetryPixelLimit(primaryLimit);
|
|
3138
|
-
const preparedPrimary = await this.
|
|
3301
|
+
const preparedPrimary = await this.prepareVisionImageSegmentsForModel(imagePath, primaryLimit, "primary");
|
|
3139
3302
|
try {
|
|
3140
|
-
return await this.requestVisionModel(preparedPrimary.
|
|
3303
|
+
return await this.requestVisionModel(preparedPrimary.imagePaths);
|
|
3141
3304
|
} catch (error) {
|
|
3142
3305
|
if (!isVisionImageSizeLimitMessage(error?.message || "")) {
|
|
3143
3306
|
throw error;
|
|
@@ -3145,12 +3308,13 @@ class RecommendScreenCli {
|
|
|
3145
3308
|
log(
|
|
3146
3309
|
`[VISION] 检测到图片尺寸超限,准备降采样重试: ` +
|
|
3147
3310
|
`primary_limit=${primaryLimit} source=${preparedPrimary.source} ` +
|
|
3148
|
-
`source_pixels=${preparedPrimary.sourcePixels ?? "unknown"}`
|
|
3311
|
+
`source_pixels=${preparedPrimary.sourcePixels ?? "unknown"} ` +
|
|
3312
|
+
`segments=${preparedPrimary.imagePaths?.length || 1}`
|
|
3149
3313
|
);
|
|
3150
3314
|
}
|
|
3151
|
-
const preparedRetry = await this.
|
|
3315
|
+
const preparedRetry = await this.prepareVisionImageSegmentsForModel(imagePath, retryLimit, "retry");
|
|
3152
3316
|
try {
|
|
3153
|
-
return await this.requestVisionModel(preparedRetry.
|
|
3317
|
+
return await this.requestVisionModel(preparedRetry.imagePaths);
|
|
3154
3318
|
} catch (retryError) {
|
|
3155
3319
|
if (!isVisionImageSizeLimitMessage(retryError?.message || "")) {
|
|
3156
3320
|
throw retryError;
|
|
@@ -3161,11 +3325,106 @@ class RecommendScreenCli {
|
|
|
3161
3325
|
`primary_limit=${primaryLimit}; retry_limit=${retryLimit}; ` +
|
|
3162
3326
|
`source_pixels=${preparedRetry.sourcePixels ?? "unknown"}; ` +
|
|
3163
3327
|
`retry_pixels=${preparedRetry.currentPixels ?? "unknown"}; ` +
|
|
3328
|
+
`segments=${preparedRetry.imagePaths?.length || 1}; ` +
|
|
3164
3329
|
`last_error=${normalizeText(retryError?.message || retryError)}`
|
|
3165
3330
|
);
|
|
3166
3331
|
}
|
|
3167
3332
|
}
|
|
3168
3333
|
|
|
3334
|
+
async prepareVisionImageSegmentsForModel(imagePath, maxPixels, attemptTag = "primary") {
|
|
3335
|
+
const resolvedMaxPixels = parsePositiveInteger(maxPixels);
|
|
3336
|
+
if (!resolvedMaxPixels) {
|
|
3337
|
+
return {
|
|
3338
|
+
imagePaths: [imagePath],
|
|
3339
|
+
source: "no_limit",
|
|
3340
|
+
sourcePixels: null,
|
|
3341
|
+
currentPixels: null
|
|
3342
|
+
};
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
let sharp;
|
|
3346
|
+
try {
|
|
3347
|
+
sharp = loadVisionSharp();
|
|
3348
|
+
} catch (error) {
|
|
3349
|
+
log(`[VISION] 加载 sharp 失败,回退到单图模式: ${error?.message || error}`);
|
|
3350
|
+
const single = await this.prepareVisionImageForModel(imagePath, resolvedMaxPixels, attemptTag);
|
|
3351
|
+
return {
|
|
3352
|
+
imagePaths: [single.imagePath],
|
|
3353
|
+
source: `single_${single.source}`,
|
|
3354
|
+
sourcePixels: single.sourcePixels ?? null,
|
|
3355
|
+
currentPixels: single.currentPixels ?? null
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
let metadata;
|
|
3360
|
+
try {
|
|
3361
|
+
metadata = await sharp(imagePath).metadata();
|
|
3362
|
+
} catch (error) {
|
|
3363
|
+
log(`[VISION] 读取图片尺寸失败,回退到单图模式: ${error?.message || error}`);
|
|
3364
|
+
const single = await this.prepareVisionImageForModel(imagePath, resolvedMaxPixels, attemptTag);
|
|
3365
|
+
return {
|
|
3366
|
+
imagePaths: [single.imagePath],
|
|
3367
|
+
source: `single_${single.source}`,
|
|
3368
|
+
sourcePixels: single.sourcePixels ?? null,
|
|
3369
|
+
currentPixels: single.currentPixels ?? null
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
const width = Number(metadata?.width || 0);
|
|
3374
|
+
const height = Number(metadata?.height || 0);
|
|
3375
|
+
const sourcePixels = width > 0 && height > 0 ? width * height : null;
|
|
3376
|
+
if (!sourcePixels || sourcePixels <= resolvedMaxPixels) {
|
|
3377
|
+
return {
|
|
3378
|
+
imagePaths: [imagePath],
|
|
3379
|
+
source: "within_limit",
|
|
3380
|
+
sourcePixels,
|
|
3381
|
+
currentPixels: sourcePixels
|
|
3382
|
+
};
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
const maxTileHeight = Math.floor(resolvedMaxPixels / Math.max(1, width));
|
|
3386
|
+
if (!Number.isFinite(maxTileHeight) || maxTileHeight < 64) {
|
|
3387
|
+
const single = await this.prepareVisionImageForModel(imagePath, resolvedMaxPixels, attemptTag);
|
|
3388
|
+
return {
|
|
3389
|
+
imagePaths: [single.imagePath],
|
|
3390
|
+
source: `single_${single.source}`,
|
|
3391
|
+
sourcePixels: single.sourcePixels ?? sourcePixels,
|
|
3392
|
+
currentPixels: single.currentPixels ?? sourcePixels
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
const parsedPath = path.parse(imagePath);
|
|
3397
|
+
const imagePaths = [];
|
|
3398
|
+
for (let top = 0, index = 0; top < height; top += maxTileHeight, index += 1) {
|
|
3399
|
+
const segmentHeight = Math.min(maxTileHeight, height - top);
|
|
3400
|
+
const segmentPath = path.join(
|
|
3401
|
+
parsedPath.dir,
|
|
3402
|
+
`${parsedPath.name}.${attemptTag}.seg${String(index + 1).padStart(3, "0")}.png`
|
|
3403
|
+
);
|
|
3404
|
+
await sharp(imagePath)
|
|
3405
|
+
.extract({
|
|
3406
|
+
left: 0,
|
|
3407
|
+
top,
|
|
3408
|
+
width,
|
|
3409
|
+
height: segmentHeight
|
|
3410
|
+
})
|
|
3411
|
+
.png()
|
|
3412
|
+
.toFile(segmentPath);
|
|
3413
|
+
imagePaths.push(segmentPath);
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
log(
|
|
3417
|
+
`[VISION] 长简历按分段输入模型: ${width}x${height}(${sourcePixels}) ` +
|
|
3418
|
+
`-> segments=${imagePaths.length}, max_pixels_per_segment=${resolvedMaxPixels}, attempt=${attemptTag}`
|
|
3419
|
+
);
|
|
3420
|
+
return {
|
|
3421
|
+
imagePaths,
|
|
3422
|
+
source: "segmented",
|
|
3423
|
+
sourcePixels,
|
|
3424
|
+
currentPixels: resolvedMaxPixels
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3169
3428
|
async prepareVisionImageForModel(imagePath, maxPixels, attemptTag = "primary") {
|
|
3170
3429
|
const resolvedMaxPixels = parsePositiveInteger(maxPixels);
|
|
3171
3430
|
if (!resolvedMaxPixels) {
|
|
@@ -3254,7 +3513,38 @@ class RecommendScreenCli {
|
|
|
3254
3513
|
}
|
|
3255
3514
|
|
|
3256
3515
|
async requestVisionModel(imagePath) {
|
|
3257
|
-
const
|
|
3516
|
+
const imagePaths = Array.isArray(imagePath) ? imagePath.filter(Boolean) : [imagePath].filter(Boolean);
|
|
3517
|
+
if (imagePaths.length <= 0) {
|
|
3518
|
+
throw this.buildError("VISION_MODEL_FAILED", "No vision image input provided.");
|
|
3519
|
+
}
|
|
3520
|
+
const userContent = [
|
|
3521
|
+
{
|
|
3522
|
+
type: "text",
|
|
3523
|
+
text:
|
|
3524
|
+
"请根据以下标准判断候选人是否通过筛选。\n\n" +
|
|
3525
|
+
`筛选标准:\n${this.args.criteria}\n\n` +
|
|
3526
|
+
"你将收到候选人完整简历的一个或多个顺序分段图片。必须完整阅读全部分段后再判断," +
|
|
3527
|
+
"严禁编造任何不存在的经历、项目、学校、公司或时间线;证据不足时必须判定为不通过。\n\n" +
|
|
3528
|
+
"请返回严格 JSON: " +
|
|
3529
|
+
"{\"passed\": true/false, \"reason\": \"...\", \"summary\": \"...\", \"evidence\": [\"证据原文1\", \"证据原文2\"]}"
|
|
3530
|
+
}
|
|
3531
|
+
];
|
|
3532
|
+
for (let index = 0; index < imagePaths.length; index += 1) {
|
|
3533
|
+
const segmentPath = imagePaths[index];
|
|
3534
|
+
const imageBase64 = fs.readFileSync(segmentPath, "base64");
|
|
3535
|
+
if (imagePaths.length > 1) {
|
|
3536
|
+
userContent.push({
|
|
3537
|
+
type: "text",
|
|
3538
|
+
text: `简历分段 ${index + 1}/${imagePaths.length}`
|
|
3539
|
+
});
|
|
3540
|
+
}
|
|
3541
|
+
userContent.push({
|
|
3542
|
+
type: "image_url",
|
|
3543
|
+
image_url: {
|
|
3544
|
+
url: `data:image/png;base64,${imageBase64}`
|
|
3545
|
+
}
|
|
3546
|
+
});
|
|
3547
|
+
}
|
|
3258
3548
|
const rawBaseUrl = this.args.baseUrl;
|
|
3259
3549
|
log(`[callVisionModel] baseUrl 原始值类型=${typeof rawBaseUrl}, 长度=${rawBaseUrl != null ? String(rawBaseUrl).length : "null/undefined"}, JSON编码=${JSON.stringify(rawBaseUrl)}`);
|
|
3260
3550
|
const baseUrl = String(rawBaseUrl || "").replace(/\/$/, "");
|
|
@@ -3264,22 +3554,13 @@ class RecommendScreenCli {
|
|
|
3264
3554
|
messages: [
|
|
3265
3555
|
{
|
|
3266
3556
|
role: "system",
|
|
3267
|
-
content:
|
|
3557
|
+
content:
|
|
3558
|
+
"你是一位严谨的招聘筛选助手。必须完整阅读所有输入材料,严禁臆造不存在的简历经历。" +
|
|
3559
|
+
"只能返回 JSON,不要输出任何额外文字。"
|
|
3268
3560
|
},
|
|
3269
3561
|
{
|
|
3270
3562
|
role: "user",
|
|
3271
|
-
content:
|
|
3272
|
-
{
|
|
3273
|
-
type: "text",
|
|
3274
|
-
text: `请根据以下标准判断候选人是否通过筛选。\n\n筛选标准:\n${this.args.criteria}\n\n你看到的是整份候选人简历长图。请返回严格 JSON: {\"passed\": true/false, \"reason\": \"...\", \"summary\": \"...\"}`
|
|
3275
|
-
},
|
|
3276
|
-
{
|
|
3277
|
-
type: "image_url",
|
|
3278
|
-
image_url: {
|
|
3279
|
-
url: `data:image/png;base64,${imageBase64}`
|
|
3280
|
-
}
|
|
3281
|
-
}
|
|
3282
|
-
]
|
|
3563
|
+
content: userContent
|
|
3283
3564
|
}
|
|
3284
3565
|
]
|
|
3285
3566
|
};
|
|
@@ -3304,15 +3585,79 @@ class RecommendScreenCli {
|
|
|
3304
3585
|
? json.choices[0].message.content.map((item) => item?.text || "").join("\n")
|
|
3305
3586
|
: json?.choices?.[0]?.message?.content || "";
|
|
3306
3587
|
const parsed = extractJsonObject(content);
|
|
3588
|
+
const reason = normalizeText(parsed.reason);
|
|
3589
|
+
const summary = normalizeText(parsed.summary || reason);
|
|
3590
|
+
const evidence = toStringArray(parsed.evidence);
|
|
3307
3591
|
return {
|
|
3308
3592
|
passed: parsed.passed === true,
|
|
3309
|
-
reason:
|
|
3310
|
-
summary:
|
|
3593
|
+
reason: reason || "未满足筛选标准。",
|
|
3594
|
+
summary: summary || reason || "未满足筛选标准。",
|
|
3595
|
+
evidence
|
|
3311
3596
|
};
|
|
3312
3597
|
}
|
|
3313
3598
|
|
|
3314
3599
|
async callTextModel(resumeText) {
|
|
3315
|
-
const
|
|
3600
|
+
const fullResumeText = String(resumeText || "");
|
|
3601
|
+
if (!normalizeText(fullResumeText)) {
|
|
3602
|
+
throw this.buildError("TEXT_MODEL_FAILED", "Resume text is empty.");
|
|
3603
|
+
}
|
|
3604
|
+
try {
|
|
3605
|
+
return await this.requestTextModel(fullResumeText, {
|
|
3606
|
+
chunkIndex: 1,
|
|
3607
|
+
chunkTotal: 1
|
|
3608
|
+
});
|
|
3609
|
+
} catch (error) {
|
|
3610
|
+
if (!isTextContextLimitMessage(error?.message || "")) {
|
|
3611
|
+
throw error;
|
|
3612
|
+
}
|
|
3613
|
+
log("[TEXT_MODEL] 检测到上下文长度限制,启用分段筛选模式。");
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
const chunkSize = parsePositiveInteger(process.env.BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS) || DEFAULT_TEXT_MODEL_CHUNK_SIZE_CHARS;
|
|
3617
|
+
const overlap = parsePositiveInteger(process.env.BOSS_RECOMMEND_TEXT_CHUNK_OVERLAP_CHARS) || DEFAULT_TEXT_MODEL_CHUNK_OVERLAP_CHARS;
|
|
3618
|
+
const maxChunks = parsePositiveInteger(process.env.BOSS_RECOMMEND_TEXT_MAX_CHUNKS) || DEFAULT_TEXT_MODEL_MAX_CHUNKS;
|
|
3619
|
+
const chunks = splitTextByChunks(fullResumeText, chunkSize, overlap, maxChunks);
|
|
3620
|
+
if (!chunks.length) {
|
|
3621
|
+
throw this.buildError("TEXT_MODEL_FAILED", "Resume text is empty after chunk split.");
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
const chunkResults = [];
|
|
3625
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
3626
|
+
const chunk = chunks[index];
|
|
3627
|
+
const result = await this.requestTextModel(chunk.text, {
|
|
3628
|
+
chunkIndex: index + 1,
|
|
3629
|
+
chunkTotal: chunks.length
|
|
3630
|
+
});
|
|
3631
|
+
chunkResults.push(result);
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
const passedChunks = chunkResults.filter((item) => item?.passed === true);
|
|
3635
|
+
if (passedChunks.length > 0) {
|
|
3636
|
+
const best = passedChunks[0];
|
|
3637
|
+
return {
|
|
3638
|
+
passed: true,
|
|
3639
|
+
reason: best.reason || `分段筛选命中(${best.chunkIndex}/${chunks.length})。`,
|
|
3640
|
+
summary: best.summary || best.reason || "分段筛选命中",
|
|
3641
|
+
evidence: Array.isArray(best.evidence) ? best.evidence : []
|
|
3642
|
+
};
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
const firstReason = chunkResults.map((item) => normalizeText(item?.reason)).find(Boolean);
|
|
3646
|
+
return {
|
|
3647
|
+
passed: false,
|
|
3648
|
+
reason: firstReason || `分段筛选未找到满足标准的证据(共 ${chunks.length} 段)。`,
|
|
3649
|
+
summary: firstReason || `分段筛选未找到满足标准的证据(共 ${chunks.length} 段)。`,
|
|
3650
|
+
evidence: []
|
|
3651
|
+
};
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
async requestTextModel(resumeText, options = {}) {
|
|
3655
|
+
const safeResumeText = String(resumeText || "");
|
|
3656
|
+
const chunkIndex = Number.isInteger(options.chunkIndex) && options.chunkIndex > 0 ? options.chunkIndex : 1;
|
|
3657
|
+
const chunkTotal = Number.isInteger(options.chunkTotal) && options.chunkTotal > 0 ? options.chunkTotal : 1;
|
|
3658
|
+
const chunkHint = chunkTotal > 1
|
|
3659
|
+
? `\n\n当前输入是简历分段 ${chunkIndex}/${chunkTotal}。请严格基于本分段文本判断;如果本分段证据不足,必须返回 passed=false。`
|
|
3660
|
+
: "";
|
|
3316
3661
|
const rawBaseUrl = this.args.baseUrl;
|
|
3317
3662
|
log(`[callTextModel] baseUrl 原始值类型=${typeof rawBaseUrl}, 长度=${rawBaseUrl != null ? String(rawBaseUrl).length : "null/undefined"}, JSON编码=${JSON.stringify(rawBaseUrl)}`);
|
|
3318
3663
|
const baseUrl = String(rawBaseUrl || "").replace(/\/$/, "");
|
|
@@ -3322,11 +3667,21 @@ class RecommendScreenCli {
|
|
|
3322
3667
|
messages: [
|
|
3323
3668
|
{
|
|
3324
3669
|
role: "system",
|
|
3325
|
-
content:
|
|
3670
|
+
content:
|
|
3671
|
+
"你是一位严谨的招聘筛选助手。必须完整阅读输入内容,严禁编造不存在的简历经历。" +
|
|
3672
|
+
"只能返回 JSON,不要输出任何额外文字。"
|
|
3326
3673
|
},
|
|
3327
3674
|
{
|
|
3328
3675
|
role: "user",
|
|
3329
|
-
content:
|
|
3676
|
+
content:
|
|
3677
|
+
`请根据以下标准判断候选人是否通过筛选。\n\n筛选标准:\n${this.args.criteria}\n\n` +
|
|
3678
|
+
`简历内容:\n${safeResumeText}${chunkHint}\n\n` +
|
|
3679
|
+
"要求:\n" +
|
|
3680
|
+
"1) 必须完整阅读上面的全部简历文本。\n" +
|
|
3681
|
+
"2) 只能依据简历中真实出现的信息判断,严禁编造不存在的经历/项目/学历/公司。\n" +
|
|
3682
|
+
"3) 若证据不足,必须返回 passed=false。\n\n" +
|
|
3683
|
+
"请返回严格 JSON: " +
|
|
3684
|
+
"{\"passed\": true/false, \"reason\": \"...\", \"summary\": \"...\", \"evidence\": [\"证据原文1\", \"证据原文2\"]}"
|
|
3330
3685
|
}
|
|
3331
3686
|
]
|
|
3332
3687
|
};
|
|
@@ -3351,10 +3706,28 @@ class RecommendScreenCli {
|
|
|
3351
3706
|
? json.choices[0].message.content.map((item) => item?.text || "").join("\n")
|
|
3352
3707
|
: json?.choices?.[0]?.message?.content || "";
|
|
3353
3708
|
const parsed = extractJsonObject(content);
|
|
3709
|
+
const reason = normalizeText(parsed.reason);
|
|
3710
|
+
const summary = normalizeText(parsed.summary || reason);
|
|
3711
|
+
const normalizedResume = normalizeText(safeResumeText);
|
|
3712
|
+
const parsedEvidence = toStringArray(parsed.evidence);
|
|
3713
|
+
const evidence = parsedEvidence.filter((item) => {
|
|
3714
|
+
const normalizedEvidence = normalizeText(item);
|
|
3715
|
+
if (!normalizedEvidence) return false;
|
|
3716
|
+
return safeResumeText.includes(item) || normalizedResume.includes(normalizedEvidence);
|
|
3717
|
+
});
|
|
3718
|
+
let passed = parsed.passed === true;
|
|
3719
|
+
let finalReason = reason || (passed ? "满足筛选标准。" : "不满足筛选标准。");
|
|
3720
|
+
if (passed && evidence.length <= 0) {
|
|
3721
|
+
passed = false;
|
|
3722
|
+
finalReason = `模型未给出可在简历原文中校验的证据,按安全策略判为不通过。${reason ? ` 原始原因: ${reason}` : ""}`;
|
|
3723
|
+
}
|
|
3354
3724
|
return {
|
|
3355
|
-
passed
|
|
3356
|
-
reason:
|
|
3357
|
-
summary:
|
|
3725
|
+
passed,
|
|
3726
|
+
reason: finalReason,
|
|
3727
|
+
summary: summary || finalReason,
|
|
3728
|
+
evidence,
|
|
3729
|
+
chunkIndex,
|
|
3730
|
+
chunkTotal
|
|
3358
3731
|
};
|
|
3359
3732
|
}
|
|
3360
3733
|
|
|
@@ -3580,8 +3953,13 @@ class RecommendScreenCli {
|
|
|
3580
3953
|
}
|
|
3581
3954
|
|
|
3582
3955
|
state = await this.getDetailClosedState();
|
|
3583
|
-
|
|
3584
|
-
|
|
3956
|
+
const listState = await this.evaluate(jsGetListState);
|
|
3957
|
+
if (listState?.ok) {
|
|
3958
|
+
log(`[关闭详情] 连续 ESC 后仍未确认关闭(${state?.reason || "unknown"}),但候选人列表已可用,按就绪状态继续。`);
|
|
3959
|
+
return true;
|
|
3960
|
+
}
|
|
3961
|
+
log(`[关闭详情] 连续 ESC 后仍未确认关闭(${state?.reason || "unknown"}),且候选人列表未恢复,判定关闭失败。`);
|
|
3962
|
+
return false;
|
|
3585
3963
|
}
|
|
3586
3964
|
|
|
3587
3965
|
async waitForListReady(maxRounds = 30) {
|
|
@@ -3650,7 +4028,10 @@ class RecommendScreenCli {
|
|
|
3650
4028
|
|
|
3651
4029
|
const restoredFromCheckpoint = this.loadCheckpointIfNeeded();
|
|
3652
4030
|
if (restoredFromCheckpoint) {
|
|
3653
|
-
log(
|
|
4031
|
+
log(
|
|
4032
|
+
`[恢复] 已从 checkpoint 恢复,已处理 ${this.processedCount} 位候选人,` +
|
|
4033
|
+
`其中通过 ${this.passedCandidates.length} 位。`
|
|
4034
|
+
);
|
|
3654
4035
|
}
|
|
3655
4036
|
|
|
3656
4037
|
await this.connect();
|
|
@@ -3684,7 +4065,7 @@ class RecommendScreenCli {
|
|
|
3684
4065
|
}
|
|
3685
4066
|
|
|
3686
4067
|
let pageExhaustion = null;
|
|
3687
|
-
while (!this.args.targetCount || this.
|
|
4068
|
+
while (!this.args.targetCount || this.passedCandidates.length < this.args.targetCount) {
|
|
3688
4069
|
if (this.shouldPauseAtBoundary()) {
|
|
3689
4070
|
this.saveCsv();
|
|
3690
4071
|
this.saveCheckpoint();
|
|
@@ -3942,10 +4323,10 @@ class RecommendScreenCli {
|
|
|
3942
4323
|
}
|
|
3943
4324
|
}
|
|
3944
4325
|
|
|
3945
|
-
if (this.args.targetCount && this.
|
|
4326
|
+
if (this.args.targetCount && this.passedCandidates.length < this.args.targetCount) {
|
|
3946
4327
|
throw this.buildError(
|
|
3947
4328
|
"TARGET_COUNT_NOT_REACHED_PAGE_EXHAUSTED",
|
|
3948
|
-
|
|
4329
|
+
`推荐列表已到底,但当前仅通过 ${this.passedCandidates.length} 位,尚未达到目标 ${this.args.targetCount} 位。`,
|
|
3949
4330
|
true,
|
|
3950
4331
|
{
|
|
3951
4332
|
partial_result: this.buildProgressSnapshot("page_exhausted_before_target_count"),
|
|
@@ -3964,11 +4345,11 @@ class RecommendScreenCli {
|
|
|
3964
4345
|
status: "COMPLETED",
|
|
3965
4346
|
result: {
|
|
3966
4347
|
...this.buildProgressSnapshot(
|
|
3967
|
-
this.args.targetCount && this.
|
|
4348
|
+
this.args.targetCount && this.passedCandidates.length >= this.args.targetCount
|
|
3968
4349
|
? "target_count_reached"
|
|
3969
4350
|
: "page_exhausted"
|
|
3970
4351
|
),
|
|
3971
|
-
completion_reason: this.args.targetCount && this.
|
|
4352
|
+
completion_reason: this.args.targetCount && this.passedCandidates.length >= this.args.targetCount
|
|
3972
4353
|
? "target_count_reached"
|
|
3973
4354
|
: "page_exhausted",
|
|
3974
4355
|
}
|