@reconcrap/boss-recommend-mcp 2.0.39 → 2.0.41

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.39",
3
+ "version": "2.0.41",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -92,10 +92,28 @@ export async function findRecommendJobTrigger(client, frameNodeId) {
92
92
  return null;
93
93
  }
94
94
 
95
+ export async function waitForRecommendJobTrigger(client, frameNodeId, {
96
+ timeoutMs = 8000,
97
+ intervalMs = 250
98
+ } = {}) {
99
+ const started = Date.now();
100
+ while (Date.now() - started <= timeoutMs) {
101
+ const trigger = await findRecommendJobTrigger(client, frameNodeId);
102
+ if (trigger) return trigger;
103
+ await sleep(intervalMs);
104
+ }
105
+ return null;
106
+ }
107
+
95
108
  export async function openRecommendJobDropdown(client, frameNodeId, {
96
- timeoutMs = 4000
109
+ timeoutMs = 4000,
110
+ triggerTimeoutMs = Math.max(8000, timeoutMs),
111
+ triggerIntervalMs = 250
97
112
  } = {}) {
98
- const trigger = await findRecommendJobTrigger(client, frameNodeId);
113
+ const trigger = await waitForRecommendJobTrigger(client, frameNodeId, {
114
+ timeoutMs: triggerTimeoutMs,
115
+ intervalMs: triggerIntervalMs
116
+ });
99
117
  if (!trigger) {
100
118
  throw new Error("Recommend job trigger was not found");
101
119
  }
@@ -166,7 +184,8 @@ export async function closeRecommendJobDropdown(client) {
166
184
 
167
185
  export async function selectRecommendJob(client, frameNodeId, {
168
186
  jobLabel = "",
169
- settleMs = 6000
187
+ settleMs = 6000,
188
+ dropdownTimeoutMs = Math.max(8000, settleMs)
170
189
  } = {}) {
171
190
  const target = normalizeText(jobLabel);
172
191
  if (!target) {
@@ -178,7 +197,10 @@ export async function selectRecommendJob(client, frameNodeId, {
178
197
  };
179
198
  }
180
199
 
181
- const opened = await openRecommendJobDropdown(client, frameNodeId);
200
+ const opened = await openRecommendJobDropdown(client, frameNodeId, {
201
+ timeoutMs: dropdownTimeoutMs,
202
+ triggerTimeoutMs: dropdownTimeoutMs
203
+ });
182
204
  const options = opened.options.length
183
205
  ? opened.options
184
206
  : await listRecommendJobOptions(client, frameNodeId, { openDropdown: false });
@@ -101,6 +101,91 @@ function compactFilterReapplyError(error) {
101
101
  return error?.message || String(error || "Recommend filter reapply failed");
102
102
  }
103
103
 
104
+ export function isRetryableRecommendJobSelectionError(error) {
105
+ const message = String(error?.message || error || "");
106
+ return /Recommend job trigger was not found|Recommend job dropdown did not mount options/i.test(message);
107
+ }
108
+
109
+ function compactJobSelectionAttempt({
110
+ ok = false,
111
+ attempt = 0,
112
+ iframeDocumentNodeId = 0,
113
+ error = null,
114
+ selection = null
115
+ } = {}) {
116
+ return {
117
+ ok: Boolean(ok),
118
+ method: "job_select",
119
+ reason: error ? "job_select_failed" : null,
120
+ error: error ? (error?.message || String(error)) : null,
121
+ attempt,
122
+ iframe_document_node_id: iframeDocumentNodeId || 0,
123
+ selected: Boolean(selection?.selected),
124
+ selection_reason: selection?.reason || null
125
+ };
126
+ }
127
+
128
+ export async function selectRecommendJobWithRootRefresh(client, rootState, {
129
+ jobLabel = "",
130
+ settleMs = 6000,
131
+ dropdownTimeoutMs = 4000,
132
+ totalTimeoutMs = 30000,
133
+ retryDelayMs = 1000
134
+ } = {}) {
135
+ const started = Date.now();
136
+ const attempts = [];
137
+ let currentRootState = rootState || null;
138
+ let lastError = null;
139
+ let attempt = 0;
140
+
141
+ while (Date.now() - started <= totalTimeoutMs) {
142
+ attempt += 1;
143
+ if (!currentRootState?.iframe?.documentNodeId) {
144
+ currentRootState = await getRecommendRoots(client);
145
+ }
146
+ const iframeDocumentNodeId = currentRootState?.iframe?.documentNodeId || 0;
147
+ try {
148
+ const selection = await selectRecommendJob(client, iframeDocumentNodeId, {
149
+ jobLabel,
150
+ settleMs,
151
+ dropdownTimeoutMs
152
+ });
153
+ attempts.push(compactJobSelectionAttempt({
154
+ ok: true,
155
+ attempt,
156
+ iframeDocumentNodeId,
157
+ selection
158
+ }));
159
+ return {
160
+ job_selection: {
161
+ ...selection,
162
+ refresh_attempts: attempts
163
+ },
164
+ root_state: currentRootState,
165
+ attempts
166
+ };
167
+ } catch (error) {
168
+ lastError = error;
169
+ attempts.push(compactJobSelectionAttempt({
170
+ ok: false,
171
+ attempt,
172
+ iframeDocumentNodeId,
173
+ error
174
+ }));
175
+ if (!isRetryableRecommendJobSelectionError(error) || Date.now() - started >= totalTimeoutMs) {
176
+ break;
177
+ }
178
+ if (retryDelayMs > 0) await sleep(retryDelayMs);
179
+ currentRootState = await getRecommendRoots(client);
180
+ }
181
+ }
182
+
183
+ const wrapped = new Error(lastError?.message || "Recommend job selection failed after refresh reload");
184
+ wrapped.cause = lastError;
185
+ wrapped.job_selection_attempts = attempts;
186
+ throw wrapped;
187
+ }
188
+
104
189
  async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
105
190
  maxAttempts = 3,
106
191
  retryDelayMs = 1500
@@ -162,6 +247,7 @@ async function applyRefreshMethod(client, method, {
162
247
  const started = Date.now();
163
248
  let currentRootState = null;
164
249
  let jobSelection = null;
250
+ let jobSelectionAttempts = [];
165
251
  let pageScopeResult = null;
166
252
  let filterResult = null;
167
253
  let filterReapplyAttempts = [];
@@ -180,14 +266,19 @@ async function applyRefreshMethod(client, method, {
180
266
  throw new Error("Recommend iframe was not ready after refresh reload");
181
267
  }
182
268
  if (jobLabel) {
183
- jobSelection = await selectRecommendJob(client, currentRootState.iframe.documentNodeId, {
269
+ const jobSelectionResult = await selectRecommendJobWithRootRefresh(client, currentRootState, {
184
270
  jobLabel,
185
- settleMs: reloadSettleMs > 10000 ? 12000 : 6000
271
+ settleMs: reloadSettleMs > 10000 ? 12000 : 6000,
272
+ dropdownTimeoutMs: 4000,
273
+ totalTimeoutMs: reloadSettleMs > 10000 ? 45000 : 30000,
274
+ retryDelayMs: 1200
186
275
  });
276
+ jobSelection = jobSelectionResult.job_selection;
277
+ jobSelectionAttempts = jobSelectionResult.attempts;
187
278
  if (!jobSelection.selected) {
188
279
  throw new Error(`Requested recommend job was not selected after refresh reload: ${jobSelection.reason}`);
189
280
  }
190
- currentRootState = await getRecommendRoots(client);
281
+ currentRootState = jobSelectionResult.root_state || await getRecommendRoots(client);
191
282
  }
192
283
  pageScopeResult = await selectRecommendPageScope(
193
284
  client,
@@ -226,6 +317,7 @@ async function applyRefreshMethod(client, method, {
226
317
  method,
227
318
  target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
228
319
  job_selection: jobSelection,
320
+ job_selection_attempts: jobSelectionAttempts,
229
321
  page_scope: pageScopeResult,
230
322
  filter: filterResult,
231
323
  filter_reapply_attempts: filterReapplyAttempts,
@@ -242,6 +334,7 @@ async function applyRefreshMethod(client, method, {
242
334
  error: error?.message || String(error),
243
335
  target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
244
336
  job_selection: jobSelection,
337
+ job_selection_attempts: error?.job_selection_attempts || jobSelectionAttempts,
245
338
  page_scope: pageScopeResult,
246
339
  filter: filterResult,
247
340
  filter_reapply_attempts: error?.filter_reapply_attempts || filterReapplyAttempts,
@@ -379,6 +379,16 @@ function compactRefreshAttempt(refreshAttempt) {
379
379
  error: attempt.error || null,
380
380
  attempt: attempt.attempt || 0
381
381
  })),
382
+ job_selection_attempts: (refreshAttempt.job_selection_attempts || []).map((attempt) => ({
383
+ ok: Boolean(attempt.ok),
384
+ method: attempt.method || "job_select",
385
+ reason: attempt.reason || null,
386
+ error: attempt.error || null,
387
+ attempt: attempt.attempt || 0,
388
+ iframe_document_node_id: attempt.iframe_document_node_id || 0,
389
+ selected: Boolean(attempt.selected),
390
+ selection_reason: attempt.selection_reason || null
391
+ })),
382
392
  job_selection: compactJobSelection(refreshAttempt.job_selection),
383
393
  page_scope: compactPageScopeSelection(refreshAttempt.page_scope),
384
394
  filter: compactFilterResult(refreshAttempt.filter)