@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
|
@@ -124,6 +124,30 @@ class FakeRecommendScreenCli extends RecommendScreenCli {
|
|
|
124
124
|
saveCheckpoint() {}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
class FakeDetailCloseProbeCli extends RecommendScreenCli {
|
|
128
|
+
constructor(args, options = {}) {
|
|
129
|
+
super(args);
|
|
130
|
+
this.listReady = options.listReady === true;
|
|
131
|
+
this.evaluateCallCount = 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async getDetailClosedState() {
|
|
135
|
+
return { closed: false, reason: "popup visible: .boss-popup__wrapper" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async evaluate() {
|
|
139
|
+
this.evaluateCallCount += 1;
|
|
140
|
+
if (this.evaluateCallCount >= 2) {
|
|
141
|
+
return this.listReady
|
|
142
|
+
? { ok: true, candidate_count: 1 }
|
|
143
|
+
: { ok: false, error: "LIST_NOT_READY" };
|
|
144
|
+
}
|
|
145
|
+
return { ok: false, error: "CLOSE_ACTION_NOOP" };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async pressEsc() {}
|
|
149
|
+
}
|
|
150
|
+
|
|
127
151
|
function createResumeCaptureError(message = "Resume canvas not found") {
|
|
128
152
|
const error = new Error(message);
|
|
129
153
|
error.code = "RESUME_CAPTURE_FAILED";
|
|
@@ -306,6 +330,55 @@ async function testPageExhaustedWithoutTargetShouldStillComplete() {
|
|
|
306
330
|
assert.equal(result.result.completion_reason, "page_exhausted");
|
|
307
331
|
}
|
|
308
332
|
|
|
333
|
+
async function testTargetCountShouldStopWhenPassedCountReached() {
|
|
334
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-target-pass-stop-"));
|
|
335
|
+
const args = createArgs(tempDir);
|
|
336
|
+
args.targetCount = 1;
|
|
337
|
+
const first = { key: "pass-1", geek_id: "pass-1", name: "pass-1" };
|
|
338
|
+
const second = { key: "skip-2", geek_id: "skip-2", name: "skip-2" };
|
|
339
|
+
const cli = new FakeRecommendScreenCli(args, {
|
|
340
|
+
candidates: [first, second],
|
|
341
|
+
screeningByKey: new Map([
|
|
342
|
+
["pass-1", { passed: true, reason: "matched", summary: "matched" }],
|
|
343
|
+
["skip-2", { passed: false, reason: "not matched", summary: "not matched" }]
|
|
344
|
+
])
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const result = await cli.run();
|
|
348
|
+
assert.equal(result.status, "COMPLETED");
|
|
349
|
+
assert.equal(result.result.processed_count, 1);
|
|
350
|
+
assert.equal(result.result.passed_count, 1);
|
|
351
|
+
assert.equal(result.result.skipped_count, 0);
|
|
352
|
+
assert.equal(result.result.completion_reason, "target_count_reached");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function testTargetCountShouldNotTreatProcessedCountAsReached() {
|
|
356
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-target-pass-only-"));
|
|
357
|
+
const args = createArgs(tempDir);
|
|
358
|
+
args.targetCount = 1;
|
|
359
|
+
const first = { key: "skip-a", geek_id: "skip-a", name: "skip-a" };
|
|
360
|
+
const second = { key: "skip-b", geek_id: "skip-b", name: "skip-b" };
|
|
361
|
+
const cli = new FakeRecommendScreenCli(args, {
|
|
362
|
+
candidates: [first, second],
|
|
363
|
+
screeningByKey: new Map([
|
|
364
|
+
["skip-a", { passed: false, reason: "not matched", summary: "not matched" }],
|
|
365
|
+
["skip-b", { passed: false, reason: "not matched", summary: "not matched" }]
|
|
366
|
+
])
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
await assert.rejects(
|
|
370
|
+
() => cli.run(),
|
|
371
|
+
(error) => {
|
|
372
|
+
assert.equal(error.code, "TARGET_COUNT_NOT_REACHED_PAGE_EXHAUSTED");
|
|
373
|
+
assert.equal(error.retryable, true);
|
|
374
|
+
assert.equal(error.partial_result?.processed_count, 2);
|
|
375
|
+
assert.equal(error.partial_result?.passed_count, 0);
|
|
376
|
+
assert.equal(error.partial_result?.completion_reason, "page_exhausted_before_target_count");
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
309
382
|
async function testFeaturedShouldUseNetworkResumeOnly() {
|
|
310
383
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-network-first-"));
|
|
311
384
|
const candidate = { key: "net-1", geek_id: "net-1", name: "network candidate" };
|
|
@@ -842,12 +915,152 @@ function testParseArgsShouldSupportLatestPageScope() {
|
|
|
842
915
|
assert.equal(parsed.port, 9222);
|
|
843
916
|
}
|
|
844
917
|
|
|
918
|
+
async function testCallTextModelShouldNotTruncateLongResume() {
|
|
919
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-text-full-"));
|
|
920
|
+
const cli = new RecommendScreenCli(createArgs(tempDir));
|
|
921
|
+
const marker = "__END_OF_RESUME_MARKER__";
|
|
922
|
+
const resumeText = `${"A".repeat(32000)}${marker}`;
|
|
923
|
+
const originalFetch = global.fetch;
|
|
924
|
+
let capturedUserContent = "";
|
|
925
|
+
global.fetch = async (_url, options = {}) => {
|
|
926
|
+
const payload = JSON.parse(String(options.body || "{}"));
|
|
927
|
+
capturedUserContent = String(payload?.messages?.[1]?.content || "");
|
|
928
|
+
return {
|
|
929
|
+
ok: true,
|
|
930
|
+
status: 200,
|
|
931
|
+
async json() {
|
|
932
|
+
return {
|
|
933
|
+
choices: [
|
|
934
|
+
{
|
|
935
|
+
message: {
|
|
936
|
+
content: "{\"passed\": false, \"reason\": \"not matched\", \"summary\": \"not matched\", \"evidence\": [\"A\"]}"
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
]
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
};
|
|
944
|
+
try {
|
|
945
|
+
const result = await cli.callTextModel(resumeText);
|
|
946
|
+
assert.equal(result.passed, false);
|
|
947
|
+
assert.equal(capturedUserContent.includes(marker), true);
|
|
948
|
+
} finally {
|
|
949
|
+
global.fetch = originalFetch;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
async function testCallTextModelShouldFallbackToChunkModeOnContextLimit() {
|
|
954
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-text-chunk-fallback-"));
|
|
955
|
+
const cli = new RecommendScreenCli(createArgs(tempDir));
|
|
956
|
+
const originalFetch = global.fetch;
|
|
957
|
+
const prevChunkSize = process.env.BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS;
|
|
958
|
+
const prevChunkOverlap = process.env.BOSS_RECOMMEND_TEXT_CHUNK_OVERLAP_CHARS;
|
|
959
|
+
const prevMaxChunks = process.env.BOSS_RECOMMEND_TEXT_MAX_CHUNKS;
|
|
960
|
+
process.env.BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS = "80";
|
|
961
|
+
process.env.BOSS_RECOMMEND_TEXT_CHUNK_OVERLAP_CHARS = "0";
|
|
962
|
+
process.env.BOSS_RECOMMEND_TEXT_MAX_CHUNKS = "6";
|
|
963
|
+
|
|
964
|
+
const passMarker = "PASS_MARKER_ABC";
|
|
965
|
+
const resumeText = `${"x".repeat(120)}${passMarker}${"y".repeat(120)}`;
|
|
966
|
+
let callCount = 0;
|
|
967
|
+
global.fetch = async (_url, options = {}) => {
|
|
968
|
+
callCount += 1;
|
|
969
|
+
if (callCount === 1) {
|
|
970
|
+
return {
|
|
971
|
+
ok: false,
|
|
972
|
+
status: 400,
|
|
973
|
+
async text() {
|
|
974
|
+
return "maximum context length exceeded";
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const payload = JSON.parse(String(options.body || "{}"));
|
|
980
|
+
const userContent = String(payload?.messages?.[1]?.content || "");
|
|
981
|
+
const passed = userContent.includes(passMarker);
|
|
982
|
+
const response = passed
|
|
983
|
+
? "{\"passed\": true, \"reason\": \"命中证据\", \"summary\": \"命中\", \"evidence\": [\"PASS_MARKER_ABC\"]}"
|
|
984
|
+
: "{\"passed\": false, \"reason\": \"本段证据不足\", \"summary\": \"不足\", \"evidence\": []}";
|
|
985
|
+
return {
|
|
986
|
+
ok: true,
|
|
987
|
+
status: 200,
|
|
988
|
+
async json() {
|
|
989
|
+
return {
|
|
990
|
+
choices: [
|
|
991
|
+
{
|
|
992
|
+
message: {
|
|
993
|
+
content: response
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
]
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
};
|
|
1001
|
+
try {
|
|
1002
|
+
const result = await cli.callTextModel(resumeText);
|
|
1003
|
+
assert.equal(result.passed, true);
|
|
1004
|
+
assert.equal(callCount >= 2, true);
|
|
1005
|
+
assert.equal(Array.isArray(result.evidence), true);
|
|
1006
|
+
} finally {
|
|
1007
|
+
global.fetch = originalFetch;
|
|
1008
|
+
if (prevChunkSize === undefined) {
|
|
1009
|
+
delete process.env.BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS;
|
|
1010
|
+
} else {
|
|
1011
|
+
process.env.BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS = prevChunkSize;
|
|
1012
|
+
}
|
|
1013
|
+
if (prevChunkOverlap === undefined) {
|
|
1014
|
+
delete process.env.BOSS_RECOMMEND_TEXT_CHUNK_OVERLAP_CHARS;
|
|
1015
|
+
} else {
|
|
1016
|
+
process.env.BOSS_RECOMMEND_TEXT_CHUNK_OVERLAP_CHARS = prevChunkOverlap;
|
|
1017
|
+
}
|
|
1018
|
+
if (prevMaxChunks === undefined) {
|
|
1019
|
+
delete process.env.BOSS_RECOMMEND_TEXT_MAX_CHUNKS;
|
|
1020
|
+
} else {
|
|
1021
|
+
process.env.BOSS_RECOMMEND_TEXT_MAX_CHUNKS = prevMaxChunks;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async function testPrepareVisionImageSegmentsShouldSplitLongImage() {
|
|
1027
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-vision-segments-"));
|
|
1028
|
+
const cli = new RecommendScreenCli(createArgs(tempDir));
|
|
1029
|
+
const imagePath = path.join(tempDir, "long.png");
|
|
1030
|
+
await sharp({
|
|
1031
|
+
create: { width: 400, height: 1200, channels: 3, background: { r: 240, g: 240, b: 240 } }
|
|
1032
|
+
}).png().toFile(imagePath);
|
|
1033
|
+
|
|
1034
|
+
const prepared = await cli.prepareVisionImageSegmentsForModel(imagePath, 120000, "test");
|
|
1035
|
+
assert.equal(Array.isArray(prepared.imagePaths), true);
|
|
1036
|
+
assert.equal(prepared.imagePaths.length > 1, true);
|
|
1037
|
+
for (const segmentPath of prepared.imagePaths) {
|
|
1038
|
+
assert.equal(fs.existsSync(segmentPath), true);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
async function testCloseDetailPageShouldFailWhenDetailStillOpenAndListNotReady() {
|
|
1043
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-close-detail-fail-"));
|
|
1044
|
+
const cli = new FakeDetailCloseProbeCli(createArgs(tempDir), { listReady: false });
|
|
1045
|
+
const closed = await cli.closeDetailPage(1);
|
|
1046
|
+
assert.equal(closed, false);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
async function testCloseDetailPageShouldContinueWhenListReady() {
|
|
1050
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-close-detail-list-ready-"));
|
|
1051
|
+
const cli = new FakeDetailCloseProbeCli(createArgs(tempDir), { listReady: true });
|
|
1052
|
+
const closed = await cli.closeDetailPage(1);
|
|
1053
|
+
assert.equal(closed, true);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
845
1056
|
async function main() {
|
|
846
1057
|
testShouldAbortResumeProbeEarly();
|
|
847
1058
|
await testSingleResumeCaptureFailureIsSkipped();
|
|
848
1059
|
await testConsecutiveResumeCaptureFailuresStillAbort();
|
|
849
1060
|
await testPageExhaustedBeforeTargetShouldRaiseRecoverableError();
|
|
850
1061
|
await testPageExhaustedWithoutTargetShouldStillComplete();
|
|
1062
|
+
await testTargetCountShouldStopWhenPassedCountReached();
|
|
1063
|
+
await testTargetCountShouldNotTreatProcessedCountAsReached();
|
|
851
1064
|
await testFeaturedShouldUseNetworkResumeOnly();
|
|
852
1065
|
await testRecommendShouldPreferNetworkResumeWhenAvailable();
|
|
853
1066
|
await testNetworkMissShouldFallbackToImageCapture();
|
|
@@ -871,6 +1084,11 @@ async function main() {
|
|
|
871
1084
|
testStitchWithAvailablePythonShouldFailWhenScriptMissing();
|
|
872
1085
|
testParseArgsShouldSupportFeaturedAliasesAndInlinePort();
|
|
873
1086
|
testParseArgsShouldSupportLatestPageScope();
|
|
1087
|
+
await testCallTextModelShouldNotTruncateLongResume();
|
|
1088
|
+
await testCallTextModelShouldFallbackToChunkModeOnContextLimit();
|
|
1089
|
+
await testPrepareVisionImageSegmentsShouldSplitLongImage();
|
|
1090
|
+
await testCloseDetailPageShouldFailWhenDetailStillOpenAndListNotReady();
|
|
1091
|
+
await testCloseDetailPageShouldContinueWhenListReady();
|
|
874
1092
|
console.log("recoverable resume failure tests passed");
|
|
875
1093
|
}
|
|
876
1094
|
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import readline from "node:readline";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
4
5
|
import { pathToFileURL } from "node:url";
|
|
5
6
|
import CDP from "chrome-remote-interface";
|
|
7
|
+
import {
|
|
8
|
+
buildFirstSelectorLookupExpression,
|
|
9
|
+
getRecommendSelectorRule
|
|
10
|
+
} from "../../../src/recommend-healing-config.js";
|
|
6
11
|
|
|
7
12
|
const DEFAULT_PORT = 9222;
|
|
8
13
|
const RECOMMEND_URL_FRAGMENT = "/web/chat/recommend";
|
|
@@ -14,6 +19,56 @@ const DEGREE_OPTIONS = ["不限", "初中及以下", "中专/中技", "高中",
|
|
|
14
19
|
const DEGREE_ORDER = ["初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
|
|
15
20
|
const GENDER_OPTIONS = ["不限", "男", "女"];
|
|
16
21
|
const RECENT_NOT_VIEW_OPTIONS = ["不限", "近14天没有"];
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
require("../../../src/recommend-healing-rules.json");
|
|
24
|
+
const RECOMMEND_IFRAME_SELECTORS = getRecommendSelectorRule(
|
|
25
|
+
["top", "recommend_iframe"],
|
|
26
|
+
['iframe[name="recommendFrame"]', 'iframe[src*="/web/frame/recommend/"]', "iframe"]
|
|
27
|
+
);
|
|
28
|
+
const FILTER_TRIGGER_SELECTORS = getRecommendSelectorRule(
|
|
29
|
+
["frame", "filter_trigger"],
|
|
30
|
+
[".filter-label-wrap", ".recommend-filter.op-filter"]
|
|
31
|
+
);
|
|
32
|
+
const JOB_DROPDOWN_TRIGGER_SELECTORS = getRecommendSelectorRule(
|
|
33
|
+
["frame", "job_dropdown_trigger"],
|
|
34
|
+
[
|
|
35
|
+
".chat-job-select",
|
|
36
|
+
".chat-job-selector",
|
|
37
|
+
".job-selecter",
|
|
38
|
+
".job-selector",
|
|
39
|
+
".job-select-wrap",
|
|
40
|
+
".job-select",
|
|
41
|
+
".job-select-box",
|
|
42
|
+
".job-wrap",
|
|
43
|
+
".chat-job-name",
|
|
44
|
+
".top-chat-search"
|
|
45
|
+
]
|
|
46
|
+
);
|
|
47
|
+
const JOB_LIST_ITEM_SELECTORS = getRecommendSelectorRule(
|
|
48
|
+
["frame", "job_list_items"],
|
|
49
|
+
[
|
|
50
|
+
".ui-dropmenu-list .job-list .job-item",
|
|
51
|
+
".job-selecter-options .job-list .job-item",
|
|
52
|
+
".job-selector-options .job-list .job-item",
|
|
53
|
+
".dropmenu-list .job-list .job-item",
|
|
54
|
+
".job-list .job-item"
|
|
55
|
+
]
|
|
56
|
+
);
|
|
57
|
+
const JOB_SELECTED_LABEL_SELECTORS = getRecommendSelectorRule(
|
|
58
|
+
["frame", "job_selected_label"],
|
|
59
|
+
[".chat-job-name", ".job-selecter .label", ".job-selecter .job-name", ".job-select .label"]
|
|
60
|
+
);
|
|
61
|
+
const RECOMMEND_CARD_SELECTORS = getRecommendSelectorRule(["frame", "recommend_cards"], ["ul.card-list > li.card-item"]);
|
|
62
|
+
const FEATURED_CARD_SELECTORS = getRecommendSelectorRule(["frame", "featured_cards"], ["li.geek-info-card"]);
|
|
63
|
+
const LATEST_CARD_SELECTORS = getRecommendSelectorRule(["frame", "latest_cards"], [".candidate-card-wrap"]);
|
|
64
|
+
const RECOMMEND_TAB_SELECTORS = getRecommendSelectorRule(
|
|
65
|
+
["frame", "tab_items"],
|
|
66
|
+
["li.tab-item[data-status]", 'li[data-status][class*="tab"]']
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
function buildRecommendFrameExpression() {
|
|
70
|
+
return buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS);
|
|
71
|
+
}
|
|
17
72
|
|
|
18
73
|
function normalizeText(value) {
|
|
19
74
|
return String(value || "").replace(/\s+/g, " ").trim();
|
|
@@ -419,9 +474,7 @@ class RecommendSearchCli {
|
|
|
419
474
|
title
|
|
420
475
|
};
|
|
421
476
|
}
|
|
422
|
-
const frame =
|
|
423
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
424
|
-
|| document.querySelector('iframe');
|
|
477
|
+
const frame = ${buildRecommendFrameExpression()};
|
|
425
478
|
if (!frame || !frame.contentDocument) {
|
|
426
479
|
return { ok: false, error: 'NO_RECOMMEND_IFRAME', currentUrl, title };
|
|
427
480
|
}
|
|
@@ -438,14 +491,16 @@ class RecommendSearchCli {
|
|
|
438
491
|
|
|
439
492
|
async getFilterEntryPoint() {
|
|
440
493
|
return this.evaluate(`(() => {
|
|
441
|
-
const frame =
|
|
442
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
443
|
-
|| document.querySelector('iframe');
|
|
494
|
+
const frame = ${buildRecommendFrameExpression()};
|
|
444
495
|
if (!frame || !frame.contentDocument) {
|
|
445
496
|
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
446
497
|
}
|
|
447
498
|
const doc = frame.contentDocument;
|
|
448
|
-
const el =
|
|
499
|
+
const el = ${JSON.stringify(FILTER_TRIGGER_SELECTORS)}
|
|
500
|
+
.map((selector) => {
|
|
501
|
+
try { return doc.querySelector(selector); } catch { return null; }
|
|
502
|
+
})
|
|
503
|
+
.find((node) => node) || null;
|
|
449
504
|
if (!el) {
|
|
450
505
|
return { ok: false, error: 'FILTER_TRIGGER_NOT_FOUND' };
|
|
451
506
|
}
|
|
@@ -461,9 +516,7 @@ class RecommendSearchCli {
|
|
|
461
516
|
|
|
462
517
|
async getJobListState() {
|
|
463
518
|
return this.evaluate(`(() => {
|
|
464
|
-
const frame =
|
|
465
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
466
|
-
|| document.querySelector('iframe');
|
|
519
|
+
const frame = ${buildRecommendFrameExpression()};
|
|
467
520
|
if (!frame || !frame.contentDocument) {
|
|
468
521
|
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
469
522
|
}
|
|
@@ -491,13 +544,10 @@ class RecommendSearchCli {
|
|
|
491
544
|
return rect.width > 2 && rect.height > 2;
|
|
492
545
|
};
|
|
493
546
|
|
|
494
|
-
const items =
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
'.dropmenu-list .job-list .job-item',
|
|
499
|
-
'.job-list .job-item'
|
|
500
|
-
].join(',')));
|
|
547
|
+
const items = ${JSON.stringify(JOB_LIST_ITEM_SELECTORS)}
|
|
548
|
+
.flatMap((selector) => {
|
|
549
|
+
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
550
|
+
});
|
|
501
551
|
const jobs = [];
|
|
502
552
|
const seen = new Set();
|
|
503
553
|
for (const item of items) {
|
|
@@ -516,7 +566,11 @@ class RecommendSearchCli {
|
|
|
516
566
|
});
|
|
517
567
|
}
|
|
518
568
|
|
|
519
|
-
const selectedLabelNode =
|
|
569
|
+
const selectedLabelNode = ${JSON.stringify(JOB_SELECTED_LABEL_SELECTORS)}
|
|
570
|
+
.map((selector) => {
|
|
571
|
+
try { return doc.querySelector(selector); } catch { return null; }
|
|
572
|
+
})
|
|
573
|
+
.find((node) => node) || null;
|
|
520
574
|
return {
|
|
521
575
|
ok: true,
|
|
522
576
|
jobs,
|
|
@@ -530,25 +584,12 @@ class RecommendSearchCli {
|
|
|
530
584
|
|
|
531
585
|
async clickJobDropdownTriggerBySelector() {
|
|
532
586
|
return this.evaluate(`(() => {
|
|
533
|
-
const frame =
|
|
534
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
535
|
-
|| document.querySelector('iframe');
|
|
587
|
+
const frame = ${buildRecommendFrameExpression()};
|
|
536
588
|
if (!frame || !frame.contentDocument) {
|
|
537
589
|
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
538
590
|
}
|
|
539
591
|
const doc = frame.contentDocument;
|
|
540
|
-
const selectors =
|
|
541
|
-
'.chat-job-select',
|
|
542
|
-
'.chat-job-selector',
|
|
543
|
-
'.job-selecter',
|
|
544
|
-
'.job-selector',
|
|
545
|
-
'.job-select-wrap',
|
|
546
|
-
'.job-select',
|
|
547
|
-
'.job-select-box',
|
|
548
|
-
'.job-wrap',
|
|
549
|
-
'.chat-job-name',
|
|
550
|
-
'.top-chat-search'
|
|
551
|
-
];
|
|
592
|
+
const selectors = ${JSON.stringify(JOB_DROPDOWN_TRIGGER_SELECTORS)};
|
|
552
593
|
const isVisible = (el) => {
|
|
553
594
|
if (!el) return false;
|
|
554
595
|
const style = getComputedStyle(el);
|
|
@@ -615,9 +656,7 @@ class RecommendSearchCli {
|
|
|
615
656
|
|
|
616
657
|
async clickJobBySelector(job) {
|
|
617
658
|
return this.evaluate(`((job) => {
|
|
618
|
-
const frame =
|
|
619
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
620
|
-
|| document.querySelector('iframe');
|
|
659
|
+
const frame = ${buildRecommendFrameExpression()};
|
|
621
660
|
if (!frame || !frame.contentDocument) {
|
|
622
661
|
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
623
662
|
}
|
|
@@ -635,13 +674,10 @@ class RecommendSearchCli {
|
|
|
635
674
|
.trim();
|
|
636
675
|
return strippedSingle || byGap;
|
|
637
676
|
};
|
|
638
|
-
const items =
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
'.dropmenu-list .job-list .job-item',
|
|
643
|
-
'.job-list .job-item'
|
|
644
|
-
].join(',')));
|
|
677
|
+
const items = ${JSON.stringify(JOB_LIST_ITEM_SELECTORS)}
|
|
678
|
+
.flatMap((selector) => {
|
|
679
|
+
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
680
|
+
});
|
|
645
681
|
const target = items.find((item) => {
|
|
646
682
|
const value = normalize(item.getAttribute('value') || item.dataset?.value || '');
|
|
647
683
|
const label = normalize(item.querySelector('.label')?.textContent || item.textContent || '');
|
|
@@ -1439,20 +1475,30 @@ class RecommendSearchCli {
|
|
|
1439
1475
|
|
|
1440
1476
|
async countCandidates() {
|
|
1441
1477
|
return this.evaluate(`(() => {
|
|
1442
|
-
const frame =
|
|
1443
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1444
|
-
|| document.querySelector('iframe');
|
|
1478
|
+
const frame = ${buildRecommendFrameExpression()};
|
|
1445
1479
|
if (!frame || !frame.contentDocument) {
|
|
1446
1480
|
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1447
1481
|
}
|
|
1448
1482
|
const doc = frame.contentDocument;
|
|
1449
|
-
const cards =
|
|
1483
|
+
const cards = ${JSON.stringify(RECOMMEND_CARD_SELECTORS)}
|
|
1484
|
+
.flatMap((selector) => {
|
|
1485
|
+
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
1486
|
+
});
|
|
1450
1487
|
const recommendCandidates = cards.filter((card) => card.querySelector('.card-inner[data-geekid]'));
|
|
1451
|
-
const featuredCards =
|
|
1488
|
+
const featuredCards = ${JSON.stringify(FEATURED_CARD_SELECTORS)}
|
|
1489
|
+
.flatMap((selector) => {
|
|
1490
|
+
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
1491
|
+
});
|
|
1452
1492
|
const featuredCandidates = featuredCards.filter((card) => card.querySelector('a[data-geekid]'));
|
|
1453
|
-
const latestCards =
|
|
1493
|
+
const latestCards = ${JSON.stringify(LATEST_CARD_SELECTORS)}
|
|
1494
|
+
.flatMap((selector) => {
|
|
1495
|
+
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
1496
|
+
});
|
|
1454
1497
|
const latestCandidates = latestCards.filter((card) => card.querySelector('.card-inner[data-geek], [data-geek]'));
|
|
1455
|
-
const tabs =
|
|
1498
|
+
const tabs = ${JSON.stringify(RECOMMEND_TAB_SELECTORS)}
|
|
1499
|
+
.flatMap((selector) => {
|
|
1500
|
+
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
1501
|
+
});
|
|
1456
1502
|
const activeTab = tabs.find((node) => {
|
|
1457
1503
|
const className = String(node.className || '');
|
|
1458
1504
|
const selected = String(node.getAttribute('aria-selected') || '').toLowerCase() === 'true';
|
|
@@ -1648,3 +1694,5 @@ export {
|
|
|
1648
1694
|
normalizeJobTitle,
|
|
1649
1695
|
parseArgs
|
|
1650
1696
|
};
|
|
1697
|
+
|
|
1698
|
+
|