@reconcrap/boss-recommend-mcp 2.0.10 → 2.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "2.0.10",
3
+ "version": "2.0.11",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -146,6 +146,14 @@ function filePathForSequence(basePath, index, extension) {
146
146
  return path.join(parsed.dir, `${parsed.name}-page-${page}${parsed.ext || `.${extension}`}`);
147
147
  }
148
148
 
149
+ function filePathForLlmSequence(basePath, index) {
150
+ const resolved = resolveOutputPath(basePath);
151
+ if (!resolved) return null;
152
+ const parsed = path.parse(resolved);
153
+ const page = String(index + 1).padStart(2, "0");
154
+ return path.join(parsed.dir, `${parsed.name}-llm-${page}.jpg`);
155
+ }
156
+
149
157
  function screenshotHash(buffer) {
150
158
  return crypto.createHash("sha256").update(buffer).digest("hex");
151
159
  }
@@ -207,6 +215,111 @@ async function optimizeScreenshotBuffer(buffer, {
207
215
  }
208
216
  }
209
217
 
218
+ async function composeScreenshotsForLlm(screenshots = [], {
219
+ basePath,
220
+ pagesPerImage = 3,
221
+ resizeMaxWidth = 1100,
222
+ quality = 72
223
+ } = {}) {
224
+ const fileScreenshots = screenshots.filter((item) => item?.file_path);
225
+ if (!basePath || fileScreenshots.length <= 1) {
226
+ return {
227
+ llm_file_paths: fileScreenshots.map((item) => item.file_path),
228
+ llm_screenshots: [],
229
+ llm_total_byte_length: 0,
230
+ llm_original_total_byte_length: 0,
231
+ llm_composition_error: null
232
+ };
233
+ }
234
+
235
+ const safePagesPerImage = Math.max(1, Math.min(5, Number(pagesPerImage) || 3));
236
+ const safeWidth = Math.max(700, Math.min(1400, Number(resizeMaxWidth) || 1100));
237
+ const safeQuality = Math.max(45, Math.min(90, Number(quality) || 72));
238
+ const llmScreenshots = [];
239
+
240
+ try {
241
+ for (let index = 0; index < fileScreenshots.length; index += safePagesPerImage) {
242
+ const group = fileScreenshots.slice(index, index + safePagesPerImage);
243
+ const prepared = [];
244
+ for (const item of group) {
245
+ const sourceBuffer = fs.readFileSync(item.file_path);
246
+ const { data, info } = await sharp(sourceBuffer, { failOn: "none" })
247
+ .resize({
248
+ width: safeWidth,
249
+ withoutEnlargement: true
250
+ })
251
+ .jpeg({
252
+ quality: safeQuality,
253
+ mozjpeg: true
254
+ })
255
+ .toBuffer({ resolveWithObject: true });
256
+ prepared.push({
257
+ input: data,
258
+ width: info.width,
259
+ height: info.height,
260
+ source_file_path: item.file_path
261
+ });
262
+ }
263
+
264
+ const width = Math.max(...prepared.map((item) => item.width), 1);
265
+ const height = prepared.reduce((sum, item) => sum + item.height, 0);
266
+ let top = 0;
267
+ const composites = prepared.map((item) => {
268
+ const layer = {
269
+ input: item.input,
270
+ left: 0,
271
+ top
272
+ };
273
+ top += item.height;
274
+ return layer;
275
+ });
276
+ const outputBuffer = await sharp({
277
+ create: {
278
+ width,
279
+ height,
280
+ channels: 3,
281
+ background: "#ffffff"
282
+ }
283
+ })
284
+ .composite(composites)
285
+ .jpeg({
286
+ quality: safeQuality,
287
+ mozjpeg: true
288
+ })
289
+ .toBuffer();
290
+ const outputPath = filePathForLlmSequence(basePath, llmScreenshots.length);
291
+ fs.writeFileSync(outputPath, outputBuffer);
292
+ llmScreenshots.push({
293
+ index: llmScreenshots.length,
294
+ file_path: outputPath,
295
+ byte_length: outputBuffer.length,
296
+ source_file_paths: prepared.map((item) => item.source_file_path),
297
+ source_page_count: prepared.length,
298
+ width,
299
+ height,
300
+ format: "jpeg",
301
+ mime_type: "image/jpeg"
302
+ });
303
+ }
304
+ } catch (error) {
305
+ return {
306
+ llm_file_paths: fileScreenshots.map((item) => item.file_path),
307
+ llm_screenshots: [],
308
+ llm_total_byte_length: 0,
309
+ llm_original_total_byte_length: fileScreenshots.reduce((sum, item) => sum + (Number(item.byte_length) || 0), 0),
310
+ llm_composition_error: error?.message || String(error)
311
+ };
312
+ }
313
+
314
+ return {
315
+ llm_file_paths: llmScreenshots.map((item) => item.file_path),
316
+ llm_screenshots: llmScreenshots,
317
+ llm_total_byte_length: llmScreenshots.reduce((sum, item) => sum + (Number(item.byte_length) || 0), 0),
318
+ llm_original_total_byte_length: fileScreenshots.reduce((sum, item) => sum + (Number(item.byte_length) || 0), 0),
319
+ llm_composition_error: null
320
+ };
321
+ }
322
+
210
323
  export async function captureScrolledNodeScreenshots(client, nodeId, {
211
324
  filePath,
212
325
  format = "png",
@@ -222,6 +335,10 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
222
335
  skipDuplicateScreenshots = false,
223
336
  optimize = false,
224
337
  resizeMaxWidth = 0,
338
+ composeForLlm = false,
339
+ llmPagesPerImage = 3,
340
+ llmResizeMaxWidth = 1100,
341
+ llmQuality = 72,
225
342
  metadata = {}
226
343
  } = {}) {
227
344
  if (!nodeId) throw new Error("captureScrolledNodeScreenshots requires nodeId");
@@ -322,6 +439,21 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
322
439
  }
323
440
  }
324
441
 
442
+ const llmComposition = composeForLlm
443
+ ? await composeScreenshotsForLlm(screenshots, {
444
+ basePath: filePath,
445
+ pagesPerImage: llmPagesPerImage,
446
+ resizeMaxWidth: llmResizeMaxWidth,
447
+ quality: llmQuality
448
+ })
449
+ : {
450
+ llm_file_paths: screenshots.map((item) => item.file_path).filter(Boolean),
451
+ llm_screenshots: [],
452
+ llm_total_byte_length: 0,
453
+ llm_original_total_byte_length: 0,
454
+ llm_composition_error: null
455
+ };
456
+
325
457
  return {
326
458
  schema_version: 1,
327
459
  source: "image-scroll-sequence",
@@ -335,12 +467,22 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
335
467
  dropped_duplicate_count: droppedDuplicateCount,
336
468
  total_byte_length: screenshots.reduce((sum, item) => sum + (Number(item.byte_length) || 0), 0),
337
469
  original_total_byte_length: screenshots.reduce((sum, item) => sum + (Number(item.original_byte_length) || 0), 0),
470
+ llm_file_paths: llmComposition.llm_file_paths,
471
+ llm_screenshot_count: llmComposition.llm_file_paths.length,
472
+ llm_total_byte_length: llmComposition.llm_total_byte_length,
473
+ llm_original_total_byte_length: llmComposition.llm_original_total_byte_length,
474
+ llm_composition_error: llmComposition.llm_composition_error,
475
+ llm_screenshots: llmComposition.llm_screenshots,
338
476
  optimization: {
339
477
  enabled: Boolean(optimize),
340
478
  resize_max_width: Math.max(0, Number(resizeMaxWidth) || 0),
341
479
  capture_viewport: Boolean(captureViewport),
342
480
  format,
343
- quality: quality ?? null
481
+ quality: quality ?? null,
482
+ llm_compose_enabled: Boolean(composeForLlm),
483
+ llm_pages_per_image: Math.max(1, Math.min(5, Number(llmPagesPerImage) || 3)),
484
+ llm_resize_max_width: Math.max(0, Number(llmResizeMaxWidth) || 0),
485
+ llm_quality: llmQuality ?? null
344
486
  },
345
487
  file_paths: screenshots.map((item) => item.file_path).filter(Boolean),
346
488
  screenshots,
@@ -132,8 +132,13 @@ export function summarizeImageEvidence(imageEvidence = null) {
132
132
  dropped_duplicate_count: imageEvidence.dropped_duplicate_count || 0,
133
133
  total_byte_length: imageEvidence.total_byte_length || 0,
134
134
  original_total_byte_length: imageEvidence.original_total_byte_length || 0,
135
+ llm_screenshot_count: imageEvidence.llm_screenshot_count || 0,
136
+ llm_total_byte_length: imageEvidence.llm_total_byte_length || 0,
137
+ llm_original_total_byte_length: imageEvidence.llm_original_total_byte_length || 0,
138
+ llm_composition_error: imageEvidence.llm_composition_error || null,
135
139
  optimization: imageEvidence.optimization || null,
136
140
  file_paths: imageEvidence.file_paths || [],
141
+ llm_file_paths: imageEvidence.llm_file_paths || [],
137
142
  first_clip: imageEvidence.screenshots?.[0]?.clip || imageEvidence.clip || null
138
143
  };
139
144
  }
@@ -392,11 +392,18 @@ function normalizeImagePaths({ imageEvidence = null, imagePaths = [] } = {}) {
392
392
  if (Array.isArray(imagePaths)) {
393
393
  paths.push(...imagePaths);
394
394
  }
395
- if (Array.isArray(imageEvidence?.file_paths)) {
396
- paths.push(...imageEvidence.file_paths);
397
- }
398
- if (Array.isArray(imageEvidence?.screenshots)) {
399
- paths.push(...imageEvidence.screenshots.map((item) => item.file_path));
395
+ const evidenceLlmPaths = Array.isArray(imageEvidence?.llm_file_paths)
396
+ ? imageEvidence.llm_file_paths
397
+ : [];
398
+ if (evidenceLlmPaths.length) {
399
+ paths.push(...evidenceLlmPaths);
400
+ } else {
401
+ if (Array.isArray(imageEvidence?.file_paths)) {
402
+ paths.push(...imageEvidence.file_paths);
403
+ }
404
+ if (Array.isArray(imageEvidence?.screenshots)) {
405
+ paths.push(...imageEvidence.screenshots.map((item) => item.file_path));
406
+ }
400
407
  }
401
408
  return unique(paths.map((filePath) => String(filePath || "").trim()).filter(Boolean));
402
409
  }
@@ -1292,6 +1299,7 @@ export function compactScreeningLlmResult(llmResult) {
1292
1299
  usage: llmResult.usage || null,
1293
1300
  finish_reason: llmResult.finish_reason || null,
1294
1301
  image_input_count: llmResult.image_input_count || 0,
1302
+ attempt_count: llmResult.attempt_count || 0,
1295
1303
  error: llmResult.error || null,
1296
1304
  screened_at: llmResult.screened_at || null
1297
1305
  };
@@ -1324,6 +1332,7 @@ export function createFailedLlmScreeningResult(error) {
1324
1332
  raw_model_output: "",
1325
1333
  image_input_count: Number(error?.image_input_count) || 0,
1326
1334
  image_inputs: Array.isArray(error?.image_inputs) ? error.image_inputs : [],
1335
+ attempt_count: Number(error?.llm_attempt_count) || 0,
1327
1336
  error: error?.message || String(error || "unknown"),
1328
1337
  screened_at: nowIso()
1329
1338
  };
@@ -1352,7 +1361,7 @@ export function buildScreeningLlmMessages({
1352
1361
  `请根据以下标准判断候选人是否通过筛选。\n\n筛选标准:\n${safeCriteria}\n\n`
1353
1362
  + `候选人信息:\n${safeText || "候选人的完整简历信息在后续截图中,请按截图顺序阅读。"}\n\n`
1354
1363
  + (images.length
1355
- ? `候选人简历截图共 ${images.length} 张,按从上到下的滚动顺序排列。请完整阅读所有截图后再判断。\n\n`
1364
+ ? `候选人简历截图共 ${images.length} 张,按从上到下的滚动顺序排列。若截图是拼接长图,请按图内从上到下顺序完整阅读;不要跳过任何一段简历内容。\n\n`
1356
1365
  : "")
1357
1366
  + "要求:\n"
1358
1367
  + "1) 只能依据候选人信息或截图中真实出现的内容判断。\n"
@@ -1383,6 +1392,24 @@ export function buildScreeningLlmMessages({
1383
1392
  ];
1384
1393
  }
1385
1394
 
1395
+ function normalizeLlmMaxRetries(value) {
1396
+ if (value == null || value === "") return 1;
1397
+ const parsed = Number(value);
1398
+ if (!Number.isFinite(parsed) || parsed < 0) return 1;
1399
+ return Math.min(3, Math.floor(parsed));
1400
+ }
1401
+
1402
+ function isRetryableLlmRequestError(error) {
1403
+ const status = Number(error?.status);
1404
+ if ([408, 409, 425, 429].includes(status) || status >= 500) return true;
1405
+ return /(?:aborted|abort|timeout|timed out|fetch failed|socket|network|ECONNRESET|ETIMEDOUT|EAI_AGAIN)/i
1406
+ .test(String(error?.message || error || ""));
1407
+ }
1408
+
1409
+ function sleepMs(ms) {
1410
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
1411
+ }
1412
+
1386
1413
  export async function callScreeningLlm({
1387
1414
  candidate,
1388
1415
  criteria,
@@ -1425,9 +1452,13 @@ export async function callScreeningLlm({
1425
1452
  thinkingLevel: config.llmThinkingLevel || config.thinkingLevel || config.reasoningEffort || "low"
1426
1453
  });
1427
1454
 
1428
- const controller = new AbortController();
1429
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1430
- try {
1455
+ const maxRetries = normalizeLlmMaxRetries(config.llmMaxRetries ?? config.maxRetries);
1456
+ const maxAttempts = maxRetries + 1;
1457
+ let lastError = null;
1458
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1459
+ const controller = new AbortController();
1460
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1461
+ try {
1431
1462
  const headers = {
1432
1463
  "Content-Type": "application/json",
1433
1464
  Authorization: `Bearer ${apiKey}`
@@ -1443,7 +1474,9 @@ export async function callScreeningLlm({
1443
1474
  });
1444
1475
  const responseText = await response.text();
1445
1476
  if (!response.ok) {
1446
- throw new Error(`LLM request failed: ${response.status} ${responseText.slice(0, 400)}`);
1477
+ const error = new Error(`LLM request failed: ${response.status} ${responseText.slice(0, 400)}`);
1478
+ error.status = response.status;
1479
+ throw error;
1447
1480
  }
1448
1481
  const json = tryParseJson(responseText);
1449
1482
  if (!json) {
@@ -1485,13 +1518,25 @@ export async function callScreeningLlm({
1485
1518
  raw_content_length: content.length,
1486
1519
  image_input_count: imageInputs.length,
1487
1520
  image_inputs: summarizeLlmImageInputs(imageInputs),
1521
+ attempt_count: attempt,
1488
1522
  screened_at: nowIso()
1489
1523
  };
1490
1524
  } catch (error) {
1491
- error.image_input_count = imageInputs.length;
1492
- error.image_inputs = summarizeLlmImageInputs(imageInputs);
1493
- throw error;
1525
+ lastError = error;
1526
+ if (attempt >= maxAttempts || !isRetryableLlmRequestError(error)) {
1527
+ error.image_input_count = imageInputs.length;
1528
+ error.image_inputs = summarizeLlmImageInputs(imageInputs);
1529
+ error.llm_attempt_count = attempt;
1530
+ throw error;
1531
+ }
1532
+ await sleepMs(Math.min(2500, 500 * attempt));
1494
1533
  } finally {
1495
1534
  clearTimeout(timer);
1496
1535
  }
1536
+ }
1537
+ lastError = lastError || new Error("LLM request failed without response");
1538
+ lastError.image_input_count = imageInputs.length;
1539
+ lastError.image_inputs = summarizeLlmImageInputs(imageInputs);
1540
+ lastError.llm_attempt_count = maxAttempts;
1541
+ throw lastError;
1497
1542
  }
@@ -98,6 +98,7 @@ function compactLlmResult(llmResult) {
98
98
  usage: llmResult.usage || null,
99
99
  finish_reason: llmResult.finish_reason || null,
100
100
  image_input_count: llmResult.image_input_count || 0,
101
+ attempt_count: llmResult.attempt_count || 0,
101
102
  error: llmResult.error || null
102
103
  };
103
104
  }
@@ -826,11 +827,15 @@ export async function runChatWorkflow({
826
827
  maxScreenshots: maxImagePages,
827
828
  wheelDeltaY: imageWheelDeltaY,
828
829
  settleMs: 350,
829
- duplicateStopCount: 1,
830
- skipDuplicateScreenshots: true,
831
- metadata: {
832
- domain: "chat",
833
- capture_mode: "scroll_sequence",
830
+ duplicateStopCount: 1,
831
+ skipDuplicateScreenshots: true,
832
+ composeForLlm: true,
833
+ llmPagesPerImage: 3,
834
+ llmResizeMaxWidth: 1100,
835
+ llmQuality: 72,
836
+ metadata: {
837
+ domain: "chat",
838
+ capture_mode: "scroll_sequence",
834
839
  acquisition_reason: normalizedDetailSource === "image"
835
840
  ? "forced_image"
836
841
  : "network_miss_image_fallback",
@@ -110,95 +110,117 @@ export async function refreshRecommendListAtEnd(client, {
110
110
  );
111
111
  attempts.push(buttonResult);
112
112
  if (buttonResult.ok) {
113
- currentRootState = await getRecommendRoots(client);
114
- const pageScopeResult = await selectRecommendPageScope(
115
- client,
116
- currentRootState.iframe.documentNodeId,
117
- {
118
- pageScope,
119
- fallbackScope: fallbackPageScope,
120
- settleMs: buttonSettleMs > 10000 ? 3000 : 1200,
121
- timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
113
+ try {
114
+ currentRootState = await getRecommendRoots(client);
115
+ const pageScopeResult = await selectRecommendPageScope(
116
+ client,
117
+ currentRootState.iframe.documentNodeId,
118
+ {
119
+ pageScope,
120
+ fallbackScope: fallbackPageScope,
121
+ settleMs: buttonSettleMs > 10000 ? 3000 : 1200,
122
+ timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
123
+ }
124
+ );
125
+ if (!pageScopeResult.selected) {
126
+ throw new Error(`Recommend page scope was not selected after end refresh: ${pageScopeResult.reason || pageScope}`);
122
127
  }
123
- );
124
- if (!pageScopeResult.selected) {
125
- throw new Error(`Recommend page scope was not selected after end refresh: ${pageScopeResult.reason || pageScope}`);
128
+ currentRootState = await getRecommendRoots(client);
129
+ const filterResult = await selectAndConfirmFirstSafeFilter(
130
+ client,
131
+ currentRootState.iframe.documentNodeId,
132
+ buildRecommendFilterSelectionOptions(filter, { forceRecentNotView })
133
+ );
134
+ const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
135
+ timeoutMs: cardTimeoutMs,
136
+ intervalMs: 500
137
+ });
138
+ return {
139
+ ok: cardNodeIds.length > 0,
140
+ method: "end_refresh_button",
141
+ attempts,
142
+ page_scope: pageScopeResult,
143
+ filter: filterResult,
144
+ card_count: cardNodeIds.length,
145
+ root_state: currentRootState,
146
+ forced_recent_not_view: forceRecentNotView
147
+ };
148
+ } catch (error) {
149
+ attempts.push({
150
+ ok: false,
151
+ method: "end_refresh_button_after_click",
152
+ reason: "end_refresh_reapply_failed",
153
+ error: error?.message || String(error)
154
+ });
126
155
  }
127
- currentRootState = await getRecommendRoots(client);
128
- const filterResult = await selectAndConfirmFirstSafeFilter(
129
- client,
130
- currentRootState.iframe.documentNodeId,
131
- buildRecommendFilterSelectionOptions(filter, { forceRecentNotView })
132
- );
133
- const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
134
- timeoutMs: cardTimeoutMs,
135
- intervalMs: 500
136
- });
137
- return {
138
- ok: cardNodeIds.length > 0,
139
- method: "end_refresh_button",
140
- attempts,
141
- page_scope: pageScopeResult,
142
- filter: filterResult,
143
- card_count: cardNodeIds.length,
144
- root_state: currentRootState,
145
- forced_recent_not_view: forceRecentNotView
146
- };
147
156
  }
148
157
  }
149
158
 
150
- await client.Page.reload({ ignoreCache: true });
151
- if (reloadSettleMs > 0) await sleep(reloadSettleMs);
152
- currentRootState = await waitForRecommendRoots(client, {
153
- timeoutMs: Math.max(30000, reloadSettleMs * 4),
154
- intervalMs: 500
155
- });
156
- if (!currentRootState?.iframe?.documentNodeId) {
157
- throw new Error("Recommend iframe was not ready after refresh reload");
158
- }
159
- let jobSelection = null;
160
- if (jobLabel) {
161
- jobSelection = await selectRecommendJob(client, currentRootState.iframe.documentNodeId, {
162
- jobLabel,
163
- settleMs: reloadSettleMs > 10000 ? 12000 : 6000
159
+ try {
160
+ await client.Page.reload({ ignoreCache: true });
161
+ if (reloadSettleMs > 0) await sleep(reloadSettleMs);
162
+ currentRootState = await waitForRecommendRoots(client, {
163
+ timeoutMs: Math.max(30000, reloadSettleMs * 4),
164
+ intervalMs: 500
164
165
  });
165
- if (!jobSelection.selected) {
166
- throw new Error(`Requested recommend job was not selected after refresh reload: ${jobSelection.reason}`);
166
+ if (!currentRootState?.iframe?.documentNodeId) {
167
+ throw new Error("Recommend iframe was not ready after refresh reload");
167
168
  }
168
- currentRootState = await getRecommendRoots(client);
169
- }
170
- const pageScopeResult = await selectRecommendPageScope(
171
- client,
172
- currentRootState.iframe.documentNodeId,
173
- {
174
- pageScope,
175
- fallbackScope: fallbackPageScope,
176
- settleMs: reloadSettleMs > 10000 ? 3000 : 1200,
177
- timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
169
+ let jobSelection = null;
170
+ if (jobLabel) {
171
+ jobSelection = await selectRecommendJob(client, currentRootState.iframe.documentNodeId, {
172
+ jobLabel,
173
+ settleMs: reloadSettleMs > 10000 ? 12000 : 6000
174
+ });
175
+ if (!jobSelection.selected) {
176
+ throw new Error(`Requested recommend job was not selected after refresh reload: ${jobSelection.reason}`);
177
+ }
178
+ currentRootState = await getRecommendRoots(client);
178
179
  }
179
- );
180
- if (!pageScopeResult.selected) {
181
- throw new Error(`Recommend page scope was not selected after refresh reload: ${pageScopeResult.reason || pageScope}`);
180
+ const pageScopeResult = await selectRecommendPageScope(
181
+ client,
182
+ currentRootState.iframe.documentNodeId,
183
+ {
184
+ pageScope,
185
+ fallbackScope: fallbackPageScope,
186
+ settleMs: reloadSettleMs > 10000 ? 3000 : 1200,
187
+ timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
188
+ }
189
+ );
190
+ if (!pageScopeResult.selected) {
191
+ throw new Error(`Recommend page scope was not selected after refresh reload: ${pageScopeResult.reason || pageScope}`);
192
+ }
193
+ currentRootState = await getRecommendRoots(client);
194
+ const filterResult = await selectAndConfirmFirstSafeFilter(
195
+ client,
196
+ currentRootState.iframe.documentNodeId,
197
+ buildRecommendFilterSelectionOptions(filter, { forceRecentNotView })
198
+ );
199
+ const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
200
+ timeoutMs: cardTimeoutMs,
201
+ intervalMs: 500
202
+ });
203
+ return {
204
+ ok: cardNodeIds.length > 0,
205
+ method: "page_reload",
206
+ attempts,
207
+ job_selection: jobSelection,
208
+ page_scope: pageScopeResult,
209
+ filter: filterResult,
210
+ card_count: cardNodeIds.length,
211
+ root_state: currentRootState,
212
+ forced_recent_not_view: forceRecentNotView
213
+ };
214
+ } catch (error) {
215
+ return {
216
+ ok: false,
217
+ method: "page_reload",
218
+ reason: "page_reload_failed",
219
+ error: error?.message || String(error),
220
+ attempts,
221
+ card_count: 0,
222
+ root_state: currentRootState,
223
+ forced_recent_not_view: forceRecentNotView
224
+ };
182
225
  }
183
- currentRootState = await getRecommendRoots(client);
184
- const filterResult = await selectAndConfirmFirstSafeFilter(
185
- client,
186
- currentRootState.iframe.documentNodeId,
187
- buildRecommendFilterSelectionOptions(filter, { forceRecentNotView })
188
- );
189
- const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
190
- timeoutMs: cardTimeoutMs,
191
- intervalMs: 500
192
- });
193
- return {
194
- ok: cardNodeIds.length > 0,
195
- method: "page_reload",
196
- attempts,
197
- job_selection: jobSelection,
198
- page_scope: pageScopeResult,
199
- filter: filterResult,
200
- card_count: cardNodeIds.length,
201
- root_state: currentRootState,
202
- forced_recent_not_view: forceRecentNotView
203
- };
204
226
  }
@@ -715,6 +715,10 @@ export async function runRecommendWorkflow({
715
715
  settleMs: 350,
716
716
  duplicateStopCount: 1,
717
717
  skipDuplicateScreenshots: true,
718
+ composeForLlm: true,
719
+ llmPagesPerImage: 3,
720
+ llmResizeMaxWidth: 1100,
721
+ llmQuality: 72,
718
722
  metadata: {
719
723
  domain: "recommend",
720
724
  capture_mode: "scroll_sequence",
@@ -440,6 +440,10 @@ export async function runRecruitWorkflow({
440
440
  settleMs: 350,
441
441
  duplicateStopCount: 1,
442
442
  skipDuplicateScreenshots: true,
443
+ composeForLlm: true,
444
+ llmPagesPerImage: 3,
445
+ llmResizeMaxWidth: 1100,
446
+ llmQuality: 72,
443
447
  metadata: {
444
448
  domain: "recruit",
445
449
  capture_mode: "scroll_sequence",