@reconcrap/boss-recommend-mcp 2.1.9 → 2.1.11

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.
@@ -75,13 +75,119 @@ let recommendRunService = createRecommendRunService({
75
75
  });
76
76
  const recommendRunMeta = new Map();
77
77
 
78
- function normalizeText(value) {
79
- return String(value || "").replace(/\s+/g, " ").trim();
80
- }
81
-
82
- function parsePositiveInteger(raw, fallback) {
83
- const parsed = Number.parseInt(String(raw || ""), 10);
84
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
78
+ function normalizeText(value) {
79
+ return String(value || "").replace(/\s+/g, " ").trim();
80
+ }
81
+
82
+ function collectRecommendRouteText(args = {}) {
83
+ return normalizeText([
84
+ args.instruction,
85
+ args.criteria,
86
+ args.target_url_includes,
87
+ args.page_scope,
88
+ args.confirmation?.page_value,
89
+ args.confirmation?.job_value,
90
+ args.overrides?.criteria,
91
+ args.overrides?.page_scope,
92
+ args.overrides?.job,
93
+ args.overrides?.target_url_includes
94
+ ].filter((value) => value !== undefined && value !== null).join(" "));
95
+ }
96
+
97
+ function findRouteSignals(text, patterns = []) {
98
+ const signals = [];
99
+ for (const { label, pattern } of patterns) {
100
+ if (pattern.test(text)) signals.push(label);
101
+ }
102
+ return signals;
103
+ }
104
+
105
+ function detectNonRecommendRoute(args = {}) {
106
+ const text = collectRecommendRouteText(args);
107
+ if (!text) return null;
108
+ const chatSignals = findRouteSignals(text, [
109
+ { label: "chat-only", pattern: /\bchat[-\s]?only\b/i },
110
+ { label: "boss-chat", pattern: /\bboss[-\s]?chat\b/i },
111
+ { label: "chat page", pattern: /\bchat\s+page\b/i },
112
+ { label: "chat/index", pattern: /(?:\/web\/chat\/index|chat\/index)/i },
113
+ { label: "聊天页", pattern: /聊天页/ },
114
+ { label: "聊天列表", pattern: /聊天列表/ },
115
+ { label: "未读", pattern: /未读/ },
116
+ { label: "全部聊天", pattern: /全部聊天|所有聊天/ },
117
+ { label: "求简历", pattern: /求简历|索要简历|要简历|在线简历/ }
118
+ ]);
119
+ if (chatSignals.length) {
120
+ return {
121
+ domain: "chat",
122
+ signals: chatSignals,
123
+ text
124
+ };
125
+ }
126
+ const searchSignals = findRouteSignals(text, [
127
+ { label: "search-only", pattern: /\bsearch[-\s]?only\b/i },
128
+ { label: "search page", pattern: /\bsearch\s+page\b/i },
129
+ { label: "recruit pipeline", pattern: /\brecruit\s+pipeline\b/i },
130
+ { label: "chat/search", pattern: /(?:\/web\/chat\/search|chat\/search)/i },
131
+ { label: "搜索页", pattern: /搜索页/ },
132
+ { label: "招聘搜索", pattern: /招聘搜索/ }
133
+ ]);
134
+ if (searchSignals.length) {
135
+ return {
136
+ domain: "search",
137
+ signals: searchSignals,
138
+ text
139
+ };
140
+ }
141
+ return null;
142
+ }
143
+
144
+ function buildWrongRecommendRouteResponse(route) {
145
+ if (route?.domain === "chat") {
146
+ return {
147
+ status: "FAILED",
148
+ route_guard: true,
149
+ error: {
150
+ code: "WRONG_BOSS_TOOL_FOR_CHAT",
151
+ message: "This request is explicitly chat-only/chat-page work. Do not use recommend tools. Use boss_chat_health_check, then list_boss_chat_jobs or prepare_boss_chat_run, then start_boss_chat_run.",
152
+ retryable: false
153
+ },
154
+ detected_domain: "chat",
155
+ detected_signals: route.signals || [],
156
+ recommended_tool_sequence: [
157
+ "boss_chat_health_check",
158
+ "list_boss_chat_jobs",
159
+ "prepare_boss_chat_run",
160
+ "start_boss_chat_run"
161
+ ]
162
+ };
163
+ }
164
+ if (route?.domain === "search") {
165
+ return {
166
+ status: "FAILED",
167
+ route_guard: true,
168
+ error: {
169
+ code: "WRONG_BOSS_TOOL_FOR_SEARCH",
170
+ message: "This request is explicitly search/recruit-page work. Do not use recommend tools. Use run_recruit_pipeline or start_recruit_pipeline_run.",
171
+ retryable: false
172
+ },
173
+ detected_domain: "search",
174
+ detected_signals: route.signals || [],
175
+ recommended_tool_sequence: [
176
+ "run_recruit_pipeline",
177
+ "start_recruit_pipeline_run"
178
+ ]
179
+ };
180
+ }
181
+ return null;
182
+ }
183
+
184
+ function guardRecommendRoute(args = {}) {
185
+ return buildWrongRecommendRouteResponse(detectNonRecommendRoute(args));
186
+ }
187
+
188
+ function parsePositiveInteger(raw, fallback) {
189
+ const parsed = Number.parseInt(String(raw || ""), 10);
190
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
85
191
  }
86
192
 
87
193
  function parseNonNegativeInteger(raw, fallback) {
@@ -345,9 +451,9 @@ function normalizeErrorText(error = {}) {
345
451
  ].join(" "));
346
452
  }
347
453
 
348
- function classifyRecommendRecovery(error = {}) {
349
- const text = normalizeErrorText(error);
350
- if (!text) return null;
454
+ function classifyRecommendRecovery(error = {}) {
455
+ const text = normalizeErrorText(error);
456
+ if (!text) return null;
351
457
  if (/BOSS_LOGIN_REQUIRED/i.test(text)) return "login_required";
352
458
  if (/Could not find node with given id|No node with given id|Node is detached|Cannot find node|DETAIL_STALE_NODE|IMAGE_CAPTURE_STALE_NODE/i.test(text)) {
353
459
  return "transient_stale_dom";
@@ -358,8 +464,13 @@ function classifyRecommendRecovery(error = {}) {
358
464
  if (/(?:aborted|abort|timeout|timed out|fetch failed|socket|network|ECONNRESET|ETIMEDOUT|EAI_AGAIN)/i.test(text)) {
359
465
  return "transient_network_or_llm";
360
466
  }
361
- return null;
362
- }
467
+ return null;
468
+ }
469
+
470
+ function isCancelShutdownError(error = {}) {
471
+ const text = normalizeErrorText(error);
472
+ return /socket hang up|ECONNREFUSED|ECONNRESET|WebSocket is not open|Target closed|Session closed|Connection closed|RUN_PROCESS_EXITED|DETACHED_WORKER|RUN_WORKER/i.test(text);
473
+ }
363
474
 
364
475
  function buildConstrainedAgentRecovery(snapshot = {}, meta = {}, artifacts = null) {
365
476
  const error = snapshot?.error || snapshot?.result?.error || null;
@@ -574,15 +685,31 @@ function normalizeRunSnapshot(snapshot) {
574
685
  };
575
686
  }
576
687
 
577
- function mergePersistedControlRequest(normalized, existing) {
578
- const control = {
579
- ...(normalized?.control || {})
580
- };
581
- if (!normalized || TERMINAL_STATUSES.has(normalized.state)) return control;
582
- const existingControl = plainRecord(existing?.control);
583
- if (existingControl.cancel_requested === true) {
584
- return {
585
- ...control,
688
+ function mergePersistedControlRequest(normalized, existing) {
689
+ const control = {
690
+ ...(normalized?.control || {})
691
+ };
692
+ const existingControl = plainRecord(existing?.control);
693
+ if (!normalized) return control;
694
+ if (TERMINAL_STATUSES.has(normalized.state)) {
695
+ if (
696
+ normalized.state === RUN_STATUS_FAILED
697
+ && existingControl.cancel_requested === true
698
+ && isCancelShutdownError(normalized.error || normalized.result?.error || "")
699
+ ) {
700
+ return {
701
+ ...control,
702
+ pause_requested: true,
703
+ pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
704
+ pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "cancel_recommend_pipeline_run",
705
+ cancel_requested: true
706
+ };
707
+ }
708
+ return control;
709
+ }
710
+ if (existingControl.cancel_requested === true) {
711
+ return {
712
+ ...control,
586
713
  pause_requested: true,
587
714
  pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
588
715
  pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "cancel_recommend_pipeline_run",
@@ -605,22 +732,81 @@ function mergePersistedControlRequest(normalized, existing) {
605
732
  pause_requested_by: null,
606
733
  cancel_requested: false
607
734
  };
608
- }
609
- return control;
610
- }
611
-
612
- function persistRecommendRunSnapshot(snapshot, {
613
- persistActiveCheckpoint = false
614
- } = {}) {
615
- const normalized = normalizeRunSnapshot(snapshot);
616
- if (!normalized?.run_id) return normalized;
617
- const artifacts = getRecommendRunArtifacts(normalized.run_id);
618
- if (!artifacts) return normalized;
619
- const existing = readJsonFile(artifacts.run_state_path);
620
- normalized.control = mergePersistedControlRequest(normalized, existing);
621
- if (persistActiveCheckpoint) {
622
- persistRecommendCheckpointSnapshot(normalized);
623
- }
735
+ }
736
+ return control;
737
+ }
738
+
739
+ function cancelErrorFromShutdown(shutdownError = null) {
740
+ return {
741
+ code: "PIPELINE_CANCELED",
742
+ message: "流水线已取消。",
743
+ retryable: true,
744
+ shutdown_error: shutdownError || undefined
745
+ };
746
+ }
747
+
748
+ function coerceCanceledTerminalSnapshot(normalized, existing) {
749
+ const existingControl = plainRecord(existing?.control);
750
+ const shutdownError = normalized?.error || normalized?.result?.error || null;
751
+ const shouldWrapCanceledShutdown = (
752
+ normalized
753
+ && (
754
+ (
755
+ normalized.state === RUN_STATUS_FAILED
756
+ && existingControl.cancel_requested === true
757
+ )
758
+ || normalized.state === RUN_STATUS_CANCELED
759
+ )
760
+ && isCancelShutdownError(shutdownError || "")
761
+ );
762
+ if (
763
+ !shouldWrapCanceledShutdown
764
+ ) {
765
+ return normalized;
766
+ }
767
+ const canceledError = cancelErrorFromShutdown(shutdownError);
768
+ return {
769
+ ...normalized,
770
+ state: RUN_STATUS_CANCELED,
771
+ status: RUN_STATUS_CANCELED,
772
+ last_message: "流水线已取消;取消收尾时浏览器连接已关闭。",
773
+ control: {
774
+ pause_requested: false,
775
+ pause_requested_at: null,
776
+ pause_requested_by: null,
777
+ cancel_requested: false
778
+ },
779
+ error: canceledError,
780
+ result: normalized.result ? {
781
+ ...normalized.result,
782
+ status: "CANCELED",
783
+ completion_reason: "canceled_by_user",
784
+ error: canceledError
785
+ } : {
786
+ status: "CANCELED",
787
+ completion_reason: "canceled_by_user",
788
+ error: canceledError,
789
+ run_id: normalized.run_id,
790
+ processed_count: normalized.progress?.processed || 0,
791
+ screened_count: normalized.progress?.screened || normalized.progress?.processed || 0,
792
+ passed_count: normalized.progress?.passed || 0
793
+ }
794
+ };
795
+ }
796
+
797
+ function persistRecommendRunSnapshot(snapshot, {
798
+ persistActiveCheckpoint = false
799
+ } = {}) {
800
+ let normalized = normalizeRunSnapshot(snapshot);
801
+ if (!normalized?.run_id) return normalized;
802
+ const artifacts = getRecommendRunArtifacts(normalized.run_id);
803
+ if (!artifacts) return normalized;
804
+ const existing = readJsonFile(artifacts.run_state_path);
805
+ normalized.control = mergePersistedControlRequest(normalized, existing);
806
+ normalized = coerceCanceledTerminalSnapshot(normalized, existing);
807
+ if (persistActiveCheckpoint) {
808
+ persistRecommendCheckpointSnapshot(normalized);
809
+ }
624
810
  const payload = {
625
811
  run_id: normalized.run_id,
626
812
  mode: normalized.mode,
@@ -643,29 +829,59 @@ function persistRecommendRunSnapshot(snapshot, {
643
829
  summary: normalized.summary,
644
830
  artifacts: normalized.artifacts
645
831
  };
646
- writeJsonAtomic(artifacts.run_state_path, payload);
647
- return normalized;
648
- }
649
-
650
- function reconcilePersistedRecommendRunIfNeeded(persisted) {
651
- if (!persisted || typeof persisted !== "object") return persisted;
652
- const persistedState = normalizeText(persisted.state || persisted.status);
653
- if (TERMINAL_STATUSES.has(persistedState)) return persisted;
832
+ writeJsonAtomic(artifacts.run_state_path, payload);
833
+ return normalized;
834
+ }
835
+
836
+ function patchPersistedRecommendRunControl(runId, controlPatch = {}, {
837
+ message = ""
838
+ } = {}) {
839
+ const artifacts = getRecommendRunArtifacts(runId);
840
+ if (!artifacts) return null;
841
+ const current = readJsonFile(artifacts.run_state_path);
842
+ const state = normalizeText(current?.state || current?.status || "");
843
+ if (!current || TERMINAL_STATUSES.has(state)) return null;
844
+ const now = new Date().toISOString();
845
+ const patched = {
846
+ ...current,
847
+ updated_at: now,
848
+ heartbeat_at: current.heartbeat_at || now,
849
+ last_message: message || current.last_message || "",
850
+ control: {
851
+ ...(current.control || {}),
852
+ ...controlPatch
853
+ }
854
+ };
855
+ writeJsonAtomic(artifacts.run_state_path, patched);
856
+ return patched;
857
+ }
858
+
859
+ function reconcilePersistedRecommendRunIfNeeded(persisted) {
860
+ if (!persisted || typeof persisted !== "object") return persisted;
861
+ const persistedState = normalizeText(persisted.state || persisted.status);
862
+ if (TERMINAL_STATUSES.has(persistedState)) return persisted;
654
863
  if (isProcessAlive(persisted.pid)) return persisted;
655
864
 
656
- const runId = normalizeRunId(persisted.run_id || persisted.runId);
657
- const artifacts = getRecommendRunArtifacts(runId);
658
- const checkpoint = artifacts?.checkpoint_path ? readJsonFile(artifacts.checkpoint_path) : null;
659
- const now = new Date().toISOString();
660
- const error = {
661
- code: "RUN_PROCESS_EXITED",
662
- message: `检测到推荐任务进程已退出(pid=${persisted.pid || "unknown"}),已自动标记为失败。`,
663
- retryable: true
664
- };
665
- return persistRecommendRunSnapshot({
865
+ const runId = normalizeRunId(persisted.run_id || persisted.runId);
866
+ const artifacts = getRecommendRunArtifacts(runId);
867
+ const checkpoint = artifacts?.checkpoint_path ? readJsonFile(artifacts.checkpoint_path) : null;
868
+ const now = new Date().toISOString();
869
+ const cancelRequested = persisted.control?.cancel_requested === true;
870
+ const processExitedError = {
871
+ code: "RUN_PROCESS_EXITED",
872
+ message: `检测到推荐任务进程已退出(pid=${persisted.pid || "unknown"})。`,
873
+ retryable: true
874
+ };
875
+ const error = cancelRequested
876
+ ? cancelErrorFromShutdown(processExitedError)
877
+ : {
878
+ ...processExitedError,
879
+ message: `检测到推荐任务进程已退出(pid=${persisted.pid || "unknown"}),已自动标记为失败。`
880
+ };
881
+ return persistRecommendRunSnapshot({
666
882
  runId,
667
883
  name: persisted.name || runId,
668
- status: RUN_STATUS_FAILED,
884
+ status: cancelRequested ? RUN_STATUS_CANCELED : RUN_STATUS_FAILED,
669
885
  phase: persisted.stage || persisted.phase || "recommend:orphaned",
670
886
  progress: persisted.progress || {},
671
887
  context: persisted.context || {},
@@ -745,9 +961,17 @@ async function readRecommendJobOptionsFromSession(session) {
745
961
  };
746
962
  }
747
963
 
748
- export async function listRecommendJobsTool({ workspaceRoot = "", args = {} } = {}) {
749
- const configResolution = resolveBossScreeningConfig(workspaceRoot);
750
- const host = normalizeText(args.host) || DEFAULT_RECOMMEND_HOST;
964
+ export async function listRecommendJobsTool({ workspaceRoot = "", args = {} } = {}) {
965
+ const routeGuard = guardRecommendRoute(args);
966
+ if (routeGuard) {
967
+ return {
968
+ ...routeGuard,
969
+ stage: "recommend_job_list",
970
+ message: "list_recommend_jobs is recommend-page only. For chat-only job lists, call list_boss_chat_jobs or prepare_boss_chat_run."
971
+ };
972
+ }
973
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
974
+ const host = normalizeText(args.host) || DEFAULT_RECOMMEND_HOST;
751
975
  const port = parsePositiveInteger(
752
976
  args.port,
753
977
  configResolution.ok ? configResolution.config.debugPort : DEFAULT_RECOMMEND_PORT
@@ -1367,8 +1591,10 @@ function getRunOptions(args, parsed, normalized, session, configResolution = nul
1367
1591
  };
1368
1592
  }
1369
1593
 
1370
- function prepareRecommendPipelineStart(args = {}, { workspaceRoot = "" } = {}) {
1371
- const parsed = parseRecommendPipelineRequest(args);
1594
+ function prepareRecommendPipelineStart(args = {}, { workspaceRoot = "" } = {}) {
1595
+ const routeGuard = guardRecommendRoute(args);
1596
+ if (routeGuard) return { response: routeGuard };
1597
+ const parsed = parseRecommendPipelineRequest(args);
1372
1598
  const gate = evaluateRecommendPipelineGate(parsed, args);
1373
1599
  if (gate) return { response: gate };
1374
1600
  const configResolution = resolveBossScreeningConfig(workspaceRoot);
@@ -1813,20 +2039,45 @@ export function cancelRecommendPipelineRunTool({ args = {} } = {}) {
1813
2039
  }, runId);
1814
2040
  } catch {
1815
2041
  const persisted = readRecommendRunState(runId);
1816
- if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
1817
- return {
1818
- status: "CANCEL_IGNORED",
1819
- run: persisted,
1820
- message: "目标任务已结束,无需取消。",
2042
+ if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
2043
+ return {
2044
+ status: "CANCEL_IGNORED",
2045
+ run: persisted,
2046
+ message: "目标任务已结束,无需取消。",
1821
2047
  runtime_evaluate_used: false,
1822
2048
  method_summary: {},
1823
2049
  method_log: [],
1824
- chrome: null
1825
- };
1826
- }
1827
- return getRecommendPipelineRunTool({ args });
1828
- }
1829
- }
2050
+ chrome: null
2051
+ };
2052
+ }
2053
+ const cancelMessage = "已收到取消请求,将由 detached worker 在下一个安全边界停止。";
2054
+ const patched = patchPersistedRecommendRunControl(runId, {
2055
+ pause_requested: true,
2056
+ pause_requested_at: new Date().toISOString(),
2057
+ pause_requested_by: "cancel_recommend_pipeline_run",
2058
+ cancel_requested: true
2059
+ }, {
2060
+ message: cancelMessage
2061
+ });
2062
+ if (patched) {
2063
+ return {
2064
+ status: "CANCEL_REQUESTED",
2065
+ run: patched,
2066
+ message: cancelMessage,
2067
+ persistence: {
2068
+ source: "disk",
2069
+ active_control_available: false,
2070
+ detached_control_requested: true
2071
+ },
2072
+ runtime_evaluate_used: false,
2073
+ method_summary: {},
2074
+ method_log: [],
2075
+ chrome: null
2076
+ };
2077
+ }
2078
+ return getRecommendPipelineRunTool({ args });
2079
+ }
2080
+ }
1830
2081
 
1831
2082
  export function getRecommendMcpHealthSnapshot(runId) {
1832
2083
  const meta = getRecommendRunMeta(runId);