@reconcrap/boss-recommend-mcp 1.3.38 → 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.
Files changed (85) hide show
  1. package/README.md +53 -33
  2. package/package.json +61 -9
  3. package/skills/boss-recommend-pipeline/SKILL.md +4 -0
  4. package/src/chat-mcp.js +1333 -0
  5. package/src/chat-runtime-config.js +559 -0
  6. package/src/cli.js +1095 -196
  7. package/src/core/browser/index.js +378 -0
  8. package/src/core/capture/index.js +298 -0
  9. package/src/core/cv-acquisition/index.js +219 -0
  10. package/src/core/greet-quota/index.js +54 -0
  11. package/src/core/infinite-list/index.js +459 -0
  12. package/src/core/reporting/legacy-csv.js +332 -0
  13. package/src/core/run/index.js +286 -0
  14. package/src/core/screening/index.js +1166 -0
  15. package/src/core/self-heal/index.js +848 -0
  16. package/src/domains/chat/cards.js +129 -0
  17. package/src/domains/chat/constants.js +183 -0
  18. package/src/domains/chat/detail.js +1369 -0
  19. package/src/domains/chat/index.js +7 -0
  20. package/src/domains/chat/jobs.js +334 -0
  21. package/src/domains/chat/page-guard.js +88 -0
  22. package/src/domains/chat/roots.js +56 -0
  23. package/src/domains/chat/run-service.js +1101 -0
  24. package/src/domains/recommend/actions.js +457 -0
  25. package/src/domains/recommend/cards.js +228 -0
  26. package/src/domains/recommend/constants.js +141 -0
  27. package/src/domains/recommend/detail.js +341 -0
  28. package/src/domains/recommend/filters.js +581 -0
  29. package/src/domains/recommend/index.js +10 -0
  30. package/src/domains/recommend/jobs.js +232 -0
  31. package/src/domains/recommend/refresh.js +204 -0
  32. package/src/domains/recommend/roots.js +78 -0
  33. package/src/domains/recommend/run-service.js +903 -0
  34. package/src/domains/recommend/scopes.js +245 -0
  35. package/src/domains/recruit/actions.js +277 -0
  36. package/src/domains/recruit/cards.js +67 -0
  37. package/src/domains/recruit/constants.js +130 -0
  38. package/src/domains/recruit/detail.js +414 -0
  39. package/src/domains/recruit/index.js +9 -0
  40. package/src/domains/recruit/instruction-parser.js +451 -0
  41. package/src/domains/recruit/refresh.js +40 -0
  42. package/src/domains/recruit/roots.js +68 -0
  43. package/src/domains/recruit/run-service.js +580 -0
  44. package/src/domains/recruit/search.js +1149 -0
  45. package/src/index.js +578 -419
  46. package/src/recommend-mcp.js +1257 -0
  47. package/src/recruit-mcp.js +1035 -0
  48. package/src/adapters.js +0 -3079
  49. package/src/boss-chat.js +0 -1037
  50. package/src/pipeline.js +0 -2249
  51. package/src/recommend-healing-config.js +0 -131
  52. package/src/recommend-healing-rules.json +0 -261
  53. package/src/self-heal.js +0 -2237
  54. package/src/test-adapters-runtime.js +0 -628
  55. package/src/test-boss-chat.js +0 -3196
  56. package/src/test-index-async.js +0 -498
  57. package/src/test-parser.js +0 -742
  58. package/src/test-pipeline.js +0 -2703
  59. package/src/test-run-state.js +0 -152
  60. package/src/test-self-heal.js +0 -224
  61. package/vendor/boss-chat-cli/README.md +0 -134
  62. package/vendor/boss-chat-cli/package.json +0 -53
  63. package/vendor/boss-chat-cli/src/app.js +0 -1501
  64. package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
  65. package/vendor/boss-chat-cli/src/cli.js +0 -1713
  66. package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
  67. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
  68. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
  69. package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
  70. package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
  71. package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
  72. package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
  73. package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
  74. package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
  75. package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
  76. package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
  77. package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
  78. package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
  79. package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
  80. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -6927
  81. package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
  82. package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
  83. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2294
  84. package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
  85. 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
-