@reconcrap/boss-recommend-mcp 1.3.39 → 2.0.1

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.
Files changed (88) hide show
  1. package/README.md +86 -33
  2. package/package.json +62 -9
  3. package/skills/boss-chat/SKILL.md +5 -4
  4. package/skills/boss-recommend-pipeline/SKILL.md +21 -31
  5. package/skills/boss-recruit-pipeline/README.md +17 -0
  6. package/skills/boss-recruit-pipeline/SKILL.md +55 -0
  7. package/src/chat-mcp.js +1333 -0
  8. package/src/chat-runtime-config.js +559 -0
  9. package/src/cli.js +1254 -225
  10. package/src/core/browser/index.js +378 -0
  11. package/src/core/capture/index.js +298 -0
  12. package/src/core/cv-acquisition/index.js +219 -0
  13. package/src/core/greet-quota/index.js +54 -0
  14. package/src/core/infinite-list/index.js +459 -0
  15. package/src/core/reporting/legacy-csv.js +332 -0
  16. package/src/core/run/index.js +286 -0
  17. package/src/core/screening/index.js +1166 -0
  18. package/src/core/self-heal/index.js +848 -0
  19. package/src/domains/chat/cards.js +129 -0
  20. package/src/domains/chat/constants.js +183 -0
  21. package/src/domains/chat/detail.js +1369 -0
  22. package/src/domains/chat/index.js +7 -0
  23. package/src/domains/chat/jobs.js +334 -0
  24. package/src/domains/chat/page-guard.js +88 -0
  25. package/src/domains/chat/roots.js +56 -0
  26. package/src/domains/chat/run-service.js +1101 -0
  27. package/src/domains/recommend/actions.js +457 -0
  28. package/src/domains/recommend/cards.js +228 -0
  29. package/src/domains/recommend/constants.js +141 -0
  30. package/src/domains/recommend/detail.js +341 -0
  31. package/src/domains/recommend/filters.js +581 -0
  32. package/src/domains/recommend/index.js +10 -0
  33. package/src/domains/recommend/jobs.js +232 -0
  34. package/src/domains/recommend/refresh.js +204 -0
  35. package/src/domains/recommend/roots.js +78 -0
  36. package/src/domains/recommend/run-service.js +903 -0
  37. package/src/domains/recommend/scopes.js +245 -0
  38. package/src/domains/recruit/actions.js +277 -0
  39. package/src/domains/recruit/cards.js +66 -0
  40. package/src/domains/recruit/constants.js +130 -0
  41. package/src/domains/recruit/detail.js +414 -0
  42. package/src/domains/recruit/index.js +9 -0
  43. package/src/domains/recruit/instruction-parser.js +451 -0
  44. package/src/domains/recruit/refresh.js +40 -0
  45. package/src/domains/recruit/roots.js +67 -0
  46. package/src/domains/recruit/run-service.js +580 -0
  47. package/src/domains/recruit/search.js +1149 -0
  48. package/src/index.js +578 -419
  49. package/src/recommend-mcp.js +1257 -0
  50. package/src/recruit-mcp.js +1035 -0
  51. package/src/adapters.js +0 -3079
  52. package/src/boss-chat.js +0 -1037
  53. package/src/pipeline.js +0 -2249
  54. package/src/recommend-healing-config.js +0 -131
  55. package/src/recommend-healing-rules.json +0 -261
  56. package/src/self-heal.js +0 -2237
  57. package/src/test-adapters-runtime.js +0 -628
  58. package/src/test-boss-chat.js +0 -3196
  59. package/src/test-index-async.js +0 -498
  60. package/src/test-parser.js +0 -742
  61. package/src/test-pipeline.js +0 -2703
  62. package/src/test-run-state.js +0 -152
  63. package/src/test-self-heal.js +0 -224
  64. package/vendor/boss-chat-cli/README.md +0 -134
  65. package/vendor/boss-chat-cli/package.json +0 -53
  66. package/vendor/boss-chat-cli/src/app.js +0 -1501
  67. package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
  68. package/vendor/boss-chat-cli/src/cli.js +0 -1713
  69. package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
  70. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
  71. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
  72. package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
  73. package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
  74. package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
  75. package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
  76. package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
  77. package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
  78. package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
  79. package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
  80. package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
  81. package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
  82. package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
  83. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -7072
  84. package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
  85. package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
  86. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2423
  87. package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
  88. package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
@@ -1,1501 +0,0 @@
1
- import { mkdir } from 'node:fs/promises';
2
- import path from 'node:path';
3
-
4
- import { isDomProfileConsistentWithCard, NETWORK_RESUME_RETRY_WAIT_MS } from './services/resume-network.js';
5
- import { DEFAULT_GREETING_TEXT } from './services/profile-store.js';
6
- import { createCustomerAliases, createCustomerKey } from './utils/customer-key.js';
7
-
8
- function runToken(date = new Date()) {
9
- return date.toISOString().replace(/[:.]/g, '-');
10
- }
11
-
12
- function safePathToken(value) {
13
- return String(value || 'unknown')
14
- .replace(/[^\w.-]+/g, '_')
15
- .slice(0, 80);
16
- }
17
-
18
- function normalizeText(value) {
19
- return String(value || '').replace(/\s+/g, ' ').trim();
20
- }
21
-
22
- function toStringArray(value, maxItems = 8) {
23
- if (!Array.isArray(value)) return [];
24
- const normalized = [];
25
- for (const item of value) {
26
- const text = normalizeText(item);
27
- if (!text) continue;
28
- normalized.push(text);
29
- if (normalized.length >= maxItems) break;
30
- }
31
- return normalized;
32
- }
33
-
34
- function shouldContinue(summary, targetCount) {
35
- if (!targetCount || !Number.isFinite(targetCount) || targetCount <= 0) {
36
- return true;
37
- }
38
- return summary.inspected < targetCount;
39
- }
40
-
41
- function hasResumeRequestSentMessage(state = {}) {
42
- const lastText = normalizeText(state?.lastText || '');
43
- const recent = Array.isArray(state?.recent) ? state.recent : [];
44
- if (lastText.includes('简历请求已发送')) return true;
45
- return recent.some((item) => normalizeText(item).includes('简历请求已发送'));
46
- }
47
-
48
- function resolveGreetingText(profile = {}) {
49
- return normalizeText(profile?.greetingText || '') || DEFAULT_GREETING_TEXT;
50
- }
51
-
52
- const CANDIDATE_LIST_WAIT_AFTER_CONTEXT_MS = 5000;
53
- const CANDIDATE_LIST_WAIT_POLL_MS = 500;
54
-
55
- export class BossChatApp {
56
- constructor({
57
- page,
58
- llmClient,
59
- interaction,
60
- resumeCaptureService,
61
- stateStore,
62
- reportStore,
63
- resumeNetworkTracker = null,
64
- runControl = null,
65
- logger = console,
66
- dryRun = false,
67
- artifactRootDir = '',
68
- resumeOpenCooldownMs = 3000,
69
- onProgress = null,
70
- }) {
71
- this.page = page;
72
- this.llmClient = llmClient;
73
- this.interaction = interaction;
74
- this.resumeCaptureService = resumeCaptureService;
75
- this.stateStore = stateStore;
76
- this.reportStore = reportStore;
77
- this.resumeNetworkTracker = resumeNetworkTracker;
78
- this.runControl = runControl;
79
- this.logger = logger;
80
- this.dryRun = dryRun;
81
- this.artifactRootDir = artifactRootDir;
82
- this.lastResumeOpenAt = 0;
83
- this.resumeOpenBlockedUntil = 0;
84
- this.resumeOpenCooldownMs = Number.isFinite(Number(resumeOpenCooldownMs))
85
- ? Math.max(0, Number(resumeOpenCooldownMs))
86
- : 3000;
87
- this.onProgress = typeof onProgress === 'function' ? onProgress : null;
88
- }
89
-
90
- formatProgress(summary) {
91
- const targetText = summary.profile.targetCount || '∞';
92
- return `进度: 已处理 ${summary.inspected}/${targetText},通过 ${summary.passed},已求简历 ${summary.requested},跳过 ${summary.skipped},错误 ${summary.errors}`;
93
- }
94
-
95
- async checkpoint() {
96
- if (this.runControl) {
97
- await this.runControl.checkpoint();
98
- }
99
- }
100
-
101
- async waitResumeOpenCooldown(minGapMs = this.resumeOpenCooldownMs) {
102
- const now = Date.now();
103
- const waitFromLast = Math.max(0, minGapMs - (now - this.lastResumeOpenAt));
104
- const waitFromBlock = Math.max(0, this.resumeOpenBlockedUntil - now);
105
- const waitMs = Math.max(waitFromLast, waitFromBlock);
106
- if (waitMs <= 0) return;
107
- await new Promise((resolve) => setTimeout(resolve, waitMs));
108
- }
109
-
110
- setResumeOpenBlocked(ms = 90000) {
111
- const until = Date.now() + ms;
112
- this.resumeOpenBlockedUntil = Math.max(this.resumeOpenBlockedUntil, until);
113
- }
114
-
115
- emitProgress(summary, meta = {}) {
116
- if (!this.onProgress) return;
117
- try {
118
- this.onProgress(
119
- {
120
- inspected: Number(summary?.inspected || 0),
121
- passed: Number(summary?.passed || 0),
122
- requested: Number(summary?.requested || 0),
123
- skipped: Number(summary?.skipped || 0),
124
- errors: Number(summary?.errors || 0),
125
- exhausted: Boolean(summary?.exhausted),
126
- stopped: Boolean(summary?.stopped),
127
- stopReason: String(summary?.stopReason || ''),
128
- reportPath: String(summary?.reportPath || ''),
129
- },
130
- meta,
131
- );
132
- } catch {}
133
- }
134
-
135
- buildCardProfile(customer = {}) {
136
- return {
137
- name: normalizeText(customer.name || ''),
138
- school: normalizeText(customer.school || ''),
139
- major: normalizeText(customer.major || ''),
140
- company: normalizeText(customer.company || customer.lastCompany || customer.last_company || ''),
141
- position: normalizeText(customer.position || customer.lastPosition || customer.last_position || ''),
142
- };
143
- }
144
-
145
- buildResumeCandidateContext(customer = {}, candidateInfo = null) {
146
- const info = candidateInfo && typeof candidateInfo === 'object' ? candidateInfo : {};
147
- const schools = Array.isArray(info.schools) ? info.schools.map((item) => normalizeText(item)).filter(Boolean) : [];
148
- const majors = Array.isArray(info.majors) ? info.majors.map((item) => normalizeText(item)).filter(Boolean) : [];
149
- const primarySchool = normalizeText(info.school || info.primarySchool || schools[0] || '');
150
- const primaryMajor = normalizeText(info.major || majors[0] || '');
151
- const company = normalizeText(info.company || '');
152
- const position = normalizeText(info.position || '');
153
- const hasProfileContext = Boolean(primarySchool || primaryMajor || company || position || schools.length || majors.length);
154
- return {
155
- name: customer.name || info.name || '',
156
- sourceJob: customer.sourceJob || '',
157
- resumeProfile: hasProfileContext
158
- ? {
159
- primarySchool,
160
- schools: schools.length > 0 ? schools : primarySchool ? [primarySchool] : [],
161
- major: primaryMajor,
162
- majors: majors.length > 0 ? majors : primaryMajor ? [primaryMajor] : [],
163
- company,
164
- position,
165
- }
166
- : null,
167
- resumeText: String(info.resumeText || ''),
168
- evidenceCorpus: String(info.evidenceCorpus || info.resumeText || ''),
169
- };
170
- }
171
-
172
- async extractDomResumeCandidateInfo(customer = {}) {
173
- if (typeof this.page.getResumeProfileFromDom !== 'function') {
174
- return null;
175
- }
176
- const result = await this.page.getResumeProfileFromDom();
177
- if (!result?.ok) {
178
- return null;
179
- }
180
- const resumeText = normalizeText(result.resumeText || '');
181
- if (!resumeText) {
182
- return null;
183
- }
184
- return {
185
- name: normalizeText(result.name || customer.name || ''),
186
- school: normalizeText(result.primarySchool || ''),
187
- schools: Array.isArray(result.schools) ? result.schools.map((item) => normalizeText(item)).filter(Boolean) : [],
188
- major: normalizeText(result.major || ''),
189
- majors: Array.isArray(result.majors) ? result.majors.map((item) => normalizeText(item)).filter(Boolean) : [],
190
- company: normalizeText(result.company || ''),
191
- position: normalizeText(result.position || ''),
192
- resumeText: String(result.resumeText || ''),
193
- evidenceCorpus: String(result.evidenceCorpus || result.resumeText || ''),
194
- };
195
- }
196
-
197
- async retryCandidateResumeContext(customer = {}) {
198
- if (typeof this.page.closeResumeModalDomOnce === 'function') {
199
- try {
200
- await this.page.closeResumeModalDomOnce();
201
- } catch {}
202
- }
203
- await this.checkpoint();
204
- if (typeof this.page.activateCandidate === 'function') {
205
- await this.page.activateCandidate(customer, 0);
206
- } else {
207
- const rect = await this.page.centerCustomerCard(customer.domIndex, 0);
208
- await this.interaction.clickRect(rect);
209
- }
210
- await this.interaction.sleepRange(520, 140);
211
- await this.page.waitForCandidateActivated(customer, {
212
- maxAttempts: 8,
213
- delayMs: 180,
214
- });
215
- await this.page.waitForConversationReady({
216
- maxAttempts: 8,
217
- delayMs: 220,
218
- });
219
- const retryStartedAt = Date.now();
220
- const openResult = await this.page.openOnlineResume();
221
- await this.interaction.sleepRange(520, 140);
222
- return {
223
- retryStartedAt,
224
- openResult,
225
- };
226
- }
227
-
228
- async resolveDomResumeFallback(customer = {}, cardProfile = null) {
229
- let domCandidateInfo = await this.extractDomResumeCandidateInfo(customer);
230
- let networkCandidateInfo = null;
231
- let acquisitionReason = domCandidateInfo?.resumeText ? 'dom_initial_hit' : '';
232
-
233
- if (domCandidateInfo && !isDomProfileConsistentWithCard(cardProfile, domCandidateInfo)) {
234
- this.logger.log(
235
- `DOM简历疑似错位:expected=${cardProfile?.name || 'unknown'} | actual=${domCandidateInfo?.name || 'unknown'},尝试重试点击并短暂回查 network。`,
236
- );
237
- acquisitionReason = 'dom_profile_mismatch_retry';
238
- try {
239
- const retryContext = await this.retryCandidateResumeContext(customer);
240
- if (this.resumeNetworkTracker) {
241
- const retryNetwork = await this.resumeNetworkTracker.waitForNetworkResumeCandidateInfo(
242
- customer,
243
- NETWORK_RESUME_RETRY_WAIT_MS,
244
- { minTs: retryContext.retryStartedAt },
245
- );
246
- if (retryNetwork?.candidateInfo?.resumeText) {
247
- networkCandidateInfo = retryNetwork.candidateInfo;
248
- acquisitionReason = 'dom_retry_network_recheck_hit';
249
- domCandidateInfo = null;
250
- }
251
- }
252
- if (!networkCandidateInfo) {
253
- const retryDomCandidateInfo = await this.extractDomResumeCandidateInfo(customer);
254
- if (retryDomCandidateInfo && isDomProfileConsistentWithCard(cardProfile, retryDomCandidateInfo)) {
255
- domCandidateInfo = retryDomCandidateInfo;
256
- acquisitionReason = 'dom_retry_hit';
257
- } else {
258
- domCandidateInfo = null;
259
- acquisitionReason = 'dom_profile_mismatch_unresolved';
260
- }
261
- }
262
- } catch (error) {
263
- domCandidateInfo = null;
264
- acquisitionReason = `dom_profile_retry_failed:${normalizeText(error?.message || error)}`;
265
- }
266
- }
267
-
268
- return {
269
- domCandidateInfo,
270
- networkCandidateInfo,
271
- acquisitionReason,
272
- };
273
- }
274
-
275
- async acquireResumeAndEvaluate(customer, profile, artifactDir, baseResult) {
276
- let modalOpened = false;
277
- let capture = null;
278
- let lastResumeError = null;
279
- const timings = {
280
- initialNetworkWaitMs: 0,
281
- networkRetryMs: 0,
282
- imageCaptureMs: 0,
283
- imageModelMs: 0,
284
- lateNetworkRetryMs: 0,
285
- domFallbackMs: 0,
286
- textModelMs: 0,
287
- };
288
- const cardProfile = this.buildCardProfile(customer);
289
-
290
- await this.waitResumeOpenCooldown(this.resumeOpenCooldownMs + Math.floor(Math.random() * 200));
291
- await this.checkpoint();
292
- const acquisitionStartedAt = Date.now();
293
- const openResult = await this.page.openOnlineResume();
294
- let openDetected = openResult ? Boolean(openResult?.detectedOpen) : true;
295
- this.lastResumeOpenAt = Date.now();
296
- modalOpened = openDetected;
297
- await this.interaction.sleepRange(600, 220);
298
-
299
- const rateLimit =
300
- typeof this.page.getResumeRateLimitWarning === 'function'
301
- ? await this.page.getResumeRateLimitWarning()
302
- : { hit: false, text: '' };
303
- if (rateLimit?.hit) {
304
- const backoffMs = 90000 + Math.floor(Math.random() * 30000);
305
- this.setResumeOpenBlocked(backoffMs);
306
- throw new Error(`RESUME_RATE_LIMIT_WARNING:${rateLimit.text}`);
307
- }
308
- if (openResult && !openDetected) {
309
- let delayedDetected = false;
310
- if (typeof this.page.getResumeModalState === 'function') {
311
- await new Promise((resolve) => setTimeout(resolve, 1000));
312
- const delayedState = await this.page.getResumeModalState();
313
- delayedDetected =
314
- Boolean(delayedState?.open) ||
315
- Number(delayedState?.iframeCount || 0) > 0 ||
316
- (Number(delayedState?.scopeCount || 0) > 0 && Number(delayedState?.closeCount || 0) > 0);
317
- }
318
- if (delayedDetected) {
319
- openDetected = true;
320
- modalOpened = true;
321
- this.logger.log('在线简历首次检测未命中,1秒后复检已打开,继续处理。');
322
- } else {
323
- throw new Error('RESUME_MODAL_NOT_DETECTED_AFTER_SINGLE_DOM_CLICK');
324
- }
325
- }
326
- if (!openDetected) {
327
- throw new Error('RESUME_MODAL_NOT_DETECTED');
328
- }
329
-
330
- let networkResult = null;
331
- if (this.resumeNetworkTracker) {
332
- networkResult = await this.resumeNetworkTracker.waitForResumeNetworkByMode(customer, {
333
- minTs: acquisitionStartedAt,
334
- });
335
- timings.initialNetworkWaitMs = Number(networkResult?.initialWaitMs || 0);
336
- timings.networkRetryMs = Number(networkResult?.retryWaitMs || 0);
337
- }
338
-
339
- if (networkResult?.candidateInfo?.resumeText) {
340
- await this.checkpoint();
341
- const evaluationStartedAt = Date.now();
342
- const evaluation = await this.llmClient.evaluateResume({
343
- screeningCriteria: profile.screeningCriteria,
344
- candidate: this.buildResumeCandidateContext(customer, networkResult.candidateInfo),
345
- });
346
- timings.textModelMs = Date.now() - evaluationStartedAt;
347
- return {
348
- modalOpened,
349
- capture,
350
- evaluation,
351
- timings,
352
- acquisitionMode: 'network',
353
- acquisitionReason: networkResult.acquisitionReason || 'initial_network_hit',
354
- sourceCandidateInfo: networkResult.candidateInfo,
355
- };
356
- }
357
-
358
- try {
359
- await this.checkpoint();
360
- const captureStartedAt = Date.now();
361
- capture = await this.resumeCaptureService.captureResume({
362
- artifactDir,
363
- waitResumeMs: 30000,
364
- scrollSettleMs: 500,
365
- });
366
- timings.imageCaptureMs = Date.now() - captureStartedAt;
367
- if (capture?.quality?.likelyBlank) {
368
- const blankBackoffMs = 45000 + Math.floor(Math.random() * 20000);
369
- this.setResumeOpenBlocked(blankBackoffMs);
370
- throw new Error('RESUME_CAPTURE_LIKELY_BLANK');
371
- }
372
- const modelImagePaths = Array.isArray(capture.modelImagePaths)
373
- ? capture.modelImagePaths.map((item) => String(item || '').trim()).filter(Boolean)
374
- : [];
375
- this.logger.log(`截图完成:chunks=${capture.chunkCount} | modelImages=${modelImagePaths.length}`);
376
- baseResult.artifacts.chunkDir = capture.chunkDir;
377
- baseResult.artifacts.metadataFile = capture.metadataFile;
378
- baseResult.artifacts.stitchedImage = capture.stitchedImage;
379
- baseResult.artifacts.chunkCount = capture.chunkCount;
380
- baseResult.artifacts.modelImagePaths = modelImagePaths;
381
-
382
- if (this.resumeNetworkTracker) {
383
- this.resumeNetworkTracker.setResumeAcquisitionMode('image', 'image_capture_success');
384
- }
385
-
386
- await this.checkpoint();
387
- const imageEvalStartedAt = Date.now();
388
- const evaluation = await this.llmClient.evaluateResume({
389
- screeningCriteria: profile.screeningCriteria,
390
- candidate: this.buildResumeCandidateContext(customer, null),
391
- imagePaths: modelImagePaths,
392
- });
393
- timings.imageModelMs = Date.now() - imageEvalStartedAt;
394
- return {
395
- modalOpened,
396
- capture,
397
- evaluation,
398
- timings,
399
- acquisitionMode: 'image_fallback',
400
- acquisitionReason: 'image_capture_success',
401
- sourceCandidateInfo: null,
402
- };
403
- } catch (imageError) {
404
- lastResumeError = imageError;
405
- }
406
-
407
- let lateNetworkResult = null;
408
- if (this.resumeNetworkTracker) {
409
- lateNetworkResult = await this.resumeNetworkTracker.waitForLateNetworkResumeCandidateInfo(customer, {
410
- minTs: acquisitionStartedAt,
411
- });
412
- timings.lateNetworkRetryMs = Number(lateNetworkResult?.lateRetryMs || 0);
413
- }
414
- if (lateNetworkResult?.candidateInfo?.resumeText) {
415
- await this.checkpoint();
416
- const evaluationStartedAt = Date.now();
417
- const evaluation = await this.llmClient.evaluateResume({
418
- screeningCriteria: profile.screeningCriteria,
419
- candidate: this.buildResumeCandidateContext(customer, lateNetworkResult.candidateInfo),
420
- });
421
- timings.textModelMs = Date.now() - evaluationStartedAt;
422
- return {
423
- modalOpened,
424
- capture,
425
- evaluation,
426
- timings,
427
- acquisitionMode: 'network',
428
- acquisitionReason: lateNetworkResult.acquisitionReason || 'late_network_hit',
429
- sourceCandidateInfo: lateNetworkResult.candidateInfo,
430
- };
431
- }
432
-
433
- const domStartedAt = Date.now();
434
- const domFallback = await this.resolveDomResumeFallback(customer, cardProfile);
435
- timings.domFallbackMs = Date.now() - domStartedAt;
436
- if (domFallback?.networkCandidateInfo?.resumeText) {
437
- await this.checkpoint();
438
- const evaluationStartedAt = Date.now();
439
- const evaluation = await this.llmClient.evaluateResume({
440
- screeningCriteria: profile.screeningCriteria,
441
- candidate: this.buildResumeCandidateContext(customer, domFallback.networkCandidateInfo),
442
- });
443
- timings.textModelMs = Date.now() - evaluationStartedAt;
444
- return {
445
- modalOpened,
446
- capture,
447
- evaluation,
448
- timings,
449
- acquisitionMode: 'network',
450
- acquisitionReason: domFallback.acquisitionReason || 'dom_retry_network_recheck_hit',
451
- sourceCandidateInfo: domFallback.networkCandidateInfo,
452
- };
453
- }
454
- if (domFallback?.domCandidateInfo?.resumeText) {
455
- await this.checkpoint();
456
- const evaluationStartedAt = Date.now();
457
- const evaluation = await this.llmClient.evaluateResume({
458
- screeningCriteria: profile.screeningCriteria,
459
- candidate: this.buildResumeCandidateContext(customer, domFallback.domCandidateInfo),
460
- });
461
- timings.textModelMs = Date.now() - evaluationStartedAt;
462
- return {
463
- modalOpened,
464
- capture,
465
- evaluation,
466
- timings,
467
- acquisitionMode: 'dom_fallback',
468
- acquisitionReason: domFallback.acquisitionReason || 'dom_initial_hit',
469
- sourceCandidateInfo: domFallback.domCandidateInfo,
470
- };
471
- }
472
-
473
- throw lastResumeError || new Error('DOM_RESUME_FALLBACK_FAILED');
474
- }
475
-
476
- async restoreListContext(profile) {
477
- if (typeof this.page.activatePrimaryChatLabel === 'function') {
478
- await this.page.activatePrimaryChatLabel('全部');
479
- }
480
- await this.page.selectJob(profile.jobSelection);
481
- return profile.startFrom === 'all'
482
- ? this.page.activateAllFilter()
483
- : this.page.activateUnreadFilter();
484
- }
485
-
486
- async waitForCandidateList({
487
- reason = 'unknown',
488
- maxWaitMs = CANDIDATE_LIST_WAIT_AFTER_CONTEXT_MS,
489
- pollMs = CANDIDATE_LIST_WAIT_POLL_MS,
490
- } = {}) {
491
- const startedAt = Date.now();
492
- let attempts = 0;
493
- let lastState = null;
494
- let lastError = '';
495
-
496
- while (Date.now() - startedAt <= maxWaitMs) {
497
- attempts += 1;
498
- try {
499
- if (typeof this.page.getPageState === 'function') {
500
- lastState = await this.page.getPageState();
501
- if (Number(lastState?.listItemCount || 0) > 0) {
502
- return {
503
- ready: true,
504
- waitedMs: Date.now() - startedAt,
505
- attempts,
506
- listItemCount: Number(lastState?.listItemCount || 0),
507
- lastState,
508
- lastError,
509
- };
510
- }
511
- } else if (typeof this.page.getLoadedCustomers === 'function') {
512
- const customers = await this.page.getLoadedCustomers();
513
- if (Array.isArray(customers) && customers.length > 0) {
514
- return {
515
- ready: true,
516
- waitedMs: Date.now() - startedAt,
517
- attempts,
518
- listItemCount: customers.length,
519
- lastState,
520
- lastError,
521
- };
522
- }
523
- }
524
- } catch (error) {
525
- lastError = String(error?.message || error || '');
526
- }
527
-
528
- if (Date.now() - startedAt >= maxWaitMs) {
529
- break;
530
- }
531
- await new Promise((resolve) => setTimeout(resolve, pollMs));
532
- }
533
-
534
- return {
535
- ready: false,
536
- waitedMs: Date.now() - startedAt,
537
- attempts,
538
- listItemCount: Number(lastState?.listItemCount || 0),
539
- lastState,
540
- lastError,
541
- reason,
542
- };
543
- }
544
-
545
- async cleanupPanels({
546
- resumeMaxAttempts = 6,
547
- detailMaxAttempts = 4,
548
- ensureDismiss = true,
549
- } = {}) {
550
- const resume =
551
- typeof this.page.closeResumeModalDomOnce === 'function'
552
- ? await this.page.closeResumeModalDomOnce()
553
- : await this.page.closeResumeModal({
554
- maxAttempts: resumeMaxAttempts,
555
- ensureDismiss,
556
- });
557
-
558
- let detail = {
559
- closed: true,
560
- method: 'unsupported',
561
- finalState: {
562
- panelCount: 0,
563
- closeCount: 0,
564
- topPanelClass: '',
565
- },
566
- };
567
- if (typeof this.page.closeCandidateDetailDomOnce === 'function') {
568
- detail = await this.page.closeCandidateDetailDomOnce();
569
- if (!detail.closed && typeof this.page.closeCandidateDetail === 'function') {
570
- detail = await this.page.closeCandidateDetail({
571
- maxAttempts: detailMaxAttempts,
572
- ensureDismiss,
573
- });
574
- }
575
- } else if (typeof this.page.closeCandidateDetail === 'function') {
576
- detail = await this.page.closeCandidateDetail({
577
- maxAttempts: detailMaxAttempts,
578
- ensureDismiss,
579
- });
580
- }
581
-
582
- return { resume, detail };
583
- }
584
-
585
- isResumeModalVisible(state = {}) {
586
- return (
587
- Boolean(state?.open) ||
588
- Number(state?.iframeCount || 0) > 0 ||
589
- Number(state?.scopeCount || 0) > 0
590
- );
591
- }
592
-
593
- isCandidateDetailVisible(state = {}) {
594
- return (
595
- Boolean(state?.open) ||
596
- Number(state?.panelCount || 0) > 0 ||
597
- Number(state?.closeCount || 0) > 0
598
- );
599
- }
600
-
601
- async ensurePanelsClosedBeforeOutreach({ initialResumeCloseResult = null } = {}) {
602
- const runResumeLightClose = async () => {
603
- if (initialResumeCloseResult) {
604
- return initialResumeCloseResult;
605
- }
606
- if (typeof this.page.closeResumeModalDomOnce === 'function') {
607
- return this.page.closeResumeModalDomOnce();
608
- }
609
- if (typeof this.page.closeResumeModal === 'function') {
610
- return this.page.closeResumeModal({ maxAttempts: 6, ensureDismiss: true });
611
- }
612
- return {
613
- closed: true,
614
- method: 'unsupported',
615
- finalState: { open: false, scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: '' },
616
- };
617
- };
618
- const runDetailLightClose = async () => {
619
- if (typeof this.page.closeCandidateDetailDomOnce === 'function') {
620
- return this.page.closeCandidateDetailDomOnce();
621
- }
622
- if (typeof this.page.closeCandidateDetail === 'function') {
623
- return this.page.closeCandidateDetail({ maxAttempts: 1, ensureDismiss: false });
624
- }
625
- return {
626
- closed: true,
627
- method: 'unsupported',
628
- finalState: {
629
- open: false,
630
- panelCount: 0,
631
- closeCount: 0,
632
- topPanelClass: '',
633
- overlayClass: '',
634
- contentClass: '',
635
- },
636
- };
637
- };
638
- const methodParts = [];
639
- let retried = false;
640
- let readyState = null;
641
-
642
- let resumeResult = await runResumeLightClose();
643
- let detailResult = await runDetailLightClose();
644
- methodParts.push(`resume:${resumeResult?.method || 'unknown'}`);
645
- methodParts.push(`detail:${detailResult?.method || 'unknown'}`);
646
- this.logger.log(
647
- `发送前首次关闭结果:resumeClosed=${Boolean(resumeResult?.closed)} | resumeMethod=${resumeResult?.method || 'unknown'} | detailClosed=${Boolean(detailResult?.closed)} | detailMethod=${detailResult?.method || 'unknown'}`,
648
- );
649
-
650
- let resumeClosed = Boolean(resumeResult?.closed);
651
- let detailClosed = Boolean(detailResult?.closed);
652
-
653
- if (!resumeClosed && typeof this.page.closeResumeModal === 'function') {
654
- retried = true;
655
- resumeResult = await this.page.closeResumeModal({ maxAttempts: 6, ensureDismiss: true });
656
- methodParts.push(`resume-retry:${resumeResult?.method || 'unknown'}`);
657
- resumeClosed = Boolean(resumeResult?.closed);
658
- }
659
-
660
- if (!detailClosed && typeof this.page.closeCandidateDetail === 'function') {
661
- retried = true;
662
- detailResult = await this.page.closeCandidateDetail({ maxAttempts: 4, ensureDismiss: true });
663
- methodParts.push(`detail-retry:${detailResult?.method || 'unknown'}`);
664
- detailClosed = Boolean(detailResult?.closed);
665
- }
666
-
667
- let resumeState = resumeResult?.finalState || null;
668
- let detailState = detailResult?.finalState || null;
669
-
670
- if (typeof this.page.getResumeModalState === 'function') {
671
- resumeState = await this.page.getResumeModalState();
672
- resumeClosed = !this.isResumeModalVisible(resumeState);
673
- }
674
- if (typeof this.page.getCandidateDetailState === 'function') {
675
- detailState = await this.page.getCandidateDetailState();
676
- detailClosed = !this.isCandidateDetailVisible(detailState);
677
- }
678
-
679
- if (retried) {
680
- this.logger.log(
681
- `发送前重试关闭结果:resumeClosed=${resumeClosed} | resumeMethod=${resumeResult?.method || 'unknown'} | detailClosed=${detailClosed} | detailMethod=${detailResult?.method || 'unknown'}`,
682
- );
683
- }
684
-
685
- let failureReason = '';
686
- if (resumeClosed && detailClosed) {
687
- try {
688
- readyState = await this.page.waitForConversationReady({
689
- maxAttempts: 12,
690
- delayMs: 220,
691
- requirePanelsClosed: true,
692
- });
693
- methodParts.push('ready:strict');
694
- } catch (error) {
695
- failureReason = `strict-ready-check-failed:${error?.message || error}`;
696
- methodParts.push(failureReason);
697
- }
698
- } else {
699
- failureReason = [
700
- !resumeClosed ? 'resume-modal-still-open' : '',
701
- !detailClosed ? 'candidate-detail-still-open' : '',
702
- ].filter(Boolean).join('+');
703
- }
704
-
705
- const diagnostics = {
706
- preActionResumeClosed: resumeClosed,
707
- preActionDetailClosed: detailClosed,
708
- preActionCleanupMethod: methodParts.join('|'),
709
- preActionCleanupRetried: retried,
710
- preActionCleanupFailureReason: failureReason,
711
- };
712
-
713
- return {
714
- ok: resumeClosed && detailClosed && Boolean(readyState?.panelsClosed),
715
- readyState,
716
- diagnostics,
717
- resumeResult,
718
- detailResult,
719
- resumeState,
720
- detailState,
721
- };
722
- }
723
-
724
- async run(profile) {
725
- const startedAt = new Date().toISOString();
726
- const runId = runToken(new Date());
727
- const startFrom = profile.startFrom === 'all' ? 'all' : 'unread';
728
- const filterLabel = startFrom === 'all' ? '全部' : '未读';
729
- const targetCount =
730
- Number.isFinite(Number(profile.targetCount)) && Number(profile.targetCount) > 0
731
- ? Number(profile.targetCount)
732
- : null;
733
-
734
- await this.stateStore.load();
735
- try {
736
- await this.page.ensureReady();
737
- } catch (error) {
738
- this.logger.log(`页面就绪检查告警:${error?.message || error},将继续执行预热恢复流程。`);
739
- }
740
- let filterResult = await this.restoreListContext(profile);
741
- await this.interaction.sleepRange(420, 160);
742
- let initialListWait = await this.waitForCandidateList({
743
- reason: 'initial-context-restore',
744
- });
745
- if (initialListWait.ready) {
746
- this.logger.log(
747
- `候选人列表已就绪:reason=initial-context-restore | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount}`,
748
- );
749
- } else {
750
- this.logger.log(
751
- `候选人列表等待超时:reason=initial-context-restore | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount} | lastError=${initialListWait.lastError || 'n/a'},继续尝试预热。`,
752
- );
753
- filterResult = await this.restoreListContext(profile);
754
- await this.interaction.sleepRange(420, 160);
755
- initialListWait = await this.waitForCandidateList({
756
- reason: 'initial-context-restore-reapply',
757
- });
758
- if (initialListWait.ready) {
759
- this.logger.log(
760
- `候选人列表二次恢复成功:reason=initial-context-restore-reapply | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount}`,
761
- );
762
- } else {
763
- this.logger.log(
764
- `候选人列表二次等待仍超时:reason=initial-context-restore-reapply | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount} | lastError=${initialListWait.lastError || 'n/a'},继续尝试预热。`,
765
- );
766
- }
767
- }
768
- this.logger.log('预热步骤:准备点击首位人选初始化聊天容器...');
769
- let primedCustomer = null;
770
-
771
- if (typeof this.page.primeConversationByFirstCandidate === 'function') {
772
- try {
773
- const prime = await this.page.primeConversationByFirstCandidate();
774
- const candidate = prime?.candidate || {};
775
- const candidateBase = {
776
- customerId: candidate.customerId || '',
777
- name: candidate.name || '',
778
- sourceJob: candidate.sourceJob || '',
779
- domIndex: Number.isFinite(candidate.domIndex) ? candidate.domIndex : 0,
780
- textSnippet: '',
781
- };
782
- primedCustomer = {
783
- ...candidateBase,
784
- customerKey: createCustomerKey(candidateBase),
785
- };
786
- this.logger.log(
787
- `预热完成:name=${prime?.candidate?.name || '未知'} | job=${prime?.candidate?.sourceJob || '未知'} | id=${prime?.candidate?.customerId || '无'} | domIndex=${prime?.candidate?.domIndex ?? -1} | 候选人数=${prime?.totalVisibleCandidates ?? '未知'} | ready=${prime?.readyState?.hasOnlineResume ? 'online_resume' : prime?.readyState?.hasAskResume ? 'ask_resume' : 'unknown'}`,
788
- );
789
- } catch (error) {
790
- this.logger.log(`预热失败:${error?.message || error}(将继续尝试主循环)`);
791
- }
792
- }
793
-
794
- const results = [];
795
- const summary = {
796
- startedAt,
797
- finishedAt: null,
798
- dryRun: this.dryRun,
799
- profile: {
800
- screeningCriteria: profile.screeningCriteria,
801
- targetCount,
802
- chromePort: profile.chrome.port,
803
- model: profile.llm.model,
804
- startFrom,
805
- jobSelection: profile.jobSelection,
806
- },
807
- inspected: 0,
808
- passed: 0,
809
- requested: 0,
810
- skipped: 0,
811
- errors: 0,
812
- exhausted: false,
813
- stopped: false,
814
- stopReason: '',
815
- results,
816
- reportPath: null,
817
- };
818
-
819
- this.logger.log(
820
- `岗位: ${profile.jobSelection?.label || profile.jobSelection?.value || '未知'};列表范围: ${filterLabel}${
821
- filterResult.changed
822
- ? filterResult.verified === false
823
- ? '(已尝试切换,未验证 active)'
824
- : '(已切换)'
825
- : '(已在目标筛选)'
826
- }${filterResult?.activeLabel ? ` | active=${filterResult.activeLabel}` : ''}`,
827
- );
828
- this.logger.log(this.formatProgress(summary));
829
- this.emitProgress(summary, {
830
- stage: 'running',
831
- message: '任务已启动。',
832
- });
833
-
834
- let consecutiveErrors = 0;
835
- let exhaustedScrolls = 0;
836
- let noMoreMarkerHits = 0;
837
- let fallbackBottomHits = 0;
838
- const noMoreMarkerConfirmations = 2;
839
- const exhaustedScrollLimit = targetCount ? 10 : 60;
840
- const fallbackBottomLimit = targetCount ? 4 : 12;
841
-
842
- try {
843
- while (shouldContinue(summary, targetCount)) {
844
- await this.checkpoint();
845
- if (this.resumeOpenBlockedUntil > Date.now()) {
846
- const remainMs = this.resumeOpenBlockedUntil - Date.now();
847
- this.logger.log(
848
- `简历查看冷却中:remaining=${Math.ceil(remainMs / 1000)}s,暂停打开新简历以避免频控。`,
849
- );
850
- await new Promise((resolve) => setTimeout(resolve, Math.min(remainMs, 30000)));
851
- continue;
852
- }
853
- await this.interaction.maybeRest(summary.inspected, this.logger);
854
- await this.checkpoint();
855
-
856
- let loadedCustomers = [];
857
- try {
858
- loadedCustomers = await this.page.getLoadedCustomers();
859
- } catch (error) {
860
- const message = String(error?.message || error || '');
861
- this.logger.log(`候选人扫描异常:${message}`);
862
- if (
863
- /CHAT_CARD_LIST_NOT_FOUND|CHAT_LIST_CONTAINER_NOT_FOUND|ACTIVE_TAB_IS_NOT_BOSS_CHAT_PAGE/.test(
864
- message,
865
- )
866
- ) {
867
- const delayedListWait = await this.waitForCandidateList({
868
- reason: `main-loop:${message}`,
869
- });
870
- if (delayedListWait.ready) {
871
- this.logger.log(
872
- `候选人列表延迟恢复成功:reason=main-loop:${message} | waited=${delayedListWait.waitedMs}ms | attempts=${delayedListWait.attempts} | count=${delayedListWait.listItemCount},继续重试扫描。`,
873
- );
874
- continue;
875
- }
876
- try {
877
- const recover = await this.page.recoverToChatIndex();
878
- this.logger.log(
879
- `页面恢复:changed=${recover.changed} | href=${recover.href || 'unknown'},准备重新预热并继续。`,
880
- );
881
- await this.interaction.sleepRange(900, 220);
882
- let recoveredFilterResult = await this.restoreListContext(profile);
883
- this.logger.log(
884
- `恢复后列表上下文:岗位=${profile.jobSelection?.label || profile.jobSelection?.value || '未知'};列表范围: ${filterLabel}${
885
- recoveredFilterResult.changed
886
- ? recoveredFilterResult.verified === false
887
- ? '(已尝试切换,未验证 active)'
888
- : '(已切换)'
889
- : '(已在目标筛选)'
890
- }${recoveredFilterResult?.activeLabel ? ` | active=${recoveredFilterResult.activeLabel}` : ''}`,
891
- );
892
- let recoveredListWait = await this.waitForCandidateList({
893
- reason: 'post-recovery-context-restore',
894
- });
895
- if (recoveredListWait.ready) {
896
- this.logger.log(
897
- `恢复后候选人列表已就绪:reason=post-recovery-context-restore | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount}`,
898
- );
899
- } else {
900
- this.logger.log(
901
- `恢复后候选人列表等待超时:reason=post-recovery-context-restore | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount} | lastError=${recoveredListWait.lastError || 'n/a'},继续尝试预热。`,
902
- );
903
- recoveredFilterResult = await this.restoreListContext(profile);
904
- this.logger.log(
905
- `恢复后二次应用列表上下文:岗位=${profile.jobSelection?.label || profile.jobSelection?.value || '未知'};列表范围: ${filterLabel}${
906
- recoveredFilterResult.changed
907
- ? recoveredFilterResult.verified === false
908
- ? '(已尝试切换,未验证 active)'
909
- : '(已切换)'
910
- : '(已在目标筛选)'
911
- }${recoveredFilterResult?.activeLabel ? ` | active=${recoveredFilterResult.activeLabel}` : ''}`,
912
- );
913
- await this.interaction.sleepRange(420, 160);
914
- recoveredListWait = await this.waitForCandidateList({
915
- reason: 'post-recovery-context-restore-reapply',
916
- });
917
- if (recoveredListWait.ready) {
918
- this.logger.log(
919
- `恢复后二次候选人列表恢复成功:reason=post-recovery-context-restore-reapply | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount}`,
920
- );
921
- } else {
922
- this.logger.log(
923
- `恢复后二次候选人列表等待仍超时:reason=post-recovery-context-restore-reapply | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount} | lastError=${recoveredListWait.lastError || 'n/a'},继续尝试预热。`,
924
- );
925
- }
926
- }
927
- const prime = await this.page.primeConversationByFirstCandidate();
928
- const candidate = prime?.candidate || {};
929
- const candidateBase = {
930
- customerId: candidate.customerId || '',
931
- name: candidate.name || '',
932
- sourceJob: candidate.sourceJob || '',
933
- domIndex: Number.isFinite(candidate.domIndex) ? candidate.domIndex : 0,
934
- textSnippet: '',
935
- };
936
- primedCustomer = {
937
- ...candidateBase,
938
- customerKey: createCustomerKey(candidateBase),
939
- };
940
- this.logger.log(
941
- `恢复后预热完成:name=${prime?.candidate?.name || '未知'} | id=${prime?.candidate?.customerId || '无'}`,
942
- );
943
- continue;
944
- } catch (recoverError) {
945
- throw new Error(
946
- `CHAT_LIST_RECOVERY_FAILED: ${recoverError?.message || recoverError}`,
947
- );
948
- }
949
- }
950
- throw error;
951
- }
952
-
953
- const shouldUsePrimedFirst =
954
- Boolean(primedCustomer) &&
955
- (startFrom === 'unread' || !this.stateStore.hasAny(createCustomerAliases(primedCustomer)));
956
- if (shouldUsePrimedFirst && primedCustomer) {
957
- this.logger.log(
958
- `优先处理预热候选人:name=${primedCustomer.name || '未知'} | key=${primedCustomer.customerKey}`,
959
- );
960
- const result = await this.processCustomer(primedCustomer, profile, runId, {
961
- skipCardClick: true,
962
- });
963
- primedCustomer = null;
964
- results.push(result);
965
- summary.inspected += 1;
966
-
967
- if (result.error) {
968
- summary.errors += 1;
969
- consecutiveErrors += 1;
970
- } else {
971
- consecutiveErrors = 0;
972
- }
973
- if (result.passed) summary.passed += 1;
974
- if (result.requested) summary.requested += 1;
975
- if (!result.passed && !result.error) summary.skipped += 1;
976
-
977
- this.logger.log(
978
- `候选人结果: ${result.name || '未知'} | ${result.passed ? 'passed' : result.error ? 'error' : 'skipped'}${result.reason ? ` | ${result.reason}` : ''}${result.error ? ` | ${result.error}` : ''}`,
979
- );
980
- this.logger.log(this.formatProgress(summary));
981
- this.emitProgress(summary, {
982
- stage: 'running',
983
- message: `已处理候选人:${result.name || '未知'}`,
984
- });
985
- exhaustedScrolls = 0;
986
- noMoreMarkerHits = 0;
987
- fallbackBottomHits = 0;
988
- if (consecutiveErrors >= 3) {
989
- this.logger.log('连续 3 位候选人处理失败,提前停止本轮运行。');
990
- break;
991
- }
992
- continue;
993
- }
994
- primedCustomer = null;
995
-
996
- this.logger.log(`候选人扫描:当前可见 ${loadedCustomers.length} 位`);
997
- const nextCustomer = loadedCustomers.find(
998
- (customer) => !this.stateStore.hasAny(createCustomerAliases(customer)),
999
- );
1000
-
1001
- if (!nextCustomer) {
1002
- const ratio = 0.52 + Math.random() * 0.34;
1003
- const scrollResult = await this.page.scrollCustomerList(ratio);
1004
- const noMoreDetected =
1005
- Boolean(scrollResult.noMoreDetectedAfter) || Boolean(scrollResult.noMoreDetectedBefore);
1006
- this.logger.log(
1007
- `列表滚动:ratio=${ratio.toFixed(2)} | didScroll=${Boolean(scrollResult.didScroll)} | top=${scrollResult.after?.top ?? 'n/a'} | atBottom=${Boolean(scrollResult.atBottom)} | noMore=${noMoreDetected}${scrollResult.noMoreTextAfter ? `(${scrollResult.noMoreTextAfter})` : ''} | scrollRetry=${exhaustedScrolls + 1}`,
1008
- );
1009
- if (noMoreDetected) {
1010
- noMoreMarkerHits += 1;
1011
- if (noMoreMarkerHits >= noMoreMarkerConfirmations) {
1012
- summary.exhausted = true;
1013
- this.logger.log('列表滚动终止:检测到“没有更多了”标识,判定为 exhausted。');
1014
- break;
1015
- }
1016
- await this.interaction.sleepRange(920, 260);
1017
- continue;
1018
- }
1019
-
1020
- noMoreMarkerHits = 0;
1021
- exhaustedScrolls = scrollResult.didScroll ? exhaustedScrolls + 1 : exhaustedScrolls + 2;
1022
- fallbackBottomHits = scrollResult.atBottom ? fallbackBottomHits + 1 : 0;
1023
- if (fallbackBottomHits >= fallbackBottomLimit && exhaustedScrolls >= Math.ceil(exhaustedScrollLimit / 2)) {
1024
- summary.exhausted = true;
1025
- this.logger.log('列表滚动终止:未发现“没有更多了”标识,但已多次触底且无可处理候选人,判定为 exhausted。');
1026
- break;
1027
- }
1028
- if (exhaustedScrolls >= exhaustedScrollLimit) {
1029
- summary.exhausted = true;
1030
- this.logger.log('列表滚动终止:连续无可处理候选人达到保护上限,判定为 exhausted。');
1031
- break;
1032
- }
1033
- await this.interaction.sleepRange(920, 260);
1034
- continue;
1035
- }
1036
-
1037
- exhaustedScrolls = 0;
1038
- noMoreMarkerHits = 0;
1039
- fallbackBottomHits = 0;
1040
- this.logger.log(
1041
- `准备处理候选人:name=${nextCustomer.name || '未知'} | key=${nextCustomer.customerKey} | job=${nextCustomer.sourceJob || '未知'} | domIndex=${nextCustomer.domIndex}`,
1042
- );
1043
- const result = await this.processCustomer(nextCustomer, profile, runId, {
1044
- skipCardClick: false,
1045
- });
1046
- results.push(result);
1047
- summary.inspected += 1;
1048
-
1049
- if (result.error) {
1050
- summary.errors += 1;
1051
- consecutiveErrors += 1;
1052
- } else {
1053
- consecutiveErrors = 0;
1054
- }
1055
- if (result.passed) summary.passed += 1;
1056
- if (result.requested) summary.requested += 1;
1057
- if (!result.passed && !result.error) summary.skipped += 1;
1058
-
1059
- this.logger.log(
1060
- `候选人结果: ${result.name || '未知'} | ${result.passed ? 'passed' : result.error ? 'error' : 'skipped'}${result.reason ? ` | ${result.reason}` : ''}${result.error ? ` | ${result.error}` : ''}`,
1061
- );
1062
- this.logger.log(this.formatProgress(summary));
1063
- this.emitProgress(summary, {
1064
- stage: 'running',
1065
- message: `已处理候选人:${result.name || '未知'}`,
1066
- });
1067
-
1068
- if (consecutiveErrors >= 3) {
1069
- this.logger.log('连续 3 位候选人处理失败,提前停止本轮运行。');
1070
- break;
1071
- }
1072
- }
1073
- } catch (error) {
1074
- if (error?.name !== 'StopRequestedError') {
1075
- throw error;
1076
- }
1077
- summary.stopped = true;
1078
- summary.stopReason = error.message;
1079
- this.emitProgress(summary, {
1080
- stage: 'running',
1081
- message: `运行停止:${summary.stopReason}`,
1082
- });
1083
- }
1084
-
1085
- try {
1086
- const finalClose = await this.cleanupPanels({
1087
- resumeMaxAttempts: 6,
1088
- detailMaxAttempts: 4,
1089
- ensureDismiss: true,
1090
- });
1091
- this.logger.log(
1092
- `运行收尾关闭弹层:resumeClosed=${finalClose.resume.closed} | resumeMethod=${finalClose.resume.method} | detailClosed=${finalClose.detail.closed} | detailMethod=${finalClose.detail.method}`,
1093
- );
1094
- } catch (cleanupError) {
1095
- this.logger.log(`运行收尾清理告警:${cleanupError?.message || cleanupError}`);
1096
- }
1097
-
1098
- summary.finishedAt = new Date().toISOString();
1099
- summary.reportPath = await this.reportStore.write(summary);
1100
- this.emitProgress(summary, {
1101
- stage: 'finalize',
1102
- message: summary.stopped ? '任务已停止并完成收尾。' : '任务执行完成。',
1103
- });
1104
- return summary;
1105
- }
1106
-
1107
- async processCustomer(customer, profile, runId, options = {}) {
1108
- const skipCardClick = Boolean(options?.skipCardClick);
1109
- const baseAliases = createCustomerAliases(customer);
1110
- const baseResult = {
1111
- customerKey: customer.customerKey,
1112
- name: customer.name || '',
1113
- sourceJob: customer.sourceJob || '',
1114
- decision: 'skipped',
1115
- passed: false,
1116
- requested: false,
1117
- reason: '',
1118
- error: '',
1119
- artifacts: {},
1120
- };
1121
-
1122
- let modalOpened = false;
1123
- try {
1124
- this.logger.log(`候选人开始:${customer.name || '未知'} (${customer.customerKey})`);
1125
- const preClose = await this.cleanupPanels({
1126
- resumeMaxAttempts: 4,
1127
- detailMaxAttempts: 3,
1128
- ensureDismiss: true,
1129
- });
1130
- if (
1131
- preClose.resume.method !== 'already-closed' ||
1132
- preClose.detail.method !== 'already-closed'
1133
- ) {
1134
- this.logger.log(
1135
- `候选人开始前清理残留面板:resumeClosed=${preClose.resume.closed} | resumeMethod=${preClose.resume.method} | detailClosed=${preClose.detail.closed} | detailMethod=${preClose.detail.method}`,
1136
- );
1137
- }
1138
- if (!skipCardClick) {
1139
- await this.checkpoint();
1140
- const drift = Math.round((Math.random() - 0.5) * 46);
1141
- this.logger.log(`卡片定位:domIndex=${customer.domIndex} | drift=${drift}`);
1142
- if (typeof this.page.activateCandidate === 'function') {
1143
- await this.page.activateCandidate(customer, drift);
1144
- } else {
1145
- const rect = await this.page.centerCustomerCard(customer.domIndex, drift);
1146
- await this.interaction.sleepRange(320, 120);
1147
- await this.checkpoint();
1148
- await this.interaction.clickRect(rect);
1149
- }
1150
- await this.interaction.sleepRange(860, 280);
1151
- let activated = await this.page.waitForCandidateActivated(customer, {
1152
- maxAttempts: 12,
1153
- delayMs: 220,
1154
- });
1155
- if (!activated?.matched) {
1156
- this.logger.log(
1157
- `候选人激活首次校验未命中,开始重试:expectedId=${customer.customerId || 'n/a'} | expectedName=${customer.name || 'n/a'} | activeId=${activated?.customerId || 'n/a'} | activeName=${activated?.name || 'n/a'}`,
1158
- );
1159
- for (let retry = 0; retry < 2; retry += 1) {
1160
- const retryDrift = Math.round((Math.random() - 0.5) * 36);
1161
- if (typeof this.page.activateCandidate === 'function') {
1162
- await this.page.activateCandidate(customer, retryDrift);
1163
- } else {
1164
- const retryRect = await this.page.centerCustomerCard(customer.domIndex, retryDrift);
1165
- await this.interaction.clickRect(retryRect);
1166
- }
1167
- await this.interaction.sleepRange(700, 200);
1168
- activated = await this.page.waitForCandidateActivated(customer, {
1169
- maxAttempts: 8,
1170
- delayMs: 180,
1171
- });
1172
- if (activated?.matched) break;
1173
- }
1174
- if (!activated?.matched) {
1175
- baseResult.decision = 'skipped';
1176
- baseResult.reason = `候选人上下文切换失败,已跳过避免误判(expected=${customer.name || customer.customerId || 'unknown'}, active=${activated?.name || activated?.customerId || 'unknown'})`;
1177
- this.logger.log(
1178
- `候选人跳过:name=${customer.name || '未知'} | key=${customer.customerKey} | reason=${baseResult.reason}`,
1179
- );
1180
- await this.stateStore.record(baseResult.customerKey, baseResult, baseAliases);
1181
- return baseResult;
1182
- }
1183
- }
1184
- } else {
1185
- this.logger.log('复用预热候选人上下文,跳过再次点击卡片。');
1186
- }
1187
- await this.checkpoint();
1188
- const readyState = await this.page.waitForConversationReady();
1189
- this.logger.log(
1190
- `会话面板就绪。onlineResume=${Boolean(readyState?.hasOnlineResume)} | askResume=${Boolean(readyState?.hasAskResume)} | attachmentResume=${Boolean(readyState?.hasAttachmentResume)} | attachmentResumeEnabled=${Boolean(readyState?.attachmentResumeEnabled)}`,
1191
- );
1192
- if (readyState?.attachmentResumeEnabled) {
1193
- baseResult.decision = 'skipped';
1194
- baseResult.reason = '检测到附件简历按钮可用,按策略跳过,不进入在线简历截图与LLM评估。';
1195
- baseResult.artifacts.attachmentResume = {
1196
- present: Boolean(readyState?.hasAttachmentResume),
1197
- enabled: Boolean(readyState?.attachmentResumeEnabled),
1198
- className: String(readyState?.attachmentResumeClass || ''),
1199
- };
1200
- this.logger.log(
1201
- `候选人跳过:name=${customer.name || '未知'} | key=${customer.customerKey} | reason=${baseResult.reason}`,
1202
- );
1203
- await this.stateStore.record(baseResult.customerKey, baseResult, baseAliases);
1204
- return baseResult;
1205
- }
1206
- if (!readyState?.hasOnlineResume) {
1207
- throw new Error('ONLINE_RESUME_UNAVAILABLE');
1208
- }
1209
-
1210
- const candidateToken = safePathToken(customer.customerKey || customer.name || 'candidate');
1211
- const artifactDir = path.join(this.artifactRootDir, runId, candidateToken);
1212
- await mkdir(artifactDir, { recursive: true });
1213
-
1214
- const acquisition = await this.acquireResumeAndEvaluate(
1215
- customer,
1216
- profile,
1217
- artifactDir,
1218
- baseResult,
1219
- );
1220
- const evaluation = acquisition.evaluation;
1221
- const capture = acquisition.capture;
1222
- modalOpened = Boolean(acquisition.modalOpened);
1223
- const finalReason =
1224
- normalizeText(evaluation.reason || evaluation.summary || evaluation.cot) ||
1225
- (evaluation.passed ? 'LLM判定通过' : 'LLM判定不通过');
1226
- this.logger.log(
1227
- `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'}`,
1228
- );
1229
-
1230
- baseResult.reason = finalReason;
1231
- baseResult.passed = evaluation.passed;
1232
- baseResult.decision = evaluation.passed ? 'passed' : 'skipped';
1233
- baseResult.artifacts.finalPassed = Boolean(evaluation.passed);
1234
- baseResult.artifacts.evaluationMode = String(evaluation.evaluationMode || '');
1235
- baseResult.artifacts.evaluationImageCount = Number.isFinite(Number(evaluation.imageCount))
1236
- ? Number(evaluation.imageCount)
1237
- : Array.isArray(baseResult.artifacts.modelImagePaths)
1238
- ? baseResult.artifacts.modelImagePaths.length
1239
- : 0;
1240
- baseResult.artifacts.evaluationChunkIndex = Number.isFinite(Number(evaluation.chunkIndex))
1241
- ? Number(evaluation.chunkIndex)
1242
- : null;
1243
- baseResult.artifacts.evaluationChunkTotal = Number.isFinite(Number(evaluation.chunkTotal))
1244
- ? Number(evaluation.chunkTotal)
1245
- : null;
1246
- baseResult.artifacts.evaluationAggregateRetryUsed = evaluation.aggregateRetryUsed === true;
1247
- baseResult.artifacts.llmReason = normalizeText(evaluation.reason || '');
1248
- baseResult.artifacts.llmSummary = normalizeText(evaluation.summary || '');
1249
- baseResult.artifacts.llmCot = normalizeText(evaluation.cot || '');
1250
- baseResult.artifacts.llmEvidence = toStringArray(evaluation.evidence);
1251
- baseResult.artifacts.llmRawReasoning = String(evaluation.rawReasoningText || '');
1252
- baseResult.artifacts.llmRawOutput = String(evaluation.rawOutputText || '');
1253
- baseResult.artifacts.resumeAcquisitionMode = String(acquisition.acquisitionMode || '');
1254
- baseResult.artifacts.resumeAcquisitionReason = String(acquisition.acquisitionReason || '');
1255
- baseResult.artifacts.initialNetworkWaitMs = Number(acquisition.timings?.initialNetworkWaitMs || 0);
1256
- baseResult.artifacts.networkRetryMs = Number(acquisition.timings?.networkRetryMs || 0);
1257
- baseResult.artifacts.imageCaptureMs = Number(acquisition.timings?.imageCaptureMs || 0);
1258
- baseResult.artifacts.imageModelMs = Number(acquisition.timings?.imageModelMs || 0);
1259
- baseResult.artifacts.lateNetworkRetryMs = Number(acquisition.timings?.lateNetworkRetryMs || 0);
1260
- baseResult.artifacts.domFallbackMs = Number(acquisition.timings?.domFallbackMs || 0);
1261
- baseResult.artifacts.textModelMs = Number(acquisition.timings?.textModelMs || 0);
1262
- if (acquisition.sourceCandidateInfo) {
1263
- baseResult.artifacts.resumeProfile = {
1264
- primarySchool: normalizeText(acquisition.sourceCandidateInfo.school || ''),
1265
- schools: Array.isArray(acquisition.sourceCandidateInfo.schools)
1266
- ? acquisition.sourceCandidateInfo.schools
1267
- : [],
1268
- major: normalizeText(acquisition.sourceCandidateInfo.major || ''),
1269
- majors: Array.isArray(acquisition.sourceCandidateInfo.majors)
1270
- ? acquisition.sourceCandidateInfo.majors
1271
- : [],
1272
- company: normalizeText(acquisition.sourceCandidateInfo.company || ''),
1273
- position: normalizeText(acquisition.sourceCandidateInfo.position || ''),
1274
- resumeTextLength: String(acquisition.sourceCandidateInfo.resumeText || '').length,
1275
- evidenceCorpusLength: String(
1276
- acquisition.sourceCandidateInfo.evidenceCorpus || acquisition.sourceCandidateInfo.resumeText || '',
1277
- ).length,
1278
- };
1279
- }
1280
- if (this.resumeNetworkTracker) {
1281
- baseResult.artifacts.resumeNetworkMode = this.resumeNetworkTracker.getResumeAcquisitionState().mode;
1282
- baseResult.artifacts.resumeNetworkModeReason =
1283
- this.resumeNetworkTracker.getResumeAcquisitionState().reason;
1284
- baseResult.artifacts.resumeNetworkDiagnostics =
1285
- this.resumeNetworkTracker.resumeNetworkDiagnostics.slice(-12);
1286
- }
1287
-
1288
- await this.checkpoint();
1289
- const closeResult =
1290
- typeof this.page.closeResumeModalDomOnce === 'function'
1291
- ? await this.page.closeResumeModalDomOnce()
1292
- : await this.page.closeResumeModal({ maxAttempts: 6, ensureDismiss: true });
1293
- modalOpened = false;
1294
- baseResult.artifacts.resumeCloseMethod = closeResult.method;
1295
- baseResult.artifacts.resumeClosed = closeResult.closed;
1296
- this.logger.log(
1297
- `简历关闭结果:closed=${closeResult.closed} | method=${closeResult.method} | scope=${closeResult?.finalState?.scopeCount ?? 'n/a'} | iframe=${closeResult?.finalState?.iframeCount ?? 'n/a'} | close=${closeResult?.finalState?.closeCount ?? 'n/a'} | class=${closeResult?.finalState?.topScopeClass || 'n/a'}`,
1298
- );
1299
- if (!closeResult.closed) {
1300
- baseResult.artifacts.resumeCloseWarning = 'resume modal not fully closed by single DOM close';
1301
- }
1302
-
1303
- if (evaluation.passed && !this.dryRun) {
1304
- await this.checkpoint();
1305
- const preAction = await this.ensurePanelsClosedBeforeOutreach({
1306
- initialResumeCloseResult: closeResult,
1307
- });
1308
- Object.assign(baseResult.artifacts, preAction.diagnostics);
1309
- if (!preAction.ok) {
1310
- baseResult.decision = 'skipped';
1311
- baseResult.passed = false;
1312
- baseResult.requested = false;
1313
- baseResult.reason =
1314
- '发送前未能安全关闭简历/详情面板,已跳过避免触发风控';
1315
- this.logger.log(
1316
- `候选人跳过:name=${customer.name || '未知'} | key=${customer.customerKey} | reason=${baseResult.reason} | cleanupFailure=${preAction.diagnostics.preActionCleanupFailureReason || 'unknown'}`,
1317
- );
1318
- const finalPanels = await this.cleanupPanels({
1319
- resumeMaxAttempts: 4,
1320
- detailMaxAttempts: 4,
1321
- ensureDismiss: true,
1322
- });
1323
- baseResult.artifacts.finalResumeCloseMethod = finalPanels.resume.method;
1324
- baseResult.artifacts.finalResumeClosed = finalPanels.resume.closed;
1325
- baseResult.artifacts.finalDetailCloseMethod = finalPanels.detail.method;
1326
- baseResult.artifacts.finalDetailClosed = finalPanels.detail.closed;
1327
- await this.stateStore.record(baseResult.customerKey, baseResult, baseAliases);
1328
- return baseResult;
1329
- }
1330
-
1331
- const greetingText = resolveGreetingText(profile);
1332
- this.logger.log(`候选人通过,先发送消息:${greetingText}`);
1333
- await this.checkpoint();
1334
- const editorState = await this.page.setEditorMessage(greetingText);
1335
- if (!normalizeText(editorState?.value || '').includes(normalizeText(greetingText))) {
1336
- throw new Error('CHAT_EDITOR_MESSAGE_MISMATCH');
1337
- }
1338
- this.logger.log(
1339
- `招呼语写入输入框:activeSubmit=${Boolean(editorState?.activeSubmit)} | valueLen=${String(editorState?.value || '').length}`,
1340
- );
1341
- await this.interaction.sleepRange(320, 120);
1342
- await this.checkpoint();
1343
- const sendResult = await this.page.sendMessage(greetingText);
1344
- if (!sendResult?.sent) {
1345
- throw new Error(
1346
- `CHAT_GREETING_SEND_FAILED(method=${sendResult?.method || 'unknown'},editorAfter=${sendResult?.editorAfter || ''})`,
1347
- );
1348
- }
1349
- baseResult.artifacts.greetingMessage = greetingText;
1350
- baseResult.artifacts.greetingSent = Boolean(sendResult?.sent);
1351
- baseResult.artifacts.greetingSendMethod = sendResult?.method || 'unknown';
1352
- this.logger.log(
1353
- `招呼语发送结果:sent=${Boolean(sendResult?.sent)} | method=${sendResult?.method || 'unknown'} | cleared=${Boolean(sendResult?.cleared)} | editorAfter=${sendResult?.editorAfter || ''}`,
1354
- );
1355
- await this.interaction.sleepRange(360, 120);
1356
-
1357
- this.logger.log('候选人通过,执行求简历动作。');
1358
- const maxRequestAttempts = 3;
1359
- let requestSucceeded = false;
1360
- let lastAttempt = null;
1361
-
1362
- for (let requestAttempt = 0; requestAttempt < maxRequestAttempts; requestAttempt += 1) {
1363
- await this.checkpoint();
1364
- const messageBefore =
1365
- typeof this.page.getResumeRequestMessageState === 'function'
1366
- ? await this.page.getResumeRequestMessageState()
1367
- : { ok: false, count: 0, lastText: '', recent: [] };
1368
- const askResult = await this.page.clickAskResume();
1369
- await this.interaction.sleepRange(460, 150);
1370
-
1371
- let confirmResult = {
1372
- confirmed: false,
1373
- requestedVerified: false,
1374
- assumedRequested: false,
1375
- uiState: null,
1376
- };
1377
- if (!askResult?.alreadyRequested) {
1378
- await this.checkpoint();
1379
- confirmResult = await this.page.clickConfirmRequestResume();
1380
- }
1381
-
1382
- let messageObserved = false;
1383
- let messageAfter = null;
1384
- if (typeof this.page.waitForResumeRequestMessage === 'function') {
1385
- const messageCheck = await this.page.waitForResumeRequestMessage({
1386
- baselineCount: Number(messageBefore?.count || 0),
1387
- timeoutMs: 7000,
1388
- pollMs: 260,
1389
- });
1390
- messageAfter = messageCheck?.state || null;
1391
- messageObserved = Boolean(messageCheck?.observed) || hasResumeRequestSentMessage(messageAfter || {});
1392
- }
1393
-
1394
- const requestedVerified = Boolean(messageObserved);
1395
- lastAttempt = {
1396
- attempt: requestAttempt + 1,
1397
- askResult,
1398
- confirmResult,
1399
- messageBefore,
1400
- messageAfter,
1401
- messageObserved,
1402
- requestedVerified,
1403
- };
1404
-
1405
- if (messageAfter) {
1406
- baseResult.artifacts.resumeRequestMessageBefore = Number(messageBefore?.count || 0);
1407
- baseResult.artifacts.resumeRequestMessageAfter = Number(messageAfter?.count || 0);
1408
- baseResult.artifacts.resumeRequestMessageObserved = messageObserved;
1409
- baseResult.artifacts.resumeRequestMessageLastText = String(messageAfter?.lastText || '');
1410
- }
1411
-
1412
- this.logger.log(
1413
- `求简历动作检查:attempt=${requestAttempt + 1}/${maxRequestAttempts} | alreadyRequested=${Boolean(askResult?.alreadyRequested)} | confirmed=${Boolean(confirmResult?.confirmed)} | disabledOperateAsk=${Boolean(confirmResult?.uiState?.hasDisabledOperateAsk)} | messageObserved=${messageObserved} | verified=${requestedVerified} | assumed=${Boolean(confirmResult?.assumedRequested)}`,
1414
- );
1415
-
1416
- if (requestedVerified) {
1417
- requestSucceeded = true;
1418
- break;
1419
- }
1420
-
1421
- if (requestAttempt < maxRequestAttempts - 1) {
1422
- this.logger.log('未检测到“简历请求已发送”提示,重新发起求简历。');
1423
- await this.interaction.sleepRange(640, 180);
1424
- }
1425
- }
1426
-
1427
- baseResult.requested = requestSucceeded;
1428
- if (!requestSucceeded) {
1429
- const confirmStateText = JSON.stringify(lastAttempt?.confirmResult?.uiState || {});
1430
- throw new Error(
1431
- `REQUEST_RESUME_MESSAGE_NOT_OBSERVED(state=${confirmStateText},messageBefore=${Number(lastAttempt?.messageBefore?.count || 0)},messageAfter=${Number(lastAttempt?.messageAfter?.count || 0)},attempts=${maxRequestAttempts})`,
1432
- );
1433
- }
1434
- }
1435
-
1436
- const finalPanels = await this.cleanupPanels({
1437
- resumeMaxAttempts: 4,
1438
- detailMaxAttempts: 4,
1439
- ensureDismiss: true,
1440
- });
1441
- baseResult.artifacts.finalResumeCloseMethod = finalPanels.resume.method;
1442
- baseResult.artifacts.finalResumeClosed = finalPanels.resume.closed;
1443
- baseResult.artifacts.finalDetailCloseMethod = finalPanels.detail.method;
1444
- baseResult.artifacts.finalDetailClosed = finalPanels.detail.closed;
1445
- if (
1446
- finalPanels.resume.method !== 'already-closed' ||
1447
- finalPanels.detail.method !== 'already-closed'
1448
- ) {
1449
- this.logger.log(
1450
- `候选人收尾清理:resumeClosed=${finalPanels.resume.closed} | resumeMethod=${finalPanels.resume.method} | detailClosed=${finalPanels.detail.closed} | detailMethod=${finalPanels.detail.method}`,
1451
- );
1452
- }
1453
-
1454
- await this.stateStore.record(baseResult.customerKey, baseResult, baseAliases);
1455
- return baseResult;
1456
- } catch (error) {
1457
- if (error?.name === 'StopRequestedError') {
1458
- throw error;
1459
- }
1460
-
1461
- if (modalOpened || typeof this.page.closeCandidateDetailDomOnce === 'function' || typeof this.page.closeCandidateDetail === 'function') {
1462
- try {
1463
- const closeResult = await this.cleanupPanels({
1464
- resumeMaxAttempts: 6,
1465
- detailMaxAttempts: 4,
1466
- ensureDismiss: true,
1467
- });
1468
- baseResult.artifacts.resumeCloseMethod = closeResult.resume.method;
1469
- baseResult.artifacts.resumeClosed = closeResult.resume.closed;
1470
- baseResult.artifacts.finalDetailCloseMethod = closeResult.detail.method;
1471
- baseResult.artifacts.finalDetailClosed = closeResult.detail.closed;
1472
- this.logger.log(
1473
- `异常后关闭面板结果:resumeClosed=${closeResult.resume.closed} | resumeMethod=${closeResult.resume.method} | resumeScope=${closeResult?.resume?.finalState?.scopeCount ?? 'n/a'} | resumeIframe=${closeResult?.resume?.finalState?.iframeCount ?? 'n/a'} | resumeClose=${closeResult?.resume?.finalState?.closeCount ?? 'n/a'} | resumeClass=${closeResult?.resume?.finalState?.topScopeClass || 'n/a'} | detailClosed=${closeResult.detail.closed} | detailMethod=${closeResult.detail.method} | detailPanels=${closeResult?.detail?.finalState?.panelCount ?? 'n/a'} | detailClose=${closeResult?.detail?.finalState?.closeCount ?? 'n/a'} | detailClass=${closeResult?.detail?.finalState?.topPanelClass || 'n/a'}`,
1474
- );
1475
- } catch {}
1476
- }
1477
-
1478
- const message = error.message || String(error);
1479
- if (
1480
- /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(
1481
- message,
1482
- )
1483
- ) {
1484
- baseResult.decision = 'skipped';
1485
- baseResult.reason = `在线简历不可用或未加载,已跳过该候选人(${message})`;
1486
- baseResult.artifacts.resumeUnavailable = true;
1487
- this.logger.log(
1488
- `候选人跳过:name=${customer.name || '未知'} | key=${customer.customerKey} | reason=${baseResult.reason}`,
1489
- );
1490
- } else {
1491
- baseResult.error = message;
1492
- baseResult.decision = 'error';
1493
- this.logger.log(
1494
- `候选人处理异常:name=${customer.name || '未知'} | key=${customer.customerKey} | error=${baseResult.error}`,
1495
- );
1496
- }
1497
- await this.stateStore.record(baseResult.customerKey, baseResult, baseAliases);
1498
- return baseResult;
1499
- }
1500
- }
1501
- }