@reconcrap/boss-recruit-mcp 1.0.1 → 1.0.3
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 +19 -5
- package/package.json +1 -1
- package/skills/boss-recruit-pipeline/README.md +10 -1
- package/skills/boss-recruit-pipeline/SKILL.md +138 -20
- package/src/adapters.js +4 -1
- package/src/cli.js +5 -3
- package/src/index.js +15 -2
- package/src/parser.js +217 -16
- package/src/pipeline.js +32 -9
- package/src/test-parser.js +60 -2
package/README.md
CHANGED
|
@@ -52,16 +52,24 @@ boss-recruit-mcp start
|
|
|
52
52
|
|
|
53
53
|
## Chrome 与校准
|
|
54
54
|
|
|
55
|
+
先确认你要使用的 Chrome 远程调试端口。推荐 `9222`,但如果你已经有一个正在运行的远程调试 Chrome,也可以继续使用那个端口。确认端口后,再执行下面的命令。
|
|
56
|
+
|
|
55
57
|
推荐先启动调试 Chrome:
|
|
56
58
|
|
|
57
59
|
```bash
|
|
58
|
-
boss-recruit-mcp launch-chrome --port
|
|
60
|
+
boss-recruit-mcp launch-chrome --port <port>
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
然后执行校准:
|
|
62
64
|
|
|
63
65
|
```bash
|
|
64
|
-
boss-recruit-mcp calibrate --port
|
|
66
|
+
boss-recruit-mcp calibrate --port <port>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
如果你的 `screening-config.json` 里配置了自定义 `calibrationFile` 路径,而该路径当前不存在,直接把校准结果输出到那个路径:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
boss-recruit-mcp calibrate --port <port> --output <expected-calibration-path>
|
|
65
73
|
```
|
|
66
74
|
|
|
67
75
|
校准前请按这个顺序操作:
|
|
@@ -78,10 +86,12 @@ boss-recruit-mcp calibrate --port 9222
|
|
|
78
86
|
$CODEX_HOME/boss-recruit-mcp/favorite-calibration.json
|
|
79
87
|
```
|
|
80
88
|
|
|
89
|
+
不要从 npm 包目录、vendor 目录、旧工作区或其他账号目录复制 `favorite-calibration.json` 来替代当前环境的校准文件;应始终在当前环境重新生成。
|
|
90
|
+
|
|
81
91
|
也可以用下面的命令检查依赖、配置和校准文件:
|
|
82
92
|
|
|
83
93
|
```bash
|
|
84
|
-
boss-recruit-mcp doctor --port
|
|
94
|
+
boss-recruit-mcp doctor --port <port>
|
|
85
95
|
```
|
|
86
96
|
|
|
87
97
|
## 工具输入
|
|
@@ -91,7 +101,9 @@ boss-recruit-mcp doctor --port 9222
|
|
|
91
101
|
"instruction": "自然语言招聘指令",
|
|
92
102
|
"confirmation": {
|
|
93
103
|
"keyword_confirmed": true,
|
|
94
|
-
"keyword_value": "ai infra"
|
|
104
|
+
"keyword_value": "ai infra",
|
|
105
|
+
"search_params_confirmed": true,
|
|
106
|
+
"use_default_for_missing": false
|
|
95
107
|
},
|
|
96
108
|
"overrides": {
|
|
97
109
|
"target_count": 500
|
|
@@ -102,7 +114,9 @@ boss-recruit-mcp doctor --port 9222
|
|
|
102
114
|
## 行为说明
|
|
103
115
|
|
|
104
116
|
- 若缺 `city/degree/schools/keyword/target_count`,返回 `NEED_INPUT`
|
|
105
|
-
- 若 keyword
|
|
117
|
+
- 若 keyword 由语义自动抽取、或搜索参数仍未被用户明确确认,返回 `NEED_CONFIRMATION`
|
|
118
|
+
- 正式执行前应先单独做一轮参数确认,把已识别参数、待确认项、缺失项、默认值风险分开给用户确认
|
|
119
|
+
- 用户未补齐缺失参数时,只有在明确同意默认值及其质量风险后,才允许继续
|
|
106
120
|
- 确认后自动执行:搜索 CLI -> 筛选 CLI
|
|
107
121
|
- 返回摘要:目标数、已处理、通过数、耗时、输出 CSV
|
|
108
122
|
- 执行前会先做本地依赖预检查,若目录 / 入口 / 配置文件缺失则返回 `PIPELINE_PREFLIGHT_FAILED`
|
package/package.json
CHANGED
|
@@ -16,7 +16,16 @@ npx @reconcrap/boss-recruit-mcp install
|
|
|
16
16
|
|
|
17
17
|
## 前置要求
|
|
18
18
|
|
|
19
|
-
- Chrome
|
|
19
|
+
- Chrome 需使用远程调试端口启动;推荐 `9222`,但也可以使用你已在运行的其他端口
|
|
20
20
|
- Boss 页面已登录
|
|
21
21
|
- 已在用户配置中填写有效的 `baseUrl`、`apiKey`、`model`
|
|
22
22
|
- 已生成 `$CODEX_HOME/boss-recruit-mcp/favorite-calibration.json`
|
|
23
|
+
|
|
24
|
+
## 运行注意事项
|
|
25
|
+
|
|
26
|
+
- 正式开始前,必须先做一轮参数确认,分开展示已识别参数、待确认参数、缺失参数。
|
|
27
|
+
- 参数确认尽量复用统一模板:`已识别参数` / `待确认或待修正` / `缺失参数` / `默认值提醒` / `请用户回复`。
|
|
28
|
+
- 端口未确认时,必须先询问用户是否使用推荐的 `9222`,或提供一个已有的其他远程调试端口,不能直接默认 `9222`。
|
|
29
|
+
- 如果识别结果里出现明显脏值或可疑字段,例如“杭州筛选做过”,必须要求用户改成标准值后再继续。
|
|
30
|
+
- 如果缺少 `favorite-calibration.json`,必须指导用户在当前环境重新校准,不能搜索或复制历史遗留校准文件来顶替。
|
|
31
|
+
- 若缺失参数仍未补齐,只能在用户明确确认接受默认值和质量风险后继续,不能静默按默认执行。
|
|
@@ -22,10 +22,12 @@
|
|
|
22
22
|
|
|
23
23
|
1. 先检查 MCP 是否已安装 / 可调用;若未安装,优先执行:
|
|
24
24
|
- `npx @reconcrap/boss-recruit-mcp install`
|
|
25
|
-
2.
|
|
26
|
-
- `
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
2. 若用户还未确认 Chrome 调试端口,必须先询问:
|
|
26
|
+
- 建议使用 `9222`
|
|
27
|
+
- 但也允许用户明确提供一个已在使用的其他远程调试端口
|
|
28
|
+
3. 在用户确认端口前,不要直接假设 `9222` 并执行任何依赖端口的命令。
|
|
29
|
+
4. 端口确认后,再检查依赖与配置:
|
|
30
|
+
- `boss-recruit-mcp doctor --port <port>`
|
|
29
31
|
5. 若 Chrome 未以远程调试模式启动,优先帮助用户启动:
|
|
30
32
|
- `boss-recruit-mcp launch-chrome --port <port>`
|
|
31
33
|
- 打开页面:`https://www.zhipin.com/web/chat/search`
|
|
@@ -35,6 +37,12 @@
|
|
|
35
37
|
|
|
36
38
|
- 如果缺少 `favorite-calibration.json`,不要直接搜索筛选。
|
|
37
39
|
- 需要明确提醒用户先做校准。
|
|
40
|
+
- 校准文件缺失时,不能去电脑里搜索其他历史遗留的 `favorite-calibration.json`,也不能复制 npm 包、vendor 目录、旧工作区、旧账号目录里的文件来凑。
|
|
41
|
+
- 只能引导用户在当前环境、当前页面布局、当前端口下重新生成校准文件。
|
|
42
|
+
- 如果 `screening-config.json` 配了自定义 `calibrationFile` 路径,而该路径缺失:
|
|
43
|
+
- 必须明确告诉用户“当前期望的校准文件路径”;
|
|
44
|
+
- 应指导用户用 `boss-recruit-mcp calibrate --port <port> --output <expected-path>` 直接生成到该路径;
|
|
45
|
+
- 不要静默改写配置,也不要把别处的文件复制过去。
|
|
38
46
|
- 提示语必须包含这几步:
|
|
39
47
|
- 打开 Boss 直聘
|
|
40
48
|
- 去到搜索页面
|
|
@@ -53,7 +61,39 @@
|
|
|
53
61
|
- Input:
|
|
54
62
|
- `instruction` (string, required)
|
|
55
63
|
- `confirmation` (object, optional)
|
|
64
|
+
- `keyword_confirmed` (boolean): 是否确认关键词
|
|
65
|
+
- `keyword_value` (string): 用户确认或改写后的关键词
|
|
66
|
+
- `search_params_confirmed` (boolean): 用户是否已明确确认当前参数集
|
|
67
|
+
- `use_default_for_missing` (boolean): 用户是否明确同意对缺失参数使用默认值
|
|
56
68
|
- `overrides` (object, optional)
|
|
69
|
+
- `city` (string)
|
|
70
|
+
- `degree` (string)
|
|
71
|
+
- `schools` (string[] | comma-separated string)
|
|
72
|
+
- `keyword` (string)
|
|
73
|
+
- `target_count` (number)
|
|
74
|
+
- Tool response 重点字段:
|
|
75
|
+
- `status`
|
|
76
|
+
- `required_confirmations`
|
|
77
|
+
- `review.extracted_search_params`
|
|
78
|
+
- `review.current_search_params`
|
|
79
|
+
- `review.missing_fields`
|
|
80
|
+
- `review.suspicious_fields`
|
|
81
|
+
- `review.default_preview`
|
|
82
|
+
- `review.applied_defaults`
|
|
83
|
+
|
|
84
|
+
## Confirmation First
|
|
85
|
+
|
|
86
|
+
- 在任何一次正式搜索 / 筛选开始前,必须先单开一轮“参数确认对话”,不能在首轮解析后直接开跑。
|
|
87
|
+
- 这轮确认对话必须把当前已提取参数、疑似错误参数、缺失参数分开列给用户。
|
|
88
|
+
- 对于明显异常或语义可疑的提取值,不能默认接受,必须让用户二次确认后才能继续。
|
|
89
|
+
- 例如:
|
|
90
|
+
- 提取出的城市像“杭州筛选做过”这类明显脏值时,必须明确告诉用户“当前识别结果可能不正确”,并请用户改成标准城市名如“杭州”。
|
|
91
|
+
- 学历、学校标签、关键词、目标人数等若提取结果带噪声、过长、混入条件短语,也都要按“待确认项”处理。
|
|
92
|
+
- 如果用户在这轮确认后仍未补全缺失项,agent 也不能直接静默开始,必须再明确说明:
|
|
93
|
+
- 哪些参数仍缺失;
|
|
94
|
+
- 将会使用什么默认值;
|
|
95
|
+
- 这些默认值会降低搜索结果质量或扩大偏差。
|
|
96
|
+
- 只有在用户明确回复“确认按这些默认值继续”后,才允许正式执行。
|
|
57
97
|
|
|
58
98
|
## Execution Policy
|
|
59
99
|
|
|
@@ -67,23 +107,42 @@
|
|
|
67
107
|
- 优先使用 `npx @reconcrap/boss-recruit-mcp install`
|
|
68
108
|
- 然后使用用户的 MCP 配置启动 `boss-recruit-mcp`
|
|
69
109
|
3. 若缺少校准文件:
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
-
|
|
80
|
-
|
|
110
|
+
- 明确提示用户先完成校准,不要直接调用流水线;
|
|
111
|
+
- 明确给出校准步骤与命令;
|
|
112
|
+
- 明确指出期望生成到哪个路径;
|
|
113
|
+
- 不要在本机搜索并复用其他 `favorite-calibration.json`。
|
|
114
|
+
4. 只有当以上条件满足时,才首次调用 `run_recruit_pipeline`(只传 `instruction`),用于“解析”,不是立刻执行最终搜索结论。
|
|
115
|
+
5. 拿到首次解析结果后,先进入单独的“参数确认对话”:
|
|
116
|
+
- 列出当前已提取到的参数;
|
|
117
|
+
- 单独标出需要用户确认的参数;
|
|
118
|
+
- 单独列出 `missing_fields` 中所有缺失项;
|
|
119
|
+
- 如果某个字段看起来明显异常、像脏字符串、或不符合标准筛选值,直接归入“待确认 / 待修正”而不是默认使用。
|
|
120
|
+
6. 缺失项常见含义:
|
|
121
|
+
- `city`: 城市,如“杭州”
|
|
122
|
+
- `degree`: 学历,如“本科”“硕士及以上”
|
|
123
|
+
- `schools`: 学校标签,如“985、211、qs100”
|
|
124
|
+
- `target_count`: 目标筛选人数,如“10”
|
|
125
|
+
- `keyword`: 搜索关键词,如“AI infra”“推荐系统”
|
|
126
|
+
7. 若返回 `NEED_INPUT`:
|
|
127
|
+
- 不要只问一次就结束;
|
|
128
|
+
- 要把缺失参数集中列出,请用户一次性补充;
|
|
129
|
+
- 若用户补充后仍有缺失,再次单独列出剩余缺失项;
|
|
130
|
+
- 若用户始终不补充,必须显式征求“是否接受默认值继续”的确认,不能直接默认执行。
|
|
131
|
+
8. 若返回 `NEED_CONFIRMATION`:
|
|
81
132
|
- 询问用户是否确认 `proposed_keyword`;
|
|
133
|
+
- 同时也要让用户确认其他已提取参数里是否有误;
|
|
82
134
|
- 若确认,带 `confirmation.keyword_confirmed=true` 和 `keyword_value` 再次调用;
|
|
83
135
|
- 若用户修改关键词,传用户给的新词作为 `keyword_value` 再次调用。
|
|
84
|
-
|
|
136
|
+
9. 当仍有缺失参数但用户想直接开始时:
|
|
137
|
+
- 先明确告知默认值及风险;
|
|
138
|
+
- 必须得到用户明确确认“可以按默认值继续”后,才能继续执行。
|
|
139
|
+
10. 只有在以下条件都满足后,才允许正式开始:
|
|
140
|
+
- 用户已经确认已提取参数无误;
|
|
141
|
+
- 缺失参数已补齐,或用户已明确接受默认值;
|
|
142
|
+
- `NEED_CONFIRMATION` 分支中的关键词也已确认。
|
|
143
|
+
11. 若返回 `COMPLETED`:
|
|
85
144
|
- 向用户返回摘要:目标数、已处理、通过数、耗时、输出文件路径。
|
|
86
|
-
|
|
145
|
+
12. 若返回 `FAILED`:
|
|
87
146
|
- 先提炼 `error.code`、`error.message`、`diagnostics`;
|
|
88
147
|
- 如果是 `PIPELINE_PREFLIGHT_FAILED`,明确指出缺失的本地目录 / 文件;
|
|
89
148
|
- 如果是 `CALIBRATION_REQUIRED`,明确提醒用户执行校准,并给出校准步骤;
|
|
@@ -96,15 +155,69 @@
|
|
|
96
155
|
- 优先鼓励用户一次性给全这些字段:城市、学历、学校标签、目标人数、核心方向关键词。
|
|
97
156
|
- 当用户提到“做过 AI infra / 推荐系统 / 搜索 / 广告 / 多模态”等经历,但没有显式写“关键词”,默认允许流水线先自动抽取,再走确认分支。
|
|
98
157
|
- 当用户附带筛选要求(如“必须发表过 CCF-A 区论文”“有开源项目”“带过团队”),这些要求应该保留在 `criteria` 中,不应被误当作搜索过滤条件。
|
|
158
|
+
- 若参数提取结果出现明显噪声、截断、短语串接、非标准枚举值,优先视为“识别不可靠”,要求用户确认,不要为了推进流程直接采用。
|
|
159
|
+
- 不要把“用户没有继续回复”解释为“默认同意”;默认值只能在用户明确口头确认后使用。
|
|
160
|
+
- 参数确认对话里,优先采用这种结构:
|
|
161
|
+
- 已识别参数
|
|
162
|
+
- 待确认 / 待修正参数
|
|
163
|
+
- 缺失参数
|
|
164
|
+
- 若继续默认执行会采用的默认值与风险
|
|
99
165
|
- 回答时不要暴露 `screening-config.json` 中的 `apiKey`、`baseUrl` 等敏感值。
|
|
100
166
|
|
|
167
|
+
## Standard Confirmation Template
|
|
168
|
+
|
|
169
|
+
- 发起参数确认时,优先复用统一结构,不要每次自由发挥。
|
|
170
|
+
- 首轮确认模板建议按下面顺序输出:
|
|
171
|
+
- `已识别参数`
|
|
172
|
+
- `待确认 / 待修正`
|
|
173
|
+
- `缺失参数`
|
|
174
|
+
- `默认值提醒(如果适用)`
|
|
175
|
+
- `请用户回复`
|
|
176
|
+
- `已识别参数` 只放当前看起来可信的值,例如:
|
|
177
|
+
- 城市:杭州
|
|
178
|
+
- 学历:本科
|
|
179
|
+
- 学校标签:985 / 211 / QS100
|
|
180
|
+
- 关键词:AI infra
|
|
181
|
+
- 目标人数:10
|
|
182
|
+
- `待确认 / 待修正` 要明确写出“识别值 -> 疑点 -> 需要用户给出的标准值”,例如:
|
|
183
|
+
- 城市:当前识别为“杭州筛选做过”,这看起来混入了其他短语,请确认是否应为“杭州”
|
|
184
|
+
- 关键词:当前识别为“AI infra 论文”,看起来混入了附加条件,请确认是否只保留“AI infra”
|
|
185
|
+
- `缺失参数` 要逐项列出,不要笼统说“还差一些信息”,例如:
|
|
186
|
+
- 缺少城市
|
|
187
|
+
- 缺少目标人数
|
|
188
|
+
- `默认值提醒` 只在仍有缺失项时出现,且必须同时包含三部分:
|
|
189
|
+
- 还缺哪些参数
|
|
190
|
+
- 若继续会使用哪些默认值
|
|
191
|
+
- 会导致搜索范围变宽、相关性下降或结果偏差增大
|
|
192
|
+
- `请用户回复` 要求用户一次性回复完整,优先使用这种收口方式:
|
|
193
|
+
- 请直接按“城市 / 学历 / 学校标签 / 关键词 / 目标人数”补充或修正
|
|
194
|
+
- 如果你接受默认值继续,请明确回复“确认按默认值继续”
|
|
195
|
+
- 若用户补充后仍有缺失,再发第二轮确认时继续复用同一结构,只保留:
|
|
196
|
+
- 已更新的参数
|
|
197
|
+
- 仍待确认项
|
|
198
|
+
- 仍缺失项
|
|
199
|
+
- 默认值风险
|
|
200
|
+
- 若用户明确表示“不想再补充,直接开始”,也不能跳过模板;要先发一版精简确认:
|
|
201
|
+
- 当前仍缺失的参数
|
|
202
|
+
- 将采用的默认值
|
|
203
|
+
- 风险提示
|
|
204
|
+
- 明确询问“请确认是否按默认值继续”
|
|
205
|
+
- 不要把下面这类表达当成有效确认:
|
|
206
|
+
- “先这样吧”
|
|
207
|
+
- “你看着办”
|
|
208
|
+
- “差不多”
|
|
209
|
+
- “随便”
|
|
210
|
+
- 只有当用户明确确认参数无误,且对默认值给出清晰同意后,才设置:
|
|
211
|
+
- `confirmation.search_params_confirmed=true`
|
|
212
|
+
- `confirmation.use_default_for_missing=true`(如适用)
|
|
213
|
+
|
|
101
214
|
## Failure Handling
|
|
102
215
|
|
|
103
216
|
- 不要把底层 stderr 原样大段贴给用户,只提炼关键错误和下一步。
|
|
104
217
|
- 如果失败原因明显是环境问题,要直接说明不是用户输入有误。
|
|
105
218
|
- 如果工具已经返回 `diagnostics.checks`,优先基于这些检查项生成排障建议。
|
|
106
219
|
- 如果工具返回 `output_csv`,在摘要里给出路径,避免重复解释内部流程。
|
|
107
|
-
-
|
|
220
|
+
- 如果端口还没确认,必须先问用户“是否使用推荐的 `9222`,还是你已经有别的远程调试端口”,不能直接把 `9222` 当成已确认值。
|
|
108
221
|
- 如果需要打开 Chrome,优先帮用户执行而不是只给命令。
|
|
109
222
|
|
|
110
223
|
## Example
|
|
@@ -115,8 +228,8 @@
|
|
|
115
228
|
|
|
116
229
|
期望行为:
|
|
117
230
|
|
|
118
|
-
1.
|
|
119
|
-
2.
|
|
231
|
+
1. 先询问用户是否使用推荐的 Chrome 调试端口 `9222`,或提供一个已有的其他端口。
|
|
232
|
+
2. 用户确认端口后,启动对应端口的调试 Chrome 并打开 Boss 搜索页面。
|
|
120
233
|
3. 先检查校准文件是否存在;若不存在,提醒用户按步骤完成校准。
|
|
121
234
|
4. 环境就绪后再首次调用流水线。
|
|
122
235
|
5. 若 keyword 被自动提取为 `AI infra`,先让用户确认。
|
|
@@ -128,4 +241,9 @@
|
|
|
128
241
|
- 优先结构化、简洁中文输出。
|
|
129
242
|
- 不展示密钥和底层敏感配置。
|
|
130
243
|
- 不跳过 `NEED_CONFIRMATION` 分支。
|
|
244
|
+
- 正式开始前,优先给用户一轮“参数确认卡片式摘要”。
|
|
245
|
+
- 参数确认阶段尽量复用统一模板,减少自由表述带来的漏项。
|
|
246
|
+
- 端口未确认时,用“推荐值 + 可选其他端口”的话术,不要直接替用户决定。
|
|
247
|
+
- 校准缺失时,直接指导用户重新校准,不要建议复制或复用任何历史 calibration 文件。
|
|
248
|
+
- 若要使用默认值,必须写明“请确认是否按默认值继续”,不能模糊带过。
|
|
131
249
|
- 若运行失败,优先给用户“现在卡在哪一步 + 怎么继续”。
|
package/src/adapters.js
CHANGED
|
@@ -214,6 +214,7 @@ export function runPipelinePreflight(workspaceRoot) {
|
|
|
214
214
|
const screenDir = resolveScreenCliDir(workspaceRoot);
|
|
215
215
|
const screenConfigPath = resolveScreenConfigPath(workspaceRoot);
|
|
216
216
|
const loaded = pathExists(screenConfigPath) ? loadScreenConfig(screenConfigPath) : null;
|
|
217
|
+
const debugPort = loaded?.ok ? resolveDebugPort(loaded.config) : resolveDebugPort(null);
|
|
217
218
|
const calibrationPath = loaded?.ok
|
|
218
219
|
? (loaded.config.calibrationFile
|
|
219
220
|
? path.resolve(path.dirname(screenConfigPath), loaded.config.calibrationFile)
|
|
@@ -260,7 +261,9 @@ export function runPipelinePreflight(workspaceRoot) {
|
|
|
260
261
|
|
|
261
262
|
return {
|
|
262
263
|
ok: checks.every((item) => item.ok),
|
|
263
|
-
checks
|
|
264
|
+
checks,
|
|
265
|
+
debug_port: debugPort,
|
|
266
|
+
calibration_path: calibrationPath
|
|
264
267
|
};
|
|
265
268
|
}
|
|
266
269
|
|
package/src/cli.js
CHANGED
|
@@ -237,9 +237,11 @@ function installAll() {
|
|
|
237
237
|
console.log("");
|
|
238
238
|
console.log("Next steps:");
|
|
239
239
|
console.log("1. Fill in baseUrl/apiKey/model in the config file above.");
|
|
240
|
-
console.log("2.
|
|
241
|
-
console.log("3. Run `boss-recruit-mcp
|
|
242
|
-
console.log("4. Run `boss-recruit-mcp
|
|
240
|
+
console.log("2. Choose a Chrome remote-debugging port (9222 is recommended, but you can reuse an existing port).");
|
|
241
|
+
console.log("3. Run `boss-recruit-mcp doctor --port <your-port>` to verify config, calibration, and runtime prerequisites.");
|
|
242
|
+
console.log("4. Run `boss-recruit-mcp launch-chrome --port <your-port>` and log in to Boss if needed.");
|
|
243
|
+
console.log("5. Run `boss-recruit-mcp calibrate --port <your-port>` to generate favorite-calibration.json for this environment.");
|
|
244
|
+
console.log("6. Run `boss-recruit-mcp start` or configure your MCP client to launch `boss-recruit-mcp`.");
|
|
243
245
|
}
|
|
244
246
|
|
|
245
247
|
const command = process.argv[2] || "start";
|
package/src/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import process from "node:process";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { runRecruitPipeline } from "./pipeline.js";
|
|
5
6
|
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const { version: SERVER_VERSION } = require("../package.json");
|
|
6
9
|
const TOOL_NAME = "run_recruit_pipeline";
|
|
7
10
|
const SERVER_NAME = "boss-recruit-mcp";
|
|
8
|
-
const SERVER_VERSION = "1.0.0";
|
|
9
11
|
|
|
10
12
|
function writeMessage(message) {
|
|
11
13
|
const body = JSON.stringify(message);
|
|
@@ -36,13 +38,24 @@ function createToolSchema() {
|
|
|
36
38
|
type: "object",
|
|
37
39
|
properties: {
|
|
38
40
|
keyword_confirmed: { type: "boolean" },
|
|
39
|
-
keyword_value: { type: "string" }
|
|
41
|
+
keyword_value: { type: "string" },
|
|
42
|
+
search_params_confirmed: { type: "boolean" },
|
|
43
|
+
use_default_for_missing: { type: "boolean" }
|
|
40
44
|
},
|
|
41
45
|
additionalProperties: false
|
|
42
46
|
},
|
|
43
47
|
overrides: {
|
|
44
48
|
type: "object",
|
|
45
49
|
properties: {
|
|
50
|
+
city: { type: "string" },
|
|
51
|
+
degree: { type: "string" },
|
|
52
|
+
schools: {
|
|
53
|
+
anyOf: [
|
|
54
|
+
{ type: "array", items: { type: "string" } },
|
|
55
|
+
{ type: "string" }
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
keyword: { type: "string" },
|
|
46
59
|
target_count: { type: "integer", minimum: 1 }
|
|
47
60
|
},
|
|
48
61
|
additionalProperties: false
|
package/src/parser.js
CHANGED
|
@@ -3,6 +3,23 @@ const SEARCH_SCHOOL_MAP = {
|
|
|
3
3
|
"211": "211院校",
|
|
4
4
|
"qs100": "QS 100"
|
|
5
5
|
};
|
|
6
|
+
const KNOWN_SCHOOL_LABELS = new Set(Object.values(SEARCH_SCHOOL_MAP));
|
|
7
|
+
const DEFAULT_PARAM_VALUES = {
|
|
8
|
+
city: null,
|
|
9
|
+
degree: "不限",
|
|
10
|
+
schools: [],
|
|
11
|
+
keyword: "算法工程师",
|
|
12
|
+
target_count: 10
|
|
13
|
+
};
|
|
14
|
+
const DEFAULT_PARAM_LABELS = {
|
|
15
|
+
city: "不限城市",
|
|
16
|
+
degree: "不限",
|
|
17
|
+
schools: "不限院校标签",
|
|
18
|
+
keyword: "算法工程师",
|
|
19
|
+
target_count: 10
|
|
20
|
+
};
|
|
21
|
+
const DEGREE_VALUES = new Set(["不限", "本科", "本科及以上", "硕士及以上", "博士"]);
|
|
22
|
+
const CITY_STOP_PATTERN = /(?:筛选|搜索|查找|找|做过|从事过|有过|相关|的人选|的人|并且|且|学历|学校|目标|必须|优先|,|。|;|;|,)/;
|
|
6
23
|
|
|
7
24
|
function normalizeText(input) {
|
|
8
25
|
return String(input || "").replace(/\s+/g, " ").trim();
|
|
@@ -12,16 +29,47 @@ function uniqueList(items) {
|
|
|
12
29
|
return Array.from(new Set(items.filter(Boolean)));
|
|
13
30
|
}
|
|
14
31
|
|
|
32
|
+
function normalizeSchoolLabel(value) {
|
|
33
|
+
if (typeof value !== "string") return null;
|
|
34
|
+
const raw = value.trim();
|
|
35
|
+
if (!raw) return null;
|
|
36
|
+
|
|
37
|
+
if (KNOWN_SCHOOL_LABELS.has(raw)) {
|
|
38
|
+
return raw;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const compact = raw.toLowerCase().replace(/\s+/g, "");
|
|
42
|
+
return SEARCH_SCHOOL_MAP[compact] || SEARCH_SCHOOL_MAP[raw] || raw;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sanitizeCityCandidate(value) {
|
|
46
|
+
if (typeof value !== "string") return null;
|
|
47
|
+
let candidate = value.trim();
|
|
48
|
+
if (!candidate) return null;
|
|
49
|
+
|
|
50
|
+
candidate = candidate.replace(/^(在|是|为)\s*/, "").trim();
|
|
51
|
+
const stopIndex = candidate.search(CITY_STOP_PATTERN);
|
|
52
|
+
if (stopIndex >= 0) {
|
|
53
|
+
candidate = candidate.slice(0, stopIndex).trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
candidate = candidate.replace(/[的\s]+$/g, "").trim();
|
|
57
|
+
return candidate || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
15
60
|
function extractCity(text) {
|
|
16
61
|
const explicitPatterns = [
|
|
17
|
-
/地点(?:在|是|为|:|:)?\s*([^\
|
|
18
|
-
/城市(?:在|是|为|:|:)?\s*([^\
|
|
62
|
+
/地点(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
|
|
63
|
+
/城市(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
|
|
64
|
+
/工作地(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
|
|
65
|
+
/base(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i
|
|
19
66
|
];
|
|
20
67
|
|
|
21
68
|
for (const pattern of explicitPatterns) {
|
|
22
69
|
const m = text.match(pattern);
|
|
23
70
|
if (m && m[1]) {
|
|
24
|
-
|
|
71
|
+
const city = sanitizeCityCandidate(m[1]);
|
|
72
|
+
if (city) return city;
|
|
25
73
|
}
|
|
26
74
|
}
|
|
27
75
|
|
|
@@ -44,6 +92,24 @@ function extractSchools(text) {
|
|
|
44
92
|
return uniqueList(schools);
|
|
45
93
|
}
|
|
46
94
|
|
|
95
|
+
function normalizeStringOverride(value) {
|
|
96
|
+
if (typeof value !== "string") return null;
|
|
97
|
+
const normalized = value.trim();
|
|
98
|
+
return normalized || null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeSchoolsOverride(value) {
|
|
102
|
+
if (Array.isArray(value)) {
|
|
103
|
+
return uniqueList(value.map(normalizeSchoolLabel));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeof value === "string") {
|
|
107
|
+
return uniqueList(value.split(/[,,]/).map(normalizeSchoolLabel));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
47
113
|
function extractKeywordExplicit(text) {
|
|
48
114
|
const patterns = [
|
|
49
115
|
/搜索关键词(?:为|是|:|:)?\s*([^\n,。;;]+)/i,
|
|
@@ -145,6 +211,10 @@ function buildScreenCriteria(text, searchParams) {
|
|
|
145
211
|
}
|
|
146
212
|
|
|
147
213
|
function resolveKeyword(parsed, confirmation) {
|
|
214
|
+
if (parsed.keyword_override) {
|
|
215
|
+
return { keyword: parsed.keyword_override, needsConfirmation: false, proposedKeyword: null };
|
|
216
|
+
}
|
|
217
|
+
|
|
148
218
|
const explicit = parsed.keyword_explicit;
|
|
149
219
|
const auto = parsed.keyword_auto;
|
|
150
220
|
const confirmed = confirmation && confirmation.keyword_confirmed === true;
|
|
@@ -175,6 +245,97 @@ function resolveKeyword(parsed, confirmation) {
|
|
|
175
245
|
return { keyword: null, needsConfirmation: false, proposedKeyword: null };
|
|
176
246
|
}
|
|
177
247
|
|
|
248
|
+
function collectSuspiciousFields(searchParams, screenParams) {
|
|
249
|
+
const suspicious = [];
|
|
250
|
+
|
|
251
|
+
if (searchParams.city && (/\s/.test(searchParams.city) || CITY_STOP_PATTERN.test(searchParams.city) || searchParams.city.length > 8)) {
|
|
252
|
+
suspicious.push({
|
|
253
|
+
field: "city",
|
|
254
|
+
value: searchParams.city,
|
|
255
|
+
reason: "城市提取结果看起来包含多余短语,请确认是否为标准城市名。"
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (searchParams.degree && !DEGREE_VALUES.has(searchParams.degree)) {
|
|
260
|
+
suspicious.push({
|
|
261
|
+
field: "degree",
|
|
262
|
+
value: searchParams.degree,
|
|
263
|
+
reason: "学历提取结果不在预期枚举内,请确认。"
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (searchParams.keyword && /城市|学历|学校|目标人数|目标数量|筛选\d+位/i.test(searchParams.keyword)) {
|
|
268
|
+
suspicious.push({
|
|
269
|
+
field: "keyword",
|
|
270
|
+
value: searchParams.keyword,
|
|
271
|
+
reason: "关键词看起来混入了筛选条件,请确认是否只保留核心方向词。"
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (screenParams.target_count && (!Number.isInteger(screenParams.target_count) || screenParams.target_count <= 0)) {
|
|
276
|
+
suspicious.push({
|
|
277
|
+
field: "target_count",
|
|
278
|
+
value: screenParams.target_count,
|
|
279
|
+
reason: "目标人数不是有效正整数,请确认。"
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return suspicious;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function buildDefaultPreview(missingFields, options = {}) {
|
|
287
|
+
const { skipKeywordDefault = false } = options;
|
|
288
|
+
return missingFields.reduce((acc, field) => {
|
|
289
|
+
if (field === "keyword" && skipKeywordDefault) {
|
|
290
|
+
return acc;
|
|
291
|
+
}
|
|
292
|
+
acc[field] = DEFAULT_PARAM_LABELS[field];
|
|
293
|
+
return acc;
|
|
294
|
+
}, {});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function applyDefaults(searchParams, screenParams, missingFields, useDefaultForMissing, options = {}) {
|
|
298
|
+
const { skipKeywordDefault = false } = options;
|
|
299
|
+
if (!useDefaultForMissing) {
|
|
300
|
+
return {
|
|
301
|
+
searchParams,
|
|
302
|
+
screenParams,
|
|
303
|
+
appliedDefaults: {}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const appliedDefaults = {};
|
|
308
|
+
const nextSearchParams = { ...searchParams };
|
|
309
|
+
const nextScreenParams = { ...screenParams };
|
|
310
|
+
|
|
311
|
+
if (missingFields.includes("city")) {
|
|
312
|
+
nextSearchParams.city = DEFAULT_PARAM_VALUES.city;
|
|
313
|
+
appliedDefaults.city = DEFAULT_PARAM_LABELS.city;
|
|
314
|
+
}
|
|
315
|
+
if (missingFields.includes("degree")) {
|
|
316
|
+
nextSearchParams.degree = DEFAULT_PARAM_VALUES.degree;
|
|
317
|
+
appliedDefaults.degree = DEFAULT_PARAM_LABELS.degree;
|
|
318
|
+
}
|
|
319
|
+
if (missingFields.includes("schools")) {
|
|
320
|
+
nextSearchParams.schools = DEFAULT_PARAM_VALUES.schools.slice();
|
|
321
|
+
appliedDefaults.schools = DEFAULT_PARAM_LABELS.schools;
|
|
322
|
+
}
|
|
323
|
+
if (missingFields.includes("keyword") && !skipKeywordDefault) {
|
|
324
|
+
nextSearchParams.keyword = DEFAULT_PARAM_VALUES.keyword;
|
|
325
|
+
appliedDefaults.keyword = DEFAULT_PARAM_LABELS.keyword;
|
|
326
|
+
}
|
|
327
|
+
if (missingFields.includes("target_count")) {
|
|
328
|
+
nextScreenParams.target_count = DEFAULT_PARAM_VALUES.target_count;
|
|
329
|
+
appliedDefaults.target_count = DEFAULT_PARAM_LABELS.target_count;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
searchParams: nextSearchParams,
|
|
334
|
+
screenParams: nextScreenParams,
|
|
335
|
+
appliedDefaults
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
178
339
|
export function parseRecruitInstruction({ instruction, confirmation, overrides }) {
|
|
179
340
|
const text = normalizeText(instruction);
|
|
180
341
|
const parsed = {
|
|
@@ -186,36 +347,76 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
|
|
|
186
347
|
target_count: extractTargetCount(text)
|
|
187
348
|
};
|
|
188
349
|
|
|
189
|
-
if (overrides
|
|
190
|
-
|
|
350
|
+
if (overrides) {
|
|
351
|
+
const overrideCity = sanitizeCityCandidate(normalizeStringOverride(overrides.city));
|
|
352
|
+
const overrideDegree = normalizeStringOverride(overrides.degree);
|
|
353
|
+
const overrideSchools = normalizeSchoolsOverride(overrides.schools);
|
|
354
|
+
const overrideKeyword = normalizeStringOverride(overrides.keyword);
|
|
355
|
+
|
|
356
|
+
if (overrideCity) parsed.city = overrideCity;
|
|
357
|
+
if (overrideDegree) parsed.degree = overrideDegree;
|
|
358
|
+
if (overrideSchools && overrideSchools.length > 0) parsed.schools = overrideSchools;
|
|
359
|
+
if (overrideKeyword) parsed.keyword_override = overrideKeyword;
|
|
360
|
+
|
|
361
|
+
if (Number.isFinite(overrides.target_count) && overrides.target_count > 0) {
|
|
362
|
+
parsed.target_count = Number.parseInt(String(overrides.target_count), 10);
|
|
363
|
+
}
|
|
191
364
|
}
|
|
192
365
|
|
|
193
366
|
const keywordResolution = resolveKeyword(parsed, confirmation);
|
|
194
|
-
const
|
|
367
|
+
const baseSearchParams = {
|
|
195
368
|
city: parsed.city,
|
|
196
369
|
degree: parsed.degree,
|
|
197
370
|
schools: parsed.schools,
|
|
198
371
|
keyword: keywordResolution.keyword
|
|
199
372
|
};
|
|
200
373
|
|
|
201
|
-
const
|
|
202
|
-
criteria: buildScreenCriteria(text,
|
|
374
|
+
const baseScreenParams = {
|
|
375
|
+
criteria: buildScreenCriteria(text, baseSearchParams),
|
|
203
376
|
target_count: parsed.target_count
|
|
204
377
|
};
|
|
205
378
|
|
|
206
|
-
const
|
|
207
|
-
if (!
|
|
208
|
-
if (!
|
|
209
|
-
if (!
|
|
210
|
-
if (!
|
|
211
|
-
if (!
|
|
379
|
+
const missingBeforeDefaults = [];
|
|
380
|
+
if (!baseSearchParams.city) missingBeforeDefaults.push("city");
|
|
381
|
+
if (!baseSearchParams.degree) missingBeforeDefaults.push("degree");
|
|
382
|
+
if (!baseSearchParams.schools || baseSearchParams.schools.length === 0) missingBeforeDefaults.push("schools");
|
|
383
|
+
if (!baseSearchParams.keyword) missingBeforeDefaults.push("keyword");
|
|
384
|
+
if (!baseScreenParams.target_count) missingBeforeDefaults.push("target_count");
|
|
385
|
+
|
|
386
|
+
const useDefaultForMissing = confirmation?.use_default_for_missing === true;
|
|
387
|
+
const skipKeywordDefault = keywordResolution.needsConfirmation;
|
|
388
|
+
const defaultPreview = buildDefaultPreview(missingBeforeDefaults, { skipKeywordDefault });
|
|
389
|
+
const { searchParams, screenParams, appliedDefaults } = applyDefaults(
|
|
390
|
+
baseSearchParams,
|
|
391
|
+
baseScreenParams,
|
|
392
|
+
missingBeforeDefaults,
|
|
393
|
+
useDefaultForMissing,
|
|
394
|
+
{ skipKeywordDefault }
|
|
395
|
+
);
|
|
396
|
+
const suspicious_fields = collectSuspiciousFields(searchParams, screenParams);
|
|
212
397
|
|
|
213
398
|
return {
|
|
214
399
|
parsed,
|
|
215
400
|
searchParams,
|
|
216
401
|
screenParams,
|
|
217
|
-
missing_fields:
|
|
402
|
+
missing_fields: missingBeforeDefaults,
|
|
403
|
+
has_unresolved_missing_fields: missingBeforeDefaults.length > 0 && !useDefaultForMissing,
|
|
404
|
+
suspicious_fields,
|
|
218
405
|
needs_keyword_confirmation: keywordResolution.needsConfirmation,
|
|
219
|
-
|
|
406
|
+
needs_search_params_confirmation: confirmation?.search_params_confirmed !== true,
|
|
407
|
+
proposed_keyword: keywordResolution.proposedKeyword,
|
|
408
|
+
default_preview: defaultPreview,
|
|
409
|
+
applied_defaults: appliedDefaults,
|
|
410
|
+
review: {
|
|
411
|
+
extracted_search_params: baseSearchParams,
|
|
412
|
+
extracted_screen_params: baseScreenParams,
|
|
413
|
+
current_search_params: searchParams,
|
|
414
|
+
current_screen_params: screenParams,
|
|
415
|
+
missing_fields: missingBeforeDefaults,
|
|
416
|
+
has_unresolved_missing_fields: missingBeforeDefaults.length > 0 && !useDefaultForMissing,
|
|
417
|
+
suspicious_fields,
|
|
418
|
+
default_preview: defaultPreview,
|
|
419
|
+
applied_defaults: appliedDefaults
|
|
420
|
+
}
|
|
220
421
|
};
|
|
221
422
|
}
|
package/src/pipeline.js
CHANGED
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
import { parseRecruitInstruction } from "./parser.js";
|
|
2
2
|
import { runPipelinePreflight, runSearchCli, runScreenCli } from "./adapters.js";
|
|
3
3
|
|
|
4
|
+
function buildRequiredConfirmations(parsedResult) {
|
|
5
|
+
const confirmations = [];
|
|
6
|
+
|
|
7
|
+
if (parsedResult.needs_search_params_confirmation) {
|
|
8
|
+
confirmations.push("search_params");
|
|
9
|
+
}
|
|
10
|
+
if (parsedResult.needs_keyword_confirmation) {
|
|
11
|
+
confirmations.push("keyword");
|
|
12
|
+
}
|
|
13
|
+
if (parsedResult.has_unresolved_missing_fields) {
|
|
14
|
+
confirmations.push("missing_fields_or_defaults");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return confirmations;
|
|
18
|
+
}
|
|
19
|
+
|
|
4
20
|
function buildNeedInputResponse(parsedResult) {
|
|
5
21
|
return {
|
|
6
22
|
status: "NEED_INPUT",
|
|
7
23
|
missing_fields: parsedResult.missing_fields,
|
|
24
|
+
proposed_keyword: parsedResult.proposed_keyword,
|
|
25
|
+
required_confirmations: buildRequiredConfirmations(parsedResult),
|
|
8
26
|
search_params: parsedResult.searchParams,
|
|
9
27
|
screen_params: parsedResult.screenParams,
|
|
28
|
+
review: parsedResult.review,
|
|
10
29
|
error: {
|
|
11
30
|
code: "MISSING_REQUIRED_FIELDS",
|
|
12
|
-
message: "
|
|
31
|
+
message: "缺少必要字段。请先补齐缺失项;若要按默认值继续,必须先明确确认默认值及其风险。",
|
|
13
32
|
retryable: true
|
|
14
33
|
}
|
|
15
34
|
};
|
|
@@ -19,11 +38,13 @@ function buildNeedConfirmationResponse(parsedResult) {
|
|
|
19
38
|
return {
|
|
20
39
|
status: "NEED_CONFIRMATION",
|
|
21
40
|
proposed_keyword: parsedResult.proposed_keyword,
|
|
41
|
+
required_confirmations: buildRequiredConfirmations(parsedResult),
|
|
22
42
|
search_params: {
|
|
23
43
|
...parsedResult.searchParams,
|
|
24
|
-
keyword: parsedResult.proposed_keyword
|
|
44
|
+
keyword: parsedResult.proposed_keyword || parsedResult.searchParams.keyword
|
|
25
45
|
},
|
|
26
|
-
screen_params: parsedResult.screenParams
|
|
46
|
+
screen_params: parsedResult.screenParams,
|
|
47
|
+
review: parsedResult.review
|
|
27
48
|
};
|
|
28
49
|
}
|
|
29
50
|
|
|
@@ -121,12 +142,12 @@ export async function runRecruitPipeline({
|
|
|
121
142
|
overrides
|
|
122
143
|
});
|
|
123
144
|
|
|
124
|
-
if (parsed.
|
|
125
|
-
return
|
|
145
|
+
if (parsed.has_unresolved_missing_fields) {
|
|
146
|
+
return buildNeedInputResponse(parsed);
|
|
126
147
|
}
|
|
127
148
|
|
|
128
|
-
if (parsed.
|
|
129
|
-
return
|
|
149
|
+
if (parsed.needs_keyword_confirmation || parsed.needs_search_params_confirmation) {
|
|
150
|
+
return buildNeedConfirmationResponse(parsed);
|
|
130
151
|
}
|
|
131
152
|
|
|
132
153
|
const preflight = runPipelinePreflight(workspaceRoot);
|
|
@@ -137,13 +158,15 @@ export async function runRecruitPipeline({
|
|
|
137
158
|
return buildFailedResponse(
|
|
138
159
|
missingCalibration ? "CALIBRATION_REQUIRED" : "PIPELINE_PREFLIGHT_FAILED",
|
|
139
160
|
missingCalibration
|
|
140
|
-
? "
|
|
161
|
+
? `尚未完成收藏按钮校准。请先在当前环境打开 Boss 搜索页,打开任意候选人详情页,点击收藏,再点击取消收藏,然后关闭详情页;接着运行 boss-recruit-mcp calibrate --port ${preflight.debug_port} --output "${preflight.calibration_path}" 生成校准文件。不要复制其他目录或历史遗留的 favorite-calibration.json。`
|
|
141
162
|
: "招聘流水线运行前检查失败,请先修复缺失的本地依赖或配置文件。",
|
|
142
163
|
{
|
|
143
164
|
search_params: parsed.searchParams,
|
|
144
165
|
screen_params: parsed.screenParams,
|
|
145
166
|
diagnostics: {
|
|
146
|
-
checks: preflight.checks
|
|
167
|
+
checks: preflight.checks,
|
|
168
|
+
debug_port: preflight.debug_port,
|
|
169
|
+
calibration_path: preflight.calibration_path
|
|
147
170
|
}
|
|
148
171
|
}
|
|
149
172
|
);
|
package/src/test-parser.js
CHANGED
|
@@ -9,7 +9,17 @@ function testNeedInput() {
|
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
assert.equal(r.needs_keyword_confirmation, true);
|
|
12
|
+
assert.equal(r.needs_search_params_confirmation, true);
|
|
12
13
|
assert.equal(r.proposed_keyword?.toLowerCase(), "ai infra");
|
|
14
|
+
assert.deepEqual(
|
|
15
|
+
r.default_preview,
|
|
16
|
+
{
|
|
17
|
+
city: "不限城市",
|
|
18
|
+
degree: "不限",
|
|
19
|
+
schools: "不限院校标签",
|
|
20
|
+
target_count: 10
|
|
21
|
+
}
|
|
22
|
+
);
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
function testExampleExtraction() {
|
|
@@ -31,11 +41,16 @@ function testExampleExtraction() {
|
|
|
31
41
|
|
|
32
42
|
const confirmed = parseRecruitInstruction({
|
|
33
43
|
instruction,
|
|
34
|
-
confirmation: {
|
|
44
|
+
confirmation: {
|
|
45
|
+
keyword_confirmed: true,
|
|
46
|
+
keyword_value: "ai infra",
|
|
47
|
+
search_params_confirmed: true
|
|
48
|
+
},
|
|
35
49
|
overrides: null
|
|
36
50
|
});
|
|
37
51
|
|
|
38
52
|
assert.equal(confirmed.needs_keyword_confirmation, false);
|
|
53
|
+
assert.equal(confirmed.needs_search_params_confirmation, false);
|
|
39
54
|
assert.equal(confirmed.searchParams.keyword, "ai infra");
|
|
40
55
|
assert.equal(confirmed.missing_fields.length, 0);
|
|
41
56
|
}
|
|
@@ -54,7 +69,11 @@ function testStructuredInputAndCriteriaCleanup() {
|
|
|
54
69
|
const r = parseRecruitInstruction({
|
|
55
70
|
instruction:
|
|
56
71
|
"使用boss-recruit-pipeline skills帮我在boss上找做过AI infra的人选,必须发表过CCF-A区论文。城市:杭州,学历:本科,学校:985、211、qs100,目标人数:10人",
|
|
57
|
-
confirmation: {
|
|
72
|
+
confirmation: {
|
|
73
|
+
keyword_confirmed: true,
|
|
74
|
+
keyword_value: "AI infra",
|
|
75
|
+
search_params_confirmed: true
|
|
76
|
+
},
|
|
58
77
|
overrides: null
|
|
59
78
|
});
|
|
60
79
|
|
|
@@ -67,11 +86,50 @@ function testStructuredInputAndCriteriaCleanup() {
|
|
|
67
86
|
);
|
|
68
87
|
}
|
|
69
88
|
|
|
89
|
+
function testCitySanitizationAndConfirmationGate() {
|
|
90
|
+
const r = parseRecruitInstruction({
|
|
91
|
+
instruction:
|
|
92
|
+
"在 Boss 直聘按城市杭州筛选做过 AI infra 的人选,学历为本科及以上,来自 985/211/QS100 学校,必须有 CCF-A 区会议论文,目标 10 人,关键词 AI infra。",
|
|
93
|
+
confirmation: null,
|
|
94
|
+
overrides: null
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
assert.equal(r.searchParams.city, "杭州");
|
|
98
|
+
assert.equal(r.needs_search_params_confirmation, true);
|
|
99
|
+
assert.equal(r.suspicious_fields.length, 0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function testDefaultsCanOnlyApplyWhenExplicitlyRequested() {
|
|
103
|
+
const r = parseRecruitInstruction({
|
|
104
|
+
instruction: "帮我找做过推荐系统的人",
|
|
105
|
+
confirmation: {
|
|
106
|
+
keyword_confirmed: true,
|
|
107
|
+
keyword_value: "推荐系统",
|
|
108
|
+
use_default_for_missing: true,
|
|
109
|
+
search_params_confirmed: true
|
|
110
|
+
},
|
|
111
|
+
overrides: null
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
assert.equal(r.searchParams.city, null);
|
|
115
|
+
assert.equal(r.searchParams.degree, "不限");
|
|
116
|
+
assert.deepEqual(r.searchParams.schools, []);
|
|
117
|
+
assert.equal(r.screenParams.target_count, 10);
|
|
118
|
+
assert.deepEqual(r.applied_defaults, {
|
|
119
|
+
city: "不限城市",
|
|
120
|
+
degree: "不限",
|
|
121
|
+
schools: "不限院校标签",
|
|
122
|
+
target_count: 10
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
70
126
|
function main() {
|
|
71
127
|
testNeedInput();
|
|
72
128
|
testExampleExtraction();
|
|
73
129
|
testMissingFieldsBatch();
|
|
74
130
|
testStructuredInputAndCriteriaCleanup();
|
|
131
|
+
testCitySanitizationAndConfirmationGate();
|
|
132
|
+
testDefaultsCanOnlyApplyWhenExplicitlyRequested();
|
|
75
133
|
// eslint-disable-next-line no-console
|
|
76
134
|
console.log("parser tests passed");
|
|
77
135
|
}
|