@reconcrap/boss-recommend-mcp 2.0.8 → 2.0.9

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.8",
3
+ "version": "2.0.9",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
package/src/chat-mcp.js CHANGED
@@ -863,9 +863,17 @@ function getRunOptions(args, normalized, session, { workspaceRoot = "", configRe
863
863
  llmConfig: resolvedConfig.ok ? {
864
864
  ...resolvedConfig.config
865
865
  } : null,
866
- llmTimeoutMs: parsePositiveInteger(args.llm_timeout_ms, slowLive ? 180000 : 120000),
867
- llmImageLimit: parsePositiveInteger(args.llm_image_limit, 8),
868
- llmImageDetail: normalizeText(args.llm_image_detail) || "high",
866
+ llmTimeoutMs: parsePositiveInteger(
867
+ args.llm_timeout_ms,
868
+ parsePositiveInteger(resolvedConfig.config?.llmTimeoutMs || resolvedConfig.config?.timeoutMs, slowLive ? 180000 : 120000)
869
+ ),
870
+ llmImageLimit: parsePositiveInteger(
871
+ args.llm_image_limit,
872
+ parsePositiveInteger(resolvedConfig.config?.llmImageLimit || resolvedConfig.config?.imageLimit, 8)
873
+ ),
874
+ llmImageDetail: normalizeText(
875
+ args.llm_image_detail || resolvedConfig.config?.llmImageDetail || resolvedConfig.config?.imageDetail
876
+ ) || "low",
869
877
  screeningMode: normalizeScreeningModeArg(args),
870
878
  listMaxScrolls: parsePositiveInteger(args.list_max_scrolls, 200),
871
879
  listStableSignatureLimit: parsePositiveInteger(args.list_stable_signature_limit, 2),
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import crypto from "node:crypto";
3
3
  import path from "node:path";
4
+ import sharp from "sharp";
4
5
  import {
5
6
  getAttributesMap,
6
7
  getNodeBox,
@@ -149,6 +150,63 @@ function screenshotHash(buffer) {
149
150
  return crypto.createHash("sha256").update(buffer).digest("hex");
150
151
  }
151
152
 
153
+ async function optimizeScreenshotBuffer(buffer, {
154
+ enabled = false,
155
+ format = "png",
156
+ quality,
157
+ resizeMaxWidth = 0
158
+ } = {}) {
159
+ if (!enabled && !resizeMaxWidth) {
160
+ return {
161
+ buffer,
162
+ optimized: false,
163
+ optimization_error: null
164
+ };
165
+ }
166
+ try {
167
+ const normalizedFormat = format === "jpg" ? "jpeg" : format;
168
+ let pipeline = sharp(buffer, { failOn: "none" });
169
+ const metadata = await pipeline.metadata();
170
+ const width = Number(metadata.width) || 0;
171
+ const safeMaxWidth = Math.max(0, Number(resizeMaxWidth) || 0);
172
+ if (safeMaxWidth > 0 && width > safeMaxWidth) {
173
+ pipeline = pipeline.resize({
174
+ width: safeMaxWidth,
175
+ withoutEnlargement: true
176
+ });
177
+ }
178
+ if (normalizedFormat === "jpeg") {
179
+ pipeline = pipeline.jpeg({
180
+ quality: quality == null ? 72 : Math.max(35, Math.min(95, Number(quality) || 72)),
181
+ mozjpeg: true
182
+ });
183
+ } else if (normalizedFormat === "webp") {
184
+ pipeline = pipeline.webp({
185
+ quality: quality == null ? 76 : Math.max(35, Math.min(95, Number(quality) || 76))
186
+ });
187
+ } else {
188
+ pipeline = pipeline.png({
189
+ compressionLevel: 9,
190
+ adaptiveFiltering: true
191
+ });
192
+ }
193
+ const optimizedBuffer = await pipeline.toBuffer();
194
+ return {
195
+ buffer: optimizedBuffer,
196
+ optimized: true,
197
+ original_byte_length: buffer.length,
198
+ optimization_error: null
199
+ };
200
+ } catch (error) {
201
+ return {
202
+ buffer,
203
+ optimized: false,
204
+ original_byte_length: buffer.length,
205
+ optimization_error: error?.message || String(error)
206
+ };
207
+ }
208
+ }
209
+
152
210
  export async function captureScrolledNodeScreenshots(client, nodeId, {
153
211
  filePath,
154
212
  format = "png",
@@ -156,10 +214,14 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
156
214
  padding = 0,
157
215
  captureBeyondViewport = true,
158
216
  fromSurface = true,
217
+ captureViewport = false,
159
218
  maxScreenshots = 6,
160
219
  wheelDeltaY = 650,
161
220
  settleMs = 900,
162
221
  duplicateStopCount = 2,
222
+ skipDuplicateScreenshots = false,
223
+ optimize = false,
224
+ resizeMaxWidth = 0,
163
225
  metadata = {}
164
226
  } = {}) {
165
227
  if (!nodeId) throw new Error("captureScrolledNodeScreenshots requires nodeId");
@@ -167,12 +229,19 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
167
229
  const screenshots = [];
168
230
  let consecutiveDuplicates = 0;
169
231
  let previousHash = "";
232
+ let captureCount = 0;
233
+ let droppedDuplicateCount = 0;
170
234
 
171
235
  for (let index = 0; index < Math.max(1, Number(maxScreenshots) || 1); index += 1) {
236
+ captureCount += 1;
172
237
  const captureStarted = Date.now();
173
238
  const box = await getNodeBox(client, nodeId);
174
239
  const clip = withPadding(box.rect, padding);
175
- const captureOptions = {
240
+ const captureOptions = captureViewport ? {
241
+ format,
242
+ fromSurface,
243
+ captureBeyondViewport: false
244
+ } : {
176
245
  format,
177
246
  fromSurface,
178
247
  captureBeyondViewport,
@@ -182,7 +251,14 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
182
251
  captureOptions.quality = quality;
183
252
  }
184
253
  const screenshot = await client.Page.captureScreenshot(captureOptions);
185
- const buffer = Buffer.from(screenshot.data || "", "base64");
254
+ const originalBuffer = Buffer.from(screenshot.data || "", "base64");
255
+ const optimized = await optimizeScreenshotBuffer(originalBuffer, {
256
+ enabled: optimize,
257
+ format,
258
+ quality,
259
+ resizeMaxWidth
260
+ });
261
+ const buffer = optimized.buffer;
186
262
  const hash = screenshotHash(buffer);
187
263
  const duplicateOfPrevious = previousHash && previousHash === hash;
188
264
  if (duplicateOfPrevious) {
@@ -191,30 +267,40 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
191
267
  consecutiveDuplicates = 0;
192
268
  }
193
269
 
194
- const outputPath = filePath ? filePathForSequence(filePath, index, format) : null;
195
- if (outputPath) {
196
- fs.writeFileSync(outputPath, buffer);
197
- }
270
+ let outputPath = null;
271
+ if (duplicateOfPrevious && skipDuplicateScreenshots) {
272
+ droppedDuplicateCount += 1;
273
+ } else {
274
+ outputPath = filePath ? filePathForSequence(filePath, screenshots.length, format) : null;
275
+ if (outputPath) {
276
+ fs.writeFileSync(outputPath, buffer);
277
+ }
198
278
 
199
- screenshots.push({
200
- index,
201
- source: "image",
202
- captured_at: nowIso(),
203
- node_id: nodeId,
204
- format,
205
- mime_type: `image/${format === "jpeg" ? "jpeg" : "png"}`,
206
- byte_length: buffer.length,
207
- elapsed_ms: Date.now() - captureStarted,
208
- file_path: outputPath,
209
- sha256: hash,
210
- duplicate_of_previous: Boolean(duplicateOfPrevious),
211
- clip,
212
- node_rect: box.rect,
213
- scroll: index === 0
214
- ? { before_capture: "initial" }
215
- : { before_capture: `wheel_down_${index}` },
216
- metadata
217
- });
279
+ screenshots.push({
280
+ index: screenshots.length,
281
+ capture_index: index,
282
+ source: "image",
283
+ captured_at: nowIso(),
284
+ node_id: nodeId,
285
+ format,
286
+ mime_type: `image/${format === "jpeg" ? "jpeg" : "png"}`,
287
+ byte_length: buffer.length,
288
+ original_byte_length: optimized.original_byte_length || originalBuffer.length,
289
+ optimized: Boolean(optimized.optimized),
290
+ optimization_error: optimized.optimization_error || null,
291
+ elapsed_ms: Date.now() - captureStarted,
292
+ file_path: outputPath,
293
+ sha256: hash,
294
+ duplicate_of_previous: Boolean(duplicateOfPrevious),
295
+ clip,
296
+ capture_viewport: Boolean(captureViewport),
297
+ node_rect: box.rect,
298
+ scroll: index === 0
299
+ ? { before_capture: "initial" }
300
+ : { before_capture: `wheel_down_${index}` },
301
+ metadata
302
+ });
303
+ }
218
304
 
219
305
  previousHash = hash;
220
306
  if (consecutiveDuplicates >= Math.max(1, Number(duplicateStopCount) || 1)) {
@@ -242,8 +328,20 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
242
328
  captured_at: nowIso(),
243
329
  node_id: nodeId,
244
330
  elapsed_ms: Date.now() - sequenceStarted,
331
+ capture_count: captureCount,
245
332
  screenshot_count: screenshots.length,
246
333
  unique_screenshot_count: new Set(screenshots.map((item) => item.sha256)).size,
334
+ duplicate_screenshot_count: captureCount - new Set(screenshots.map((item) => item.sha256)).size,
335
+ dropped_duplicate_count: droppedDuplicateCount,
336
+ total_byte_length: screenshots.reduce((sum, item) => sum + (Number(item.byte_length) || 0), 0),
337
+ original_total_byte_length: screenshots.reduce((sum, item) => sum + (Number(item.original_byte_length) || 0), 0),
338
+ optimization: {
339
+ enabled: Boolean(optimize),
340
+ resize_max_width: Math.max(0, Number(resizeMaxWidth) || 0),
341
+ capture_viewport: Boolean(captureViewport),
342
+ format,
343
+ quality: quality ?? null
344
+ },
247
345
  file_paths: screenshots.map((item) => item.file_path).filter(Boolean),
248
346
  screenshots,
249
347
  metadata
@@ -126,8 +126,13 @@ export function summarizeImageEvidence(imageEvidence = null) {
126
126
  return {
127
127
  source: imageEvidence.source || "",
128
128
  elapsed_ms: imageEvidence.elapsed_ms || 0,
129
+ capture_count: imageEvidence.capture_count || imageEvidence.screenshot_count || 0,
129
130
  screenshot_count: imageEvidence.screenshot_count || 0,
130
131
  unique_screenshot_count: imageEvidence.unique_screenshot_count || 0,
132
+ dropped_duplicate_count: imageEvidence.dropped_duplicate_count || 0,
133
+ total_byte_length: imageEvidence.total_byte_length || 0,
134
+ original_total_byte_length: imageEvidence.original_total_byte_length || 0,
135
+ optimization: imageEvidence.optimization || null,
131
136
  file_paths: imageEvidence.file_paths || [],
132
137
  first_clip: imageEvidence.screenshots?.[0]?.clip || imageEvidence.clip || null
133
138
  };
@@ -289,6 +289,72 @@ function tryExtractJsonObject(text) {
289
289
  return null;
290
290
  }
291
291
 
292
+ function extractBalancedJsonAt(text = "", startIndex = 0) {
293
+ const source = String(text || "");
294
+ const start = source.indexOf("{", Math.max(0, Number(startIndex) || 0));
295
+ if (start < 0) return "";
296
+ let depth = 0;
297
+ let inString = false;
298
+ let quote = "";
299
+ let escaped = false;
300
+ for (let index = start; index < source.length; index += 1) {
301
+ const char = source[index];
302
+ if (inString) {
303
+ if (escaped) {
304
+ escaped = false;
305
+ } else if (char === "\\") {
306
+ escaped = true;
307
+ } else if (char === quote) {
308
+ inString = false;
309
+ quote = "";
310
+ }
311
+ continue;
312
+ }
313
+ if (char === "\"" || char === "'") {
314
+ inString = true;
315
+ quote = char;
316
+ continue;
317
+ }
318
+ if (char === "{") depth += 1;
319
+ if (char === "}") {
320
+ depth -= 1;
321
+ if (depth === 0) return source.slice(start, index + 1);
322
+ }
323
+ }
324
+ return "";
325
+ }
326
+
327
+ function tryParseEmbeddedJsonObjects(text = "") {
328
+ const source = decodeHtmlEntities(String(text || ""));
329
+ const objects = [];
330
+ const anchors = [
331
+ "__INITIAL_STATE__",
332
+ "__NEXT_DATA__",
333
+ "geekDetailInfo",
334
+ "geekDetail",
335
+ "geekBaseInfo",
336
+ "geekEduExpList",
337
+ "geekWorkExpList",
338
+ "resume"
339
+ ];
340
+ for (const anchor of anchors) {
341
+ let searchIndex = 0;
342
+ while (searchIndex >= 0 && searchIndex < source.length) {
343
+ const anchorIndex = source.indexOf(anchor, searchIndex);
344
+ if (anchorIndex < 0) break;
345
+ const windowStart = Math.max(0, anchorIndex - 4000);
346
+ const braceIndex = source.lastIndexOf("{", anchorIndex);
347
+ if (braceIndex >= windowStart) {
348
+ const jsonText = extractBalancedJsonAt(source, braceIndex);
349
+ const parsed = tryParseJson(jsonText);
350
+ if (parsed && typeof parsed === "object") objects.push(parsed);
351
+ }
352
+ searchIndex = anchorIndex + anchor.length;
353
+ }
354
+ }
355
+ return objects;
356
+ }
357
+
292
358
  function flattenChatMessageContent(content) {
293
359
  if (typeof content === "string") return content;
294
360
  if (Array.isArray(content)) {
@@ -392,6 +458,65 @@ function pickFirst(...values) {
392
458
  return "";
393
459
  }
394
460
 
461
+ function isPlainObject(value) {
462
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
463
+ }
464
+
465
+ function isBossGeekDetailShape(value) {
466
+ if (!isPlainObject(value)) return false;
467
+ return Boolean(
468
+ isPlainObject(value.geekBaseInfo)
469
+ || value.geekName
470
+ || value.geekAdvantage
471
+ || Array.isArray(value.geekEduExpList)
472
+ || Array.isArray(value.geekEducationList)
473
+ || Array.isArray(value.geekWorkExpList)
474
+ || Array.isArray(value.geekProjExpList)
475
+ || Array.isArray(value.geekCertificationList)
476
+ || Array.isArray(value.geekSkillList)
477
+ || isPlainObject(value.highestEduExp)
478
+ );
479
+ }
480
+
481
+ function isBossChatProfileShape(value) {
482
+ if (!isPlainObject(value)) return false;
483
+ return Boolean(
484
+ (value.name || value.encryptGeekId || value.uid)
485
+ && (
486
+ Array.isArray(value.eduExpList)
487
+ || Array.isArray(value.workExpList)
488
+ || value.school
489
+ || value.major
490
+ || value.lastCompany
491
+ || value.positionName
492
+ )
493
+ );
494
+ }
495
+
496
+ function collectObjects(root, {
497
+ maxObjects = 500,
498
+ maxDepth = 8
499
+ } = {}) {
500
+ if (!root || typeof root !== "object") return [];
501
+ const queue = [{ value: root, depth: 0 }];
502
+ const seen = new WeakSet();
503
+ const objects = [];
504
+ while (queue.length && objects.length < maxObjects) {
505
+ const { value, depth } = queue.shift();
506
+ if (!value || typeof value !== "object" || seen.has(value)) continue;
507
+ seen.add(value);
508
+ if (isPlainObject(value)) objects.push(value);
509
+ if (depth >= maxDepth) continue;
510
+ const children = Array.isArray(value) ? value : Object.values(value);
511
+ for (const child of children) {
512
+ if (child && typeof child === "object") {
513
+ queue.push({ value: child, depth: depth + 1 });
514
+ }
515
+ }
516
+ }
517
+ return objects;
518
+ }
519
+
395
520
  function joinRange(start, end, fallback = "") {
396
521
  const left = parseDateLike(start);
397
522
  const right = parseDateLike(end);
@@ -461,8 +586,8 @@ function formatEducation(item = {}, index = 0) {
461
586
  ];
462
587
  return [
463
588
  `${index + 1}. ${[
464
- pickFirst(item.school),
465
- pickFirst(item.major),
589
+ pickFirst(item.school, item.schoolName),
590
+ pickFirst(item.major, item.majorName),
466
591
  pickFirst(item.degreeName, item.degree),
467
592
  period
468
593
  ].filter(Boolean).join(" / ")}`,
@@ -507,16 +632,28 @@ function resolveBossGeekDetail(payload = {}) {
507
632
  const candidates = [
508
633
  { sourceKey: "geekDetailInfo", detail: payload?.zpData?.geekDetailInfo },
509
634
  { sourceKey: "geekDetail", detail: payload?.zpData?.geekDetail },
635
+ { sourceKey: "geekDetailInfo", detail: payload?.zpData?.data?.geekDetailInfo },
636
+ { sourceKey: "geekDetail", detail: payload?.zpData?.data?.geekDetail },
637
+ { sourceKey: "geekDetailInfo", detail: payload?.zpData?.data?.detailInfo },
638
+ { sourceKey: "geekDetailInfo", detail: payload?.zpData?.data?.resumeDetail },
639
+ { sourceKey: "geekDetailInfo", detail: payload?.zpData?.data },
640
+ { sourceKey: "geekDetailInfo", detail: payload?.data?.geekDetailInfo },
641
+ { sourceKey: "geekDetail", detail: payload?.data?.geekDetail },
642
+ { sourceKey: "geekDetailInfo", detail: payload?.data?.detailInfo },
643
+ { sourceKey: "geekDetailInfo", detail: payload?.data?.resumeDetail },
644
+ { sourceKey: "geekDetailInfo", detail: payload?.data },
510
645
  { sourceKey: "geekDetailInfo", detail: payload?.geekDetailInfo },
511
- { sourceKey: "geekDetail", detail: payload?.geekDetail }
646
+ { sourceKey: "geekDetail", detail: payload?.geekDetail },
647
+ { sourceKey: "geekDetailInfo", detail: payload }
512
648
  ];
513
- const found = candidates.find((item) => item.detail && typeof item.detail === "object");
649
+ const found = candidates.find((item) => isBossGeekDetailShape(item.detail));
514
650
  return found || { sourceKey: "", detail: null };
515
651
  }
516
652
 
517
653
  function extractBossChatGeekInfo(payload = {}) {
518
- const data = payload?.zpData?.data;
654
+ const data = payload?.zpData?.data || payload?.data || payload?.zpData?.geekInfo || payload?.geekInfo;
519
655
  if (!data || typeof data !== "object") return null;
656
+ if (!isBossChatProfileShape(data)) return null;
520
657
  const educationList = normalizeList(data.eduExpList);
521
658
  const workList = normalizeList(data.workExpList);
522
659
  const firstEducation = educationList[0] || {};
@@ -585,7 +722,13 @@ function extractBossChatGeekInfo(payload = {}) {
585
722
  }
586
723
 
587
724
  function extractBossChatHistoryResume(payload = {}) {
588
- const messages = normalizeList(payload?.zpData?.messages);
725
+ const messages = normalizeList(payload?.zpData?.messages).length
726
+ ? normalizeList(payload?.zpData?.messages)
727
+ : normalizeList(payload?.messages).length
728
+ ? normalizeList(payload?.messages)
729
+ : normalizeList(payload?.data?.messages).length
730
+ ? normalizeList(payload?.data?.messages)
731
+ : normalizeList(payload?.zpData?.data?.messages);
589
732
  const resumes = messages
590
733
  .map((message) => message?.body?.resume)
591
734
  .filter((resume) => resume && typeof resume === "object");
@@ -647,12 +790,56 @@ function extractBossChatHistoryResume(payload = {}) {
647
790
  };
648
791
  }
649
792
 
793
+ function extractBossProfileRecursively(payload = {}) {
794
+ for (const object of collectObjects(payload)) {
795
+ if (isBossGeekDetailShape(object)) {
796
+ const profile = extractBossGeekDetailInfo({ geekDetailInfo: object });
797
+ if (profile?.text || profile?.identity?.name) {
798
+ return {
799
+ ...profile,
800
+ source_keys: {
801
+ ...(profile.source_keys || {}),
802
+ recursive_profile_match: true
803
+ }
804
+ };
805
+ }
806
+ }
807
+ if (isBossChatProfileShape(object)) {
808
+ const profile = extractBossChatGeekInfo({ zpData: { data: object } });
809
+ if (profile?.text || profile?.identity?.name) {
810
+ return {
811
+ ...profile,
812
+ source_keys: {
813
+ ...(profile.source_keys || {}),
814
+ recursive_profile_match: true
815
+ }
816
+ };
817
+ }
818
+ }
819
+ if (isPlainObject(object.resume)) {
820
+ const profile = extractBossChatHistoryResume({ zpData: { messages: [{ body: { resume: object.resume } }] } });
821
+ if (profile?.text || profile?.identity?.name) {
822
+ return {
823
+ ...profile,
824
+ source_keys: {
825
+ ...(profile.source_keys || {}),
826
+ recursive_profile_match: true
827
+ }
828
+ };
829
+ }
830
+ }
831
+ }
832
+ return null;
833
+ }
834
+
650
835
  function extractBossGeekDetailInfo(payload = {}) {
651
836
  const { sourceKey, detail } = resolveBossGeekDetail(payload);
652
837
  if (!detail || typeof detail !== "object") return null;
653
838
 
654
- const base = detail.geekBaseInfo || {};
655
- const educationList = normalizeList(detail.geekEduExpList);
839
+ const base = detail.geekBaseInfo || detail.baseInfo || detail.base || {};
840
+ const educationList = normalizeList(detail.geekEduExpList).length
841
+ ? normalizeList(detail.geekEduExpList)
842
+ : normalizeList(detail.geekEducationList);
656
843
  const firstEducation = educationList[0] || detail.highestEduExp || {};
657
844
  const workList = normalizeList(detail.geekWorkExpList);
658
845
  const firstWork = workList[0] || {};
@@ -666,6 +853,8 @@ function extractBossGeekDetailInfo(payload = {}) {
666
853
  const normalizedExpectationList = expectationList.length ? expectationList : expectationFallback;
667
854
  const certifications = normalizeList(detail.geekCertificationList);
668
855
  const skillTags = [
856
+ ...normalizeTagList(detail.geekSkillList),
857
+ ...normalizeTagList(detail.skillList),
669
858
  ...normalizeTagList(detail.blueGeekSkills),
670
859
  ...normalizeTagList(base.userHighlightList),
671
860
  ...normalizeTagList(base.userDescHighlightList),
@@ -674,6 +863,7 @@ function extractBossGeekDetailInfo(payload = {}) {
674
863
  ...normalizeTagList(detail.professionalSkill)
675
864
  ];
676
865
  const summaryParts = [
866
+ pickFirst(detail.geekAdvantage),
677
867
  pickFirst(base.userDescription),
678
868
  pickFirst(base.userDesc),
679
869
  pickFirst(base.workEduDesc),
@@ -681,7 +871,7 @@ function extractBossGeekDetailInfo(payload = {}) {
681
871
  ].filter(Boolean);
682
872
  const sections = {
683
873
  base: [
684
- base.name ? `姓名:${normalizeText(base.name)}` : "",
874
+ pickFirst(base.name, detail.geekName, detail.name) ? `姓名:${pickFirst(base.name, detail.geekName, detail.name)}` : "",
685
875
  normalizeGenderValue(base.gender) ? `性别:${normalizeGenderValue(base.gender)}` : "",
686
876
  pickFirst(base.ageDesc, base.age) ? `年龄:${pickFirst(base.ageDesc, base.age)}` : "",
687
877
  pickFirst(base.degreeCategory) ? `最高学历:${pickFirst(base.degreeCategory)}` : "",
@@ -710,11 +900,11 @@ function extractBossGeekDetailInfo(payload = {}) {
710
900
 
711
901
  return {
712
902
  identity: {
713
- name: pickFirst(base.name),
903
+ name: pickFirst(base.name, detail.geekName, detail.name),
714
904
  current_position: pickFirst(firstWork.positionName, firstWork.positionTitle, firstWork.position),
715
- current_company: pickFirst(firstWork.formattedCompany, firstWork.company),
716
- school: pickFirst(firstEducation.school),
717
- major: pickFirst(firstEducation.major),
905
+ current_company: pickFirst(firstWork.formattedCompany, firstWork.company, firstWork.brandName),
906
+ school: pickFirst(firstEducation.school, firstEducation.schoolName),
907
+ major: pickFirst(firstEducation.major, firstEducation.majorName),
718
908
  degree: pickFirst(base.degreeCategory, firstEducation.degreeName, firstEducation.degree),
719
909
  years_experience: parseYearsExperience(pickFirst(base.workYearDesc, base.workYearsDesc)) ?? null,
720
910
  age: parseAge(pickFirst(base.ageDesc, base.age)) ?? null,
@@ -744,24 +934,68 @@ function extractBossGeekDetailInfo(payload = {}) {
744
934
 
745
935
  export function extractBossProfileFromNetworkBody(networkBody = {}) {
746
936
  const text = parseNetworkBodyText(networkBody);
747
- const parsed = tryParseJson(text);
748
- if (!parsed) {
937
+ const parsedObjects = [
938
+ tryParseJson(text),
939
+ ...tryParseEmbeddedJsonObjects(text)
940
+ ].filter((item) => item && typeof item === "object");
941
+ if (!parsedObjects.length) {
942
+ const htmlText = /<html|<body|<div|<section|<script/i.test(text) ? htmlToText(text) : "";
943
+ if (htmlText && htmlText.length > 80) {
944
+ const candidate = normalizeCandidateProfile({
945
+ domain: "recommend",
946
+ source: "network-html-fallback",
947
+ text: htmlText
948
+ });
949
+ return {
950
+ ok: true,
951
+ url: networkBody.url || null,
952
+ status: networkBody.status ?? null,
953
+ mimeType: networkBody.mimeType || null,
954
+ text_length: text.length,
955
+ profile: {
956
+ identity: candidate.identity,
957
+ tags: candidate.tags,
958
+ sections: { html_text: [htmlText] },
959
+ text: htmlText,
960
+ source_keys: {
961
+ network_html_text: true,
962
+ html_text_length: htmlText.length
963
+ }
964
+ }
965
+ };
966
+ }
749
967
  return {
750
968
  ok: false,
751
969
  error: "NETWORK_BODY_NOT_JSON",
752
970
  text_length: text.length
753
971
  };
754
972
  }
755
- const profile = extractBossGeekDetailInfo(parsed)
756
- || extractBossChatGeekInfo(parsed)
757
- || extractBossChatHistoryResume(parsed);
973
+ let profile = null;
974
+ let parsed = parsedObjects[0];
975
+ for (const candidateObject of parsedObjects) {
976
+ profile = extractBossGeekDetailInfo(candidateObject)
977
+ || extractBossChatGeekInfo(candidateObject)
978
+ || extractBossChatHistoryResume(candidateObject)
979
+ || extractBossProfileRecursively(candidateObject);
980
+ if (profile) {
981
+ parsed = candidateObject;
982
+ break;
983
+ }
984
+ }
758
985
  if (!profile) {
986
+ const encryptedPayload = parsedObjects.find((item) => (
987
+ normalizeText(item?.zpData?.encryptGeekDetailInfo || item?.encryptGeekDetailInfo || "")
988
+ ));
759
989
  return {
760
990
  ok: false,
761
- error: "BOSS_GEEK_DETAIL_INFO_NOT_FOUND",
991
+ error: encryptedPayload ? "BOSS_GEEK_DETAIL_INFO_ENCRYPTED" : "BOSS_GEEK_DETAIL_INFO_NOT_FOUND",
762
992
  text_length: text.length,
763
- top_level_keys: Object.keys(parsed).slice(0, 30),
764
- zpData_keys: Object.keys(parsed?.zpData || {}).slice(0, 50)
993
+ parsed_object_count: parsedObjects.length,
994
+ top_level_keys: Object.keys(parsed || {}).slice(0, 30),
995
+ zpData_keys: Object.keys(parsed?.zpData || {}).slice(0, 50),
996
+ data_keys: Object.keys(parsed?.data || parsed?.zpData?.data || {}).slice(0, 50),
997
+ encrypted_resume: Boolean(encryptedPayload),
998
+ encrypted_resume_length: normalizeText(encryptedPayload?.zpData?.encryptGeekDetailInfo || encryptedPayload?.encryptGeekDetailInfo || "").length
765
999
  };
766
1000
  }
767
1001
  return {
@@ -1088,6 +1322,8 @@ export function createFailedLlmScreeningResult(error) {
1088
1322
  decision_cot: "",
1089
1323
  reasoning_content: "",
1090
1324
  raw_model_output: "",
1325
+ image_input_count: Number(error?.image_input_count) || 0,
1326
+ image_inputs: Array.isArray(error?.image_inputs) ? error.image_inputs : [],
1091
1327
  error: error?.message || String(error || "unknown"),
1092
1328
  screened_at: nowIso()
1093
1329
  };
@@ -1176,6 +1412,7 @@ export async function callScreeningLlm({
1176
1412
  const payload = {
1177
1413
  model,
1178
1414
  temperature: 0.1,
1415
+ max_tokens: Math.max(1, Number(config.maxTokens || config.llmMaxTokens) || 64),
1179
1416
  messages: buildScreeningLlmMessages({
1180
1417
  candidate,
1181
1418
  criteria,
@@ -1185,7 +1422,7 @@ export async function callScreeningLlm({
1185
1422
  applyChatCompletionThinking(payload, {
1186
1423
  baseUrl,
1187
1424
  model,
1188
- thinkingLevel: config.llmThinkingLevel || config.thinkingLevel || config.reasoningEffort
1425
+ thinkingLevel: config.llmThinkingLevel || config.thinkingLevel || config.reasoningEffort || "off"
1189
1426
  });
1190
1427
 
1191
1428
  const controller = new AbortController();
@@ -1250,6 +1487,10 @@ export async function callScreeningLlm({
1250
1487
  image_inputs: summarizeLlmImageInputs(imageInputs),
1251
1488
  screened_at: nowIso()
1252
1489
  };
1490
+ } catch (error) {
1491
+ error.image_input_count = imageInputs.length;
1492
+ error.image_inputs = summarizeLlmImageInputs(imageInputs);
1493
+ throw error;
1253
1494
  } finally {
1254
1495
  clearTimeout(timer);
1255
1496
  }
@@ -1273,10 +1273,10 @@ export async function extractChatProfileCandidate(client, {
1273
1273
  resumeHtml: providedResumeHtml = null,
1274
1274
  networkEvents = [],
1275
1275
  targetUrl = "",
1276
- closeResume = true
1276
+ closeResume = true,
1277
+ networkParseRetryMs = 1800,
1278
+ networkParseIntervalMs = 250
1277
1279
  } = {}) {
1278
- await sleep(1000);
1279
- const networkBodies = await readChatProfileNetworkBodies(client, networkEvents);
1280
1280
  let resumeHtml = providedResumeHtml || null;
1281
1281
  if (!resumeHtml) {
1282
1282
  try {
@@ -1292,21 +1292,30 @@ export async function extractChatProfileCandidate(client, {
1292
1292
  resumeHtml.resumeIframeText
1293
1293
  ].filter(Boolean).join("\n\n");
1294
1294
 
1295
- const detailCandidateResult = buildScreeningCandidateFromDetail({
1296
- domain: "chat",
1297
- source: "chat-live-cdp-profile",
1298
- cardCandidate,
1299
- detailText,
1300
- networkBodies,
1301
- metadata: {
1302
- target_url: targetUrl,
1303
- card_node_id: cardNodeId,
1304
- resume_popup_selector: resumeState?.popup?.selector || null,
1305
- resume_content_selector: resumeState?.content?.selector || null,
1306
- resume_iframe_selector: resumeState?.resumeIframe?.selector || null,
1307
- resume_iframe_document_node_id: resumeHtml.resumeIframeDocumentNodeId
1308
- }
1309
- });
1295
+ const parseStarted = Date.now();
1296
+ let networkBodies = [];
1297
+ let detailCandidateResult = null;
1298
+ do {
1299
+ networkBodies = await readChatProfileNetworkBodies(client, networkEvents);
1300
+ detailCandidateResult = buildScreeningCandidateFromDetail({
1301
+ domain: "chat",
1302
+ source: "chat-live-cdp-profile",
1303
+ cardCandidate,
1304
+ detailText,
1305
+ networkBodies,
1306
+ metadata: {
1307
+ target_url: targetUrl,
1308
+ card_node_id: cardNodeId,
1309
+ resume_popup_selector: resumeState?.popup?.selector || null,
1310
+ resume_content_selector: resumeState?.content?.selector || null,
1311
+ resume_iframe_selector: resumeState?.resumeIframe?.selector || null,
1312
+ resume_iframe_document_node_id: resumeHtml.resumeIframeDocumentNodeId
1313
+ }
1314
+ });
1315
+ if (detailCandidateResult.parsed_network_profiles.some((item) => item.ok)) break;
1316
+ if (Date.now() - parseStarted >= Math.max(0, Number(networkParseRetryMs) || 0)) break;
1317
+ await sleep(Math.max(50, Number(networkParseIntervalMs) || 250));
1318
+ } while (true);
1310
1319
 
1311
1320
  let closeResult = null;
1312
1321
  if (closeResume) {
@@ -1317,6 +1326,8 @@ export async function extractChatProfileCandidate(client, {
1317
1326
  candidate: detailCandidateResult.candidate,
1318
1327
  parsed_network_profiles: detailCandidateResult.parsed_network_profiles,
1319
1328
  network_bodies: networkBodies,
1329
+ network_parse_retry_elapsed_ms: Date.now() - parseStarted,
1330
+ network_event_count: networkEvents.length,
1320
1331
  detail: {
1321
1332
  popup_text: resumeHtml.popupText,
1322
1333
  content_text: resumeHtml.contentText,
@@ -752,8 +752,11 @@ export async function runChatWorkflow({
752
752
  resumeNetworkEvents
753
753
  ),
754
754
  targetUrl,
755
- closeResume: false
755
+ closeResume: false,
756
+ networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
757
+ networkParseIntervalMs: 250
756
758
  });
759
+ addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
757
760
  parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
758
761
  if (parsedNetworkProfileCount > 0) {
759
762
  contentWait = {
@@ -789,8 +792,11 @@ export async function runChatWorkflow({
789
792
  resumeNetworkEvents
790
793
  ),
791
794
  targetUrl,
792
- closeResume: false
795
+ closeResume: false,
796
+ networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
797
+ networkParseIntervalMs: 250
793
798
  });
799
+ addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
794
800
  parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
795
801
  }
796
802
 
@@ -808,12 +814,20 @@ export async function runChatWorkflow({
808
814
  imageOutputDir,
809
815
  domain: "chat",
810
816
  runId: runControl?.runId,
811
- index
817
+ index,
818
+ extension: "jpg"
812
819
  }),
820
+ format: "jpeg",
821
+ quality: 72,
822
+ optimize: true,
823
+ resizeMaxWidth: 1100,
824
+ captureViewport: true,
813
825
  padding: 8,
814
826
  maxScreenshots: maxImagePages,
815
827
  wheelDeltaY: imageWheelDeltaY,
816
- settleMs: 1200,
828
+ settleMs: 350,
829
+ duplicateStopCount: 1,
830
+ skipDuplicateScreenshots: true,
817
831
  metadata: {
818
832
  domain: "chat",
819
833
  capture_mode: "scroll_sequence",
@@ -487,31 +487,40 @@ export async function extractRecommendDetailCandidate(client, {
487
487
  detailState,
488
488
  networkEvents = [],
489
489
  targetUrl = "",
490
- closeDetail = true
490
+ closeDetail = true,
491
+ networkParseRetryMs = 1800,
492
+ networkParseIntervalMs = 250
491
493
  } = {}) {
492
- await sleep(1000);
493
- const networkBodies = await readRecommendDetailNetworkBodies(client, networkEvents);
494
494
  const detailHtml = await readRecommendDetailHtml(client, detailState);
495
495
  const detailText = [
496
496
  detailHtml.popupText,
497
497
  detailHtml.resumeText
498
498
  ].filter(Boolean).join("\n\n");
499
499
 
500
- const detailCandidateResult = buildScreeningCandidateFromDetail({
501
- cardCandidate,
502
- detailText,
503
- networkBodies,
504
- metadata: {
505
- target_url: targetUrl,
506
- card_node_id: cardNodeId,
507
- detail_popup_selector: detailState?.popup?.selector || null,
508
- detail_popup_root: detailState?.popup?.root || null,
509
- resume_iframe_selector: detailState?.resumeIframe?.selector || null,
510
- resume_iframe_root: detailState?.resumeIframe?.root || null,
511
- resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId,
512
- detail_html_errors: detailHtml.errors || []
513
- }
514
- });
500
+ const parseStarted = Date.now();
501
+ let networkBodies = [];
502
+ let detailCandidateResult = null;
503
+ do {
504
+ networkBodies = await readRecommendDetailNetworkBodies(client, networkEvents);
505
+ detailCandidateResult = buildScreeningCandidateFromDetail({
506
+ cardCandidate,
507
+ detailText,
508
+ networkBodies,
509
+ metadata: {
510
+ target_url: targetUrl,
511
+ card_node_id: cardNodeId,
512
+ detail_popup_selector: detailState?.popup?.selector || null,
513
+ detail_popup_root: detailState?.popup?.root || null,
514
+ resume_iframe_selector: detailState?.resumeIframe?.selector || null,
515
+ resume_iframe_root: detailState?.resumeIframe?.root || null,
516
+ resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId,
517
+ detail_html_errors: detailHtml.errors || []
518
+ }
519
+ });
520
+ if (detailCandidateResult.parsed_network_profiles.some((item) => item.ok)) break;
521
+ if (Date.now() - parseStarted >= Math.max(0, Number(networkParseRetryMs) || 0)) break;
522
+ await sleep(Math.max(50, Number(networkParseIntervalMs) || 250));
523
+ } while (true);
515
524
 
516
525
  let closeResult = null;
517
526
  if (closeDetail) {
@@ -522,6 +531,8 @@ export async function extractRecommendDetailCandidate(client, {
522
531
  candidate: detailCandidateResult.candidate,
523
532
  parsed_network_profiles: detailCandidateResult.parsed_network_profiles,
524
533
  network_bodies: networkBodies,
534
+ network_parse_retry_elapsed_ms: Date.now() - parseStarted,
535
+ network_event_count: networkEvents.length,
525
536
  detail: {
526
537
  popup_text: detailHtml.popupText,
527
538
  resume_text: detailHtml.resumeText,
@@ -677,8 +677,11 @@ export async function runRecommendWorkflow({
677
677
  detailState: openedDetail.detail_state,
678
678
  networkEvents: networkRecorder.events,
679
679
  targetUrl,
680
- closeDetail: false
680
+ closeDetail: false,
681
+ networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
682
+ networkParseIntervalMs: 250
681
683
  });
684
+ addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
682
685
 
683
686
  const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
684
687
  let source = "network";
@@ -698,12 +701,20 @@ export async function runRecommendWorkflow({
698
701
  imageOutputDir,
699
702
  domain: "recommend",
700
703
  runId: runControl?.runId,
701
- index
704
+ index,
705
+ extension: "jpg"
702
706
  }),
707
+ format: "jpeg",
708
+ quality: 72,
709
+ optimize: true,
710
+ resizeMaxWidth: 1100,
711
+ captureViewport: true,
703
712
  padding: 4,
704
713
  maxScreenshots: maxImagePages,
705
714
  wheelDeltaY: imageWheelDeltaY,
706
- settleMs: 1200,
715
+ settleMs: 350,
716
+ duplicateStopCount: 1,
717
+ skipDuplicateScreenshots: true,
707
718
  metadata: {
708
719
  domain: "recommend",
709
720
  capture_mode: "scroll_sequence",
@@ -379,31 +379,40 @@ export async function extractRecruitDetailCandidate(client, {
379
379
  detailHtml: providedDetailHtml = null,
380
380
  networkEvents = [],
381
381
  targetUrl = "",
382
- closeDetail = true
382
+ closeDetail = true,
383
+ networkParseRetryMs = 1800,
384
+ networkParseIntervalMs = 250
383
385
  } = {}) {
384
- await sleep(1000);
385
- const networkBodies = await readRecruitDetailNetworkBodies(client, networkEvents);
386
386
  const detailHtml = providedDetailHtml || await readRecruitDetailHtml(client, detailState);
387
387
  const detailText = [
388
388
  detailHtml.popupText,
389
389
  detailHtml.resumeText
390
390
  ].filter(Boolean).join("\n\n");
391
391
 
392
- const detailCandidateResult = buildScreeningCandidateFromDetail({
393
- domain: "recruit",
394
- cardCandidate,
395
- detailText,
396
- networkBodies,
397
- metadata: {
398
- target_url: targetUrl,
399
- card_node_id: cardNodeId,
400
- detail_popup_selector: detailState?.popup?.selector || null,
401
- detail_popup_root: detailState?.popup?.root || null,
402
- resume_iframe_selector: detailState?.resumeIframe?.selector || null,
403
- resume_iframe_root: detailState?.resumeIframe?.root || null,
404
- resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId
405
- }
406
- });
392
+ const parseStarted = Date.now();
393
+ let networkBodies = [];
394
+ let detailCandidateResult = null;
395
+ do {
396
+ networkBodies = await readRecruitDetailNetworkBodies(client, networkEvents);
397
+ detailCandidateResult = buildScreeningCandidateFromDetail({
398
+ domain: "recruit",
399
+ cardCandidate,
400
+ detailText,
401
+ networkBodies,
402
+ metadata: {
403
+ target_url: targetUrl,
404
+ card_node_id: cardNodeId,
405
+ detail_popup_selector: detailState?.popup?.selector || null,
406
+ detail_popup_root: detailState?.popup?.root || null,
407
+ resume_iframe_selector: detailState?.resumeIframe?.selector || null,
408
+ resume_iframe_root: detailState?.resumeIframe?.root || null,
409
+ resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId
410
+ }
411
+ });
412
+ if (detailCandidateResult.parsed_network_profiles.some((item) => item.ok)) break;
413
+ if (Date.now() - parseStarted >= Math.max(0, Number(networkParseRetryMs) || 0)) break;
414
+ await sleep(Math.max(50, Number(networkParseIntervalMs) || 250));
415
+ } while (true);
407
416
 
408
417
  let closeResult = null;
409
418
  if (closeDetail) {
@@ -414,6 +423,8 @@ export async function extractRecruitDetailCandidate(client, {
414
423
  candidate: detailCandidateResult.candidate,
415
424
  parsed_network_profiles: detailCandidateResult.parsed_network_profiles,
416
425
  network_bodies: networkBodies,
426
+ network_parse_retry_elapsed_ms: Date.now() - parseStarted,
427
+ network_event_count: networkEvents.length,
417
428
  detail: {
418
429
  popup_text: detailHtml.popupText,
419
430
  resume_text: detailHtml.resumeText,
@@ -403,8 +403,11 @@ export async function runRecruitWorkflow({
403
403
  detailState: openedDetail.detail_state,
404
404
  networkEvents: networkRecorder.events,
405
405
  targetUrl,
406
- closeDetail: false
406
+ closeDetail: false,
407
+ networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
408
+ networkParseIntervalMs: 250
407
409
  });
410
+ addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
408
411
  const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
409
412
  let source = "network";
410
413
  let imageEvidence = null;
@@ -423,12 +426,20 @@ export async function runRecruitWorkflow({
423
426
  imageOutputDir,
424
427
  domain: "recruit",
425
428
  runId: runControl?.runId,
426
- index
429
+ index,
430
+ extension: "jpg"
427
431
  }),
432
+ format: "jpeg",
433
+ quality: 72,
434
+ optimize: true,
435
+ resizeMaxWidth: 1100,
436
+ captureViewport: true,
428
437
  padding: 4,
429
438
  maxScreenshots: maxImagePages,
430
439
  wheelDeltaY: imageWheelDeltaY,
431
- settleMs: 1200,
440
+ settleMs: 350,
441
+ duplicateStopCount: 1,
442
+ skipDuplicateScreenshots: true,
432
443
  metadata: {
433
444
  domain: "recruit",
434
445
  capture_mode: "scroll_sequence",
package/src/index.js CHANGED
@@ -572,7 +572,7 @@ function createRunInputSchema() {
572
572
  },
573
573
  llm_image_detail: {
574
574
  type: "string",
575
- description: "可选,图片输入 detail,默认 high"
575
+ description: "可选,图片输入 detail,默认 low"
576
576
  },
577
577
  delay_ms: {
578
578
  type: "integer",
@@ -746,7 +746,7 @@ function createBossChatStartInputSchema({ requireFullInput = false } = {}) {
746
746
  },
747
747
  llm_image_detail: {
748
748
  type: "string",
749
- description: "可选,图片输入 detail,默认 high"
749
+ description: "可选,图片输入 detail,默认 low"
750
750
  },
751
751
  online_resume_button_timeout_ms: {
752
752
  type: "integer",
@@ -1045,9 +1045,17 @@ function getRunOptions(args, parsed, normalized, session, configResolution = nul
1045
1045
  llmConfig: normalized.screeningMode === "llm" && configResolution?.ok ? {
1046
1046
  ...configResolution.config
1047
1047
  } : null,
1048
- llmTimeoutMs: parsePositiveInteger(args.llm_timeout_ms, slowLive ? 180000 : 120000),
1049
- llmImageLimit: parsePositiveInteger(args.llm_image_limit, 8),
1050
- llmImageDetail: normalizeText(args.llm_image_detail) || "high",
1048
+ llmTimeoutMs: parsePositiveInteger(
1049
+ args.llm_timeout_ms,
1050
+ parsePositiveInteger(configResolution?.config?.llmTimeoutMs || configResolution?.config?.timeoutMs, slowLive ? 180000 : 120000)
1051
+ ),
1052
+ llmImageLimit: parsePositiveInteger(
1053
+ args.llm_image_limit,
1054
+ parsePositiveInteger(configResolution?.config?.llmImageLimit || configResolution?.config?.imageLimit, 8)
1055
+ ),
1056
+ llmImageDetail: normalizeText(
1057
+ args.llm_image_detail || configResolution?.config?.llmImageDetail || configResolution?.config?.imageDetail
1058
+ ) || "low",
1051
1059
  imageOutputDir: resolveBossConfiguredOutputDir("", getRunsDir()),
1052
1060
  name: "mcp-recommend-pipeline-run",
1053
1061
  parsed
@@ -449,7 +449,7 @@ export function createRecruitPipelineInputSchema() {
449
449
  },
450
450
  llm_image_detail: {
451
451
  type: "string",
452
- description: "可选,图片输入 detail,默认 high"
452
+ description: "可选,图片输入 detail,默认 low"
453
453
  },
454
454
  delay_ms: {
455
455
  type: "integer",
@@ -834,9 +834,17 @@ function getRunOptions(args, parsed, session, configResolution = null) {
834
834
  llmConfig: screeningMode === "llm" && configResolution?.ok ? {
835
835
  ...configResolution.config
836
836
  } : null,
837
- llmTimeoutMs: parsePositiveInteger(args.llm_timeout_ms, slowLive ? 180000 : 120000),
838
- llmImageLimit: parsePositiveInteger(args.llm_image_limit, 8),
839
- llmImageDetail: normalizeText(args.llm_image_detail) || "high",
837
+ llmTimeoutMs: parsePositiveInteger(
838
+ args.llm_timeout_ms,
839
+ parsePositiveInteger(configResolution?.config?.llmTimeoutMs || configResolution?.config?.timeoutMs, slowLive ? 180000 : 120000)
840
+ ),
841
+ llmImageLimit: parsePositiveInteger(
842
+ args.llm_image_limit,
843
+ parsePositiveInteger(configResolution?.config?.llmImageLimit || configResolution?.config?.imageLimit, 8)
844
+ ),
845
+ llmImageDetail: normalizeText(
846
+ args.llm_image_detail || configResolution?.config?.llmImageDetail || configResolution?.config?.imageDetail
847
+ ) || "low",
840
848
  imageOutputDir: resolveBossConfiguredOutputDir("", getRecruitRunsDir()),
841
849
  name: "mcp-recruit-pipeline-run"
842
850
  };