@reconcrap/boss-recommend-mcp 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/pipeline.js CHANGED
@@ -1,17 +1,18 @@
1
1
  import path from "node:path";
2
2
  import { parseRecommendInstruction } from "./parser.js";
3
- import {
4
- attemptPipelineAutoRepair,
5
- ensureBossRecommendPageReady,
6
- listRecommendJobs,
7
- reloadBossRecommendPage,
8
- runPipelinePreflight,
9
- runRecommendSearchCli,
10
- runRecommendScreenCli
11
- } from "./adapters.js";
12
-
13
- const FORCED_RECENT_NOT_VIEW_ON_SCREEN_RECOVERY = "近14天没有";
14
- const MAX_SCREEN_AUTO_RECOVERY_ATTEMPTS = 5;
3
+ import {
4
+ attemptPipelineAutoRepair,
5
+ ensureBossRecommendPageReady,
6
+ listRecommendJobs,
7
+ refreshBossRecommendList,
8
+ reloadBossRecommendPage,
9
+ runPipelinePreflight,
10
+ runRecommendSearchCli,
11
+ runRecommendScreenCli
12
+ } from "./adapters.js";
13
+
14
+ const FORCED_RECENT_NOT_VIEW_ON_SCREEN_RECOVERY = "近14天没有";
15
+ const MAX_SCREEN_AUTO_RECOVERY_ATTEMPTS = 5;
15
16
 
16
17
  function dedupe(values = []) {
17
18
  return [...new Set(values.filter(Boolean))];
@@ -114,7 +115,8 @@ function collectNpmInstallDirs(checks = [], workspaceRoot) {
114
115
  const npmCheckKeys = new Set([
115
116
  "npm_dep_chrome_remote_interface_search",
116
117
  "npm_dep_chrome_remote_interface_screen",
117
- "npm_dep_ws"
118
+ "npm_dep_ws",
119
+ "npm_dep_sharp"
118
120
  ]);
119
121
  const dirs = checks
120
122
  .filter((item) => item && item.ok === false && npmCheckKeys.has(item.key))
@@ -156,26 +158,6 @@ function getNodeInstallCommands() {
156
158
  ];
157
159
  }
158
160
 
159
- function getPythonInstallCommands() {
160
- if (process.platform === "win32") {
161
- return [
162
- "winget install Python.Python.3.12",
163
- "python --version"
164
- ];
165
- }
166
- if (process.platform === "darwin") {
167
- return [
168
- "brew install python",
169
- "python3 --version",
170
- "若系统无 python 命令,请在当前终端建立 python -> python3 别名后重试。"
171
- ];
172
- }
173
- return [
174
- "使用系统包管理器安装 Python(例如 apt / yum / brew)",
175
- "python --version"
176
- ];
177
- }
178
-
179
161
  function formatCommandBlock(commands = []) {
180
162
  return commands.map((command) => `- ${command}`).join("\n");
181
163
  }
@@ -190,9 +172,8 @@ function buildPreflightRecovery(checks = [], workspaceRoot) {
190
172
  failed.has("npm_dep_chrome_remote_interface_search")
191
173
  || failed.has("npm_dep_chrome_remote_interface_screen")
192
174
  || failed.has("npm_dep_ws")
175
+ || failed.has("npm_dep_sharp")
193
176
  );
194
- const needPython = failed.has("python_cli");
195
- const needPillow = failed.has("python_pillow");
196
177
 
197
178
  const ordered_steps = [];
198
179
  if (needScreenConfig) {
@@ -218,37 +199,16 @@ function buildPreflightRecovery(checks = [], workspaceRoot) {
218
199
  if (needNpm) {
219
200
  ordered_steps.push({
220
201
  id: "install_npm_dependencies",
221
- title: "安装 npm 依赖(chrome-remote-interface / ws)",
202
+ title: "安装 npm 依赖(chrome-remote-interface / ws / sharp)",
222
203
  blocked_by: needNode ? ["install_nodejs"] : [],
223
204
  commands: buildNpmInstallCommands(checks, workspaceRoot)
224
205
  });
225
206
  }
226
- if (needPython) {
227
- ordered_steps.push({
228
- id: "install_python",
229
- title: "安装 Python(确保 python 命令可用)",
230
- blocked_by: [],
231
- commands: getPythonInstallCommands()
232
- });
233
- }
234
- if (needPillow) {
235
- ordered_steps.push({
236
- id: "install_pillow",
237
- title: "安装 Pillow",
238
- blocked_by: needPython ? ["install_python"] : [],
239
- commands: [
240
- "python -m pip install --upgrade pip",
241
- "python -m pip install pillow"
242
- ]
243
- });
244
- }
245
207
 
246
208
  const promptLines = [
247
209
  "你是环境修复 agent。请先读取 diagnostics.checks,再严格按下面顺序执行,不要并行跳步:",
248
210
  "1) node_cli 失败 -> 先安装 Node.js,未成功前禁止执行 npm install。",
249
- "2) npm_dep_* 失败 -> 再安装 npm 依赖(chrome-remote-interface / ws)。",
250
- "3) python_cli 失败 -> 安装 Python 并确保 python 命令可用。",
251
- "4) python_pillow 失败 -> 最后安装 Pillow。",
211
+ "2) npm_dep_* 失败 -> 再安装 npm 依赖(chrome-remote-interface / ws / sharp)。",
252
212
  "每一步完成后都重新运行 doctor,直到所有检查通过后再重试流水线。"
253
213
  ];
254
214
  if (needScreenConfig) {
@@ -494,16 +454,17 @@ function buildChromeSetupGuidance({ debugPort, pageState }) {
494
454
  };
495
455
  }
496
456
 
497
- const defaultDependencies = {
498
- attemptPipelineAutoRepair,
499
- parseRecommendInstruction,
500
- ensureBossRecommendPageReady,
501
- listRecommendJobs,
502
- reloadBossRecommendPage,
503
- runPipelinePreflight,
504
- runRecommendSearchCli,
505
- runRecommendScreenCli
506
- };
457
+ const defaultDependencies = {
458
+ attemptPipelineAutoRepair,
459
+ parseRecommendInstruction,
460
+ ensureBossRecommendPageReady,
461
+ listRecommendJobs,
462
+ refreshBossRecommendList,
463
+ reloadBossRecommendPage,
464
+ runPipelinePreflight,
465
+ runRecommendSearchCli,
466
+ runRecommendScreenCli
467
+ };
507
468
 
508
469
  export async function runRecommendPipeline(
509
470
  { workspaceRoot, instruction, confirmation, overrides, resume = null },
@@ -512,16 +473,17 @@ export async function runRecommendPipeline(
512
473
  ) {
513
474
  const injectedDependencies = dependencies || {};
514
475
  const resolvedDependencies = { ...defaultDependencies, ...(dependencies || {}) };
515
- const {
516
- attemptPipelineAutoRepair: attemptAutoRepair,
517
- parseRecommendInstruction: parseInstruction,
518
- ensureBossRecommendPageReady: ensureRecommendPageReady,
519
- listRecommendJobs: listJobs,
520
- reloadBossRecommendPage: reloadRecommendPage,
521
- runPipelinePreflight: runPreflight,
522
- runRecommendSearchCli: searchCli,
523
- runRecommendScreenCli: screenCli
524
- } = resolvedDependencies;
476
+ const {
477
+ attemptPipelineAutoRepair: attemptAutoRepair,
478
+ parseRecommendInstruction: parseInstruction,
479
+ ensureBossRecommendPageReady: ensureRecommendPageReady,
480
+ listRecommendJobs: listJobs,
481
+ refreshBossRecommendList: refreshRecommendList,
482
+ reloadBossRecommendPage: reloadRecommendPage,
483
+ runPipelinePreflight: runPreflight,
484
+ runRecommendSearchCli: searchCli,
485
+ runRecommendScreenCli: screenCli
486
+ } = resolvedDependencies;
525
487
  const runtimeHooks = createPipelineRuntime(runtime);
526
488
  ensurePipelineNotAborted(runtimeHooks.signal);
527
489
 
@@ -811,300 +773,413 @@ export async function runRecommendPipeline(
811
773
  };
812
774
  }
813
775
 
814
- const resumeCompletionReason = normalizeText(resume?.previous_completion_reason || "").toLowerCase();
815
- const isResumeRun = resume?.resume === true;
816
- const resumeFromPausedBeforeScreen = isResumeRun && resumeCompletionReason === "paused_before_screen";
817
- const skipSearchOnResume = isResumeRun && !resumeFromPausedBeforeScreen;
818
- let effectiveSearchParams = { ...parsed.searchParams };
819
- let searchSummary = null;
820
- let shouldRunSearch = !skipSearchOnResume;
821
- let screenAutoRecoveryCount = 0;
822
- let lastAutoRecovery = null;
823
- let currentResumeConfig = {
824
- checkpoint_path: resume?.checkpoint_path || null,
825
- pause_control_path: resume?.pause_control_path || null,
826
- output_csv: resume?.output_csv || null,
827
- resume: resume?.resume === true,
828
- require_checkpoint: skipSearchOnResume
829
- };
830
-
831
- while (true) {
832
- if (shouldRunSearch) {
833
- ensurePipelineNotAborted(runtimeHooks.signal);
834
- runtimeHooks.setStage(
835
- "search",
836
- screenAutoRecoveryCount > 0
837
- ? `自动恢复第 ${screenAutoRecoveryCount} 次:重新执行 recommend search(强制 recent_not_view=${FORCED_RECENT_NOT_VIEW_ON_SCREEN_RECOVERY})。`
838
- : "岗位已确认,开始执行 recommend search。"
839
- );
840
- runtimeHooks.heartbeat("search", lastAutoRecovery);
841
- const searchResult = await searchCli({
842
- workspaceRoot,
843
- searchParams: effectiveSearchParams,
844
- selectedJob: selectedJobToken,
845
- runtime: runtimeHooks.adapterRuntime("search")
846
- });
847
- ensurePipelineNotAborted(runtimeHooks.signal);
848
- if (isProcessAbortError(searchResult.error)) {
849
- throw new PipelineAbortError(searchResult.error?.message || "推荐筛选已取消。");
850
- }
851
- if (!searchResult.ok) {
852
- const searchErrorCode = String(searchResult.error?.code || "");
853
- const searchErrorMessage = String(searchResult.error?.message || "");
854
- const loginRelatedSearchFailure = (
855
- searchErrorCode === "LOGIN_REQUIRED"
856
- || searchErrorCode === "NO_RECOMMEND_IFRAME"
857
- || searchErrorMessage.includes("LOGIN_REQUIRED")
858
- || searchErrorMessage.includes("NO_RECOMMEND_IFRAME")
859
- );
860
- if (loginRelatedSearchFailure) {
861
- const recheck = await ensureRecommendPageReady(workspaceRoot, {
862
- port: preflight.debug_port
863
- });
864
- if (recheck.state === "LOGIN_REQUIRED" || recheck.state === "LOGIN_REQUIRED_AFTER_REDIRECT") {
865
- const guidance = buildChromeSetupGuidance({
866
- debugPort: preflight.debug_port,
867
- pageState: recheck.page_state
868
- });
869
- return buildFailedResponse(
870
- "BOSS_LOGIN_REQUIRED",
871
- "检测到当前 Boss 处于未登录状态,请先登录后再继续。登录页:https://www.zhipin.com/web/user/?ka=bticket",
872
- {
873
- search_params: effectiveSearchParams,
874
- screen_params: parsed.screenParams,
875
- selected_job: selectedJob,
876
- required_user_action: "prepare_boss_recommend_page",
877
- guidance,
878
- diagnostics: {
879
- debug_port: preflight.debug_port,
880
- page_state: recheck.page_state,
881
- stdout: searchResult.stdout?.slice(-1000),
882
- stderr: searchResult.stderr?.slice(-1000),
883
- result: searchResult.structured || null,
884
- auto_recovery: lastAutoRecovery
885
- }
886
- }
887
- );
888
- }
889
- }
890
- return buildFailedResponse(
891
- searchResult.error?.code || "RECOMMEND_SEARCH_FAILED",
892
- searchResult.error?.message || "推荐页筛选执行失败。",
893
- {
894
- search_params: effectiveSearchParams,
895
- screen_params: parsed.screenParams,
896
- selected_job: selectedJob,
897
- diagnostics: {
898
- debug_port: preflight.debug_port,
899
- stdout: searchResult.stdout?.slice(-1000),
900
- stderr: searchResult.stderr?.slice(-1000),
901
- result: searchResult.structured || null,
902
- auto_recovery: lastAutoRecovery
903
- }
904
- }
905
- );
906
- }
907
-
908
- searchSummary = searchResult.summary || {};
909
- if (isPauseRequested(runtimeHooks)) {
910
- return buildPausedResponse("已在 screen 阶段开始前暂停 Recommend 流水线。", {
911
- search_params: effectiveSearchParams,
912
- screen_params: parsed.screenParams,
913
- selected_job: selectedJob,
914
- partial_result: {
915
- candidate_count: searchSummary.candidate_count ?? null,
916
- applied_filters: searchSummary.applied_filters || effectiveSearchParams,
917
- output_csv: currentResumeConfig.output_csv || null,
918
- completion_reason: "paused_before_screen"
919
- }
920
- });
921
- }
922
- ensurePipelineNotAborted(runtimeHooks.signal);
923
- runtimeHooks.setStage("screen", "search 完成,开始执行 recommend screen。");
924
- } else {
925
- ensurePipelineNotAborted(runtimeHooks.signal);
926
- runtimeHooks.setStage("screen", "检测到可续跑 checkpoint,跳过 search,直接恢复 recommend screen。");
927
- }
928
-
929
- runtimeHooks.heartbeat("screen", lastAutoRecovery);
930
- const screenResult = await screenCli({
931
- workspaceRoot,
932
- screenParams: parsed.screenParams,
933
- resume: currentResumeConfig,
934
- runtime: runtimeHooks.adapterRuntime("screen")
935
- });
936
- ensurePipelineNotAborted(runtimeHooks.signal);
937
- if (isProcessAbortError(screenResult.error)) {
938
- throw new PipelineAbortError(screenResult.error?.message || "推荐筛选已取消。");
939
- }
940
- if (screenResult.paused) {
941
- return buildPausedResponse("Recommend 流水线已暂停,可使用 resume 继续。", {
942
- search_params: effectiveSearchParams,
943
- screen_params: parsed.screenParams,
944
- selected_job: selectedJob,
945
- partial_result: screenResult.summary || screenResult.structured?.result || null
946
- });
947
- }
948
- if (!screenResult.ok) {
949
- const partialScreenResult = screenResult.summary || screenResult.structured?.result || null;
950
- const resumeOutputCsv = normalizeText(partialScreenResult?.output_csv || currentResumeConfig.output_csv || "");
951
- const hasCheckpointForRecovery = Boolean(normalizeText(currentResumeConfig.checkpoint_path || ""));
952
- const shouldAutoRecover = (
953
- screenResult.error?.code === "RESUME_CAPTURE_FAILED_CONSECUTIVE_LIMIT"
954
- && screenAutoRecoveryCount < MAX_SCREEN_AUTO_RECOVERY_ATTEMPTS
955
- && hasCheckpointForRecovery
956
- && Boolean(resumeOutputCsv)
957
- );
958
-
959
- if (shouldAutoRecover) {
960
- screenAutoRecoveryCount += 1;
961
- effectiveSearchParams = {
962
- ...effectiveSearchParams,
963
- recent_not_view: FORCED_RECENT_NOT_VIEW_ON_SCREEN_RECOVERY
964
- };
965
- lastAutoRecovery = {
966
- trigger: "RESUME_CAPTURE_FAILED_CONSECUTIVE_LIMIT",
967
- attempt: screenAutoRecoveryCount,
968
- max_attempts: MAX_SCREEN_AUTO_RECOVERY_ATTEMPTS,
969
- original_recent_not_view: parsed.searchParams.recent_not_view,
970
- effective_recent_not_view: effectiveSearchParams.recent_not_view,
971
- partial_result: partialScreenResult
972
- ? {
973
- processed_count: partialScreenResult.processed_count ?? null,
974
- passed_count: partialScreenResult.passed_count ?? null,
975
- skipped_count: partialScreenResult.skipped_count ?? null,
976
- output_csv: partialScreenResult.output_csv || currentResumeConfig.output_csv || null
977
- }
978
- : null
979
- };
980
-
981
- runtimeHooks.setStage(
982
- "screen_recovery",
983
- `screen 连续截图失败,开始自动恢复(第 ${screenAutoRecoveryCount} 次):刷新 recommend 页面并重跑 search。`
984
- );
985
- runtimeHooks.heartbeat("screen_recovery", lastAutoRecovery);
986
-
987
- const reloadResult = typeof reloadRecommendPage === "function"
988
- ? await reloadRecommendPage(workspaceRoot, {
989
- port: preflight.debug_port
990
- })
991
- : null;
992
- ensurePipelineNotAborted(runtimeHooks.signal);
993
-
994
- lastAutoRecovery = {
995
- ...lastAutoRecovery,
996
- reload: reloadResult
997
- ? {
998
- ok: reloadResult.ok,
999
- state: reloadResult.state || null,
1000
- message: reloadResult.message || null,
1001
- reloaded_url: reloadResult.reloaded_url || null
1002
- }
1003
- : null
1004
- };
1005
-
1006
- const recoveryPageCheck = await ensureRecommendPageReady(workspaceRoot, {
1007
- port: preflight.debug_port
1008
- });
1009
- ensurePipelineNotAborted(runtimeHooks.signal);
1010
- if (!recoveryPageCheck.ok) {
1011
- const guidance = buildChromeSetupGuidance({
1012
- debugPort: preflight.debug_port,
1013
- pageState: recoveryPageCheck.page_state
1014
- });
1015
- return buildFailedResponse(
1016
- recoveryPageCheck.state === "LOGIN_REQUIRED" || recoveryPageCheck.state === "LOGIN_REQUIRED_AFTER_REDIRECT"
1017
- ? "BOSS_LOGIN_REQUIRED"
1018
- : recoveryPageCheck.state === "DEBUG_PORT_UNREACHABLE"
1019
- ? "BOSS_CHROME_NOT_CONNECTED"
1020
- : "BOSS_RECOMMEND_PAGE_NOT_READY",
1021
- "自动恢复时无法重新就绪 recommend 页面,请先处理页面状态后再继续。",
1022
- {
1023
- search_params: effectiveSearchParams,
1024
- screen_params: parsed.screenParams,
1025
- selected_job: selectedJob,
1026
- partial_result: partialScreenResult,
1027
- required_user_action: "prepare_boss_recommend_page",
1028
- guidance,
1029
- diagnostics: {
1030
- debug_port: preflight.debug_port,
1031
- page_state: recoveryPageCheck.page_state,
1032
- stdout: screenResult.stdout?.slice(-1000),
1033
- stderr: screenResult.stderr?.slice(-1000),
1034
- result: screenResult.structured || null,
1035
- auto_recovery: lastAutoRecovery
1036
- }
1037
- }
1038
- );
1039
- }
1040
-
1041
- currentResumeConfig = {
1042
- checkpoint_path: currentResumeConfig.checkpoint_path || null,
1043
- pause_control_path: currentResumeConfig.pause_control_path || null,
1044
- output_csv: resumeOutputCsv || null,
1045
- resume: true,
1046
- require_checkpoint: true
1047
- };
1048
- shouldRunSearch = true;
1049
- searchSummary = null;
1050
- continue;
1051
- }
1052
-
1053
- return buildFailedResponse(
1054
- screenResult.error?.code || "RECOMMEND_SCREEN_FAILED",
1055
- screenResult.error?.message || "推荐页筛选执行失败。",
1056
- {
1057
- search_params: effectiveSearchParams,
1058
- screen_params: parsed.screenParams,
1059
- selected_job: selectedJob,
1060
- partial_result: partialScreenResult,
1061
- diagnostics: {
1062
- debug_port: preflight.debug_port,
1063
- stdout: screenResult.stdout?.slice(-1000),
1064
- stderr: screenResult.stderr?.slice(-1000),
1065
- result: screenResult.structured || null,
1066
- auto_recovery: lastAutoRecovery
1067
- }
1068
- }
1069
- );
1070
- }
1071
-
1072
- runtimeHooks.setStage("finalize", "screen 完成,正在汇总结果。");
1073
- runtimeHooks.heartbeat("finalize");
1074
- const durationSec = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
1075
- const finalSearchSummary = searchSummary || {};
1076
- const screenSummary = screenResult.summary || {};
1077
- runtimeHooks.progress("finalize", {
1078
- processed: screenSummary.processed_count ?? 0,
1079
- passed: screenSummary.passed_count ?? 0,
1080
- skipped: screenSummary.skipped_count ?? 0,
1081
- greet_count: screenSummary.greet_count ?? 0
1082
- });
1083
-
1084
- return {
1085
- status: "COMPLETED",
1086
- search_params: effectiveSearchParams,
1087
- screen_params: parsed.screenParams,
1088
- result: {
1089
- candidate_count: finalSearchSummary.candidate_count ?? null,
1090
- applied_filters: finalSearchSummary.applied_filters || effectiveSearchParams,
1091
- processed_count: screenSummary.processed_count ?? 0,
1092
- passed_count: screenSummary.passed_count ?? 0,
1093
- skipped_count: screenSummary.skipped_count ?? 0,
1094
- duration_sec: durationSec,
1095
- output_csv: screenSummary.output_csv || null,
1096
- completion_reason: screenSummary.completion_reason || "screen_completed",
1097
- page_state: finalSearchSummary.page_state || pageCheck.page_state,
1098
- selected_job: finalSearchSummary.selected_job || selectedJob,
1099
- post_action: parsed.screenParams.post_action,
1100
- max_greet_count: parsed.screenParams.max_greet_count,
1101
- greet_count: screenSummary.greet_count ?? 0,
1102
- greet_limit_fallback_count: screenSummary.greet_limit_fallback_count ?? 0,
1103
- auto_recovery: lastAutoRecovery
1104
- },
1105
- message: parsed.screenParams.post_action === "none"
1106
- ? "Recommend 流水线已完成。本次 post_action=none:符合条件的人选仅记录到 CSV,不执行收藏或打招呼。"
1107
- : "Recommend 流水线已完成。post_action 在运行开始时已一次性确认;若选择打招呼并设置上限,超出上限后会自动改为收藏。"
1108
- };
1109
- }
1110
- }
776
+ const resumeCompletionReason = normalizeText(resume?.previous_completion_reason || "").toLowerCase();
777
+ const isResumeRun = resume?.resume === true;
778
+ const resumeFromPausedBeforeScreen = isResumeRun && resumeCompletionReason === "paused_before_screen";
779
+ const skipSearchOnResume = isResumeRun && !resumeFromPausedBeforeScreen;
780
+ let effectiveSearchParams = { ...parsed.searchParams };
781
+ let searchSummary = null;
782
+ let shouldRunSearch = !skipSearchOnResume;
783
+ let screenAutoRecoveryCount = 0;
784
+ let lastAutoRecovery = null;
785
+ let currentResumeConfig = {
786
+ checkpoint_path: resume?.checkpoint_path || null,
787
+ pause_control_path: resume?.pause_control_path || null,
788
+ output_csv: resume?.output_csv || null,
789
+ resume: resume?.resume === true,
790
+ require_checkpoint: skipSearchOnResume
791
+ };
792
+
793
+ while (true) {
794
+ if (shouldRunSearch) {
795
+ ensurePipelineNotAborted(runtimeHooks.signal);
796
+ runtimeHooks.setStage(
797
+ "search",
798
+ screenAutoRecoveryCount > 0
799
+ ? `自动恢复第 ${screenAutoRecoveryCount} 次:重新执行 recommend search(强制 recent_not_view=${FORCED_RECENT_NOT_VIEW_ON_SCREEN_RECOVERY})。`
800
+ : "岗位已确认,开始执行 recommend search。"
801
+ );
802
+ runtimeHooks.heartbeat("search", lastAutoRecovery);
803
+ const searchResult = await searchCli({
804
+ workspaceRoot,
805
+ searchParams: effectiveSearchParams,
806
+ selectedJob: selectedJobToken,
807
+ runtime: runtimeHooks.adapterRuntime("search")
808
+ });
809
+ ensurePipelineNotAborted(runtimeHooks.signal);
810
+ if (isProcessAbortError(searchResult.error)) {
811
+ throw new PipelineAbortError(searchResult.error?.message || "推荐筛选已取消。");
812
+ }
813
+ if (!searchResult.ok) {
814
+ const searchErrorCode = String(searchResult.error?.code || "");
815
+ const searchErrorMessage = String(searchResult.error?.message || "");
816
+ const loginRelatedSearchFailure = (
817
+ searchErrorCode === "LOGIN_REQUIRED"
818
+ || searchErrorCode === "NO_RECOMMEND_IFRAME"
819
+ || searchErrorMessage.includes("LOGIN_REQUIRED")
820
+ || searchErrorMessage.includes("NO_RECOMMEND_IFRAME")
821
+ );
822
+ if (loginRelatedSearchFailure) {
823
+ const recheck = await ensureRecommendPageReady(workspaceRoot, {
824
+ port: preflight.debug_port
825
+ });
826
+ if (recheck.state === "LOGIN_REQUIRED" || recheck.state === "LOGIN_REQUIRED_AFTER_REDIRECT") {
827
+ const guidance = buildChromeSetupGuidance({
828
+ debugPort: preflight.debug_port,
829
+ pageState: recheck.page_state
830
+ });
831
+ return buildFailedResponse(
832
+ "BOSS_LOGIN_REQUIRED",
833
+ "检测到当前 Boss 处于未登录状态,请先登录后再继续。登录页:https://www.zhipin.com/web/user/?ka=bticket",
834
+ {
835
+ search_params: effectiveSearchParams,
836
+ screen_params: parsed.screenParams,
837
+ selected_job: selectedJob,
838
+ required_user_action: "prepare_boss_recommend_page",
839
+ guidance,
840
+ diagnostics: {
841
+ debug_port: preflight.debug_port,
842
+ page_state: recheck.page_state,
843
+ stdout: searchResult.stdout?.slice(-1000),
844
+ stderr: searchResult.stderr?.slice(-1000),
845
+ result: searchResult.structured || null,
846
+ auto_recovery: lastAutoRecovery
847
+ }
848
+ }
849
+ );
850
+ }
851
+ }
852
+ return buildFailedResponse(
853
+ searchResult.error?.code || "RECOMMEND_SEARCH_FAILED",
854
+ searchResult.error?.message || "推荐页筛选执行失败。",
855
+ {
856
+ search_params: effectiveSearchParams,
857
+ screen_params: parsed.screenParams,
858
+ selected_job: selectedJob,
859
+ diagnostics: {
860
+ debug_port: preflight.debug_port,
861
+ stdout: searchResult.stdout?.slice(-1000),
862
+ stderr: searchResult.stderr?.slice(-1000),
863
+ result: searchResult.structured || null,
864
+ auto_recovery: lastAutoRecovery
865
+ }
866
+ }
867
+ );
868
+ }
869
+
870
+ searchSummary = searchResult.summary || {};
871
+ if (isPauseRequested(runtimeHooks)) {
872
+ return buildPausedResponse("已在 screen 阶段开始前暂停 Recommend 流水线。", {
873
+ search_params: effectiveSearchParams,
874
+ screen_params: parsed.screenParams,
875
+ selected_job: selectedJob,
876
+ partial_result: {
877
+ candidate_count: searchSummary.candidate_count ?? null,
878
+ applied_filters: searchSummary.applied_filters || effectiveSearchParams,
879
+ output_csv: currentResumeConfig.output_csv || null,
880
+ completion_reason: "paused_before_screen"
881
+ }
882
+ });
883
+ }
884
+ ensurePipelineNotAborted(runtimeHooks.signal);
885
+ runtimeHooks.setStage("screen", "search 完成,开始执行 recommend screen。");
886
+ } else {
887
+ ensurePipelineNotAborted(runtimeHooks.signal);
888
+ runtimeHooks.setStage("screen", "检测到可续跑 checkpoint,跳过 search,直接恢复 recommend screen。");
889
+ }
890
+
891
+ runtimeHooks.heartbeat("screen", lastAutoRecovery);
892
+ const screenResult = await screenCli({
893
+ workspaceRoot,
894
+ screenParams: parsed.screenParams,
895
+ resume: currentResumeConfig,
896
+ runtime: runtimeHooks.adapterRuntime("screen")
897
+ });
898
+ ensurePipelineNotAborted(runtimeHooks.signal);
899
+ if (isProcessAbortError(screenResult.error)) {
900
+ throw new PipelineAbortError(screenResult.error?.message || "推荐筛选已取消。");
901
+ }
902
+ if (screenResult.paused) {
903
+ return buildPausedResponse("Recommend 流水线已暂停,可使用 resume 继续。", {
904
+ search_params: effectiveSearchParams,
905
+ screen_params: parsed.screenParams,
906
+ selected_job: selectedJob,
907
+ partial_result: screenResult.summary || screenResult.structured?.result || null
908
+ });
909
+ }
910
+ if (!screenResult.ok) {
911
+ const screenErrorCode = String(screenResult.error?.code || "");
912
+ const partialScreenResult = screenResult.summary || screenResult.structured?.result || null;
913
+ const resumeOutputCsv = normalizeText(partialScreenResult?.output_csv || currentResumeConfig.output_csv || "");
914
+ const hasCheckpointForRecovery = Boolean(normalizeText(currentResumeConfig.checkpoint_path || ""));
915
+ const screenPartialForRecovery = partialScreenResult
916
+ ? {
917
+ processed_count: partialScreenResult.processed_count ?? null,
918
+ passed_count: partialScreenResult.passed_count ?? null,
919
+ skipped_count: partialScreenResult.skipped_count ?? null,
920
+ output_csv: partialScreenResult.output_csv || currentResumeConfig.output_csv || null,
921
+ checkpoint_path: partialScreenResult.checkpoint_path || currentResumeConfig.checkpoint_path || null,
922
+ completion_reason: partialScreenResult.completion_reason || null
923
+ }
924
+ : null;
925
+ const isResumeCaptureRecovery = screenErrorCode === "RESUME_CAPTURE_FAILED_CONSECUTIVE_LIMIT";
926
+ const isPageExhaustedRecovery = screenErrorCode === "TARGET_COUNT_NOT_REACHED_PAGE_EXHAUSTED";
927
+ const isRecoverableScreenFailure = isResumeCaptureRecovery || isPageExhaustedRecovery;
928
+ const canRecoverSafely = hasCheckpointForRecovery && Boolean(resumeOutputCsv);
929
+ const hasRecoveryAttemptsRemaining = screenAutoRecoveryCount < MAX_SCREEN_AUTO_RECOVERY_ATTEMPTS;
930
+
931
+ if (isRecoverableScreenFailure && !canRecoverSafely) {
932
+ return buildFailedResponse(
933
+ "SCREEN_AUTO_RECOVERY_UNSAFE",
934
+ "检测到 recommend 自动恢复触发,但缺少 checkpoint 或 output_csv,无法安全续跑。",
935
+ {
936
+ search_params: effectiveSearchParams,
937
+ screen_params: parsed.screenParams,
938
+ selected_job: selectedJob,
939
+ partial_result: partialScreenResult,
940
+ diagnostics: {
941
+ debug_port: preflight.debug_port,
942
+ stdout: screenResult.stdout?.slice(-1000),
943
+ stderr: screenResult.stderr?.slice(-1000),
944
+ result: screenResult.structured || null,
945
+ auto_recovery: {
946
+ trigger: screenErrorCode,
947
+ attempt: screenAutoRecoveryCount,
948
+ max_attempts: MAX_SCREEN_AUTO_RECOVERY_ATTEMPTS,
949
+ original_recent_not_view: parsed.searchParams.recent_not_view,
950
+ effective_recent_not_view: effectiveSearchParams.recent_not_view,
951
+ partial_result: screenPartialForRecovery
952
+ }
953
+ }
954
+ }
955
+ );
956
+ }
957
+
958
+ if (isRecoverableScreenFailure && !hasRecoveryAttemptsRemaining) {
959
+ return buildFailedResponse(
960
+ screenResult.error?.code || "RECOMMEND_SCREEN_FAILED",
961
+ `${screenResult.error?.message || "推荐页筛选执行失败。"} 已达到自动恢复上限 ${MAX_SCREEN_AUTO_RECOVERY_ATTEMPTS} 次。`,
962
+ {
963
+ search_params: effectiveSearchParams,
964
+ screen_params: parsed.screenParams,
965
+ selected_job: selectedJob,
966
+ partial_result: partialScreenResult,
967
+ diagnostics: {
968
+ debug_port: preflight.debug_port,
969
+ stdout: screenResult.stdout?.slice(-1000),
970
+ stderr: screenResult.stderr?.slice(-1000),
971
+ result: screenResult.structured || null,
972
+ auto_recovery: lastAutoRecovery
973
+ }
974
+ }
975
+ );
976
+ }
977
+
978
+ if (isRecoverableScreenFailure && canRecoverSafely && hasRecoveryAttemptsRemaining) {
979
+ screenAutoRecoveryCount += 1;
980
+ lastAutoRecovery = {
981
+ trigger: screenErrorCode,
982
+ attempt: screenAutoRecoveryCount,
983
+ max_attempts: MAX_SCREEN_AUTO_RECOVERY_ATTEMPTS,
984
+ original_recent_not_view: parsed.searchParams.recent_not_view,
985
+ effective_recent_not_view: effectiveSearchParams.recent_not_view,
986
+ partial_result: screenPartialForRecovery,
987
+ page_exhaustion: screenResult.error?.page_exhaustion || null
988
+ };
989
+
990
+ if (isPageExhaustedRecovery) {
991
+ runtimeHooks.setStage(
992
+ "screen_recovery",
993
+ `推荐列表已到底但未达目标,开始自动补货(第 ${screenAutoRecoveryCount} 次):优先尝试页内刷新。`
994
+ );
995
+ runtimeHooks.heartbeat("screen_recovery", lastAutoRecovery);
996
+
997
+ const refreshResult = typeof refreshRecommendList === "function"
998
+ ? await refreshRecommendList(workspaceRoot, {
999
+ port: preflight.debug_port
1000
+ })
1001
+ : {
1002
+ ok: false,
1003
+ action: "in_page_refresh",
1004
+ state: "REFRESH_ADAPTER_MISSING",
1005
+ message: "缺少页内刷新适配器。"
1006
+ };
1007
+ ensurePipelineNotAborted(runtimeHooks.signal);
1008
+
1009
+ lastAutoRecovery = {
1010
+ ...lastAutoRecovery,
1011
+ refresh: refreshResult
1012
+ ? {
1013
+ ok: refreshResult.ok,
1014
+ state: refreshResult.state || null,
1015
+ message: refreshResult.message || null,
1016
+ before_state: refreshResult.before_state || null,
1017
+ after_state: refreshResult.after_state || null
1018
+ }
1019
+ : null
1020
+ };
1021
+
1022
+ if (refreshResult?.ok) {
1023
+ lastAutoRecovery = {
1024
+ ...lastAutoRecovery,
1025
+ action: "in_page_refresh"
1026
+ };
1027
+ currentResumeConfig = {
1028
+ checkpoint_path: currentResumeConfig.checkpoint_path || null,
1029
+ pause_control_path: currentResumeConfig.pause_control_path || null,
1030
+ output_csv: resumeOutputCsv || null,
1031
+ resume: true,
1032
+ require_checkpoint: true
1033
+ };
1034
+ shouldRunSearch = false;
1035
+ searchSummary = null;
1036
+ continue;
1037
+ }
1038
+
1039
+ runtimeHooks.setStage(
1040
+ "screen_recovery",
1041
+ `页内刷新不可用(${refreshResult?.state || "unknown"}),改为刷新 recommend 页面并重跑 search。`
1042
+ );
1043
+ runtimeHooks.heartbeat("screen_recovery", lastAutoRecovery);
1044
+ } else {
1045
+ runtimeHooks.setStage(
1046
+ "screen_recovery",
1047
+ `screen 连续截图失败,开始自动恢复(第 ${screenAutoRecoveryCount} 次):刷新 recommend 页面并重跑 search。`
1048
+ );
1049
+ runtimeHooks.heartbeat("screen_recovery", lastAutoRecovery);
1050
+ }
1051
+
1052
+ effectiveSearchParams = {
1053
+ ...effectiveSearchParams,
1054
+ recent_not_view: FORCED_RECENT_NOT_VIEW_ON_SCREEN_RECOVERY
1055
+ };
1056
+ lastAutoRecovery = {
1057
+ ...lastAutoRecovery,
1058
+ action: "reload_page_and_rerun_search",
1059
+ effective_recent_not_view: effectiveSearchParams.recent_not_view
1060
+ };
1061
+
1062
+ const reloadResult = typeof reloadRecommendPage === "function"
1063
+ ? await reloadRecommendPage(workspaceRoot, {
1064
+ port: preflight.debug_port
1065
+ })
1066
+ : null;
1067
+ ensurePipelineNotAborted(runtimeHooks.signal);
1068
+
1069
+ lastAutoRecovery = {
1070
+ ...lastAutoRecovery,
1071
+ reload: reloadResult
1072
+ ? {
1073
+ ok: reloadResult.ok,
1074
+ state: reloadResult.state || null,
1075
+ message: reloadResult.message || null,
1076
+ reloaded_url: reloadResult.reloaded_url || null
1077
+ }
1078
+ : null
1079
+ };
1080
+
1081
+ const recoveryPageCheck = await ensureRecommendPageReady(workspaceRoot, {
1082
+ port: preflight.debug_port
1083
+ });
1084
+ ensurePipelineNotAborted(runtimeHooks.signal);
1085
+ if (!recoveryPageCheck.ok) {
1086
+ const guidance = buildChromeSetupGuidance({
1087
+ debugPort: preflight.debug_port,
1088
+ pageState: recoveryPageCheck.page_state
1089
+ });
1090
+ return buildFailedResponse(
1091
+ recoveryPageCheck.state === "LOGIN_REQUIRED" || recoveryPageCheck.state === "LOGIN_REQUIRED_AFTER_REDIRECT"
1092
+ ? "BOSS_LOGIN_REQUIRED"
1093
+ : recoveryPageCheck.state === "DEBUG_PORT_UNREACHABLE"
1094
+ ? "BOSS_CHROME_NOT_CONNECTED"
1095
+ : "BOSS_RECOMMEND_PAGE_NOT_READY",
1096
+ "自动恢复时无法重新就绪 recommend 页面,请先处理页面状态后再继续。",
1097
+ {
1098
+ search_params: effectiveSearchParams,
1099
+ screen_params: parsed.screenParams,
1100
+ selected_job: selectedJob,
1101
+ partial_result: partialScreenResult,
1102
+ required_user_action: "prepare_boss_recommend_page",
1103
+ guidance,
1104
+ diagnostics: {
1105
+ debug_port: preflight.debug_port,
1106
+ page_state: recoveryPageCheck.page_state,
1107
+ stdout: screenResult.stdout?.slice(-1000),
1108
+ stderr: screenResult.stderr?.slice(-1000),
1109
+ result: screenResult.structured || null,
1110
+ auto_recovery: lastAutoRecovery
1111
+ }
1112
+ }
1113
+ );
1114
+ }
1115
+
1116
+ currentResumeConfig = {
1117
+ checkpoint_path: currentResumeConfig.checkpoint_path || null,
1118
+ pause_control_path: currentResumeConfig.pause_control_path || null,
1119
+ output_csv: resumeOutputCsv || null,
1120
+ resume: true,
1121
+ require_checkpoint: true
1122
+ };
1123
+ shouldRunSearch = true;
1124
+ searchSummary = null;
1125
+ continue;
1126
+ }
1127
+
1128
+ return buildFailedResponse(
1129
+ screenResult.error?.code || "RECOMMEND_SCREEN_FAILED",
1130
+ screenResult.error?.message || "推荐页筛选执行失败。",
1131
+ {
1132
+ search_params: effectiveSearchParams,
1133
+ screen_params: parsed.screenParams,
1134
+ selected_job: selectedJob,
1135
+ partial_result: partialScreenResult,
1136
+ diagnostics: {
1137
+ debug_port: preflight.debug_port,
1138
+ stdout: screenResult.stdout?.slice(-1000),
1139
+ stderr: screenResult.stderr?.slice(-1000),
1140
+ result: screenResult.structured || null,
1141
+ auto_recovery: lastAutoRecovery
1142
+ }
1143
+ }
1144
+ );
1145
+ }
1146
+
1147
+ runtimeHooks.setStage("finalize", "screen 完成,正在汇总结果。");
1148
+ runtimeHooks.heartbeat("finalize");
1149
+ const durationSec = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
1150
+ const finalSearchSummary = searchSummary || {};
1151
+ const screenSummary = screenResult.summary || {};
1152
+ runtimeHooks.progress("finalize", {
1153
+ processed: screenSummary.processed_count ?? 0,
1154
+ passed: screenSummary.passed_count ?? 0,
1155
+ skipped: screenSummary.skipped_count ?? 0,
1156
+ greet_count: screenSummary.greet_count ?? 0
1157
+ });
1158
+
1159
+ return {
1160
+ status: "COMPLETED",
1161
+ search_params: effectiveSearchParams,
1162
+ screen_params: parsed.screenParams,
1163
+ result: {
1164
+ candidate_count: finalSearchSummary.candidate_count ?? null,
1165
+ applied_filters: finalSearchSummary.applied_filters || effectiveSearchParams,
1166
+ processed_count: screenSummary.processed_count ?? 0,
1167
+ passed_count: screenSummary.passed_count ?? 0,
1168
+ skipped_count: screenSummary.skipped_count ?? 0,
1169
+ duration_sec: durationSec,
1170
+ output_csv: screenSummary.output_csv || null,
1171
+ completion_reason: screenSummary.completion_reason || "screen_completed",
1172
+ page_state: finalSearchSummary.page_state || pageCheck.page_state,
1173
+ selected_job: finalSearchSummary.selected_job || selectedJob,
1174
+ post_action: parsed.screenParams.post_action,
1175
+ max_greet_count: parsed.screenParams.max_greet_count,
1176
+ greet_count: screenSummary.greet_count ?? 0,
1177
+ greet_limit_fallback_count: screenSummary.greet_limit_fallback_count ?? 0,
1178
+ auto_recovery: lastAutoRecovery
1179
+ },
1180
+ message: parsed.screenParams.post_action === "none"
1181
+ ? "Recommend 流水线已完成。本次 post_action=none:符合条件的人选仅记录到 CSV,不执行收藏或打招呼。"
1182
+ : "Recommend 流水线已完成。post_action 在运行开始时已一次性确认;若选择打招呼并设置上限,超出上限后会自动改为收藏。"
1183
+ };
1184
+ }
1185
+ }