@reconcrap/boss-recommend-mcp 2.0.1 → 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 +10 -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/domains/recommend/run-service.js +8 -6
- package/src/index.js +5 -1
- package/src/recommend-mcp.js +132 -28
- 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)”处理,不要再追问正整数。
|
|
@@ -31,6 +31,7 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
|
|
|
31
31
|
- `criteria` 必须是用户开放式自然语言;禁止“严格/宽松执行”等预设替代。
|
|
32
32
|
- `post_action=greet` 时,必须确认 `max_greet_count`;禁止自动默认为 `target_count`。
|
|
33
33
|
- 正式执行前必须 `final_confirmed=true`。
|
|
34
|
+
- 真实筛选禁止传 `detail_limit: 0`;recommend 默认必须打开候选人详情/CV。只有用户明确要求“卡片-only 调试”时,才允许同时传 `detail_limit: 0` 和 `allow_card_only_screening: true`。
|
|
34
35
|
|
|
35
36
|
- **Instruction 原文锁定**
|
|
36
37
|
- 首次用户需求原文锁定为 `locked_instruction_raw`。
|
|
@@ -117,8 +118,14 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
|
|
|
117
118
|
|
|
118
119
|
- 执行前必须通过:
|
|
119
120
|
- `screening-config.json` 可用且非占位值(`baseUrl/apiKey/model`)
|
|
120
|
-
- Chrome DevTools
|
|
121
|
-
- 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,工具无法安全自动启动
|
|
122
129
|
|
|
123
130
|
- `PIPELINE_PREFLIGHT_FAILED` 处理顺序:
|
|
124
131
|
1. 若 `screen_config` 失败:让用户提供真实 `baseUrl/apiKey/model`,并在 `guidance.config_path` 修改后明确回复“已修改完成”。
|
|
@@ -152,3 +159,4 @@ MCP 不可用时:
|
|
|
152
159
|
- 页面就绪失败提示必须包含 `debug_port`、recommend URL、以及登录 URL(若未登录):
|
|
153
160
|
- `https://www.zhipin.com/web/chat/recommend`
|
|
154
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) {
|
|
@@ -332,7 +332,7 @@ export async function runRecommendWorkflow({
|
|
|
332
332
|
fallbackPageScope = "recommend",
|
|
333
333
|
filter = {},
|
|
334
334
|
maxCandidates = 5,
|
|
335
|
-
detailLimit
|
|
335
|
+
detailLimit,
|
|
336
336
|
closeDetail = true,
|
|
337
337
|
delayMs = 0,
|
|
338
338
|
cardTimeoutMs = 10000,
|
|
@@ -362,7 +362,7 @@ export async function runRecommendWorkflow({
|
|
|
362
362
|
const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
|
|
363
363
|
const postActionEnabled = normalizedPostAction !== "none";
|
|
364
364
|
const limit = Math.max(1, Number(maxCandidates) || 1);
|
|
365
|
-
const detailCountLimit = Math.max(0, Number(detailLimit) || 0);
|
|
365
|
+
const detailCountLimit = detailLimit == null ? limit : Math.max(0, Number(detailLimit) || 0);
|
|
366
366
|
const effectiveDetailLimit = postActionEnabled ? limit : detailCountLimit;
|
|
367
367
|
const networkRecorder = effectiveDetailLimit > 0
|
|
368
368
|
? createRecommendDetailNetworkRecorder(client)
|
|
@@ -785,7 +785,7 @@ export function createRecommendRunService({
|
|
|
785
785
|
fallbackPageScope = "recommend",
|
|
786
786
|
filter = {},
|
|
787
787
|
maxCandidates = 5,
|
|
788
|
-
detailLimit
|
|
788
|
+
detailLimit,
|
|
789
789
|
closeDetail = true,
|
|
790
790
|
delayMs = 0,
|
|
791
791
|
cardTimeoutMs = 10000,
|
|
@@ -814,6 +814,8 @@ export function createRecommendRunService({
|
|
|
814
814
|
const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
|
|
815
815
|
const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
|
|
816
816
|
const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
|
|
817
|
+
const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
|
|
818
|
+
const normalizedDetailLimit = detailLimit == null ? candidateLimit : Math.max(0, Number(detailLimit) || 0);
|
|
817
819
|
return manager.startRun({
|
|
818
820
|
name,
|
|
819
821
|
context: {
|
|
@@ -825,7 +827,7 @@ export function createRecommendRunService({
|
|
|
825
827
|
fallback_page_scope: normalizedFallbackPageScope,
|
|
826
828
|
filter: normalizedFilter,
|
|
827
829
|
max_candidates: maxCandidates,
|
|
828
|
-
detail_limit:
|
|
830
|
+
detail_limit: normalizedDetailLimit,
|
|
829
831
|
close_detail: closeDetail,
|
|
830
832
|
cv_acquisition_mode: cvAcquisitionMode,
|
|
831
833
|
max_image_pages: maxImagePages,
|
|
@@ -846,7 +848,7 @@ export function createRecommendRunService({
|
|
|
846
848
|
},
|
|
847
849
|
progress: {
|
|
848
850
|
card_count: 0,
|
|
849
|
-
target_count:
|
|
851
|
+
target_count: candidateLimit,
|
|
850
852
|
processed: 0,
|
|
851
853
|
screened: 0,
|
|
852
854
|
detail_opened: 0,
|
|
@@ -864,7 +866,7 @@ export function createRecommendRunService({
|
|
|
864
866
|
fallbackPageScope: normalizedFallbackPageScope,
|
|
865
867
|
filter: normalizedFilter,
|
|
866
868
|
maxCandidates,
|
|
867
|
-
detailLimit,
|
|
869
|
+
detailLimit: normalizedDetailLimit,
|
|
868
870
|
closeDetail,
|
|
869
871
|
delayMs,
|
|
870
872
|
cardTimeoutMs,
|
package/src/index.js
CHANGED
|
@@ -540,7 +540,11 @@ function createRunInputSchema() {
|
|
|
540
540
|
detail_limit: {
|
|
541
541
|
type: "integer",
|
|
542
542
|
minimum: 0,
|
|
543
|
-
description: "
|
|
543
|
+
description: "打开详情/CV 的人数上限;默认跟随 target_count/max_candidates。生产筛选不应传 0;只有 allow_card_only_screening=true 时才会接受 0 作为调试卡片-only 模式"
|
|
544
|
+
},
|
|
545
|
+
allow_card_only_screening: {
|
|
546
|
+
type: "boolean",
|
|
547
|
+
description: "高级调试开关;默认 false。只有显式为 true 时,recommend 才会尊重 detail_limit=0 并只用卡片信息筛选"
|
|
544
548
|
},
|
|
545
549
|
delay_ms: {
|
|
546
550
|
type: "integer",
|
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 {
|
|
@@ -75,6 +80,15 @@ function parseNonNegativeInteger(raw, fallback) {
|
|
|
75
80
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
function resolveRecommendDetailLimit(args = {}, normalized = {}) {
|
|
84
|
+
const fallback = parsePositiveInteger(normalized.targetCount, 5);
|
|
85
|
+
const requested = parseNonNegativeInteger(args.detail_limit, fallback);
|
|
86
|
+
if (requested === 0 && args.allow_card_only_screening !== true) {
|
|
87
|
+
return fallback;
|
|
88
|
+
}
|
|
89
|
+
return requested;
|
|
90
|
+
}
|
|
91
|
+
|
|
78
92
|
function methodSummary(methodLog = []) {
|
|
79
93
|
const summary = {};
|
|
80
94
|
for (const entry of methodLog || []) {
|
|
@@ -517,27 +531,36 @@ export async function listRecommendJobsTool({ workspaceRoot = "", args = {} } =
|
|
|
517
531
|
host,
|
|
518
532
|
port,
|
|
519
533
|
target_url: session.navigation?.url || session.target?.url || RECOMMEND_TARGET_URL,
|
|
520
|
-
target_id: session.target?.id || null
|
|
534
|
+
target_id: session.target?.id || null,
|
|
535
|
+
auto_launch: session.chrome || null
|
|
521
536
|
},
|
|
522
537
|
method_summary: methodSummary(session.methodLog || []),
|
|
523
538
|
method_log: session.methodLog || []
|
|
524
539
|
};
|
|
525
540
|
} catch (error) {
|
|
526
541
|
const methodLog = session?.methodLog || [];
|
|
542
|
+
const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
|
|
527
543
|
return {
|
|
528
544
|
status: "FAILED",
|
|
529
545
|
stage: "recommend_job_list",
|
|
530
546
|
cdp_only: true,
|
|
531
547
|
runtime_evaluate_used: methodLog.some((entry) => String(entry?.method || entry).startsWith("Runtime.")),
|
|
532
548
|
error: {
|
|
533
|
-
code: "RECOMMEND_JOB_LIST_FAILED",
|
|
549
|
+
code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "RECOMMEND_JOB_LIST_FAILED",
|
|
534
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,
|
|
535
557
|
retryable: true
|
|
536
558
|
},
|
|
537
559
|
chrome: {
|
|
538
560
|
host,
|
|
539
561
|
port,
|
|
540
|
-
target_url: targetUrlIncludes
|
|
562
|
+
target_url: targetUrlIncludes,
|
|
563
|
+
auto_launch: error?.chrome || session?.chrome || null
|
|
541
564
|
},
|
|
542
565
|
method_summary: methodSummary(methodLog),
|
|
543
566
|
method_log: methodLog
|
|
@@ -576,6 +599,14 @@ async function waitForHealthyRecommend(client, config, {
|
|
|
576
599
|
const started = Date.now();
|
|
577
600
|
let lastCheck = null;
|
|
578
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
|
+
}
|
|
579
610
|
const roots = await resolveRecommendSelfHealRoots(client, config);
|
|
580
611
|
lastCheck = await runSelfHealCheck({
|
|
581
612
|
client,
|
|
@@ -601,24 +632,18 @@ async function connectRecommendChromeSession({
|
|
|
601
632
|
allowNavigate = true,
|
|
602
633
|
slowLive = false
|
|
603
634
|
} = {}) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
targetPredicate: (target) => (
|
|
617
|
-
target?.type === "page"
|
|
618
|
-
&& String(target?.url || "").includes("zhipin.com/web/chat")
|
|
619
|
-
)
|
|
620
|
-
});
|
|
621
|
-
}
|
|
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
|
+
});
|
|
622
647
|
|
|
623
648
|
const { client, target } = session;
|
|
624
649
|
await enableDomains(client, ["Page", "DOM", "Input", "Network", "Accessibility"]);
|
|
@@ -635,12 +660,56 @@ async function connectRecommendChromeSession({
|
|
|
635
660
|
if (allowNavigate && shouldNavigateToRecommend(targetUrl)) {
|
|
636
661
|
await client.Page.navigate({ url: RECOMMEND_TARGET_URL });
|
|
637
662
|
const settleMs = slowLive ? 12000 : 5000;
|
|
638
|
-
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
|
+
);
|
|
639
685
|
navigation = {
|
|
640
686
|
navigated: true,
|
|
641
687
|
url: RECOMMEND_TARGET_URL,
|
|
642
|
-
settle_ms: settleMs
|
|
688
|
+
settle_ms: settleMs,
|
|
689
|
+
observed_url: waited.url || null,
|
|
690
|
+
observed_url_ok: waited.ok,
|
|
691
|
+
reason: "observed_url_mismatch"
|
|
643
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"}`);
|
|
644
713
|
}
|
|
645
714
|
|
|
646
715
|
const selfHealConfig = buildRecommendSelfHealConfig();
|
|
@@ -648,7 +717,33 @@ async function connectRecommendChromeSession({
|
|
|
648
717
|
timeoutMs: slowLive ? 180000 : 90000,
|
|
649
718
|
intervalMs: slowLive ? 1200 : 800
|
|
650
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
|
+
}
|
|
651
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
|
+
}
|
|
652
747
|
throw new Error(`Boss recommend page is not healthy: ${health?.status || "missing"}`);
|
|
653
748
|
}
|
|
654
749
|
|
|
@@ -867,7 +962,7 @@ function getRunOptions(args, parsed, normalized, session) {
|
|
|
867
962
|
fallbackPageScope: "recommend",
|
|
868
963
|
filter: normalized.filter,
|
|
869
964
|
maxCandidates: normalized.targetCount,
|
|
870
|
-
detailLimit:
|
|
965
|
+
detailLimit: resolveRecommendDetailLimit(args, normalized),
|
|
871
966
|
closeDetail: true,
|
|
872
967
|
delayMs: parseNonNegativeInteger(args.delay_ms, 0),
|
|
873
968
|
cardTimeoutMs: slowLive ? 180000 : 90000,
|
|
@@ -955,13 +1050,21 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
|
|
|
955
1050
|
slowLive: normalized.slowLive
|
|
956
1051
|
});
|
|
957
1052
|
} catch (error) {
|
|
1053
|
+
const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
|
|
958
1054
|
return {
|
|
959
1055
|
status: "FAILED",
|
|
960
1056
|
error: {
|
|
961
|
-
code: "BOSS_RECOMMEND_PAGE_NOT_READY",
|
|
1057
|
+
code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "BOSS_RECOMMEND_PAGE_NOT_READY",
|
|
962
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,
|
|
963
1065
|
retryable: true
|
|
964
|
-
}
|
|
1066
|
+
},
|
|
1067
|
+
chrome: error?.chrome || null
|
|
965
1068
|
};
|
|
966
1069
|
}
|
|
967
1070
|
|
|
@@ -991,7 +1094,8 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
|
|
|
991
1094
|
host: normalized.host,
|
|
992
1095
|
port: normalized.port,
|
|
993
1096
|
target_url: session.navigation?.url || session.target?.url || RECOMMEND_TARGET_URL,
|
|
994
|
-
target_id: session.target?.id || null
|
|
1097
|
+
target_id: session.target?.id || null,
|
|
1098
|
+
auto_launch: session.chrome || null
|
|
995
1099
|
},
|
|
996
1100
|
health: session.health || null
|
|
997
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
|
});
|