@reconcrap/boss-recommend-mcp 1.3.30 → 1.3.32

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.
@@ -1,6 +1,7 @@
1
1
  import { mkdir } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
+ import { isDomProfileConsistentWithCard, NETWORK_RESUME_RETRY_WAIT_MS } from './services/resume-network.js';
4
5
  import { createCustomerAliases, createCustomerKey } from './utils/customer-key.js';
5
6
 
6
7
  function runToken(date = new Date()) {
@@ -17,37 +18,16 @@ function normalizeText(value) {
17
18
  return String(value || '').replace(/\s+/g, ' ').trim();
18
19
  }
19
20
 
20
- function sanitizeReasonWithResumeProfile(reason, resumeProfile) {
21
- const rawReason = normalizeText(reason);
22
- if (!rawReason) return rawReason;
23
- const schools = Array.isArray(resumeProfile?.schools)
24
- ? resumeProfile.schools.map((item) => normalizeText(item)).filter(Boolean)
25
- : [];
26
- const primarySchool = normalizeText(resumeProfile?.primarySchool || schools[0] || '');
27
- const schoolPool = primarySchool ? [primarySchool, ...schools] : schools;
28
- if (schoolPool.length <= 0) return rawReason;
29
-
30
- if (schoolPool.some((school) => rawReason.includes(school))) {
31
- return rawReason;
21
+ function toStringArray(value, maxItems = 8) {
22
+ if (!Array.isArray(value)) return [];
23
+ const normalized = [];
24
+ for (const item of value) {
25
+ const text = normalizeText(item);
26
+ if (!text) continue;
27
+ normalized.push(text);
28
+ if (normalized.length >= maxItems) break;
32
29
  }
33
- if (!/(大学|学院|院校|中科院|学校)/.test(rawReason)) {
34
- return rawReason;
35
- }
36
-
37
- const sentences = rawReason
38
- .split(/[。;;]+/)
39
- .map((item) => normalizeText(item))
40
- .filter(Boolean);
41
- const filtered = sentences.filter((sentence) => {
42
- if (!/(大学|学院|院校|中科院|学校)/.test(sentence)) return true;
43
- return schoolPool.some((school) => sentence.includes(school));
44
- });
45
-
46
- const prefix = `教育经历学校以简历主内容为准:${schoolPool[0]}`;
47
- if (filtered.length <= 0) {
48
- return `${prefix}。`;
49
- }
50
- return `${prefix}。${filtered.join(';')}。`;
30
+ return normalized;
51
31
  }
52
32
 
53
33
  function shouldContinue(summary, targetCount) {
@@ -75,6 +55,7 @@ export class BossChatApp {
75
55
  resumeCaptureService,
76
56
  stateStore,
77
57
  reportStore,
58
+ resumeNetworkTracker = null,
78
59
  runControl = null,
79
60
  logger = console,
80
61
  dryRun = false,
@@ -88,6 +69,7 @@ export class BossChatApp {
88
69
  this.resumeCaptureService = resumeCaptureService;
89
70
  this.stateStore = stateStore;
90
71
  this.reportStore = reportStore;
72
+ this.resumeNetworkTracker = resumeNetworkTracker;
91
73
  this.runControl = runControl;
92
74
  this.logger = logger;
93
75
  this.dryRun = dryRun;
@@ -145,6 +127,347 @@ export class BossChatApp {
145
127
  } catch {}
146
128
  }
147
129
 
130
+ buildCardProfile(customer = {}) {
131
+ return {
132
+ name: normalizeText(customer.name || ''),
133
+ school: normalizeText(customer.school || ''),
134
+ major: normalizeText(customer.major || ''),
135
+ company: normalizeText(customer.company || customer.lastCompany || customer.last_company || ''),
136
+ position: normalizeText(customer.position || customer.lastPosition || customer.last_position || ''),
137
+ };
138
+ }
139
+
140
+ buildResumeCandidateContext(customer = {}, candidateInfo = null) {
141
+ const info = candidateInfo && typeof candidateInfo === 'object' ? candidateInfo : {};
142
+ const schools = Array.isArray(info.schools) ? info.schools.map((item) => normalizeText(item)).filter(Boolean) : [];
143
+ const majors = Array.isArray(info.majors) ? info.majors.map((item) => normalizeText(item)).filter(Boolean) : [];
144
+ const primarySchool = normalizeText(info.school || info.primarySchool || schools[0] || '');
145
+ const primaryMajor = normalizeText(info.major || majors[0] || '');
146
+ const company = normalizeText(info.company || '');
147
+ const position = normalizeText(info.position || '');
148
+ const hasProfileContext = Boolean(primarySchool || primaryMajor || company || position || schools.length || majors.length);
149
+ return {
150
+ name: customer.name || info.name || '',
151
+ sourceJob: customer.sourceJob || '',
152
+ resumeProfile: hasProfileContext
153
+ ? {
154
+ primarySchool,
155
+ schools: schools.length > 0 ? schools : primarySchool ? [primarySchool] : [],
156
+ major: primaryMajor,
157
+ majors: majors.length > 0 ? majors : primaryMajor ? [primaryMajor] : [],
158
+ company,
159
+ position,
160
+ }
161
+ : null,
162
+ resumeText: String(info.resumeText || ''),
163
+ evidenceCorpus: String(info.evidenceCorpus || info.resumeText || ''),
164
+ };
165
+ }
166
+
167
+ async extractDomResumeCandidateInfo(customer = {}) {
168
+ if (typeof this.page.getResumeProfileFromDom !== 'function') {
169
+ return null;
170
+ }
171
+ const result = await this.page.getResumeProfileFromDom();
172
+ if (!result?.ok) {
173
+ return null;
174
+ }
175
+ const resumeText = normalizeText(result.resumeText || '');
176
+ if (!resumeText) {
177
+ return null;
178
+ }
179
+ return {
180
+ name: normalizeText(result.name || customer.name || ''),
181
+ school: normalizeText(result.primarySchool || ''),
182
+ schools: Array.isArray(result.schools) ? result.schools.map((item) => normalizeText(item)).filter(Boolean) : [],
183
+ major: normalizeText(result.major || ''),
184
+ majors: Array.isArray(result.majors) ? result.majors.map((item) => normalizeText(item)).filter(Boolean) : [],
185
+ company: normalizeText(result.company || ''),
186
+ position: normalizeText(result.position || ''),
187
+ resumeText: String(result.resumeText || ''),
188
+ evidenceCorpus: String(result.evidenceCorpus || result.resumeText || ''),
189
+ };
190
+ }
191
+
192
+ async retryCandidateResumeContext(customer = {}) {
193
+ if (typeof this.page.closeResumeModalDomOnce === 'function') {
194
+ try {
195
+ await this.page.closeResumeModalDomOnce();
196
+ } catch {}
197
+ }
198
+ await this.checkpoint();
199
+ if (typeof this.page.activateCandidate === 'function') {
200
+ await this.page.activateCandidate(customer, 0);
201
+ } else {
202
+ const rect = await this.page.centerCustomerCard(customer.domIndex, 0);
203
+ await this.interaction.clickRect(rect);
204
+ }
205
+ await this.interaction.sleepRange(520, 140);
206
+ await this.page.waitForCandidateActivated(customer, {
207
+ maxAttempts: 8,
208
+ delayMs: 180,
209
+ });
210
+ await this.page.waitForConversationReady({
211
+ maxAttempts: 8,
212
+ delayMs: 220,
213
+ });
214
+ const retryStartedAt = Date.now();
215
+ const openResult = await this.page.openOnlineResume();
216
+ await this.interaction.sleepRange(520, 140);
217
+ return {
218
+ retryStartedAt,
219
+ openResult,
220
+ };
221
+ }
222
+
223
+ async resolveDomResumeFallback(customer = {}, cardProfile = null) {
224
+ let domCandidateInfo = await this.extractDomResumeCandidateInfo(customer);
225
+ let networkCandidateInfo = null;
226
+ let acquisitionReason = domCandidateInfo?.resumeText ? 'dom_initial_hit' : '';
227
+
228
+ if (domCandidateInfo && !isDomProfileConsistentWithCard(cardProfile, domCandidateInfo)) {
229
+ this.logger.log(
230
+ `DOM简历疑似错位:expected=${cardProfile?.name || 'unknown'} | actual=${domCandidateInfo?.name || 'unknown'},尝试重试点击并短暂回查 network。`,
231
+ );
232
+ acquisitionReason = 'dom_profile_mismatch_retry';
233
+ try {
234
+ const retryContext = await this.retryCandidateResumeContext(customer);
235
+ if (this.resumeNetworkTracker) {
236
+ const retryNetwork = await this.resumeNetworkTracker.waitForNetworkResumeCandidateInfo(
237
+ customer,
238
+ NETWORK_RESUME_RETRY_WAIT_MS,
239
+ { minTs: retryContext.retryStartedAt },
240
+ );
241
+ if (retryNetwork?.candidateInfo?.resumeText) {
242
+ networkCandidateInfo = retryNetwork.candidateInfo;
243
+ acquisitionReason = 'dom_retry_network_recheck_hit';
244
+ domCandidateInfo = null;
245
+ }
246
+ }
247
+ if (!networkCandidateInfo) {
248
+ const retryDomCandidateInfo = await this.extractDomResumeCandidateInfo(customer);
249
+ if (retryDomCandidateInfo && isDomProfileConsistentWithCard(cardProfile, retryDomCandidateInfo)) {
250
+ domCandidateInfo = retryDomCandidateInfo;
251
+ acquisitionReason = 'dom_retry_hit';
252
+ } else {
253
+ domCandidateInfo = null;
254
+ acquisitionReason = 'dom_profile_mismatch_unresolved';
255
+ }
256
+ }
257
+ } catch (error) {
258
+ domCandidateInfo = null;
259
+ acquisitionReason = `dom_profile_retry_failed:${normalizeText(error?.message || error)}`;
260
+ }
261
+ }
262
+
263
+ return {
264
+ domCandidateInfo,
265
+ networkCandidateInfo,
266
+ acquisitionReason,
267
+ };
268
+ }
269
+
270
+ async acquireResumeAndEvaluate(customer, profile, artifactDir, baseResult) {
271
+ let modalOpened = false;
272
+ let capture = null;
273
+ let lastResumeError = null;
274
+ const timings = {
275
+ initialNetworkWaitMs: 0,
276
+ networkRetryMs: 0,
277
+ imageCaptureMs: 0,
278
+ imageModelMs: 0,
279
+ lateNetworkRetryMs: 0,
280
+ domFallbackMs: 0,
281
+ textModelMs: 0,
282
+ };
283
+ const cardProfile = this.buildCardProfile(customer);
284
+
285
+ await this.waitResumeOpenCooldown(this.resumeOpenCooldownMs + Math.floor(Math.random() * 200));
286
+ await this.checkpoint();
287
+ const acquisitionStartedAt = Date.now();
288
+ const openResult = await this.page.openOnlineResume();
289
+ let openDetected = openResult ? Boolean(openResult?.detectedOpen) : true;
290
+ this.lastResumeOpenAt = Date.now();
291
+ modalOpened = openDetected;
292
+ await this.interaction.sleepRange(600, 220);
293
+
294
+ const rateLimit =
295
+ typeof this.page.getResumeRateLimitWarning === 'function'
296
+ ? await this.page.getResumeRateLimitWarning()
297
+ : { hit: false, text: '' };
298
+ if (rateLimit?.hit) {
299
+ const backoffMs = 90000 + Math.floor(Math.random() * 30000);
300
+ this.setResumeOpenBlocked(backoffMs);
301
+ throw new Error(`RESUME_RATE_LIMIT_WARNING:${rateLimit.text}`);
302
+ }
303
+ if (openResult && !openDetected) {
304
+ let delayedDetected = false;
305
+ if (typeof this.page.getResumeModalState === 'function') {
306
+ await new Promise((resolve) => setTimeout(resolve, 1000));
307
+ const delayedState = await this.page.getResumeModalState();
308
+ delayedDetected =
309
+ Boolean(delayedState?.open) ||
310
+ Number(delayedState?.iframeCount || 0) > 0 ||
311
+ (Number(delayedState?.scopeCount || 0) > 0 && Number(delayedState?.closeCount || 0) > 0);
312
+ }
313
+ if (delayedDetected) {
314
+ openDetected = true;
315
+ modalOpened = true;
316
+ this.logger.log('在线简历首次检测未命中,1秒后复检已打开,继续处理。');
317
+ } else {
318
+ throw new Error('RESUME_MODAL_NOT_DETECTED_AFTER_SINGLE_DOM_CLICK');
319
+ }
320
+ }
321
+ if (!openDetected) {
322
+ throw new Error('RESUME_MODAL_NOT_DETECTED');
323
+ }
324
+
325
+ let networkResult = null;
326
+ if (this.resumeNetworkTracker) {
327
+ networkResult = await this.resumeNetworkTracker.waitForResumeNetworkByMode(customer, {
328
+ minTs: acquisitionStartedAt,
329
+ });
330
+ timings.initialNetworkWaitMs = Number(networkResult?.initialWaitMs || 0);
331
+ timings.networkRetryMs = Number(networkResult?.retryWaitMs || 0);
332
+ }
333
+
334
+ if (networkResult?.candidateInfo?.resumeText) {
335
+ await this.checkpoint();
336
+ const evaluationStartedAt = Date.now();
337
+ const evaluation = await this.llmClient.evaluateResume({
338
+ screeningCriteria: profile.screeningCriteria,
339
+ candidate: this.buildResumeCandidateContext(customer, networkResult.candidateInfo),
340
+ });
341
+ timings.textModelMs = Date.now() - evaluationStartedAt;
342
+ return {
343
+ modalOpened,
344
+ capture,
345
+ evaluation,
346
+ timings,
347
+ acquisitionMode: 'network',
348
+ acquisitionReason: networkResult.acquisitionReason || 'initial_network_hit',
349
+ sourceCandidateInfo: networkResult.candidateInfo,
350
+ };
351
+ }
352
+
353
+ try {
354
+ await this.checkpoint();
355
+ const captureStartedAt = Date.now();
356
+ capture = await this.resumeCaptureService.captureResume({
357
+ artifactDir,
358
+ waitResumeMs: 30000,
359
+ scrollSettleMs: 500,
360
+ });
361
+ timings.imageCaptureMs = Date.now() - captureStartedAt;
362
+ if (capture?.quality?.likelyBlank) {
363
+ const blankBackoffMs = 45000 + Math.floor(Math.random() * 20000);
364
+ this.setResumeOpenBlocked(blankBackoffMs);
365
+ throw new Error('RESUME_CAPTURE_LIKELY_BLANK');
366
+ }
367
+ const modelImagePaths = Array.isArray(capture.modelImagePaths)
368
+ ? capture.modelImagePaths.map((item) => String(item || '').trim()).filter(Boolean)
369
+ : [];
370
+ this.logger.log(`截图完成:chunks=${capture.chunkCount} | modelImages=${modelImagePaths.length}`);
371
+ baseResult.artifacts.chunkDir = capture.chunkDir;
372
+ baseResult.artifacts.metadataFile = capture.metadataFile;
373
+ baseResult.artifacts.stitchedImage = capture.stitchedImage;
374
+ baseResult.artifacts.chunkCount = capture.chunkCount;
375
+ baseResult.artifacts.modelImagePaths = modelImagePaths;
376
+
377
+ if (this.resumeNetworkTracker) {
378
+ this.resumeNetworkTracker.setResumeAcquisitionMode('image', 'image_capture_success');
379
+ }
380
+
381
+ await this.checkpoint();
382
+ const imageEvalStartedAt = Date.now();
383
+ const evaluation = await this.llmClient.evaluateResume({
384
+ screeningCriteria: profile.screeningCriteria,
385
+ candidate: this.buildResumeCandidateContext(customer, null),
386
+ imagePaths: modelImagePaths,
387
+ });
388
+ timings.imageModelMs = Date.now() - imageEvalStartedAt;
389
+ return {
390
+ modalOpened,
391
+ capture,
392
+ evaluation,
393
+ timings,
394
+ acquisitionMode: 'image_fallback',
395
+ acquisitionReason: 'image_capture_success',
396
+ sourceCandidateInfo: null,
397
+ };
398
+ } catch (imageError) {
399
+ lastResumeError = imageError;
400
+ }
401
+
402
+ let lateNetworkResult = null;
403
+ if (this.resumeNetworkTracker) {
404
+ lateNetworkResult = await this.resumeNetworkTracker.waitForLateNetworkResumeCandidateInfo(customer, {
405
+ minTs: acquisitionStartedAt,
406
+ });
407
+ timings.lateNetworkRetryMs = Number(lateNetworkResult?.lateRetryMs || 0);
408
+ }
409
+ if (lateNetworkResult?.candidateInfo?.resumeText) {
410
+ await this.checkpoint();
411
+ const evaluationStartedAt = Date.now();
412
+ const evaluation = await this.llmClient.evaluateResume({
413
+ screeningCriteria: profile.screeningCriteria,
414
+ candidate: this.buildResumeCandidateContext(customer, lateNetworkResult.candidateInfo),
415
+ });
416
+ timings.textModelMs = Date.now() - evaluationStartedAt;
417
+ return {
418
+ modalOpened,
419
+ capture,
420
+ evaluation,
421
+ timings,
422
+ acquisitionMode: 'network',
423
+ acquisitionReason: lateNetworkResult.acquisitionReason || 'late_network_hit',
424
+ sourceCandidateInfo: lateNetworkResult.candidateInfo,
425
+ };
426
+ }
427
+
428
+ const domStartedAt = Date.now();
429
+ const domFallback = await this.resolveDomResumeFallback(customer, cardProfile);
430
+ timings.domFallbackMs = Date.now() - domStartedAt;
431
+ if (domFallback?.networkCandidateInfo?.resumeText) {
432
+ await this.checkpoint();
433
+ const evaluationStartedAt = Date.now();
434
+ const evaluation = await this.llmClient.evaluateResume({
435
+ screeningCriteria: profile.screeningCriteria,
436
+ candidate: this.buildResumeCandidateContext(customer, domFallback.networkCandidateInfo),
437
+ });
438
+ timings.textModelMs = Date.now() - evaluationStartedAt;
439
+ return {
440
+ modalOpened,
441
+ capture,
442
+ evaluation,
443
+ timings,
444
+ acquisitionMode: 'network',
445
+ acquisitionReason: domFallback.acquisitionReason || 'dom_retry_network_recheck_hit',
446
+ sourceCandidateInfo: domFallback.networkCandidateInfo,
447
+ };
448
+ }
449
+ if (domFallback?.domCandidateInfo?.resumeText) {
450
+ await this.checkpoint();
451
+ const evaluationStartedAt = Date.now();
452
+ const evaluation = await this.llmClient.evaluateResume({
453
+ screeningCriteria: profile.screeningCriteria,
454
+ candidate: this.buildResumeCandidateContext(customer, domFallback.domCandidateInfo),
455
+ });
456
+ timings.textModelMs = Date.now() - evaluationStartedAt;
457
+ return {
458
+ modalOpened,
459
+ capture,
460
+ evaluation,
461
+ timings,
462
+ acquisitionMode: 'dom_fallback',
463
+ acquisitionReason: domFallback.acquisitionReason || 'dom_initial_hit',
464
+ sourceCandidateInfo: domFallback.domCandidateInfo,
465
+ };
466
+ }
467
+
468
+ throw lastResumeError || new Error('DOM_RESUME_FALLBACK_FAILED');
469
+ }
470
+
148
471
  async restoreListContext(profile) {
149
472
  if (typeof this.page.activatePrimaryChatLabel === 'function') {
150
473
  await this.page.activatePrimaryChatLabel('全部');
@@ -744,165 +1067,78 @@ export class BossChatApp {
744
1067
  const artifactDir = path.join(this.artifactRootDir, runId, candidateToken);
745
1068
  await mkdir(artifactDir, { recursive: true });
746
1069
 
747
- let capture = null;
748
- let lastResumeError = null;
749
- let resumeProfile = null;
750
- await this.waitResumeOpenCooldown(this.resumeOpenCooldownMs + Math.floor(Math.random() * 200));
751
- await this.checkpoint();
752
- const openResult = await this.page.openOnlineResume();
753
- let openDetected = openResult ? Boolean(openResult?.detectedOpen) : true;
754
- this.lastResumeOpenAt = Date.now();
755
- modalOpened = openDetected;
756
- await this.interaction.sleepRange(600, 220);
757
- const rateLimit =
758
- typeof this.page.getResumeRateLimitWarning === 'function'
759
- ? await this.page.getResumeRateLimitWarning()
760
- : { hit: false, text: '' };
761
- if (rateLimit?.hit) {
762
- const backoffMs = 90000 + Math.floor(Math.random() * 30000);
763
- this.setResumeOpenBlocked(backoffMs);
764
- this.logger.log(
765
- `检测到简历查看频控提示:${rateLimit.text},进入冷却 ${Math.round(backoffMs / 1000)}s,当前候选跳过。`,
766
- );
767
- lastResumeError = new Error(`RESUME_RATE_LIMIT_WARNING:${rateLimit.text}`);
768
- } else if (openResult && !openDetected) {
769
- let delayedDetected = false;
770
- if (typeof this.page.getResumeModalState === 'function') {
771
- await new Promise((resolve) => setTimeout(resolve, 1000));
772
- const delayedState = await this.page.getResumeModalState();
773
- delayedDetected =
774
- Boolean(delayedState?.open) ||
775
- Number(delayedState?.iframeCount || 0) > 0 ||
776
- (Number(delayedState?.scopeCount || 0) > 0 &&
777
- Number(delayedState?.closeCount || 0) > 0);
778
- }
779
- if (delayedDetected) {
780
- openDetected = true;
781
- modalOpened = true;
782
- this.logger.log('在线简历首次检测未命中,1秒后复检已打开,继续处理。');
783
- } else {
784
- lastResumeError = new Error('RESUME_MODAL_NOT_DETECTED_AFTER_SINGLE_DOM_CLICK');
785
- }
786
- }
787
-
788
- if (!lastResumeError && openDetected) {
789
- if (typeof this.page.getResumeProfileFromDom === 'function') {
790
- resumeProfile = await this.page.getResumeProfileFromDom();
791
- if (resumeProfile?.ok) {
792
- this.logger.log(
793
- `简历结构化信息:school=${resumeProfile.primarySchool || 'n/a'} | major=${resumeProfile.major || 'n/a'} | company=${resumeProfile.company || 'n/a'} | position=${resumeProfile.position || 'n/a'}`,
794
- );
795
- baseResult.artifacts.resumeProfile = {
796
- primarySchool: resumeProfile.primarySchool || '',
797
- schools: Array.isArray(resumeProfile.schools) ? resumeProfile.schools : [],
798
- major: resumeProfile.major || '',
799
- majors: Array.isArray(resumeProfile.majors) ? resumeProfile.majors : [],
800
- company: resumeProfile.company || '',
801
- position: resumeProfile.position || '',
802
- resumeTextLength: String(resumeProfile.resumeText || '').length,
803
- evidenceCorpusLength: String(resumeProfile.evidenceCorpus || '').length,
804
- };
805
- } else {
806
- this.logger.log(`简历结构化提取未命中:${resumeProfile?.error || 'unknown'}`);
807
- }
808
- }
809
- this.logger.log(
810
- `在线简历点击完成:clicked=${Boolean(openResult?.clicked)} | detectedOpen=${openDetected} | by=${openResult?.by || 'unknown'},开始截图探测与拼接。`,
811
- );
812
- this.logger.log(
813
- `在线简历截图前状态:modalOpened=${modalOpened} | openDetected=${openDetected}`,
814
- );
815
- try {
816
- await this.checkpoint();
817
- capture = await this.resumeCaptureService.captureResume({
818
- artifactDir,
819
- waitResumeMs: 30000,
820
- scrollSettleMs: 500,
821
- });
822
- if (capture?.quality?.likelyBlank) {
823
- const blankBackoffMs = 45000 + Math.floor(Math.random() * 20000);
824
- this.setResumeOpenBlocked(blankBackoffMs);
825
- this.logger.log(
826
- `检测到疑似空白简历截图(luma=${capture?.quality?.luma},std=${capture?.quality?.avgStd}),冷却 ${Math.round(blankBackoffMs / 1000)}s,当前候选跳过。`,
827
- );
828
- lastResumeError = new Error('RESUME_CAPTURE_LIKELY_BLANK');
829
- capture = null;
830
- }
831
- } catch (error) {
832
- lastResumeError = error;
833
- }
834
- } else if (!lastResumeError && !openDetected) {
835
- lastResumeError = new Error('RESUME_MODAL_NOT_DETECTED');
836
- }
837
- if (!capture) {
838
- throw lastResumeError || new Error('RESUME_CAPTURE_FAILED');
839
- }
840
- this.logger.log(
841
- `截图完成:chunks=${capture.chunkCount} | image=${capture.stitchedImage}`,
1070
+ const acquisition = await this.acquireResumeAndEvaluate(
1071
+ customer,
1072
+ profile,
1073
+ artifactDir,
1074
+ baseResult,
842
1075
  );
843
- baseResult.artifacts = {
844
- chunkDir: capture.chunkDir,
845
- metadataFile: capture.metadataFile,
846
- stitchedImage: capture.stitchedImage,
847
- chunkCount: capture.chunkCount,
848
- };
849
-
850
- await this.checkpoint();
851
- const evaluation = await this.llmClient.evaluateResume({
852
- screeningCriteria: profile.screeningCriteria,
853
- candidate: {
854
- name: customer.name || '',
855
- sourceJob: customer.sourceJob || '',
856
- resumeProfile: resumeProfile?.ok ? {
857
- primarySchool: resumeProfile.primarySchool || '',
858
- schools: Array.isArray(resumeProfile.schools) ? resumeProfile.schools : [],
859
- major: resumeProfile.major || '',
860
- majors: Array.isArray(resumeProfile.majors) ? resumeProfile.majors : [],
861
- company: resumeProfile.company || '',
862
- position: resumeProfile.position || '',
863
- } : null,
864
- resumeText: resumeProfile?.ok ? String(resumeProfile.resumeText || '') : '',
865
- evidenceCorpus: resumeProfile?.ok ? String(resumeProfile.evidenceCorpus || '') : '',
866
- },
867
- imagePath: capture.stitchedImage,
868
- });
869
- const finalReason = sanitizeReasonWithResumeProfile(evaluation.reason, resumeProfile);
870
- if (finalReason !== evaluation.reason) {
871
- this.logger.log(
872
- `评估理由学校字段已按主简历纠偏:rawReason=${evaluation.reason} | finalReason=${finalReason}`,
873
- );
874
- }
875
- if (evaluation.evidenceGateDemoted === true) {
876
- this.logger.log(
877
- `证据闸门降级:rawPassed=${Boolean(evaluation.rawPassed)} | evidenceRawCount=${Number(evaluation.evidenceRawCount || 0)} | evidenceMatchedCount=${Number(evaluation.evidenceMatchedCount || 0)} | mode=${evaluation.evaluationMode || 'unknown'}`,
878
- );
879
- }
1076
+ const evaluation = acquisition.evaluation;
1077
+ const capture = acquisition.capture;
1078
+ modalOpened = Boolean(acquisition.modalOpened);
1079
+ const finalReason =
1080
+ normalizeText(evaluation.reason || evaluation.summary || evaluation.cot) ||
1081
+ (evaluation.passed ? 'LLM判定通过' : 'LLM判定不通过');
880
1082
  this.logger.log(
881
- `LLM评估完成:passed=${evaluation.passed} | rawPassed=${Boolean(evaluation.rawPassed)} | mode=${evaluation.evaluationMode || 'unknown'} | reason=${finalReason}`,
1083
+ `LLM评估完成:passed=${evaluation.passed} | source=${acquisition.acquisitionMode} | reason=${acquisition.acquisitionReason || 'n/a'} | mode=${evaluation.evaluationMode || 'unknown'} | imageCount=${Number(evaluation.imageCount || baseResult.artifacts.modelImagePaths?.length || 0)} | result=${normalizeText(evaluation.rawOutputText || '') || 'n/a'}`,
882
1084
  );
883
1085
 
884
1086
  baseResult.reason = finalReason;
885
1087
  baseResult.passed = evaluation.passed;
886
1088
  baseResult.decision = evaluation.passed ? 'passed' : 'skipped';
887
- baseResult.artifacts.rawPassed = Boolean(evaluation.rawPassed);
888
1089
  baseResult.artifacts.finalPassed = Boolean(evaluation.passed);
889
- baseResult.artifacts.evidenceRawCount = Number.isFinite(Number(evaluation.evidenceRawCount))
890
- ? Number(evaluation.evidenceRawCount)
891
- : 0;
892
- baseResult.artifacts.evidenceMatchedCount = Number.isFinite(Number(evaluation.evidenceMatchedCount))
893
- ? Number(evaluation.evidenceMatchedCount)
894
- : 0;
895
- baseResult.artifacts.evidenceGateDemoted = evaluation.evidenceGateDemoted === true;
896
1090
  baseResult.artifacts.evaluationMode = String(evaluation.evaluationMode || '');
1091
+ baseResult.artifacts.evaluationImageCount = Number.isFinite(Number(evaluation.imageCount))
1092
+ ? Number(evaluation.imageCount)
1093
+ : Array.isArray(baseResult.artifacts.modelImagePaths)
1094
+ ? baseResult.artifacts.modelImagePaths.length
1095
+ : 0;
897
1096
  baseResult.artifacts.evaluationChunkIndex = Number.isFinite(Number(evaluation.chunkIndex))
898
1097
  ? Number(evaluation.chunkIndex)
899
1098
  : null;
900
1099
  baseResult.artifacts.evaluationChunkTotal = Number.isFinite(Number(evaluation.chunkTotal))
901
1100
  ? Number(evaluation.chunkTotal)
902
1101
  : null;
903
- baseResult.artifacts.evaluationEvidence = Array.isArray(evaluation.evidence)
904
- ? evaluation.evidence.slice(0, 5).map((item) => String(item || '').trim()).filter(Boolean)
905
- : [];
1102
+ baseResult.artifacts.llmReason = normalizeText(evaluation.reason || '');
1103
+ baseResult.artifacts.llmSummary = normalizeText(evaluation.summary || '');
1104
+ baseResult.artifacts.llmCot = normalizeText(evaluation.cot || '');
1105
+ baseResult.artifacts.llmEvidence = toStringArray(evaluation.evidence);
1106
+ baseResult.artifacts.llmRawReasoning = String(evaluation.rawReasoningText || '');
1107
+ baseResult.artifacts.llmRawOutput = String(evaluation.rawOutputText || '');
1108
+ baseResult.artifacts.resumeAcquisitionMode = String(acquisition.acquisitionMode || '');
1109
+ baseResult.artifacts.resumeAcquisitionReason = String(acquisition.acquisitionReason || '');
1110
+ baseResult.artifacts.initialNetworkWaitMs = Number(acquisition.timings?.initialNetworkWaitMs || 0);
1111
+ baseResult.artifacts.networkRetryMs = Number(acquisition.timings?.networkRetryMs || 0);
1112
+ baseResult.artifacts.imageCaptureMs = Number(acquisition.timings?.imageCaptureMs || 0);
1113
+ baseResult.artifacts.imageModelMs = Number(acquisition.timings?.imageModelMs || 0);
1114
+ baseResult.artifacts.lateNetworkRetryMs = Number(acquisition.timings?.lateNetworkRetryMs || 0);
1115
+ baseResult.artifacts.domFallbackMs = Number(acquisition.timings?.domFallbackMs || 0);
1116
+ baseResult.artifacts.textModelMs = Number(acquisition.timings?.textModelMs || 0);
1117
+ if (acquisition.sourceCandidateInfo) {
1118
+ baseResult.artifacts.resumeProfile = {
1119
+ primarySchool: normalizeText(acquisition.sourceCandidateInfo.school || ''),
1120
+ schools: Array.isArray(acquisition.sourceCandidateInfo.schools)
1121
+ ? acquisition.sourceCandidateInfo.schools
1122
+ : [],
1123
+ major: normalizeText(acquisition.sourceCandidateInfo.major || ''),
1124
+ majors: Array.isArray(acquisition.sourceCandidateInfo.majors)
1125
+ ? acquisition.sourceCandidateInfo.majors
1126
+ : [],
1127
+ company: normalizeText(acquisition.sourceCandidateInfo.company || ''),
1128
+ position: normalizeText(acquisition.sourceCandidateInfo.position || ''),
1129
+ resumeTextLength: String(acquisition.sourceCandidateInfo.resumeText || '').length,
1130
+ evidenceCorpusLength: String(
1131
+ acquisition.sourceCandidateInfo.evidenceCorpus || acquisition.sourceCandidateInfo.resumeText || '',
1132
+ ).length,
1133
+ };
1134
+ }
1135
+ if (this.resumeNetworkTracker) {
1136
+ baseResult.artifacts.resumeNetworkMode = this.resumeNetworkTracker.getResumeAcquisitionState().mode;
1137
+ baseResult.artifacts.resumeNetworkModeReason =
1138
+ this.resumeNetworkTracker.getResumeAcquisitionState().reason;
1139
+ baseResult.artifacts.resumeNetworkDiagnostics =
1140
+ this.resumeNetworkTracker.resumeNetworkDiagnostics.slice(-12);
1141
+ }
906
1142
 
907
1143
  await this.checkpoint();
908
1144
  const closeResult =
@@ -1069,7 +1305,7 @@ export class BossChatApp {
1069
1305
 
1070
1306
  const message = error.message || String(error);
1071
1307
  if (
1072
- /ONLINE_RESUME_UNAVAILABLE|ONLINE_RESUME_BUTTON_NOT_FOUND|OPEN_ONLINE_RESUME_FAILED|NO_RESUME_IFRAME|NO_SCROLL_CONTAINER|RESUME_MODAL_OPEN_TIMEOUT|Resume context probe timeout: reason=NO_RESUME_IFRAME|RESUME_RATE_LIMIT_WARNING|RESUME_CAPTURE_LIKELY_BLANK/i.test(
1308
+ /ONLINE_RESUME_UNAVAILABLE|ONLINE_RESUME_BUTTON_NOT_FOUND|OPEN_ONLINE_RESUME_FAILED|NO_RESUME_IFRAME|NO_SCROLL_CONTAINER|RESUME_MODAL_OPEN_TIMEOUT|Resume context probe timeout: reason=NO_RESUME_IFRAME|RESUME_RATE_LIMIT_WARNING|RESUME_CAPTURE_LIKELY_BLANK|DOM_RESUME_FALLBACK_FAILED|RESUME_MODAL_NOT_DETECTED/i.test(
1073
1309
  message,
1074
1310
  )
1075
1311
  ) {