@reconcrap/boss-recommend-mcp 1.3.39 → 2.0.0
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 +53 -33
- package/package.json +61 -9
- package/skills/boss-recommend-pipeline/SKILL.md +4 -0
- package/src/chat-mcp.js +1333 -0
- package/src/chat-runtime-config.js +559 -0
- package/src/cli.js +1095 -196
- package/src/core/browser/index.js +378 -0
- package/src/core/capture/index.js +298 -0
- package/src/core/cv-acquisition/index.js +219 -0
- package/src/core/greet-quota/index.js +54 -0
- package/src/core/infinite-list/index.js +459 -0
- package/src/core/reporting/legacy-csv.js +332 -0
- package/src/core/run/index.js +286 -0
- package/src/core/screening/index.js +1166 -0
- package/src/core/self-heal/index.js +848 -0
- package/src/domains/chat/cards.js +129 -0
- package/src/domains/chat/constants.js +183 -0
- package/src/domains/chat/detail.js +1369 -0
- package/src/domains/chat/index.js +7 -0
- package/src/domains/chat/jobs.js +334 -0
- package/src/domains/chat/page-guard.js +88 -0
- package/src/domains/chat/roots.js +56 -0
- package/src/domains/chat/run-service.js +1101 -0
- package/src/domains/recommend/actions.js +457 -0
- package/src/domains/recommend/cards.js +228 -0
- package/src/domains/recommend/constants.js +141 -0
- package/src/domains/recommend/detail.js +341 -0
- package/src/domains/recommend/filters.js +581 -0
- package/src/domains/recommend/index.js +10 -0
- package/src/domains/recommend/jobs.js +232 -0
- package/src/domains/recommend/refresh.js +204 -0
- package/src/domains/recommend/roots.js +78 -0
- package/src/domains/recommend/run-service.js +903 -0
- package/src/domains/recommend/scopes.js +245 -0
- package/src/domains/recruit/actions.js +277 -0
- package/src/domains/recruit/cards.js +67 -0
- package/src/domains/recruit/constants.js +130 -0
- package/src/domains/recruit/detail.js +414 -0
- package/src/domains/recruit/index.js +9 -0
- package/src/domains/recruit/instruction-parser.js +451 -0
- package/src/domains/recruit/refresh.js +40 -0
- package/src/domains/recruit/roots.js +68 -0
- package/src/domains/recruit/run-service.js +580 -0
- package/src/domains/recruit/search.js +1149 -0
- package/src/index.js +578 -419
- package/src/recommend-mcp.js +1257 -0
- package/src/recruit-mcp.js +1035 -0
- package/src/adapters.js +0 -3079
- package/src/boss-chat.js +0 -1037
- package/src/pipeline.js +0 -2249
- package/src/recommend-healing-config.js +0 -131
- package/src/recommend-healing-rules.json +0 -261
- package/src/self-heal.js +0 -2237
- package/src/test-adapters-runtime.js +0 -628
- package/src/test-boss-chat.js +0 -3196
- package/src/test-index-async.js +0 -498
- package/src/test-parser.js +0 -742
- package/src/test-pipeline.js +0 -2703
- package/src/test-run-state.js +0 -152
- package/src/test-self-heal.js +0 -224
- package/vendor/boss-chat-cli/README.md +0 -134
- package/vendor/boss-chat-cli/package.json +0 -53
- package/vendor/boss-chat-cli/src/app.js +0 -1501
- package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
- package/vendor/boss-chat-cli/src/cli.js +0 -1713
- package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
- package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
- package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
- package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
- package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
- package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
- package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
- package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
- package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
- package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
- package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
- package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
- package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
- package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -7072
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
- package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2423
- package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
- package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
|
@@ -1,1698 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import process from "node:process";
|
|
3
|
-
import readline from "node:readline";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
|
-
import { pathToFileURL } from "node:url";
|
|
6
|
-
import CDP from "chrome-remote-interface";
|
|
7
|
-
import {
|
|
8
|
-
buildFirstSelectorLookupExpression,
|
|
9
|
-
getRecommendSelectorRule
|
|
10
|
-
} from "../../../src/recommend-healing-config.js";
|
|
11
|
-
|
|
12
|
-
const DEFAULT_PORT = 9222;
|
|
13
|
-
const RECOMMEND_URL_FRAGMENT = "/web/chat/recommend";
|
|
14
|
-
const BOSS_LOGIN_URL = "https://www.zhipin.com/web/user/?ka=bticket";
|
|
15
|
-
const BOSS_LOGIN_URL_PATTERN = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
|
|
16
|
-
const BOSS_LOGIN_TITLE_PATTERN = /登录|signin|扫码登录|BOSS直聘登录/i;
|
|
17
|
-
const SCHOOL_TAG_OPTIONS = ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"];
|
|
18
|
-
const DEGREE_OPTIONS = ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
|
|
19
|
-
const DEGREE_ORDER = ["初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
|
|
20
|
-
const GENDER_OPTIONS = ["不限", "男", "女"];
|
|
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
|
-
}
|
|
72
|
-
|
|
73
|
-
function normalizeText(value) {
|
|
74
|
-
return String(value || "").replace(/\s+/g, " ").trim();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function normalizeJobTitle(value) {
|
|
78
|
-
const text = normalizeText(value);
|
|
79
|
-
if (!text) return "";
|
|
80
|
-
const byGap = text.split(/\s{2,}/).map((item) => item.trim()).filter(Boolean)[0] || text;
|
|
81
|
-
const strippedRange = byGap
|
|
82
|
-
.replace(/\s+\d+(?:\.\d+)?\s*(?:-|~|—|至)\s*\d+(?:\.\d+)?\s*(?:k|K|千|万|元\/天|元\/月|元\/年|K\/月|k\/月|万\/月|万\/年)?$/u, "")
|
|
83
|
-
.trim();
|
|
84
|
-
const strippedSingle = strippedRange
|
|
85
|
-
.replace(/\s+\d+(?:\.\d+)?\s*(?:k|K|千|万|元\/天|元\/月|元\/年|K\/月|k\/月|万\/月|万\/年)$/u, "")
|
|
86
|
-
.trim();
|
|
87
|
-
return strippedSingle || byGap;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function parsePositiveInteger(raw) {
|
|
91
|
-
const value = Number.parseInt(String(raw || ""), 10);
|
|
92
|
-
return Number.isFinite(value) && value > 0 ? value : null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function normalizePageScope(value) {
|
|
96
|
-
const normalized = normalizeText(value).toLowerCase();
|
|
97
|
-
if (!normalized) return null;
|
|
98
|
-
if (["recommend", "推荐", "推荐页", "推荐页面"].includes(normalized)) return "recommend";
|
|
99
|
-
if (["latest", "最新", "最新页", "最新页面"].includes(normalized)) return "latest";
|
|
100
|
-
if (["featured", "精选", "精选页", "精选页面", "精选牛人"].includes(normalized)) return "featured";
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function sortSchoolSelection(values) {
|
|
105
|
-
const order = new Map(SCHOOL_TAG_OPTIONS.map((label, index) => [label, index]));
|
|
106
|
-
const unique = Array.from(new Set((values || []).filter((item) => order.has(item))));
|
|
107
|
-
if (!unique.length) return [];
|
|
108
|
-
if (unique.includes("不限")) {
|
|
109
|
-
return unique.length === 1
|
|
110
|
-
? ["不限"]
|
|
111
|
-
: unique.filter((item) => item !== "不限").sort((left, right) => order.get(left) - order.get(right));
|
|
112
|
-
}
|
|
113
|
-
return unique.sort((left, right) => order.get(left) - order.get(right));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function parseSchoolSelection(raw) {
|
|
117
|
-
const text = normalizeText(raw);
|
|
118
|
-
if (!text) return null;
|
|
119
|
-
if (text === "不限") return ["不限"];
|
|
120
|
-
|
|
121
|
-
const selected = [];
|
|
122
|
-
for (const chunk of text.split(/[,,、/|]/)) {
|
|
123
|
-
const value = normalizeText(chunk);
|
|
124
|
-
if (SCHOOL_TAG_OPTIONS.includes(value)) {
|
|
125
|
-
selected.push(value);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
for (const label of SCHOOL_TAG_OPTIONS) {
|
|
129
|
-
if (label !== "不限" && text.includes(label)) {
|
|
130
|
-
selected.push(label);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
const normalized = sortSchoolSelection(selected);
|
|
134
|
-
return normalized.length ? normalized : null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function normalizeDegree(value) {
|
|
138
|
-
const normalized = normalizeText(value);
|
|
139
|
-
if (!normalized) return null;
|
|
140
|
-
if (normalized === "专科") return "大专";
|
|
141
|
-
if (normalized === "研究生") return "硕士";
|
|
142
|
-
if (normalized === "中专" || normalized === "中技" || normalized === "中专中技") return "中专/中技";
|
|
143
|
-
return DEGREE_OPTIONS.includes(normalized) ? normalized : null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function sortDegreeSelection(values) {
|
|
147
|
-
return Array.from(new Set(values.filter(Boolean))).sort((left, right) => DEGREE_ORDER.indexOf(left) - DEGREE_ORDER.indexOf(right));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function selectionEquals(left, right) {
|
|
151
|
-
if (!Array.isArray(left) || !Array.isArray(right)) return false;
|
|
152
|
-
if (left.length !== right.length) return false;
|
|
153
|
-
return left.every((value, index) => value === right[index]);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function uniqueNormalizedLabels(values) {
|
|
157
|
-
return Array.from(
|
|
158
|
-
new Set(
|
|
159
|
-
(values || [])
|
|
160
|
-
.map((item) => normalizeText(item))
|
|
161
|
-
.filter(Boolean)
|
|
162
|
-
)
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function expandDegreeAtOrAbove(value) {
|
|
167
|
-
const normalized = normalizeDegree(value);
|
|
168
|
-
if (!normalized || normalized === "不限") return [];
|
|
169
|
-
const index = DEGREE_ORDER.indexOf(normalized);
|
|
170
|
-
if (index === -1) return [];
|
|
171
|
-
return DEGREE_ORDER.slice(index);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function parseDegreeSelection(raw) {
|
|
175
|
-
const text = normalizeText(raw);
|
|
176
|
-
if (!text) return null;
|
|
177
|
-
if (text === "不限") return ["不限"];
|
|
178
|
-
if (/不限/.test(text) && !/(初中|中专|中技|高中|大专|专科|本科|硕士|研究生|博士)/.test(text)) {
|
|
179
|
-
return ["不限"];
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const selected = [];
|
|
183
|
-
const atOrAbovePattern = /(初中及以下|中专\/中技|中专中技|中专|中技|高中|大专|专科|本科|硕士|研究生|博士)\s*(?:及|或)?以上/g;
|
|
184
|
-
let match;
|
|
185
|
-
while ((match = atOrAbovePattern.exec(text)) !== null) {
|
|
186
|
-
selected.push(...expandDegreeAtOrAbove(match[1]));
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const chunks = text.split(/[,,、/|]/).map((item) => normalizeDegree(item)).filter(Boolean);
|
|
190
|
-
selected.push(...chunks);
|
|
191
|
-
|
|
192
|
-
for (const label of DEGREE_OPTIONS) {
|
|
193
|
-
if (label === "不限") continue;
|
|
194
|
-
if (text.includes(label)) {
|
|
195
|
-
selected.push(label);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const normalized = sortDegreeSelection(selected);
|
|
200
|
-
return normalized.length ? normalized : null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function parseArgs(argv) {
|
|
204
|
-
const args = {
|
|
205
|
-
schoolTag: ["不限"],
|
|
206
|
-
degree: ["不限"],
|
|
207
|
-
gender: "不限",
|
|
208
|
-
recentNotView: "不限",
|
|
209
|
-
pageScope: "recommend",
|
|
210
|
-
port: DEFAULT_PORT,
|
|
211
|
-
listJobs: false,
|
|
212
|
-
job: null,
|
|
213
|
-
help: false,
|
|
214
|
-
__provided: {
|
|
215
|
-
schoolTag: false,
|
|
216
|
-
degree: false,
|
|
217
|
-
gender: false,
|
|
218
|
-
recentNotView: false,
|
|
219
|
-
pageScope: true,
|
|
220
|
-
port: false,
|
|
221
|
-
job: false
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
226
|
-
const token = argv[index];
|
|
227
|
-
const next = argv[index + 1];
|
|
228
|
-
if (token === "--school-tag" && next) {
|
|
229
|
-
args.schoolTag = parseSchoolSelection(next);
|
|
230
|
-
args.__provided.schoolTag = true;
|
|
231
|
-
index += 1;
|
|
232
|
-
} else if (token === "--degree" && next) {
|
|
233
|
-
args.degree = parseDegreeSelection(next);
|
|
234
|
-
args.__provided.degree = true;
|
|
235
|
-
index += 1;
|
|
236
|
-
} else if (token === "--gender" && next) {
|
|
237
|
-
args.gender = next;
|
|
238
|
-
args.__provided.gender = true;
|
|
239
|
-
index += 1;
|
|
240
|
-
} else if (token === "--recent-not-view" && next) {
|
|
241
|
-
args.recentNotView = next;
|
|
242
|
-
args.__provided.recentNotView = true;
|
|
243
|
-
index += 1;
|
|
244
|
-
} else if (token === "--port" && next) {
|
|
245
|
-
args.port = parsePositiveInteger(next) || DEFAULT_PORT;
|
|
246
|
-
args.__provided.port = true;
|
|
247
|
-
index += 1;
|
|
248
|
-
} else if (token === "--page-scope" && next) {
|
|
249
|
-
args.pageScope = normalizePageScope(next) || "recommend";
|
|
250
|
-
args.__provided.pageScope = true;
|
|
251
|
-
index += 1;
|
|
252
|
-
} else if (token === "--job" && next) {
|
|
253
|
-
args.job = normalizeText(next) || null;
|
|
254
|
-
args.__provided.job = true;
|
|
255
|
-
index += 1;
|
|
256
|
-
} else if (token === "--list-jobs") {
|
|
257
|
-
args.listJobs = true;
|
|
258
|
-
} else if (token === "--help" || token === "-h") {
|
|
259
|
-
args.help = true;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return args;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function isInteractiveTTY() {
|
|
267
|
-
return Boolean(process.stdin?.isTTY && process.stdout?.isTTY);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
async function promptValue(ask, question, validate, defaultValue) {
|
|
271
|
-
while (true) {
|
|
272
|
-
const answer = normalizeText(await ask(question));
|
|
273
|
-
if (!answer && defaultValue !== undefined) return defaultValue;
|
|
274
|
-
const validated = validate(answer);
|
|
275
|
-
if (validated !== null && validated !== undefined) return validated;
|
|
276
|
-
console.error("输入无效,请重试。");
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
async function enrichArgsFromPrompt(args) {
|
|
281
|
-
if (!isInteractiveTTY() || args.help) return args;
|
|
282
|
-
if (args.listJobs) return args;
|
|
283
|
-
const askTargets =
|
|
284
|
-
Object.values(args.__provided || {}).some((item) => item === false)
|
|
285
|
-
|| !Array.isArray(args.schoolTag)
|
|
286
|
-
|| args.schoolTag.length === 0
|
|
287
|
-
|| !Array.isArray(args.degree)
|
|
288
|
-
|| args.degree.length === 0;
|
|
289
|
-
if (!askTargets) return args;
|
|
290
|
-
|
|
291
|
-
const rl = readline.createInterface({
|
|
292
|
-
input: process.stdin,
|
|
293
|
-
output: process.stdout
|
|
294
|
-
});
|
|
295
|
-
const ask = (question) => new Promise((resolve) => rl.question(question, resolve));
|
|
296
|
-
try {
|
|
297
|
-
if (!args.__provided.schoolTag) {
|
|
298
|
-
const current = Array.isArray(args.schoolTag) && args.schoolTag.length > 0 ? args.schoolTag.join("/") : "不限";
|
|
299
|
-
args.schoolTag = await promptValue(
|
|
300
|
-
ask,
|
|
301
|
-
`学校标签(可多选,逗号/斜杠分隔;${SCHOOL_TAG_OPTIONS.join("/")},默认: ${current}): `,
|
|
302
|
-
(value) => parseSchoolSelection(value),
|
|
303
|
-
Array.isArray(args.schoolTag) && args.schoolTag.length > 0 ? args.schoolTag : ["不限"]
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
if (!args.__provided.gender) {
|
|
307
|
-
args.gender = await promptValue(
|
|
308
|
-
ask,
|
|
309
|
-
`性别(${GENDER_OPTIONS.join("/")},默认: ${args.gender}): `,
|
|
310
|
-
(value) => GENDER_OPTIONS.includes(value) ? value : null,
|
|
311
|
-
args.gender
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
if (!args.__provided.recentNotView) {
|
|
315
|
-
args.recentNotView = await promptValue(
|
|
316
|
-
ask,
|
|
317
|
-
`近14天已看过滤(${RECENT_NOT_VIEW_OPTIONS.join("/")},默认: ${args.recentNotView}): `,
|
|
318
|
-
(value) => RECENT_NOT_VIEW_OPTIONS.includes(value) ? value : null,
|
|
319
|
-
args.recentNotView
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
if (!args.__provided.degree || !Array.isArray(args.degree) || args.degree.length === 0) {
|
|
323
|
-
const current = Array.isArray(args.degree) && args.degree.length > 0 ? args.degree.join(",") : "不限";
|
|
324
|
-
args.degree = await promptValue(
|
|
325
|
-
ask,
|
|
326
|
-
`学历(可多选逗号分隔,支持“本科及以上”;默认: ${current}): `,
|
|
327
|
-
(value) => parseDegreeSelection(value),
|
|
328
|
-
Array.isArray(args.degree) && args.degree.length > 0 ? args.degree : ["不限"]
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
if (!args.__provided.port) {
|
|
332
|
-
args.port = await promptValue(
|
|
333
|
-
ask,
|
|
334
|
-
`Chrome 调试端口(默认: ${args.port}): `,
|
|
335
|
-
(value) => parsePositiveInteger(value),
|
|
336
|
-
args.port
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
return args;
|
|
340
|
-
} finally {
|
|
341
|
-
rl.close();
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function sleep(ms) {
|
|
346
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function humanDelay(baseMs, varianceMs) {
|
|
350
|
-
const u1 = Math.random();
|
|
351
|
-
const u2 = Math.random();
|
|
352
|
-
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
353
|
-
return Math.max(80, Math.round(baseMs + z * varianceMs));
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function generateBezierPath(start, end, steps = 18) {
|
|
357
|
-
const path = [];
|
|
358
|
-
const midX = (start.x + end.x) / 2 + (Math.random() - 0.5) * 80;
|
|
359
|
-
const midY = (start.y + end.y) / 2 + (Math.random() - 0.5) * 40;
|
|
360
|
-
for (let index = 0; index <= steps; index += 1) {
|
|
361
|
-
const t = index / steps;
|
|
362
|
-
const x = (1 - t) * (1 - t) * start.x + 2 * (1 - t) * t * midX + t * t * end.x;
|
|
363
|
-
const y = (1 - t) * (1 - t) * start.y + 2 * (1 - t) * t * midY + t * t * end.y;
|
|
364
|
-
path.push({ x, y });
|
|
365
|
-
}
|
|
366
|
-
return path;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
class RecommendSearchCli {
|
|
370
|
-
constructor(args) {
|
|
371
|
-
this.args = args;
|
|
372
|
-
this.client = null;
|
|
373
|
-
this.Runtime = null;
|
|
374
|
-
this.Input = null;
|
|
375
|
-
this.target = null;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
async connect() {
|
|
379
|
-
const targets = await CDP.List({ port: this.args.port });
|
|
380
|
-
this.target = targets.find(
|
|
381
|
-
(item) => typeof item?.url === "string" && item.url.includes(RECOMMEND_URL_FRAGMENT)
|
|
382
|
-
) || targets.find((item) => item?.type === "page");
|
|
383
|
-
|
|
384
|
-
if (!this.target) {
|
|
385
|
-
throw new Error("No debuggable recommend page target found");
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
this.client = await CDP({ port: this.args.port, target: this.target });
|
|
389
|
-
const { Runtime, Input, Page } = this.client;
|
|
390
|
-
this.Runtime = Runtime;
|
|
391
|
-
this.Input = Input;
|
|
392
|
-
await Runtime.enable();
|
|
393
|
-
await Page.enable();
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
async disconnect() {
|
|
397
|
-
if (this.client) {
|
|
398
|
-
try {
|
|
399
|
-
await this.client.close();
|
|
400
|
-
} catch {}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
async evaluate(expression) {
|
|
405
|
-
const result = await this.Runtime.evaluate({
|
|
406
|
-
expression,
|
|
407
|
-
returnByValue: true,
|
|
408
|
-
awaitPromise: true
|
|
409
|
-
});
|
|
410
|
-
if (result.exceptionDetails) {
|
|
411
|
-
throw new Error(result.exceptionDetails.exception?.description || "Runtime.evaluate failed");
|
|
412
|
-
}
|
|
413
|
-
return result.result?.value;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
async simulateHumanClick(targetX, targetY) {
|
|
417
|
-
const start = {
|
|
418
|
-
x: Math.round(Math.random() * 180 + 80),
|
|
419
|
-
y: Math.round(Math.random() * 160 + 80)
|
|
420
|
-
};
|
|
421
|
-
const path = generateBezierPath(start, { x: targetX, y: targetY });
|
|
422
|
-
for (const point of path) {
|
|
423
|
-
await this.Input.dispatchMouseEvent({
|
|
424
|
-
type: "mouseMoved",
|
|
425
|
-
x: Math.round(point.x + (Math.random() - 0.5) * 3),
|
|
426
|
-
y: Math.round(point.y + (Math.random() - 0.5) * 3)
|
|
427
|
-
});
|
|
428
|
-
await sleep(6 + Math.floor(Math.random() * 18));
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const hoverSteps = 2 + Math.floor(Math.random() * 3);
|
|
432
|
-
for (let index = 0; index < hoverSteps; index += 1) {
|
|
433
|
-
await this.Input.dispatchMouseEvent({
|
|
434
|
-
type: "mouseMoved",
|
|
435
|
-
x: Math.round(targetX + (Math.random() - 0.5) * 5),
|
|
436
|
-
y: Math.round(targetY + (Math.random() - 0.5) * 5)
|
|
437
|
-
});
|
|
438
|
-
await sleep(10 + Math.floor(Math.random() * 20));
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
await sleep(humanDelay(220, 60));
|
|
442
|
-
await this.Input.dispatchMouseEvent({
|
|
443
|
-
type: "mousePressed",
|
|
444
|
-
x: Math.round(targetX),
|
|
445
|
-
y: Math.round(targetY),
|
|
446
|
-
button: "left",
|
|
447
|
-
clickCount: 1
|
|
448
|
-
});
|
|
449
|
-
await sleep(30 + Math.floor(Math.random() * 30));
|
|
450
|
-
await this.Input.dispatchMouseEvent({
|
|
451
|
-
type: "mouseReleased",
|
|
452
|
-
x: Math.round(targetX),
|
|
453
|
-
y: Math.round(targetY),
|
|
454
|
-
button: "left",
|
|
455
|
-
clickCount: 1
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
async getFrameState() {
|
|
460
|
-
return this.evaluate(`(() => {
|
|
461
|
-
const currentUrl = (() => {
|
|
462
|
-
try { return String(window.location.href || ''); } catch { return ''; }
|
|
463
|
-
})();
|
|
464
|
-
const title = (() => {
|
|
465
|
-
try { return String(document.title || ''); } catch { return ''; }
|
|
466
|
-
})();
|
|
467
|
-
const isLogin = ${BOSS_LOGIN_URL_PATTERN}.test(currentUrl)
|
|
468
|
-
|| ${BOSS_LOGIN_TITLE_PATTERN}.test(title);
|
|
469
|
-
if (isLogin) {
|
|
470
|
-
return {
|
|
471
|
-
ok: false,
|
|
472
|
-
error: 'LOGIN_REQUIRED',
|
|
473
|
-
currentUrl: currentUrl || ${JSON.stringify(BOSS_LOGIN_URL)},
|
|
474
|
-
title
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
const frame = ${buildRecommendFrameExpression()};
|
|
478
|
-
if (!frame || !frame.contentDocument) {
|
|
479
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME', currentUrl, title };
|
|
480
|
-
}
|
|
481
|
-
return {
|
|
482
|
-
ok: true,
|
|
483
|
-
currentUrl,
|
|
484
|
-
title,
|
|
485
|
-
frameUrl: (() => {
|
|
486
|
-
try { return String(frame.contentWindow.location.href || ''); } catch { return ''; }
|
|
487
|
-
})()
|
|
488
|
-
};
|
|
489
|
-
})()`);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
async getFilterEntryPoint() {
|
|
493
|
-
return this.evaluate(`(() => {
|
|
494
|
-
const frame = ${buildRecommendFrameExpression()};
|
|
495
|
-
if (!frame || !frame.contentDocument) {
|
|
496
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
497
|
-
}
|
|
498
|
-
const doc = frame.contentDocument;
|
|
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;
|
|
504
|
-
if (!el) {
|
|
505
|
-
return { ok: false, error: 'FILTER_TRIGGER_NOT_FOUND' };
|
|
506
|
-
}
|
|
507
|
-
const frameRect = frame.getBoundingClientRect();
|
|
508
|
-
const rect = el.getBoundingClientRect();
|
|
509
|
-
return {
|
|
510
|
-
ok: true,
|
|
511
|
-
x: frameRect.left + rect.left + rect.width / 2,
|
|
512
|
-
y: frameRect.top + rect.top + rect.height / 2
|
|
513
|
-
};
|
|
514
|
-
})()`);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
async getJobListState() {
|
|
518
|
-
return this.evaluate(`(() => {
|
|
519
|
-
const frame = ${buildRecommendFrameExpression()};
|
|
520
|
-
if (!frame || !frame.contentDocument) {
|
|
521
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
522
|
-
}
|
|
523
|
-
const doc = frame.contentDocument;
|
|
524
|
-
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
525
|
-
const normalizeTitle = (value) => {
|
|
526
|
-
const text = normalize(value);
|
|
527
|
-
if (!text) return '';
|
|
528
|
-
const byGap = text.split(/\\s{2,}/).map((item) => item.trim()).filter(Boolean)[0] || text;
|
|
529
|
-
const strippedRange = byGap
|
|
530
|
-
.replace(/\\s+\\d+(?:\\.\\d+)?\\s*(?:-|~|—|至)\\s*\\d+(?:\\.\\d+)?\\s*(?:k|K|千|万|元\\/天|元\\/月|元\\/年|K\\/月|k\\/月|万\\/月|万\\/年)?$/u, '')
|
|
531
|
-
.trim();
|
|
532
|
-
const strippedSingle = strippedRange
|
|
533
|
-
.replace(/\\s+\\d+(?:\\.\\d+)?\\s*(?:k|K|千|万|元\\/天|元\\/月|元\\/年|K\\/月|k\\/月|万\\/月|万\\/年)$/u, '')
|
|
534
|
-
.trim();
|
|
535
|
-
return strippedSingle || byGap;
|
|
536
|
-
};
|
|
537
|
-
const isVisible = (el) => {
|
|
538
|
-
if (!el) return false;
|
|
539
|
-
const style = getComputedStyle(el);
|
|
540
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
541
|
-
return false;
|
|
542
|
-
}
|
|
543
|
-
const rect = el.getBoundingClientRect();
|
|
544
|
-
return rect.width > 2 && rect.height > 2;
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
const items = ${JSON.stringify(JOB_LIST_ITEM_SELECTORS)}
|
|
548
|
-
.flatMap((selector) => {
|
|
549
|
-
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
550
|
-
});
|
|
551
|
-
const jobs = [];
|
|
552
|
-
const seen = new Set();
|
|
553
|
-
for (const item of items) {
|
|
554
|
-
const label = normalize(item.querySelector('.label')?.textContent || item.textContent || '');
|
|
555
|
-
const title = normalizeTitle(label);
|
|
556
|
-
const value = normalize(item.getAttribute('value') || item.dataset?.value || '');
|
|
557
|
-
const dedupeKey = value || title || label;
|
|
558
|
-
if (!dedupeKey || seen.has(dedupeKey)) continue;
|
|
559
|
-
seen.add(dedupeKey);
|
|
560
|
-
jobs.push({
|
|
561
|
-
value: value || null,
|
|
562
|
-
title: title || label || null,
|
|
563
|
-
label: label || null,
|
|
564
|
-
current: item.classList.contains('curr') || item.classList.contains('active'),
|
|
565
|
-
visible: isVisible(item)
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
|
|
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;
|
|
574
|
-
return {
|
|
575
|
-
ok: true,
|
|
576
|
-
jobs,
|
|
577
|
-
selected_label: normalize(selectedLabelNode ? selectedLabelNode.textContent : ''),
|
|
578
|
-
frame_url: (() => {
|
|
579
|
-
try { return String(frame.contentWindow.location.href || ''); } catch { return ''; }
|
|
580
|
-
})()
|
|
581
|
-
};
|
|
582
|
-
})()`);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
async clickJobDropdownTriggerBySelector() {
|
|
586
|
-
return this.evaluate(`(() => {
|
|
587
|
-
const frame = ${buildRecommendFrameExpression()};
|
|
588
|
-
if (!frame || !frame.contentDocument) {
|
|
589
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
590
|
-
}
|
|
591
|
-
const doc = frame.contentDocument;
|
|
592
|
-
const selectors = ${JSON.stringify(JOB_DROPDOWN_TRIGGER_SELECTORS)};
|
|
593
|
-
const isVisible = (el) => {
|
|
594
|
-
if (!el) return false;
|
|
595
|
-
const style = getComputedStyle(el);
|
|
596
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
597
|
-
return false;
|
|
598
|
-
}
|
|
599
|
-
const rect = el.getBoundingClientRect();
|
|
600
|
-
return rect.width > 2 && rect.height > 2;
|
|
601
|
-
};
|
|
602
|
-
for (const selector of selectors) {
|
|
603
|
-
const el = doc.querySelector(selector);
|
|
604
|
-
if (el && isVisible(el)) {
|
|
605
|
-
el.click();
|
|
606
|
-
return { ok: true };
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
return { ok: false, error: 'JOB_TRIGGER_NOT_FOUND' };
|
|
610
|
-
})()`);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
async ensureJobListReady() {
|
|
614
|
-
let lastError = "JOB_LIST_NOT_FOUND";
|
|
615
|
-
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
616
|
-
const state = await this.getJobListState();
|
|
617
|
-
if (state?.ok && Array.isArray(state.jobs) && state.jobs.length > 0) {
|
|
618
|
-
return state;
|
|
619
|
-
}
|
|
620
|
-
lastError = state?.error || lastError;
|
|
621
|
-
const clickResult = await this.clickJobDropdownTriggerBySelector();
|
|
622
|
-
if (!clickResult?.ok) {
|
|
623
|
-
lastError = clickResult?.error || lastError;
|
|
624
|
-
}
|
|
625
|
-
await sleep(220 + attempt * 80);
|
|
626
|
-
}
|
|
627
|
-
throw new Error(lastError);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
findJobMatch(jobList, requestedJobRaw) {
|
|
631
|
-
const requested = normalizeText(requestedJobRaw);
|
|
632
|
-
if (!requested) return null;
|
|
633
|
-
const normalizedRequestedTitle = normalizeJobTitle(requested);
|
|
634
|
-
const normalize = (value) => normalizeText(value).toLowerCase();
|
|
635
|
-
const byValue = jobList.find((job) => normalize(job.value || "") === normalize(requested));
|
|
636
|
-
if (byValue) return byValue;
|
|
637
|
-
const exactTitle = jobList.find((job) => normalize(job.title || "") === normalize(normalizedRequestedTitle));
|
|
638
|
-
if (exactTitle) return exactTitle;
|
|
639
|
-
const exactLabel = jobList.find((job) => normalize(job.label || "") === normalize(requested));
|
|
640
|
-
if (exactLabel) return exactLabel;
|
|
641
|
-
const contains = jobList.filter((job) => {
|
|
642
|
-
const title = normalize(job.title || "");
|
|
643
|
-
const label = normalize(job.label || "");
|
|
644
|
-
const target = normalize(normalizedRequestedTitle);
|
|
645
|
-
return (
|
|
646
|
-
(title && (title.includes(target) || target.includes(title)))
|
|
647
|
-
|| (label && (label.includes(normalize(requested)) || normalize(requested).includes(label)))
|
|
648
|
-
);
|
|
649
|
-
});
|
|
650
|
-
if (contains.length === 1) return contains[0];
|
|
651
|
-
if (contains.length > 1) {
|
|
652
|
-
throw new Error("JOB_SELECTION_AMBIGUOUS");
|
|
653
|
-
}
|
|
654
|
-
return null;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
async clickJobBySelector(job) {
|
|
658
|
-
return this.evaluate(`((job) => {
|
|
659
|
-
const frame = ${buildRecommendFrameExpression()};
|
|
660
|
-
if (!frame || !frame.contentDocument) {
|
|
661
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
662
|
-
}
|
|
663
|
-
const doc = frame.contentDocument;
|
|
664
|
-
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
665
|
-
const normalizeTitle = (value) => {
|
|
666
|
-
const text = normalize(value);
|
|
667
|
-
if (!text) return '';
|
|
668
|
-
const byGap = text.split(/\\s{2,}/).map((item) => item.trim()).filter(Boolean)[0] || text;
|
|
669
|
-
const strippedRange = byGap
|
|
670
|
-
.replace(/\\s+\\d+(?:\\.\\d+)?\\s*(?:-|~|—|至)\\s*\\d+(?:\\.\\d+)?\\s*(?:k|K|千|万|元\\/天|元\\/月|元\\/年|K\\/月|k\\/月|万\\/月|万\\/年)?$/u, '')
|
|
671
|
-
.trim();
|
|
672
|
-
const strippedSingle = strippedRange
|
|
673
|
-
.replace(/\\s+\\d+(?:\\.\\d+)?\\s*(?:k|K|千|万|元\\/天|元\\/月|元\\/年|K\\/月|k\\/月|万\\/月|万\\/年)$/u, '')
|
|
674
|
-
.trim();
|
|
675
|
-
return strippedSingle || byGap;
|
|
676
|
-
};
|
|
677
|
-
const items = ${JSON.stringify(JOB_LIST_ITEM_SELECTORS)}
|
|
678
|
-
.flatMap((selector) => {
|
|
679
|
-
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
680
|
-
});
|
|
681
|
-
const target = items.find((item) => {
|
|
682
|
-
const value = normalize(item.getAttribute('value') || item.dataset?.value || '');
|
|
683
|
-
const label = normalize(item.querySelector('.label')?.textContent || item.textContent || '');
|
|
684
|
-
const title = normalizeTitle(label);
|
|
685
|
-
const matchValue = job.value && value && value === normalize(job.value);
|
|
686
|
-
const matchTitle = job.title && title && title === normalize(job.title);
|
|
687
|
-
const matchLabel = job.label && label && label === normalize(job.label);
|
|
688
|
-
return matchValue || matchTitle || matchLabel;
|
|
689
|
-
});
|
|
690
|
-
if (!target) {
|
|
691
|
-
return { ok: false, error: 'JOB_OPTION_NOT_FOUND' };
|
|
692
|
-
}
|
|
693
|
-
target.click();
|
|
694
|
-
return { ok: true };
|
|
695
|
-
})(${JSON.stringify(job)})`);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
async waitJobSelected(job, rounds = 8) {
|
|
699
|
-
const selectedValue = normalizeText(job.value || "");
|
|
700
|
-
const selectedTitle = normalizeText(job.title || "");
|
|
701
|
-
const selectedLabel = normalizeText(job.label || "");
|
|
702
|
-
for (let index = 0; index < rounds; index += 1) {
|
|
703
|
-
const state = await this.getJobListState();
|
|
704
|
-
if (state?.ok) {
|
|
705
|
-
const current = (state.jobs || []).find((item) => item.current);
|
|
706
|
-
if (current) {
|
|
707
|
-
const sameValue = selectedValue && normalizeText(current.value || "") === selectedValue;
|
|
708
|
-
const sameTitle = selectedTitle && normalizeText(current.title || "") === selectedTitle;
|
|
709
|
-
const sameLabel = selectedLabel && normalizeText(current.label || "") === selectedLabel;
|
|
710
|
-
if (sameValue || sameTitle || sameLabel) return true;
|
|
711
|
-
}
|
|
712
|
-
const selectedText = normalizeText(state.selected_label || "");
|
|
713
|
-
if (selectedTitle && selectedText && (selectedText === selectedTitle || selectedText.includes(selectedTitle))) {
|
|
714
|
-
return true;
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
await sleep(150 + index * 40);
|
|
718
|
-
}
|
|
719
|
-
return false;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
async selectJob(jobSelection) {
|
|
723
|
-
const state = await this.ensureJobListReady();
|
|
724
|
-
const matched = this.findJobMatch(state.jobs || [], jobSelection);
|
|
725
|
-
if (!matched) {
|
|
726
|
-
throw new Error("JOB_OPTION_NOT_FOUND");
|
|
727
|
-
}
|
|
728
|
-
const clicked = await this.clickJobBySelector(matched);
|
|
729
|
-
if (!clicked?.ok) {
|
|
730
|
-
throw new Error(clicked?.error || "JOB_SELECT_FAILED");
|
|
731
|
-
}
|
|
732
|
-
const selected = await this.waitJobSelected(matched, 10);
|
|
733
|
-
if (!selected) {
|
|
734
|
-
throw new Error("JOB_SELECTION_NOT_APPLIED");
|
|
735
|
-
}
|
|
736
|
-
return matched;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
async isFilterPanelVisible() {
|
|
740
|
-
const result = await this.evaluate(`(() => {
|
|
741
|
-
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
742
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
743
|
-
|| document.querySelector('iframe');
|
|
744
|
-
if (!frame || !frame.contentDocument) return false;
|
|
745
|
-
const doc = frame.contentDocument;
|
|
746
|
-
const panel = doc.querySelector('.recommend-filter.op-filter .filter-panel');
|
|
747
|
-
const isVisible = (el) => {
|
|
748
|
-
if (!el) return false;
|
|
749
|
-
const style = getComputedStyle(el);
|
|
750
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
751
|
-
return false;
|
|
752
|
-
}
|
|
753
|
-
const rect = el.getBoundingClientRect();
|
|
754
|
-
return rect.width > 2 && rect.height > 2;
|
|
755
|
-
};
|
|
756
|
-
const groups = Array.from(doc.querySelectorAll('.check-box'));
|
|
757
|
-
const visibleGroups = groups.filter((group) => isVisible(group));
|
|
758
|
-
if (visibleGroups.length >= 2) return true;
|
|
759
|
-
return Boolean(isVisible(panel) && visibleGroups.length >= 1);
|
|
760
|
-
})()`);
|
|
761
|
-
return result === true;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
async clickFilterEntryBySelector() {
|
|
765
|
-
return this.evaluate(`(() => {
|
|
766
|
-
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
767
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
768
|
-
|| document.querySelector('iframe');
|
|
769
|
-
if (!frame || !frame.contentDocument) {
|
|
770
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
771
|
-
}
|
|
772
|
-
const doc = frame.contentDocument;
|
|
773
|
-
const entry = doc.querySelector('.filter-label-wrap') || doc.querySelector('.recommend-filter.op-filter');
|
|
774
|
-
if (!entry) {
|
|
775
|
-
return { ok: false, error: 'FILTER_TRIGGER_NOT_FOUND' };
|
|
776
|
-
}
|
|
777
|
-
entry.click();
|
|
778
|
-
return { ok: true };
|
|
779
|
-
})()`);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
async getFilterConfirmButton() {
|
|
783
|
-
return this.evaluate(`(() => {
|
|
784
|
-
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
785
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
786
|
-
|| document.querySelector('iframe');
|
|
787
|
-
if (!frame || !frame.contentDocument) {
|
|
788
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
789
|
-
}
|
|
790
|
-
const doc = frame.contentDocument;
|
|
791
|
-
const panel = doc.querySelector('.recommend-filter.op-filter .filter-panel');
|
|
792
|
-
if (!panel) {
|
|
793
|
-
return { ok: false, error: 'FILTER_PANEL_NOT_FOUND' };
|
|
794
|
-
}
|
|
795
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, '').trim();
|
|
796
|
-
const isVisible = (el) => {
|
|
797
|
-
if (!el) return false;
|
|
798
|
-
const style = getComputedStyle(el);
|
|
799
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
800
|
-
return false;
|
|
801
|
-
}
|
|
802
|
-
const rect = el.getBoundingClientRect();
|
|
803
|
-
return rect.width > 2 && rect.height > 2;
|
|
804
|
-
};
|
|
805
|
-
const button = Array.from(panel.querySelectorAll('.btn, button')).find((el) => {
|
|
806
|
-
return normalize(el.textContent) === '确定' && isVisible(el);
|
|
807
|
-
});
|
|
808
|
-
if (!button) {
|
|
809
|
-
return { ok: false, error: 'FILTER_CONFIRM_BUTTON_NOT_FOUND' };
|
|
810
|
-
}
|
|
811
|
-
const frameRect = frame.getBoundingClientRect();
|
|
812
|
-
const rect = button.getBoundingClientRect();
|
|
813
|
-
return {
|
|
814
|
-
ok: true,
|
|
815
|
-
x: frameRect.left + rect.left + rect.width / 2,
|
|
816
|
-
y: frameRect.top + rect.top + rect.height / 2
|
|
817
|
-
};
|
|
818
|
-
})()`);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
async clickFilterConfirmBySelector() {
|
|
822
|
-
return this.evaluate(`(() => {
|
|
823
|
-
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
824
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
825
|
-
|| document.querySelector('iframe');
|
|
826
|
-
if (!frame || !frame.contentDocument) {
|
|
827
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
828
|
-
}
|
|
829
|
-
const doc = frame.contentDocument;
|
|
830
|
-
const panel = doc.querySelector('.recommend-filter.op-filter .filter-panel');
|
|
831
|
-
if (!panel) {
|
|
832
|
-
return { ok: false, error: 'FILTER_PANEL_NOT_FOUND' };
|
|
833
|
-
}
|
|
834
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, '').trim();
|
|
835
|
-
const button = Array.from(panel.querySelectorAll('.btn, button')).find((el) => {
|
|
836
|
-
return normalize(el.textContent) === '确定';
|
|
837
|
-
});
|
|
838
|
-
if (!button) {
|
|
839
|
-
return { ok: false, error: 'FILTER_CONFIRM_BUTTON_NOT_FOUND' };
|
|
840
|
-
}
|
|
841
|
-
button.click();
|
|
842
|
-
return { ok: true };
|
|
843
|
-
})()`);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
async openFilterPanel() {
|
|
847
|
-
if (await this.isFilterPanelVisible()) return;
|
|
848
|
-
let lastError = 'FILTER_PANEL_UNAVAILABLE';
|
|
849
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
850
|
-
const point = await this.getFilterEntryPoint();
|
|
851
|
-
if (point?.ok) {
|
|
852
|
-
await this.simulateHumanClick(point.x, point.y);
|
|
853
|
-
} else {
|
|
854
|
-
lastError = point?.error || lastError;
|
|
855
|
-
}
|
|
856
|
-
for (let index = 0; index < 8; index += 1) {
|
|
857
|
-
await sleep(140 + index * 40);
|
|
858
|
-
if (await this.isFilterPanelVisible()) {
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
const fallback = await this.clickFilterEntryBySelector();
|
|
864
|
-
if (fallback?.ok) {
|
|
865
|
-
for (let index = 0; index < 8; index += 1) {
|
|
866
|
-
await sleep(140 + index * 40);
|
|
867
|
-
if (await this.isFilterPanelVisible()) {
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
} else {
|
|
872
|
-
lastError = fallback?.error || lastError;
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
throw new Error(lastError === 'FILTER_TRIGGER_NOT_FOUND' ? lastError : 'FILTER_PANEL_UNAVAILABLE');
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
async closeFilterPanel() {
|
|
879
|
-
if (!(await this.isFilterPanelVisible())) {
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
const selectorClickResult = await this.clickFilterConfirmBySelector();
|
|
884
|
-
if (selectorClickResult?.ok) {
|
|
885
|
-
for (let index = 0; index < 10; index += 1) {
|
|
886
|
-
await sleep(140 + index * 40);
|
|
887
|
-
if (!(await this.isFilterPanelVisible())) {
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
const point = await this.getFilterConfirmButton();
|
|
894
|
-
if (point?.ok) {
|
|
895
|
-
await this.simulateHumanClick(point.x, point.y);
|
|
896
|
-
for (let index = 0; index < 10; index += 1) {
|
|
897
|
-
await sleep(140 + index * 40);
|
|
898
|
-
if (!(await this.isFilterPanelVisible())) {
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
throw new Error('FILTER_CONFIRM_FAILED');
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
async getOptionInfo(groupClass, label) {
|
|
908
|
-
return this.evaluate(`((groupClass, label) => {
|
|
909
|
-
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
910
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
911
|
-
|| document.querySelector('iframe');
|
|
912
|
-
if (!frame || !frame.contentDocument) {
|
|
913
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
914
|
-
}
|
|
915
|
-
const doc = frame.contentDocument;
|
|
916
|
-
const normalize = (value) => String(value || '').replace(/\\s+/g, '').trim();
|
|
917
|
-
const groupCandidates = Array.from(doc.querySelectorAll('.check-box'));
|
|
918
|
-
const getOptionSet = (group) => new Set(
|
|
919
|
-
Array.from(group.querySelectorAll('.default.option, .options .option, .option'))
|
|
920
|
-
.map((item) => normalize(item.textContent))
|
|
921
|
-
.filter(Boolean)
|
|
922
|
-
);
|
|
923
|
-
const findGroup = () => {
|
|
924
|
-
const direct = doc.querySelector('.check-box.' + groupClass);
|
|
925
|
-
if (direct) return direct;
|
|
926
|
-
if (groupClass === 'school') {
|
|
927
|
-
return groupCandidates.find((group) => {
|
|
928
|
-
const set = getOptionSet(group);
|
|
929
|
-
return set.has('985') || set.has('211') || set.has('双一流院校');
|
|
930
|
-
}) || null;
|
|
931
|
-
}
|
|
932
|
-
if (groupClass === 'degree') {
|
|
933
|
-
return groupCandidates.find((group) => {
|
|
934
|
-
const set = getOptionSet(group);
|
|
935
|
-
return set.has('大专') || set.has('本科') || set.has('硕士') || set.has('博士');
|
|
936
|
-
}) || null;
|
|
937
|
-
}
|
|
938
|
-
if (groupClass === 'gender') {
|
|
939
|
-
return groupCandidates.find((group) => {
|
|
940
|
-
const set = getOptionSet(group);
|
|
941
|
-
return set.has('男') || set.has('女');
|
|
942
|
-
}) || null;
|
|
943
|
-
}
|
|
944
|
-
if (groupClass === 'recentNotView') {
|
|
945
|
-
return groupCandidates.find((group) => {
|
|
946
|
-
const set = getOptionSet(group);
|
|
947
|
-
return set.has('近14天没有');
|
|
948
|
-
}) || null;
|
|
949
|
-
}
|
|
950
|
-
return null;
|
|
951
|
-
};
|
|
952
|
-
const group = findGroup();
|
|
953
|
-
if (!group) {
|
|
954
|
-
return { ok: false, error: 'GROUP_NOT_FOUND' };
|
|
955
|
-
}
|
|
956
|
-
const frameRect = frame.getBoundingClientRect();
|
|
957
|
-
const getPoint = (el) => {
|
|
958
|
-
const rect = el.getBoundingClientRect();
|
|
959
|
-
return {
|
|
960
|
-
x: frameRect.left + rect.left + rect.width / 2,
|
|
961
|
-
y: frameRect.top + rect.top + rect.height / 2
|
|
962
|
-
};
|
|
963
|
-
};
|
|
964
|
-
const options = Array.from(group.querySelectorAll('.options .option, .option'));
|
|
965
|
-
const active = group.querySelector('.default.option.active, .options .option.active, .option.active');
|
|
966
|
-
const activeText = normalize(active ? active.textContent : '');
|
|
967
|
-
const target = label === '不限'
|
|
968
|
-
? (group.querySelector('.default.option') || options.find((item) => normalize(item.textContent) === '不限'))
|
|
969
|
-
: options.find((item) => normalize(item.textContent) === normalize(label));
|
|
970
|
-
if (!target) {
|
|
971
|
-
return { ok: false, error: 'OPTION_NOT_FOUND', activeText };
|
|
972
|
-
}
|
|
973
|
-
const targetActive = target.classList.contains('active');
|
|
974
|
-
return {
|
|
975
|
-
ok: true,
|
|
976
|
-
activeText,
|
|
977
|
-
alreadySelected: targetActive || activeText === normalize(label),
|
|
978
|
-
x: getPoint(target).x,
|
|
979
|
-
y: getPoint(target).y
|
|
980
|
-
};
|
|
981
|
-
})(${JSON.stringify(groupClass)}, ${JSON.stringify(label)})`);
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
async ensureGroupReady(groupClass) {
|
|
985
|
-
return this.evaluate(`((groupClass) => {
|
|
986
|
-
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
987
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
988
|
-
|| document.querySelector('iframe');
|
|
989
|
-
if (!frame || !frame.contentDocument) {
|
|
990
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
991
|
-
}
|
|
992
|
-
const doc = frame.contentDocument;
|
|
993
|
-
const normalize = (value) => String(value || '').replace(/\\s+/g, '').trim();
|
|
994
|
-
const groupCandidates = Array.from(doc.querySelectorAll('.check-box'));
|
|
995
|
-
const getOptionSet = (group) => new Set(
|
|
996
|
-
Array.from(group.querySelectorAll('.default.option, .options .option, .option'))
|
|
997
|
-
.map((item) => normalize(item.textContent))
|
|
998
|
-
.filter(Boolean)
|
|
999
|
-
);
|
|
1000
|
-
const findGroup = () => {
|
|
1001
|
-
const direct = doc.querySelector('.check-box.' + groupClass);
|
|
1002
|
-
if (direct) return direct;
|
|
1003
|
-
if (groupClass === 'school') {
|
|
1004
|
-
return groupCandidates.find((group) => {
|
|
1005
|
-
const set = getOptionSet(group);
|
|
1006
|
-
return set.has('985') || set.has('211') || set.has('双一流院校');
|
|
1007
|
-
}) || null;
|
|
1008
|
-
}
|
|
1009
|
-
if (groupClass === 'degree') {
|
|
1010
|
-
return groupCandidates.find((group) => {
|
|
1011
|
-
const set = getOptionSet(group);
|
|
1012
|
-
return set.has('大专') || set.has('本科') || set.has('硕士') || set.has('博士');
|
|
1013
|
-
}) || null;
|
|
1014
|
-
}
|
|
1015
|
-
if (groupClass === 'gender') {
|
|
1016
|
-
return groupCandidates.find((group) => {
|
|
1017
|
-
const set = getOptionSet(group);
|
|
1018
|
-
return set.has('男') || set.has('女');
|
|
1019
|
-
}) || null;
|
|
1020
|
-
}
|
|
1021
|
-
if (groupClass === 'recentNotView') {
|
|
1022
|
-
return groupCandidates.find((group) => {
|
|
1023
|
-
const set = getOptionSet(group);
|
|
1024
|
-
return set.has('近14天没有');
|
|
1025
|
-
}) || null;
|
|
1026
|
-
}
|
|
1027
|
-
return null;
|
|
1028
|
-
};
|
|
1029
|
-
|
|
1030
|
-
const scrollGroupIntoView = (group) => {
|
|
1031
|
-
try {
|
|
1032
|
-
group.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
1033
|
-
} catch {
|
|
1034
|
-
try { group.scrollIntoView({ block: 'center' }); } catch {}
|
|
1035
|
-
}
|
|
1036
|
-
};
|
|
1037
|
-
|
|
1038
|
-
let group = findGroup();
|
|
1039
|
-
if (group) {
|
|
1040
|
-
scrollGroupIntoView(group);
|
|
1041
|
-
return { ok: true, found: true, scrolled: false };
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
const topScroller = doc.querySelector('.recommend-filter.op-filter .filter-panel .top')
|
|
1045
|
-
|| doc.querySelector('.recommend-filter.op-filter .top')
|
|
1046
|
-
|| doc.querySelector('.recommend-filter.op-filter .filter-panel');
|
|
1047
|
-
if (!topScroller) {
|
|
1048
|
-
return { ok: false, error: 'FILTER_SCROLL_CONTAINER_NOT_FOUND' };
|
|
1049
|
-
}
|
|
1050
|
-
const maxScrollTop = Math.max(0, topScroller.scrollHeight - topScroller.clientHeight);
|
|
1051
|
-
const steps = 14;
|
|
1052
|
-
for (let index = 0; index <= steps; index += 1) {
|
|
1053
|
-
const nextTop = maxScrollTop <= 0 ? 0 : Math.round((maxScrollTop * index) / steps);
|
|
1054
|
-
topScroller.scrollTop = nextTop;
|
|
1055
|
-
group = findGroup();
|
|
1056
|
-
if (group) {
|
|
1057
|
-
scrollGroupIntoView(group);
|
|
1058
|
-
return { ok: true, found: true, scrolled: true, step: index };
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
return { ok: false, error: 'GROUP_NOT_FOUND' };
|
|
1062
|
-
})(${JSON.stringify(groupClass)})`);
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
async selectOption(groupClass, label) {
|
|
1066
|
-
let option = await this.getOptionInfo(groupClass, label);
|
|
1067
|
-
if (!option?.ok && option?.error === "GROUP_NOT_FOUND") {
|
|
1068
|
-
await this.openFilterPanel();
|
|
1069
|
-
const ensure = await this.ensureGroupReady(groupClass);
|
|
1070
|
-
if (!ensure?.ok) {
|
|
1071
|
-
throw new Error(ensure?.error || "GROUP_NOT_FOUND");
|
|
1072
|
-
}
|
|
1073
|
-
await sleep(humanDelay(180, 60));
|
|
1074
|
-
option = await this.getOptionInfo(groupClass, label);
|
|
1075
|
-
}
|
|
1076
|
-
if (!option?.ok) {
|
|
1077
|
-
throw new Error(option?.error || 'OPTION_NOT_FOUND');
|
|
1078
|
-
}
|
|
1079
|
-
if (option.alreadySelected) {
|
|
1080
|
-
return;
|
|
1081
|
-
}
|
|
1082
|
-
const domClick = await this.clickOptionBySelector(groupClass, label);
|
|
1083
|
-
if (!domClick?.ok) {
|
|
1084
|
-
throw new Error(domClick?.error || "OPTION_DOM_CLICK_FAILED");
|
|
1085
|
-
}
|
|
1086
|
-
if (await this.waitOptionSelected(groupClass, label, 10)) {
|
|
1087
|
-
return;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
await this.simulateHumanClick(option.x, option.y);
|
|
1091
|
-
if (!(await this.waitOptionSelected(groupClass, label, 10))) {
|
|
1092
|
-
throw new Error("OPTION_SELECTION_NOT_APPLIED");
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
async clickOptionBySelector(groupClass, label) {
|
|
1097
|
-
return this.evaluate(`((groupClass, label) => {
|
|
1098
|
-
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
1099
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1100
|
-
|| document.querySelector('iframe');
|
|
1101
|
-
if (!frame || !frame.contentDocument) {
|
|
1102
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1103
|
-
}
|
|
1104
|
-
const doc = frame.contentDocument;
|
|
1105
|
-
const normalize = (value) => String(value || '').replace(/\\s+/g, '').trim();
|
|
1106
|
-
const groupCandidates = Array.from(doc.querySelectorAll('.check-box'));
|
|
1107
|
-
const getOptionSet = (group) => new Set(
|
|
1108
|
-
Array.from(group.querySelectorAll('.default.option, .options .option, .option'))
|
|
1109
|
-
.map((item) => normalize(item.textContent))
|
|
1110
|
-
.filter(Boolean)
|
|
1111
|
-
);
|
|
1112
|
-
const findGroup = () => {
|
|
1113
|
-
const direct = doc.querySelector('.check-box.' + groupClass);
|
|
1114
|
-
if (direct) return direct;
|
|
1115
|
-
if (groupClass === 'school') {
|
|
1116
|
-
return groupCandidates.find((group) => {
|
|
1117
|
-
const set = getOptionSet(group);
|
|
1118
|
-
return set.has('985') || set.has('211') || set.has('双一流院校');
|
|
1119
|
-
}) || null;
|
|
1120
|
-
}
|
|
1121
|
-
if (groupClass === 'degree') {
|
|
1122
|
-
return groupCandidates.find((group) => {
|
|
1123
|
-
const set = getOptionSet(group);
|
|
1124
|
-
return set.has('大专') || set.has('本科') || set.has('硕士') || set.has('博士');
|
|
1125
|
-
}) || null;
|
|
1126
|
-
}
|
|
1127
|
-
if (groupClass === 'gender') {
|
|
1128
|
-
return groupCandidates.find((group) => {
|
|
1129
|
-
const set = getOptionSet(group);
|
|
1130
|
-
return set.has('男') || set.has('女');
|
|
1131
|
-
}) || null;
|
|
1132
|
-
}
|
|
1133
|
-
if (groupClass === 'recentNotView') {
|
|
1134
|
-
return groupCandidates.find((group) => {
|
|
1135
|
-
const set = getOptionSet(group);
|
|
1136
|
-
return set.has('近14天没有');
|
|
1137
|
-
}) || null;
|
|
1138
|
-
}
|
|
1139
|
-
return null;
|
|
1140
|
-
};
|
|
1141
|
-
const group = findGroup();
|
|
1142
|
-
if (!group) {
|
|
1143
|
-
return { ok: false, error: 'GROUP_NOT_FOUND' };
|
|
1144
|
-
}
|
|
1145
|
-
const options = Array.from(group.querySelectorAll('.options .option, .option'));
|
|
1146
|
-
const target = label === '不限'
|
|
1147
|
-
? (group.querySelector('.default.option') || options.find((item) => normalize(item.textContent) === '不限'))
|
|
1148
|
-
: options.find((item) => normalize(item.textContent) === normalize(label));
|
|
1149
|
-
if (!target) {
|
|
1150
|
-
return { ok: false, error: 'OPTION_NOT_FOUND' };
|
|
1151
|
-
}
|
|
1152
|
-
target.click();
|
|
1153
|
-
return { ok: true };
|
|
1154
|
-
})(${JSON.stringify(groupClass)}, ${JSON.stringify(label)})`);
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
async waitOptionSelected(groupClass, label, rounds = 8) {
|
|
1158
|
-
for (let index = 0; index < rounds; index += 1) {
|
|
1159
|
-
const state = await this.getOptionInfo(groupClass, label);
|
|
1160
|
-
if (state?.ok && state.alreadySelected) {
|
|
1161
|
-
return true;
|
|
1162
|
-
}
|
|
1163
|
-
await sleep(120 + index * 40);
|
|
1164
|
-
}
|
|
1165
|
-
return false;
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
async getFilterGroupState(groupClass) {
|
|
1169
|
-
return this.evaluate(`((groupClass) => {
|
|
1170
|
-
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
1171
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1172
|
-
|| document.querySelector('iframe');
|
|
1173
|
-
if (!frame || !frame.contentDocument) {
|
|
1174
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1175
|
-
}
|
|
1176
|
-
const doc = frame.contentDocument;
|
|
1177
|
-
const normalize = (value) => String(value || '').replace(/\\s+/g, '').trim();
|
|
1178
|
-
const groupCandidates = Array.from(doc.querySelectorAll('.check-box'));
|
|
1179
|
-
const getOptionSet = (group) => new Set(
|
|
1180
|
-
Array.from(group.querySelectorAll('.default.option, .options .option, .option'))
|
|
1181
|
-
.map((item) => normalize(item.textContent))
|
|
1182
|
-
.filter(Boolean)
|
|
1183
|
-
);
|
|
1184
|
-
const findGroup = () => {
|
|
1185
|
-
const direct = doc.querySelector('.check-box.' + groupClass);
|
|
1186
|
-
if (direct) return direct;
|
|
1187
|
-
if (groupClass === 'school') {
|
|
1188
|
-
return groupCandidates.find((group) => {
|
|
1189
|
-
const set = getOptionSet(group);
|
|
1190
|
-
return set.has('985') || set.has('211') || set.has('双一流院校');
|
|
1191
|
-
}) || null;
|
|
1192
|
-
}
|
|
1193
|
-
if (groupClass === 'degree') {
|
|
1194
|
-
return groupCandidates.find((group) => {
|
|
1195
|
-
const set = getOptionSet(group);
|
|
1196
|
-
return set.has('大专') || set.has('本科') || set.has('硕士') || set.has('博士');
|
|
1197
|
-
}) || null;
|
|
1198
|
-
}
|
|
1199
|
-
if (groupClass === 'gender') {
|
|
1200
|
-
return groupCandidates.find((group) => {
|
|
1201
|
-
const set = getOptionSet(group);
|
|
1202
|
-
return set.has('男') || set.has('女');
|
|
1203
|
-
}) || null;
|
|
1204
|
-
}
|
|
1205
|
-
if (groupClass === 'recentNotView') {
|
|
1206
|
-
return groupCandidates.find((group) => {
|
|
1207
|
-
const set = getOptionSet(group);
|
|
1208
|
-
return set.has('近14天没有');
|
|
1209
|
-
}) || null;
|
|
1210
|
-
}
|
|
1211
|
-
return null;
|
|
1212
|
-
};
|
|
1213
|
-
|
|
1214
|
-
const group = findGroup();
|
|
1215
|
-
if (!group) {
|
|
1216
|
-
return { ok: false, error: 'GROUP_NOT_FOUND' };
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
const defaultOption = group.querySelector('.default.option');
|
|
1220
|
-
const options = Array.from(group.querySelectorAll('.default.option, .options .option, .option'));
|
|
1221
|
-
const byLabel = new Map();
|
|
1222
|
-
for (const node of options) {
|
|
1223
|
-
const label = normalize(node.textContent);
|
|
1224
|
-
if (!label) continue;
|
|
1225
|
-
const className = String(node.className || '').trim();
|
|
1226
|
-
const active = node.classList.contains('active');
|
|
1227
|
-
const existing = byLabel.get(label);
|
|
1228
|
-
if (existing) {
|
|
1229
|
-
existing.active = existing.active || active;
|
|
1230
|
-
if (className && !existing.classNames.includes(className)) {
|
|
1231
|
-
existing.classNames.push(className);
|
|
1232
|
-
}
|
|
1233
|
-
} else {
|
|
1234
|
-
byLabel.set(label, {
|
|
1235
|
-
label,
|
|
1236
|
-
active,
|
|
1237
|
-
classNames: className ? [className] : []
|
|
1238
|
-
});
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
const normalizedOptions = Array.from(byLabel.values()).map((item) => ({
|
|
1243
|
-
label: item.label,
|
|
1244
|
-
active: item.active,
|
|
1245
|
-
class_name: item.classNames.join(' | ')
|
|
1246
|
-
}));
|
|
1247
|
-
return {
|
|
1248
|
-
ok: true,
|
|
1249
|
-
group_class: groupClass,
|
|
1250
|
-
defaultActive: Boolean(defaultOption && defaultOption.classList.contains('active')),
|
|
1251
|
-
defaultClassName: defaultOption ? String(defaultOption.className || '').trim() : '',
|
|
1252
|
-
options: normalizedOptions,
|
|
1253
|
-
activeLabels: normalizedOptions.filter((item) => item.active).map((item) => item.label)
|
|
1254
|
-
};
|
|
1255
|
-
})(${JSON.stringify(groupClass)})`);
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
async getSchoolFilterState() {
|
|
1259
|
-
return this.getFilterGroupState("school");
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
async selectSchoolFilter(labels) {
|
|
1263
|
-
const ensure = await this.ensureGroupReady("school");
|
|
1264
|
-
if (!ensure?.ok) {
|
|
1265
|
-
throw new Error(ensure?.error || "GROUP_NOT_FOUND");
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
const targetLabels = Array.isArray(labels) && labels.length > 0 ? labels : ["不限"];
|
|
1269
|
-
const desired = sortSchoolSelection(targetLabels);
|
|
1270
|
-
const expectDefaultOnly = desired.includes("不限");
|
|
1271
|
-
let lastState = null;
|
|
1272
|
-
|
|
1273
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1274
|
-
const state = await this.getSchoolFilterState();
|
|
1275
|
-
if (!state?.ok) {
|
|
1276
|
-
throw new Error(state?.error || "SCHOOL_FILTER_STATE_FAILED");
|
|
1277
|
-
}
|
|
1278
|
-
lastState = state;
|
|
1279
|
-
const current = sortSchoolSelection(state.activeLabels || []);
|
|
1280
|
-
const matched = expectDefaultOnly
|
|
1281
|
-
? Boolean(state.defaultActive)
|
|
1282
|
-
: (!state.defaultActive && selectionEquals(current, desired));
|
|
1283
|
-
if (matched) {
|
|
1284
|
-
return;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
if (expectDefaultOnly) {
|
|
1288
|
-
await this.selectOption("school", "不限");
|
|
1289
|
-
await sleep(humanDelay(180, 50));
|
|
1290
|
-
continue;
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
if (state.defaultActive) {
|
|
1294
|
-
const clearDefault = await this.clickOptionBySelector("school", "不限");
|
|
1295
|
-
if (!clearDefault?.ok) {
|
|
1296
|
-
throw new Error(clearDefault?.error || "SCHOOL_DEFAULT_CLEAR_FAILED");
|
|
1297
|
-
}
|
|
1298
|
-
await sleep(humanDelay(180, 50));
|
|
1299
|
-
}
|
|
1300
|
-
for (const label of desired) {
|
|
1301
|
-
await this.selectOption("school", label);
|
|
1302
|
-
await sleep(humanDelay(120, 40));
|
|
1303
|
-
}
|
|
1304
|
-
await sleep(humanDelay(180, 50));
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
throw new Error(`SCHOOL_FILTER_VERIFY_FAILED:${JSON.stringify(lastState || {})}`);
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
async getDegreeFilterState() {
|
|
1311
|
-
return this.getFilterGroupState("degree");
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
async getGenderFilterState() {
|
|
1315
|
-
return this.getFilterGroupState("gender");
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
async getRecentNotViewFilterState() {
|
|
1319
|
-
return this.getFilterGroupState("recentNotView");
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
async selectDegreeFilter(labels) {
|
|
1323
|
-
const ensure = await this.ensureGroupReady("degree");
|
|
1324
|
-
if (!ensure?.ok) {
|
|
1325
|
-
throw new Error(ensure?.error || "GROUP_NOT_FOUND");
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
const targetLabels = Array.isArray(labels) && labels.length > 0 ? labels : ["不限"];
|
|
1329
|
-
const desired = sortDegreeSelection(targetLabels);
|
|
1330
|
-
const expectDefaultOnly = desired.includes("不限");
|
|
1331
|
-
let lastState = null;
|
|
1332
|
-
|
|
1333
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1334
|
-
const state = await this.getDegreeFilterState();
|
|
1335
|
-
if (!state?.ok) {
|
|
1336
|
-
throw new Error(state?.error || "DEGREE_FILTER_STATE_FAILED");
|
|
1337
|
-
}
|
|
1338
|
-
lastState = state;
|
|
1339
|
-
const current = sortDegreeSelection(state.activeLabels || []);
|
|
1340
|
-
const matched = expectDefaultOnly
|
|
1341
|
-
? Boolean(state.defaultActive)
|
|
1342
|
-
: (!state.defaultActive && selectionEquals(current, desired));
|
|
1343
|
-
if (matched) {
|
|
1344
|
-
return;
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
if (expectDefaultOnly) {
|
|
1348
|
-
await this.selectOption("degree", "不限");
|
|
1349
|
-
await sleep(humanDelay(180, 50));
|
|
1350
|
-
continue;
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
if (state.defaultActive) {
|
|
1354
|
-
const clearDefault = await this.clickOptionBySelector("degree", "不限");
|
|
1355
|
-
if (!clearDefault?.ok) {
|
|
1356
|
-
throw new Error(clearDefault?.error || "DEGREE_DEFAULT_CLEAR_FAILED");
|
|
1357
|
-
}
|
|
1358
|
-
await sleep(humanDelay(180, 50));
|
|
1359
|
-
}
|
|
1360
|
-
for (const label of desired) {
|
|
1361
|
-
await this.selectOption("degree", label);
|
|
1362
|
-
await sleep(humanDelay(120, 40));
|
|
1363
|
-
}
|
|
1364
|
-
await sleep(humanDelay(180, 50));
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
throw new Error(`DEGREE_FILTER_VERIFY_FAILED:${JSON.stringify(lastState || {})}`);
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
buildGroupClassVerification(groupName, state, expectedLabels, availableOptions, sortFn) {
|
|
1371
|
-
if (!state?.ok) {
|
|
1372
|
-
return {
|
|
1373
|
-
group: groupName,
|
|
1374
|
-
ok: false,
|
|
1375
|
-
reason: state?.error || "GROUP_STATE_UNAVAILABLE",
|
|
1376
|
-
expected_labels: expectedLabels,
|
|
1377
|
-
state: state || null
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
const expectedSorted = sortFn(uniqueNormalizedLabels(expectedLabels));
|
|
1382
|
-
const expectedSet = new Set(expectedSorted);
|
|
1383
|
-
const allowedSet = new Set(uniqueNormalizedLabels(availableOptions));
|
|
1384
|
-
const optionMap = new Map();
|
|
1385
|
-
for (const option of state.options || []) {
|
|
1386
|
-
optionMap.set(normalizeText(option.label), option);
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
const selectedNotActive = [];
|
|
1390
|
-
const unselectedButActive = [];
|
|
1391
|
-
for (const label of expectedSorted) {
|
|
1392
|
-
const option = optionMap.get(label);
|
|
1393
|
-
if (!option || option.active !== true) {
|
|
1394
|
-
selectedNotActive.push(label);
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
for (const label of allowedSet) {
|
|
1398
|
-
if (expectedSet.has(label)) continue;
|
|
1399
|
-
const option = optionMap.get(label);
|
|
1400
|
-
if (option?.active === true) {
|
|
1401
|
-
unselectedButActive.push(label);
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
const expectDefault = expectedSet.has("不限");
|
|
1406
|
-
const defaultMismatch = expectDefault ? !state.defaultActive : Boolean(state.defaultActive);
|
|
1407
|
-
const ok = (
|
|
1408
|
-
selectedNotActive.length === 0
|
|
1409
|
-
&& unselectedButActive.length === 0
|
|
1410
|
-
&& !defaultMismatch
|
|
1411
|
-
);
|
|
1412
|
-
|
|
1413
|
-
return {
|
|
1414
|
-
group: groupName,
|
|
1415
|
-
ok,
|
|
1416
|
-
expected_labels: expectedSorted,
|
|
1417
|
-
actual_active_labels: sortFn(uniqueNormalizedLabels(state.activeLabels || [])),
|
|
1418
|
-
default_active: Boolean(state.defaultActive),
|
|
1419
|
-
selected_not_active: selectedNotActive,
|
|
1420
|
-
unselected_but_active: unselectedButActive,
|
|
1421
|
-
default_mismatch: defaultMismatch,
|
|
1422
|
-
options: state.options || []
|
|
1423
|
-
};
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
async verifyFilterDomClassStates(expected) {
|
|
1427
|
-
const schoolState = await this.getSchoolFilterState();
|
|
1428
|
-
const degreeState = await this.getDegreeFilterState();
|
|
1429
|
-
const genderState = await this.getGenderFilterState();
|
|
1430
|
-
const recentState = await this.getRecentNotViewFilterState();
|
|
1431
|
-
|
|
1432
|
-
const checks = [
|
|
1433
|
-
this.buildGroupClassVerification(
|
|
1434
|
-
"school",
|
|
1435
|
-
schoolState,
|
|
1436
|
-
Array.isArray(expected?.schoolTag) && expected.schoolTag.length > 0 ? expected.schoolTag : ["不限"],
|
|
1437
|
-
SCHOOL_TAG_OPTIONS,
|
|
1438
|
-
sortSchoolSelection
|
|
1439
|
-
),
|
|
1440
|
-
this.buildGroupClassVerification(
|
|
1441
|
-
"degree",
|
|
1442
|
-
degreeState,
|
|
1443
|
-
Array.isArray(expected?.degree) && expected.degree.length > 0 ? expected.degree : ["不限"],
|
|
1444
|
-
DEGREE_OPTIONS,
|
|
1445
|
-
sortDegreeSelection
|
|
1446
|
-
),
|
|
1447
|
-
this.buildGroupClassVerification(
|
|
1448
|
-
"gender",
|
|
1449
|
-
genderState,
|
|
1450
|
-
[normalizeText(expected?.gender || "不限")],
|
|
1451
|
-
GENDER_OPTIONS,
|
|
1452
|
-
uniqueNormalizedLabels
|
|
1453
|
-
),
|
|
1454
|
-
this.buildGroupClassVerification(
|
|
1455
|
-
"recent_not_view",
|
|
1456
|
-
recentState,
|
|
1457
|
-
[normalizeText(expected?.recentNotView || "不限")],
|
|
1458
|
-
RECENT_NOT_VIEW_OPTIONS,
|
|
1459
|
-
uniqueNormalizedLabels
|
|
1460
|
-
)
|
|
1461
|
-
];
|
|
1462
|
-
const failures = checks.filter((item) => item.ok === false);
|
|
1463
|
-
return {
|
|
1464
|
-
ok: failures.length === 0,
|
|
1465
|
-
checks,
|
|
1466
|
-
failures,
|
|
1467
|
-
states: {
|
|
1468
|
-
school: schoolState,
|
|
1469
|
-
degree: degreeState,
|
|
1470
|
-
gender: genderState,
|
|
1471
|
-
recent_not_view: recentState
|
|
1472
|
-
}
|
|
1473
|
-
};
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
async countCandidates() {
|
|
1477
|
-
return this.evaluate(`(() => {
|
|
1478
|
-
const frame = ${buildRecommendFrameExpression()};
|
|
1479
|
-
if (!frame || !frame.contentDocument) {
|
|
1480
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1481
|
-
}
|
|
1482
|
-
const doc = frame.contentDocument;
|
|
1483
|
-
const cards = ${JSON.stringify(RECOMMEND_CARD_SELECTORS)}
|
|
1484
|
-
.flatMap((selector) => {
|
|
1485
|
-
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
1486
|
-
});
|
|
1487
|
-
const recommendCandidates = cards.filter((card) => card.querySelector('.card-inner[data-geekid]'));
|
|
1488
|
-
const featuredCards = ${JSON.stringify(FEATURED_CARD_SELECTORS)}
|
|
1489
|
-
.flatMap((selector) => {
|
|
1490
|
-
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
1491
|
-
});
|
|
1492
|
-
const featuredCandidates = featuredCards.filter((card) => card.querySelector('a[data-geekid]'));
|
|
1493
|
-
const latestCards = ${JSON.stringify(LATEST_CARD_SELECTORS)}
|
|
1494
|
-
.flatMap((selector) => {
|
|
1495
|
-
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
1496
|
-
});
|
|
1497
|
-
const latestCandidates = latestCards.filter((card) => card.querySelector('.card-inner[data-geek], [data-geek]'));
|
|
1498
|
-
const tabs = ${JSON.stringify(RECOMMEND_TAB_SELECTORS)}
|
|
1499
|
-
.flatMap((selector) => {
|
|
1500
|
-
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
1501
|
-
});
|
|
1502
|
-
const activeTab = tabs.find((node) => {
|
|
1503
|
-
const className = String(node.className || '');
|
|
1504
|
-
const selected = String(node.getAttribute('aria-selected') || '').toLowerCase() === 'true';
|
|
1505
|
-
return /(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(className) || selected;
|
|
1506
|
-
}) || null;
|
|
1507
|
-
const activeTabStatus = activeTab ? String(activeTab.getAttribute('data-status') || '') : '';
|
|
1508
|
-
const inferredStatus = activeTabStatus
|
|
1509
|
-
|| (featuredCandidates.length > 0 && recommendCandidates.length === 0 && latestCandidates.length === 0
|
|
1510
|
-
? '3'
|
|
1511
|
-
: latestCandidates.length > 0 && recommendCandidates.length === 0 && featuredCandidates.length === 0
|
|
1512
|
-
? '1'
|
|
1513
|
-
: recommendCandidates.length > 0 && featuredCandidates.length === 0 && latestCandidates.length === 0
|
|
1514
|
-
? '0'
|
|
1515
|
-
: '');
|
|
1516
|
-
const effectiveCount = inferredStatus === '3'
|
|
1517
|
-
? featuredCandidates.length
|
|
1518
|
-
: inferredStatus === '1'
|
|
1519
|
-
? latestCandidates.length
|
|
1520
|
-
: inferredStatus === '0'
|
|
1521
|
-
? recommendCandidates.length
|
|
1522
|
-
: Math.max(recommendCandidates.length, featuredCandidates.length, latestCandidates.length);
|
|
1523
|
-
const body = doc.body;
|
|
1524
|
-
return {
|
|
1525
|
-
ok: true,
|
|
1526
|
-
candidateCount: effectiveCount,
|
|
1527
|
-
recommendCandidateCount: recommendCandidates.length,
|
|
1528
|
-
featuredCandidateCount: featuredCandidates.length,
|
|
1529
|
-
latestCandidateCount: latestCandidates.length,
|
|
1530
|
-
activeTabStatus: inferredStatus || null,
|
|
1531
|
-
totalCardCount: cards.length,
|
|
1532
|
-
scrollTop: body ? body.scrollTop : 0,
|
|
1533
|
-
scrollHeight: body ? body.scrollHeight : 0,
|
|
1534
|
-
clientHeight: body ? body.clientHeight : 0
|
|
1535
|
-
};
|
|
1536
|
-
})()`);
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
async waitForCandidateCountStable() {
|
|
1540
|
-
let lastCount = null;
|
|
1541
|
-
let stableRounds = 0;
|
|
1542
|
-
let latest = null;
|
|
1543
|
-
for (let index = 0; index < 10; index += 1) {
|
|
1544
|
-
latest = await this.countCandidates();
|
|
1545
|
-
const current = latest?.candidateCount ?? null;
|
|
1546
|
-
if (current !== null && current === lastCount) {
|
|
1547
|
-
stableRounds += 1;
|
|
1548
|
-
if (stableRounds >= 2) {
|
|
1549
|
-
return latest;
|
|
1550
|
-
}
|
|
1551
|
-
} else {
|
|
1552
|
-
stableRounds = 0;
|
|
1553
|
-
}
|
|
1554
|
-
lastCount = current;
|
|
1555
|
-
await sleep(350 + index * 50);
|
|
1556
|
-
}
|
|
1557
|
-
return latest;
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
async run() {
|
|
1561
|
-
if (this.args.help) {
|
|
1562
|
-
console.log(JSON.stringify({
|
|
1563
|
-
status: "COMPLETED",
|
|
1564
|
-
result: {
|
|
1565
|
-
usage: "node src/cli.js --school-tag 985/211 --degree 本科及以上 --gender 男 --recent-not-view 近14天没有 --job \"算法工程师(视频/图像模型方向) _ 杭州\" --page-scope recommend|latest|featured --port 9222",
|
|
1566
|
-
list_jobs_usage: "node src/cli.js --list-jobs --port 9222"
|
|
1567
|
-
}
|
|
1568
|
-
}));
|
|
1569
|
-
return;
|
|
1570
|
-
}
|
|
1571
|
-
if (!Array.isArray(this.args.schoolTag) || this.args.schoolTag.length === 0) {
|
|
1572
|
-
throw new Error("INVALID_SCHOOL_TAG_INPUT");
|
|
1573
|
-
}
|
|
1574
|
-
if (!Array.isArray(this.args.degree) || this.args.degree.length === 0) {
|
|
1575
|
-
throw new Error("INVALID_DEGREE_INPUT");
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
await this.connect();
|
|
1579
|
-
try {
|
|
1580
|
-
const frameState = await this.getFrameState();
|
|
1581
|
-
if (!frameState?.ok) {
|
|
1582
|
-
if (frameState?.error === "LOGIN_REQUIRED") {
|
|
1583
|
-
throw new Error("LOGIN_REQUIRED");
|
|
1584
|
-
}
|
|
1585
|
-
throw new Error(frameState?.error || 'NO_RECOMMEND_IFRAME');
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
if (this.args.listJobs) {
|
|
1589
|
-
const jobState = await this.ensureJobListReady();
|
|
1590
|
-
console.log(JSON.stringify({
|
|
1591
|
-
status: "COMPLETED",
|
|
1592
|
-
result: {
|
|
1593
|
-
jobs: jobState.jobs || [],
|
|
1594
|
-
page_state: {
|
|
1595
|
-
target_url: this.target?.url || null,
|
|
1596
|
-
frame_url: frameState.frameUrl || jobState.frame_url || null
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
}));
|
|
1600
|
-
return;
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
let selectedJob = null;
|
|
1604
|
-
if (this.args.job) {
|
|
1605
|
-
selectedJob = await this.selectJob(this.args.job);
|
|
1606
|
-
await sleep(humanDelay(220, 70));
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
await this.openFilterPanel();
|
|
1610
|
-
await this.selectSchoolFilter(this.args.schoolTag);
|
|
1611
|
-
await this.selectOption("gender", this.args.gender);
|
|
1612
|
-
await this.selectOption("recentNotView", this.args.recentNotView);
|
|
1613
|
-
await this.selectDegreeFilter(this.args.degree);
|
|
1614
|
-
const domClassVerification = await this.verifyFilterDomClassStates({
|
|
1615
|
-
schoolTag: this.args.schoolTag,
|
|
1616
|
-
degree: this.args.degree,
|
|
1617
|
-
gender: this.args.gender,
|
|
1618
|
-
recentNotView: this.args.recentNotView
|
|
1619
|
-
});
|
|
1620
|
-
if (!domClassVerification.ok) {
|
|
1621
|
-
throw new Error(`FILTER_DOM_CLASS_VERIFY_FAILED:${JSON.stringify(domClassVerification.failures)}`);
|
|
1622
|
-
}
|
|
1623
|
-
await this.closeFilterPanel();
|
|
1624
|
-
const candidateInfo = await this.waitForCandidateCountStable();
|
|
1625
|
-
|
|
1626
|
-
console.log(JSON.stringify({
|
|
1627
|
-
status: "COMPLETED",
|
|
1628
|
-
result: {
|
|
1629
|
-
applied_filters: {
|
|
1630
|
-
school_tag: this.args.schoolTag,
|
|
1631
|
-
degree: this.args.degree,
|
|
1632
|
-
gender: this.args.gender,
|
|
1633
|
-
recent_not_view: this.args.recentNotView
|
|
1634
|
-
},
|
|
1635
|
-
verified_filters: {
|
|
1636
|
-
school: domClassVerification.states.school,
|
|
1637
|
-
degree: domClassVerification.states.degree,
|
|
1638
|
-
gender: domClassVerification.states.gender,
|
|
1639
|
-
recent_not_view: domClassVerification.states.recent_not_view,
|
|
1640
|
-
dom_class_check: {
|
|
1641
|
-
ok: domClassVerification.ok,
|
|
1642
|
-
checks: domClassVerification.checks
|
|
1643
|
-
}
|
|
1644
|
-
},
|
|
1645
|
-
selected_job: selectedJob,
|
|
1646
|
-
candidate_count: candidateInfo?.candidateCount ?? null,
|
|
1647
|
-
active_tab_status: candidateInfo?.activeTabStatus ?? null,
|
|
1648
|
-
selected_page: this.args.pageScope || "recommend",
|
|
1649
|
-
page_state: {
|
|
1650
|
-
target_url: this.target?.url || null,
|
|
1651
|
-
frame_url: frameState.frameUrl || null
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
}));
|
|
1655
|
-
} finally {
|
|
1656
|
-
await this.disconnect();
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
async function main() {
|
|
1662
|
-
const args = parseArgs(process.argv.slice(2));
|
|
1663
|
-
const finalArgs = await enrichArgsFromPrompt(args);
|
|
1664
|
-
const cli = new RecommendSearchCli(finalArgs);
|
|
1665
|
-
await cli.run();
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
function isDirectExecution() {
|
|
1669
|
-
const entry = process.argv?.[1];
|
|
1670
|
-
if (!entry) return false;
|
|
1671
|
-
try {
|
|
1672
|
-
return import.meta.url === pathToFileURL(entry).href;
|
|
1673
|
-
} catch {
|
|
1674
|
-
return false;
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
if (isDirectExecution()) {
|
|
1679
|
-
main().catch((error) => {
|
|
1680
|
-
console.log(JSON.stringify({
|
|
1681
|
-
status: "FAILED",
|
|
1682
|
-
error: {
|
|
1683
|
-
code: error.message || "RECOMMEND_SEARCH_FAILED",
|
|
1684
|
-
message: error.message || "推荐页筛选执行失败。",
|
|
1685
|
-
retryable: true
|
|
1686
|
-
}
|
|
1687
|
-
}));
|
|
1688
|
-
process.exitCode = 1;
|
|
1689
|
-
});
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
export {
|
|
1693
|
-
RecommendSearchCli,
|
|
1694
|
-
normalizeJobTitle,
|
|
1695
|
-
parseArgs
|
|
1696
|
-
};
|
|
1697
|
-
|
|
1698
|
-
|