@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.
- package/config/screening-config.example.json +2 -0
- package/package.json +1 -1
- package/src/adapters.js +22 -0
- package/src/boss-chat.js +14 -1
- package/src/test-adapters-runtime.js +90 -0
- package/src/test-boss-chat.js +719 -58
- package/vendor/boss-chat-cli/src/app.js +411 -175
- package/vendor/boss-chat-cli/src/cli.js +20 -0
- package/vendor/boss-chat-cli/src/services/chrome-client.js +8 -2
- package/vendor/boss-chat-cli/src/services/llm.js +252 -84
- package/vendor/boss-chat-cli/src/services/profile-store.js +6 -0
- package/vendor/boss-chat-cli/src/services/report-store.js +301 -3
- package/vendor/boss-chat-cli/src/services/resume-capture.js +41 -126
- package/vendor/boss-chat-cli/src/services/resume-network.js +727 -0
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +44 -2
|
@@ -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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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} |
|
|
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.
|
|
904
|
-
|
|
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
|
) {
|