@reconcrap/boss-recommend-mcp 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/package.json +1 -1
- package/scripts/postinstall.cjs +7 -5
- package/skills/boss-recommend-pipeline/SKILL.md +6 -3
- package/src/adapters.js +6 -1
- package/src/cli.js +8 -2
- package/src/pipeline.js +30 -8
- package/src/test-pipeline.js +57 -0
- package/vendor/boss-recommend-search-cli/src/cli.js +117 -69
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ MCP 工具名:`run_recommend_pipeline`
|
|
|
27
27
|
- 页面就绪(已登录且在 recommend 页)后,会先提取岗位栏全部岗位并要求用户确认本次岗位;确认后先点击岗位,再执行 search/screen
|
|
28
28
|
- 在真正开始 search/screen 前,会进行最后一轮全参数总确认(岗位 + 全部筛选参数 + criteria + target_count + post_action + max_greet_count)
|
|
29
29
|
- npm 全局安装后会自动执行 install:生成 skill、导出 MCP 模板,并自动尝试写入已检测到的外部 agent MCP 配置(含 Trae / trae-cn / Cursor / Claude / OpenClaw)
|
|
30
|
+
- npm / npx 安装后会自动初始化 `screening-config.json` 模板(优先写入 workspace 的 `config/`,不可写时回退到用户目录)
|
|
30
31
|
- `post_action` 必须在每次完整运行开始时确认一次
|
|
31
32
|
- `target_count` 会在每次运行开始时询问一次(可留空,不设上限)
|
|
32
33
|
- 当 `post_action=greet` 时,必须在运行开始时确认 `max_greet_count`
|
|
@@ -70,11 +71,13 @@ npx -y @reconcrap/boss-recommend-mcp@latest run --instruction "推荐页筛选98
|
|
|
70
71
|
|
|
71
72
|
## 配置
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
`screening-config.json` 默认写入路径(按优先级):
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
1. `BOSS_RECOMMEND_SCREEN_CONFIG`(若已设置)
|
|
77
|
+
2. `<workspace>/config/screening-config.json`(本地 `npm install` / `npx ... install` 默认会优先写这里)
|
|
78
|
+
3. `<workspace>/boss-recommend-mcp/config/screening-config.json`
|
|
79
|
+
4. `~/.boss-recommend-mcp/screening-config.json`(当 workspace 不可写或无 workspace 时回退)
|
|
80
|
+
5. 兼容旧路径:`$CODEX_HOME/boss-recommend-mcp/screening-config.json`
|
|
78
81
|
|
|
79
82
|
配置路径优先级:
|
|
80
83
|
|
|
@@ -86,8 +89,8 @@ npx -y @reconcrap/boss-recommend-mcp@latest run --instruction "推荐页筛选98
|
|
|
86
89
|
|
|
87
90
|
注意:
|
|
88
91
|
|
|
89
|
-
- `install`
|
|
90
|
-
-
|
|
92
|
+
- `install` / `postinstall` 会自动创建 `screening-config.json` 模板(若目标路径可写)
|
|
93
|
+
- 首次运行时,若仍检测到默认占位词(如 `replace-with-openai-api-key`),pipeline 会返回配置目录并要求用户修改后确认“已修改完成”再继续
|
|
91
94
|
- 在 `npx` 临时目录(如 `AppData\\Local\\npm-cache\\_npx\\...`)执行时,不会再把该临时目录当作 `screening-config.json` 目标路径
|
|
92
95
|
|
|
93
96
|
配置样例见:
|
package/package.json
CHANGED
package/scripts/postinstall.cjs
CHANGED
|
@@ -14,16 +14,18 @@ function isGlobalInstall() {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function main() {
|
|
17
|
-
if (!isGlobalInstall()) {
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
17
|
const cliPath = path.join(__dirname, "..", "src", "cli.js");
|
|
22
18
|
if (!fs.existsSync(cliPath)) {
|
|
23
19
|
return;
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
const
|
|
22
|
+
const initCwd = String(process.env.INIT_CWD || "").trim();
|
|
23
|
+
const workspaceArgs = initCwd ? ["--workspace-root", path.resolve(initCwd)] : [];
|
|
24
|
+
const cliArgs = isGlobalInstall()
|
|
25
|
+
? [cliPath, "install", ...workspaceArgs]
|
|
26
|
+
: [cliPath, "init-config", ...workspaceArgs];
|
|
27
|
+
|
|
28
|
+
const result = spawnSync(process.execPath, cliArgs, {
|
|
27
29
|
cwd: path.join(__dirname, ".."),
|
|
28
30
|
stdio: "inherit",
|
|
29
31
|
windowsHide: true,
|
|
@@ -131,7 +131,8 @@ CLI fallback 的状态机与 MCP 保持一致:
|
|
|
131
131
|
执行前先检查:
|
|
132
132
|
|
|
133
133
|
- `boss-recommend-mcp` 是否已安装
|
|
134
|
-
- `screening-config.json`
|
|
134
|
+
- `screening-config.json` 是否存在(安装后通常会自动生成模板)
|
|
135
|
+
- `baseUrl/apiKey/model` 是否已由用户填写为可用值(不能是模板占位符)
|
|
135
136
|
- Chrome 远程调试端口是否可连
|
|
136
137
|
- 当前 Chrome 是否停留在 `https://www.zhipin.com/web/chat/recommend`
|
|
137
138
|
|
|
@@ -145,11 +146,13 @@ CLI fallback 的状态机与 MCP 保持一致:
|
|
|
145
146
|
- 若检测到 Boss 未登录:提示用户先登录;用户登录后先 navigate 到 recommend 页面再继续
|
|
146
147
|
- 自动修复后仍失败时,才提示用户介入并等待“已就绪”后重试
|
|
147
148
|
|
|
148
|
-
## Preflight 失败自动修复
|
|
149
|
-
|
|
149
|
+
## Preflight 失败自动修复
|
|
150
|
+
|
|
150
151
|
当工具返回 `status=FAILED` 且 `error.code=PIPELINE_PREFLIGHT_FAILED` 时:
|
|
151
152
|
|
|
152
153
|
1. 若 `diagnostics.checks` 中 `screen_config` 失败,优先引导用户填写 `screening-config.json` 的 `baseUrl/apiKey/model`(必须让用户提供真实值,不可保留模板值)。
|
|
154
|
+
- 若 `required_user_action=confirm_screening_config_updated`,表示检测到默认占位词未替换。
|
|
155
|
+
- 这时必须先把 `guidance.config_dir` / `guidance.config_path` 告诉用户,让用户去该目录修改后明确回复“已修改完成”,再继续下一步。
|
|
153
156
|
- 禁止 agent 自行代填或猜测示例值(如 `test-key` / `mock-key` / `https://example.com` / `gpt-4` 占位等)
|
|
154
157
|
- 必须逐项向用户确认 `baseUrl`、`apiKey`、`model` 后再写入
|
|
155
158
|
2. 优先查看 `diagnostics.auto_repair`,若有自动修复动作则先基于其结果继续执行或给出最小化补救提示。
|
package/src/adapters.js
CHANGED
|
@@ -198,6 +198,7 @@ function validateScreenConfig(config) {
|
|
|
198
198
|
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
199
199
|
return {
|
|
200
200
|
ok: false,
|
|
201
|
+
reason: "INVALID_OR_MISSING_CONFIG",
|
|
201
202
|
message: "screening-config.json 缺失或格式无效。请填写 baseUrl、apiKey、model。"
|
|
202
203
|
};
|
|
203
204
|
}
|
|
@@ -211,12 +212,14 @@ function validateScreenConfig(config) {
|
|
|
211
212
|
if (missing.length > 0) {
|
|
212
213
|
return {
|
|
213
214
|
ok: false,
|
|
215
|
+
reason: "MISSING_REQUIRED_FIELDS",
|
|
214
216
|
message: `screening-config.json 缺少必填字段:${missing.join(", ")}。`
|
|
215
217
|
};
|
|
216
218
|
}
|
|
217
219
|
if (/^replace-with/i.test(apiKey) || apiKey === screenConfigTemplateDefaults.apiKey) {
|
|
218
220
|
return {
|
|
219
221
|
ok: false,
|
|
222
|
+
reason: "PLACEHOLDER_API_KEY",
|
|
220
223
|
message: "screening-config.json 的 apiKey 仍是模板占位符,请填写真实 API Key。"
|
|
221
224
|
};
|
|
222
225
|
}
|
|
@@ -227,10 +230,11 @@ function validateScreenConfig(config) {
|
|
|
227
230
|
) {
|
|
228
231
|
return {
|
|
229
232
|
ok: false,
|
|
233
|
+
reason: "PLACEHOLDER_TEMPLATE_VALUES",
|
|
230
234
|
message: "screening-config.json 仍是默认模板值,请填写 baseUrl、apiKey、model。"
|
|
231
235
|
};
|
|
232
236
|
}
|
|
233
|
-
return { ok: true, message: "screening-config.json 校验通过。" };
|
|
237
|
+
return { ok: true, reason: "OK", message: "screening-config.json 校验通过。" };
|
|
234
238
|
}
|
|
235
239
|
|
|
236
240
|
function resolveWorkspaceDebugPort(workspaceRoot) {
|
|
@@ -696,6 +700,7 @@ export function runPipelinePreflight(workspaceRoot) {
|
|
|
696
700
|
key: "screen_config",
|
|
697
701
|
ok: screenConfigValidation.ok,
|
|
698
702
|
path: screenConfigPath,
|
|
703
|
+
reason: screenConfigValidation.reason || null,
|
|
699
704
|
message: screenConfigValidation.ok ? "screening-config.json 可用" : screenConfigValidation.message
|
|
700
705
|
}
|
|
701
706
|
];
|
package/src/cli.js
CHANGED
|
@@ -1016,7 +1016,7 @@ function printHelp() {
|
|
|
1016
1016
|
console.log(" boss-recommend-mcp Start the MCP server");
|
|
1017
1017
|
console.log(" boss-recommend-mcp start Start the MCP server");
|
|
1018
1018
|
console.log(" boss-recommend-mcp run Run the recommend pipeline once via CLI and print JSON");
|
|
1019
|
-
console.log(" boss-recommend-mcp install Install skill and
|
|
1019
|
+
console.log(" boss-recommend-mcp install Install skill/MCP templates and auto-init screening-config.json (supports --agent trae-cn/cursor/...)");
|
|
1020
1020
|
console.log(" boss-recommend-mcp install-skill Install only the Codex skill");
|
|
1021
1021
|
console.log(" boss-recommend-mcp init-config Create screening-config.json if missing (prefer workspace config/, fallback ~/.boss-recommend-mcp)");
|
|
1022
1022
|
console.log(" boss-recommend-mcp config set Write baseUrl/apiKey/model (prefer workspace config/, fallback ~/.boss-recommend-mcp)");
|
|
@@ -1048,11 +1048,17 @@ function printMcpConfig(options = {}) {
|
|
|
1048
1048
|
|
|
1049
1049
|
function installAll(options = {}) {
|
|
1050
1050
|
const skillTarget = installSkill();
|
|
1051
|
+
const configResult = ensureUserConfig(options);
|
|
1051
1052
|
const mcpTemplateResult = writeMcpConfigFiles({ client: "all" });
|
|
1052
1053
|
const externalMcpResult = installExternalMcpConfigs(options);
|
|
1053
1054
|
const externalSkillResult = mirrorSkillToExternalDirs(options);
|
|
1054
1055
|
console.log(`Skill installed to: ${skillTarget}`);
|
|
1055
|
-
console.log(
|
|
1056
|
+
console.log(
|
|
1057
|
+
configResult.created
|
|
1058
|
+
? `screening-config.json created: ${configResult.path}`
|
|
1059
|
+
: `screening-config.json already exists: ${configResult.path}`
|
|
1060
|
+
);
|
|
1061
|
+
console.log(`请在该目录修改 baseUrl/apiKey/model 并替换占位词后再运行:${path.dirname(configResult.path)}`);
|
|
1056
1062
|
console.log(`MCP config templates exported to: ${mcpTemplateResult.outputDir}`);
|
|
1057
1063
|
for (const item of mcpTemplateResult.files) {
|
|
1058
1064
|
console.log(`- ${item.client}: ${item.file}`);
|
package/src/pipeline.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { parseRecommendInstruction } from "./parser.js";
|
|
2
3
|
import {
|
|
3
4
|
attemptPipelineAutoRepair,
|
|
4
5
|
ensureBossRecommendPageReady,
|
|
@@ -441,6 +442,14 @@ export async function runRecommendPipeline(
|
|
|
441
442
|
|
|
442
443
|
if (!preflight.ok) {
|
|
443
444
|
const screenConfigCheck = preflight.checks?.find((item) => item?.key === "screen_config" && item?.ok === false);
|
|
445
|
+
const screenConfigPath = String(screenConfigCheck?.path || "");
|
|
446
|
+
const screenConfigDir = screenConfigPath ? path.dirname(screenConfigPath) : null;
|
|
447
|
+
const screenConfigReason = String(screenConfigCheck?.reason || "").trim().toUpperCase();
|
|
448
|
+
const screenConfigMessage = String(screenConfigCheck?.message || "");
|
|
449
|
+
const screenConfigHasPlaceholder = (
|
|
450
|
+
screenConfigReason.includes("PLACEHOLDER")
|
|
451
|
+
|| /占位符|默认模板值|replace-with-openai-api-key/i.test(screenConfigMessage)
|
|
452
|
+
);
|
|
444
453
|
const recovery = buildPreflightRecovery(preflight.checks, workspaceRoot);
|
|
445
454
|
return buildFailedResponse(
|
|
446
455
|
"PIPELINE_PREFLIGHT_FAILED",
|
|
@@ -448,17 +457,30 @@ export async function runRecommendPipeline(
|
|
|
448
457
|
{
|
|
449
458
|
search_params: parsed.searchParams,
|
|
450
459
|
screen_params: parsed.screenParams,
|
|
451
|
-
required_user_action: screenConfigCheck
|
|
460
|
+
required_user_action: screenConfigCheck
|
|
461
|
+
? (screenConfigHasPlaceholder ? "confirm_screening_config_updated" : "provide_screening_config")
|
|
462
|
+
: undefined,
|
|
452
463
|
guidance: screenConfigCheck
|
|
453
464
|
? {
|
|
454
465
|
config_path: screenConfigCheck.path,
|
|
466
|
+
config_dir: screenConfigDir,
|
|
455
467
|
agent_prompt: [
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
468
|
+
...(screenConfigHasPlaceholder
|
|
469
|
+
? [
|
|
470
|
+
"检测到 screening-config.json 仍包含默认占位词,当前禁止继续执行。",
|
|
471
|
+
`请引导用户在以下目录修改配置文件:${screenConfigDir || "(unknown)"}`,
|
|
472
|
+
`配置文件路径:${screenConfigCheck.path}`,
|
|
473
|
+
"必须替换为真实可用值:baseUrl、apiKey、model(不要保留任何模板占位符)。",
|
|
474
|
+
"修改完成后,必须先让用户明确回复“已修改完成”,再继续下一步。"
|
|
475
|
+
]
|
|
476
|
+
: [
|
|
477
|
+
"请先让用户填写 screening-config.json 的以下字段:",
|
|
478
|
+
"1) baseUrl",
|
|
479
|
+
"2) apiKey",
|
|
480
|
+
"3) model",
|
|
481
|
+
`配置文件路径:${screenConfigCheck.path}`,
|
|
482
|
+
"注意:不要使用模板占位符(例如 replace-with-openai-api-key),也不要由 agent 自行猜测或代填示例值。必须向用户逐项确认真实可用值后再重试。"
|
|
483
|
+
])
|
|
462
484
|
].join("\n")
|
|
463
485
|
}
|
|
464
486
|
: undefined,
|
package/src/test-pipeline.js
CHANGED
|
@@ -543,6 +543,7 @@ async function testScreenConfigFailureShouldRequireUserProvidedConfig() {
|
|
|
543
543
|
key: "screen_config",
|
|
544
544
|
ok: false,
|
|
545
545
|
path: "C:/Users/test/.boss-recommend-mcp/screening-config.json",
|
|
546
|
+
reason: "MISSING_REQUIRED_FIELDS",
|
|
546
547
|
message: "screening-config.json 缺失或格式无效"
|
|
547
548
|
}
|
|
548
549
|
]
|
|
@@ -558,6 +559,7 @@ async function testScreenConfigFailureShouldRequireUserProvidedConfig() {
|
|
|
558
559
|
key: "screen_config",
|
|
559
560
|
ok: false,
|
|
560
561
|
path: "C:/Users/test/.boss-recommend-mcp/screening-config.json",
|
|
562
|
+
reason: "MISSING_REQUIRED_FIELDS",
|
|
561
563
|
message: "screening-config.json 缺失或格式无效"
|
|
562
564
|
}
|
|
563
565
|
]
|
|
@@ -573,11 +575,65 @@ async function testScreenConfigFailureShouldRequireUserProvidedConfig() {
|
|
|
573
575
|
assert.equal(result.error.code, "PIPELINE_PREFLIGHT_FAILED");
|
|
574
576
|
assert.equal(result.required_user_action, "provide_screening_config");
|
|
575
577
|
assert.equal(result.guidance.config_path.includes("screening-config.json"), true);
|
|
578
|
+
assert.equal(result.guidance.config_dir.includes(".boss-recommend-mcp"), true);
|
|
576
579
|
assert.equal(result.guidance.agent_prompt.includes("baseUrl"), true);
|
|
577
580
|
assert.equal(result.guidance.agent_prompt.includes("apiKey"), true);
|
|
578
581
|
assert.equal(result.guidance.agent_prompt.includes("model"), true);
|
|
579
582
|
}
|
|
580
583
|
|
|
584
|
+
async function testScreenConfigPlaceholderShouldRequireUserConfirmationAfterUpdate() {
|
|
585
|
+
const result = await runRecommendPipeline(
|
|
586
|
+
{
|
|
587
|
+
workspaceRoot: process.cwd(),
|
|
588
|
+
instruction: "test",
|
|
589
|
+
confirmation: {},
|
|
590
|
+
overrides: {}
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
parseRecommendInstruction: () => createParsed(),
|
|
594
|
+
runPipelinePreflight: () => ({
|
|
595
|
+
ok: false,
|
|
596
|
+
debug_port: 9222,
|
|
597
|
+
checks: [
|
|
598
|
+
{
|
|
599
|
+
key: "screen_config",
|
|
600
|
+
ok: false,
|
|
601
|
+
path: "C:/Users/test/workspace/config/screening-config.json",
|
|
602
|
+
reason: "PLACEHOLDER_API_KEY",
|
|
603
|
+
message: "screening-config.json 的 apiKey 仍是模板占位符,请填写真实 API Key。"
|
|
604
|
+
}
|
|
605
|
+
]
|
|
606
|
+
}),
|
|
607
|
+
attemptPipelineAutoRepair: () => ({
|
|
608
|
+
attempted: false,
|
|
609
|
+
actions: [],
|
|
610
|
+
preflight: {
|
|
611
|
+
ok: false,
|
|
612
|
+
debug_port: 9222,
|
|
613
|
+
checks: [
|
|
614
|
+
{
|
|
615
|
+
key: "screen_config",
|
|
616
|
+
ok: false,
|
|
617
|
+
path: "C:/Users/test/workspace/config/screening-config.json",
|
|
618
|
+
reason: "PLACEHOLDER_API_KEY",
|
|
619
|
+
message: "screening-config.json 的 apiKey 仍是模板占位符,请填写真实 API Key。"
|
|
620
|
+
}
|
|
621
|
+
]
|
|
622
|
+
}
|
|
623
|
+
}),
|
|
624
|
+
ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: {} }),
|
|
625
|
+
runRecommendSearchCli: async () => ({ ok: true, summary: {} }),
|
|
626
|
+
runRecommendScreenCli: async () => ({ ok: true, summary: {} })
|
|
627
|
+
}
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
assert.equal(result.status, "FAILED");
|
|
631
|
+
assert.equal(result.error.code, "PIPELINE_PREFLIGHT_FAILED");
|
|
632
|
+
assert.equal(result.required_user_action, "confirm_screening_config_updated");
|
|
633
|
+
assert.equal(result.guidance.config_dir, "C:/Users/test/workspace/config");
|
|
634
|
+
assert.equal(result.guidance.agent_prompt.includes("已修改完成"), true);
|
|
635
|
+
}
|
|
636
|
+
|
|
581
637
|
async function testScreenConfigRecoveryStepShouldBeFirst() {
|
|
582
638
|
const result = await runRecommendPipeline(
|
|
583
639
|
{
|
|
@@ -627,6 +683,7 @@ async function main() {
|
|
|
627
683
|
await testPreflightAutoRepairCanUnblockPipeline();
|
|
628
684
|
await testPreflightAutoRepairStillFailShouldExposeDiagnostics();
|
|
629
685
|
await testScreenConfigFailureShouldRequireUserProvidedConfig();
|
|
686
|
+
await testScreenConfigPlaceholderShouldRequireUserConfirmationAfterUpdate();
|
|
630
687
|
await testScreenConfigRecoveryStepShouldBeFirst();
|
|
631
688
|
console.log("pipeline tests passed");
|
|
632
689
|
}
|
|
@@ -76,9 +76,15 @@ function normalizeDegree(value) {
|
|
|
76
76
|
return DEGREE_OPTIONS.includes(normalized) ? normalized : null;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
function sortDegreeSelection(values) {
|
|
80
|
-
return Array.from(new Set(values.filter(Boolean))).sort((left, right) => DEGREE_ORDER.indexOf(left) - DEGREE_ORDER.indexOf(right));
|
|
81
|
-
}
|
|
79
|
+
function sortDegreeSelection(values) {
|
|
80
|
+
return Array.from(new Set(values.filter(Boolean))).sort((left, right) => DEGREE_ORDER.indexOf(left) - DEGREE_ORDER.indexOf(right));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function selectionEquals(left, right) {
|
|
84
|
+
if (!Array.isArray(left) || !Array.isArray(right)) return false;
|
|
85
|
+
if (left.length !== right.length) return false;
|
|
86
|
+
return left.every((value, index) => value === right[index]);
|
|
87
|
+
}
|
|
82
88
|
|
|
83
89
|
function expandDegreeAtOrAbove(value) {
|
|
84
90
|
const normalized = normalizeDegree(value);
|
|
@@ -1115,35 +1121,53 @@ class RecommendSearchCli {
|
|
|
1115
1121
|
})()`);
|
|
1116
1122
|
}
|
|
1117
1123
|
|
|
1118
|
-
async selectSchoolFilter(labels) {
|
|
1119
|
-
const ensure = await this.ensureGroupReady("school");
|
|
1120
|
-
if (!ensure?.ok) {
|
|
1121
|
-
throw new Error(ensure?.error || "GROUP_NOT_FOUND");
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
const targetLabels = Array.isArray(labels) && labels.length > 0 ? labels : ["不限"];
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1124
|
+
async selectSchoolFilter(labels) {
|
|
1125
|
+
const ensure = await this.ensureGroupReady("school");
|
|
1126
|
+
if (!ensure?.ok) {
|
|
1127
|
+
throw new Error(ensure?.error || "GROUP_NOT_FOUND");
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const targetLabels = Array.isArray(labels) && labels.length > 0 ? labels : ["不限"];
|
|
1131
|
+
const desired = sortSchoolSelection(targetLabels);
|
|
1132
|
+
const expectDefaultOnly = desired.includes("不限");
|
|
1133
|
+
let lastState = null;
|
|
1134
|
+
|
|
1135
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1136
|
+
const state = await this.getSchoolFilterState();
|
|
1137
|
+
if (!state?.ok) {
|
|
1138
|
+
throw new Error(state?.error || "SCHOOL_FILTER_STATE_FAILED");
|
|
1139
|
+
}
|
|
1140
|
+
lastState = state;
|
|
1141
|
+
const current = sortSchoolSelection(state.activeLabels || []);
|
|
1142
|
+
const matched = expectDefaultOnly
|
|
1143
|
+
? Boolean(state.defaultActive)
|
|
1144
|
+
: (!state.defaultActive && selectionEquals(current, desired));
|
|
1145
|
+
if (matched) {
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
if (expectDefaultOnly) {
|
|
1150
|
+
await this.selectOption("school", "不限");
|
|
1151
|
+
await sleep(humanDelay(180, 50));
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (state.defaultActive) {
|
|
1156
|
+
const clearDefault = await this.clickOptionBySelector("school", "不限");
|
|
1157
|
+
if (!clearDefault?.ok) {
|
|
1158
|
+
throw new Error(clearDefault?.error || "SCHOOL_DEFAULT_CLEAR_FAILED");
|
|
1159
|
+
}
|
|
1160
|
+
await sleep(humanDelay(180, 50));
|
|
1161
|
+
}
|
|
1162
|
+
for (const label of desired) {
|
|
1163
|
+
await this.selectOption("school", label);
|
|
1164
|
+
await sleep(humanDelay(120, 40));
|
|
1165
|
+
}
|
|
1166
|
+
await sleep(humanDelay(180, 50));
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
throw new Error(`SCHOOL_FILTER_VERIFY_FAILED:${JSON.stringify(lastState || {})}`);
|
|
1170
|
+
}
|
|
1147
1171
|
|
|
1148
1172
|
async getDegreeFilterState() {
|
|
1149
1173
|
return this.evaluate(`(() => {
|
|
@@ -1183,35 +1207,53 @@ class RecommendSearchCli {
|
|
|
1183
1207
|
})()`);
|
|
1184
1208
|
}
|
|
1185
1209
|
|
|
1186
|
-
async selectDegreeFilter(labels) {
|
|
1187
|
-
const ensure = await this.ensureGroupReady("degree");
|
|
1188
|
-
if (!ensure?.ok) {
|
|
1189
|
-
throw new Error(ensure?.error || "GROUP_NOT_FOUND");
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
const targetLabels = Array.isArray(labels) && labels.length > 0 ? labels : ["不限"];
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1210
|
+
async selectDegreeFilter(labels) {
|
|
1211
|
+
const ensure = await this.ensureGroupReady("degree");
|
|
1212
|
+
if (!ensure?.ok) {
|
|
1213
|
+
throw new Error(ensure?.error || "GROUP_NOT_FOUND");
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const targetLabels = Array.isArray(labels) && labels.length > 0 ? labels : ["不限"];
|
|
1217
|
+
const desired = sortDegreeSelection(targetLabels);
|
|
1218
|
+
const expectDefaultOnly = desired.includes("不限");
|
|
1219
|
+
let lastState = null;
|
|
1220
|
+
|
|
1221
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1222
|
+
const state = await this.getDegreeFilterState();
|
|
1223
|
+
if (!state?.ok) {
|
|
1224
|
+
throw new Error(state?.error || "DEGREE_FILTER_STATE_FAILED");
|
|
1225
|
+
}
|
|
1226
|
+
lastState = state;
|
|
1227
|
+
const current = sortDegreeSelection(state.activeLabels || []);
|
|
1228
|
+
const matched = expectDefaultOnly
|
|
1229
|
+
? Boolean(state.defaultActive)
|
|
1230
|
+
: (!state.defaultActive && selectionEquals(current, desired));
|
|
1231
|
+
if (matched) {
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
if (expectDefaultOnly) {
|
|
1236
|
+
await this.selectOption("degree", "不限");
|
|
1237
|
+
await sleep(humanDelay(180, 50));
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
if (state.defaultActive) {
|
|
1242
|
+
const clearDefault = await this.clickOptionBySelector("degree", "不限");
|
|
1243
|
+
if (!clearDefault?.ok) {
|
|
1244
|
+
throw new Error(clearDefault?.error || "DEGREE_DEFAULT_CLEAR_FAILED");
|
|
1245
|
+
}
|
|
1246
|
+
await sleep(humanDelay(180, 50));
|
|
1247
|
+
}
|
|
1248
|
+
for (const label of desired) {
|
|
1249
|
+
await this.selectOption("degree", label);
|
|
1250
|
+
await sleep(humanDelay(120, 40));
|
|
1251
|
+
}
|
|
1252
|
+
await sleep(humanDelay(180, 50));
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
throw new Error(`DEGREE_FILTER_VERIFY_FAILED:${JSON.stringify(lastState || {})}`);
|
|
1256
|
+
}
|
|
1215
1257
|
|
|
1216
1258
|
async countCandidates() {
|
|
1217
1259
|
return this.evaluate(`(() => {
|
|
@@ -1306,20 +1348,26 @@ class RecommendSearchCli {
|
|
|
1306
1348
|
await this.openFilterPanel();
|
|
1307
1349
|
await this.selectSchoolFilter(this.args.schoolTag);
|
|
1308
1350
|
await this.selectOption("gender", this.args.gender);
|
|
1309
|
-
await this.selectOption("recentNotView", this.args.recentNotView);
|
|
1310
|
-
await this.selectDegreeFilter(this.args.degree);
|
|
1311
|
-
await this.
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1351
|
+
await this.selectOption("recentNotView", this.args.recentNotView);
|
|
1352
|
+
await this.selectDegreeFilter(this.args.degree);
|
|
1353
|
+
const verifiedSchoolState = await this.getSchoolFilterState();
|
|
1354
|
+
const verifiedDegreeState = await this.getDegreeFilterState();
|
|
1355
|
+
await this.closeFilterPanel();
|
|
1356
|
+
const candidateInfo = await this.waitForCandidateCountStable();
|
|
1357
|
+
|
|
1358
|
+
console.log(JSON.stringify({
|
|
1359
|
+
status: "COMPLETED",
|
|
1360
|
+
result: {
|
|
1317
1361
|
applied_filters: {
|
|
1318
1362
|
school_tag: this.args.schoolTag,
|
|
1319
1363
|
degree: this.args.degree,
|
|
1320
1364
|
gender: this.args.gender,
|
|
1321
1365
|
recent_not_view: this.args.recentNotView
|
|
1322
1366
|
},
|
|
1367
|
+
verified_filters: {
|
|
1368
|
+
school: verifiedSchoolState,
|
|
1369
|
+
degree: verifiedDegreeState
|
|
1370
|
+
},
|
|
1323
1371
|
selected_job: selectedJob,
|
|
1324
1372
|
candidate_count: candidateInfo?.candidateCount ?? null,
|
|
1325
1373
|
page_state: {
|