@reconcrap/boss-recommend-mcp 2.0.2 → 2.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/package.json +1 -1
- package/skills/boss-chat/SKILL.md +3 -0
- package/skills/boss-recommend-pipeline/SKILL.md +9 -2
- package/skills/boss-recruit-pipeline/SKILL.md +3 -0
- package/src/chat-mcp.js +144 -32
- package/src/core/browser/index.js +392 -0
- package/src/recommend-mcp.js +122 -27
- package/src/recruit-mcp.js +139 -24
package/package.json
CHANGED
|
@@ -57,6 +57,9 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
|
|
|
57
57
|
- 只在用户明确是 chat-only 任务时使用本 skill。
|
|
58
58
|
- 只要用户提到推荐页、先找人后沟通、或需要推荐筛选阶段,禁止直接调用 `start_boss_chat_run`;必须先交给 `boss-recommend-pipeline` 完成推荐页任务。
|
|
59
59
|
- 不得在 recommend 任务尚未完成时并行启动独立 chat run。
|
|
60
|
+
- 启动或准备 chat run 时,若本机默认 `127.0.0.1:9222` Chrome DevTools 端口不可连,工具会自动打开 Chrome 并导航到 `https://www.zhipin.com/web/chat/index`。
|
|
61
|
+
- 只有工具返回 `BOSS_LOGIN_REQUIRED` / `requires_login=true` 时,才要求用户在自动打开的 Chrome 窗口人工登录 Boss 后重试;不要把“没开 9222 Chrome”当作缺参。
|
|
62
|
+
- 若本机找不到 Chrome,可提示用户设置 `BOSS_MCP_CHROME_PATH` 或 `BOSS_RECOMMEND_CHROME_PATH`;非本机 debug host 不自动启动。
|
|
60
63
|
- `job` / `start_from` / `criteria` 缺一不可;缺参时只补缺口。
|
|
61
64
|
- `target_count` 在 chat-only 启动前也是必填项,不能默认省略。
|
|
62
65
|
- 当用户说“全部候选人/所有候选人”时,必须按“扫到底(unlimited)”处理,不要再追问正整数。
|
|
@@ -118,8 +118,14 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
|
|
|
118
118
|
|
|
119
119
|
- 执行前必须通过:
|
|
120
120
|
- `screening-config.json` 可用且非占位值(`baseUrl/apiKey/model`)
|
|
121
|
-
- Chrome DevTools
|
|
122
|
-
- Boss
|
|
121
|
+
- 工具可连接或自动启动本机 Chrome DevTools 端口(默认 `127.0.0.1:9222`)
|
|
122
|
+
- Boss 已登录;若当前没有 9222 Chrome,工具会自动打开 Chrome 并导航到 `https://www.zhipin.com/web/chat/recommend`
|
|
123
|
+
- 只有工具返回 `BOSS_LOGIN_REQUIRED` / `requires_login=true` 时,才要求用户人工登录 Boss 后重试
|
|
124
|
+
|
|
125
|
+
- 不要在运行前要求用户手动打开 9222 Chrome。只有这些情况需要人工介入:
|
|
126
|
+
- 工具明确报告 `BOSS_LOGIN_REQUIRED`
|
|
127
|
+
- 本机找不到 Chrome 可执行文件,并提示设置 `BOSS_MCP_CHROME_PATH` 或 `BOSS_RECOMMEND_CHROME_PATH`
|
|
128
|
+
- 用户配置的是非本机 debug host,工具无法安全自动启动
|
|
123
129
|
|
|
124
130
|
- `PIPELINE_PREFLIGHT_FAILED` 处理顺序:
|
|
125
131
|
1. 若 `screen_config` 失败:让用户提供真实 `baseUrl/apiKey/model`,并在 `guidance.config_path` 修改后明确回复“已修改完成”。
|
|
@@ -153,3 +159,4 @@ MCP 不可用时:
|
|
|
153
159
|
- 页面就绪失败提示必须包含 `debug_port`、recommend URL、以及登录 URL(若未登录):
|
|
154
160
|
- `https://www.zhipin.com/web/chat/recommend`
|
|
155
161
|
- `https://www.zhipin.com/web/user/?ka=bticket`
|
|
162
|
+
- 若错误是 `BOSS_LOGIN_REQUIRED`,提示用户在自动打开的 Chrome 窗口完成登录,然后原参数重试;不要改用 search/recruit 路径。
|
|
@@ -25,6 +25,9 @@ description: "Use when users want Boss search/recruit-page screening via the uni
|
|
|
25
25
|
- 如果用户说聊天页、未读、全部聊天、求简历,必须交给 `boss-chat`。
|
|
26
26
|
- 禁止调用旧包:`@reconcrap/boss-recruit-mcp`、`boss-recruit-mcp`、旧本地 recruit repo、旧 vendor 脚本。
|
|
27
27
|
- 浏览器自动化必须走 CDP-only 2.x MCP 工具;不得要求用户启用 legacy page-JS 或 `Runtime.evaluate` 路径。
|
|
28
|
+
- 启动 search/recruit run 时,若本机默认 `127.0.0.1:9222` Chrome DevTools 端口不可连,工具会自动打开 Chrome 并导航到 `https://www.zhipin.com/web/chat/search`。
|
|
29
|
+
- 只有工具返回 `BOSS_LOGIN_REQUIRED` / `requires_login=true` 时,才要求用户在自动打开的 Chrome 窗口人工登录 Boss 后重试;不要把“没开 9222 Chrome”当作缺参。
|
|
30
|
+
- 若本机找不到 Chrome,可提示用户设置 `BOSS_MCP_CHROME_PATH` 或 `BOSS_RECOMMEND_CHROME_PATH`;非本机 debug host 不自动启动。
|
|
28
31
|
- 若用户未提供岗位,必须先询问岗位。搜索页岗位选择在关键词输入框旁边;不要猜测默认岗位。
|
|
29
32
|
- 若用户提供城市、学历、学校、关键词、过滤已看、人选目标数、筛选条件、post action、max greet 等参数,必须逐项传入或确认。
|
|
30
33
|
- `post_action=greet` 时必须确认 `max_greet_count`;不要默认等于 `target_count`。
|
package/src/chat-mcp.js
CHANGED
|
@@ -4,8 +4,13 @@ import process from "node:process";
|
|
|
4
4
|
import {
|
|
5
5
|
assertNoForbiddenCdpCalls,
|
|
6
6
|
bringPageToFront,
|
|
7
|
-
|
|
7
|
+
connectToChromeTargetOrOpen,
|
|
8
|
+
createBossLoginRequiredError,
|
|
9
|
+
detectBossLoginState,
|
|
8
10
|
enableDomains,
|
|
11
|
+
getMainFrameUrl,
|
|
12
|
+
isBossLoginUrl,
|
|
13
|
+
waitForMainFrameUrl,
|
|
9
14
|
sleep
|
|
10
15
|
} from "./core/browser/index.js";
|
|
11
16
|
import {
|
|
@@ -472,6 +477,14 @@ async function waitForHealthyChat(client, config, {
|
|
|
472
477
|
const started = Date.now();
|
|
473
478
|
let lastCheck = null;
|
|
474
479
|
while (Date.now() - started <= timeoutMs) {
|
|
480
|
+
const loginDetection = await detectBossLoginState(client).catch(() => null);
|
|
481
|
+
if (loginDetection?.requires_login) {
|
|
482
|
+
return {
|
|
483
|
+
status: "login_required",
|
|
484
|
+
summary: "Boss login is required",
|
|
485
|
+
loginDetection
|
|
486
|
+
};
|
|
487
|
+
}
|
|
475
488
|
const roots = await resolveChatSelfHealRoots(client, config);
|
|
476
489
|
lastCheck = await runSelfHealCheck({
|
|
477
490
|
client,
|
|
@@ -493,24 +506,18 @@ async function connectChatChromeSession({
|
|
|
493
506
|
allowNavigate = true,
|
|
494
507
|
slowLive = false
|
|
495
508
|
} = {}) {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
targetPredicate: (target) => (
|
|
509
|
-
target?.type === "page"
|
|
510
|
-
&& isRecoverableChatTargetUrl(target?.url)
|
|
511
|
-
)
|
|
512
|
-
});
|
|
513
|
-
}
|
|
509
|
+
const session = await connectToChromeTargetOrOpen({
|
|
510
|
+
host,
|
|
511
|
+
port,
|
|
512
|
+
targetUrlIncludes,
|
|
513
|
+
targetUrl: CHAT_TARGET_URL,
|
|
514
|
+
allowNavigate,
|
|
515
|
+
slowLive,
|
|
516
|
+
fallbackTargetPredicate: (target) => (
|
|
517
|
+
target?.type === "page"
|
|
518
|
+
&& (isRecoverableChatTargetUrl(target?.url) || String(target?.url || "").includes("zhipin.com"))
|
|
519
|
+
)
|
|
520
|
+
});
|
|
514
521
|
|
|
515
522
|
const { client, target } = session;
|
|
516
523
|
await enableDomains(client, ["Page", "DOM", "Input", "Network", "Accessibility"]);
|
|
@@ -527,20 +534,90 @@ async function connectChatChromeSession({
|
|
|
527
534
|
if (allowNavigate && shouldNavigateToChat(targetUrl)) {
|
|
528
535
|
await client.Page.navigate({ url: CHAT_TARGET_URL });
|
|
529
536
|
const settleMs = slowLive ? 10000 : 5000;
|
|
530
|
-
await
|
|
537
|
+
const waited = await waitForMainFrameUrl(
|
|
538
|
+
client,
|
|
539
|
+
(url) => isBossLoginUrl(url) || !shouldNavigateToChat(url),
|
|
540
|
+
{ timeoutMs: settleMs, intervalMs: 500 }
|
|
541
|
+
);
|
|
531
542
|
navigation = {
|
|
532
543
|
navigated: true,
|
|
533
544
|
url: CHAT_TARGET_URL,
|
|
534
|
-
settle_ms: settleMs
|
|
545
|
+
settle_ms: settleMs,
|
|
546
|
+
observed_url: waited.url || null,
|
|
547
|
+
observed_url_ok: waited.ok
|
|
535
548
|
};
|
|
536
549
|
}
|
|
550
|
+
let currentUrl = await getMainFrameUrl(client).catch(() => navigation.url || targetUrl);
|
|
551
|
+
if (allowNavigate && shouldNavigateToChat(currentUrl) && !isBossLoginUrl(currentUrl)) {
|
|
552
|
+
await client.Page.navigate({ url: CHAT_TARGET_URL });
|
|
553
|
+
const settleMs = slowLive ? 10000 : 5000;
|
|
554
|
+
const waited = await waitForMainFrameUrl(
|
|
555
|
+
client,
|
|
556
|
+
(url) => isBossLoginUrl(url) || !shouldNavigateToChat(url),
|
|
557
|
+
{ timeoutMs: settleMs, intervalMs: 500 }
|
|
558
|
+
);
|
|
559
|
+
navigation = {
|
|
560
|
+
navigated: true,
|
|
561
|
+
url: CHAT_TARGET_URL,
|
|
562
|
+
settle_ms: settleMs,
|
|
563
|
+
observed_url: waited.url || null,
|
|
564
|
+
observed_url_ok: waited.ok,
|
|
565
|
+
reason: "observed_url_mismatch"
|
|
566
|
+
};
|
|
567
|
+
currentUrl = await getMainFrameUrl(client).catch(() => waited.url || currentUrl);
|
|
568
|
+
}
|
|
569
|
+
const loginDetection = await detectBossLoginState(client, { currentUrl }).catch(() => ({
|
|
570
|
+
requires_login: isBossLoginUrl(currentUrl),
|
|
571
|
+
reason: "login_detection_failed",
|
|
572
|
+
current_url: currentUrl
|
|
573
|
+
}));
|
|
574
|
+
if (loginDetection.requires_login) {
|
|
575
|
+
await session.close?.();
|
|
576
|
+
throw createBossLoginRequiredError({
|
|
577
|
+
domain: "chat",
|
|
578
|
+
currentUrl: loginDetection.current_url || currentUrl,
|
|
579
|
+
targetUrl: CHAT_TARGET_URL,
|
|
580
|
+
loginDetection,
|
|
581
|
+
chrome: session.chrome || null
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
if (shouldNavigateToChat(currentUrl)) {
|
|
585
|
+
await session.close?.();
|
|
586
|
+
throw new Error(`Boss chat page did not navigate to ${CHAT_TARGET_URL}; current URL: ${currentUrl || "unknown"}`);
|
|
587
|
+
}
|
|
537
588
|
|
|
538
589
|
const selfHealConfig = buildChatSelfHealConfig();
|
|
539
590
|
const health = await waitForHealthyChat(client, selfHealConfig, {
|
|
540
591
|
timeoutMs: slowLive ? 180000 : 90000,
|
|
541
592
|
intervalMs: slowLive ? 1200 : 800
|
|
542
593
|
});
|
|
594
|
+
if (health?.loginDetection?.requires_login) {
|
|
595
|
+
await session.close?.();
|
|
596
|
+
throw createBossLoginRequiredError({
|
|
597
|
+
domain: "chat",
|
|
598
|
+
currentUrl: health.loginDetection.current_url || currentUrl,
|
|
599
|
+
targetUrl: CHAT_TARGET_URL,
|
|
600
|
+
loginDetection: health.loginDetection,
|
|
601
|
+
chrome: session.chrome || null
|
|
602
|
+
});
|
|
603
|
+
}
|
|
543
604
|
if (!health || health.status !== HEALTH_STATUS.HEALTHY) {
|
|
605
|
+
const latestUrl = await getMainFrameUrl(client).catch(() => currentUrl);
|
|
606
|
+
const latestLoginDetection = await detectBossLoginState(client, { currentUrl: latestUrl }).catch(() => ({
|
|
607
|
+
requires_login: isBossLoginUrl(latestUrl),
|
|
608
|
+
reason: "login_detection_failed",
|
|
609
|
+
current_url: latestUrl
|
|
610
|
+
}));
|
|
611
|
+
if (latestLoginDetection.requires_login) {
|
|
612
|
+
await session.close?.();
|
|
613
|
+
throw createBossLoginRequiredError({
|
|
614
|
+
domain: "chat",
|
|
615
|
+
currentUrl: latestLoginDetection.current_url || latestUrl,
|
|
616
|
+
targetUrl: CHAT_TARGET_URL,
|
|
617
|
+
loginDetection: latestLoginDetection,
|
|
618
|
+
chrome: session.chrome || null
|
|
619
|
+
});
|
|
620
|
+
}
|
|
544
621
|
throw new Error(`Boss chat page is not healthy: ${health?.status || "missing"}`);
|
|
545
622
|
}
|
|
546
623
|
|
|
@@ -845,13 +922,21 @@ async function startBossChatRunInternal(args = {}, { workspaceRoot = "" } = {})
|
|
|
845
922
|
slowLive: normalized.slowLive
|
|
846
923
|
});
|
|
847
924
|
} catch (error) {
|
|
925
|
+
const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
|
|
848
926
|
return {
|
|
849
927
|
status: "FAILED",
|
|
850
928
|
error: {
|
|
851
|
-
code: "BOSS_CHAT_PAGE_NOT_READY",
|
|
929
|
+
code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "BOSS_CHAT_PAGE_NOT_READY",
|
|
852
930
|
message: error?.message || "Boss chat page is not ready",
|
|
931
|
+
requires_login: Boolean(error?.requires_login),
|
|
932
|
+
login_url: error?.login_url || null,
|
|
933
|
+
login_detection: error?.login_detection || null,
|
|
934
|
+
chrome: error?.chrome || null,
|
|
935
|
+
current_url: error?.current_url || null,
|
|
936
|
+
target_url: error?.target_url || CHAT_TARGET_URL,
|
|
853
937
|
retryable: true
|
|
854
|
-
}
|
|
938
|
+
},
|
|
939
|
+
chrome: error?.chrome || null
|
|
855
940
|
};
|
|
856
941
|
}
|
|
857
942
|
|
|
@@ -880,7 +965,8 @@ async function startBossChatRunInternal(args = {}, { workspaceRoot = "" } = {})
|
|
|
880
965
|
host: normalized.host,
|
|
881
966
|
port: normalized.port,
|
|
882
967
|
target_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
|
|
883
|
-
target_id: session.target?.id || null
|
|
968
|
+
target_id: session.target?.id || null,
|
|
969
|
+
auto_launch: session.chrome || null
|
|
884
970
|
},
|
|
885
971
|
health: session.health || null
|
|
886
972
|
});
|
|
@@ -912,12 +998,19 @@ export async function prepareBossChatRunTool({ workspaceRoot = "", args = {} } =
|
|
|
912
998
|
slowLive: normalized.slowLive
|
|
913
999
|
});
|
|
914
1000
|
} catch (error) {
|
|
1001
|
+
const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
|
|
915
1002
|
return {
|
|
916
1003
|
status: "FAILED",
|
|
917
1004
|
stage: "chat_run_setup",
|
|
918
1005
|
error: {
|
|
919
|
-
code: "BOSS_CHAT_PAGE_NOT_READY",
|
|
1006
|
+
code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "BOSS_CHAT_PAGE_NOT_READY",
|
|
920
1007
|
message: error?.message || "Boss chat page is not ready",
|
|
1008
|
+
requires_login: Boolean(error?.requires_login),
|
|
1009
|
+
login_url: error?.login_url || null,
|
|
1010
|
+
login_detection: error?.login_detection || null,
|
|
1011
|
+
chrome: error?.chrome || null,
|
|
1012
|
+
current_url: error?.current_url || null,
|
|
1013
|
+
target_url: error?.target_url || CHAT_TARGET_URL,
|
|
921
1014
|
retryable: true
|
|
922
1015
|
},
|
|
923
1016
|
runtime_evaluate_used: false,
|
|
@@ -926,7 +1019,8 @@ export async function prepareBossChatRunTool({ workspaceRoot = "", args = {} } =
|
|
|
926
1019
|
chrome: {
|
|
927
1020
|
host: normalized.host,
|
|
928
1021
|
port: normalized.port,
|
|
929
|
-
target_url: CHAT_TARGET_URL
|
|
1022
|
+
target_url: CHAT_TARGET_URL,
|
|
1023
|
+
auto_launch: error?.chrome || null
|
|
930
1024
|
}
|
|
931
1025
|
};
|
|
932
1026
|
}
|
|
@@ -979,16 +1073,24 @@ export async function prepareBossChatRunTool({ workspaceRoot = "", args = {} } =
|
|
|
979
1073
|
host: normalized.host,
|
|
980
1074
|
port: normalized.port,
|
|
981
1075
|
target_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
|
|
982
|
-
target_id: session.target?.id || null
|
|
1076
|
+
target_id: session.target?.id || null,
|
|
1077
|
+
auto_launch: session.chrome || null
|
|
983
1078
|
}
|
|
984
1079
|
};
|
|
985
1080
|
} catch (error) {
|
|
1081
|
+
const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
|
|
986
1082
|
return {
|
|
987
1083
|
status: "FAILED",
|
|
988
1084
|
stage: "chat_run_setup",
|
|
989
1085
|
error: {
|
|
990
|
-
code: "BOSS_CHAT_PREPARE_FAILED",
|
|
1086
|
+
code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "BOSS_CHAT_PREPARE_FAILED",
|
|
991
1087
|
message: error?.message || "Boss chat CDP-only prepare failed",
|
|
1088
|
+
requires_login: Boolean(error?.requires_login),
|
|
1089
|
+
login_url: error?.login_url || null,
|
|
1090
|
+
login_detection: error?.login_detection || null,
|
|
1091
|
+
chrome: error?.chrome || null,
|
|
1092
|
+
current_url: error?.current_url || null,
|
|
1093
|
+
target_url: error?.target_url || CHAT_TARGET_URL,
|
|
992
1094
|
retryable: true
|
|
993
1095
|
},
|
|
994
1096
|
runtime_evaluate_used: false,
|
|
@@ -998,7 +1100,8 @@ export async function prepareBossChatRunTool({ workspaceRoot = "", args = {} } =
|
|
|
998
1100
|
host: normalized.host,
|
|
999
1101
|
port: normalized.port,
|
|
1000
1102
|
target_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
|
|
1001
|
-
target_id: session.target?.id || null
|
|
1103
|
+
target_id: session.target?.id || null,
|
|
1104
|
+
auto_launch: session.chrome || null
|
|
1002
1105
|
}
|
|
1003
1106
|
};
|
|
1004
1107
|
} finally {
|
|
@@ -1073,17 +1176,25 @@ export async function bossChatHealthCheckTool({ workspaceRoot = "", args = {} }
|
|
|
1073
1176
|
host,
|
|
1074
1177
|
port,
|
|
1075
1178
|
target_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
|
|
1076
|
-
target_id: session.target?.id || null
|
|
1179
|
+
target_id: session.target?.id || null,
|
|
1180
|
+
auto_launch: session.chrome || null
|
|
1077
1181
|
},
|
|
1078
1182
|
message: "Boss chat CDP-only health check passed with shared self-heal probes."
|
|
1079
1183
|
};
|
|
1080
1184
|
} catch (error) {
|
|
1185
|
+
const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
|
|
1081
1186
|
return {
|
|
1082
1187
|
status: "FAILED",
|
|
1083
1188
|
...basePayload,
|
|
1084
1189
|
error: {
|
|
1085
|
-
code: "BOSS_CHAT_PAGE_NOT_READY",
|
|
1190
|
+
code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "BOSS_CHAT_PAGE_NOT_READY",
|
|
1086
1191
|
message: error?.message || "Boss chat page is not ready",
|
|
1192
|
+
requires_login: Boolean(error?.requires_login),
|
|
1193
|
+
login_url: error?.login_url || null,
|
|
1194
|
+
login_detection: error?.login_detection || null,
|
|
1195
|
+
chrome: error?.chrome || null,
|
|
1196
|
+
current_url: error?.current_url || null,
|
|
1197
|
+
target_url: error?.target_url || CHAT_TARGET_URL,
|
|
1087
1198
|
retryable: true
|
|
1088
1199
|
},
|
|
1089
1200
|
runtime_evaluate_used: false,
|
|
@@ -1093,7 +1204,8 @@ export async function bossChatHealthCheckTool({ workspaceRoot = "", args = {} }
|
|
|
1093
1204
|
host,
|
|
1094
1205
|
port,
|
|
1095
1206
|
target_url: session?.navigation?.url || session?.target?.url || targetUrlIncludes,
|
|
1096
|
-
target_id: session?.target?.id || null
|
|
1207
|
+
target_id: session?.target?.id || null,
|
|
1208
|
+
auto_launch: error?.chrome || session?.chrome || null
|
|
1097
1209
|
}
|
|
1098
1210
|
};
|
|
1099
1211
|
} finally {
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
1
5
|
import CDP from "chrome-remote-interface";
|
|
2
6
|
|
|
3
7
|
export const DEFAULT_CHROME_HOST = "127.0.0.1";
|
|
4
8
|
export const DEFAULT_CHROME_PORT = 9222;
|
|
9
|
+
export const BOSS_LOGIN_URL = "https://www.zhipin.com/web/user/?ka=bticket";
|
|
5
10
|
|
|
6
11
|
export const ALLOWED_CDP_DOMAINS = new Set([
|
|
7
12
|
"Accessibility",
|
|
@@ -14,6 +19,21 @@ export const ALLOWED_CDP_DOMAINS = new Set([
|
|
|
14
19
|
|
|
15
20
|
export const FORBIDDEN_CDP_DOMAINS = new Set(["Runtime"]);
|
|
16
21
|
|
|
22
|
+
const BOSS_LOGIN_URL_PATTERN = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com|login\.zhipin\.com)/i;
|
|
23
|
+
const BOSS_LOGIN_TEXT_PATTERN = /扫码登录|验证码登录|密码登录|登录后|请登录|登录BOSS直聘|Boss登录|BOSS登录/i;
|
|
24
|
+
const CHROME_DEBUG_UNAVAILABLE_PATTERN = /ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|connect|socket hang up/i;
|
|
25
|
+
const BOSS_LOGIN_DOM_SELECTORS = [
|
|
26
|
+
".login-box",
|
|
27
|
+
".login-form",
|
|
28
|
+
".login-dialog",
|
|
29
|
+
".sign-form",
|
|
30
|
+
".qrcode-box",
|
|
31
|
+
".user-login",
|
|
32
|
+
"input[name='phone']",
|
|
33
|
+
"input[placeholder*='手机号']",
|
|
34
|
+
"input[placeholder*='验证码']"
|
|
35
|
+
];
|
|
36
|
+
|
|
17
37
|
function nowIso() {
|
|
18
38
|
return new Date().toISOString();
|
|
19
39
|
}
|
|
@@ -49,6 +69,378 @@ export function assertNoForbiddenCdpCalls(methodLog = []) {
|
|
|
49
69
|
}
|
|
50
70
|
}
|
|
51
71
|
|
|
72
|
+
export function isBossLoginUrl(url) {
|
|
73
|
+
return BOSS_LOGIN_URL_PATTERN.test(String(url || ""));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createBossLoginRequiredError({
|
|
77
|
+
domain = "boss",
|
|
78
|
+
currentUrl = "",
|
|
79
|
+
targetUrl = "",
|
|
80
|
+
loginUrl = BOSS_LOGIN_URL,
|
|
81
|
+
loginDetection = null,
|
|
82
|
+
chrome = null
|
|
83
|
+
} = {}) {
|
|
84
|
+
const error = new Error(`Boss login is required before starting the ${domain} run.`);
|
|
85
|
+
error.code = "BOSS_LOGIN_REQUIRED";
|
|
86
|
+
error.requires_login = true;
|
|
87
|
+
error.current_url = currentUrl || null;
|
|
88
|
+
error.target_url = targetUrl || null;
|
|
89
|
+
error.login_url = loginUrl;
|
|
90
|
+
error.login_detection = loginDetection || null;
|
|
91
|
+
error.chrome = chrome || null;
|
|
92
|
+
error.retryable = true;
|
|
93
|
+
return error;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function detectBossLoginState(client, { currentUrl = "" } = {}) {
|
|
97
|
+
const inspectedUrl = currentUrl || await getMainFrameUrl(client).catch(() => "");
|
|
98
|
+
if (isBossLoginUrl(inspectedUrl)) {
|
|
99
|
+
return {
|
|
100
|
+
requires_login: true,
|
|
101
|
+
reason: "url",
|
|
102
|
+
current_url: inspectedUrl,
|
|
103
|
+
matched_selectors: []
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let root = null;
|
|
108
|
+
try {
|
|
109
|
+
root = await getDocumentRoot(client, { depth: 1, pierce: true });
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return {
|
|
112
|
+
requires_login: false,
|
|
113
|
+
reason: "dom_unavailable",
|
|
114
|
+
current_url: inspectedUrl,
|
|
115
|
+
error: error?.message || String(error || "")
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const matchedSelectors = [];
|
|
120
|
+
for (const selector of BOSS_LOGIN_DOM_SELECTORS) {
|
|
121
|
+
const nodeId = await querySelector(client, root.nodeId, selector).catch(() => 0);
|
|
122
|
+
if (nodeId) matchedSelectors.push(selector);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (matchedSelectors.length === 0) {
|
|
126
|
+
return {
|
|
127
|
+
requires_login: false,
|
|
128
|
+
reason: "no_login_dom",
|
|
129
|
+
current_url: inspectedUrl,
|
|
130
|
+
matched_selectors: []
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const html = await getOuterHTML(client, root.nodeId).catch(() => "");
|
|
135
|
+
const looksLikeLogin = BOSS_LOGIN_TEXT_PATTERN.test(html);
|
|
136
|
+
return {
|
|
137
|
+
requires_login: looksLikeLogin,
|
|
138
|
+
reason: looksLikeLogin ? "dom" : "login_selector_without_login_text",
|
|
139
|
+
current_url: inspectedUrl,
|
|
140
|
+
matched_selectors: matchedSelectors
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function isChromeDebugUnavailableError(error) {
|
|
145
|
+
return CHROME_DEBUG_UNAVAILABLE_PATTERN.test(String(error?.message || error || ""));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function pathExists(targetPath) {
|
|
149
|
+
try {
|
|
150
|
+
return Boolean(targetPath) && fs.existsSync(targetPath);
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function ensureDir(targetPath) {
|
|
157
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isLocalChromeHost(host) {
|
|
161
|
+
const normalized = String(host || "").trim().toLowerCase();
|
|
162
|
+
return !normalized || normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getCodexHome() {
|
|
166
|
+
return process.env.CODEX_HOME
|
|
167
|
+
? path.resolve(process.env.CODEX_HOME)
|
|
168
|
+
: path.join(os.homedir(), ".codex");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getDefaultChromeExecutableCandidates() {
|
|
172
|
+
const candidates = [
|
|
173
|
+
process.env.BOSS_MCP_CHROME_PATH,
|
|
174
|
+
process.env.BOSS_RECOMMEND_CHROME_PATH
|
|
175
|
+
].filter(Boolean);
|
|
176
|
+
if (process.platform === "win32") {
|
|
177
|
+
candidates.push(
|
|
178
|
+
path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
179
|
+
path.join(process.env.ProgramFiles || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
180
|
+
path.join(process.env["ProgramFiles(x86)"] || "", "Google", "Chrome", "Application", "chrome.exe")
|
|
181
|
+
);
|
|
182
|
+
} else if (process.platform === "darwin") {
|
|
183
|
+
candidates.push(
|
|
184
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
185
|
+
path.join(os.homedir(), "Applications", "Google Chrome.app", "Contents", "MacOS", "Google Chrome"),
|
|
186
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
187
|
+
);
|
|
188
|
+
} else {
|
|
189
|
+
candidates.push(
|
|
190
|
+
"/usr/bin/google-chrome",
|
|
191
|
+
"/usr/bin/google-chrome-stable",
|
|
192
|
+
"/usr/bin/chromium-browser",
|
|
193
|
+
"/usr/bin/chromium",
|
|
194
|
+
"/snap/bin/chromium"
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return Array.from(new Set(candidates.filter(Boolean)));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function getChromeExecutable() {
|
|
201
|
+
return getDefaultChromeExecutableCandidates().find((candidate) => pathExists(candidate)) || null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function getBossChromeUserDataDir(port = DEFAULT_CHROME_PORT) {
|
|
205
|
+
const sharedPath = path.join(getCodexHome(), "boss-mcp", `chrome-profile-${port}`);
|
|
206
|
+
ensureDir(sharedPath);
|
|
207
|
+
return sharedPath;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function waitForChromeDebugPort({
|
|
211
|
+
host = DEFAULT_CHROME_HOST,
|
|
212
|
+
port = DEFAULT_CHROME_PORT,
|
|
213
|
+
timeoutMs = 8000,
|
|
214
|
+
intervalMs = 300
|
|
215
|
+
} = {}) {
|
|
216
|
+
const started = Date.now();
|
|
217
|
+
let lastError = null;
|
|
218
|
+
while (Date.now() - started <= timeoutMs) {
|
|
219
|
+
try {
|
|
220
|
+
const targets = await listChromeTargets({ host, port });
|
|
221
|
+
return {
|
|
222
|
+
ok: true,
|
|
223
|
+
elapsed_ms: Date.now() - started,
|
|
224
|
+
targets
|
|
225
|
+
};
|
|
226
|
+
} catch (error) {
|
|
227
|
+
lastError = error;
|
|
228
|
+
await sleep(intervalMs);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
elapsed_ms: Date.now() - started,
|
|
234
|
+
error: lastError?.message || String(lastError || "Chrome debug port did not become ready")
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function launchChromeDebugInstance({
|
|
239
|
+
host = DEFAULT_CHROME_HOST,
|
|
240
|
+
port = DEFAULT_CHROME_PORT,
|
|
241
|
+
url = "about:blank",
|
|
242
|
+
slowLive = false
|
|
243
|
+
} = {}) {
|
|
244
|
+
if (!isLocalChromeHost(host)) {
|
|
245
|
+
throw new Error(`Cannot auto-launch Chrome for non-local debug host: ${host}`);
|
|
246
|
+
}
|
|
247
|
+
const chromePath = getChromeExecutable();
|
|
248
|
+
if (!chromePath) {
|
|
249
|
+
throw new Error("Chrome executable not found. Set BOSS_MCP_CHROME_PATH or BOSS_RECOMMEND_CHROME_PATH.");
|
|
250
|
+
}
|
|
251
|
+
const userDataDir = getBossChromeUserDataDir(port);
|
|
252
|
+
const args = [
|
|
253
|
+
`--remote-debugging-port=${port}`,
|
|
254
|
+
`--user-data-dir=${userDataDir}`,
|
|
255
|
+
"--no-first-run",
|
|
256
|
+
"--no-default-browser-check",
|
|
257
|
+
"--new-window",
|
|
258
|
+
url
|
|
259
|
+
];
|
|
260
|
+
const child = spawn(chromePath, args, {
|
|
261
|
+
detached: true,
|
|
262
|
+
stdio: "ignore",
|
|
263
|
+
windowsHide: false
|
|
264
|
+
});
|
|
265
|
+
child.unref();
|
|
266
|
+
const readiness = await waitForChromeDebugPort({
|
|
267
|
+
host,
|
|
268
|
+
port,
|
|
269
|
+
timeoutMs: slowLive ? 30000 : 12000,
|
|
270
|
+
intervalMs: slowLive ? 700 : 300
|
|
271
|
+
});
|
|
272
|
+
if (!readiness.ok) {
|
|
273
|
+
throw new Error(`Chrome launched but DevTools port ${port} did not become reachable: ${readiness.error}`);
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
launched: true,
|
|
277
|
+
chrome_path: chromePath,
|
|
278
|
+
user_data_dir: userDataDir,
|
|
279
|
+
port,
|
|
280
|
+
url,
|
|
281
|
+
readiness: {
|
|
282
|
+
elapsed_ms: readiness.elapsed_ms,
|
|
283
|
+
target_count: readiness.targets.length
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function ensureChromeDebugPort({
|
|
289
|
+
host = DEFAULT_CHROME_HOST,
|
|
290
|
+
port = DEFAULT_CHROME_PORT,
|
|
291
|
+
url = "about:blank",
|
|
292
|
+
slowLive = false,
|
|
293
|
+
launchIfMissing = true
|
|
294
|
+
} = {}) {
|
|
295
|
+
try {
|
|
296
|
+
const targets = await listChromeTargets({ host, port });
|
|
297
|
+
return {
|
|
298
|
+
launched: false,
|
|
299
|
+
reused: true,
|
|
300
|
+
port,
|
|
301
|
+
target_count: targets.length
|
|
302
|
+
};
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (!launchIfMissing || !isChromeDebugUnavailableError(error)) {
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
return launchChromeDebugInstance({ host, port, url, slowLive });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export async function openChromeTarget({
|
|
312
|
+
host = DEFAULT_CHROME_HOST,
|
|
313
|
+
port = DEFAULT_CHROME_PORT,
|
|
314
|
+
url
|
|
315
|
+
} = {}) {
|
|
316
|
+
const encodedUrl = encodeURIComponent(url || "about:blank");
|
|
317
|
+
const endpoint = `http://${host}:${port}/json/new?${encodedUrl}`;
|
|
318
|
+
const methods = ["PUT", "GET"];
|
|
319
|
+
let lastError = null;
|
|
320
|
+
for (const method of methods) {
|
|
321
|
+
try {
|
|
322
|
+
const response = await fetch(endpoint, { method });
|
|
323
|
+
if (response.ok) {
|
|
324
|
+
let payload = null;
|
|
325
|
+
try {
|
|
326
|
+
payload = await response.json();
|
|
327
|
+
} catch {}
|
|
328
|
+
return {
|
|
329
|
+
ok: true,
|
|
330
|
+
method,
|
|
331
|
+
target_id: payload?.id || null,
|
|
332
|
+
url: payload?.url || url || null
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
lastError = new Error(`DevTools /json/new returned ${response.status}`);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
lastError = error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
ok: false,
|
|
342
|
+
error: lastError?.message || "Failed to open Chrome target"
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export async function connectToChromeTargetOrOpen({
|
|
347
|
+
host = DEFAULT_CHROME_HOST,
|
|
348
|
+
port = DEFAULT_CHROME_PORT,
|
|
349
|
+
targetUrlIncludes,
|
|
350
|
+
targetPredicate,
|
|
351
|
+
fallbackTargetPredicate,
|
|
352
|
+
targetUrl,
|
|
353
|
+
allowNavigate = true,
|
|
354
|
+
slowLive = false,
|
|
355
|
+
launchIfMissing = true
|
|
356
|
+
} = {}) {
|
|
357
|
+
let chrome = null;
|
|
358
|
+
if (allowNavigate && targetUrl) {
|
|
359
|
+
chrome = await ensureChromeDebugPort({
|
|
360
|
+
host,
|
|
361
|
+
port,
|
|
362
|
+
url: targetUrl,
|
|
363
|
+
slowLive,
|
|
364
|
+
launchIfMissing
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const session = await connectToChromeTarget({
|
|
370
|
+
host,
|
|
371
|
+
port,
|
|
372
|
+
targetUrlIncludes,
|
|
373
|
+
targetPredicate
|
|
374
|
+
});
|
|
375
|
+
return {
|
|
376
|
+
...session,
|
|
377
|
+
chrome: {
|
|
378
|
+
...(chrome || { launched: false, reused: true, port }),
|
|
379
|
+
target_created: false
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
} catch (primaryError) {
|
|
383
|
+
if (!allowNavigate) throw primaryError;
|
|
384
|
+
|
|
385
|
+
if (typeof fallbackTargetPredicate === "function") {
|
|
386
|
+
try {
|
|
387
|
+
const session = await connectToChromeTarget({
|
|
388
|
+
host,
|
|
389
|
+
port,
|
|
390
|
+
targetPredicate: fallbackTargetPredicate
|
|
391
|
+
});
|
|
392
|
+
return {
|
|
393
|
+
...session,
|
|
394
|
+
chrome: {
|
|
395
|
+
...(chrome || { launched: false, reused: true, port }),
|
|
396
|
+
target_created: false,
|
|
397
|
+
fallback_target: true
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
} catch {}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
let openAttempt = null;
|
|
404
|
+
if (targetUrl) {
|
|
405
|
+
openAttempt = await openChromeTarget({ host, port, url: targetUrl });
|
|
406
|
+
if (openAttempt.ok) {
|
|
407
|
+
const session = await connectToChromeTarget({
|
|
408
|
+
host,
|
|
409
|
+
port,
|
|
410
|
+
targetPredicate: (target) => (
|
|
411
|
+
(openAttempt.target_id && target?.id === openAttempt.target_id)
|
|
412
|
+
|| String(target?.url || "").includes(targetUrlIncludes || targetUrl)
|
|
413
|
+
|| (targetUrl.includes("zhipin.com") && String(target?.url || "").includes("zhipin.com"))
|
|
414
|
+
)
|
|
415
|
+
});
|
|
416
|
+
return {
|
|
417
|
+
...session,
|
|
418
|
+
chrome: {
|
|
419
|
+
...(chrome || { launched: false, reused: true, port }),
|
|
420
|
+
target_created: true,
|
|
421
|
+
open_attempt: openAttempt
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const session = await connectToChromeTarget({
|
|
428
|
+
host,
|
|
429
|
+
port,
|
|
430
|
+
targetPredicate: (target) => target?.type === "page"
|
|
431
|
+
});
|
|
432
|
+
return {
|
|
433
|
+
...session,
|
|
434
|
+
chrome: {
|
|
435
|
+
...(chrome || { launched: false, reused: true, port }),
|
|
436
|
+
target_created: false,
|
|
437
|
+
open_attempt: openAttempt,
|
|
438
|
+
fallback_any_page: true
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
52
444
|
export function createGuardedCdpClient(client, { methodLog = [] } = {}) {
|
|
53
445
|
return new Proxy(client, {
|
|
54
446
|
get(target, property, receiver) {
|
package/src/recommend-mcp.js
CHANGED
|
@@ -4,8 +4,13 @@ import process from "node:process";
|
|
|
4
4
|
import {
|
|
5
5
|
assertNoForbiddenCdpCalls,
|
|
6
6
|
bringPageToFront,
|
|
7
|
-
|
|
7
|
+
connectToChromeTargetOrOpen,
|
|
8
|
+
createBossLoginRequiredError,
|
|
9
|
+
detectBossLoginState,
|
|
8
10
|
enableDomains,
|
|
11
|
+
getMainFrameUrl,
|
|
12
|
+
isBossLoginUrl,
|
|
13
|
+
waitForMainFrameUrl,
|
|
9
14
|
sleep
|
|
10
15
|
} from "./core/browser/index.js";
|
|
11
16
|
import {
|
|
@@ -526,27 +531,36 @@ export async function listRecommendJobsTool({ workspaceRoot = "", args = {} } =
|
|
|
526
531
|
host,
|
|
527
532
|
port,
|
|
528
533
|
target_url: session.navigation?.url || session.target?.url || RECOMMEND_TARGET_URL,
|
|
529
|
-
target_id: session.target?.id || null
|
|
534
|
+
target_id: session.target?.id || null,
|
|
535
|
+
auto_launch: session.chrome || null
|
|
530
536
|
},
|
|
531
537
|
method_summary: methodSummary(session.methodLog || []),
|
|
532
538
|
method_log: session.methodLog || []
|
|
533
539
|
};
|
|
534
540
|
} catch (error) {
|
|
535
541
|
const methodLog = session?.methodLog || [];
|
|
542
|
+
const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
|
|
536
543
|
return {
|
|
537
544
|
status: "FAILED",
|
|
538
545
|
stage: "recommend_job_list",
|
|
539
546
|
cdp_only: true,
|
|
540
547
|
runtime_evaluate_used: methodLog.some((entry) => String(entry?.method || entry).startsWith("Runtime.")),
|
|
541
548
|
error: {
|
|
542
|
-
code: "RECOMMEND_JOB_LIST_FAILED",
|
|
549
|
+
code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "RECOMMEND_JOB_LIST_FAILED",
|
|
543
550
|
message: error?.message || "Failed to read recommend job list",
|
|
551
|
+
requires_login: Boolean(error?.requires_login),
|
|
552
|
+
login_url: error?.login_url || null,
|
|
553
|
+
login_detection: error?.login_detection || null,
|
|
554
|
+
current_url: error?.current_url || null,
|
|
555
|
+
target_url: error?.target_url || RECOMMEND_TARGET_URL,
|
|
556
|
+
chrome: error?.chrome || null,
|
|
544
557
|
retryable: true
|
|
545
558
|
},
|
|
546
559
|
chrome: {
|
|
547
560
|
host,
|
|
548
561
|
port,
|
|
549
|
-
target_url: targetUrlIncludes
|
|
562
|
+
target_url: targetUrlIncludes,
|
|
563
|
+
auto_launch: error?.chrome || session?.chrome || null
|
|
550
564
|
},
|
|
551
565
|
method_summary: methodSummary(methodLog),
|
|
552
566
|
method_log: methodLog
|
|
@@ -585,6 +599,14 @@ async function waitForHealthyRecommend(client, config, {
|
|
|
585
599
|
const started = Date.now();
|
|
586
600
|
let lastCheck = null;
|
|
587
601
|
while (Date.now() - started <= timeoutMs) {
|
|
602
|
+
const loginDetection = await detectBossLoginState(client).catch(() => null);
|
|
603
|
+
if (loginDetection?.requires_login) {
|
|
604
|
+
return {
|
|
605
|
+
status: "login_required",
|
|
606
|
+
summary: "Boss login is required",
|
|
607
|
+
loginDetection
|
|
608
|
+
};
|
|
609
|
+
}
|
|
588
610
|
const roots = await resolveRecommendSelfHealRoots(client, config);
|
|
589
611
|
lastCheck = await runSelfHealCheck({
|
|
590
612
|
client,
|
|
@@ -610,24 +632,18 @@ async function connectRecommendChromeSession({
|
|
|
610
632
|
allowNavigate = true,
|
|
611
633
|
slowLive = false
|
|
612
634
|
} = {}) {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
targetPredicate: (target) => (
|
|
626
|
-
target?.type === "page"
|
|
627
|
-
&& String(target?.url || "").includes("zhipin.com/web/chat")
|
|
628
|
-
)
|
|
629
|
-
});
|
|
630
|
-
}
|
|
635
|
+
const session = await connectToChromeTargetOrOpen({
|
|
636
|
+
host,
|
|
637
|
+
port,
|
|
638
|
+
targetUrlIncludes,
|
|
639
|
+
targetUrl: RECOMMEND_TARGET_URL,
|
|
640
|
+
allowNavigate,
|
|
641
|
+
slowLive,
|
|
642
|
+
fallbackTargetPredicate: (target) => (
|
|
643
|
+
target?.type === "page"
|
|
644
|
+
&& String(target?.url || "").includes("zhipin.com")
|
|
645
|
+
)
|
|
646
|
+
});
|
|
631
647
|
|
|
632
648
|
const { client, target } = session;
|
|
633
649
|
await enableDomains(client, ["Page", "DOM", "Input", "Network", "Accessibility"]);
|
|
@@ -644,12 +660,56 @@ async function connectRecommendChromeSession({
|
|
|
644
660
|
if (allowNavigate && shouldNavigateToRecommend(targetUrl)) {
|
|
645
661
|
await client.Page.navigate({ url: RECOMMEND_TARGET_URL });
|
|
646
662
|
const settleMs = slowLive ? 12000 : 5000;
|
|
647
|
-
await
|
|
663
|
+
const waited = await waitForMainFrameUrl(
|
|
664
|
+
client,
|
|
665
|
+
(url) => isBossLoginUrl(url) || !shouldNavigateToRecommend(url),
|
|
666
|
+
{ timeoutMs: settleMs, intervalMs: 500 }
|
|
667
|
+
);
|
|
668
|
+
navigation = {
|
|
669
|
+
navigated: true,
|
|
670
|
+
url: RECOMMEND_TARGET_URL,
|
|
671
|
+
settle_ms: settleMs,
|
|
672
|
+
observed_url: waited.url || null,
|
|
673
|
+
observed_url_ok: waited.ok
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
let currentUrl = await getMainFrameUrl(client).catch(() => navigation.url || targetUrl);
|
|
677
|
+
if (allowNavigate && shouldNavigateToRecommend(currentUrl) && !isBossLoginUrl(currentUrl)) {
|
|
678
|
+
await client.Page.navigate({ url: RECOMMEND_TARGET_URL });
|
|
679
|
+
const settleMs = slowLive ? 12000 : 5000;
|
|
680
|
+
const waited = await waitForMainFrameUrl(
|
|
681
|
+
client,
|
|
682
|
+
(url) => isBossLoginUrl(url) || !shouldNavigateToRecommend(url),
|
|
683
|
+
{ timeoutMs: settleMs, intervalMs: 500 }
|
|
684
|
+
);
|
|
648
685
|
navigation = {
|
|
649
686
|
navigated: true,
|
|
650
687
|
url: RECOMMEND_TARGET_URL,
|
|
651
|
-
settle_ms: settleMs
|
|
688
|
+
settle_ms: settleMs,
|
|
689
|
+
observed_url: waited.url || null,
|
|
690
|
+
observed_url_ok: waited.ok,
|
|
691
|
+
reason: "observed_url_mismatch"
|
|
652
692
|
};
|
|
693
|
+
currentUrl = await getMainFrameUrl(client).catch(() => waited.url || currentUrl);
|
|
694
|
+
}
|
|
695
|
+
const loginDetection = await detectBossLoginState(client, { currentUrl }).catch(() => ({
|
|
696
|
+
requires_login: isBossLoginUrl(currentUrl),
|
|
697
|
+
reason: "login_detection_failed",
|
|
698
|
+
current_url: currentUrl
|
|
699
|
+
}));
|
|
700
|
+
if (loginDetection.requires_login) {
|
|
701
|
+
await session.close?.();
|
|
702
|
+
throw createBossLoginRequiredError({
|
|
703
|
+
domain: "recommend",
|
|
704
|
+
currentUrl: loginDetection.current_url || currentUrl,
|
|
705
|
+
targetUrl: RECOMMEND_TARGET_URL,
|
|
706
|
+
loginDetection,
|
|
707
|
+
chrome: session.chrome || null
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
if (shouldNavigateToRecommend(currentUrl)) {
|
|
711
|
+
await session.close?.();
|
|
712
|
+
throw new Error(`Boss recommend page did not navigate to ${RECOMMEND_TARGET_URL}; current URL: ${currentUrl || "unknown"}`);
|
|
653
713
|
}
|
|
654
714
|
|
|
655
715
|
const selfHealConfig = buildRecommendSelfHealConfig();
|
|
@@ -657,7 +717,33 @@ async function connectRecommendChromeSession({
|
|
|
657
717
|
timeoutMs: slowLive ? 180000 : 90000,
|
|
658
718
|
intervalMs: slowLive ? 1200 : 800
|
|
659
719
|
});
|
|
720
|
+
if (health?.loginDetection?.requires_login) {
|
|
721
|
+
await session.close?.();
|
|
722
|
+
throw createBossLoginRequiredError({
|
|
723
|
+
domain: "recommend",
|
|
724
|
+
currentUrl: health.loginDetection.current_url || currentUrl,
|
|
725
|
+
targetUrl: RECOMMEND_TARGET_URL,
|
|
726
|
+
loginDetection: health.loginDetection,
|
|
727
|
+
chrome: session.chrome || null
|
|
728
|
+
});
|
|
729
|
+
}
|
|
660
730
|
if (!health || health.status !== HEALTH_STATUS.HEALTHY) {
|
|
731
|
+
const latestUrl = await getMainFrameUrl(client).catch(() => currentUrl);
|
|
732
|
+
const latestLoginDetection = await detectBossLoginState(client, { currentUrl: latestUrl }).catch(() => ({
|
|
733
|
+
requires_login: isBossLoginUrl(latestUrl),
|
|
734
|
+
reason: "login_detection_failed",
|
|
735
|
+
current_url: latestUrl
|
|
736
|
+
}));
|
|
737
|
+
if (latestLoginDetection.requires_login) {
|
|
738
|
+
await session.close?.();
|
|
739
|
+
throw createBossLoginRequiredError({
|
|
740
|
+
domain: "recommend",
|
|
741
|
+
currentUrl: latestLoginDetection.current_url || latestUrl,
|
|
742
|
+
targetUrl: RECOMMEND_TARGET_URL,
|
|
743
|
+
loginDetection: latestLoginDetection,
|
|
744
|
+
chrome: session.chrome || null
|
|
745
|
+
});
|
|
746
|
+
}
|
|
661
747
|
throw new Error(`Boss recommend page is not healthy: ${health?.status || "missing"}`);
|
|
662
748
|
}
|
|
663
749
|
|
|
@@ -964,13 +1050,21 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
|
|
|
964
1050
|
slowLive: normalized.slowLive
|
|
965
1051
|
});
|
|
966
1052
|
} catch (error) {
|
|
1053
|
+
const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
|
|
967
1054
|
return {
|
|
968
1055
|
status: "FAILED",
|
|
969
1056
|
error: {
|
|
970
|
-
code: "BOSS_RECOMMEND_PAGE_NOT_READY",
|
|
1057
|
+
code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "BOSS_RECOMMEND_PAGE_NOT_READY",
|
|
971
1058
|
message: error?.message || "Boss recommend page is not ready",
|
|
1059
|
+
requires_login: Boolean(error?.requires_login),
|
|
1060
|
+
login_url: error?.login_url || null,
|
|
1061
|
+
login_detection: error?.login_detection || null,
|
|
1062
|
+
chrome: error?.chrome || null,
|
|
1063
|
+
current_url: error?.current_url || null,
|
|
1064
|
+
target_url: error?.target_url || RECOMMEND_TARGET_URL,
|
|
972
1065
|
retryable: true
|
|
973
|
-
}
|
|
1066
|
+
},
|
|
1067
|
+
chrome: error?.chrome || null
|
|
974
1068
|
};
|
|
975
1069
|
}
|
|
976
1070
|
|
|
@@ -1000,7 +1094,8 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
|
|
|
1000
1094
|
host: normalized.host,
|
|
1001
1095
|
port: normalized.port,
|
|
1002
1096
|
target_url: session.navigation?.url || session.target?.url || RECOMMEND_TARGET_URL,
|
|
1003
|
-
target_id: session.target?.id || null
|
|
1097
|
+
target_id: session.target?.id || null,
|
|
1098
|
+
auto_launch: session.chrome || null
|
|
1004
1099
|
},
|
|
1005
1100
|
health: session.health || null
|
|
1006
1101
|
});
|
package/src/recruit-mcp.js
CHANGED
|
@@ -4,8 +4,13 @@ import path from "node:path";
|
|
|
4
4
|
import {
|
|
5
5
|
assertNoForbiddenCdpCalls,
|
|
6
6
|
bringPageToFront,
|
|
7
|
-
|
|
7
|
+
connectToChromeTargetOrOpen,
|
|
8
|
+
createBossLoginRequiredError,
|
|
9
|
+
detectBossLoginState,
|
|
8
10
|
enableDomains,
|
|
11
|
+
getMainFrameUrl,
|
|
12
|
+
isBossLoginUrl,
|
|
13
|
+
waitForMainFrameUrl,
|
|
9
14
|
sleep
|
|
10
15
|
} from "./core/browser/index.js";
|
|
11
16
|
import {
|
|
@@ -586,6 +591,32 @@ function attachMethodEvidence(payload, runId) {
|
|
|
586
591
|
};
|
|
587
592
|
}
|
|
588
593
|
|
|
594
|
+
async function waitForRecruitSearchControlsOrLogin(client, {
|
|
595
|
+
timeoutMs = 90000,
|
|
596
|
+
intervalMs = 300
|
|
597
|
+
} = {}) {
|
|
598
|
+
const started = Date.now();
|
|
599
|
+
let lastControls = null;
|
|
600
|
+
while (Date.now() - started <= timeoutMs) {
|
|
601
|
+
const loginDetection = await detectBossLoginState(client).catch(() => null);
|
|
602
|
+
if (loginDetection?.requires_login) {
|
|
603
|
+
return {
|
|
604
|
+
ok: false,
|
|
605
|
+
reason: "login_required",
|
|
606
|
+
loginDetection
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
const remainingMs = Math.max(1, timeoutMs - (Date.now() - started));
|
|
610
|
+
lastControls = await waitForRecruitSearchControls(client, {
|
|
611
|
+
timeoutMs: Math.min(remainingMs, 1500),
|
|
612
|
+
intervalMs
|
|
613
|
+
});
|
|
614
|
+
if (lastControls.ok) return lastControls;
|
|
615
|
+
await sleep(intervalMs);
|
|
616
|
+
}
|
|
617
|
+
return lastControls || { ok: false, reason: "timeout" };
|
|
618
|
+
}
|
|
619
|
+
|
|
589
620
|
async function connectRecruitChromeSession({
|
|
590
621
|
host = DEFAULT_RECRUIT_HOST,
|
|
591
622
|
port = DEFAULT_RECRUIT_PORT,
|
|
@@ -593,24 +624,18 @@ async function connectRecruitChromeSession({
|
|
|
593
624
|
allowNavigate = true,
|
|
594
625
|
slowLive = false
|
|
595
626
|
} = {}) {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
targetPredicate: (target) => (
|
|
609
|
-
target?.type === "page"
|
|
610
|
-
&& String(target?.url || "").includes("zhipin.com/web/chat")
|
|
611
|
-
)
|
|
612
|
-
});
|
|
613
|
-
}
|
|
627
|
+
const session = await connectToChromeTargetOrOpen({
|
|
628
|
+
host,
|
|
629
|
+
port,
|
|
630
|
+
targetUrlIncludes,
|
|
631
|
+
targetUrl: RECRUIT_TARGET_URL,
|
|
632
|
+
allowNavigate,
|
|
633
|
+
slowLive,
|
|
634
|
+
fallbackTargetPredicate: (target) => (
|
|
635
|
+
target?.type === "page"
|
|
636
|
+
&& String(target?.url || "").includes("zhipin.com")
|
|
637
|
+
)
|
|
638
|
+
});
|
|
614
639
|
|
|
615
640
|
const { client, target } = session;
|
|
616
641
|
await enableDomains(client, ["Page", "DOM", "Input", "Network", "Accessibility"]);
|
|
@@ -620,21 +645,102 @@ async function connectRecruitChromeSession({
|
|
|
620
645
|
await bringPageToFront(client);
|
|
621
646
|
|
|
622
647
|
const targetUrl = String(target?.url || "");
|
|
648
|
+
let navigation = {
|
|
649
|
+
navigated: false,
|
|
650
|
+
url: targetUrl
|
|
651
|
+
};
|
|
623
652
|
if (allowNavigate && !targetUrl.includes(targetUrlIncludes)) {
|
|
624
653
|
await client.Page.navigate({ url: RECRUIT_TARGET_URL });
|
|
625
|
-
|
|
654
|
+
const settleMs = slowLive ? 8000 : 3000;
|
|
655
|
+
const waited = await waitForMainFrameUrl(
|
|
656
|
+
client,
|
|
657
|
+
(url) => isBossLoginUrl(url) || String(url || "").includes(RECRUIT_TARGET_URL),
|
|
658
|
+
{ timeoutMs: settleMs, intervalMs: 500 }
|
|
659
|
+
);
|
|
660
|
+
navigation = {
|
|
661
|
+
navigated: true,
|
|
662
|
+
url: RECRUIT_TARGET_URL,
|
|
663
|
+
settle_ms: settleMs,
|
|
664
|
+
observed_url: waited.url || null,
|
|
665
|
+
observed_url_ok: waited.ok
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
let currentUrl = await getMainFrameUrl(client).catch(() => targetUrl);
|
|
669
|
+
if (allowNavigate && !String(currentUrl || "").includes(RECRUIT_TARGET_URL) && !isBossLoginUrl(currentUrl)) {
|
|
670
|
+
await client.Page.navigate({ url: RECRUIT_TARGET_URL });
|
|
671
|
+
const settleMs = slowLive ? 8000 : 3000;
|
|
672
|
+
const waited = await waitForMainFrameUrl(
|
|
673
|
+
client,
|
|
674
|
+
(url) => isBossLoginUrl(url) || String(url || "").includes(RECRUIT_TARGET_URL),
|
|
675
|
+
{ timeoutMs: settleMs, intervalMs: 500 }
|
|
676
|
+
);
|
|
677
|
+
navigation = {
|
|
678
|
+
navigated: true,
|
|
679
|
+
url: RECRUIT_TARGET_URL,
|
|
680
|
+
settle_ms: settleMs,
|
|
681
|
+
observed_url: waited.url || null,
|
|
682
|
+
observed_url_ok: waited.ok,
|
|
683
|
+
reason: "observed_url_mismatch"
|
|
684
|
+
};
|
|
685
|
+
currentUrl = await getMainFrameUrl(client).catch(() => waited.url || currentUrl);
|
|
686
|
+
}
|
|
687
|
+
const loginDetection = await detectBossLoginState(client, { currentUrl }).catch(() => ({
|
|
688
|
+
requires_login: isBossLoginUrl(currentUrl),
|
|
689
|
+
reason: "login_detection_failed",
|
|
690
|
+
current_url: currentUrl
|
|
691
|
+
}));
|
|
692
|
+
if (loginDetection.requires_login) {
|
|
693
|
+
await session.close?.();
|
|
694
|
+
throw createBossLoginRequiredError({
|
|
695
|
+
domain: "search",
|
|
696
|
+
currentUrl: loginDetection.current_url || currentUrl,
|
|
697
|
+
targetUrl: RECRUIT_TARGET_URL,
|
|
698
|
+
loginDetection,
|
|
699
|
+
chrome: session.chrome || null
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
if (!String(currentUrl || "").includes(RECRUIT_TARGET_URL)) {
|
|
703
|
+
await session.close?.();
|
|
704
|
+
throw new Error(`Boss search page did not navigate to ${RECRUIT_TARGET_URL}; current URL: ${currentUrl || "unknown"}`);
|
|
626
705
|
}
|
|
627
706
|
|
|
628
|
-
const controls = await
|
|
707
|
+
const controls = await waitForRecruitSearchControlsOrLogin(client, {
|
|
629
708
|
timeoutMs: slowLive ? 180000 : 90000,
|
|
630
709
|
intervalMs: 300
|
|
631
710
|
});
|
|
711
|
+
if (controls.loginDetection?.requires_login) {
|
|
712
|
+
await session.close?.();
|
|
713
|
+
throw createBossLoginRequiredError({
|
|
714
|
+
domain: "search",
|
|
715
|
+
currentUrl: controls.loginDetection.current_url || currentUrl,
|
|
716
|
+
targetUrl: RECRUIT_TARGET_URL,
|
|
717
|
+
loginDetection: controls.loginDetection,
|
|
718
|
+
chrome: session.chrome || null
|
|
719
|
+
});
|
|
720
|
+
}
|
|
632
721
|
if (!controls.ok) {
|
|
722
|
+
const latestUrl = await getMainFrameUrl(client).catch(() => currentUrl);
|
|
723
|
+
const latestLoginDetection = await detectBossLoginState(client, { currentUrl: latestUrl }).catch(() => ({
|
|
724
|
+
requires_login: isBossLoginUrl(latestUrl),
|
|
725
|
+
reason: "login_detection_failed",
|
|
726
|
+
current_url: latestUrl
|
|
727
|
+
}));
|
|
728
|
+
if (latestLoginDetection.requires_login) {
|
|
729
|
+
await session.close?.();
|
|
730
|
+
throw createBossLoginRequiredError({
|
|
731
|
+
domain: "search",
|
|
732
|
+
currentUrl: latestLoginDetection.current_url || latestUrl,
|
|
733
|
+
targetUrl: RECRUIT_TARGET_URL,
|
|
734
|
+
loginDetection: latestLoginDetection,
|
|
735
|
+
chrome: session.chrome || null
|
|
736
|
+
});
|
|
737
|
+
}
|
|
633
738
|
throw new Error("Boss recruit search page did not expose ready search controls");
|
|
634
739
|
}
|
|
635
740
|
|
|
636
741
|
return {
|
|
637
742
|
...session,
|
|
743
|
+
navigation,
|
|
638
744
|
controls
|
|
639
745
|
};
|
|
640
746
|
}
|
|
@@ -712,13 +818,21 @@ async function startRecruitPipelineRunInternal(args = {}, { workspaceRoot = "" }
|
|
|
712
818
|
slowLive: args.slow_live === true
|
|
713
819
|
});
|
|
714
820
|
} catch (error) {
|
|
821
|
+
const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
|
|
715
822
|
return {
|
|
716
823
|
status: "FAILED",
|
|
717
824
|
error: {
|
|
718
|
-
code: "BOSS_SEARCH_PAGE_NOT_READY",
|
|
825
|
+
code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "BOSS_SEARCH_PAGE_NOT_READY",
|
|
719
826
|
message: error?.message || "Boss recruit search page is not ready",
|
|
827
|
+
requires_login: Boolean(error?.requires_login),
|
|
828
|
+
login_url: error?.login_url || null,
|
|
829
|
+
login_detection: error?.login_detection || null,
|
|
830
|
+
chrome: error?.chrome || null,
|
|
831
|
+
current_url: error?.current_url || null,
|
|
832
|
+
target_url: error?.target_url || RECRUIT_TARGET_URL,
|
|
720
833
|
retryable: true
|
|
721
|
-
}
|
|
834
|
+
},
|
|
835
|
+
chrome: error?.chrome || null
|
|
722
836
|
};
|
|
723
837
|
}
|
|
724
838
|
|
|
@@ -747,7 +861,8 @@ async function startRecruitPipelineRunInternal(args = {}, { workspaceRoot = "" }
|
|
|
747
861
|
host: normalizeText(args.host) || DEFAULT_RECRUIT_HOST,
|
|
748
862
|
port: parsePositiveInteger(args.port, DEFAULT_RECRUIT_PORT),
|
|
749
863
|
target_url: session.target?.url || RECRUIT_TARGET_URL,
|
|
750
|
-
target_id: session.target?.id || null
|
|
864
|
+
target_id: session.target?.id || null,
|
|
865
|
+
auto_launch: session.chrome || null
|
|
751
866
|
},
|
|
752
867
|
parsed
|
|
753
868
|
});
|