@reconcrap/boss-recommend-mcp 2.0.26 → 2.0.27
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 +11 -3
- package/package.json +1 -1
- package/skills/boss-recommend-pipeline/SKILL.md +21 -3
- package/src/cli.js +336 -82
package/README.md
CHANGED
|
@@ -132,9 +132,9 @@ node src/cli.js start
|
|
|
132
132
|
|
|
133
133
|
### 迁移 legacy MCP / skills
|
|
134
134
|
|
|
135
|
-
全局 npm 安装会自动运行 `boss-recommend-mcp install`。该安装器会在 Windows 和 macOS 上自动检测 Trae / Trae CN / OpenClaw 的常见配置目录:
|
|
135
|
+
全局 npm 安装会自动运行 `boss-recommend-mcp install`。该安装器会在 Windows 和 macOS 上自动检测 Trae / Trae CN / OpenClaw / QClaw 的常见配置目录:
|
|
136
136
|
|
|
137
|
-
- Windows: `%APPDATA%\Trae*\User\mcp.json`、`%USERPROFILE%\.trae*\mcp.json`、`%USERPROFILE%\.openclaw\mcp.json`、`%APPDATA%\OpenClaw\User\mcp.json`
|
|
137
|
+
- Windows: `%APPDATA%\Trae*\User\mcp.json`、`%USERPROFILE%\.trae*\mcp.json`、`%USERPROFILE%\.openclaw\mcp.json`、`%APPDATA%\OpenClaw\User\mcp.json`、`%USERPROFILE%\.qclaw\openclaw.json`
|
|
138
138
|
- macOS: `~/Library/Application Support/Trae*/User/mcp.json`、`~/.trae*/mcp.json`、`~/.openclaw/mcp.json`、`~/Library/Application Support/OpenClaw/User/mcp.json`
|
|
139
139
|
|
|
140
140
|
如果检测到 legacy Boss server entries,installer 会:
|
|
@@ -150,8 +150,10 @@ node src/cli.js start
|
|
|
150
150
|
```bash
|
|
151
151
|
boss-recommend-mcp install --agent trae-cn
|
|
152
152
|
boss-recommend-mcp install --agent openclaw
|
|
153
|
+
boss-recommend-mcp install --agent qclaw
|
|
153
154
|
boss-recommend-mcp doctor --agent trae-cn
|
|
154
155
|
boss-recommend-mcp doctor --agent openclaw
|
|
156
|
+
boss-recommend-mcp doctor --agent qclaw
|
|
155
157
|
```
|
|
156
158
|
|
|
157
159
|
自定义路径:
|
|
@@ -171,7 +173,13 @@ BOSS_RECOMMEND_MCP_CONFIG_TARGETS # JSON 数组或系统 path 分隔路径列
|
|
|
171
173
|
BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS # JSON 数组或系统 path 分隔路径列表,指定额外 skills 根目录
|
|
172
174
|
```
|
|
173
175
|
|
|
174
|
-
推荐运行入口是 MCP
|
|
176
|
+
推荐运行入口是 MCP 工具 `start_recommend_pipeline_run`。如果宿主是 QClaw 这类 shell-only agent,没有把 MCP tools 暴露给模型,可以使用 CDP-only CLI fallback:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npx -y @reconcrap/boss-recommend-mcp@latest run --detached --instruction-file boss-recommend-instruction.txt --overrides-file boss-recommend-overrides.json --confirmation-file boss-recommend-confirmation.json --slow-live --port 9222
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`--detached` 会让父进程输出 `ACCEPTED + run_id` 后退出,子进程继续持有 Chrome DevTools 会话并执行长任务。岗位发现可以使用只读 CLI:
|
|
175
183
|
|
|
176
184
|
```bash
|
|
177
185
|
npx -y @reconcrap/boss-recommend-mcp@latest list-jobs --slow-live --port 9222
|
package/package.json
CHANGED
|
@@ -139,11 +139,29 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
|
|
|
139
139
|
- `featured` 且缺少校准文件:先 `boss-recommend-mcp calibrate`。
|
|
140
140
|
- `latest`:流程同 `recommend`,但使用最新 tab 结构(`data-status=1`)。
|
|
141
141
|
|
|
142
|
-
## Fallback
|
|
142
|
+
## QClaw / Shell-Only Fallback
|
|
143
143
|
|
|
144
|
-
MCP
|
|
144
|
+
当 QClaw/OpenClaw 变体只暴露 shell、没有原生 MCP tool list 时,禁止停在“请使用 `start_recommend_pipeline_run` 工具”。必须用 shell 继续启动。
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
推荐做法:
|
|
147
|
+
|
|
148
|
+
1. 将锁定的用户原文写入 instruction 文件,将已确认参数写入 `overrides` 与 `confirmation` JSON 文件。
|
|
149
|
+
2. 用 detached CLI 启动,让父命令返回启动证据,子进程继续持有 CDP 会话:
|
|
150
|
+
|
|
151
|
+
```powershell
|
|
152
|
+
npx -y @reconcrap/boss-recommend-mcp@latest run --detached --instruction-file .\boss-recommend-instruction.txt --overrides-file .\boss-recommend-overrides.json --confirmation-file .\boss-recommend-confirmation.json --slow-live --port 9222
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
3. 若返回 `ACCEPTED + run_id`,即任务已启动;记录 `run_id/stdout_path/stderr_path`。若返回 `NEED_INPUT` 或 `NEED_CONFIRMATION`,只补 `pending_questions`,不要重写已锁定的用户原文。
|
|
156
|
+
|
|
157
|
+
兼容路径:
|
|
158
|
+
|
|
159
|
+
- 若 `--detached` 不可用,或返回 `RECOMMEND_CLI_RUN_UNSUPPORTED_CDP_ONLY`,说明 npm/QClaw 仍在使用旧包;先运行 `npx -y @reconcrap/boss-recommend-mcp@latest install --agent qclaw` 并重启 QClaw。
|
|
160
|
+
- 在包更新未生效时,可以使用已验证过的直接 MCP stdio JSON-RPC 方式调用 `start_recommend_pipeline_run`;该方式等价于原生 MCP tool 调用,不能改用 recruit/search 路径。
|
|
161
|
+
|
|
162
|
+
普通 MCP 可用时:
|
|
163
|
+
|
|
164
|
+
`start_recommend_pipeline_run` 仍是首选。
|
|
147
165
|
|
|
148
166
|
禁止错误回退:
|
|
149
167
|
|
package/src/cli.js
CHANGED
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
prepareBossChatRunTool,
|
|
24
24
|
resumeBossChatRunTool
|
|
25
25
|
} from "./chat-mcp.js";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
listRecommendJobsTool,
|
|
28
|
+
startRecommendPipelineRunTool
|
|
29
|
+
} from "./recommend-mcp.js";
|
|
27
30
|
import {
|
|
28
31
|
getBossScreenConfigResolution,
|
|
29
32
|
resolveBossChatRuntimeLayout as resolveCdpBossChatRuntimeLayout,
|
|
@@ -45,7 +48,7 @@ const bossLoginUrl = "https://www.zhipin.com/web/user/?ka=bticket";
|
|
|
45
48
|
const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|history-sync|settings\/syncSetup)/i;
|
|
46
49
|
const bossLoginUrlPattern = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
|
|
47
50
|
const bossLoginTitlePattern = /登录|signin|扫码登录|BOSS直聘登录/i;
|
|
48
|
-
const supportedMcpClients = ["generic", "cursor", "trae", "claudecode", "openclaw"];
|
|
51
|
+
const supportedMcpClients = ["generic", "cursor", "trae", "claudecode", "openclaw", "qclaw"];
|
|
49
52
|
const defaultMcpServerName = "boss-recommend";
|
|
50
53
|
const defaultMcpCommand = "npx";
|
|
51
54
|
const recommendMcpPackageName = "@reconcrap/boss-recommend-mcp";
|
|
@@ -64,7 +67,7 @@ const installConfigDefaults = Object.freeze({
|
|
|
64
67
|
const bossChatRuntimeChildDirs = ["logs", "runs", "profiles", "reports", "artifacts", "state"];
|
|
65
68
|
const bossChatCliUnsupportedStartCode = "CHAT_CLI_ASYNC_UNSUPPORTED_CDP_ONLY";
|
|
66
69
|
const calibrateUnsupportedCode = "CALIBRATE_UNSUPPORTED_CDP_ONLY";
|
|
67
|
-
const
|
|
70
|
+
const detachedRecommendRunChildEnv = "BOSS_RECOMMEND_DETACHED_RUN_CHILD";
|
|
68
71
|
|
|
69
72
|
function getSkillSourceDir(name = skillName) {
|
|
70
73
|
return path.join(packageRoot, "skills", name);
|
|
@@ -673,22 +676,25 @@ function getRunInstruction(options) {
|
|
|
673
676
|
}
|
|
674
677
|
|
|
675
678
|
function getRunConfirmation(options) {
|
|
676
|
-
|
|
677
|
-
|
|
679
|
+
const filePath = options["confirmation-file"] || options["confirmation-json-path"];
|
|
680
|
+
if (typeof filePath === "string" && filePath.trim()) {
|
|
681
|
+
return parseJsonOption(readTextFile(filePath, "confirmation"), "confirmation");
|
|
678
682
|
}
|
|
679
683
|
return parseJsonOption(options["confirmation-json"], "confirmation");
|
|
680
684
|
}
|
|
681
685
|
|
|
682
686
|
function getRunOverrides(options) {
|
|
683
|
-
|
|
684
|
-
|
|
687
|
+
const filePath = options["overrides-file"] || options["overrides-json-path"];
|
|
688
|
+
if (typeof filePath === "string" && filePath.trim()) {
|
|
689
|
+
return parseJsonOption(readTextFile(filePath, "overrides"), "overrides");
|
|
685
690
|
}
|
|
686
691
|
return parseJsonOption(options["overrides-json"], "overrides");
|
|
687
692
|
}
|
|
688
693
|
|
|
689
694
|
function getRunFollowUp(options) {
|
|
690
|
-
|
|
691
|
-
|
|
695
|
+
const filePath = options["follow-up-file"] || options["follow-up-json-path"];
|
|
696
|
+
if (typeof filePath === "string" && filePath.trim()) {
|
|
697
|
+
return parseJsonOption(readTextFile(filePath, "follow_up"), "follow_up");
|
|
692
698
|
}
|
|
693
699
|
return parseJsonOption(options["follow-up-json"], "follow_up");
|
|
694
700
|
}
|
|
@@ -698,6 +704,7 @@ function normalizeMcpClientName(value) {
|
|
|
698
704
|
if (!raw) return "";
|
|
699
705
|
if (raw === "claude-code") return "claudecode";
|
|
700
706
|
if (raw === "trae-cn") return "trae";
|
|
707
|
+
if (raw === "q-claw" || raw === "qclaw-win" || raw === "qclaw_win") return "qclaw";
|
|
701
708
|
return raw;
|
|
702
709
|
}
|
|
703
710
|
|
|
@@ -743,9 +750,19 @@ function buildMcpConfigFileContent(options = {}) {
|
|
|
743
750
|
const serverName = typeof options["server-name"] === "string" && options["server-name"].trim()
|
|
744
751
|
? options["server-name"].trim()
|
|
745
752
|
: defaultMcpServerName;
|
|
753
|
+
const launchConfig = buildMcpLaunchConfig(options);
|
|
754
|
+
if (normalizeMcpClientName(options.client) === "qclaw") {
|
|
755
|
+
return {
|
|
756
|
+
mcp: {
|
|
757
|
+
servers: {
|
|
758
|
+
[serverName]: launchConfig
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
}
|
|
746
763
|
return {
|
|
747
764
|
mcpServers: {
|
|
748
|
-
[serverName]:
|
|
765
|
+
[serverName]: launchConfig
|
|
749
766
|
}
|
|
750
767
|
};
|
|
751
768
|
}
|
|
@@ -757,7 +774,7 @@ function writeMcpConfigFiles(options = {}) {
|
|
|
757
774
|
const files = [];
|
|
758
775
|
for (const client of clients) {
|
|
759
776
|
const filePath = path.join(outputDir, `mcp.${client}.json`);
|
|
760
|
-
fs.writeFileSync(filePath, JSON.stringify(buildMcpConfigFileContent(options), null, 2), "utf8");
|
|
777
|
+
fs.writeFileSync(filePath, JSON.stringify(buildMcpConfigFileContent({ ...options, client }), null, 2), "utf8");
|
|
761
778
|
files.push({ client, file: filePath });
|
|
762
779
|
}
|
|
763
780
|
return { outputDir, files };
|
|
@@ -778,12 +795,13 @@ function parsePathListFromEnv(raw) {
|
|
|
778
795
|
return dedupePaths(text.split(path.delimiter).map((item) => item.trim()).filter(Boolean));
|
|
779
796
|
}
|
|
780
797
|
|
|
781
|
-
const supportedExternalAgents = ["cursor", "trae", "trae-cn", "claude", "openclaw"];
|
|
798
|
+
const supportedExternalAgents = ["cursor", "trae", "trae-cn", "claude", "openclaw", "qclaw"];
|
|
782
799
|
|
|
783
800
|
function normalizeAgentName(value) {
|
|
784
801
|
const raw = String(value || "").trim().toLowerCase();
|
|
785
802
|
if (!raw) return "";
|
|
786
803
|
if (raw === "claude-code") return "claude";
|
|
804
|
+
if (raw === "q-claw" || raw === "qclaw-win" || raw === "qclaw_win") return "qclaw";
|
|
787
805
|
return raw;
|
|
788
806
|
}
|
|
789
807
|
|
|
@@ -849,7 +867,8 @@ function getKnownExternalMcpConfigPathsByAgent() {
|
|
|
849
867
|
trae: [...traeConfigPaths, path.join(home, ".trae", "mcp.json"), path.join(home, ".trae-cn", "mcp.json")],
|
|
850
868
|
"trae-cn": [...traeConfigPaths, path.join(home, ".trae-cn", "mcp.json"), path.join(home, ".trae", "mcp.json")],
|
|
851
869
|
claude: [path.join(home, ".claude", "mcp.json")],
|
|
852
|
-
openclaw: [path.join(home, ".openclaw", "mcp.json"), ...openClawConfigPaths]
|
|
870
|
+
openclaw: [path.join(home, ".openclaw", "mcp.json"), ...openClawConfigPaths],
|
|
871
|
+
qclaw: [path.join(home, ".qclaw", "openclaw.json")]
|
|
853
872
|
};
|
|
854
873
|
}
|
|
855
874
|
|
|
@@ -866,15 +885,38 @@ function resolveExternalMcpConfigTargets(options = {}) {
|
|
|
866
885
|
return dedupePaths([...fromEnv, ...known]);
|
|
867
886
|
}
|
|
868
887
|
|
|
888
|
+
function isQClawMcpConfigTarget(filePath, options = {}, current = null) {
|
|
889
|
+
if (normalizeAgentName(options.agent) === "qclaw" || normalizeMcpClientName(options.client) === "qclaw") {
|
|
890
|
+
return true;
|
|
891
|
+
}
|
|
892
|
+
const normalized = path.resolve(String(filePath || "")).replace(/\\/g, "/").toLowerCase();
|
|
893
|
+
if (normalized.endsWith("/.qclaw/openclaw.json")) {
|
|
894
|
+
return true;
|
|
895
|
+
}
|
|
896
|
+
return Boolean(
|
|
897
|
+
current?.mcp?.servers
|
|
898
|
+
&& typeof current.mcp.servers === "object"
|
|
899
|
+
&& !Array.isArray(current.mcp.servers)
|
|
900
|
+
&& !current?.mcpServers
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function getMcpServersFromConfig(config = {}, useQClawShape = false) {
|
|
905
|
+
const servers = useQClawShape ? config?.mcp?.servers : config?.mcpServers;
|
|
906
|
+
if (servers && typeof servers === "object" && !Array.isArray(servers)) {
|
|
907
|
+
return servers;
|
|
908
|
+
}
|
|
909
|
+
return {};
|
|
910
|
+
}
|
|
911
|
+
|
|
869
912
|
function mergeMcpServerConfigFile(filePath, options = {}) {
|
|
870
|
-
const nextConfig = buildMcpConfigFileContent(options);
|
|
871
|
-
const serverName = Object.keys(nextConfig.mcpServers || {})[0] || defaultMcpServerName;
|
|
872
|
-
const launchConfig = nextConfig.mcpServers?.[serverName] || buildMcpLaunchConfig(options);
|
|
873
913
|
const current = readJsonObjectFileSafe(filePath);
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
914
|
+
const useQClawShape = isQClawMcpConfigTarget(filePath, options, current);
|
|
915
|
+
const nextConfig = buildMcpConfigFileContent({ ...options, client: useQClawShape ? "qclaw" : options.client });
|
|
916
|
+
const nextServers = useQClawShape ? nextConfig.mcp?.servers : nextConfig.mcpServers;
|
|
917
|
+
const serverName = Object.keys(nextServers || {})[0] || defaultMcpServerName;
|
|
918
|
+
const launchConfig = nextServers?.[serverName] || buildMcpLaunchConfig(options);
|
|
919
|
+
const existingServers = getMcpServersFromConfig(current, useQClawShape);
|
|
878
920
|
const existingEntry = existingServers[serverName];
|
|
879
921
|
const retainedServers = {};
|
|
880
922
|
const migratedLegacyServers = [];
|
|
@@ -886,13 +928,24 @@ function mergeMcpServerConfigFile(filePath, options = {}) {
|
|
|
886
928
|
}
|
|
887
929
|
retainedServers[name] = config;
|
|
888
930
|
}
|
|
889
|
-
const merged =
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
931
|
+
const merged = useQClawShape
|
|
932
|
+
? {
|
|
933
|
+
...current,
|
|
934
|
+
mcp: {
|
|
935
|
+
...(current?.mcp && typeof current.mcp === "object" && !Array.isArray(current.mcp) ? current.mcp : {}),
|
|
936
|
+
servers: {
|
|
937
|
+
...retainedServers,
|
|
938
|
+
[serverName]: launchConfig
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
: {
|
|
943
|
+
...current,
|
|
944
|
+
mcpServers: {
|
|
945
|
+
...retainedServers,
|
|
946
|
+
[serverName]: launchConfig
|
|
947
|
+
}
|
|
948
|
+
};
|
|
896
949
|
|
|
897
950
|
ensureDir(path.dirname(filePath));
|
|
898
951
|
const before = pathExists(filePath) ? fs.readFileSync(filePath, "utf8") : "";
|
|
@@ -907,6 +960,7 @@ function mergeMcpServerConfigFile(filePath, options = {}) {
|
|
|
907
960
|
return {
|
|
908
961
|
file: filePath,
|
|
909
962
|
server: serverName,
|
|
963
|
+
config_shape: useQClawShape ? "qclaw" : "mcpServers",
|
|
910
964
|
updated,
|
|
911
965
|
migrated_legacy_servers: migratedLegacyServers,
|
|
912
966
|
backup_file: backupFile
|
|
@@ -957,7 +1011,8 @@ function getKnownExternalSkillBaseDirsByAgent() {
|
|
|
957
1011
|
trae: [path.join(home, ".trae", "skills"), path.join(home, ".trae-cn", "skills"), ...traeSkillDirs],
|
|
958
1012
|
"trae-cn": [path.join(home, ".trae-cn", "skills"), path.join(home, ".trae", "skills"), ...traeSkillDirs],
|
|
959
1013
|
claude: [path.join(home, ".claude", "skills")],
|
|
960
|
-
openclaw: [path.join(home, ".openclaw", "skills"), ...openClawSkillDirs]
|
|
1014
|
+
openclaw: [path.join(home, ".openclaw", "skills"), ...openClawSkillDirs],
|
|
1015
|
+
qclaw: [path.join(home, ".qclaw", "skills")]
|
|
961
1016
|
};
|
|
962
1017
|
}
|
|
963
1018
|
|
|
@@ -1008,9 +1063,9 @@ function inspectMcpServerEntries(filePath) {
|
|
|
1008
1063
|
};
|
|
1009
1064
|
}
|
|
1010
1065
|
const parsed = readJsonObjectFileSafe(filePath);
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1066
|
+
const rootServers = getMcpServersFromConfig(parsed, false);
|
|
1067
|
+
const qclawServers = getMcpServersFromConfig(parsed, true);
|
|
1068
|
+
const servers = { ...rootServers, ...qclawServers };
|
|
1014
1069
|
const recommendNames = [];
|
|
1015
1070
|
const recruitNames = [];
|
|
1016
1071
|
const chatNames = [];
|
|
@@ -1402,6 +1457,177 @@ function printJson(value) {
|
|
|
1402
1457
|
console.log(JSON.stringify(value, null, 2));
|
|
1403
1458
|
}
|
|
1404
1459
|
|
|
1460
|
+
function stripDetachedRunArgs(args = []) {
|
|
1461
|
+
const booleanKeys = new Set(["--detached", "--background"]);
|
|
1462
|
+
const valueKeys = new Set(["--detached-start-timeout-ms"]);
|
|
1463
|
+
const nextArgs = [];
|
|
1464
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1465
|
+
const token = args[index];
|
|
1466
|
+
if (booleanKeys.has(token)) continue;
|
|
1467
|
+
if (valueKeys.has(token)) {
|
|
1468
|
+
index += 1;
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
nextArgs.push(token);
|
|
1472
|
+
}
|
|
1473
|
+
return nextArgs;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
function extractFirstJsonObject(text = "") {
|
|
1477
|
+
const start = text.indexOf("{");
|
|
1478
|
+
if (start < 0) return null;
|
|
1479
|
+
let depth = 0;
|
|
1480
|
+
let inString = false;
|
|
1481
|
+
let escaped = false;
|
|
1482
|
+
for (let index = start; index < text.length; index += 1) {
|
|
1483
|
+
const char = text[index];
|
|
1484
|
+
if (inString) {
|
|
1485
|
+
if (escaped) {
|
|
1486
|
+
escaped = false;
|
|
1487
|
+
} else if (char === "\\") {
|
|
1488
|
+
escaped = true;
|
|
1489
|
+
} else if (char === "\"") {
|
|
1490
|
+
inString = false;
|
|
1491
|
+
}
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
if (char === "\"") {
|
|
1495
|
+
inString = true;
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
if (char === "{") depth += 1;
|
|
1499
|
+
if (char === "}") {
|
|
1500
|
+
depth -= 1;
|
|
1501
|
+
if (depth === 0) return text.slice(start, index + 1);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function readFirstJsonObjectFromFile(filePath) {
|
|
1508
|
+
try {
|
|
1509
|
+
const text = fs.readFileSync(filePath, "utf8");
|
|
1510
|
+
const jsonText = extractFirstJsonObject(text);
|
|
1511
|
+
return jsonText ? JSON.parse(jsonText) : null;
|
|
1512
|
+
} catch {
|
|
1513
|
+
return null;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function createDetachedRecommendRunPaths() {
|
|
1518
|
+
const dir = path.join(getStateHome(), "detached-runs");
|
|
1519
|
+
ensureDir(dir);
|
|
1520
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1521
|
+
const nonce = Math.random().toString(36).slice(2, 8);
|
|
1522
|
+
const base = `recommend-run-${stamp}-${nonce}`;
|
|
1523
|
+
return {
|
|
1524
|
+
dir,
|
|
1525
|
+
stdoutPath: path.join(dir, `${base}.stdout.json`),
|
|
1526
|
+
stderrPath: path.join(dir, `${base}.stderr.log`)
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function appendDetachedCliMeta(payload, meta) {
|
|
1531
|
+
return {
|
|
1532
|
+
...payload,
|
|
1533
|
+
cli: {
|
|
1534
|
+
...(payload?.cli || {}),
|
|
1535
|
+
command: "run",
|
|
1536
|
+
cdp_only: true,
|
|
1537
|
+
detached: true,
|
|
1538
|
+
detached_parent: true,
|
|
1539
|
+
child_pid: meta.childPid || null,
|
|
1540
|
+
stdout_path: meta.stdoutPath,
|
|
1541
|
+
stderr_path: meta.stderrPath
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
async function waitForDetachedRecommendRunStart({
|
|
1547
|
+
child,
|
|
1548
|
+
stdoutPath,
|
|
1549
|
+
stderrPath,
|
|
1550
|
+
timeoutMs
|
|
1551
|
+
}) {
|
|
1552
|
+
const deadline = Date.now() + timeoutMs;
|
|
1553
|
+
let childExit = null;
|
|
1554
|
+
child.once("exit", (code, signal) => {
|
|
1555
|
+
childExit = { code, signal };
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
while (Date.now() <= deadline) {
|
|
1559
|
+
const parsed = readFirstJsonObjectFromFile(stdoutPath);
|
|
1560
|
+
if (parsed) return appendDetachedCliMeta(parsed, {
|
|
1561
|
+
childPid: child.pid,
|
|
1562
|
+
stdoutPath,
|
|
1563
|
+
stderrPath
|
|
1564
|
+
});
|
|
1565
|
+
if (childExit) break;
|
|
1566
|
+
await sleepMs(500);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
const stderrPreview = (() => {
|
|
1570
|
+
try {
|
|
1571
|
+
return fs.readFileSync(stderrPath, "utf8").slice(-2000);
|
|
1572
|
+
} catch {
|
|
1573
|
+
return "";
|
|
1574
|
+
}
|
|
1575
|
+
})();
|
|
1576
|
+
|
|
1577
|
+
return appendDetachedCliMeta({
|
|
1578
|
+
status: "FAILED",
|
|
1579
|
+
error: {
|
|
1580
|
+
code: childExit ? "DETACHED_RECOMMEND_RUN_CHILD_EXITED" : "DETACHED_RECOMMEND_RUN_START_TIMEOUT",
|
|
1581
|
+
message: childExit
|
|
1582
|
+
? `Detached recommend run child exited before producing a JSON result (code=${childExit.code ?? "null"}, signal=${childExit.signal ?? "null"}).`
|
|
1583
|
+
: `Timed out waiting ${timeoutMs}ms for detached recommend run start output.`,
|
|
1584
|
+
retryable: true,
|
|
1585
|
+
child_exit: childExit,
|
|
1586
|
+
stderr_preview: stderrPreview || null
|
|
1587
|
+
}
|
|
1588
|
+
}, {
|
|
1589
|
+
childPid: child.pid,
|
|
1590
|
+
stdoutPath,
|
|
1591
|
+
stderrPath
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
async function runPipelineDetached(rawArgs = [], options = {}) {
|
|
1596
|
+
const timeoutMs = parseNonNegativeInteger(options["detached-start-timeout-ms"], 180000);
|
|
1597
|
+
const childArgs = stripDetachedRunArgs(rawArgs);
|
|
1598
|
+
const { stdoutPath, stderrPath } = createDetachedRecommendRunPaths();
|
|
1599
|
+
const stdoutFd = fs.openSync(stdoutPath, "a");
|
|
1600
|
+
const stderrFd = fs.openSync(stderrPath, "a");
|
|
1601
|
+
let child;
|
|
1602
|
+
try {
|
|
1603
|
+
child = spawn(process.execPath, [currentFilePath, "run", ...childArgs], {
|
|
1604
|
+
cwd: process.cwd(),
|
|
1605
|
+
detached: true,
|
|
1606
|
+
env: {
|
|
1607
|
+
...process.env,
|
|
1608
|
+
[detachedRecommendRunChildEnv]: "1"
|
|
1609
|
+
},
|
|
1610
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
1611
|
+
windowsHide: true
|
|
1612
|
+
});
|
|
1613
|
+
} finally {
|
|
1614
|
+
fs.closeSync(stdoutFd);
|
|
1615
|
+
fs.closeSync(stderrFd);
|
|
1616
|
+
}
|
|
1617
|
+
child.unref();
|
|
1618
|
+
|
|
1619
|
+
const result = await waitForDetachedRecommendRunStart({
|
|
1620
|
+
child,
|
|
1621
|
+
stdoutPath,
|
|
1622
|
+
stderrPath,
|
|
1623
|
+
timeoutMs
|
|
1624
|
+
});
|
|
1625
|
+
printJson(result);
|
|
1626
|
+
if (result.status !== "ACCEPTED") {
|
|
1627
|
+
process.exitCode = 1;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1405
1631
|
async function listChromeTabs(port) {
|
|
1406
1632
|
const response = await fetch(`http://127.0.0.1:${port}/json/list`);
|
|
1407
1633
|
if (!response.ok) {
|
|
@@ -2293,26 +2519,28 @@ function printHelp() {
|
|
|
2293
2519
|
console.log("Usage:");
|
|
2294
2520
|
console.log(" boss-recommend-mcp Start the MCP server");
|
|
2295
2521
|
console.log(" boss-recommend-mcp start Start the MCP server");
|
|
2296
|
-
console.log(" boss-recommend-mcp run
|
|
2522
|
+
console.log(" boss-recommend-mcp run Start a CDP-only recommend run through the shared run service");
|
|
2297
2523
|
console.log(" boss-recommend-mcp list-jobs CDP-only list of exact recommend job names for cron/one-shot inputs");
|
|
2298
2524
|
console.log(" boss-recommend-mcp chat <subcommand> Run CDP-only boss-chat health/prepare/status commands");
|
|
2299
|
-
console.log(" boss-recommend-mcp install Install/migrate skills and MCP configs; replaces legacy Boss MCP routes (supports --agent trae-cn/openclaw/...)");
|
|
2525
|
+
console.log(" boss-recommend-mcp install Install/migrate skills and MCP configs; replaces legacy Boss MCP routes (supports --agent trae-cn/openclaw/qclaw/...)");
|
|
2300
2526
|
console.log(" boss-recommend-mcp install-skill Install bundled Codex skills (recommend/recruit/chat)");
|
|
2301
2527
|
console.log(" boss-recommend-mcp init-config Create screening-config.json if missing (prefer workspace config/, fallback ~/.boss-recommend-mcp)");
|
|
2302
2528
|
console.log(" boss-recommend-mcp config set Write baseUrl/apiKey/model (prefer workspace config/, fallback ~/.boss-recommend-mcp)");
|
|
2303
2529
|
console.log(" boss-recommend-mcp set-port Persist preferred Chrome debug port to screening-config.json");
|
|
2304
|
-
console.log(" boss-recommend-mcp mcp-config Generate MCP config JSON for Cursor/Trae(含 trae-cn)/Claude Code/OpenClaw");
|
|
2305
|
-
console.log(" boss-recommend-mcp doctor Check config/runtime/calibration prerequisites (supports --agent trae-cn/cursor/...)");
|
|
2530
|
+
console.log(" boss-recommend-mcp mcp-config Generate MCP config JSON for Cursor/Trae(含 trae-cn)/Claude Code/OpenClaw/QClaw");
|
|
2531
|
+
console.log(" boss-recommend-mcp doctor Check config/runtime/calibration prerequisites (supports --agent trae-cn/qclaw/cursor/...)");
|
|
2306
2532
|
console.log(" boss-recommend-mcp calibrate Disabled until CDP-only featured calibration is live-verified");
|
|
2307
2533
|
console.log(" boss-recommend-mcp launch-chrome Launch or reuse Chrome debug instance and open Boss recommend page");
|
|
2308
2534
|
console.log(" boss-recommend-mcp where Print installed package, skill, and config paths");
|
|
2309
2535
|
console.log("");
|
|
2310
2536
|
console.log("Run command:");
|
|
2311
|
-
console.log(" boss-recommend-mcp run --instruction \"推荐页上筛选211男生,近14天没有,有大模型平台经验\"
|
|
2537
|
+
console.log(" boss-recommend-mcp run --instruction \"推荐页上筛选211男生,近14天没有,有大模型平台经验\" --overrides-file overrides.json --confirmation-file confirmation.json");
|
|
2538
|
+
console.log(" boss-recommend-mcp run --detached --instruction \"...\" --overrides-file overrides.json --confirmation-file confirmation.json");
|
|
2312
2539
|
console.log(" boss-recommend-mcp list-jobs --slow-live --port 9222");
|
|
2313
2540
|
console.log(" boss-recommend-mcp chat prepare-run --slow-live --port 9222 # CDP-only preflight; start runs through MCP start_boss_chat_run");
|
|
2314
2541
|
console.log(" boss-recommend-mcp config set --base-url <url> --api-key <key> --model <model> [--thinking-level off|low|medium|high|current] [--openai-organization <id>] [--openai-project <id>]");
|
|
2315
2542
|
console.log(" boss-recommend-mcp install --agent trae-cn");
|
|
2543
|
+
console.log(" boss-recommend-mcp install --agent qclaw # updates ~/.qclaw/openclaw.json mcp.servers and mirrors skills");
|
|
2316
2544
|
console.log(" boss-recommend-mcp doctor --agent trae-cn --page-scope featured");
|
|
2317
2545
|
console.log(" boss-recommend-mcp calibrate --port 9222 # returns CALIBRATE_UNSUPPORTED_CDP_ONLY during rewrite");
|
|
2318
2546
|
}
|
|
@@ -2401,42 +2629,6 @@ async function installAll(options = {}) {
|
|
|
2401
2629
|
}
|
|
2402
2630
|
}
|
|
2403
2631
|
|
|
2404
|
-
function buildUnsupportedRecommendCliRunResponse({
|
|
2405
|
-
instruction,
|
|
2406
|
-
confirmation,
|
|
2407
|
-
overrides,
|
|
2408
|
-
followUp,
|
|
2409
|
-
workspaceRoot,
|
|
2410
|
-
port
|
|
2411
|
-
} = {}) {
|
|
2412
|
-
return {
|
|
2413
|
-
status: "FAILED",
|
|
2414
|
-
error: {
|
|
2415
|
-
code: recommendCliRunUnsupportedCode,
|
|
2416
|
-
message: "boss-recommend-mcp run is fenced during the CDP-only rewrite because the old one-shot CLI route can reach page-JS/Runtime-based orchestration. Use the MCP tool start_recommend_pipeline_run for CDP-only recommend runs until a live-verified one-shot CLI replacement exists.",
|
|
2417
|
-
retryable: false
|
|
2418
|
-
},
|
|
2419
|
-
cdp_only: true,
|
|
2420
|
-
runtime_evaluate_used: false,
|
|
2421
|
-
method_summary: {},
|
|
2422
|
-
method_log: [],
|
|
2423
|
-
run_mode: "mcp_async_required",
|
|
2424
|
-
port,
|
|
2425
|
-
target_url: bossUrl,
|
|
2426
|
-
input: {
|
|
2427
|
-
workspace_root: workspaceRoot,
|
|
2428
|
-
instruction,
|
|
2429
|
-
confirmation: confirmation ?? null,
|
|
2430
|
-
overrides: overrides ?? null,
|
|
2431
|
-
follow_up: followUp ?? null
|
|
2432
|
-
},
|
|
2433
|
-
guidance: {
|
|
2434
|
-
recommended_tool: "start_recommend_pipeline_run",
|
|
2435
|
-
next_development_task: "Implement a CDP-only CLI wrapper that starts a durable shared run-service session, persists run state, and exits only after its live gate proves no Runtime.* methods are reachable."
|
|
2436
|
-
}
|
|
2437
|
-
};
|
|
2438
|
-
}
|
|
2439
|
-
|
|
2440
2632
|
async function runPipelineOnce(options = {}) {
|
|
2441
2633
|
const instruction = getRunInstruction(options);
|
|
2442
2634
|
const confirmation = getRunConfirmation(options);
|
|
@@ -2445,15 +2637,70 @@ async function runPipelineOnce(options = {}) {
|
|
|
2445
2637
|
const workspaceRoot = getWorkspaceRoot(options);
|
|
2446
2638
|
const port = parsePositivePort(options.port) || parsePositivePort(process.env.BOSS_RECOMMEND_CHROME_PORT) || 9222;
|
|
2447
2639
|
|
|
2448
|
-
|
|
2449
|
-
workspaceRoot,
|
|
2640
|
+
const args = {
|
|
2450
2641
|
instruction,
|
|
2451
|
-
confirmation,
|
|
2452
|
-
overrides,
|
|
2453
|
-
followUp,
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2642
|
+
confirmation: confirmation ?? undefined,
|
|
2643
|
+
overrides: overrides ?? undefined,
|
|
2644
|
+
follow_up: followUp ?? undefined,
|
|
2645
|
+
host: typeof options.host === "string" && options.host.trim() ? options.host.trim() : undefined,
|
|
2646
|
+
port,
|
|
2647
|
+
target_url_includes: typeof options["target-url-includes"] === "string" && options["target-url-includes"].trim()
|
|
2648
|
+
? options["target-url-includes"].trim()
|
|
2649
|
+
: undefined,
|
|
2650
|
+
allow_navigate: !(options["no-navigate"] === true || options.noNavigate === true || options.allow_navigate === false),
|
|
2651
|
+
slow_live: options["slow-live"] === true || options.slowLive === true || options.slow_live === true
|
|
2652
|
+
};
|
|
2653
|
+
|
|
2654
|
+
const optionalPassthrough = [
|
|
2655
|
+
"detail_limit",
|
|
2656
|
+
"allow_card_only_screening",
|
|
2657
|
+
"debug_test_mode",
|
|
2658
|
+
"screening_mode",
|
|
2659
|
+
"use_llm",
|
|
2660
|
+
"delay_ms",
|
|
2661
|
+
"max_image_pages",
|
|
2662
|
+
"image_wheel_delta_y",
|
|
2663
|
+
"cv_acquisition_mode",
|
|
2664
|
+
"list_max_scrolls",
|
|
2665
|
+
"list_stable_signature_limit",
|
|
2666
|
+
"list_wheel_delta_y",
|
|
2667
|
+
"list_settle_ms",
|
|
2668
|
+
"refresh_on_end",
|
|
2669
|
+
"max_refresh_rounds",
|
|
2670
|
+
"refresh_button_settle_ms",
|
|
2671
|
+
"refresh_reload_settle_ms",
|
|
2672
|
+
"dry_run_post_action",
|
|
2673
|
+
"execute_post_action",
|
|
2674
|
+
"action_timeout_ms",
|
|
2675
|
+
"action_interval_ms",
|
|
2676
|
+
"action_after_click_delay_ms",
|
|
2677
|
+
"llm_timeout_ms",
|
|
2678
|
+
"llm_image_limit",
|
|
2679
|
+
"llm_image_detail"
|
|
2680
|
+
];
|
|
2681
|
+
for (const key of optionalPassthrough) {
|
|
2682
|
+
const kebab = key.replace(/_/g, "-");
|
|
2683
|
+
if (options[key] !== undefined) args[key] = options[key];
|
|
2684
|
+
else if (options[kebab] !== undefined) args[key] = options[kebab];
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
const result = await startRecommendPipelineRunTool({
|
|
2688
|
+
workspaceRoot,
|
|
2689
|
+
args
|
|
2690
|
+
});
|
|
2691
|
+
printJson({
|
|
2692
|
+
...result,
|
|
2693
|
+
cli: {
|
|
2694
|
+
command: "run",
|
|
2695
|
+
cdp_only: true,
|
|
2696
|
+
shared_run_service: true,
|
|
2697
|
+
workspace_root: workspaceRoot,
|
|
2698
|
+
port
|
|
2699
|
+
}
|
|
2700
|
+
});
|
|
2701
|
+
if (result.status !== "ACCEPTED") {
|
|
2702
|
+
process.exitCode = 1;
|
|
2703
|
+
}
|
|
2457
2704
|
}
|
|
2458
2705
|
|
|
2459
2706
|
function buildRecommendJobListCliInput(options = {}) {
|
|
@@ -2590,11 +2837,19 @@ export async function runCli(argv = process.argv) {
|
|
|
2590
2837
|
|
|
2591
2838
|
switch (command) {
|
|
2592
2839
|
case "start":
|
|
2840
|
+
case "mcp":
|
|
2593
2841
|
startServer();
|
|
2594
2842
|
break;
|
|
2595
2843
|
case "run":
|
|
2596
2844
|
try {
|
|
2597
|
-
|
|
2845
|
+
if (
|
|
2846
|
+
(options.detached === true || options.background === true)
|
|
2847
|
+
&& process.env[detachedRecommendRunChildEnv] !== "1"
|
|
2848
|
+
) {
|
|
2849
|
+
await runPipelineDetached(argv.slice(3), options);
|
|
2850
|
+
} else {
|
|
2851
|
+
await runPipelineOnce(options);
|
|
2852
|
+
}
|
|
2598
2853
|
} catch (error) {
|
|
2599
2854
|
printJson({
|
|
2600
2855
|
status: "FAILED",
|
|
@@ -2754,7 +3009,6 @@ export const __testables = {
|
|
|
2754
3009
|
buildBossChatCliInput,
|
|
2755
3010
|
buildDefaultMcpArgs,
|
|
2756
3011
|
buildMcpLaunchConfig,
|
|
2757
|
-
buildUnsupportedRecommendCliRunResponse,
|
|
2758
3012
|
collectRuntimeDirectories,
|
|
2759
3013
|
ensureBossChatRuntimeReady: ensureBossChatRuntimeReadyLocal,
|
|
2760
3014
|
ensureRuntimeDirectories,
|