@reconcrap/boss-recommend-mcp 1.1.2 → 1.1.4
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/package.json +3 -2
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-recommend-pipeline/README.md +12 -12
- package/skills/boss-recommend-pipeline/SKILL.md +195 -195
- package/src/adapters.js +1876 -1806
- package/src/index.js +1254 -1254
- package/src/parser.js +19 -28
- package/src/pipeline.js +919 -792
- package/src/run-state.js +351 -351
- package/src/test-adapters-runtime.js +163 -163
- package/src/test-index-async.js +236 -236
- package/src/test-parser.js +55 -0
- package/src/test-pipeline.js +103 -0
- package/src/test-run-state.js +152 -152
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +111 -18
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +508 -452
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +245 -0
- package/vendor/boss-recommend-search-cli/src/cli.js +811 -811
- package/vendor/boss-recommend-search-cli/src/test-job-selection.js +201 -201
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import process from "node:process";
|
|
3
|
-
import readline from "node:readline";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
import CDP from "chrome-remote-interface";
|
|
6
|
-
|
|
7
|
-
const DEFAULT_PORT = 9222;
|
|
8
|
-
const RECOMMEND_URL_FRAGMENT = "/web/chat/recommend";
|
|
9
|
-
const BOSS_LOGIN_URL = "https://www.zhipin.com/web/user/?ka=bticket";
|
|
10
|
-
const BOSS_LOGIN_URL_PATTERN = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
|
|
11
|
-
const BOSS_LOGIN_TITLE_PATTERN = /登录|signin|扫码登录|BOSS直聘登录/i;
|
|
12
|
-
const SCHOOL_TAG_OPTIONS = ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"];
|
|
13
|
-
const DEGREE_OPTIONS = ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
|
|
14
|
-
const DEGREE_ORDER = ["初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
|
|
15
|
-
const GENDER_OPTIONS = ["不限", "男", "女"];
|
|
16
|
-
const RECENT_NOT_VIEW_OPTIONS = ["不限", "近14天没有"];
|
|
17
|
-
|
|
18
|
-
function normalizeText(value) {
|
|
19
|
-
return String(value || "").replace(/\s+/g, " ").trim();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function normalizeJobTitle(value) {
|
|
23
|
-
const text = normalizeText(value);
|
|
24
|
-
if (!text) return "";
|
|
25
|
-
const byGap = text.split(/\s{2,}/).map((item) => item.trim()).filter(Boolean)[0] || text;
|
|
26
|
-
const strippedRange = byGap
|
|
27
|
-
.replace(/\s+\d+(?:\.\d+)?\s*(?:-|~|—|至)\s*\d+(?:\.\d+)?\s*(?:k|K|千|万|元\/天|元\/月|元\/年|K\/月|k\/月|万\/月|万\/年)?$/u, "")
|
|
28
|
-
.trim();
|
|
29
|
-
const strippedSingle = strippedRange
|
|
30
|
-
.replace(/\s+\d+(?:\.\d+)?\s*(?:k|K|千|万|元\/天|元\/月|元\/年|K\/月|k\/月|万\/月|万\/年)$/u, "")
|
|
31
|
-
.trim();
|
|
32
|
-
return strippedSingle || byGap;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function parsePositiveInteger(raw) {
|
|
36
|
-
const value = Number.parseInt(String(raw || ""), 10);
|
|
37
|
-
return Number.isFinite(value) && value > 0 ? value : null;
|
|
38
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import readline from "node:readline";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import CDP from "chrome-remote-interface";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_PORT = 9222;
|
|
8
|
+
const RECOMMEND_URL_FRAGMENT = "/web/chat/recommend";
|
|
9
|
+
const BOSS_LOGIN_URL = "https://www.zhipin.com/web/user/?ka=bticket";
|
|
10
|
+
const BOSS_LOGIN_URL_PATTERN = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
|
|
11
|
+
const BOSS_LOGIN_TITLE_PATTERN = /登录|signin|扫码登录|BOSS直聘登录/i;
|
|
12
|
+
const SCHOOL_TAG_OPTIONS = ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"];
|
|
13
|
+
const DEGREE_OPTIONS = ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
|
|
14
|
+
const DEGREE_ORDER = ["初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
|
|
15
|
+
const GENDER_OPTIONS = ["不限", "男", "女"];
|
|
16
|
+
const RECENT_NOT_VIEW_OPTIONS = ["不限", "近14天没有"];
|
|
17
|
+
|
|
18
|
+
function normalizeText(value) {
|
|
19
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeJobTitle(value) {
|
|
23
|
+
const text = normalizeText(value);
|
|
24
|
+
if (!text) return "";
|
|
25
|
+
const byGap = text.split(/\s{2,}/).map((item) => item.trim()).filter(Boolean)[0] || text;
|
|
26
|
+
const strippedRange = byGap
|
|
27
|
+
.replace(/\s+\d+(?:\.\d+)?\s*(?:-|~|—|至)\s*\d+(?:\.\d+)?\s*(?:k|K|千|万|元\/天|元\/月|元\/年|K\/月|k\/月|万\/月|万\/年)?$/u, "")
|
|
28
|
+
.trim();
|
|
29
|
+
const strippedSingle = strippedRange
|
|
30
|
+
.replace(/\s+\d+(?:\.\d+)?\s*(?:k|K|千|万|元\/天|元\/月|元\/年|K\/月|k\/月|万\/月|万\/年)$/u, "")
|
|
31
|
+
.trim();
|
|
32
|
+
return strippedSingle || byGap;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parsePositiveInteger(raw) {
|
|
36
|
+
const value = Number.parseInt(String(raw || ""), 10);
|
|
37
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
38
|
+
}
|
|
39
39
|
|
|
40
40
|
function sortSchoolSelection(values) {
|
|
41
41
|
const order = new Map(SCHOOL_TAG_OPTIONS.map((label, index) => [label, index]));
|
|
@@ -79,25 +79,25 @@ function normalizeDegree(value) {
|
|
|
79
79
|
return DEGREE_OPTIONS.includes(normalized) ? normalized : null;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
function sortDegreeSelection(values) {
|
|
83
|
-
return Array.from(new Set(values.filter(Boolean))).sort((left, right) => DEGREE_ORDER.indexOf(left) - DEGREE_ORDER.indexOf(right));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function selectionEquals(left, right) {
|
|
87
|
-
if (!Array.isArray(left) || !Array.isArray(right)) return false;
|
|
88
|
-
if (left.length !== right.length) return false;
|
|
89
|
-
return left.every((value, index) => value === right[index]);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function uniqueNormalizedLabels(values) {
|
|
93
|
-
return Array.from(
|
|
94
|
-
new Set(
|
|
95
|
-
(values || [])
|
|
96
|
-
.map((item) => normalizeText(item))
|
|
97
|
-
.filter(Boolean)
|
|
98
|
-
)
|
|
99
|
-
);
|
|
100
|
-
}
|
|
82
|
+
function sortDegreeSelection(values) {
|
|
83
|
+
return Array.from(new Set(values.filter(Boolean))).sort((left, right) => DEGREE_ORDER.indexOf(left) - DEGREE_ORDER.indexOf(right));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function selectionEquals(left, right) {
|
|
87
|
+
if (!Array.isArray(left) || !Array.isArray(right)) return false;
|
|
88
|
+
if (left.length !== right.length) return false;
|
|
89
|
+
return left.every((value, index) => value === right[index]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function uniqueNormalizedLabels(values) {
|
|
93
|
+
return Array.from(
|
|
94
|
+
new Set(
|
|
95
|
+
(values || [])
|
|
96
|
+
.map((item) => normalizeText(item))
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
101
|
|
|
102
102
|
function expandDegreeAtOrAbove(value) {
|
|
103
103
|
const normalized = normalizeDegree(value);
|
|
@@ -107,7 +107,7 @@ function expandDegreeAtOrAbove(value) {
|
|
|
107
107
|
return DEGREE_ORDER.slice(index);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
function parseDegreeSelection(raw) {
|
|
110
|
+
function parseDegreeSelection(raw) {
|
|
111
111
|
const text = normalizeText(raw);
|
|
112
112
|
if (!text) return null;
|
|
113
113
|
if (text === "不限") return ["不限"];
|
|
@@ -132,29 +132,29 @@ function parseDegreeSelection(raw) {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
const normalized = sortDegreeSelection(selected);
|
|
136
|
-
return normalized.length ? normalized : null;
|
|
137
|
-
}
|
|
135
|
+
const normalized = sortDegreeSelection(selected);
|
|
136
|
+
return normalized.length ? normalized : null;
|
|
137
|
+
}
|
|
138
138
|
|
|
139
139
|
function parseArgs(argv) {
|
|
140
140
|
const args = {
|
|
141
141
|
schoolTag: ["不限"],
|
|
142
142
|
degree: ["不限"],
|
|
143
143
|
gender: "不限",
|
|
144
|
-
recentNotView: "不限",
|
|
145
|
-
port: DEFAULT_PORT,
|
|
146
|
-
listJobs: false,
|
|
147
|
-
job: null,
|
|
148
|
-
help: false,
|
|
149
|
-
__provided: {
|
|
150
|
-
schoolTag: false,
|
|
151
|
-
degree: false,
|
|
152
|
-
gender: false,
|
|
153
|
-
recentNotView: false,
|
|
154
|
-
port: false,
|
|
155
|
-
job: false
|
|
156
|
-
}
|
|
157
|
-
};
|
|
144
|
+
recentNotView: "不限",
|
|
145
|
+
port: DEFAULT_PORT,
|
|
146
|
+
listJobs: false,
|
|
147
|
+
job: null,
|
|
148
|
+
help: false,
|
|
149
|
+
__provided: {
|
|
150
|
+
schoolTag: false,
|
|
151
|
+
degree: false,
|
|
152
|
+
gender: false,
|
|
153
|
+
recentNotView: false,
|
|
154
|
+
port: false,
|
|
155
|
+
job: false
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
158
|
|
|
159
159
|
for (let index = 0; index < argv.length; index += 1) {
|
|
160
160
|
const token = argv[index];
|
|
@@ -175,20 +175,20 @@ function parseArgs(argv) {
|
|
|
175
175
|
args.recentNotView = next;
|
|
176
176
|
args.__provided.recentNotView = true;
|
|
177
177
|
index += 1;
|
|
178
|
-
} else if (token === "--port" && next) {
|
|
179
|
-
args.port = parsePositiveInteger(next) || DEFAULT_PORT;
|
|
180
|
-
args.__provided.port = true;
|
|
181
|
-
index += 1;
|
|
182
|
-
} else if (token === "--job" && next) {
|
|
183
|
-
args.job = normalizeText(next) || null;
|
|
184
|
-
args.__provided.job = true;
|
|
185
|
-
index += 1;
|
|
186
|
-
} else if (token === "--list-jobs") {
|
|
187
|
-
args.listJobs = true;
|
|
188
|
-
} else if (token === "--help" || token === "-h") {
|
|
189
|
-
args.help = true;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
178
|
+
} else if (token === "--port" && next) {
|
|
179
|
+
args.port = parsePositiveInteger(next) || DEFAULT_PORT;
|
|
180
|
+
args.__provided.port = true;
|
|
181
|
+
index += 1;
|
|
182
|
+
} else if (token === "--job" && next) {
|
|
183
|
+
args.job = normalizeText(next) || null;
|
|
184
|
+
args.__provided.job = true;
|
|
185
|
+
index += 1;
|
|
186
|
+
} else if (token === "--list-jobs") {
|
|
187
|
+
args.listJobs = true;
|
|
188
|
+
} else if (token === "--help" || token === "-h") {
|
|
189
|
+
args.help = true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
192
|
|
|
193
193
|
return args;
|
|
194
194
|
}
|
|
@@ -207,12 +207,12 @@ async function promptValue(ask, question, validate, defaultValue) {
|
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
async function enrichArgsFromPrompt(args) {
|
|
211
|
-
if (!isInteractiveTTY() || args.help) return args;
|
|
212
|
-
if (args.listJobs) return args;
|
|
213
|
-
const askTargets =
|
|
214
|
-
Object.values(args.__provided || {}).some((item) => item === false)
|
|
215
|
-
|| !Array.isArray(args.schoolTag)
|
|
210
|
+
async function enrichArgsFromPrompt(args) {
|
|
211
|
+
if (!isInteractiveTTY() || args.help) return args;
|
|
212
|
+
if (args.listJobs) return args;
|
|
213
|
+
const askTargets =
|
|
214
|
+
Object.values(args.__provided || {}).some((item) => item === false)
|
|
215
|
+
|| !Array.isArray(args.schoolTag)
|
|
216
216
|
|| args.schoolTag.length === 0
|
|
217
217
|
|| !Array.isArray(args.degree)
|
|
218
218
|
|| args.degree.length === 0;
|
|
@@ -296,14 +296,14 @@ function generateBezierPath(start, end, steps = 18) {
|
|
|
296
296
|
return path;
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
class RecommendSearchCli {
|
|
300
|
-
constructor(args) {
|
|
301
|
-
this.args = args;
|
|
302
|
-
this.client = null;
|
|
303
|
-
this.Runtime = null;
|
|
304
|
-
this.Input = null;
|
|
305
|
-
this.target = null;
|
|
306
|
-
}
|
|
299
|
+
class RecommendSearchCli {
|
|
300
|
+
constructor(args) {
|
|
301
|
+
this.args = args;
|
|
302
|
+
this.client = null;
|
|
303
|
+
this.Runtime = null;
|
|
304
|
+
this.Input = null;
|
|
305
|
+
this.target = null;
|
|
306
|
+
}
|
|
307
307
|
|
|
308
308
|
async connect() {
|
|
309
309
|
const targets = await CDP.List({ port: this.args.port });
|
|
@@ -386,42 +386,42 @@ class RecommendSearchCli {
|
|
|
386
386
|
});
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
async getFrameState() {
|
|
390
|
-
return this.evaluate(`(() => {
|
|
391
|
-
const currentUrl = (() => {
|
|
392
|
-
try { return String(window.location.href || ''); } catch { return ''; }
|
|
393
|
-
})();
|
|
394
|
-
const title = (() => {
|
|
395
|
-
try { return String(document.title || ''); } catch { return ''; }
|
|
396
|
-
})();
|
|
397
|
-
const isLogin = ${BOSS_LOGIN_URL_PATTERN}.test(currentUrl)
|
|
398
|
-
|| ${BOSS_LOGIN_TITLE_PATTERN}.test(title);
|
|
399
|
-
if (isLogin) {
|
|
400
|
-
return {
|
|
401
|
-
ok: false,
|
|
402
|
-
error: 'LOGIN_REQUIRED',
|
|
403
|
-
currentUrl: currentUrl || ${JSON.stringify(BOSS_LOGIN_URL)},
|
|
404
|
-
title
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
408
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
409
|
-
|| document.querySelector('iframe');
|
|
410
|
-
if (!frame || !frame.contentDocument) {
|
|
411
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME', currentUrl, title };
|
|
412
|
-
}
|
|
413
|
-
return {
|
|
414
|
-
ok: true,
|
|
415
|
-
currentUrl,
|
|
416
|
-
title,
|
|
417
|
-
frameUrl: (() => {
|
|
418
|
-
try { return String(frame.contentWindow.location.href || ''); } catch { return ''; }
|
|
419
|
-
})()
|
|
420
|
-
};
|
|
421
|
-
})()`);
|
|
389
|
+
async getFrameState() {
|
|
390
|
+
return this.evaluate(`(() => {
|
|
391
|
+
const currentUrl = (() => {
|
|
392
|
+
try { return String(window.location.href || ''); } catch { return ''; }
|
|
393
|
+
})();
|
|
394
|
+
const title = (() => {
|
|
395
|
+
try { return String(document.title || ''); } catch { return ''; }
|
|
396
|
+
})();
|
|
397
|
+
const isLogin = ${BOSS_LOGIN_URL_PATTERN}.test(currentUrl)
|
|
398
|
+
|| ${BOSS_LOGIN_TITLE_PATTERN}.test(title);
|
|
399
|
+
if (isLogin) {
|
|
400
|
+
return {
|
|
401
|
+
ok: false,
|
|
402
|
+
error: 'LOGIN_REQUIRED',
|
|
403
|
+
currentUrl: currentUrl || ${JSON.stringify(BOSS_LOGIN_URL)},
|
|
404
|
+
title
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
408
|
+
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
409
|
+
|| document.querySelector('iframe');
|
|
410
|
+
if (!frame || !frame.contentDocument) {
|
|
411
|
+
return { ok: false, error: 'NO_RECOMMEND_IFRAME', currentUrl, title };
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
ok: true,
|
|
415
|
+
currentUrl,
|
|
416
|
+
title,
|
|
417
|
+
frameUrl: (() => {
|
|
418
|
+
try { return String(frame.contentWindow.location.href || ''); } catch { return ''; }
|
|
419
|
+
})()
|
|
420
|
+
};
|
|
421
|
+
})()`);
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
-
async getFilterEntryPoint() {
|
|
424
|
+
async getFilterEntryPoint() {
|
|
425
425
|
return this.evaluate(`(() => {
|
|
426
426
|
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
427
427
|
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
@@ -442,248 +442,248 @@ class RecommendSearchCli {
|
|
|
442
442
|
y: frameRect.top + rect.top + rect.height / 2
|
|
443
443
|
};
|
|
444
444
|
})()`);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
async getJobListState() {
|
|
448
|
-
return this.evaluate(`(() => {
|
|
449
|
-
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
450
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
451
|
-
|| document.querySelector('iframe');
|
|
452
|
-
if (!frame || !frame.contentDocument) {
|
|
453
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
454
|
-
}
|
|
455
|
-
const doc = frame.contentDocument;
|
|
456
|
-
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
457
|
-
const normalizeTitle = (value) => {
|
|
458
|
-
const text = normalize(value);
|
|
459
|
-
if (!text) return '';
|
|
460
|
-
const byGap = text.split(/\\s{2,}/).map((item) => item.trim()).filter(Boolean)[0] || text;
|
|
461
|
-
const strippedRange = byGap
|
|
462
|
-
.replace(/\\s+\\d+(?:\\.\\d+)?\\s*(?:-|~|—|至)\\s*\\d+(?:\\.\\d+)?\\s*(?:k|K|千|万|元\\/天|元\\/月|元\\/年|K\\/月|k\\/月|万\\/月|万\\/年)?$/u, '')
|
|
463
|
-
.trim();
|
|
464
|
-
const strippedSingle = strippedRange
|
|
465
|
-
.replace(/\\s+\\d+(?:\\.\\d+)?\\s*(?:k|K|千|万|元\\/天|元\\/月|元\\/年|K\\/月|k\\/月|万\\/月|万\\/年)$/u, '')
|
|
466
|
-
.trim();
|
|
467
|
-
return strippedSingle || byGap;
|
|
468
|
-
};
|
|
469
|
-
const isVisible = (el) => {
|
|
470
|
-
if (!el) return false;
|
|
471
|
-
const style = getComputedStyle(el);
|
|
472
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
473
|
-
return false;
|
|
474
|
-
}
|
|
475
|
-
const rect = el.getBoundingClientRect();
|
|
476
|
-
return rect.width > 2 && rect.height > 2;
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
const items = Array.from(doc.querySelectorAll([
|
|
480
|
-
'.ui-dropmenu-list .job-list .job-item',
|
|
481
|
-
'.job-selecter-options .job-list .job-item',
|
|
482
|
-
'.job-selector-options .job-list .job-item',
|
|
483
|
-
'.dropmenu-list .job-list .job-item',
|
|
484
|
-
'.job-list .job-item'
|
|
485
|
-
].join(',')));
|
|
486
|
-
const jobs = [];
|
|
487
|
-
const seen = new Set();
|
|
488
|
-
for (const item of items) {
|
|
489
|
-
const label = normalize(item.querySelector('.label')?.textContent || item.textContent || '');
|
|
490
|
-
const title = normalizeTitle(label);
|
|
491
|
-
const value = normalize(item.getAttribute('value') || item.dataset?.value || '');
|
|
492
|
-
const dedupeKey = value || title || label;
|
|
493
|
-
if (!dedupeKey || seen.has(dedupeKey)) continue;
|
|
494
|
-
seen.add(dedupeKey);
|
|
495
|
-
jobs.push({
|
|
496
|
-
value: value || null,
|
|
497
|
-
title: title || label || null,
|
|
498
|
-
label: label || null,
|
|
499
|
-
current: item.classList.contains('curr') || item.classList.contains('active'),
|
|
500
|
-
visible: isVisible(item)
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const selectedLabelNode = doc.querySelector('.chat-job-name, .job-selecter .label, .job-selecter .job-name, .job-select .label');
|
|
505
|
-
return {
|
|
506
|
-
ok: true,
|
|
507
|
-
jobs,
|
|
508
|
-
selected_label: normalize(selectedLabelNode ? selectedLabelNode.textContent : ''),
|
|
509
|
-
frame_url: (() => {
|
|
510
|
-
try { return String(frame.contentWindow.location.href || ''); } catch { return ''; }
|
|
511
|
-
})()
|
|
512
|
-
};
|
|
513
|
-
})()`);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
async clickJobDropdownTriggerBySelector() {
|
|
517
|
-
return this.evaluate(`(() => {
|
|
518
|
-
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
519
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
520
|
-
|| document.querySelector('iframe');
|
|
521
|
-
if (!frame || !frame.contentDocument) {
|
|
522
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
523
|
-
}
|
|
524
|
-
const doc = frame.contentDocument;
|
|
525
|
-
const selectors = [
|
|
526
|
-
'.chat-job-select',
|
|
527
|
-
'.chat-job-selector',
|
|
528
|
-
'.job-selecter',
|
|
529
|
-
'.job-selector',
|
|
530
|
-
'.job-select-wrap',
|
|
531
|
-
'.job-select',
|
|
532
|
-
'.job-select-box',
|
|
533
|
-
'.job-wrap',
|
|
534
|
-
'.chat-job-name',
|
|
535
|
-
'.top-chat-search'
|
|
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
|
-
for (const selector of selectors) {
|
|
547
|
-
const el = doc.querySelector(selector);
|
|
548
|
-
if (el && isVisible(el)) {
|
|
549
|
-
el.click();
|
|
550
|
-
return { ok: true };
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
return { ok: false, error: 'JOB_TRIGGER_NOT_FOUND' };
|
|
554
|
-
})()`);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
async ensureJobListReady() {
|
|
558
|
-
let lastError = "JOB_LIST_NOT_FOUND";
|
|
559
|
-
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
560
|
-
const state = await this.getJobListState();
|
|
561
|
-
if (state?.ok && Array.isArray(state.jobs) && state.jobs.length > 0) {
|
|
562
|
-
return state;
|
|
563
|
-
}
|
|
564
|
-
lastError = state?.error || lastError;
|
|
565
|
-
const clickResult = await this.clickJobDropdownTriggerBySelector();
|
|
566
|
-
if (!clickResult?.ok) {
|
|
567
|
-
lastError = clickResult?.error || lastError;
|
|
568
|
-
}
|
|
569
|
-
await sleep(220 + attempt * 80);
|
|
570
|
-
}
|
|
571
|
-
throw new Error(lastError);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
findJobMatch(jobList, requestedJobRaw) {
|
|
575
|
-
const requested = normalizeText(requestedJobRaw);
|
|
576
|
-
if (!requested) return null;
|
|
577
|
-
const normalizedRequestedTitle = normalizeJobTitle(requested);
|
|
578
|
-
const normalize = (value) => normalizeText(value).toLowerCase();
|
|
579
|
-
const byValue = jobList.find((job) => normalize(job.value || "") === normalize(requested));
|
|
580
|
-
if (byValue) return byValue;
|
|
581
|
-
const exactTitle = jobList.find((job) => normalize(job.title || "") === normalize(normalizedRequestedTitle));
|
|
582
|
-
if (exactTitle) return exactTitle;
|
|
583
|
-
const exactLabel = jobList.find((job) => normalize(job.label || "") === normalize(requested));
|
|
584
|
-
if (exactLabel) return exactLabel;
|
|
585
|
-
const contains = jobList.filter((job) => {
|
|
586
|
-
const title = normalize(job.title || "");
|
|
587
|
-
const label = normalize(job.label || "");
|
|
588
|
-
const target = normalize(normalizedRequestedTitle);
|
|
589
|
-
return (
|
|
590
|
-
(title && (title.includes(target) || target.includes(title)))
|
|
591
|
-
|| (label && (label.includes(normalize(requested)) || normalize(requested).includes(label)))
|
|
592
|
-
);
|
|
593
|
-
});
|
|
594
|
-
if (contains.length === 1) return contains[0];
|
|
595
|
-
if (contains.length > 1) {
|
|
596
|
-
throw new Error("JOB_SELECTION_AMBIGUOUS");
|
|
597
|
-
}
|
|
598
|
-
return null;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
async clickJobBySelector(job) {
|
|
602
|
-
return this.evaluate(`((job) => {
|
|
603
|
-
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
604
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
605
|
-
|| document.querySelector('iframe');
|
|
606
|
-
if (!frame || !frame.contentDocument) {
|
|
607
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
608
|
-
}
|
|
609
|
-
const doc = frame.contentDocument;
|
|
610
|
-
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
611
|
-
const normalizeTitle = (value) => {
|
|
612
|
-
const text = normalize(value);
|
|
613
|
-
if (!text) return '';
|
|
614
|
-
const byGap = text.split(/\\s{2,}/).map((item) => item.trim()).filter(Boolean)[0] || text;
|
|
615
|
-
const strippedRange = byGap
|
|
616
|
-
.replace(/\\s+\\d+(?:\\.\\d+)?\\s*(?:-|~|—|至)\\s*\\d+(?:\\.\\d+)?\\s*(?:k|K|千|万|元\\/天|元\\/月|元\\/年|K\\/月|k\\/月|万\\/月|万\\/年)?$/u, '')
|
|
617
|
-
.trim();
|
|
618
|
-
const strippedSingle = strippedRange
|
|
619
|
-
.replace(/\\s+\\d+(?:\\.\\d+)?\\s*(?:k|K|千|万|元\\/天|元\\/月|元\\/年|K\\/月|k\\/月|万\\/月|万\\/年)$/u, '')
|
|
620
|
-
.trim();
|
|
621
|
-
return strippedSingle || byGap;
|
|
622
|
-
};
|
|
623
|
-
const items = Array.from(doc.querySelectorAll([
|
|
624
|
-
'.ui-dropmenu-list .job-list .job-item',
|
|
625
|
-
'.job-selecter-options .job-list .job-item',
|
|
626
|
-
'.job-selector-options .job-list .job-item',
|
|
627
|
-
'.dropmenu-list .job-list .job-item',
|
|
628
|
-
'.job-list .job-item'
|
|
629
|
-
].join(',')));
|
|
630
|
-
const target = items.find((item) => {
|
|
631
|
-
const value = normalize(item.getAttribute('value') || item.dataset?.value || '');
|
|
632
|
-
const label = normalize(item.querySelector('.label')?.textContent || item.textContent || '');
|
|
633
|
-
const title = normalizeTitle(label);
|
|
634
|
-
const matchValue = job.value && value && value === normalize(job.value);
|
|
635
|
-
const matchTitle = job.title && title && title === normalize(job.title);
|
|
636
|
-
const matchLabel = job.label && label && label === normalize(job.label);
|
|
637
|
-
return matchValue || matchTitle || matchLabel;
|
|
638
|
-
});
|
|
639
|
-
if (!target) {
|
|
640
|
-
return { ok: false, error: 'JOB_OPTION_NOT_FOUND' };
|
|
641
|
-
}
|
|
642
|
-
target.click();
|
|
643
|
-
return { ok: true };
|
|
644
|
-
})(${JSON.stringify(job)})`);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
async waitJobSelected(job, rounds = 8) {
|
|
648
|
-
const selectedValue = normalizeText(job.value || "");
|
|
649
|
-
const selectedTitle = normalizeText(job.title || "");
|
|
650
|
-
const selectedLabel = normalizeText(job.label || "");
|
|
651
|
-
for (let index = 0; index < rounds; index += 1) {
|
|
652
|
-
const state = await this.getJobListState();
|
|
653
|
-
if (state?.ok) {
|
|
654
|
-
const current = (state.jobs || []).find((item) => item.current);
|
|
655
|
-
if (current) {
|
|
656
|
-
const sameValue = selectedValue && normalizeText(current.value || "") === selectedValue;
|
|
657
|
-
const sameTitle = selectedTitle && normalizeText(current.title || "") === selectedTitle;
|
|
658
|
-
const sameLabel = selectedLabel && normalizeText(current.label || "") === selectedLabel;
|
|
659
|
-
if (sameValue || sameTitle || sameLabel) return true;
|
|
660
|
-
}
|
|
661
|
-
const selectedText = normalizeText(state.selected_label || "");
|
|
662
|
-
if (selectedTitle && selectedText && (selectedText === selectedTitle || selectedText.includes(selectedTitle))) {
|
|
663
|
-
return true;
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
await sleep(150 + index * 40);
|
|
667
|
-
}
|
|
668
|
-
return false;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
async selectJob(jobSelection) {
|
|
672
|
-
const state = await this.ensureJobListReady();
|
|
673
|
-
const matched = this.findJobMatch(state.jobs || [], jobSelection);
|
|
674
|
-
if (!matched) {
|
|
675
|
-
throw new Error("JOB_OPTION_NOT_FOUND");
|
|
676
|
-
}
|
|
677
|
-
const clicked = await this.clickJobBySelector(matched);
|
|
678
|
-
if (!clicked?.ok) {
|
|
679
|
-
throw new Error(clicked?.error || "JOB_SELECT_FAILED");
|
|
680
|
-
}
|
|
681
|
-
const selected = await this.waitJobSelected(matched, 10);
|
|
682
|
-
if (!selected) {
|
|
683
|
-
throw new Error("JOB_SELECTION_NOT_APPLIED");
|
|
684
|
-
}
|
|
685
|
-
return matched;
|
|
686
|
-
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async getJobListState() {
|
|
448
|
+
return this.evaluate(`(() => {
|
|
449
|
+
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
450
|
+
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
451
|
+
|| document.querySelector('iframe');
|
|
452
|
+
if (!frame || !frame.contentDocument) {
|
|
453
|
+
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
454
|
+
}
|
|
455
|
+
const doc = frame.contentDocument;
|
|
456
|
+
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
457
|
+
const normalizeTitle = (value) => {
|
|
458
|
+
const text = normalize(value);
|
|
459
|
+
if (!text) return '';
|
|
460
|
+
const byGap = text.split(/\\s{2,}/).map((item) => item.trim()).filter(Boolean)[0] || text;
|
|
461
|
+
const strippedRange = byGap
|
|
462
|
+
.replace(/\\s+\\d+(?:\\.\\d+)?\\s*(?:-|~|—|至)\\s*\\d+(?:\\.\\d+)?\\s*(?:k|K|千|万|元\\/天|元\\/月|元\\/年|K\\/月|k\\/月|万\\/月|万\\/年)?$/u, '')
|
|
463
|
+
.trim();
|
|
464
|
+
const strippedSingle = strippedRange
|
|
465
|
+
.replace(/\\s+\\d+(?:\\.\\d+)?\\s*(?:k|K|千|万|元\\/天|元\\/月|元\\/年|K\\/月|k\\/月|万\\/月|万\\/年)$/u, '')
|
|
466
|
+
.trim();
|
|
467
|
+
return strippedSingle || byGap;
|
|
468
|
+
};
|
|
469
|
+
const isVisible = (el) => {
|
|
470
|
+
if (!el) return false;
|
|
471
|
+
const style = getComputedStyle(el);
|
|
472
|
+
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
const rect = el.getBoundingClientRect();
|
|
476
|
+
return rect.width > 2 && rect.height > 2;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const items = Array.from(doc.querySelectorAll([
|
|
480
|
+
'.ui-dropmenu-list .job-list .job-item',
|
|
481
|
+
'.job-selecter-options .job-list .job-item',
|
|
482
|
+
'.job-selector-options .job-list .job-item',
|
|
483
|
+
'.dropmenu-list .job-list .job-item',
|
|
484
|
+
'.job-list .job-item'
|
|
485
|
+
].join(',')));
|
|
486
|
+
const jobs = [];
|
|
487
|
+
const seen = new Set();
|
|
488
|
+
for (const item of items) {
|
|
489
|
+
const label = normalize(item.querySelector('.label')?.textContent || item.textContent || '');
|
|
490
|
+
const title = normalizeTitle(label);
|
|
491
|
+
const value = normalize(item.getAttribute('value') || item.dataset?.value || '');
|
|
492
|
+
const dedupeKey = value || title || label;
|
|
493
|
+
if (!dedupeKey || seen.has(dedupeKey)) continue;
|
|
494
|
+
seen.add(dedupeKey);
|
|
495
|
+
jobs.push({
|
|
496
|
+
value: value || null,
|
|
497
|
+
title: title || label || null,
|
|
498
|
+
label: label || null,
|
|
499
|
+
current: item.classList.contains('curr') || item.classList.contains('active'),
|
|
500
|
+
visible: isVisible(item)
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const selectedLabelNode = doc.querySelector('.chat-job-name, .job-selecter .label, .job-selecter .job-name, .job-select .label');
|
|
505
|
+
return {
|
|
506
|
+
ok: true,
|
|
507
|
+
jobs,
|
|
508
|
+
selected_label: normalize(selectedLabelNode ? selectedLabelNode.textContent : ''),
|
|
509
|
+
frame_url: (() => {
|
|
510
|
+
try { return String(frame.contentWindow.location.href || ''); } catch { return ''; }
|
|
511
|
+
})()
|
|
512
|
+
};
|
|
513
|
+
})()`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async clickJobDropdownTriggerBySelector() {
|
|
517
|
+
return this.evaluate(`(() => {
|
|
518
|
+
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
519
|
+
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
520
|
+
|| document.querySelector('iframe');
|
|
521
|
+
if (!frame || !frame.contentDocument) {
|
|
522
|
+
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
523
|
+
}
|
|
524
|
+
const doc = frame.contentDocument;
|
|
525
|
+
const selectors = [
|
|
526
|
+
'.chat-job-select',
|
|
527
|
+
'.chat-job-selector',
|
|
528
|
+
'.job-selecter',
|
|
529
|
+
'.job-selector',
|
|
530
|
+
'.job-select-wrap',
|
|
531
|
+
'.job-select',
|
|
532
|
+
'.job-select-box',
|
|
533
|
+
'.job-wrap',
|
|
534
|
+
'.chat-job-name',
|
|
535
|
+
'.top-chat-search'
|
|
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
|
+
for (const selector of selectors) {
|
|
547
|
+
const el = doc.querySelector(selector);
|
|
548
|
+
if (el && isVisible(el)) {
|
|
549
|
+
el.click();
|
|
550
|
+
return { ok: true };
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return { ok: false, error: 'JOB_TRIGGER_NOT_FOUND' };
|
|
554
|
+
})()`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async ensureJobListReady() {
|
|
558
|
+
let lastError = "JOB_LIST_NOT_FOUND";
|
|
559
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
560
|
+
const state = await this.getJobListState();
|
|
561
|
+
if (state?.ok && Array.isArray(state.jobs) && state.jobs.length > 0) {
|
|
562
|
+
return state;
|
|
563
|
+
}
|
|
564
|
+
lastError = state?.error || lastError;
|
|
565
|
+
const clickResult = await this.clickJobDropdownTriggerBySelector();
|
|
566
|
+
if (!clickResult?.ok) {
|
|
567
|
+
lastError = clickResult?.error || lastError;
|
|
568
|
+
}
|
|
569
|
+
await sleep(220 + attempt * 80);
|
|
570
|
+
}
|
|
571
|
+
throw new Error(lastError);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
findJobMatch(jobList, requestedJobRaw) {
|
|
575
|
+
const requested = normalizeText(requestedJobRaw);
|
|
576
|
+
if (!requested) return null;
|
|
577
|
+
const normalizedRequestedTitle = normalizeJobTitle(requested);
|
|
578
|
+
const normalize = (value) => normalizeText(value).toLowerCase();
|
|
579
|
+
const byValue = jobList.find((job) => normalize(job.value || "") === normalize(requested));
|
|
580
|
+
if (byValue) return byValue;
|
|
581
|
+
const exactTitle = jobList.find((job) => normalize(job.title || "") === normalize(normalizedRequestedTitle));
|
|
582
|
+
if (exactTitle) return exactTitle;
|
|
583
|
+
const exactLabel = jobList.find((job) => normalize(job.label || "") === normalize(requested));
|
|
584
|
+
if (exactLabel) return exactLabel;
|
|
585
|
+
const contains = jobList.filter((job) => {
|
|
586
|
+
const title = normalize(job.title || "");
|
|
587
|
+
const label = normalize(job.label || "");
|
|
588
|
+
const target = normalize(normalizedRequestedTitle);
|
|
589
|
+
return (
|
|
590
|
+
(title && (title.includes(target) || target.includes(title)))
|
|
591
|
+
|| (label && (label.includes(normalize(requested)) || normalize(requested).includes(label)))
|
|
592
|
+
);
|
|
593
|
+
});
|
|
594
|
+
if (contains.length === 1) return contains[0];
|
|
595
|
+
if (contains.length > 1) {
|
|
596
|
+
throw new Error("JOB_SELECTION_AMBIGUOUS");
|
|
597
|
+
}
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async clickJobBySelector(job) {
|
|
602
|
+
return this.evaluate(`((job) => {
|
|
603
|
+
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
604
|
+
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
605
|
+
|| document.querySelector('iframe');
|
|
606
|
+
if (!frame || !frame.contentDocument) {
|
|
607
|
+
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
608
|
+
}
|
|
609
|
+
const doc = frame.contentDocument;
|
|
610
|
+
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
611
|
+
const normalizeTitle = (value) => {
|
|
612
|
+
const text = normalize(value);
|
|
613
|
+
if (!text) return '';
|
|
614
|
+
const byGap = text.split(/\\s{2,}/).map((item) => item.trim()).filter(Boolean)[0] || text;
|
|
615
|
+
const strippedRange = byGap
|
|
616
|
+
.replace(/\\s+\\d+(?:\\.\\d+)?\\s*(?:-|~|—|至)\\s*\\d+(?:\\.\\d+)?\\s*(?:k|K|千|万|元\\/天|元\\/月|元\\/年|K\\/月|k\\/月|万\\/月|万\\/年)?$/u, '')
|
|
617
|
+
.trim();
|
|
618
|
+
const strippedSingle = strippedRange
|
|
619
|
+
.replace(/\\s+\\d+(?:\\.\\d+)?\\s*(?:k|K|千|万|元\\/天|元\\/月|元\\/年|K\\/月|k\\/月|万\\/月|万\\/年)$/u, '')
|
|
620
|
+
.trim();
|
|
621
|
+
return strippedSingle || byGap;
|
|
622
|
+
};
|
|
623
|
+
const items = Array.from(doc.querySelectorAll([
|
|
624
|
+
'.ui-dropmenu-list .job-list .job-item',
|
|
625
|
+
'.job-selecter-options .job-list .job-item',
|
|
626
|
+
'.job-selector-options .job-list .job-item',
|
|
627
|
+
'.dropmenu-list .job-list .job-item',
|
|
628
|
+
'.job-list .job-item'
|
|
629
|
+
].join(',')));
|
|
630
|
+
const target = items.find((item) => {
|
|
631
|
+
const value = normalize(item.getAttribute('value') || item.dataset?.value || '');
|
|
632
|
+
const label = normalize(item.querySelector('.label')?.textContent || item.textContent || '');
|
|
633
|
+
const title = normalizeTitle(label);
|
|
634
|
+
const matchValue = job.value && value && value === normalize(job.value);
|
|
635
|
+
const matchTitle = job.title && title && title === normalize(job.title);
|
|
636
|
+
const matchLabel = job.label && label && label === normalize(job.label);
|
|
637
|
+
return matchValue || matchTitle || matchLabel;
|
|
638
|
+
});
|
|
639
|
+
if (!target) {
|
|
640
|
+
return { ok: false, error: 'JOB_OPTION_NOT_FOUND' };
|
|
641
|
+
}
|
|
642
|
+
target.click();
|
|
643
|
+
return { ok: true };
|
|
644
|
+
})(${JSON.stringify(job)})`);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async waitJobSelected(job, rounds = 8) {
|
|
648
|
+
const selectedValue = normalizeText(job.value || "");
|
|
649
|
+
const selectedTitle = normalizeText(job.title || "");
|
|
650
|
+
const selectedLabel = normalizeText(job.label || "");
|
|
651
|
+
for (let index = 0; index < rounds; index += 1) {
|
|
652
|
+
const state = await this.getJobListState();
|
|
653
|
+
if (state?.ok) {
|
|
654
|
+
const current = (state.jobs || []).find((item) => item.current);
|
|
655
|
+
if (current) {
|
|
656
|
+
const sameValue = selectedValue && normalizeText(current.value || "") === selectedValue;
|
|
657
|
+
const sameTitle = selectedTitle && normalizeText(current.title || "") === selectedTitle;
|
|
658
|
+
const sameLabel = selectedLabel && normalizeText(current.label || "") === selectedLabel;
|
|
659
|
+
if (sameValue || sameTitle || sameLabel) return true;
|
|
660
|
+
}
|
|
661
|
+
const selectedText = normalizeText(state.selected_label || "");
|
|
662
|
+
if (selectedTitle && selectedText && (selectedText === selectedTitle || selectedText.includes(selectedTitle))) {
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
await sleep(150 + index * 40);
|
|
667
|
+
}
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async selectJob(jobSelection) {
|
|
672
|
+
const state = await this.ensureJobListReady();
|
|
673
|
+
const matched = this.findJobMatch(state.jobs || [], jobSelection);
|
|
674
|
+
if (!matched) {
|
|
675
|
+
throw new Error("JOB_OPTION_NOT_FOUND");
|
|
676
|
+
}
|
|
677
|
+
const clicked = await this.clickJobBySelector(matched);
|
|
678
|
+
if (!clicked?.ok) {
|
|
679
|
+
throw new Error(clicked?.error || "JOB_SELECT_FAILED");
|
|
680
|
+
}
|
|
681
|
+
const selected = await this.waitJobSelected(matched, 10);
|
|
682
|
+
if (!selected) {
|
|
683
|
+
throw new Error("JOB_SELECTION_NOT_APPLIED");
|
|
684
|
+
}
|
|
685
|
+
return matched;
|
|
686
|
+
}
|
|
687
687
|
|
|
688
688
|
async isFilterPanelVisible() {
|
|
689
689
|
const result = await this.evaluate(`(() => {
|
|
@@ -792,7 +792,7 @@ class RecommendSearchCli {
|
|
|
792
792
|
})()`);
|
|
793
793
|
}
|
|
794
794
|
|
|
795
|
-
async openFilterPanel() {
|
|
795
|
+
async openFilterPanel() {
|
|
796
796
|
if (await this.isFilterPanelVisible()) return;
|
|
797
797
|
let lastError = 'FILTER_PANEL_UNAVAILABLE';
|
|
798
798
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
@@ -821,8 +821,8 @@ class RecommendSearchCli {
|
|
|
821
821
|
lastError = fallback?.error || lastError;
|
|
822
822
|
}
|
|
823
823
|
}
|
|
824
|
-
throw new Error(lastError === 'FILTER_TRIGGER_NOT_FOUND' ? lastError : 'FILTER_PANEL_UNAVAILABLE');
|
|
825
|
-
}
|
|
824
|
+
throw new Error(lastError === 'FILTER_TRIGGER_NOT_FOUND' ? lastError : 'FILTER_PANEL_UNAVAILABLE');
|
|
825
|
+
}
|
|
826
826
|
|
|
827
827
|
async closeFilterPanel() {
|
|
828
828
|
if (!(await this.isFilterPanelVisible())) {
|
|
@@ -1114,313 +1114,313 @@ class RecommendSearchCli {
|
|
|
1114
1114
|
return false;
|
|
1115
1115
|
}
|
|
1116
1116
|
|
|
1117
|
-
async getFilterGroupState(groupClass) {
|
|
1118
|
-
return this.evaluate(`((groupClass) => {
|
|
1119
|
-
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
1120
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1121
|
-
|| document.querySelector('iframe');
|
|
1122
|
-
if (!frame || !frame.contentDocument) {
|
|
1123
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1124
|
-
}
|
|
1125
|
-
const doc = frame.contentDocument;
|
|
1126
|
-
const normalize = (value) => String(value || '').replace(/\\s+/g, '').trim();
|
|
1127
|
-
const groupCandidates = Array.from(doc.querySelectorAll('.check-box'));
|
|
1128
|
-
const getOptionSet = (group) => new Set(
|
|
1129
|
-
Array.from(group.querySelectorAll('.default.option, .options .option, .option'))
|
|
1130
|
-
.map((item) => normalize(item.textContent))
|
|
1131
|
-
.filter(Boolean)
|
|
1132
|
-
);
|
|
1133
|
-
const findGroup = () => {
|
|
1134
|
-
const direct = doc.querySelector('.check-box.' + groupClass);
|
|
1135
|
-
if (direct) return direct;
|
|
1136
|
-
if (groupClass === 'school') {
|
|
1137
|
-
return groupCandidates.find((group) => {
|
|
1138
|
-
const set = getOptionSet(group);
|
|
1139
|
-
return set.has('985') || set.has('211') || set.has('双一流院校');
|
|
1140
|
-
}) || null;
|
|
1141
|
-
}
|
|
1142
|
-
if (groupClass === 'degree') {
|
|
1143
|
-
return groupCandidates.find((group) => {
|
|
1144
|
-
const set = getOptionSet(group);
|
|
1145
|
-
return set.has('大专') || set.has('本科') || set.has('硕士') || set.has('博士');
|
|
1146
|
-
}) || null;
|
|
1147
|
-
}
|
|
1148
|
-
if (groupClass === 'gender') {
|
|
1149
|
-
return groupCandidates.find((group) => {
|
|
1150
|
-
const set = getOptionSet(group);
|
|
1151
|
-
return set.has('男') || set.has('女');
|
|
1152
|
-
}) || null;
|
|
1153
|
-
}
|
|
1154
|
-
if (groupClass === 'recentNotView') {
|
|
1155
|
-
return groupCandidates.find((group) => {
|
|
1156
|
-
const set = getOptionSet(group);
|
|
1157
|
-
return set.has('近14天没有');
|
|
1158
|
-
}) || null;
|
|
1159
|
-
}
|
|
1160
|
-
return null;
|
|
1161
|
-
};
|
|
1162
|
-
|
|
1163
|
-
const group = findGroup();
|
|
1164
|
-
if (!group) {
|
|
1165
|
-
return { ok: false, error: 'GROUP_NOT_FOUND' };
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
const defaultOption = group.querySelector('.default.option');
|
|
1169
|
-
const options = Array.from(group.querySelectorAll('.default.option, .options .option, .option'));
|
|
1170
|
-
const byLabel = new Map();
|
|
1171
|
-
for (const node of options) {
|
|
1172
|
-
const label = normalize(node.textContent);
|
|
1173
|
-
if (!label) continue;
|
|
1174
|
-
const className = String(node.className || '').trim();
|
|
1175
|
-
const active = node.classList.contains('active');
|
|
1176
|
-
const existing = byLabel.get(label);
|
|
1177
|
-
if (existing) {
|
|
1178
|
-
existing.active = existing.active || active;
|
|
1179
|
-
if (className && !existing.classNames.includes(className)) {
|
|
1180
|
-
existing.classNames.push(className);
|
|
1181
|
-
}
|
|
1182
|
-
} else {
|
|
1183
|
-
byLabel.set(label, {
|
|
1184
|
-
label,
|
|
1185
|
-
active,
|
|
1186
|
-
classNames: className ? [className] : []
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
const normalizedOptions = Array.from(byLabel.values()).map((item) => ({
|
|
1192
|
-
label: item.label,
|
|
1193
|
-
active: item.active,
|
|
1194
|
-
class_name: item.classNames.join(' | ')
|
|
1195
|
-
}));
|
|
1196
|
-
return {
|
|
1197
|
-
ok: true,
|
|
1198
|
-
group_class: groupClass,
|
|
1199
|
-
defaultActive: Boolean(defaultOption && defaultOption.classList.contains('active')),
|
|
1200
|
-
defaultClassName: defaultOption ? String(defaultOption.className || '').trim() : '',
|
|
1201
|
-
options: normalizedOptions,
|
|
1202
|
-
activeLabels: normalizedOptions.filter((item) => item.active).map((item) => item.label)
|
|
1203
|
-
};
|
|
1204
|
-
})(${JSON.stringify(groupClass)})`);
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
async getSchoolFilterState() {
|
|
1208
|
-
return this.getFilterGroupState("school");
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
async selectSchoolFilter(labels) {
|
|
1212
|
-
const ensure = await this.ensureGroupReady("school");
|
|
1213
|
-
if (!ensure?.ok) {
|
|
1214
|
-
throw new Error(ensure?.error || "GROUP_NOT_FOUND");
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
const targetLabels = Array.isArray(labels) && labels.length > 0 ? labels : ["不限"];
|
|
1218
|
-
const desired = sortSchoolSelection(targetLabels);
|
|
1219
|
-
const expectDefaultOnly = desired.includes("不限");
|
|
1220
|
-
let lastState = null;
|
|
1221
|
-
|
|
1222
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1223
|
-
const state = await this.getSchoolFilterState();
|
|
1224
|
-
if (!state?.ok) {
|
|
1225
|
-
throw new Error(state?.error || "SCHOOL_FILTER_STATE_FAILED");
|
|
1226
|
-
}
|
|
1227
|
-
lastState = state;
|
|
1228
|
-
const current = sortSchoolSelection(state.activeLabels || []);
|
|
1229
|
-
const matched = expectDefaultOnly
|
|
1230
|
-
? Boolean(state.defaultActive)
|
|
1231
|
-
: (!state.defaultActive && selectionEquals(current, desired));
|
|
1232
|
-
if (matched) {
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
if (expectDefaultOnly) {
|
|
1237
|
-
await this.selectOption("school", "不限");
|
|
1238
|
-
await sleep(humanDelay(180, 50));
|
|
1239
|
-
continue;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
if (state.defaultActive) {
|
|
1243
|
-
const clearDefault = await this.clickOptionBySelector("school", "不限");
|
|
1244
|
-
if (!clearDefault?.ok) {
|
|
1245
|
-
throw new Error(clearDefault?.error || "SCHOOL_DEFAULT_CLEAR_FAILED");
|
|
1246
|
-
}
|
|
1247
|
-
await sleep(humanDelay(180, 50));
|
|
1248
|
-
}
|
|
1249
|
-
for (const label of desired) {
|
|
1250
|
-
await this.selectOption("school", label);
|
|
1251
|
-
await sleep(humanDelay(120, 40));
|
|
1252
|
-
}
|
|
1253
|
-
await sleep(humanDelay(180, 50));
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
throw new Error(`SCHOOL_FILTER_VERIFY_FAILED:${JSON.stringify(lastState || {})}`);
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
async getDegreeFilterState() {
|
|
1260
|
-
return this.getFilterGroupState("degree");
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
async getGenderFilterState() {
|
|
1264
|
-
return this.getFilterGroupState("gender");
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
async getRecentNotViewFilterState() {
|
|
1268
|
-
return this.getFilterGroupState("recentNotView");
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
async selectDegreeFilter(labels) {
|
|
1272
|
-
const ensure = await this.ensureGroupReady("degree");
|
|
1273
|
-
if (!ensure?.ok) {
|
|
1274
|
-
throw new Error(ensure?.error || "GROUP_NOT_FOUND");
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
const targetLabels = Array.isArray(labels) && labels.length > 0 ? labels : ["不限"];
|
|
1278
|
-
const desired = sortDegreeSelection(targetLabels);
|
|
1279
|
-
const expectDefaultOnly = desired.includes("不限");
|
|
1280
|
-
let lastState = null;
|
|
1281
|
-
|
|
1282
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1283
|
-
const state = await this.getDegreeFilterState();
|
|
1284
|
-
if (!state?.ok) {
|
|
1285
|
-
throw new Error(state?.error || "DEGREE_FILTER_STATE_FAILED");
|
|
1286
|
-
}
|
|
1287
|
-
lastState = state;
|
|
1288
|
-
const current = sortDegreeSelection(state.activeLabels || []);
|
|
1289
|
-
const matched = expectDefaultOnly
|
|
1290
|
-
? Boolean(state.defaultActive)
|
|
1291
|
-
: (!state.defaultActive && selectionEquals(current, desired));
|
|
1292
|
-
if (matched) {
|
|
1293
|
-
return;
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
if (expectDefaultOnly) {
|
|
1297
|
-
await this.selectOption("degree", "不限");
|
|
1298
|
-
await sleep(humanDelay(180, 50));
|
|
1299
|
-
continue;
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
if (state.defaultActive) {
|
|
1303
|
-
const clearDefault = await this.clickOptionBySelector("degree", "不限");
|
|
1304
|
-
if (!clearDefault?.ok) {
|
|
1305
|
-
throw new Error(clearDefault?.error || "DEGREE_DEFAULT_CLEAR_FAILED");
|
|
1306
|
-
}
|
|
1307
|
-
await sleep(humanDelay(180, 50));
|
|
1308
|
-
}
|
|
1309
|
-
for (const label of desired) {
|
|
1310
|
-
await this.selectOption("degree", label);
|
|
1311
|
-
await sleep(humanDelay(120, 40));
|
|
1312
|
-
}
|
|
1313
|
-
await sleep(humanDelay(180, 50));
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
throw new Error(`DEGREE_FILTER_VERIFY_FAILED:${JSON.stringify(lastState || {})}`);
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
buildGroupClassVerification(groupName, state, expectedLabels, availableOptions, sortFn) {
|
|
1320
|
-
if (!state?.ok) {
|
|
1321
|
-
return {
|
|
1322
|
-
group: groupName,
|
|
1323
|
-
ok: false,
|
|
1324
|
-
reason: state?.error || "GROUP_STATE_UNAVAILABLE",
|
|
1325
|
-
expected_labels: expectedLabels,
|
|
1326
|
-
state: state || null
|
|
1327
|
-
};
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
const expectedSorted = sortFn(uniqueNormalizedLabels(expectedLabels));
|
|
1331
|
-
const expectedSet = new Set(expectedSorted);
|
|
1332
|
-
const allowedSet = new Set(uniqueNormalizedLabels(availableOptions));
|
|
1333
|
-
const optionMap = new Map();
|
|
1334
|
-
for (const option of state.options || []) {
|
|
1335
|
-
optionMap.set(normalizeText(option.label), option);
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
const selectedNotActive = [];
|
|
1339
|
-
const unselectedButActive = [];
|
|
1340
|
-
for (const label of expectedSorted) {
|
|
1341
|
-
const option = optionMap.get(label);
|
|
1342
|
-
if (!option || option.active !== true) {
|
|
1343
|
-
selectedNotActive.push(label);
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
for (const label of allowedSet) {
|
|
1347
|
-
if (expectedSet.has(label)) continue;
|
|
1348
|
-
const option = optionMap.get(label);
|
|
1349
|
-
if (option?.active === true) {
|
|
1350
|
-
unselectedButActive.push(label);
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
const expectDefault = expectedSet.has("不限");
|
|
1355
|
-
const defaultMismatch = expectDefault ? !state.defaultActive : Boolean(state.defaultActive);
|
|
1356
|
-
const ok = (
|
|
1357
|
-
selectedNotActive.length === 0
|
|
1358
|
-
&& unselectedButActive.length === 0
|
|
1359
|
-
&& !defaultMismatch
|
|
1360
|
-
);
|
|
1361
|
-
|
|
1362
|
-
return {
|
|
1363
|
-
group: groupName,
|
|
1364
|
-
ok,
|
|
1365
|
-
expected_labels: expectedSorted,
|
|
1366
|
-
actual_active_labels: sortFn(uniqueNormalizedLabels(state.activeLabels || [])),
|
|
1367
|
-
default_active: Boolean(state.defaultActive),
|
|
1368
|
-
selected_not_active: selectedNotActive,
|
|
1369
|
-
unselected_but_active: unselectedButActive,
|
|
1370
|
-
default_mismatch: defaultMismatch,
|
|
1371
|
-
options: state.options || []
|
|
1372
|
-
};
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
async verifyFilterDomClassStates(expected) {
|
|
1376
|
-
const schoolState = await this.getSchoolFilterState();
|
|
1377
|
-
const degreeState = await this.getDegreeFilterState();
|
|
1378
|
-
const genderState = await this.getGenderFilterState();
|
|
1379
|
-
const recentState = await this.getRecentNotViewFilterState();
|
|
1380
|
-
|
|
1381
|
-
const checks = [
|
|
1382
|
-
this.buildGroupClassVerification(
|
|
1383
|
-
"school",
|
|
1384
|
-
schoolState,
|
|
1385
|
-
Array.isArray(expected?.schoolTag) && expected.schoolTag.length > 0 ? expected.schoolTag : ["不限"],
|
|
1386
|
-
SCHOOL_TAG_OPTIONS,
|
|
1387
|
-
sortSchoolSelection
|
|
1388
|
-
),
|
|
1389
|
-
this.buildGroupClassVerification(
|
|
1390
|
-
"degree",
|
|
1391
|
-
degreeState,
|
|
1392
|
-
Array.isArray(expected?.degree) && expected.degree.length > 0 ? expected.degree : ["不限"],
|
|
1393
|
-
DEGREE_OPTIONS,
|
|
1394
|
-
sortDegreeSelection
|
|
1395
|
-
),
|
|
1396
|
-
this.buildGroupClassVerification(
|
|
1397
|
-
"gender",
|
|
1398
|
-
genderState,
|
|
1399
|
-
[normalizeText(expected?.gender || "不限")],
|
|
1400
|
-
GENDER_OPTIONS,
|
|
1401
|
-
uniqueNormalizedLabels
|
|
1402
|
-
),
|
|
1403
|
-
this.buildGroupClassVerification(
|
|
1404
|
-
"recent_not_view",
|
|
1405
|
-
recentState,
|
|
1406
|
-
[normalizeText(expected?.recentNotView || "不限")],
|
|
1407
|
-
RECENT_NOT_VIEW_OPTIONS,
|
|
1408
|
-
uniqueNormalizedLabels
|
|
1409
|
-
)
|
|
1410
|
-
];
|
|
1411
|
-
const failures = checks.filter((item) => item.ok === false);
|
|
1412
|
-
return {
|
|
1413
|
-
ok: failures.length === 0,
|
|
1414
|
-
checks,
|
|
1415
|
-
failures,
|
|
1416
|
-
states: {
|
|
1417
|
-
school: schoolState,
|
|
1418
|
-
degree: degreeState,
|
|
1419
|
-
gender: genderState,
|
|
1420
|
-
recent_not_view: recentState
|
|
1421
|
-
}
|
|
1422
|
-
};
|
|
1423
|
-
}
|
|
1117
|
+
async getFilterGroupState(groupClass) {
|
|
1118
|
+
return this.evaluate(`((groupClass) => {
|
|
1119
|
+
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
1120
|
+
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1121
|
+
|| document.querySelector('iframe');
|
|
1122
|
+
if (!frame || !frame.contentDocument) {
|
|
1123
|
+
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1124
|
+
}
|
|
1125
|
+
const doc = frame.contentDocument;
|
|
1126
|
+
const normalize = (value) => String(value || '').replace(/\\s+/g, '').trim();
|
|
1127
|
+
const groupCandidates = Array.from(doc.querySelectorAll('.check-box'));
|
|
1128
|
+
const getOptionSet = (group) => new Set(
|
|
1129
|
+
Array.from(group.querySelectorAll('.default.option, .options .option, .option'))
|
|
1130
|
+
.map((item) => normalize(item.textContent))
|
|
1131
|
+
.filter(Boolean)
|
|
1132
|
+
);
|
|
1133
|
+
const findGroup = () => {
|
|
1134
|
+
const direct = doc.querySelector('.check-box.' + groupClass);
|
|
1135
|
+
if (direct) return direct;
|
|
1136
|
+
if (groupClass === 'school') {
|
|
1137
|
+
return groupCandidates.find((group) => {
|
|
1138
|
+
const set = getOptionSet(group);
|
|
1139
|
+
return set.has('985') || set.has('211') || set.has('双一流院校');
|
|
1140
|
+
}) || null;
|
|
1141
|
+
}
|
|
1142
|
+
if (groupClass === 'degree') {
|
|
1143
|
+
return groupCandidates.find((group) => {
|
|
1144
|
+
const set = getOptionSet(group);
|
|
1145
|
+
return set.has('大专') || set.has('本科') || set.has('硕士') || set.has('博士');
|
|
1146
|
+
}) || null;
|
|
1147
|
+
}
|
|
1148
|
+
if (groupClass === 'gender') {
|
|
1149
|
+
return groupCandidates.find((group) => {
|
|
1150
|
+
const set = getOptionSet(group);
|
|
1151
|
+
return set.has('男') || set.has('女');
|
|
1152
|
+
}) || null;
|
|
1153
|
+
}
|
|
1154
|
+
if (groupClass === 'recentNotView') {
|
|
1155
|
+
return groupCandidates.find((group) => {
|
|
1156
|
+
const set = getOptionSet(group);
|
|
1157
|
+
return set.has('近14天没有');
|
|
1158
|
+
}) || null;
|
|
1159
|
+
}
|
|
1160
|
+
return null;
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
const group = findGroup();
|
|
1164
|
+
if (!group) {
|
|
1165
|
+
return { ok: false, error: 'GROUP_NOT_FOUND' };
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const defaultOption = group.querySelector('.default.option');
|
|
1169
|
+
const options = Array.from(group.querySelectorAll('.default.option, .options .option, .option'));
|
|
1170
|
+
const byLabel = new Map();
|
|
1171
|
+
for (const node of options) {
|
|
1172
|
+
const label = normalize(node.textContent);
|
|
1173
|
+
if (!label) continue;
|
|
1174
|
+
const className = String(node.className || '').trim();
|
|
1175
|
+
const active = node.classList.contains('active');
|
|
1176
|
+
const existing = byLabel.get(label);
|
|
1177
|
+
if (existing) {
|
|
1178
|
+
existing.active = existing.active || active;
|
|
1179
|
+
if (className && !existing.classNames.includes(className)) {
|
|
1180
|
+
existing.classNames.push(className);
|
|
1181
|
+
}
|
|
1182
|
+
} else {
|
|
1183
|
+
byLabel.set(label, {
|
|
1184
|
+
label,
|
|
1185
|
+
active,
|
|
1186
|
+
classNames: className ? [className] : []
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const normalizedOptions = Array.from(byLabel.values()).map((item) => ({
|
|
1192
|
+
label: item.label,
|
|
1193
|
+
active: item.active,
|
|
1194
|
+
class_name: item.classNames.join(' | ')
|
|
1195
|
+
}));
|
|
1196
|
+
return {
|
|
1197
|
+
ok: true,
|
|
1198
|
+
group_class: groupClass,
|
|
1199
|
+
defaultActive: Boolean(defaultOption && defaultOption.classList.contains('active')),
|
|
1200
|
+
defaultClassName: defaultOption ? String(defaultOption.className || '').trim() : '',
|
|
1201
|
+
options: normalizedOptions,
|
|
1202
|
+
activeLabels: normalizedOptions.filter((item) => item.active).map((item) => item.label)
|
|
1203
|
+
};
|
|
1204
|
+
})(${JSON.stringify(groupClass)})`);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
async getSchoolFilterState() {
|
|
1208
|
+
return this.getFilterGroupState("school");
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
async selectSchoolFilter(labels) {
|
|
1212
|
+
const ensure = await this.ensureGroupReady("school");
|
|
1213
|
+
if (!ensure?.ok) {
|
|
1214
|
+
throw new Error(ensure?.error || "GROUP_NOT_FOUND");
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const targetLabels = Array.isArray(labels) && labels.length > 0 ? labels : ["不限"];
|
|
1218
|
+
const desired = sortSchoolSelection(targetLabels);
|
|
1219
|
+
const expectDefaultOnly = desired.includes("不限");
|
|
1220
|
+
let lastState = null;
|
|
1221
|
+
|
|
1222
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1223
|
+
const state = await this.getSchoolFilterState();
|
|
1224
|
+
if (!state?.ok) {
|
|
1225
|
+
throw new Error(state?.error || "SCHOOL_FILTER_STATE_FAILED");
|
|
1226
|
+
}
|
|
1227
|
+
lastState = state;
|
|
1228
|
+
const current = sortSchoolSelection(state.activeLabels || []);
|
|
1229
|
+
const matched = expectDefaultOnly
|
|
1230
|
+
? Boolean(state.defaultActive)
|
|
1231
|
+
: (!state.defaultActive && selectionEquals(current, desired));
|
|
1232
|
+
if (matched) {
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
if (expectDefaultOnly) {
|
|
1237
|
+
await this.selectOption("school", "不限");
|
|
1238
|
+
await sleep(humanDelay(180, 50));
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
if (state.defaultActive) {
|
|
1243
|
+
const clearDefault = await this.clickOptionBySelector("school", "不限");
|
|
1244
|
+
if (!clearDefault?.ok) {
|
|
1245
|
+
throw new Error(clearDefault?.error || "SCHOOL_DEFAULT_CLEAR_FAILED");
|
|
1246
|
+
}
|
|
1247
|
+
await sleep(humanDelay(180, 50));
|
|
1248
|
+
}
|
|
1249
|
+
for (const label of desired) {
|
|
1250
|
+
await this.selectOption("school", label);
|
|
1251
|
+
await sleep(humanDelay(120, 40));
|
|
1252
|
+
}
|
|
1253
|
+
await sleep(humanDelay(180, 50));
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
throw new Error(`SCHOOL_FILTER_VERIFY_FAILED:${JSON.stringify(lastState || {})}`);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
async getDegreeFilterState() {
|
|
1260
|
+
return this.getFilterGroupState("degree");
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
async getGenderFilterState() {
|
|
1264
|
+
return this.getFilterGroupState("gender");
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
async getRecentNotViewFilterState() {
|
|
1268
|
+
return this.getFilterGroupState("recentNotView");
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
async selectDegreeFilter(labels) {
|
|
1272
|
+
const ensure = await this.ensureGroupReady("degree");
|
|
1273
|
+
if (!ensure?.ok) {
|
|
1274
|
+
throw new Error(ensure?.error || "GROUP_NOT_FOUND");
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
const targetLabels = Array.isArray(labels) && labels.length > 0 ? labels : ["不限"];
|
|
1278
|
+
const desired = sortDegreeSelection(targetLabels);
|
|
1279
|
+
const expectDefaultOnly = desired.includes("不限");
|
|
1280
|
+
let lastState = null;
|
|
1281
|
+
|
|
1282
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1283
|
+
const state = await this.getDegreeFilterState();
|
|
1284
|
+
if (!state?.ok) {
|
|
1285
|
+
throw new Error(state?.error || "DEGREE_FILTER_STATE_FAILED");
|
|
1286
|
+
}
|
|
1287
|
+
lastState = state;
|
|
1288
|
+
const current = sortDegreeSelection(state.activeLabels || []);
|
|
1289
|
+
const matched = expectDefaultOnly
|
|
1290
|
+
? Boolean(state.defaultActive)
|
|
1291
|
+
: (!state.defaultActive && selectionEquals(current, desired));
|
|
1292
|
+
if (matched) {
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
if (expectDefaultOnly) {
|
|
1297
|
+
await this.selectOption("degree", "不限");
|
|
1298
|
+
await sleep(humanDelay(180, 50));
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (state.defaultActive) {
|
|
1303
|
+
const clearDefault = await this.clickOptionBySelector("degree", "不限");
|
|
1304
|
+
if (!clearDefault?.ok) {
|
|
1305
|
+
throw new Error(clearDefault?.error || "DEGREE_DEFAULT_CLEAR_FAILED");
|
|
1306
|
+
}
|
|
1307
|
+
await sleep(humanDelay(180, 50));
|
|
1308
|
+
}
|
|
1309
|
+
for (const label of desired) {
|
|
1310
|
+
await this.selectOption("degree", label);
|
|
1311
|
+
await sleep(humanDelay(120, 40));
|
|
1312
|
+
}
|
|
1313
|
+
await sleep(humanDelay(180, 50));
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
throw new Error(`DEGREE_FILTER_VERIFY_FAILED:${JSON.stringify(lastState || {})}`);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
buildGroupClassVerification(groupName, state, expectedLabels, availableOptions, sortFn) {
|
|
1320
|
+
if (!state?.ok) {
|
|
1321
|
+
return {
|
|
1322
|
+
group: groupName,
|
|
1323
|
+
ok: false,
|
|
1324
|
+
reason: state?.error || "GROUP_STATE_UNAVAILABLE",
|
|
1325
|
+
expected_labels: expectedLabels,
|
|
1326
|
+
state: state || null
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const expectedSorted = sortFn(uniqueNormalizedLabels(expectedLabels));
|
|
1331
|
+
const expectedSet = new Set(expectedSorted);
|
|
1332
|
+
const allowedSet = new Set(uniqueNormalizedLabels(availableOptions));
|
|
1333
|
+
const optionMap = new Map();
|
|
1334
|
+
for (const option of state.options || []) {
|
|
1335
|
+
optionMap.set(normalizeText(option.label), option);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
const selectedNotActive = [];
|
|
1339
|
+
const unselectedButActive = [];
|
|
1340
|
+
for (const label of expectedSorted) {
|
|
1341
|
+
const option = optionMap.get(label);
|
|
1342
|
+
if (!option || option.active !== true) {
|
|
1343
|
+
selectedNotActive.push(label);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
for (const label of allowedSet) {
|
|
1347
|
+
if (expectedSet.has(label)) continue;
|
|
1348
|
+
const option = optionMap.get(label);
|
|
1349
|
+
if (option?.active === true) {
|
|
1350
|
+
unselectedButActive.push(label);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
const expectDefault = expectedSet.has("不限");
|
|
1355
|
+
const defaultMismatch = expectDefault ? !state.defaultActive : Boolean(state.defaultActive);
|
|
1356
|
+
const ok = (
|
|
1357
|
+
selectedNotActive.length === 0
|
|
1358
|
+
&& unselectedButActive.length === 0
|
|
1359
|
+
&& !defaultMismatch
|
|
1360
|
+
);
|
|
1361
|
+
|
|
1362
|
+
return {
|
|
1363
|
+
group: groupName,
|
|
1364
|
+
ok,
|
|
1365
|
+
expected_labels: expectedSorted,
|
|
1366
|
+
actual_active_labels: sortFn(uniqueNormalizedLabels(state.activeLabels || [])),
|
|
1367
|
+
default_active: Boolean(state.defaultActive),
|
|
1368
|
+
selected_not_active: selectedNotActive,
|
|
1369
|
+
unselected_but_active: unselectedButActive,
|
|
1370
|
+
default_mismatch: defaultMismatch,
|
|
1371
|
+
options: state.options || []
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
async verifyFilterDomClassStates(expected) {
|
|
1376
|
+
const schoolState = await this.getSchoolFilterState();
|
|
1377
|
+
const degreeState = await this.getDegreeFilterState();
|
|
1378
|
+
const genderState = await this.getGenderFilterState();
|
|
1379
|
+
const recentState = await this.getRecentNotViewFilterState();
|
|
1380
|
+
|
|
1381
|
+
const checks = [
|
|
1382
|
+
this.buildGroupClassVerification(
|
|
1383
|
+
"school",
|
|
1384
|
+
schoolState,
|
|
1385
|
+
Array.isArray(expected?.schoolTag) && expected.schoolTag.length > 0 ? expected.schoolTag : ["不限"],
|
|
1386
|
+
SCHOOL_TAG_OPTIONS,
|
|
1387
|
+
sortSchoolSelection
|
|
1388
|
+
),
|
|
1389
|
+
this.buildGroupClassVerification(
|
|
1390
|
+
"degree",
|
|
1391
|
+
degreeState,
|
|
1392
|
+
Array.isArray(expected?.degree) && expected.degree.length > 0 ? expected.degree : ["不限"],
|
|
1393
|
+
DEGREE_OPTIONS,
|
|
1394
|
+
sortDegreeSelection
|
|
1395
|
+
),
|
|
1396
|
+
this.buildGroupClassVerification(
|
|
1397
|
+
"gender",
|
|
1398
|
+
genderState,
|
|
1399
|
+
[normalizeText(expected?.gender || "不限")],
|
|
1400
|
+
GENDER_OPTIONS,
|
|
1401
|
+
uniqueNormalizedLabels
|
|
1402
|
+
),
|
|
1403
|
+
this.buildGroupClassVerification(
|
|
1404
|
+
"recent_not_view",
|
|
1405
|
+
recentState,
|
|
1406
|
+
[normalizeText(expected?.recentNotView || "不限")],
|
|
1407
|
+
RECENT_NOT_VIEW_OPTIONS,
|
|
1408
|
+
uniqueNormalizedLabels
|
|
1409
|
+
)
|
|
1410
|
+
];
|
|
1411
|
+
const failures = checks.filter((item) => item.ok === false);
|
|
1412
|
+
return {
|
|
1413
|
+
ok: failures.length === 0,
|
|
1414
|
+
checks,
|
|
1415
|
+
failures,
|
|
1416
|
+
states: {
|
|
1417
|
+
school: schoolState,
|
|
1418
|
+
degree: degreeState,
|
|
1419
|
+
gender: genderState,
|
|
1420
|
+
recent_not_view: recentState
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
1424
|
|
|
1425
1425
|
async countCandidates() {
|
|
1426
1426
|
return this.evaluate(`(() => {
|
|
@@ -1466,95 +1466,95 @@ class RecommendSearchCli {
|
|
|
1466
1466
|
return latest;
|
|
1467
1467
|
}
|
|
1468
1468
|
|
|
1469
|
-
async run() {
|
|
1470
|
-
if (this.args.help) {
|
|
1471
|
-
console.log(JSON.stringify({
|
|
1472
|
-
status: "COMPLETED",
|
|
1473
|
-
result: {
|
|
1474
|
-
usage: "node src/cli.js --school-tag 985/211 --degree 本科及以上 --gender 男 --recent-not-view 近14天没有 --job \"算法工程师(视频/图像模型方向) _ 杭州\" --port 9222",
|
|
1475
|
-
list_jobs_usage: "node src/cli.js --list-jobs --port 9222"
|
|
1476
|
-
}
|
|
1477
|
-
}));
|
|
1478
|
-
return;
|
|
1479
|
-
}
|
|
1469
|
+
async run() {
|
|
1470
|
+
if (this.args.help) {
|
|
1471
|
+
console.log(JSON.stringify({
|
|
1472
|
+
status: "COMPLETED",
|
|
1473
|
+
result: {
|
|
1474
|
+
usage: "node src/cli.js --school-tag 985/211 --degree 本科及以上 --gender 男 --recent-not-view 近14天没有 --job \"算法工程师(视频/图像模型方向) _ 杭州\" --port 9222",
|
|
1475
|
+
list_jobs_usage: "node src/cli.js --list-jobs --port 9222"
|
|
1476
|
+
}
|
|
1477
|
+
}));
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
1480
|
if (!Array.isArray(this.args.schoolTag) || this.args.schoolTag.length === 0) {
|
|
1481
1481
|
throw new Error("INVALID_SCHOOL_TAG_INPUT");
|
|
1482
1482
|
}
|
|
1483
|
-
if (!Array.isArray(this.args.degree) || this.args.degree.length === 0) {
|
|
1484
|
-
throw new Error("INVALID_DEGREE_INPUT");
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
await this.connect();
|
|
1488
|
-
try {
|
|
1489
|
-
const frameState = await this.getFrameState();
|
|
1490
|
-
if (!frameState?.ok) {
|
|
1491
|
-
if (frameState?.error === "LOGIN_REQUIRED") {
|
|
1492
|
-
throw new Error("LOGIN_REQUIRED");
|
|
1493
|
-
}
|
|
1494
|
-
throw new Error(frameState?.error || 'NO_RECOMMEND_IFRAME');
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
if (this.args.listJobs) {
|
|
1498
|
-
const jobState = await this.ensureJobListReady();
|
|
1499
|
-
console.log(JSON.stringify({
|
|
1500
|
-
status: "COMPLETED",
|
|
1501
|
-
result: {
|
|
1502
|
-
jobs: jobState.jobs || [],
|
|
1503
|
-
page_state: {
|
|
1504
|
-
target_url: this.target?.url || null,
|
|
1505
|
-
frame_url: frameState.frameUrl || jobState.frame_url || null
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
}));
|
|
1509
|
-
return;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
let selectedJob = null;
|
|
1513
|
-
if (this.args.job) {
|
|
1514
|
-
selectedJob = await this.selectJob(this.args.job);
|
|
1515
|
-
await sleep(humanDelay(220, 70));
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
await this.openFilterPanel();
|
|
1519
|
-
await this.selectSchoolFilter(this.args.schoolTag);
|
|
1520
|
-
await this.selectOption("gender", this.args.gender);
|
|
1521
|
-
await this.selectOption("recentNotView", this.args.recentNotView);
|
|
1522
|
-
await this.selectDegreeFilter(this.args.degree);
|
|
1523
|
-
const domClassVerification = await this.verifyFilterDomClassStates({
|
|
1524
|
-
schoolTag: this.args.schoolTag,
|
|
1525
|
-
degree: this.args.degree,
|
|
1526
|
-
gender: this.args.gender,
|
|
1527
|
-
recentNotView: this.args.recentNotView
|
|
1528
|
-
});
|
|
1529
|
-
if (!domClassVerification.ok) {
|
|
1530
|
-
throw new Error(`FILTER_DOM_CLASS_VERIFY_FAILED:${JSON.stringify(domClassVerification.failures)}`);
|
|
1531
|
-
}
|
|
1532
|
-
await this.closeFilterPanel();
|
|
1533
|
-
const candidateInfo = await this.waitForCandidateCountStable();
|
|
1534
|
-
|
|
1535
|
-
console.log(JSON.stringify({
|
|
1536
|
-
status: "COMPLETED",
|
|
1537
|
-
result: {
|
|
1538
|
-
applied_filters: {
|
|
1539
|
-
school_tag: this.args.schoolTag,
|
|
1540
|
-
degree: this.args.degree,
|
|
1541
|
-
gender: this.args.gender,
|
|
1542
|
-
recent_not_view: this.args.recentNotView
|
|
1543
|
-
},
|
|
1544
|
-
verified_filters: {
|
|
1545
|
-
school: domClassVerification.states.school,
|
|
1546
|
-
degree: domClassVerification.states.degree,
|
|
1547
|
-
gender: domClassVerification.states.gender,
|
|
1548
|
-
recent_not_view: domClassVerification.states.recent_not_view,
|
|
1549
|
-
dom_class_check: {
|
|
1550
|
-
ok: domClassVerification.ok,
|
|
1551
|
-
checks: domClassVerification.checks
|
|
1552
|
-
}
|
|
1553
|
-
},
|
|
1554
|
-
selected_job: selectedJob,
|
|
1555
|
-
candidate_count: candidateInfo?.candidateCount ?? null,
|
|
1556
|
-
page_state: {
|
|
1557
|
-
target_url: this.target?.url || null,
|
|
1483
|
+
if (!Array.isArray(this.args.degree) || this.args.degree.length === 0) {
|
|
1484
|
+
throw new Error("INVALID_DEGREE_INPUT");
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
await this.connect();
|
|
1488
|
+
try {
|
|
1489
|
+
const frameState = await this.getFrameState();
|
|
1490
|
+
if (!frameState?.ok) {
|
|
1491
|
+
if (frameState?.error === "LOGIN_REQUIRED") {
|
|
1492
|
+
throw new Error("LOGIN_REQUIRED");
|
|
1493
|
+
}
|
|
1494
|
+
throw new Error(frameState?.error || 'NO_RECOMMEND_IFRAME');
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
if (this.args.listJobs) {
|
|
1498
|
+
const jobState = await this.ensureJobListReady();
|
|
1499
|
+
console.log(JSON.stringify({
|
|
1500
|
+
status: "COMPLETED",
|
|
1501
|
+
result: {
|
|
1502
|
+
jobs: jobState.jobs || [],
|
|
1503
|
+
page_state: {
|
|
1504
|
+
target_url: this.target?.url || null,
|
|
1505
|
+
frame_url: frameState.frameUrl || jobState.frame_url || null
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}));
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
let selectedJob = null;
|
|
1513
|
+
if (this.args.job) {
|
|
1514
|
+
selectedJob = await this.selectJob(this.args.job);
|
|
1515
|
+
await sleep(humanDelay(220, 70));
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
await this.openFilterPanel();
|
|
1519
|
+
await this.selectSchoolFilter(this.args.schoolTag);
|
|
1520
|
+
await this.selectOption("gender", this.args.gender);
|
|
1521
|
+
await this.selectOption("recentNotView", this.args.recentNotView);
|
|
1522
|
+
await this.selectDegreeFilter(this.args.degree);
|
|
1523
|
+
const domClassVerification = await this.verifyFilterDomClassStates({
|
|
1524
|
+
schoolTag: this.args.schoolTag,
|
|
1525
|
+
degree: this.args.degree,
|
|
1526
|
+
gender: this.args.gender,
|
|
1527
|
+
recentNotView: this.args.recentNotView
|
|
1528
|
+
});
|
|
1529
|
+
if (!domClassVerification.ok) {
|
|
1530
|
+
throw new Error(`FILTER_DOM_CLASS_VERIFY_FAILED:${JSON.stringify(domClassVerification.failures)}`);
|
|
1531
|
+
}
|
|
1532
|
+
await this.closeFilterPanel();
|
|
1533
|
+
const candidateInfo = await this.waitForCandidateCountStable();
|
|
1534
|
+
|
|
1535
|
+
console.log(JSON.stringify({
|
|
1536
|
+
status: "COMPLETED",
|
|
1537
|
+
result: {
|
|
1538
|
+
applied_filters: {
|
|
1539
|
+
school_tag: this.args.schoolTag,
|
|
1540
|
+
degree: this.args.degree,
|
|
1541
|
+
gender: this.args.gender,
|
|
1542
|
+
recent_not_view: this.args.recentNotView
|
|
1543
|
+
},
|
|
1544
|
+
verified_filters: {
|
|
1545
|
+
school: domClassVerification.states.school,
|
|
1546
|
+
degree: domClassVerification.states.degree,
|
|
1547
|
+
gender: domClassVerification.states.gender,
|
|
1548
|
+
recent_not_view: domClassVerification.states.recent_not_view,
|
|
1549
|
+
dom_class_check: {
|
|
1550
|
+
ok: domClassVerification.ok,
|
|
1551
|
+
checks: domClassVerification.checks
|
|
1552
|
+
}
|
|
1553
|
+
},
|
|
1554
|
+
selected_job: selectedJob,
|
|
1555
|
+
candidate_count: candidateInfo?.candidateCount ?? null,
|
|
1556
|
+
page_state: {
|
|
1557
|
+
target_url: this.target?.url || null,
|
|
1558
1558
|
frame_url: frameState.frameUrl || null
|
|
1559
1559
|
}
|
|
1560
1560
|
}
|
|
@@ -1565,39 +1565,39 @@ class RecommendSearchCli {
|
|
|
1565
1565
|
}
|
|
1566
1566
|
}
|
|
1567
1567
|
|
|
1568
|
-
async function main() {
|
|
1569
|
-
const args = parseArgs(process.argv.slice(2));
|
|
1570
|
-
const finalArgs = await enrichArgsFromPrompt(args);
|
|
1571
|
-
const cli = new RecommendSearchCli(finalArgs);
|
|
1572
|
-
await cli.run();
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
function isDirectExecution() {
|
|
1576
|
-
const entry = process.argv?.[1];
|
|
1577
|
-
if (!entry) return false;
|
|
1578
|
-
try {
|
|
1579
|
-
return import.meta.url === pathToFileURL(entry).href;
|
|
1580
|
-
} catch {
|
|
1581
|
-
return false;
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
if (isDirectExecution()) {
|
|
1586
|
-
main().catch((error) => {
|
|
1587
|
-
console.log(JSON.stringify({
|
|
1588
|
-
status: "FAILED",
|
|
1589
|
-
error: {
|
|
1590
|
-
code: error.message || "RECOMMEND_SEARCH_FAILED",
|
|
1591
|
-
message: error.message || "推荐页筛选执行失败。",
|
|
1592
|
-
retryable: true
|
|
1593
|
-
}
|
|
1594
|
-
}));
|
|
1595
|
-
process.exitCode = 1;
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
export {
|
|
1600
|
-
RecommendSearchCli,
|
|
1601
|
-
normalizeJobTitle,
|
|
1602
|
-
parseArgs
|
|
1603
|
-
};
|
|
1568
|
+
async function main() {
|
|
1569
|
+
const args = parseArgs(process.argv.slice(2));
|
|
1570
|
+
const finalArgs = await enrichArgsFromPrompt(args);
|
|
1571
|
+
const cli = new RecommendSearchCli(finalArgs);
|
|
1572
|
+
await cli.run();
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function isDirectExecution() {
|
|
1576
|
+
const entry = process.argv?.[1];
|
|
1577
|
+
if (!entry) return false;
|
|
1578
|
+
try {
|
|
1579
|
+
return import.meta.url === pathToFileURL(entry).href;
|
|
1580
|
+
} catch {
|
|
1581
|
+
return false;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
if (isDirectExecution()) {
|
|
1586
|
+
main().catch((error) => {
|
|
1587
|
+
console.log(JSON.stringify({
|
|
1588
|
+
status: "FAILED",
|
|
1589
|
+
error: {
|
|
1590
|
+
code: error.message || "RECOMMEND_SEARCH_FAILED",
|
|
1591
|
+
message: error.message || "推荐页筛选执行失败。",
|
|
1592
|
+
retryable: true
|
|
1593
|
+
}
|
|
1594
|
+
}));
|
|
1595
|
+
process.exitCode = 1;
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
export {
|
|
1600
|
+
RecommendSearchCli,
|
|
1601
|
+
normalizeJobTitle,
|
|
1602
|
+
parseArgs
|
|
1603
|
+
};
|