@reconcrap/boss-recommend-mcp 0.1.4 → 0.1.6
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 +2 -0
- package/package.json +46 -46
- package/skills/boss-recommend-pipeline/SKILL.md +124 -103
- package/src/adapters.js +197 -1
- package/src/index.js +77 -21
- package/src/parser.js +15 -3
- package/src/pipeline.js +336 -214
- package/src/test-parser.js +2 -1
- package/src/test-pipeline.js +38 -0
- package/vendor/boss-recommend-search-cli/src/cli.js +1066 -1066
package/README.md
CHANGED
|
@@ -31,6 +31,8 @@ MCP 工具名:`run_recommend_pipeline`
|
|
|
31
31
|
- 不会对每位候选人重复确认
|
|
32
32
|
- 推荐页详情处理完成后,会强制关闭详情页并确认已关闭
|
|
33
33
|
- 简历提取采用“分段滚动截图 + 拼成长图”的方式,再交给多模态模型判断
|
|
34
|
+
- 运行前会自动做依赖体检(Node.js、Python、Pillow、`chrome-remote-interface`、`ws`),缺失时会在 `doctor` 与流水线失败诊断中明确提示
|
|
35
|
+
- 若 preflight 失败,返回 `diagnostics.recovery`(含有序修复步骤与 `agent_prompt`),可直接交给 AI agent 自动按顺序安装依赖
|
|
34
36
|
|
|
35
37
|
## 安装
|
|
36
38
|
|
package/package.json
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@reconcrap/boss-recommend-mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"boss",
|
|
7
|
-
"mcp",
|
|
8
|
-
"codex",
|
|
9
|
-
"recruiting",
|
|
10
|
-
"boss-zhipin",
|
|
11
|
-
"recommend"
|
|
12
|
-
],
|
|
13
|
-
"type": "module",
|
|
14
|
-
"main": "src/index.js",
|
|
15
|
-
"bin": {
|
|
16
|
-
"boss-recommend-mcp": "bin/boss-recommend-mcp.js"
|
|
17
|
-
},
|
|
18
|
-
"scripts": {
|
|
19
|
-
"start": "node src/index.js",
|
|
20
|
-
"cli": "node src/cli.js",
|
|
21
|
-
"install:local": "node src/cli.js install",
|
|
22
|
-
"postinstall": "node scripts/postinstall.cjs",
|
|
23
|
-
"test:parser": "node src/test-parser.js",
|
|
24
|
-
"test:pipeline": "node src/test-pipeline.js"
|
|
25
|
-
},
|
|
26
|
-
"files": [
|
|
27
|
-
"bin",
|
|
28
|
-
"config/screening-config.example.json",
|
|
29
|
-
"skills",
|
|
30
|
-
"scripts/postinstall.cjs",
|
|
31
|
-
"src",
|
|
32
|
-
"vendor",
|
|
33
|
-
"README.md"
|
|
34
|
-
],
|
|
35
|
-
"dependencies": {
|
|
36
|
-
"chrome-remote-interface": "^0.33.3",
|
|
37
|
-
"ws": "^8.19.0"
|
|
38
|
-
},
|
|
39
|
-
"engines": {
|
|
40
|
-
"node": ">=18"
|
|
41
|
-
},
|
|
42
|
-
"publishConfig": {
|
|
43
|
-
"access": "public"
|
|
44
|
-
},
|
|
45
|
-
"license": "MIT"
|
|
46
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@reconcrap/boss-recommend-mcp",
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"boss",
|
|
7
|
+
"mcp",
|
|
8
|
+
"codex",
|
|
9
|
+
"recruiting",
|
|
10
|
+
"boss-zhipin",
|
|
11
|
+
"recommend"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "src/index.js",
|
|
15
|
+
"bin": {
|
|
16
|
+
"boss-recommend-mcp": "bin/boss-recommend-mcp.js"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node src/index.js",
|
|
20
|
+
"cli": "node src/cli.js",
|
|
21
|
+
"install:local": "node src/cli.js install",
|
|
22
|
+
"postinstall": "node scripts/postinstall.cjs",
|
|
23
|
+
"test:parser": "node src/test-parser.js",
|
|
24
|
+
"test:pipeline": "node src/test-pipeline.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin",
|
|
28
|
+
"config/screening-config.example.json",
|
|
29
|
+
"skills",
|
|
30
|
+
"scripts/postinstall.cjs",
|
|
31
|
+
"src",
|
|
32
|
+
"vendor",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"chrome-remote-interface": "^0.33.3",
|
|
37
|
+
"ws": "^8.19.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT"
|
|
46
|
+
}
|
|
@@ -1,103 +1,124 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: "boss-recommend-pipeline"
|
|
3
|
-
description: "Use when users ask to run Boss recommend-page filtering and screening via boss-recommend-mcp; confirm filters, criteria, optional target_count, and run-level post_action (plus max_greet_count when post_action=greet) before execution."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Boss Recommend Pipeline Skill
|
|
7
|
-
|
|
8
|
-
## Purpose
|
|
9
|
-
|
|
10
|
-
当用户希望在 Boss 推荐页按条件筛选候选人时,优先调用 MCP 工具 `run_recommend_pipeline` 完成端到端任务:
|
|
11
|
-
|
|
12
|
-
1. 解析推荐页筛选指令
|
|
13
|
-
2. 结构化推荐页 filters
|
|
14
|
-
3. 确认筛选 criteria
|
|
15
|
-
4. 在运行开始时询问 `target_count`(可选,可留空)
|
|
16
|
-
5. 在运行开始时一次性确认 `post_action`
|
|
17
|
-
6. 若 `post_action=greet`,同时确认 `max_greet_count`
|
|
18
|
-
7. 执行 recommend-search-cli 与 recommend-screen-cli
|
|
19
|
-
8. 返回结果摘要
|
|
20
|
-
|
|
21
|
-
## Required Confirmation
|
|
22
|
-
|
|
23
|
-
在真正执行前,必须先确认:
|
|
24
|
-
|
|
25
|
-
- 学校标签(`school_tag`)
|
|
26
|
-
- 学历(`degree`)
|
|
27
|
-
- 性别(`gender`)
|
|
28
|
-
- 是否过滤近14天已看(`recent_not_view`)
|
|
29
|
-
- screening criteria 是否正确
|
|
30
|
-
- `target_count`(目标筛选人数)是否需要设置(可不设上限)
|
|
31
|
-
- `post_action` 是否确定为 `favorite` 或 `greet`
|
|
32
|
-
- 当 `post_action=greet` 时,`max_greet_count`(最多打招呼人数)是否确定
|
|
33
|
-
|
|
34
|
-
`post_action` 的确认是**单次运行级别**的:
|
|
35
|
-
|
|
36
|
-
- 若用户确认 `favorite`,则本次运行中所有通过人选都统一收藏
|
|
37
|
-
- 若用户确认 `greet`,则本次运行中先按 `max_greet_count` 执行打招呼,超出上限后自动改为收藏
|
|
38
|
-
- 不要在每位候选人通过后再次逐个确认
|
|
39
|
-
|
|
40
|
-
## Tool Contract
|
|
41
|
-
|
|
42
|
-
- Tool name: `run_recommend_pipeline`
|
|
43
|
-
- Input:
|
|
44
|
-
- `instruction` (required)
|
|
45
|
-
- `confirmation`
|
|
46
|
-
- `filters_confirmed`
|
|
47
|
-
- `school_tag_confirmed`
|
|
48
|
-
- `degree_confirmed`
|
|
49
|
-
- `gender_confirmed`
|
|
50
|
-
- `recent_not_view_confirmed`
|
|
51
|
-
- `criteria_confirmed`
|
|
52
|
-
- `target_count_confirmed`
|
|
53
|
-
- `target_count_value` (integer, optional)
|
|
54
|
-
- `post_action_confirmed`
|
|
55
|
-
- `post_action_value` (`favorite|greet`)
|
|
56
|
-
- `max_greet_count_confirmed`
|
|
57
|
-
- `max_greet_count_value` (integer)
|
|
58
|
-
- `overrides`
|
|
59
|
-
- `school_tag`
|
|
60
|
-
- `degree`(可传单值或数组;如“本科及以上”应展开为 `["本科","硕士","博士"]`)
|
|
61
|
-
- `gender`
|
|
62
|
-
- `recent_not_view`
|
|
63
|
-
- `criteria`
|
|
64
|
-
- `target_count`
|
|
65
|
-
- `post_action`
|
|
66
|
-
- `max_greet_count`
|
|
67
|
-
|
|
68
|
-
## Execution Notes
|
|
69
|
-
|
|
70
|
-
- 推荐页筛选入口在 recommend 页面,不是 search 页面。
|
|
71
|
-
- recommend-search-cli 只负责应用推荐页筛选项。
|
|
72
|
-
- recommend-screen-cli 负责滚动推荐列表、打开详情、提取完整简历图、调用多模态模型判断,并按单次确认的 `post_action` 执行收藏或打招呼。
|
|
73
|
-
- 详情页处理完成后必须关闭详情页并确认已关闭。
|
|
74
|
-
|
|
75
|
-
## Fallback
|
|
76
|
-
|
|
77
|
-
如果 MCP 不可用,改用:
|
|
78
|
-
|
|
79
|
-
`boss-recommend-mcp run --instruction "..." [--confirmation-json '{...}'] [--overrides-json '{...}']`
|
|
80
|
-
|
|
81
|
-
CLI fallback 的状态机与 MCP 保持一致:
|
|
82
|
-
|
|
83
|
-
- `NEED_INPUT`
|
|
84
|
-
- `NEED_CONFIRMATION`
|
|
85
|
-
- `COMPLETED`
|
|
86
|
-
- `FAILED`
|
|
87
|
-
|
|
88
|
-
## Setup Checklist
|
|
89
|
-
|
|
90
|
-
执行前先检查:
|
|
91
|
-
|
|
92
|
-
- `boss-recommend-mcp` 是否已安装
|
|
93
|
-
- `screening-config.json` 是否存在且包含可用模型配置
|
|
94
|
-
- Chrome 远程调试端口是否可连
|
|
95
|
-
- 当前 Chrome 是否停留在 `https://www.zhipin.com/web/chat/recommend`
|
|
96
|
-
|
|
97
|
-
##
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
1
|
+
---
|
|
2
|
+
name: "boss-recommend-pipeline"
|
|
3
|
+
description: "Use when users ask to run Boss recommend-page filtering and screening via boss-recommend-mcp; confirm filters, criteria, optional target_count, and run-level post_action (plus max_greet_count when post_action=greet) before execution."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Boss Recommend Pipeline Skill
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
当用户希望在 Boss 推荐页按条件筛选候选人时,优先调用 MCP 工具 `run_recommend_pipeline` 完成端到端任务:
|
|
11
|
+
|
|
12
|
+
1. 解析推荐页筛选指令
|
|
13
|
+
2. 结构化推荐页 filters
|
|
14
|
+
3. 确认筛选 criteria
|
|
15
|
+
4. 在运行开始时询问 `target_count`(可选,可留空)
|
|
16
|
+
5. 在运行开始时一次性确认 `post_action`
|
|
17
|
+
6. 若 `post_action=greet`,同时确认 `max_greet_count`
|
|
18
|
+
7. 执行 recommend-search-cli 与 recommend-screen-cli
|
|
19
|
+
8. 返回结果摘要
|
|
20
|
+
|
|
21
|
+
## Required Confirmation
|
|
22
|
+
|
|
23
|
+
在真正执行前,必须先确认:
|
|
24
|
+
|
|
25
|
+
- 学校标签(`school_tag`)
|
|
26
|
+
- 学历(`degree`)
|
|
27
|
+
- 性别(`gender`)
|
|
28
|
+
- 是否过滤近14天已看(`recent_not_view`)
|
|
29
|
+
- screening criteria 是否正确
|
|
30
|
+
- `target_count`(目标筛选人数)是否需要设置(可不设上限)
|
|
31
|
+
- `post_action` 是否确定为 `favorite` 或 `greet`
|
|
32
|
+
- 当 `post_action=greet` 时,`max_greet_count`(最多打招呼人数)是否确定
|
|
33
|
+
|
|
34
|
+
`post_action` 的确认是**单次运行级别**的:
|
|
35
|
+
|
|
36
|
+
- 若用户确认 `favorite`,则本次运行中所有通过人选都统一收藏
|
|
37
|
+
- 若用户确认 `greet`,则本次运行中先按 `max_greet_count` 执行打招呼,超出上限后自动改为收藏
|
|
38
|
+
- 不要在每位候选人通过后再次逐个确认
|
|
39
|
+
|
|
40
|
+
## Tool Contract
|
|
41
|
+
|
|
42
|
+
- Tool name: `run_recommend_pipeline`
|
|
43
|
+
- Input:
|
|
44
|
+
- `instruction` (required)
|
|
45
|
+
- `confirmation`
|
|
46
|
+
- `filters_confirmed`
|
|
47
|
+
- `school_tag_confirmed`
|
|
48
|
+
- `degree_confirmed`
|
|
49
|
+
- `gender_confirmed`
|
|
50
|
+
- `recent_not_view_confirmed`
|
|
51
|
+
- `criteria_confirmed`
|
|
52
|
+
- `target_count_confirmed`
|
|
53
|
+
- `target_count_value` (integer, optional)
|
|
54
|
+
- `post_action_confirmed`
|
|
55
|
+
- `post_action_value` (`favorite|greet`)
|
|
56
|
+
- `max_greet_count_confirmed`
|
|
57
|
+
- `max_greet_count_value` (integer)
|
|
58
|
+
- `overrides`
|
|
59
|
+
- `school_tag`
|
|
60
|
+
- `degree`(可传单值或数组;如“本科及以上”应展开为 `["本科","硕士","博士"]`)
|
|
61
|
+
- `gender`
|
|
62
|
+
- `recent_not_view`
|
|
63
|
+
- `criteria`
|
|
64
|
+
- `target_count`
|
|
65
|
+
- `post_action`
|
|
66
|
+
- `max_greet_count`
|
|
67
|
+
|
|
68
|
+
## Execution Notes
|
|
69
|
+
|
|
70
|
+
- 推荐页筛选入口在 recommend 页面,不是 search 页面。
|
|
71
|
+
- recommend-search-cli 只负责应用推荐页筛选项。
|
|
72
|
+
- recommend-screen-cli 负责滚动推荐列表、打开详情、提取完整简历图、调用多模态模型判断,并按单次确认的 `post_action` 执行收藏或打招呼。
|
|
73
|
+
- 详情页处理完成后必须关闭详情页并确认已关闭。
|
|
74
|
+
|
|
75
|
+
## Fallback
|
|
76
|
+
|
|
77
|
+
如果 MCP 不可用,改用:
|
|
78
|
+
|
|
79
|
+
`boss-recommend-mcp run --instruction "..." [--confirmation-json '{...}'] [--overrides-json '{...}']`
|
|
80
|
+
|
|
81
|
+
CLI fallback 的状态机与 MCP 保持一致:
|
|
82
|
+
|
|
83
|
+
- `NEED_INPUT`
|
|
84
|
+
- `NEED_CONFIRMATION`
|
|
85
|
+
- `COMPLETED`
|
|
86
|
+
- `FAILED`
|
|
87
|
+
|
|
88
|
+
## Setup Checklist
|
|
89
|
+
|
|
90
|
+
执行前先检查:
|
|
91
|
+
|
|
92
|
+
- `boss-recommend-mcp` 是否已安装
|
|
93
|
+
- `screening-config.json` 是否存在且包含可用模型配置
|
|
94
|
+
- Chrome 远程调试端口是否可连
|
|
95
|
+
- 当前 Chrome 是否停留在 `https://www.zhipin.com/web/chat/recommend`
|
|
96
|
+
|
|
97
|
+
## Preflight 失败自动修复
|
|
98
|
+
|
|
99
|
+
当工具返回 `status=FAILED` 且 `error.code=PIPELINE_PREFLIGHT_FAILED` 时:
|
|
100
|
+
|
|
101
|
+
1. 优先读取 `diagnostics.recovery.agent_prompt`,直接把这段提示词交给 AI agent 执行环境修复。
|
|
102
|
+
2. 若 `diagnostics.recovery.agent_prompt` 不存在,使用下面的兜底提示词(严格顺序,不可跳步):
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
你是环境修复 agent。请根据 diagnostics.checks 修复依赖,必须串行执行:
|
|
106
|
+
1) 若 node_cli 失败:先安装 Node.js >=18,未成功前禁止执行 npm install。
|
|
107
|
+
2) 若任意 npm_dep_* 失败:再安装 npm 依赖(chrome-remote-interface / ws)。
|
|
108
|
+
3) 若 python_cli 失败:安装 Python,并确保 python 命令可用。
|
|
109
|
+
4) 若 python_pillow 失败:最后执行 python -m pip install pillow。
|
|
110
|
+
每一步完成后重新运行 doctor,全部通过后再重试 run_recommend_pipeline。
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
安装顺序约束(必须遵守):
|
|
114
|
+
|
|
115
|
+
- 没有 Node.js 时,不能先装 npm 包
|
|
116
|
+
- 没有 Python 时,不能先装 Pillow
|
|
117
|
+
|
|
118
|
+
## Response Style
|
|
119
|
+
|
|
120
|
+
- 用结构化中文输出
|
|
121
|
+
- 先给用户确认卡片,再正式执行
|
|
122
|
+
- 对 `school_tag/degree/gender/recent_not_view` 必须逐项提问并逐项确认,不可合并成一句“filters已确认”
|
|
123
|
+
- 不要跳过 `post_action` 的首轮确认
|
|
124
|
+
- 不要把 recommend 流程说成 search 流程
|
package/src/adapters.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
const currentFilePath = fileURLToPath(import.meta.url);
|
|
@@ -174,6 +174,201 @@ function runProcess({ command, args, cwd, timeoutMs }) {
|
|
|
174
174
|
});
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
function runProcessSync({ command, args, cwd }) {
|
|
178
|
+
try {
|
|
179
|
+
const result = spawnSync(command, args, {
|
|
180
|
+
cwd,
|
|
181
|
+
windowsHide: true,
|
|
182
|
+
shell: false,
|
|
183
|
+
env: process.env,
|
|
184
|
+
encoding: "utf8"
|
|
185
|
+
});
|
|
186
|
+
const stdout = String(result.stdout || "").trim();
|
|
187
|
+
const stderr = String(result.stderr || "").trim();
|
|
188
|
+
return {
|
|
189
|
+
ok: result.status === 0,
|
|
190
|
+
status: result.status,
|
|
191
|
+
stdout,
|
|
192
|
+
stderr,
|
|
193
|
+
output: [stdout, stderr].filter(Boolean).join("\n").trim(),
|
|
194
|
+
error_code: result.error?.code || null,
|
|
195
|
+
error_message: result.error?.message || null
|
|
196
|
+
};
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
status: -1,
|
|
201
|
+
stdout: "",
|
|
202
|
+
stderr: "",
|
|
203
|
+
output: "",
|
|
204
|
+
error_code: error.code || "SPAWN_FAILED",
|
|
205
|
+
error_message: error.message || String(error)
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function parseMajorVersion(raw) {
|
|
211
|
+
const match = String(raw || "").match(/v?(\d+)(?:\.\d+){0,2}/);
|
|
212
|
+
if (!match) return null;
|
|
213
|
+
const major = Number.parseInt(match[1], 10);
|
|
214
|
+
return Number.isFinite(major) ? major : null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildNodeCommandCheck() {
|
|
218
|
+
const probe = runProcessSync({
|
|
219
|
+
command: "node",
|
|
220
|
+
args: ["--version"]
|
|
221
|
+
});
|
|
222
|
+
const major = parseMajorVersion(probe.output);
|
|
223
|
+
const versionOk = Number.isInteger(major) && major >= 18;
|
|
224
|
+
return {
|
|
225
|
+
key: "node_cli",
|
|
226
|
+
ok: probe.ok && versionOk,
|
|
227
|
+
path: "node --version",
|
|
228
|
+
message: probe.ok
|
|
229
|
+
? (versionOk
|
|
230
|
+
? `Node 命令可用 (${probe.output || "unknown version"})`
|
|
231
|
+
: `Node 版本过低 (${probe.output || "unknown version"}),要求 >= 18`)
|
|
232
|
+
: `未找到 node 命令,请先安装 Node.js >= 18。${probe.error_message ? ` (${probe.error_message})` : ""}`
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function detectPythonCommand() {
|
|
237
|
+
const python = runProcessSync({
|
|
238
|
+
command: "python",
|
|
239
|
+
args: ["--version"]
|
|
240
|
+
});
|
|
241
|
+
if (python.ok) {
|
|
242
|
+
return {
|
|
243
|
+
ok: true,
|
|
244
|
+
command: "python",
|
|
245
|
+
probe: python
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const python3 = runProcessSync({
|
|
249
|
+
command: "python3",
|
|
250
|
+
args: ["--version"]
|
|
251
|
+
});
|
|
252
|
+
if (python3.ok) {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
command: null,
|
|
256
|
+
probe: python,
|
|
257
|
+
fallback: python3
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
ok: false,
|
|
262
|
+
command: null,
|
|
263
|
+
probe: python,
|
|
264
|
+
fallback: null
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function buildPythonCommandCheck() {
|
|
269
|
+
const detected = detectPythonCommand();
|
|
270
|
+
if (detected.ok) {
|
|
271
|
+
return {
|
|
272
|
+
key: "python_cli",
|
|
273
|
+
ok: true,
|
|
274
|
+
path: "python --version",
|
|
275
|
+
message: `Python 命令可用 (${detected.probe.output || "unknown version"})`
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (detected.fallback) {
|
|
279
|
+
return {
|
|
280
|
+
key: "python_cli",
|
|
281
|
+
ok: false,
|
|
282
|
+
path: "python --version",
|
|
283
|
+
message: `检测到 ${detected.fallback.output || "python3"},但当前流程依赖 python 命令;请创建 python 别名后重试。`
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
key: "python_cli",
|
|
288
|
+
ok: false,
|
|
289
|
+
path: "python --version",
|
|
290
|
+
message: "未找到 python 命令,请安装 Python 并确保 python 在 PATH 中。"
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function buildPillowCheck() {
|
|
295
|
+
const detected = detectPythonCommand();
|
|
296
|
+
if (!detected.ok || !detected.command) {
|
|
297
|
+
return {
|
|
298
|
+
key: "python_pillow",
|
|
299
|
+
ok: false,
|
|
300
|
+
path: "python -c \"import PIL\"",
|
|
301
|
+
message: "无法校验 Pillow:python 命令不可用。"
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const probe = runProcessSync({
|
|
305
|
+
command: detected.command,
|
|
306
|
+
args: ["-c", "import PIL, PIL.Image; print(PIL.__version__)"]
|
|
307
|
+
});
|
|
308
|
+
return {
|
|
309
|
+
key: "python_pillow",
|
|
310
|
+
ok: probe.ok,
|
|
311
|
+
path: `${detected.command} -c "import PIL"`,
|
|
312
|
+
message: probe.ok
|
|
313
|
+
? `Pillow 可用 (${probe.output || "version unknown"})`
|
|
314
|
+
: "Pillow 未安装。请执行 `python -m pip install pillow`。"
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function buildNodePackageCheck({ key, moduleName, cwd, missingMessage }) {
|
|
319
|
+
if (!cwd || !pathExists(cwd)) {
|
|
320
|
+
return {
|
|
321
|
+
key,
|
|
322
|
+
ok: false,
|
|
323
|
+
path: moduleName,
|
|
324
|
+
module: moduleName,
|
|
325
|
+
install_cwd: null,
|
|
326
|
+
message: missingMessage
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const probe = runProcessSync({
|
|
330
|
+
command: "node",
|
|
331
|
+
args: ["-e", `require.resolve(${JSON.stringify(moduleName)});`],
|
|
332
|
+
cwd
|
|
333
|
+
});
|
|
334
|
+
return {
|
|
335
|
+
key,
|
|
336
|
+
ok: probe.ok,
|
|
337
|
+
path: moduleName,
|
|
338
|
+
module: moduleName,
|
|
339
|
+
install_cwd: cwd,
|
|
340
|
+
message: probe.ok
|
|
341
|
+
? `${moduleName} npm 依赖可用`
|
|
342
|
+
: `缺少 npm 依赖 ${moduleName},请在 boss-recommend-mcp 目录执行 npm install。`
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function buildRuntimeDependencyChecks({ searchDir, screenDir }) {
|
|
347
|
+
return [
|
|
348
|
+
buildNodeCommandCheck(),
|
|
349
|
+
buildPythonCommandCheck(),
|
|
350
|
+
buildPillowCheck(),
|
|
351
|
+
buildNodePackageCheck({
|
|
352
|
+
key: "npm_dep_chrome_remote_interface_search",
|
|
353
|
+
moduleName: "chrome-remote-interface",
|
|
354
|
+
cwd: searchDir,
|
|
355
|
+
missingMessage: "无法校验 chrome-remote-interface:boss-recommend-search-cli 目录不存在。"
|
|
356
|
+
}),
|
|
357
|
+
buildNodePackageCheck({
|
|
358
|
+
key: "npm_dep_chrome_remote_interface_screen",
|
|
359
|
+
moduleName: "chrome-remote-interface",
|
|
360
|
+
cwd: screenDir,
|
|
361
|
+
missingMessage: "无法校验 chrome-remote-interface:boss-recommend-screen-cli 目录不存在。"
|
|
362
|
+
}),
|
|
363
|
+
buildNodePackageCheck({
|
|
364
|
+
key: "npm_dep_ws",
|
|
365
|
+
moduleName: "ws",
|
|
366
|
+
cwd: screenDir,
|
|
367
|
+
missingMessage: "无法校验 ws:boss-recommend-screen-cli 目录不存在。"
|
|
368
|
+
})
|
|
369
|
+
];
|
|
370
|
+
}
|
|
371
|
+
|
|
177
372
|
function parseJsonOutput(text) {
|
|
178
373
|
const trimmed = String(text || "").trim();
|
|
179
374
|
if (!trimmed) return null;
|
|
@@ -248,6 +443,7 @@ export function runPipelinePreflight(workspaceRoot) {
|
|
|
248
443
|
message: "screening-config.json 不存在"
|
|
249
444
|
}
|
|
250
445
|
];
|
|
446
|
+
checks.push(...buildRuntimeDependencyChecks({ searchDir, screenDir }));
|
|
251
447
|
|
|
252
448
|
return {
|
|
253
449
|
ok: checks.every((item) => item.ok),
|
package/src/index.js
CHANGED
|
@@ -8,11 +8,18 @@ const require = createRequire(import.meta.url);
|
|
|
8
8
|
const { version: SERVER_VERSION } = require("../package.json");
|
|
9
9
|
const TOOL_NAME = "run_recommend_pipeline";
|
|
10
10
|
const SERVER_NAME = "boss-recommend-mcp";
|
|
11
|
+
const FRAMING_UNKNOWN = "unknown";
|
|
12
|
+
const FRAMING_HEADER = "header";
|
|
13
|
+
const FRAMING_LINE = "line";
|
|
11
14
|
|
|
12
|
-
function writeMessage(message) {
|
|
15
|
+
function writeMessage(message, framing = FRAMING_LINE) {
|
|
13
16
|
const body = JSON.stringify(message);
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
if (framing === FRAMING_HEADER) {
|
|
18
|
+
const header = `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n`;
|
|
19
|
+
process.stdout.write(header + body);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
process.stdout.write(`${body}\n`);
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
function createJsonRpcError(id, code, message) {
|
|
@@ -219,42 +226,91 @@ export function startServer() {
|
|
|
219
226
|
const mcpRoot = path.resolve(path.dirname(thisFile), "..");
|
|
220
227
|
const workspaceRoot = envRoot ? path.resolve(envRoot) : path.resolve(mcpRoot, "..");
|
|
221
228
|
let buffer = Buffer.alloc(0);
|
|
229
|
+
let framing = FRAMING_UNKNOWN;
|
|
222
230
|
|
|
223
231
|
process.stdin.on("data", async (chunk) => {
|
|
224
232
|
buffer = Buffer.concat([buffer, chunk]);
|
|
233
|
+
if (buffer.length >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) {
|
|
234
|
+
buffer = buffer.slice(3);
|
|
235
|
+
}
|
|
225
236
|
|
|
226
237
|
while (true) {
|
|
227
|
-
const
|
|
228
|
-
|
|
238
|
+
const crlfHeaderEnd = buffer.indexOf("\r\n\r\n");
|
|
239
|
+
const lfHeaderEnd = buffer.indexOf("\n\n");
|
|
240
|
+
const crHeaderEnd = buffer.indexOf("\r\r");
|
|
241
|
+
let headerEnd = -1;
|
|
242
|
+
let headerSeparatorLength = 0;
|
|
243
|
+
if (
|
|
244
|
+
crlfHeaderEnd !== -1
|
|
245
|
+
&& (lfHeaderEnd === -1 || crlfHeaderEnd < lfHeaderEnd)
|
|
246
|
+
&& (crHeaderEnd === -1 || crlfHeaderEnd < crHeaderEnd)
|
|
247
|
+
) {
|
|
248
|
+
headerEnd = crlfHeaderEnd;
|
|
249
|
+
headerSeparatorLength = 4;
|
|
250
|
+
} else if (lfHeaderEnd !== -1 && (crHeaderEnd === -1 || lfHeaderEnd < crHeaderEnd)) {
|
|
251
|
+
headerEnd = lfHeaderEnd;
|
|
252
|
+
headerSeparatorLength = 2;
|
|
253
|
+
} else if (crHeaderEnd !== -1) {
|
|
254
|
+
headerEnd = crHeaderEnd;
|
|
255
|
+
headerSeparatorLength = 2;
|
|
256
|
+
}
|
|
257
|
+
if (headerEnd !== -1) {
|
|
258
|
+
const headerText = buffer.slice(0, headerEnd).toString("utf8");
|
|
259
|
+
const contentLengthLine = headerText
|
|
260
|
+
.split(/\r\n|\n|\r/)
|
|
261
|
+
.find((line) => line.toLowerCase().startsWith("content-length:"));
|
|
229
262
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
263
|
+
if (!contentLengthLine) {
|
|
264
|
+
buffer = buffer.slice(headerEnd + headerSeparatorLength);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const contentLength = Number.parseInt(contentLengthLine.split(":")[1].trim(), 10);
|
|
269
|
+
if (!Number.isFinite(contentLength) || contentLength < 0) {
|
|
270
|
+
buffer = buffer.slice(headerEnd + headerSeparatorLength);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
234
273
|
|
|
235
|
-
|
|
236
|
-
|
|
274
|
+
const bodyStart = headerEnd + headerSeparatorLength;
|
|
275
|
+
const bodyEnd = bodyStart + contentLength;
|
|
276
|
+
if (buffer.length < bodyEnd) break;
|
|
277
|
+
|
|
278
|
+
const body = buffer.slice(bodyStart, bodyEnd).toString("utf8");
|
|
279
|
+
buffer = buffer.slice(bodyEnd);
|
|
280
|
+
framing = FRAMING_HEADER;
|
|
281
|
+
|
|
282
|
+
let message;
|
|
283
|
+
try {
|
|
284
|
+
message = JSON.parse(body);
|
|
285
|
+
} catch {
|
|
286
|
+
writeMessage(createJsonRpcError(null, -32700, "Parse error"), FRAMING_HEADER);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const response = await handleRequest(message, workspaceRoot);
|
|
291
|
+
if (response) writeMessage(response, framing);
|
|
237
292
|
continue;
|
|
238
293
|
}
|
|
239
294
|
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
295
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
296
|
+
if (newlineIndex === -1) break;
|
|
297
|
+
const rawLine = buffer.slice(0, newlineIndex).toString("utf8").replace(/\r$/, "");
|
|
298
|
+
if (/^\s*content-length:/i.test(rawLine)) break;
|
|
299
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
300
|
+
const line = rawLine.trim();
|
|
301
|
+
if (!line) continue;
|
|
302
|
+
framing = FRAMING_LINE;
|
|
247
303
|
|
|
248
304
|
let message;
|
|
249
305
|
try {
|
|
250
|
-
message = JSON.parse(
|
|
306
|
+
message = JSON.parse(line);
|
|
251
307
|
} catch {
|
|
252
|
-
writeMessage(createJsonRpcError(null, -32700, "Parse error"));
|
|
308
|
+
writeMessage(createJsonRpcError(null, -32700, "Parse error"), FRAMING_LINE);
|
|
253
309
|
continue;
|
|
254
310
|
}
|
|
255
311
|
|
|
256
312
|
const response = await handleRequest(message, workspaceRoot);
|
|
257
|
-
if (response) writeMessage(response);
|
|
313
|
+
if (response) writeMessage(response, framing);
|
|
258
314
|
}
|
|
259
315
|
});
|
|
260
316
|
}
|