@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,727 +0,0 @@
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
- };