@reconcrap/boss-recommend-mcp 2.0.48 → 2.0.49

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.0.48",
3
+ "version": "2.0.49",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -312,7 +312,7 @@ export async function readRecommendDetailHtml(client, detailState) {
312
312
 
313
313
  export function isStaleRecommendNodeError(error) {
314
314
  const message = String(error?.message || error || "");
315
- return /Could not find node with given id|No node with given id|Node is detached|Cannot find node/i.test(message);
315
+ 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);
316
316
  }
317
317
 
318
318
  export function isRecommendDetailOpenMissError(error) {
@@ -12,6 +12,7 @@ import {
12
12
  htmlToText,
13
13
  normalizeText
14
14
  } from "../../core/screening/index.js";
15
+ import { isStaleRecommendNodeError } from "./detail.js";
15
16
 
16
17
  export const RECOMMEND_JOB_SELECTORS = Object.freeze({
17
18
  trigger: ".job-selecter-wrap, [class*=\"job-selecter-wrap\"], .ui-dropmenu",
@@ -52,15 +53,26 @@ function isVisibleBox(box) {
52
53
  }
53
54
 
54
55
  async function readJobOption(client, nodeId, index) {
55
- const [attributes, outerHTML] = await Promise.all([
56
- getAttributesMap(client, nodeId),
57
- getOuterHTML(client, nodeId)
58
- ]);
56
+ let attributes = null;
57
+ let outerHTML = "";
58
+ try {
59
+ [attributes, outerHTML] = await Promise.all([
60
+ getAttributesMap(client, nodeId),
61
+ getOuterHTML(client, nodeId)
62
+ ]);
63
+ } catch (error) {
64
+ if (isStaleRecommendNodeError(error)) {
65
+ return null;
66
+ }
67
+ throw error;
68
+ }
59
69
  const label = normalizeText(htmlToText(outerHTML));
60
70
  let box = null;
61
71
  try {
62
72
  box = await getNodeBox(client, nodeId);
63
- } catch {}
73
+ } catch (error) {
74
+ if (!isStaleRecommendNodeError(error)) throw error;
75
+ }
64
76
  const className = attributes.class || "";
65
77
  return {
66
78
  node_id: nodeId,
@@ -75,19 +87,40 @@ async function readJobOption(client, nodeId, index) {
75
87
  };
76
88
  }
77
89
 
90
+ async function readJobTrigger(client, nodeId) {
91
+ let box = null;
92
+ try {
93
+ box = await getNodeBox(client, nodeId);
94
+ } catch {}
95
+ if (!isVisibleBox(box)) return null;
96
+
97
+ let label = "";
98
+ let className = "";
99
+ try {
100
+ const outerHTML = await getOuterHTML(client, nodeId);
101
+ label = normalizeText(htmlToText(outerHTML));
102
+ } catch {}
103
+ try {
104
+ const attributes = await getAttributesMap(client, nodeId);
105
+ className = attributes.class || "";
106
+ } catch {}
107
+
108
+ return {
109
+ node_id: nodeId,
110
+ center: box.center,
111
+ rect: box.rect,
112
+ label,
113
+ label_without_salary: trimSalarySuffix(label),
114
+ class_name: className,
115
+ visible: true
116
+ };
117
+ }
118
+
78
119
  export async function findRecommendJobTrigger(client, frameNodeId) {
79
120
  const nodeIds = await querySelectorAll(client, frameNodeId, RECOMMEND_JOB_SELECTORS.trigger);
80
121
  for (const nodeId of nodeIds) {
81
- try {
82
- const box = await getNodeBox(client, nodeId);
83
- if (isVisibleBox(box)) {
84
- return {
85
- node_id: nodeId,
86
- center: box.center,
87
- rect: box.rect
88
- };
89
- }
90
- } catch {}
122
+ const trigger = await readJobTrigger(client, nodeId);
123
+ if (trigger) return trigger;
91
124
  }
92
125
  return null;
93
126
  }
@@ -162,6 +195,7 @@ export async function openRecommendJobDropdown(client, frameNodeId, {
162
195
  }
163
196
  }
164
197
  const error = new Error("Recommend job dropdown did not expose visible options after trigger click");
198
+ error.trigger = trigger;
165
199
  error.job_dropdown_attempts = attempts;
166
200
  throw error;
167
201
  }
@@ -204,6 +238,7 @@ export async function listRecommendJobOptions(client, frameNodeId, {
204
238
  if (seen.has(nodeId)) continue;
205
239
  seen.add(nodeId);
206
240
  const option = await readJobOption(client, nodeId, index);
241
+ if (!option) continue;
207
242
  if (!option.label) continue;
208
243
  if (option.label.length > 120) continue;
209
244
  options.push(option);
@@ -245,10 +280,36 @@ export async function selectRecommendJob(client, frameNodeId, {
245
280
  };
246
281
  }
247
282
 
248
- const opened = await openRecommendJobDropdown(client, frameNodeId, {
249
- timeoutMs: dropdownTimeoutMs,
250
- triggerTimeoutMs: dropdownTimeoutMs
251
- });
283
+ let opened = null;
284
+ try {
285
+ opened = await openRecommendJobDropdown(client, frameNodeId, {
286
+ timeoutMs: dropdownTimeoutMs,
287
+ triggerTimeoutMs: dropdownTimeoutMs
288
+ });
289
+ } catch (error) {
290
+ const currentOptions = await listRecommendJobOptions(client, frameNodeId, {
291
+ openDropdown: false
292
+ }).catch(() => []);
293
+ const currentMatch = currentOptions.find((option) => (
294
+ option.current && jobLabelMatches(option.label, target)
295
+ ));
296
+ if (currentMatch) {
297
+ await closeRecommendJobDropdown(client);
298
+ return {
299
+ requested: target,
300
+ selected: true,
301
+ already_current: true,
302
+ selected_option: compactJobOption({
303
+ ...currentMatch,
304
+ source: "current_option_without_visible_dropdown"
305
+ }),
306
+ options: currentOptions.map(compactJobOption),
307
+ dropdown_error: error?.message || String(error),
308
+ job_dropdown_attempts: error?.job_dropdown_attempts || []
309
+ };
310
+ }
311
+ throw error;
312
+ }
252
313
  const options = opened.options.length
253
314
  ? opened.options
254
315
  : await listRecommendJobOptions(client, frameNodeId, { openDropdown: false });
@@ -311,6 +372,7 @@ function compactJobOption(option) {
311
372
  class_name: option.class_name,
312
373
  node_id: option.node_id,
313
374
  center: option.center,
314
- rect: option.rect
375
+ rect: option.rect,
376
+ source: option.source || null
315
377
  };
316
378
  }
@@ -14,6 +14,7 @@ import {
14
14
  getRecommendRoots,
15
15
  waitForRecommendRoots
16
16
  } from "./roots.js";
17
+ import { isStaleRecommendNodeError } from "./detail.js";
17
18
 
18
19
  function normalizeLabels(labels = []) {
19
20
  return labels.map((label) => String(label || "").trim()).filter(Boolean);
@@ -102,6 +103,7 @@ function compactFilterReapplyError(error) {
102
103
  }
103
104
 
104
105
  export function isRetryableRecommendJobSelectionError(error) {
106
+ if (isStaleRecommendNodeError(error)) return true;
105
107
  const message = String(error?.message || error || "");
106
108
  return /Recommend job trigger was not found|Recommend job dropdown did not mount options|Recommend job dropdown did not expose visible options|Matched recommend job has no clickable center|Matched recommend job has no visible clickable option/i.test(message);
107
109
  }
@@ -125,6 +127,17 @@ function compactJobSelectionAttempt({
125
127
  };
126
128
  }
127
129
 
130
+ async function waitForFreshRecommendRoots(client, {
131
+ timeoutMs = 10000,
132
+ intervalMs = 500
133
+ } = {}) {
134
+ const rootState = await waitForRecommendRoots(client, {
135
+ timeoutMs,
136
+ intervalMs
137
+ });
138
+ return rootState?.iframe?.documentNodeId ? rootState : null;
139
+ }
140
+
128
141
  export async function selectRecommendJobWithRootRefresh(client, rootState, {
129
142
  jobLabel = "",
130
143
  settleMs = 6000,
@@ -141,7 +154,10 @@ export async function selectRecommendJobWithRootRefresh(client, rootState, {
141
154
  while (Date.now() - started <= totalTimeoutMs) {
142
155
  attempt += 1;
143
156
  if (!currentRootState?.iframe?.documentNodeId) {
144
- currentRootState = await getRecommendRoots(client);
157
+ currentRootState = await waitForFreshRecommendRoots(client, {
158
+ timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
159
+ intervalMs: 500
160
+ });
145
161
  }
146
162
  const iframeDocumentNodeId = currentRootState?.iframe?.documentNodeId || 0;
147
163
  try {
@@ -176,7 +192,10 @@ export async function selectRecommendJobWithRootRefresh(client, rootState, {
176
192
  break;
177
193
  }
178
194
  if (retryDelayMs > 0) await sleep(retryDelayMs);
179
- currentRootState = await getRecommendRoots(client);
195
+ currentRootState = await waitForFreshRecommendRoots(client, {
196
+ timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
197
+ intervalMs: 500
198
+ });
180
199
  }
181
200
  }
182
201
 
@@ -1256,11 +1256,13 @@ export async function runRecommendWorkflow({
1256
1256
  : useLlmScreening
1257
1257
  ? llmResultToScreening(llmResult, screeningCandidate)
1258
1258
  : screenCandidate(screeningCandidate, { criteria });
1259
- let actionDiscovery = null;
1260
- let postActionResult = null;
1261
- if (postActionEnabled && detailResult) {
1262
- const postActionStarted = Date.now();
1263
- await runControl.waitIfPaused();
1259
+ let actionDiscovery = null;
1260
+ let postActionResult = null;
1261
+ let closeFailureError = null;
1262
+ let closeRecoveryFailure = null;
1263
+ if (postActionEnabled && detailResult) {
1264
+ const postActionStarted = Date.now();
1265
+ await runControl.waitIfPaused();
1264
1266
  runControl.throwIfCanceled();
1265
1267
  runControl.setPhase("recommend:post-action");
1266
1268
  await maybeHumanActionCooldown("before_post_action", timings);
@@ -1288,21 +1290,34 @@ export async function runRecommendWorkflow({
1288
1290
  detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
1289
1291
  await maybeHumanActionCooldown("after_detail_close", timings);
1290
1292
  if (!detailResult.close_result?.closed) {
1291
- const closeError = createRecommendCloseFailureError(detailResult.close_result);
1292
- const recovery = await recoverAndReapplyRecommendContext("detail_close_failed", closeError, {
1293
- forceRecentNotView: true
1294
- });
1295
- detailResult.cv_acquisition = {
1296
- ...(detailResult.cv_acquisition || {}),
1297
- close_recovery: {
1298
- ok: Boolean(recovery.ok),
1299
- method: recovery.method || "",
1300
- forced_recent_not_view: Boolean(recovery.forced_recent_not_view),
1301
- card_count: recovery.card_count || 0
1302
- }
1303
- };
1304
- }
1305
- }
1293
+ closeFailureError = createRecommendCloseFailureError(detailResult.close_result);
1294
+ try {
1295
+ const recovery = await recoverAndReapplyRecommendContext("detail_close_failed", closeFailureError, {
1296
+ forceRecentNotView: true
1297
+ });
1298
+ detailResult.cv_acquisition = {
1299
+ ...(detailResult.cv_acquisition || {}),
1300
+ close_recovery: {
1301
+ ok: Boolean(recovery.ok),
1302
+ method: recovery.method || "",
1303
+ forced_recent_not_view: Boolean(recovery.forced_recent_not_view),
1304
+ card_count: recovery.card_count || 0
1305
+ }
1306
+ };
1307
+ } catch (error) {
1308
+ closeRecoveryFailure = error;
1309
+ detailResult.cv_acquisition = {
1310
+ ...(detailResult.cv_acquisition || {}),
1311
+ close_recovery: {
1312
+ ok: false,
1313
+ reason: "context_recovery_failed",
1314
+ error: error?.message || String(error),
1315
+ forced_recent_not_view: true
1316
+ }
1317
+ };
1318
+ }
1319
+ }
1320
+ }
1306
1321
  timings.total_ms = Date.now() - candidateStarted;
1307
1322
  const compactResult = {
1308
1323
  index,
@@ -1313,12 +1328,14 @@ export async function runRecommendWorkflow({
1313
1328
  llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
1314
1329
  screening: compactScreening(screening),
1315
1330
  action_discovery: compactActionDiscovery(actionDiscovery),
1316
- post_action: postActionResult,
1317
- error: recoverableDetailError
1318
- ? compactRecoverableDetailError(recoverableDetailError)
1319
- : detailResult?.image_evidence?.ok === false
1320
- ? compactError({
1321
- code: detailResult.image_evidence.error_code,
1331
+ post_action: postActionResult,
1332
+ error: recoverableDetailError
1333
+ ? compactRecoverableDetailError(recoverableDetailError)
1334
+ : closeRecoveryFailure
1335
+ ? compactError(closeFailureError, "DETAIL_CLOSE_FAILED")
1336
+ : detailResult?.image_evidence?.ok === false
1337
+ ? compactError({
1338
+ code: detailResult.image_evidence.error_code,
1322
1339
  message: detailResult.image_evidence.error
1323
1340
  }, "IMAGE_CAPTURE_FAILED")
1324
1341
  : null,
@@ -1353,9 +1370,13 @@ export async function runRecommendWorkflow({
1353
1370
  error: compactResult.error,
1354
1371
  post_action: postActionResult
1355
1372
  }
1356
- });
1357
- addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
1358
-
1373
+ });
1374
+ addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
1375
+
1376
+ if (closeRecoveryFailure) {
1377
+ throw closeRecoveryFailure;
1378
+ }
1379
+
1359
1380
  if (postActionResult?.stop_run) {
1360
1381
  listEndReason = postActionResult.reason || "post_action_stop";
1361
1382
  break;