@reconcrap/boss-recommend-mcp 1.3.30 → 1.3.31

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,39 +18,6 @@ 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;
32
- }
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(';')}。`;
51
- }
52
-
53
21
  function shouldContinue(summary, targetCount) {
54
22
  if (!targetCount || !Number.isFinite(targetCount) || targetCount <= 0) {
55
23
  return true;
@@ -75,6 +43,7 @@ export class BossChatApp {
75
43
  resumeCaptureService,
76
44
  stateStore,
77
45
  reportStore,
46
+ resumeNetworkTracker = null,
78
47
  runControl = null,
79
48
  logger = console,
80
49
  dryRun = false,
@@ -88,6 +57,7 @@ export class BossChatApp {
88
57
  this.resumeCaptureService = resumeCaptureService;
89
58
  this.stateStore = stateStore;
90
59
  this.reportStore = reportStore;
60
+ this.resumeNetworkTracker = resumeNetworkTracker;
91
61
  this.runControl = runControl;
92
62
  this.logger = logger;
93
63
  this.dryRun = dryRun;
@@ -145,6 +115,347 @@ export class BossChatApp {
145
115
  } catch {}
146
116
  }
147
117
 
118
+ buildCardProfile(customer = {}) {
119
+ return {
120
+ name: normalizeText(customer.name || ''),
121
+ school: normalizeText(customer.school || ''),
122
+ major: normalizeText(customer.major || ''),
123
+ company: normalizeText(customer.company || customer.lastCompany || customer.last_company || ''),
124
+ position: normalizeText(customer.position || customer.lastPosition || customer.last_position || ''),
125
+ };
126
+ }
127
+
128
+ buildResumeCandidateContext(customer = {}, candidateInfo = null) {
129
+ const info = candidateInfo && typeof candidateInfo === 'object' ? candidateInfo : {};
130
+ const schools = Array.isArray(info.schools) ? info.schools.map((item) => normalizeText(item)).filter(Boolean) : [];
131
+ const majors = Array.isArray(info.majors) ? info.majors.map((item) => normalizeText(item)).filter(Boolean) : [];
132
+ const primarySchool = normalizeText(info.school || info.primarySchool || schools[0] || '');
133
+ const primaryMajor = normalizeText(info.major || majors[0] || '');
134
+ const company = normalizeText(info.company || '');
135
+ const position = normalizeText(info.position || '');
136
+ const hasProfileContext = Boolean(primarySchool || primaryMajor || company || position || schools.length || majors.length);
137
+ return {
138
+ name: customer.name || info.name || '',
139
+ sourceJob: customer.sourceJob || '',
140
+ resumeProfile: hasProfileContext
141
+ ? {
142
+ primarySchool,
143
+ schools: schools.length > 0 ? schools : primarySchool ? [primarySchool] : [],
144
+ major: primaryMajor,
145
+ majors: majors.length > 0 ? majors : primaryMajor ? [primaryMajor] : [],
146
+ company,
147
+ position,
148
+ }
149
+ : null,
150
+ resumeText: String(info.resumeText || ''),
151
+ evidenceCorpus: String(info.evidenceCorpus || info.resumeText || ''),
152
+ };
153
+ }
154
+
155
+ async extractDomResumeCandidateInfo(customer = {}) {
156
+ if (typeof this.page.getResumeProfileFromDom !== 'function') {
157
+ return null;
158
+ }
159
+ const result = await this.page.getResumeProfileFromDom();
160
+ if (!result?.ok) {
161
+ return null;
162
+ }
163
+ const resumeText = normalizeText(result.resumeText || '');
164
+ if (!resumeText) {
165
+ return null;
166
+ }
167
+ return {
168
+ name: normalizeText(result.name || customer.name || ''),
169
+ school: normalizeText(result.primarySchool || ''),
170
+ schools: Array.isArray(result.schools) ? result.schools.map((item) => normalizeText(item)).filter(Boolean) : [],
171
+ major: normalizeText(result.major || ''),
172
+ majors: Array.isArray(result.majors) ? result.majors.map((item) => normalizeText(item)).filter(Boolean) : [],
173
+ company: normalizeText(result.company || ''),
174
+ position: normalizeText(result.position || ''),
175
+ resumeText: String(result.resumeText || ''),
176
+ evidenceCorpus: String(result.evidenceCorpus || result.resumeText || ''),
177
+ };
178
+ }
179
+
180
+ async retryCandidateResumeContext(customer = {}) {
181
+ if (typeof this.page.closeResumeModalDomOnce === 'function') {
182
+ try {
183
+ await this.page.closeResumeModalDomOnce();
184
+ } catch {}
185
+ }
186
+ await this.checkpoint();
187
+ if (typeof this.page.activateCandidate === 'function') {
188
+ await this.page.activateCandidate(customer, 0);
189
+ } else {
190
+ const rect = await this.page.centerCustomerCard(customer.domIndex, 0);
191
+ await this.interaction.clickRect(rect);
192
+ }
193
+ await this.interaction.sleepRange(520, 140);
194
+ await this.page.waitForCandidateActivated(customer, {
195
+ maxAttempts: 8,
196
+ delayMs: 180,
197
+ });
198
+ await this.page.waitForConversationReady({
199
+ maxAttempts: 8,
200
+ delayMs: 220,
201
+ });
202
+ const retryStartedAt = Date.now();
203
+ const openResult = await this.page.openOnlineResume();
204
+ await this.interaction.sleepRange(520, 140);
205
+ return {
206
+ retryStartedAt,
207
+ openResult,
208
+ };
209
+ }
210
+
211
+ async resolveDomResumeFallback(customer = {}, cardProfile = null) {
212
+ let domCandidateInfo = await this.extractDomResumeCandidateInfo(customer);
213
+ let networkCandidateInfo = null;
214
+ let acquisitionReason = domCandidateInfo?.resumeText ? 'dom_initial_hit' : '';
215
+
216
+ if (domCandidateInfo && !isDomProfileConsistentWithCard(cardProfile, domCandidateInfo)) {
217
+ this.logger.log(
218
+ `DOM简历疑似错位:expected=${cardProfile?.name || 'unknown'} | actual=${domCandidateInfo?.name || 'unknown'},尝试重试点击并短暂回查 network。`,
219
+ );
220
+ acquisitionReason = 'dom_profile_mismatch_retry';
221
+ try {
222
+ const retryContext = await this.retryCandidateResumeContext(customer);
223
+ if (this.resumeNetworkTracker) {
224
+ const retryNetwork = await this.resumeNetworkTracker.waitForNetworkResumeCandidateInfo(
225
+ customer,
226
+ NETWORK_RESUME_RETRY_WAIT_MS,
227
+ { minTs: retryContext.retryStartedAt },
228
+ );
229
+ if (retryNetwork?.candidateInfo?.resumeText) {
230
+ networkCandidateInfo = retryNetwork.candidateInfo;
231
+ acquisitionReason = 'dom_retry_network_recheck_hit';
232
+ domCandidateInfo = null;
233
+ }
234
+ }
235
+ if (!networkCandidateInfo) {
236
+ const retryDomCandidateInfo = await this.extractDomResumeCandidateInfo(customer);
237
+ if (retryDomCandidateInfo && isDomProfileConsistentWithCard(cardProfile, retryDomCandidateInfo)) {
238
+ domCandidateInfo = retryDomCandidateInfo;
239
+ acquisitionReason = 'dom_retry_hit';
240
+ } else {
241
+ domCandidateInfo = null;
242
+ acquisitionReason = 'dom_profile_mismatch_unresolved';
243
+ }
244
+ }
245
+ } catch (error) {
246
+ domCandidateInfo = null;
247
+ acquisitionReason = `dom_profile_retry_failed:${normalizeText(error?.message || error)}`;
248
+ }
249
+ }
250
+
251
+ return {
252
+ domCandidateInfo,
253
+ networkCandidateInfo,
254
+ acquisitionReason,
255
+ };
256
+ }
257
+
258
+ async acquireResumeAndEvaluate(customer, profile, artifactDir, baseResult) {
259
+ let modalOpened = false;
260
+ let capture = null;
261
+ let lastResumeError = null;
262
+ const timings = {
263
+ initialNetworkWaitMs: 0,
264
+ networkRetryMs: 0,
265
+ imageCaptureMs: 0,
266
+ imageModelMs: 0,
267
+ lateNetworkRetryMs: 0,
268
+ domFallbackMs: 0,
269
+ textModelMs: 0,
270
+ };
271
+ const cardProfile = this.buildCardProfile(customer);
272
+
273
+ await this.waitResumeOpenCooldown(this.resumeOpenCooldownMs + Math.floor(Math.random() * 200));
274
+ await this.checkpoint();
275
+ const acquisitionStartedAt = Date.now();
276
+ const openResult = await this.page.openOnlineResume();
277
+ let openDetected = openResult ? Boolean(openResult?.detectedOpen) : true;
278
+ this.lastResumeOpenAt = Date.now();
279
+ modalOpened = openDetected;
280
+ await this.interaction.sleepRange(600, 220);
281
+
282
+ const rateLimit =
283
+ typeof this.page.getResumeRateLimitWarning === 'function'
284
+ ? await this.page.getResumeRateLimitWarning()
285
+ : { hit: false, text: '' };
286
+ if (rateLimit?.hit) {
287
+ const backoffMs = 90000 + Math.floor(Math.random() * 30000);
288
+ this.setResumeOpenBlocked(backoffMs);
289
+ throw new Error(`RESUME_RATE_LIMIT_WARNING:${rateLimit.text}`);
290
+ }
291
+ if (openResult && !openDetected) {
292
+ let delayedDetected = false;
293
+ if (typeof this.page.getResumeModalState === 'function') {
294
+ await new Promise((resolve) => setTimeout(resolve, 1000));
295
+ const delayedState = await this.page.getResumeModalState();
296
+ delayedDetected =
297
+ Boolean(delayedState?.open) ||
298
+ Number(delayedState?.iframeCount || 0) > 0 ||
299
+ (Number(delayedState?.scopeCount || 0) > 0 && Number(delayedState?.closeCount || 0) > 0);
300
+ }
301
+ if (delayedDetected) {
302
+ openDetected = true;
303
+ modalOpened = true;
304
+ this.logger.log('在线简历首次检测未命中,1秒后复检已打开,继续处理。');
305
+ } else {
306
+ throw new Error('RESUME_MODAL_NOT_DETECTED_AFTER_SINGLE_DOM_CLICK');
307
+ }
308
+ }
309
+ if (!openDetected) {
310
+ throw new Error('RESUME_MODAL_NOT_DETECTED');
311
+ }
312
+
313
+ let networkResult = null;
314
+ if (this.resumeNetworkTracker) {
315
+ networkResult = await this.resumeNetworkTracker.waitForResumeNetworkByMode(customer, {
316
+ minTs: acquisitionStartedAt,
317
+ });
318
+ timings.initialNetworkWaitMs = Number(networkResult?.initialWaitMs || 0);
319
+ timings.networkRetryMs = Number(networkResult?.retryWaitMs || 0);
320
+ }
321
+
322
+ if (networkResult?.candidateInfo?.resumeText) {
323
+ await this.checkpoint();
324
+ const evaluationStartedAt = Date.now();
325
+ const evaluation = await this.llmClient.evaluateResume({
326
+ screeningCriteria: profile.screeningCriteria,
327
+ candidate: this.buildResumeCandidateContext(customer, networkResult.candidateInfo),
328
+ });
329
+ timings.textModelMs = Date.now() - evaluationStartedAt;
330
+ return {
331
+ modalOpened,
332
+ capture,
333
+ evaluation,
334
+ timings,
335
+ acquisitionMode: 'network',
336
+ acquisitionReason: networkResult.acquisitionReason || 'initial_network_hit',
337
+ sourceCandidateInfo: networkResult.candidateInfo,
338
+ };
339
+ }
340
+
341
+ try {
342
+ await this.checkpoint();
343
+ const captureStartedAt = Date.now();
344
+ capture = await this.resumeCaptureService.captureResume({
345
+ artifactDir,
346
+ waitResumeMs: 30000,
347
+ scrollSettleMs: 500,
348
+ });
349
+ timings.imageCaptureMs = Date.now() - captureStartedAt;
350
+ if (capture?.quality?.likelyBlank) {
351
+ const blankBackoffMs = 45000 + Math.floor(Math.random() * 20000);
352
+ this.setResumeOpenBlocked(blankBackoffMs);
353
+ throw new Error('RESUME_CAPTURE_LIKELY_BLANK');
354
+ }
355
+ const modelImagePaths = Array.isArray(capture.modelImagePaths)
356
+ ? capture.modelImagePaths.map((item) => String(item || '').trim()).filter(Boolean)
357
+ : [];
358
+ this.logger.log(`截图完成:chunks=${capture.chunkCount} | modelImages=${modelImagePaths.length}`);
359
+ baseResult.artifacts.chunkDir = capture.chunkDir;
360
+ baseResult.artifacts.metadataFile = capture.metadataFile;
361
+ baseResult.artifacts.stitchedImage = capture.stitchedImage;
362
+ baseResult.artifacts.chunkCount = capture.chunkCount;
363
+ baseResult.artifacts.modelImagePaths = modelImagePaths;
364
+
365
+ if (this.resumeNetworkTracker) {
366
+ this.resumeNetworkTracker.setResumeAcquisitionMode('image', 'image_capture_success');
367
+ }
368
+
369
+ await this.checkpoint();
370
+ const imageEvalStartedAt = Date.now();
371
+ const evaluation = await this.llmClient.evaluateResume({
372
+ screeningCriteria: profile.screeningCriteria,
373
+ candidate: this.buildResumeCandidateContext(customer, null),
374
+ imagePaths: modelImagePaths,
375
+ });
376
+ timings.imageModelMs = Date.now() - imageEvalStartedAt;
377
+ return {
378
+ modalOpened,
379
+ capture,
380
+ evaluation,
381
+ timings,
382
+ acquisitionMode: 'image_fallback',
383
+ acquisitionReason: 'image_capture_success',
384
+ sourceCandidateInfo: null,
385
+ };
386
+ } catch (imageError) {
387
+ lastResumeError = imageError;
388
+ }
389
+
390
+ let lateNetworkResult = null;
391
+ if (this.resumeNetworkTracker) {
392
+ lateNetworkResult = await this.resumeNetworkTracker.waitForLateNetworkResumeCandidateInfo(customer, {
393
+ minTs: acquisitionStartedAt,
394
+ });
395
+ timings.lateNetworkRetryMs = Number(lateNetworkResult?.lateRetryMs || 0);
396
+ }
397
+ if (lateNetworkResult?.candidateInfo?.resumeText) {
398
+ await this.checkpoint();
399
+ const evaluationStartedAt = Date.now();
400
+ const evaluation = await this.llmClient.evaluateResume({
401
+ screeningCriteria: profile.screeningCriteria,
402
+ candidate: this.buildResumeCandidateContext(customer, lateNetworkResult.candidateInfo),
403
+ });
404
+ timings.textModelMs = Date.now() - evaluationStartedAt;
405
+ return {
406
+ modalOpened,
407
+ capture,
408
+ evaluation,
409
+ timings,
410
+ acquisitionMode: 'network',
411
+ acquisitionReason: lateNetworkResult.acquisitionReason || 'late_network_hit',
412
+ sourceCandidateInfo: lateNetworkResult.candidateInfo,
413
+ };
414
+ }
415
+
416
+ const domStartedAt = Date.now();
417
+ const domFallback = await this.resolveDomResumeFallback(customer, cardProfile);
418
+ timings.domFallbackMs = Date.now() - domStartedAt;
419
+ if (domFallback?.networkCandidateInfo?.resumeText) {
420
+ await this.checkpoint();
421
+ const evaluationStartedAt = Date.now();
422
+ const evaluation = await this.llmClient.evaluateResume({
423
+ screeningCriteria: profile.screeningCriteria,
424
+ candidate: this.buildResumeCandidateContext(customer, domFallback.networkCandidateInfo),
425
+ });
426
+ timings.textModelMs = Date.now() - evaluationStartedAt;
427
+ return {
428
+ modalOpened,
429
+ capture,
430
+ evaluation,
431
+ timings,
432
+ acquisitionMode: 'network',
433
+ acquisitionReason: domFallback.acquisitionReason || 'dom_retry_network_recheck_hit',
434
+ sourceCandidateInfo: domFallback.networkCandidateInfo,
435
+ };
436
+ }
437
+ if (domFallback?.domCandidateInfo?.resumeText) {
438
+ await this.checkpoint();
439
+ const evaluationStartedAt = Date.now();
440
+ const evaluation = await this.llmClient.evaluateResume({
441
+ screeningCriteria: profile.screeningCriteria,
442
+ candidate: this.buildResumeCandidateContext(customer, domFallback.domCandidateInfo),
443
+ });
444
+ timings.textModelMs = Date.now() - evaluationStartedAt;
445
+ return {
446
+ modalOpened,
447
+ capture,
448
+ evaluation,
449
+ timings,
450
+ acquisitionMode: 'dom_fallback',
451
+ acquisitionReason: domFallback.acquisitionReason || 'dom_initial_hit',
452
+ sourceCandidateInfo: domFallback.domCandidateInfo,
453
+ };
454
+ }
455
+
456
+ throw lastResumeError || new Error('DOM_RESUME_FALLBACK_FAILED');
457
+ }
458
+
148
459
  async restoreListContext(profile) {
149
460
  if (typeof this.page.activatePrimaryChatLabel === 'function') {
150
461
  await this.page.activatePrimaryChatLabel('全部');
@@ -744,165 +1055,71 @@ export class BossChatApp {
744
1055
  const artifactDir = path.join(this.artifactRootDir, runId, candidateToken);
745
1056
  await mkdir(artifactDir, { recursive: true });
746
1057
 
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}`,
1058
+ const acquisition = await this.acquireResumeAndEvaluate(
1059
+ customer,
1060
+ profile,
1061
+ artifactDir,
1062
+ baseResult,
842
1063
  );
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
- }
1064
+ const evaluation = acquisition.evaluation;
1065
+ const capture = acquisition.capture;
1066
+ modalOpened = Boolean(acquisition.modalOpened);
1067
+ const finalReason = evaluation.passed ? 'LLM判定通过' : 'LLM判定不通过';
880
1068
  this.logger.log(
881
- `LLM评估完成:passed=${evaluation.passed} | rawPassed=${Boolean(evaluation.rawPassed)} | mode=${evaluation.evaluationMode || 'unknown'} | reason=${finalReason}`,
1069
+ `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
1070
  );
883
1071
 
884
1072
  baseResult.reason = finalReason;
885
1073
  baseResult.passed = evaluation.passed;
886
1074
  baseResult.decision = evaluation.passed ? 'passed' : 'skipped';
887
- baseResult.artifacts.rawPassed = Boolean(evaluation.rawPassed);
888
1075
  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
1076
  baseResult.artifacts.evaluationMode = String(evaluation.evaluationMode || '');
1077
+ baseResult.artifacts.evaluationImageCount = Number.isFinite(Number(evaluation.imageCount))
1078
+ ? Number(evaluation.imageCount)
1079
+ : Array.isArray(baseResult.artifacts.modelImagePaths)
1080
+ ? baseResult.artifacts.modelImagePaths.length
1081
+ : 0;
897
1082
  baseResult.artifacts.evaluationChunkIndex = Number.isFinite(Number(evaluation.chunkIndex))
898
1083
  ? Number(evaluation.chunkIndex)
899
1084
  : null;
900
1085
  baseResult.artifacts.evaluationChunkTotal = Number.isFinite(Number(evaluation.chunkTotal))
901
1086
  ? Number(evaluation.chunkTotal)
902
1087
  : null;
903
- baseResult.artifacts.evaluationEvidence = Array.isArray(evaluation.evidence)
904
- ? evaluation.evidence.slice(0, 5).map((item) => String(item || '').trim()).filter(Boolean)
905
- : [];
1088
+ baseResult.artifacts.llmRawOutput = String(evaluation.rawOutputText || '');
1089
+ baseResult.artifacts.resumeAcquisitionMode = String(acquisition.acquisitionMode || '');
1090
+ baseResult.artifacts.resumeAcquisitionReason = String(acquisition.acquisitionReason || '');
1091
+ baseResult.artifacts.initialNetworkWaitMs = Number(acquisition.timings?.initialNetworkWaitMs || 0);
1092
+ baseResult.artifacts.networkRetryMs = Number(acquisition.timings?.networkRetryMs || 0);
1093
+ baseResult.artifacts.imageCaptureMs = Number(acquisition.timings?.imageCaptureMs || 0);
1094
+ baseResult.artifacts.imageModelMs = Number(acquisition.timings?.imageModelMs || 0);
1095
+ baseResult.artifacts.lateNetworkRetryMs = Number(acquisition.timings?.lateNetworkRetryMs || 0);
1096
+ baseResult.artifacts.domFallbackMs = Number(acquisition.timings?.domFallbackMs || 0);
1097
+ baseResult.artifacts.textModelMs = Number(acquisition.timings?.textModelMs || 0);
1098
+ if (acquisition.sourceCandidateInfo) {
1099
+ baseResult.artifacts.resumeProfile = {
1100
+ primarySchool: normalizeText(acquisition.sourceCandidateInfo.school || ''),
1101
+ schools: Array.isArray(acquisition.sourceCandidateInfo.schools)
1102
+ ? acquisition.sourceCandidateInfo.schools
1103
+ : [],
1104
+ major: normalizeText(acquisition.sourceCandidateInfo.major || ''),
1105
+ majors: Array.isArray(acquisition.sourceCandidateInfo.majors)
1106
+ ? acquisition.sourceCandidateInfo.majors
1107
+ : [],
1108
+ company: normalizeText(acquisition.sourceCandidateInfo.company || ''),
1109
+ position: normalizeText(acquisition.sourceCandidateInfo.position || ''),
1110
+ resumeTextLength: String(acquisition.sourceCandidateInfo.resumeText || '').length,
1111
+ evidenceCorpusLength: String(
1112
+ acquisition.sourceCandidateInfo.evidenceCorpus || acquisition.sourceCandidateInfo.resumeText || '',
1113
+ ).length,
1114
+ };
1115
+ }
1116
+ if (this.resumeNetworkTracker) {
1117
+ baseResult.artifacts.resumeNetworkMode = this.resumeNetworkTracker.getResumeAcquisitionState().mode;
1118
+ baseResult.artifacts.resumeNetworkModeReason =
1119
+ this.resumeNetworkTracker.getResumeAcquisitionState().reason;
1120
+ baseResult.artifacts.resumeNetworkDiagnostics =
1121
+ this.resumeNetworkTracker.resumeNetworkDiagnostics.slice(-12);
1122
+ }
906
1123
 
907
1124
  await this.checkpoint();
908
1125
  const closeResult =
@@ -1069,7 +1286,7 @@ export class BossChatApp {
1069
1286
 
1070
1287
  const message = error.message || String(error);
1071
1288
  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(
1289
+ /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
1290
  message,
1074
1291
  )
1075
1292
  ) {