@reconcrap/boss-recommend-mcp 2.1.22 → 2.1.23

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "2.1.22",
3
+ "version": "2.1.23",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -37,6 +37,7 @@
37
37
  "test:recommend-domain": "node src/test-recommend-domain.js",
38
38
  "test:recommend-run-service": "node src/test-recommend-run-service.js",
39
39
  "test:recommend-mcp": "node src/test-recommend-mcp.js",
40
+ "test:live-recommend-mcp-smoke": "node src/test-live-recommend-mcp-smoke.js",
40
41
  "test:recruit-domain": "node src/test-recruit-domain.js",
41
42
  "test:recruit-mcp": "node src/test-recruit-mcp.js",
42
43
  "test:chat-domain": "node src/test-chat-domain.js",
@@ -64,10 +65,10 @@
64
65
  "live:self-heal": "node scripts/live-self-heal-smoke.js",
65
66
  "live:recommend-actions": "node scripts/live-recommend-actions-smoke.js",
66
67
  "live:recommend-phase10-full": "node scripts/live-recommend-phase10-full.js",
67
- "live:recommend-domain": "node scripts/live-recommend-domain-smoke.js",
68
- "live:recommend-run-service": "node scripts/live-recommend-run-service-smoke.js",
69
- "live:recommend-mcp": "node scripts/live-recommend-mcp-smoke.js",
70
- "live:search-phase10-full": "node scripts/live-search-phase10-full.js",
68
+ "live:recommend-domain": "node scripts/live-recommend-domain-smoke.js",
69
+ "live:recommend-run-service": "node scripts/live-recommend-run-service-smoke.js",
70
+ "live:recommend-mcp": "node scripts/live-recommend-mcp-smoke.js",
71
+ "live:search-phase10-full": "node scripts/live-search-phase10-full.js",
71
72
  "live:recruit-domain": "node scripts/live-recruit-domain-smoke.js",
72
73
  "live:recruit-run-service": "node scripts/live-recruit-run-service-smoke.js",
73
74
  "live:recruit-mcp": "node scripts/live-recruit-mcp-smoke.js",
@@ -82,9 +83,9 @@
82
83
  "files": [
83
84
  "bin",
84
85
  "config/screening-config.example.json",
85
- "skills",
86
- "scripts/install-macos.sh",
87
- "scripts/postinstall.cjs",
86
+ "skills",
87
+ "scripts/install-macos.sh",
88
+ "scripts/postinstall.cjs",
88
89
  "src/core",
89
90
  "src/domains",
90
91
  "src/chat-mcp.js",
@@ -1691,14 +1691,34 @@ export function isClosedCdpTransportError(error) {
1691
1691
  return CDP_CLOSED_TRANSPORT_PATTERN.test(String(error?.message || error || ""));
1692
1692
  }
1693
1693
 
1694
- function cloneCdpParams(params = {}) {
1695
- if (!params || typeof params !== "object" || typeof params === "function") return params;
1696
- try {
1697
- return JSON.parse(JSON.stringify(params));
1698
- } catch {
1699
- return { ...params };
1700
- }
1701
- }
1694
+ function cloneCdpParams(params = {}) {
1695
+ if (!params || typeof params !== "object" || typeof params === "function") return params;
1696
+ try {
1697
+ return JSON.parse(JSON.stringify(params));
1698
+ } catch {
1699
+ return { ...params };
1700
+ }
1701
+ }
1702
+
1703
+ function annotateCdpError(error, method, params = {}) {
1704
+ if (!error || (typeof error !== "object" && typeof error !== "function")) return error;
1705
+ if (!error.cdp_method) error.cdp_method = String(method || "");
1706
+ if (!error.cdp_at) error.cdp_at = nowIso();
1707
+ const nodeId = Number(params?.nodeId);
1708
+ const backendNodeId = Number(params?.backendNodeId);
1709
+ const searchId = String(params?.searchId || "").trim();
1710
+ if (Number.isInteger(nodeId) && nodeId > 0 && !error.cdp_node_id) {
1711
+ error.cdp_node_id = nodeId;
1712
+ }
1713
+ if (Number.isInteger(backendNodeId) && backendNodeId > 0 && !error.cdp_backend_node_id) {
1714
+ error.cdp_backend_node_id = backendNodeId;
1715
+ }
1716
+ if (searchId && !error.cdp_search_id) error.cdp_search_id = searchId;
1717
+ if (!Array.isArray(error.cdp_param_keys)) {
1718
+ error.cdp_param_keys = Object.keys(params || {}).sort();
1719
+ }
1720
+ return error;
1721
+ }
1702
1722
 
1703
1723
  function shouldReplayCdpSetupCall(domain, method) {
1704
1724
  return method === "enable"
@@ -1745,23 +1765,28 @@ export function createGuardedCdpClient(client, { methodLog = [], reconnect = nul
1745
1765
  return reconnectInFlight;
1746
1766
  }
1747
1767
 
1748
- async function invokeWithReconnect({
1749
- methodNameForLog,
1750
- invoke,
1751
- retryable = true
1752
- }) {
1768
+ async function invokeWithReconnect({
1769
+ methodNameForLog,
1770
+ invoke,
1771
+ params = {},
1772
+ retryable = true
1773
+ }) {
1753
1774
  recordMethod(methodLog, methodNameForLog);
1754
1775
  try {
1755
1776
  return await invoke(currentClient);
1756
- } catch (error) {
1757
- if (!retryable || !isClosedCdpTransportError(error) || typeof reconnect !== "function") {
1758
- throw error;
1759
- }
1760
- await reconnectClient();
1761
- recordMethod(methodLog, `${methodNameForLog}:retry_after_reconnect`);
1762
- return invoke(currentClient);
1763
- }
1764
- }
1777
+ } catch (error) {
1778
+ if (!retryable || !isClosedCdpTransportError(error) || typeof reconnect !== "function") {
1779
+ throw annotateCdpError(error, methodNameForLog, params);
1780
+ }
1781
+ await reconnectClient();
1782
+ recordMethod(methodLog, `${methodNameForLog}:retry_after_reconnect`);
1783
+ try {
1784
+ return await invoke(currentClient);
1785
+ } catch (retryError) {
1786
+ throw annotateCdpError(retryError, methodNameForLog, params);
1787
+ }
1788
+ }
1789
+ }
1765
1790
 
1766
1791
  return new Proxy({}, {
1767
1792
  get(_target, property, receiver) {
@@ -1770,10 +1795,11 @@ export function createGuardedCdpClient(client, { methodLog = [], reconnect = nul
1770
1795
  if (isForbiddenMethod(method)) {
1771
1796
  throw new Error(`Forbidden CDP method blocked: ${method}`);
1772
1797
  }
1773
- return invokeWithReconnect({
1774
- methodNameForLog: method,
1775
- invoke: (activeClient) => activeClient.send(method, params)
1776
- });
1798
+ return invokeWithReconnect({
1799
+ methodNameForLog: method,
1800
+ params,
1801
+ invoke: (activeClient) => activeClient.send(method, params)
1802
+ });
1777
1803
  };
1778
1804
  }
1779
1805
 
@@ -1813,9 +1839,10 @@ export function createGuardedCdpClient(client, { methodLog = [], reconnect = nul
1813
1839
  params: cloneCdpParams(params)
1814
1840
  });
1815
1841
  }
1816
- return invokeWithReconnect({
1817
- methodNameForLog: fullMethod,
1818
- invoke: (activeClient) => {
1842
+ return invokeWithReconnect({
1843
+ methodNameForLog: fullMethod,
1844
+ params,
1845
+ invoke: (activeClient) => {
1819
1846
  const activeDomain = activeClient?.[property];
1820
1847
  const activeMethod = activeDomain?.[method];
1821
1848
  if (typeof activeMethod !== "function") {
@@ -889,12 +889,13 @@ export async function detectInfiniteListBottomMarker(client, {
889
889
  };
890
890
  }
891
891
 
892
- export async function readVisibleInfiniteListItems({
893
- nodeIds = [],
894
- readCandidate,
895
- keyForCandidate = candidateKeyFromProfile,
896
- state = null
897
- } = {}) {
892
+ export async function readVisibleInfiniteListItems({
893
+ nodeIds = [],
894
+ readCandidate,
895
+ keyForCandidate = candidateKeyFromProfile,
896
+ state = null,
897
+ shouldRethrowReadError = null
898
+ } = {}) {
898
899
  if (typeof readCandidate !== "function") {
899
900
  throw new Error("readVisibleInfiniteListItems requires readCandidate");
900
901
  }
@@ -904,19 +905,25 @@ export async function readVisibleInfiniteListItems({
904
905
  let candidate;
905
906
  try {
906
907
  candidate = await readCandidate(nodeId, { visibleIndex });
907
- } catch (error) {
908
- if (state) {
909
- state.read_error_count = (state.read_error_count || 0) + 1;
908
+ } catch (error) {
909
+ if (state) {
910
+ state.read_error_count = (state.read_error_count || 0) + 1;
910
911
  state.ledger?.push({
911
912
  at: nowIso(),
912
913
  event: "candidate_read_error",
913
914
  node_id: nodeId,
914
915
  visible_index: visibleIndex,
915
- error: error?.message || String(error)
916
- });
917
- }
918
- continue;
919
- }
916
+ error: error?.message || String(error)
917
+ });
918
+ }
919
+ if (
920
+ typeof shouldRethrowReadError === "function"
921
+ && shouldRethrowReadError(error, { nodeId, visibleIndex })
922
+ ) {
923
+ throw error;
924
+ }
925
+ continue;
926
+ }
920
927
  const key = keyForCandidate(candidate, {
921
928
  nodeId,
922
929
  visibleIndex,
@@ -1132,8 +1139,9 @@ export async function getNextInfiniteListCandidate({
1132
1139
  state,
1133
1140
  findNodeIds,
1134
1141
  readCandidate,
1135
- detectBottomMarker = null,
1136
- keyForCandidate = candidateKeyFromProfile,
1142
+ detectBottomMarker = null,
1143
+ keyForCandidate = candidateKeyFromProfile,
1144
+ shouldRethrowReadError = null,
1137
1145
  maxScrolls = 20,
1138
1146
  stableSignatureLimit = 2,
1139
1147
  minScrollsBeforeEnd = 3,
@@ -1156,12 +1164,13 @@ export async function getNextInfiniteListCandidate({
1156
1164
  const maxAttempts = Math.max(0, Number(maxScrolls) || 0);
1157
1165
  for (let scrollAttempt = 0; scrollAttempt <= maxAttempts; scrollAttempt += 1) {
1158
1166
  const nodeIds = await findNodeIds();
1159
- const items = await readVisibleInfiniteListItems({
1160
- nodeIds,
1161
- readCandidate,
1162
- keyForCandidate,
1163
- state
1164
- });
1167
+ const items = await readVisibleInfiniteListItems({
1168
+ nodeIds,
1169
+ readCandidate,
1170
+ keyForCandidate,
1171
+ state,
1172
+ shouldRethrowReadError
1173
+ });
1165
1174
  const signature = updateInfiniteListVisibleSignature(state, items);
1166
1175
  const next = firstUnseenInfiniteListItem(state, items);
1167
1176
  attempts.push({
@@ -32,15 +32,32 @@ function clone(value) {
32
32
  return JSON.parse(JSON.stringify(value));
33
33
  }
34
34
 
35
- function errorDiagnostic(error) {
35
+ function errorDiagnostic(error) {
36
36
  if (!error) return null;
37
37
  const diagnostic = {
38
38
  name: error?.name || "Error",
39
39
  message: error?.message || String(error)
40
- };
41
- if (error?.code) diagnostic.code = error.code;
42
- return diagnostic;
43
- }
40
+ };
41
+ if (error?.code) diagnostic.code = error.code;
42
+ if (error?.cdp_method) diagnostic.cdp_method = error.cdp_method;
43
+ if (error?.cdp_at) diagnostic.cdp_at = error.cdp_at;
44
+ if (Number.isInteger(error?.cdp_node_id)) diagnostic.cdp_node_id = error.cdp_node_id;
45
+ if (Number.isInteger(error?.cdp_backend_node_id)) diagnostic.cdp_backend_node_id = error.cdp_backend_node_id;
46
+ if (error?.cdp_search_id) diagnostic.cdp_search_id = error.cdp_search_id;
47
+ if (Array.isArray(error?.cdp_param_keys)) diagnostic.cdp_param_keys = error.cdp_param_keys.slice(0, 20);
48
+ if (error?.stack) diagnostic.stack = String(error.stack).split(/\r?\n/).slice(0, 12).join("\n");
49
+ if (error?.cause && error.cause !== error) {
50
+ diagnostic.cause = {
51
+ name: error.cause?.name || "Error",
52
+ message: error.cause?.message || String(error.cause),
53
+ code: error.cause?.code || undefined,
54
+ cdp_method: error.cause?.cdp_method || undefined,
55
+ cdp_at: error.cause?.cdp_at || undefined,
56
+ cdp_node_id: Number.isInteger(error.cause?.cdp_node_id) ? error.cause.cdp_node_id : undefined
57
+ };
58
+ }
59
+ return diagnostic;
60
+ }
44
61
 
45
62
  function createDeferred() {
46
63
  let resolve;
@@ -387,10 +387,19 @@ export async function readRecommendDetailHtml(client, detailState) {
387
387
  };
388
388
  }
389
389
 
390
- export function isStaleRecommendNodeError(error) {
391
- const message = String(error?.message || error || "");
392
- return /Could not find node with given id|No node with given id|Node is detached|Cannot find node|Could not compute box model/i.test(message);
393
- }
390
+ export function isStaleRecommendNodeError(error) {
391
+ const pattern = /Could not find node with given id|No node with given id|Node is detached|Cannot find node|Could not compute box model/i;
392
+ const seen = new Set();
393
+ let current = error;
394
+ for (let depth = 0; current && depth < 5; depth += 1) {
395
+ if ((typeof current === "object" || typeof current === "function") && seen.has(current)) break;
396
+ if (typeof current === "object" || typeof current === "function") seen.add(current);
397
+ const message = String(current?.message || current || "");
398
+ if (pattern.test(message)) return true;
399
+ current = current?.cause || null;
400
+ }
401
+ return false;
402
+ }
394
403
 
395
404
  export function isRecommendDetailOpenMissError(error) {
396
405
  const message = String(error?.message || error || "");
@@ -136,12 +136,88 @@ export async function applyRecommendFilterEnvelopeStages(filter = {}, {
136
136
 
137
137
  function refreshFailureReason(method = "") {
138
138
  return method === "page_navigate" ? "page_navigate_failed" : "page_reload_failed";
139
- }
140
-
141
- export function isRetryableRecommendFilterReapplyError(error) {
142
- const message = String(error?.message || error || "");
143
- return /Recommend filter panel did not open|Recommend filter trigger was not found|Recommend filter confirm button was not found|No matching recommend filter option/i.test(message);
144
- }
139
+ }
140
+
141
+ function safeDiagnosticText(value, maxLength = 4000) {
142
+ const text = String(value ?? "");
143
+ return text.length > maxLength ? `${text.slice(0, maxLength)}…` : text;
144
+ }
145
+
146
+ function safeDiagnosticStack(stack) {
147
+ if (!stack) return null;
148
+ return safeDiagnosticText(stack, 8000)
149
+ .split(/\r?\n/)
150
+ .slice(0, 12)
151
+ .join("\n");
152
+ }
153
+
154
+ function buildErrorDiagnostic(error, {
155
+ depth = 0,
156
+ seen = new Set()
157
+ } = {}) {
158
+ if (!error) return null;
159
+ const isRecord = typeof error === "object" || typeof error === "function";
160
+ if (isRecord && seen.has(error)) {
161
+ return {
162
+ name: error?.name || "Error",
163
+ message: safeDiagnosticText(error?.message || String(error)),
164
+ circular: true
165
+ };
166
+ }
167
+ if (isRecord) seen.add(error);
168
+ const diagnostic = {
169
+ name: error?.name || "Error",
170
+ message: safeDiagnosticText(error?.message || String(error))
171
+ };
172
+ if (error?.code !== undefined && error?.code !== null && error.code !== "") {
173
+ diagnostic.code = typeof error.code === "number"
174
+ ? error.code
175
+ : safeDiagnosticText(error.code, 300);
176
+ }
177
+ if (error?.phase) diagnostic.phase = safeDiagnosticText(error.phase, 300);
178
+ if (error?.cdp_method) diagnostic.cdp_method = safeDiagnosticText(error.cdp_method, 300);
179
+ if (error?.cdp_at) diagnostic.cdp_at = safeDiagnosticText(error.cdp_at, 100);
180
+ if (Number.isInteger(error?.cdp_node_id)) diagnostic.cdp_node_id = error.cdp_node_id;
181
+ if (Number.isInteger(error?.cdp_backend_node_id)) {
182
+ diagnostic.cdp_backend_node_id = error.cdp_backend_node_id;
183
+ }
184
+ if (Number.isInteger(error?.node_id)) diagnostic.node_id = error.node_id;
185
+ if (Number.isInteger(error?.backend_node_id)) diagnostic.backend_node_id = error.backend_node_id;
186
+ if (error?.cdp_search_id) diagnostic.cdp_search_id = safeDiagnosticText(error.cdp_search_id, 300);
187
+ if (Array.isArray(error?.cdp_param_keys)) {
188
+ diagnostic.cdp_param_keys = error.cdp_param_keys
189
+ .slice(0, 20)
190
+ .map((key) => safeDiagnosticText(key, 100));
191
+ }
192
+ const stack = safeDiagnosticStack(error?.stack);
193
+ if (stack) diagnostic.stack = stack;
194
+ if (depth < 2 && error?.cause && error.cause !== error) {
195
+ diagnostic.cause = buildErrorDiagnostic(error.cause, {
196
+ depth: depth + 1,
197
+ seen
198
+ });
199
+ }
200
+ return diagnostic;
201
+ }
202
+
203
+ export function compactRecommendRefreshErrorDiagnostic(error) {
204
+ return buildErrorDiagnostic(error);
205
+ }
206
+
207
+ export function isRetryableRecommendFilterReapplyError(error) {
208
+ const messages = [];
209
+ const seen = new Set();
210
+ let current = error;
211
+ for (let depth = 0; current && depth < 4; depth += 1) {
212
+ if ((typeof current === "object" || typeof current === "function") && seen.has(current)) break;
213
+ if (typeof current === "object" || typeof current === "function") seen.add(current);
214
+ if (isStaleRecommendNodeError(current)) return true;
215
+ messages.push(String(current?.message || current || ""));
216
+ current = current?.cause;
217
+ }
218
+ const message = messages.join("\n");
219
+ return /Recommend filter panel did not open|Recommend filter trigger was not found|Recommend filter confirm button was not found|No matching recommend filter option|Invalid (?:backend )?node(?:\s*id)?|Node with given id does not exist|No node found for given backend id/i.test(message);
220
+ }
145
221
 
146
222
  function compactFilterReapplyError(error) {
147
223
  return error?.message || String(error || "Recommend filter reapply failed");
@@ -163,9 +239,10 @@ function compactJobSelectionAttempt({
163
239
  return {
164
240
  ok: Boolean(ok),
165
241
  method: "job_select",
166
- reason: error ? "job_select_failed" : null,
167
- error: error ? (error?.message || String(error)) : null,
168
- attempt,
242
+ reason: error ? "job_select_failed" : null,
243
+ error: error ? (error?.message || String(error)) : null,
244
+ error_diagnostic: error ? compactRecommendRefreshErrorDiagnostic(error) : null,
245
+ attempt,
169
246
  iframe_document_node_id: iframeDocumentNodeId || 0,
170
247
  selected: Boolean(selection?.selected),
171
248
  selection_reason: selection?.reason || null,
@@ -324,9 +401,10 @@ async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
324
401
  attempts.push({
325
402
  ok: false,
326
403
  method: "filter_reapply",
327
- reason: "filter_reapply_failed",
328
- error: compactFilterReapplyError(error),
329
- attempt
404
+ reason: "filter_reapply_failed",
405
+ error: compactFilterReapplyError(error),
406
+ error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
407
+ attempt
330
408
  });
331
409
  if (attempt >= maxAttempts || !isRetryableRecommendFilterReapplyError(error)) {
332
410
  break;
@@ -387,6 +465,7 @@ async function ensureRefreshCurrentCityOnly(client, rootState, {
387
465
  method: "current_city_only_reapply",
388
466
  reason: "current_city_only_reapply_failed",
389
467
  error: compactCurrentCityOnlyReapplyError(error),
468
+ error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
390
469
  attempt
391
470
  });
392
471
  if (attempt >= maxAttempts || !isRetryableRecommendCurrentCityOnlyReapplyError(error)) {
@@ -525,8 +604,9 @@ async function applyRefreshMethod(client, method, {
525
604
  return {
526
605
  ok: false,
527
606
  method,
528
- reason: refreshFailureReason(method),
529
- error: error?.message || String(error),
607
+ reason: refreshFailureReason(method),
608
+ error: error?.message || String(error),
609
+ error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
530
610
  target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
531
611
  job_selection: jobSelection,
532
612
  job_selection_attempts: error?.job_selection_attempts || jobSelectionAttempts,
@@ -639,6 +719,7 @@ export async function refreshRecommendListAtEnd(client, {
639
719
  method: "end_refresh_button_after_click",
640
720
  reason: "end_refresh_reapply_failed",
641
721
  error: error?.message || String(error),
722
+ error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
642
723
  page_scope: pageScopeResult,
643
724
  current_city_only: currentCityOnlyResult,
644
725
  current_city_only_attempts: error?.current_city_only_attempts || currentCityOnlyAttempts,