@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.
@@ -0,0 +1,727 @@
1
+ const RESUME_INFO_URL_PATTERNS = [
2
+ /\/wapi\/zpjob\/view\/geek\/info\b/i,
3
+ /\/wapi\/zpitem\/web\/boss\/[^?#]*\/geek\/info\b/i,
4
+ /\/boss\/[^?#]*\/geek\/info\b/i,
5
+ /\/geek\/info\b/i,
6
+ /[?&](?:geekid|geek_id|encryptgeekid|encryptjid|jid|securityid)=/i,
7
+ ];
8
+ const RESUME_RELATED_KEYWORDS = ['geek', 'resume', 'candidate', 'friend'];
9
+ const NETWORK_POLL_MS = 120;
10
+
11
+ export const NETWORK_RESUME_WAIT_MS = 4200;
12
+ export const NETWORK_RESUME_RETRY_WAIT_MS = 2000;
13
+ export const NETWORK_RESUME_IMAGE_MODE_GRACE_MS = 1000;
14
+ export const NETWORK_RESUME_LATE_RETRY_MS = 3000;
15
+
16
+ function sleep(ms) {
17
+ return new Promise((resolve) => setTimeout(resolve, ms));
18
+ }
19
+
20
+ function normalizeText(value) {
21
+ return String(value || '').replace(/\s+/g, ' ').trim();
22
+ }
23
+
24
+ function toLowerSafe(value) {
25
+ return String(value || '').toLowerCase();
26
+ }
27
+
28
+ function stripHtml(value) {
29
+ return String(value || '')
30
+ .replace(/<[^>]+>/g, '')
31
+ .replace(/\s+/g, ' ')
32
+ .trim();
33
+ }
34
+
35
+ function parseGeekIdFromUrl(url) {
36
+ const raw = normalizeText(url);
37
+ if (!raw) return null;
38
+ try {
39
+ const parsed = new URL(raw);
40
+ const keys = ['geekId', 'geek_id', 'gid', 'encryptGeekId', 'encryptJid', 'jid', 'securityId'];
41
+ for (const key of keys) {
42
+ const value = normalizeText(parsed.searchParams.get(key) || '');
43
+ if (value) return value;
44
+ }
45
+ } catch {}
46
+ const matched = raw.match(/[?&](?:geekId|geek_id|gid|encryptGeekId|encryptJid|jid|securityId)=([^&]+)/i);
47
+ if (matched?.[1]) return decodeURIComponent(matched[1]);
48
+ return null;
49
+ }
50
+
51
+ function parseGeekIdFromPostData(postData) {
52
+ const raw = normalizeText(postData);
53
+ if (!raw) return null;
54
+ const keys = ['geekId', 'geek_id', 'gid', 'encryptGeekId', 'encryptJid', 'jid', 'securityId'];
55
+ try {
56
+ const parsed = JSON.parse(raw);
57
+ if (parsed && typeof parsed === 'object') {
58
+ const queue = [parsed];
59
+ while (queue.length > 0) {
60
+ const current = queue.shift();
61
+ if (!current || typeof current !== 'object') continue;
62
+ for (const key of keys) {
63
+ const value = normalizeText(current[key] || '');
64
+ if (value) return value;
65
+ }
66
+ for (const value of Object.values(current)) {
67
+ if (value && typeof value === 'object') {
68
+ queue.push(value);
69
+ }
70
+ }
71
+ }
72
+ }
73
+ } catch {}
74
+ const matched = raw.match(
75
+ /(?:^|[?&,\s"'])?(?:geekId|geek_id|gid|encryptGeekId|encryptJid|jid|securityId)(?:["']?\s*[:=]\s*["']?)([^&,"'\s}]+)/i,
76
+ );
77
+ if (matched?.[1]) return decodeURIComponent(matched[1]);
78
+ return null;
79
+ }
80
+
81
+ function collectGeekIdsFromPayload(payload, fallbackGeekId = null) {
82
+ if (!payload || typeof payload !== 'object') return [];
83
+ const geekDetail = payload?.geekDetail || payload;
84
+ const baseInfo = geekDetail?.geekBaseInfo || {};
85
+ const ids = [
86
+ fallbackGeekId,
87
+ baseInfo.geekId,
88
+ baseInfo.encryptGeekId,
89
+ baseInfo.securityId,
90
+ geekDetail?.geekId,
91
+ geekDetail?.encryptGeekId,
92
+ geekDetail?.securityId,
93
+ payload?.geekId,
94
+ payload?.encryptGeekId,
95
+ payload?.securityId,
96
+ ]
97
+ .map((value) => normalizeText(value))
98
+ .filter(Boolean);
99
+ return Array.from(new Set(ids));
100
+ }
101
+
102
+ function hasResumePayloadShape(payload) {
103
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return false;
104
+ const geekDetail =
105
+ payload?.geekDetail && typeof payload.geekDetail === 'object' ? payload.geekDetail : payload;
106
+ const baseInfo = geekDetail?.geekBaseInfo || {};
107
+ const hasIdentity = Boolean(
108
+ normalizeText(
109
+ baseInfo?.name ||
110
+ geekDetail?.geekName ||
111
+ payload?.geekName ||
112
+ baseInfo?.geekId ||
113
+ baseInfo?.encryptGeekId ||
114
+ baseInfo?.securityId ||
115
+ geekDetail?.geekId ||
116
+ geekDetail?.encryptGeekId ||
117
+ geekDetail?.securityId ||
118
+ payload?.geekId ||
119
+ payload?.encryptGeekId ||
120
+ payload?.securityId ||
121
+ '',
122
+ ),
123
+ );
124
+ const hasResumeSections = [
125
+ geekDetail?.geekExpectList,
126
+ geekDetail?.geekWorkExpList,
127
+ geekDetail?.geekProjExpList,
128
+ geekDetail?.geekEduExpList,
129
+ geekDetail?.geekEducationList,
130
+ geekDetail?.geekSkillList,
131
+ ].some((section) => Array.isArray(section) && section.length > 0);
132
+ const hasResumeTextFields = Boolean(
133
+ normalizeText(geekDetail?.geekAdvantage || baseInfo?.userDesc || baseInfo?.userDescription || ''),
134
+ );
135
+ return hasIdentity && (hasResumeSections || hasResumeTextFields);
136
+ }
137
+
138
+ function findResumePayloadInObject(root, maxDepth = 4, visited = new Set()) {
139
+ if (root === null || root === undefined || maxDepth < 0) return null;
140
+ if (typeof root !== 'object') return null;
141
+ if (visited.has(root)) return null;
142
+ visited.add(root);
143
+
144
+ if (hasResumePayloadShape(root)) {
145
+ return root;
146
+ }
147
+ if (maxDepth === 0) return null;
148
+
149
+ if (Array.isArray(root)) {
150
+ for (const item of root) {
151
+ const found = findResumePayloadInObject(item, maxDepth - 1, visited);
152
+ if (found) return found;
153
+ }
154
+ return null;
155
+ }
156
+
157
+ for (const key of ['zpData', 'data', 'result', 'geekDetail', 'detail', 'info']) {
158
+ if (!(key in root)) continue;
159
+ const found = findResumePayloadInObject(root[key], maxDepth - 1, visited);
160
+ if (found) return found;
161
+ }
162
+ for (const value of Object.values(root)) {
163
+ const found = findResumePayloadInObject(value, maxDepth - 1, visited);
164
+ if (found) return found;
165
+ }
166
+ return null;
167
+ }
168
+
169
+ function extractResumePayloadFromResponseBody(parsedBody) {
170
+ return findResumePayloadInObject(parsedBody, 4) || null;
171
+ }
172
+
173
+ function isResumeInfoRequestUrl(url) {
174
+ const normalizedUrl = normalizeText(url).toLowerCase();
175
+ if (!normalizedUrl || !normalizedUrl.includes('/wapi/')) return false;
176
+ return RESUME_INFO_URL_PATTERNS.some((pattern) => pattern.test(normalizedUrl));
177
+ }
178
+
179
+ function isResumeRelatedWapiUrl(url) {
180
+ const normalizedUrl = normalizeText(url).toLowerCase();
181
+ if (!normalizedUrl || !normalizedUrl.includes('/wapi/')) return false;
182
+ return RESUME_RELATED_KEYWORDS.some((keyword) => normalizedUrl.includes(String(keyword).toLowerCase()));
183
+ }
184
+
185
+ function formatResumeTimeRange(exp = {}) {
186
+ const start =
187
+ normalizeText(exp.startYearMonStr || exp.startYearStr || exp.startDateDesc || exp.startDate || '') || '';
188
+ const end =
189
+ normalizeText(exp.endYearMonStr || exp.endYearStr || exp.endDateDesc || exp.endDate || '') || '';
190
+ if (start && end) return `${start} - ${end}`;
191
+ if (start) return `${start} - 至今`;
192
+ if (end) return `至 ${end}`;
193
+ return '';
194
+ }
195
+
196
+ function preferReadableName(...values) {
197
+ const normalized = values.map((item) => normalizeText(item)).filter(Boolean);
198
+ if (normalized.length <= 0) return '';
199
+ const nonMasked = normalized.find((item) => !/[**]/.test(item));
200
+ return nonMasked || normalized[0];
201
+ }
202
+
203
+ function formatResumeApiData(data) {
204
+ const parts = [];
205
+ const geekDetail = data?.geekDetail || data?.geekDetailInfo || data || {};
206
+ const baseInfo = geekDetail.geekBaseInfo || {};
207
+ const expectList = Array.isArray(geekDetail.geekExpectList)
208
+ ? geekDetail.geekExpectList
209
+ : Array.isArray(geekDetail.geekExpPosList)
210
+ ? geekDetail.geekExpPosList
211
+ : [];
212
+ const workExpList = Array.isArray(geekDetail.geekWorkExpList) ? geekDetail.geekWorkExpList : [];
213
+ const projExpList = Array.isArray(geekDetail.geekProjExpList) ? geekDetail.geekProjExpList : [];
214
+ const eduExpList = Array.isArray(geekDetail.geekEduExpList)
215
+ ? geekDetail.geekEduExpList
216
+ : Array.isArray(geekDetail.geekEducationList)
217
+ ? geekDetail.geekEducationList
218
+ : [];
219
+ const skillList = Array.isArray(geekDetail.geekSkillList)
220
+ ? geekDetail.geekSkillList
221
+ : Array.isArray(geekDetail.skillList)
222
+ ? geekDetail.skillList
223
+ : [];
224
+ const certificationList = Array.isArray(geekDetail.geekCertificationList)
225
+ ? geekDetail.geekCertificationList
226
+ : [];
227
+
228
+ parts.push('=== 基本信息 ===');
229
+ if (baseInfo.name) parts.push(`姓名: ${baseInfo.name}`);
230
+ if (baseInfo.ageDesc) parts.push(`年龄: ${baseInfo.ageDesc}`);
231
+ if (baseInfo.degreeCategory) parts.push(`学历: ${baseInfo.degreeCategory}`);
232
+ if (baseInfo.workYearDesc) parts.push(`工作经验: ${baseInfo.workYearDesc}`);
233
+ if (baseInfo.activeTimeDesc) parts.push(`活跃状态: ${baseInfo.activeTimeDesc}`);
234
+ if (baseInfo.applyStatusContent) parts.push(`求职状态: ${baseInfo.applyStatusContent}`);
235
+
236
+ if (expectList.length > 0) {
237
+ parts.push('\n=== 期望工作 ===');
238
+ expectList.forEach((expect, index) => {
239
+ const line = [
240
+ `${index + 1}.`,
241
+ normalizeText(expect.locationName || ''),
242
+ normalizeText(expect.positionName || ''),
243
+ normalizeText(expect.salaryDesc || ''),
244
+ normalizeText(expect.industryDesc || ''),
245
+ ]
246
+ .filter(Boolean)
247
+ .join(' | ');
248
+ if (line) parts.push(line);
249
+ });
250
+ }
251
+
252
+ const advantage = stripHtml(geekDetail.geekAdvantage || baseInfo.userDesc || baseInfo.userDescription || '');
253
+ if (advantage) {
254
+ parts.push('\n=== 个人优势 ===');
255
+ parts.push(advantage);
256
+ }
257
+
258
+ if (workExpList.length > 0) {
259
+ parts.push('\n=== 工作经历 ===');
260
+ workExpList.forEach((exp, index) => {
261
+ const company = normalizeText(exp.company || '');
262
+ const position = stripHtml(exp.positionName || exp.position || '');
263
+ const range = formatResumeTimeRange(exp);
264
+ const responsibility = stripHtml(exp.responsibility || exp.workContent || '');
265
+ const performance = stripHtml(exp.workPerformance || exp.performance || '');
266
+ parts.push(`${index + 1}. ${[company, position].filter(Boolean).join(' - ')}`.trim());
267
+ if (range) parts.push(` 时间: ${range}`);
268
+ if (responsibility) parts.push(` 职责: ${responsibility}`);
269
+ if (performance) parts.push(` 成果: ${performance}`);
270
+ });
271
+ }
272
+
273
+ if (projExpList.length > 0) {
274
+ parts.push('\n=== 项目经历 ===');
275
+ projExpList.forEach((exp, index) => {
276
+ const projectName = normalizeText(exp.name || exp.projectName || '');
277
+ const role = stripHtml(exp.roleName || exp.role || '');
278
+ const range = formatResumeTimeRange(exp);
279
+ const desc = stripHtml(exp.projectDescription || exp.description || '');
280
+ parts.push(`${index + 1}. ${[projectName, role].filter(Boolean).join(' - ')}`.trim());
281
+ if (range) parts.push(` 时间: ${range}`);
282
+ if (desc) parts.push(` 描述: ${desc}`);
283
+ });
284
+ }
285
+
286
+ if (eduExpList.length > 0) {
287
+ parts.push('\n=== 教育经历 ===');
288
+ eduExpList.forEach((exp, index) => {
289
+ const school = normalizeText(exp.schoolName || exp.school || '');
290
+ const major = normalizeText(exp.majorName || exp.major || '');
291
+ const degree = normalizeText(exp.degreeName || exp.degree || exp.education || '');
292
+ const range = formatResumeTimeRange(exp);
293
+ parts.push(`${index + 1}. ${[school, major, degree].filter(Boolean).join(' - ')}`.trim());
294
+ if (range) parts.push(` 时间: ${range}`);
295
+ });
296
+ }
297
+
298
+ if (skillList.length > 0) {
299
+ parts.push('\n=== 技能 ===');
300
+ parts.push(
301
+ skillList
302
+ .map((item) => normalizeText(item.skillName || item.name || item))
303
+ .filter(Boolean)
304
+ .join('、'),
305
+ );
306
+ }
307
+
308
+ if (certificationList.length > 0) {
309
+ parts.push('\n=== 证书 ===');
310
+ parts.push(
311
+ certificationList
312
+ .map((item) => normalizeText(item.certificationName || item.name || item))
313
+ .filter(Boolean)
314
+ .join('、'),
315
+ );
316
+ }
317
+
318
+ const firstEducation = eduExpList[0] || {};
319
+ const firstWork = workExpList[0] || {};
320
+ return {
321
+ name: preferReadableName(baseInfo.name || '', geekDetail.geekName || ''),
322
+ school: normalizeText(firstEducation.schoolName || firstEducation.school || ''),
323
+ major: normalizeText(firstEducation.majorName || firstEducation.major || ''),
324
+ company: normalizeText(firstWork.company || ''),
325
+ position: normalizeText(firstWork.positionName || firstWork.position || ''),
326
+ resumeText: parts.join('\n').trim(),
327
+ };
328
+ }
329
+
330
+ function normalizeNameForCompare(value) {
331
+ return normalizeText(value).replace(/[**]/g, '');
332
+ }
333
+
334
+ function isLikelyNameMatch(expected, actual) {
335
+ const left = normalizeNameForCompare(expected);
336
+ const right = normalizeNameForCompare(actual);
337
+ if (!left || !right) return true;
338
+ if (left === right) return true;
339
+ if (left.includes(right) || right.includes(left)) return true;
340
+ return left[0] === right[0];
341
+ }
342
+
343
+ function isLikelyTextMatch(expected, actual) {
344
+ const left = toLowerSafe(normalizeText(expected));
345
+ const right = toLowerSafe(normalizeText(actual));
346
+ if (!left || !right) return true;
347
+ return left === right || left.includes(right) || right.includes(left);
348
+ }
349
+
350
+ export function isDomProfileConsistentWithCard(cardProfile, domProfile) {
351
+ if (!cardProfile || !domProfile) return true;
352
+ let compared = 0;
353
+ let mismatched = 0;
354
+ const compareField = (field, matcher) => {
355
+ const expected = normalizeText(cardProfile?.[field] || '');
356
+ const actual = normalizeText(domProfile?.[field] || '');
357
+ if (!expected || !actual) return;
358
+ compared += 1;
359
+ if (!matcher(expected, actual)) {
360
+ mismatched += 1;
361
+ }
362
+ };
363
+ compareField('name', isLikelyNameMatch);
364
+ compareField('school', isLikelyTextMatch);
365
+ compareField('major', isLikelyTextMatch);
366
+ if (compared <= 0) return true;
367
+ return mismatched <= 1;
368
+ }
369
+
370
+ function buildCandidateInfoFromPayload(payload, fallbackGeekId = '') {
371
+ const formatted = formatResumeApiData(payload);
372
+ return {
373
+ geekId: normalizeText(fallbackGeekId || ''),
374
+ name: formatted.name,
375
+ school: formatted.school,
376
+ major: formatted.major,
377
+ company: formatted.company,
378
+ position: formatted.position,
379
+ resumeText: normalizeText(formatted.resumeText),
380
+ evidenceCorpus: normalizeText(formatted.resumeText),
381
+ alreadyInterested: false,
382
+ };
383
+ }
384
+
385
+ export class ResumeNetworkTracker {
386
+ constructor({ chromeClient, logger = console } = {}) {
387
+ this.chromeClient = chromeClient;
388
+ this.logger = logger;
389
+ this.Network = chromeClient?.Network || null;
390
+ this.resumeNetworkRequests = new Map();
391
+ this.resumeNetworkRelatedRequests = new Map();
392
+ this.resumeNetworkByGeekId = new Map();
393
+ this.latestResumeNetworkPayload = null;
394
+ this.resumeNetworkDiagnostics = [];
395
+ this.resumeAcquisitionMode = 'unknown';
396
+ this.resumeAcquisitionModeReason = '';
397
+ this.bound = false;
398
+ this.attach();
399
+ }
400
+
401
+ attach() {
402
+ if (this.bound || !this.Network) return;
403
+ if (typeof this.Network.requestWillBeSent === 'function') {
404
+ this.Network.requestWillBeSent((params) => {
405
+ try {
406
+ this.handleNetworkRequestWillBeSent(params);
407
+ } catch {}
408
+ });
409
+ }
410
+ if (typeof this.Network.loadingFinished === 'function') {
411
+ this.Network.loadingFinished((params) => {
412
+ this.handleNetworkLoadingFinished(params).catch(() => {});
413
+ });
414
+ }
415
+ this.bound = true;
416
+ }
417
+
418
+ recordDiagnostic(entry = {}) {
419
+ const normalized = {
420
+ ts: Number.isFinite(Number(entry.ts)) ? Number(entry.ts) : Date.now(),
421
+ kind: normalizeText(entry.kind || 'unknown') || 'unknown',
422
+ request_id: normalizeText(entry.request_id || '') || null,
423
+ url: normalizeText(entry.url || '') || null,
424
+ geek_id: normalizeText(entry.geek_id || '') || null,
425
+ reason: normalizeText(entry.reason || '') || null,
426
+ source: normalizeText(entry.source || '') || null,
427
+ error: normalizeText(entry.error || '') || null,
428
+ waited_ms: Number.isFinite(Number(entry.waited_ms)) ? Number(entry.waited_ms) : null,
429
+ };
430
+ this.resumeNetworkDiagnostics.push(normalized);
431
+ if (this.resumeNetworkDiagnostics.length > 240) {
432
+ this.resumeNetworkDiagnostics.splice(0, this.resumeNetworkDiagnostics.length - 200);
433
+ }
434
+ }
435
+
436
+ setResumeAcquisitionMode(mode, reason = '') {
437
+ if (!['unknown', 'network', 'image'].includes(mode)) return;
438
+ this.resumeAcquisitionMode = mode;
439
+ this.resumeAcquisitionModeReason = normalizeText(reason || '');
440
+ }
441
+
442
+ getResumeAcquisitionState() {
443
+ return {
444
+ mode: this.resumeAcquisitionMode,
445
+ reason: this.resumeAcquisitionModeReason,
446
+ };
447
+ }
448
+
449
+ cacheResumeNetworkPayload(payload, fallbackGeekId = '') {
450
+ const geekIds = collectGeekIdsFromPayload(payload, fallbackGeekId);
451
+ const candidateInfo = buildCandidateInfoFromPayload(payload, fallbackGeekId);
452
+ const wrapped = {
453
+ ts: Date.now(),
454
+ geekIds,
455
+ data: payload,
456
+ candidateInfo,
457
+ };
458
+ this.latestResumeNetworkPayload = wrapped;
459
+ for (const id of geekIds) {
460
+ const normalizedId = normalizeText(id);
461
+ if (!normalizedId) continue;
462
+ this.resumeNetworkByGeekId.set(normalizedId, wrapped);
463
+ }
464
+ }
465
+
466
+ getCandidateKeys(candidate = {}) {
467
+ return Array.from(
468
+ new Set(
469
+ [
470
+ candidate?.key,
471
+ candidate?.geek_id,
472
+ candidate?.customerId,
473
+ candidate?.customerKey,
474
+ ]
475
+ .map((value) => normalizeText(value))
476
+ .filter(Boolean),
477
+ ),
478
+ );
479
+ }
480
+
481
+ tryExtractNetworkResumeForCandidate(candidate, options = {}) {
482
+ const candidateKeys = this.getCandidateKeys(candidate);
483
+ const minTs = Number.isFinite(Number(options?.minTs)) ? Number(options.minTs) : 0;
484
+ for (const candidateKey of candidateKeys) {
485
+ if (!this.resumeNetworkByGeekId.has(candidateKey)) continue;
486
+ const wrapped = this.resumeNetworkByGeekId.get(candidateKey);
487
+ const payloadTs = Number(wrapped?.ts || 0);
488
+ if (payloadTs >= minTs) {
489
+ return {
490
+ candidateInfo: wrapped?.candidateInfo || null,
491
+ source: 'geek_id_map',
492
+ ts: payloadTs,
493
+ };
494
+ }
495
+ }
496
+ if (this.latestResumeNetworkPayload) {
497
+ const wrapped = this.latestResumeNetworkPayload;
498
+ const payloadTs = Number(wrapped?.ts || 0);
499
+ const ageMs = Date.now() - payloadTs;
500
+ const latestGeekIds = Array.isArray(wrapped?.geekIds)
501
+ ? wrapped.geekIds.map((id) => normalizeText(id)).filter(Boolean)
502
+ : [];
503
+ const withinAge = ageMs <= 12000;
504
+ const withinTs = payloadTs >= minTs;
505
+ if (candidateKeys.length <= 0 && withinAge && withinTs) {
506
+ return {
507
+ candidateInfo: wrapped?.candidateInfo || null,
508
+ source: 'latest_payload',
509
+ ts: payloadTs,
510
+ };
511
+ }
512
+ if (candidateKeys.some((candidateKey) => withinAge && withinTs && latestGeekIds.includes(candidateKey))) {
513
+ return {
514
+ candidateInfo: wrapped?.candidateInfo || null,
515
+ source: 'latest_payload_key_match',
516
+ ts: payloadTs,
517
+ };
518
+ }
519
+ }
520
+ return null;
521
+ }
522
+
523
+ async waitForNetworkResumeCandidateInfo(candidate, timeoutMs = 2200, options = {}) {
524
+ const waitStartedAt = Date.now();
525
+ const minTs = Number.isFinite(Number(options?.minTs)) ? Number(options.minTs) : 0;
526
+ const deadline = Date.now() + timeoutMs;
527
+ while (Date.now() < deadline) {
528
+ const match = this.tryExtractNetworkResumeForCandidate(candidate, { minTs });
529
+ const info = match?.candidateInfo || null;
530
+ if (info && normalizeText(info.resumeText)) {
531
+ this.recordDiagnostic({
532
+ kind: 'wait_hit',
533
+ geek_id: this.getCandidateKeys(candidate)[0] || '',
534
+ source: match?.source || 'unknown',
535
+ waited_ms: Date.now() - waitStartedAt,
536
+ });
537
+ return {
538
+ candidateInfo: info,
539
+ source: match?.source || 'unknown',
540
+ waitedMs: Date.now() - waitStartedAt,
541
+ };
542
+ }
543
+ await sleep(NETWORK_POLL_MS);
544
+ }
545
+ this.recordDiagnostic({
546
+ kind: 'wait_timeout',
547
+ geek_id: this.getCandidateKeys(candidate)[0] || '',
548
+ waited_ms: Date.now() - waitStartedAt,
549
+ reason: 'resume_text_not_ready',
550
+ });
551
+ return null;
552
+ }
553
+
554
+ async waitForResumeNetworkByMode(candidate, options = {}) {
555
+ const minTs = Number.isFinite(Number(options?.minTs)) ? Number(options.minTs) : 0;
556
+ const mode = this.resumeAcquisitionMode || 'unknown';
557
+ const firstWaitMs = mode === 'image' ? NETWORK_RESUME_IMAGE_MODE_GRACE_MS : NETWORK_RESUME_WAIT_MS;
558
+ const firstStageStartedAt = Date.now();
559
+ let networkResult = await this.waitForNetworkResumeCandidateInfo(candidate, firstWaitMs, { minTs });
560
+ const firstStageElapsedMs = Date.now() - firstStageStartedAt;
561
+ if (networkResult?.candidateInfo?.resumeText) {
562
+ const reason = mode === 'image' ? 'image_mode_grace_hit' : 'initial_network_hit';
563
+ this.setResumeAcquisitionMode('network', reason);
564
+ return {
565
+ ...networkResult,
566
+ acquisitionReason: reason,
567
+ initialWaitMs: firstStageElapsedMs,
568
+ retryWaitMs: 0,
569
+ };
570
+ }
571
+ if (mode === 'image') {
572
+ return {
573
+ candidateInfo: null,
574
+ source: null,
575
+ waitedMs: firstStageElapsedMs,
576
+ acquisitionReason: '',
577
+ initialWaitMs: firstStageElapsedMs,
578
+ retryWaitMs: 0,
579
+ };
580
+ }
581
+ const retryStageStartedAt = Date.now();
582
+ await sleep(NETWORK_RESUME_RETRY_WAIT_MS);
583
+ networkResult = await this.waitForNetworkResumeCandidateInfo(candidate, NETWORK_RESUME_RETRY_WAIT_MS, {
584
+ minTs,
585
+ });
586
+ const retryStageElapsedMs = Date.now() - retryStageStartedAt;
587
+ if (networkResult?.candidateInfo?.resumeText) {
588
+ const reason = 'network_retry_hit';
589
+ this.setResumeAcquisitionMode('network', reason);
590
+ return {
591
+ ...networkResult,
592
+ acquisitionReason: reason,
593
+ initialWaitMs: firstStageElapsedMs,
594
+ retryWaitMs: retryStageElapsedMs,
595
+ };
596
+ }
597
+ return {
598
+ candidateInfo: null,
599
+ source: null,
600
+ waitedMs: firstStageElapsedMs + retryStageElapsedMs,
601
+ acquisitionReason: '',
602
+ initialWaitMs: firstStageElapsedMs,
603
+ retryWaitMs: retryStageElapsedMs,
604
+ };
605
+ }
606
+
607
+ async waitForLateNetworkResumeCandidateInfo(candidate, options = {}) {
608
+ const minTs = Number.isFinite(Number(options?.minTs)) ? Number(options.minTs) : 0;
609
+ const networkResult = await this.waitForNetworkResumeCandidateInfo(candidate, NETWORK_RESUME_LATE_RETRY_MS, {
610
+ minTs,
611
+ });
612
+ if (networkResult?.candidateInfo?.resumeText) {
613
+ const reason = 'late_network_hit';
614
+ this.setResumeAcquisitionMode('network', reason);
615
+ return {
616
+ ...networkResult,
617
+ acquisitionReason: reason,
618
+ lateRetryMs: Number(networkResult?.waitedMs || 0),
619
+ };
620
+ }
621
+ return {
622
+ candidateInfo: null,
623
+ source: null,
624
+ waitedMs: 0,
625
+ acquisitionReason: '',
626
+ lateRetryMs: 0,
627
+ };
628
+ }
629
+
630
+ handleNetworkRequestWillBeSent(params = {}) {
631
+ const url = normalizeText(params?.request?.url || '');
632
+ const postData = params?.request?.postData || '';
633
+ if (!url) return;
634
+ const requestTs = Date.now();
635
+ const method = normalizeText(params?.request?.method || '').toUpperCase() || 'GET';
636
+ const isResumeInfo = isResumeInfoRequestUrl(url);
637
+ const isResumeRelated = isResumeInfo || isResumeRelatedWapiUrl(url);
638
+ if (!isResumeRelated) return;
639
+ const geekId = parseGeekIdFromUrl(url) || parseGeekIdFromPostData(postData);
640
+ const meta = {
641
+ ts: requestTs,
642
+ url,
643
+ geekId,
644
+ method,
645
+ isResumeInfo,
646
+ };
647
+ this.resumeNetworkRelatedRequests.set(params.requestId, meta);
648
+ this.recordDiagnostic({
649
+ kind: 'request',
650
+ request_id: params.requestId,
651
+ url: url.slice(0, 280),
652
+ geek_id: geekId,
653
+ source: isResumeInfo ? 'resume_info_url' : 'wapi_related_non_resume_info',
654
+ });
655
+ if (isResumeInfo) {
656
+ this.resumeNetworkRequests.set(params.requestId, meta);
657
+ }
658
+ }
659
+
660
+ async handleNetworkLoadingFinished(params = {}) {
661
+ const requestId = params?.requestId;
662
+ const requestMeta = this.resumeNetworkRequests.get(requestId);
663
+ const relatedMeta = this.resumeNetworkRelatedRequests.get(requestId);
664
+ if (!requestMeta && !relatedMeta) return;
665
+ this.resumeNetworkRequests.delete(requestId);
666
+ this.resumeNetworkRelatedRequests.delete(requestId);
667
+ const effectiveMeta = requestMeta || relatedMeta || {};
668
+ const effectiveUrl = normalizeText(effectiveMeta.url || '');
669
+ const effectiveGeekId = normalizeText(effectiveMeta.geekId || '');
670
+ try {
671
+ const responseBody = await this.Network.getResponseBody({ requestId });
672
+ if (!responseBody?.body) {
673
+ this.recordDiagnostic({
674
+ kind: 'response_miss',
675
+ request_id: requestId,
676
+ url: effectiveUrl.slice(0, 280),
677
+ geek_id: effectiveGeekId,
678
+ reason: 'empty_body',
679
+ });
680
+ return;
681
+ }
682
+ const rawBody = responseBody.base64Encoded
683
+ ? Buffer.from(responseBody.body, 'base64').toString('utf8')
684
+ : responseBody.body;
685
+ const parsed = JSON.parse(rawBody);
686
+ const resumePayload = extractResumePayloadFromResponseBody(parsed);
687
+ if (!resumePayload) {
688
+ this.recordDiagnostic({
689
+ kind: 'response_miss',
690
+ request_id: requestId,
691
+ url: effectiveUrl.slice(0, 280),
692
+ geek_id: effectiveGeekId,
693
+ reason: 'payload_not_found',
694
+ });
695
+ return;
696
+ }
697
+ this.cacheResumeNetworkPayload(resumePayload, effectiveGeekId);
698
+ this.recordDiagnostic({
699
+ kind: 'response_hit',
700
+ request_id: requestId,
701
+ url: effectiveUrl.slice(0, 280),
702
+ geek_id: effectiveGeekId,
703
+ });
704
+ } catch (error) {
705
+ this.recordDiagnostic({
706
+ kind: 'response_error',
707
+ request_id: requestId,
708
+ url: effectiveUrl.slice(0, 280),
709
+ geek_id: effectiveGeekId,
710
+ error: normalizeText(error?.message || String(error)).slice(0, 240),
711
+ });
712
+ }
713
+ }
714
+ }
715
+
716
+ export const __testables = {
717
+ buildCandidateInfoFromPayload,
718
+ collectGeekIdsFromPayload,
719
+ extractResumePayloadFromResponseBody,
720
+ formatResumeApiData,
721
+ isDomProfileConsistentWithCard,
722
+ isResumeInfoRequestUrl,
723
+ isResumeRelatedWapiUrl,
724
+ parseGeekIdFromPostData,
725
+ parseGeekIdFromUrl,
726
+ preferReadableName,
727
+ };