@reconcrap/boss-recommend-mcp 2.1.23 → 2.1.25

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 (67) hide show
  1. package/README.md +5 -0
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/package.json +6 -1
  4. package/scripts/install-macos.sh +280 -280
  5. package/scripts/postinstall.cjs +44 -44
  6. package/skills/boss-chat/README.md +43 -42
  7. package/skills/boss-chat/SKILL.md +107 -106
  8. package/skills/boss-recommend-pipeline/README.md +13 -13
  9. package/skills/boss-recommend-pipeline/SKILL.md +47 -47
  10. package/skills/boss-recruit-pipeline/README.md +19 -19
  11. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  12. package/src/chat-mcp.js +301 -127
  13. package/src/chat-runtime-config.js +7 -5
  14. package/src/core/boss-cards/index.js +199 -199
  15. package/src/core/browser/index.js +302 -145
  16. package/src/core/capture/index.js +2930 -1201
  17. package/src/core/cv-acquisition/index.js +512 -238
  18. package/src/core/cv-capture-target/index.js +513 -299
  19. package/src/core/greet-quota/index.js +71 -71
  20. package/src/core/infinite-list/index.js +31 -31
  21. package/src/core/reporting/legacy-csv.js +12 -12
  22. package/src/core/run/detached-launcher.js +305 -0
  23. package/src/core/run/index.js +109 -55
  24. package/src/core/run/timing.js +33 -33
  25. package/src/core/run/windows-detached-worker.ps1 +106 -0
  26. package/src/core/screening/index.js +2400 -2135
  27. package/src/core/self-heal/index.js +989 -973
  28. package/src/core/self-heal/viewport.js +1505 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/action-journal.js +536 -0
  31. package/src/domains/chat/cards.js +137 -137
  32. package/src/domains/chat/constants.js +9 -9
  33. package/src/domains/chat/detail.js +1684 -401
  34. package/src/domains/chat/index.js +8 -7
  35. package/src/domains/chat/jobs.js +620 -620
  36. package/src/domains/chat/page-guard.js +157 -122
  37. package/src/domains/chat/roots.js +56 -56
  38. package/src/domains/chat/run-service.js +1801 -760
  39. package/src/domains/common/account-rights-panel.js +314 -314
  40. package/src/domains/common/recovery-settle.js +159 -159
  41. package/src/domains/recommend/actions.js +1219 -472
  42. package/src/domains/recommend/cards.js +243 -243
  43. package/src/domains/recommend/colleague-contact.js +1079 -333
  44. package/src/domains/recommend/constants.js +92 -92
  45. package/src/domains/recommend/detail.js +4037 -136
  46. package/src/domains/recommend/filters.js +608 -590
  47. package/src/domains/recommend/index.js +9 -9
  48. package/src/domains/recommend/jobs.js +571 -542
  49. package/src/domains/recommend/location.js +754 -707
  50. package/src/domains/recommend/refresh.js +677 -392
  51. package/src/domains/recommend/roots.js +80 -80
  52. package/src/domains/recommend/run-service.js +4048 -1447
  53. package/src/domains/recommend/scopes.js +246 -246
  54. package/src/domains/recruit/actions.js +277 -277
  55. package/src/domains/recruit/cards.js +74 -74
  56. package/src/domains/recruit/constants.js +236 -236
  57. package/src/domains/recruit/detail.js +588 -588
  58. package/src/domains/recruit/index.js +9 -9
  59. package/src/domains/recruit/instruction-parser.js +866 -866
  60. package/src/domains/recruit/refresh.js +45 -45
  61. package/src/domains/recruit/roots.js +68 -68
  62. package/src/domains/recruit/run-service.js +1817 -1620
  63. package/src/domains/recruit/search.js +3229 -3229
  64. package/src/index.js +16 -1
  65. package/src/parser.js +1296 -1296
  66. package/src/recommend-mcp.js +1061 -450
  67. package/src/recommend-scheduler.js +75 -75
@@ -1,973 +1,989 @@
1
- import {
2
- findIframeDocument,
3
- getAccessibilityTree,
4
- getDocumentRoot,
5
- querySelectorAll,
6
- sleep
7
- } from "../browser/index.js";
8
- import {
9
- compactViewportHealthResult,
10
- ensureHealthyViewport
11
- } from "./viewport.js";
12
-
13
- export {
14
- buildViewportHealthDiagnostics,
15
- compactViewportHealthResult,
16
- compactViewportState,
17
- createViewportRunGuard,
18
- ensureHealthyViewport,
19
- getCurrentWindowInfo,
20
- isListViewportCollapsed,
21
- readViewportState,
22
- setWindowStateIfPossible,
23
- toggleWindowStateForViewportRecovery,
24
- VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH,
25
- VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO,
26
- VIEWPORT_COLLAPSE_RATIO_THRESHOLD
27
- } from "./viewport.js";
28
-
29
- export const PROBE_STATUS = Object.freeze({
30
- PASS: "pass",
31
- FAIL: "fail",
32
- BLOCKED: "blocked",
33
- OPTIONAL_ABSENT: "optional_absent",
34
- ERROR: "error"
35
- });
36
-
37
- export const HEALTH_STATUS = Object.freeze({
38
- HEALTHY: "healthy",
39
- DEGRADED: "degraded",
40
- BLOCKED: "blocked"
41
- });
42
-
43
- export const DOMAIN_TARGET_HINTS = Object.freeze({
44
- recommend: ["/web/chat/recommend"],
45
- recruit: ["/web/chat/search"],
46
- chat: ["/web/chat/index", "/web/chat"]
47
- });
48
-
49
- const FALLBACK_RECOMMEND_SELECTORS = Object.freeze({
50
- top: {
51
- recommend_iframe: [
52
- 'iframe[name="recommendFrame"]',
53
- 'iframe[src*="/web/frame/recommend/"]',
54
- "iframe"
55
- ]
56
- },
57
- frame: {
58
- filter_trigger: [
59
- ".filter-label-wrap",
60
- ".recommend-filter.op-filter"
61
- ],
62
- filter_panel: [
63
- ".recommend-filter.op-filter .filter-panel",
64
- ".recommend-filter .filter-panel",
65
- ".filter-panel"
66
- ],
67
- tab_items: [
68
- "li.tab-item[data-status]",
69
- 'li[data-status][class*="tab"]'
70
- ],
71
- candidate_cards: [
72
- ".candidate-card-wrap .card-inner[data-geek]",
73
- ".candidate-card-wrap [data-geek]",
74
- "ul.card-list > li.card-item",
75
- ".card-inner[data-geekid]",
76
- "li.geek-info-card",
77
- "a[data-geekid]",
78
- ".candidate-card-wrap"
79
- ]
80
- },
81
- detail: {
82
- popup: [
83
- ".dialog-wrap.active",
84
- ".boss-popup__wrapper",
85
- ".boss-popup_wrapper",
86
- ".boss-dialog_wrapper",
87
- ".boss-dialog",
88
- ".resume-item-detail",
89
- ".geek-detail-modal",
90
- '[class*="popup"][class*="wrapper"]',
91
- '[class*="dialog"][class*="wrapper"]'
92
- ]
93
- }
94
- });
95
-
96
- const FALLBACK_RECRUIT_SELECTORS = Object.freeze({
97
- top: {
98
- search_iframe: [
99
- 'iframe[name="searchFrame"]',
100
- 'iframe[src*="/web/frame/search/"]',
101
- "iframe"
102
- ]
103
- },
104
- frame: {
105
- candidate_cards: [
106
- "li.geek-info-card a[data-jid]",
107
- "li.geek-info-card a[data-geekid]",
108
- ".geek-info-card a[data-jid]",
109
- ".geek-info-card a[data-geekid]",
110
- ".geek-info-card a",
111
- "a[data-jid]",
112
- "a[data-geekid]"
113
- ],
114
- no_data: [
115
- "i.tip-nodata",
116
- ".tip-nodata",
117
- ".empty-tip",
118
- ".empty-text"
119
- ]
120
- },
121
- detail: {
122
- popup: [
123
- ".dialog-wrap.active",
124
- ".boss-popup__wrapper",
125
- ".boss-popup_wrapper",
126
- ".boss-dialog_wrapper",
127
- ".boss-dialog",
128
- ".resume-item-detail",
129
- ".geek-detail-modal",
130
- ".resume-container",
131
- '[class*="popup"][class*="wrapper"]',
132
- '[class*="dialog"][class*="wrapper"]'
133
- ]
134
- }
135
- });
136
-
137
- const FALLBACK_CHAT_SELECTORS = Object.freeze({
138
- top: {
139
- candidate_cards: [
140
- ".geek-item[data-id]",
141
- 'div[role="listitem"] .geek-item[data-id]',
142
- ".geek-item",
143
- ".geek-item-wrap",
144
- 'div[role="listitem"]'
145
- ],
146
- selected_candidate: [
147
- ".geek-item.selected[data-id]",
148
- ".geek-item.selected",
149
- ".geek-item.active[data-id]",
150
- ".geek-item.active"
151
- ],
152
- online_resume_button: [
153
- "a.btn.resume-btn-online",
154
- "a.resume-btn-online",
155
- ".btn.resume-btn-online",
156
- ".resume-btn-online"
157
- ]
158
- },
159
- detail: {
160
- resume_modal: [
161
- ".boss-popup__wrapper",
162
- ".new-chat-resume-dialog-main-ui",
163
- ".dialog-wrap.active",
164
- ".boss-dialog",
165
- ".geek-detail-modal",
166
- ".modal",
167
- ".resume-container",
168
- ".resume-content-wrap",
169
- ".resume-common-wrap",
170
- ".resume-detail",
171
- ".resume-recommend"
172
- ],
173
- resume_iframe: [
174
- 'iframe[src*="/web/frame/c-resume/"]',
175
- 'iframe[src*="resume"]',
176
- 'iframe[name*="resume"]'
177
- ]
178
- }
179
- });
180
-
181
- function uniqueStrings(values = []) {
182
- return [...new Set(values.filter((value) => typeof value === "string" && value.trim()))];
183
- }
184
-
185
- function selectorGroup(rules, scope, name, fallback = []) {
186
- return uniqueStrings(rules?.selectors?.[scope]?.[name] || fallback);
187
- }
188
-
189
- function mergeSelectorGroups(rules, scope, names = [], fallback = []) {
190
- const selectors = names.flatMap((name) => rules?.selectors?.[scope]?.[name] || []);
191
- return uniqueStrings(selectors.length ? selectors : fallback);
192
- }
193
-
194
- function rootNodeId(roots = {}, name) {
195
- const root = roots[name];
196
- if (typeof root === "number") return root;
197
- if (root?.nodeId) return root.nodeId;
198
- if (root?.documentNodeId) return root.documentNodeId;
199
- return 0;
200
- }
201
-
202
- function stringMatchesAnyPattern(value, patterns = []) {
203
- const text = String(value || "");
204
- return patterns.some((pattern) => {
205
- if (pattern instanceof RegExp) return pattern.test(text);
206
- return text.includes(String(pattern || ""));
207
- });
208
- }
209
-
210
- export function createSelectorProbe({
211
- id,
212
- root = "frame",
213
- selectors = [],
214
- required = false,
215
- minCount = 1,
216
- description = ""
217
- } = {}) {
218
- if (!id) throw new Error("Selector probe requires an id");
219
- return {
220
- type: "selector",
221
- id,
222
- root,
223
- selectors: uniqueStrings(selectors),
224
- required: Boolean(required),
225
- minCount,
226
- description
227
- };
228
- }
229
-
230
- export function createAccessibilityProbe({
231
- id,
232
- required = false,
233
- minCount = 1,
234
- roleIncludes = [],
235
- nameIncludes = [],
236
- description = ""
237
- } = {}) {
238
- if (!id) throw new Error("Accessibility probe requires an id");
239
- return {
240
- type: "accessibility",
241
- id,
242
- required: Boolean(required),
243
- minCount,
244
- roleIncludes: uniqueStrings(roleIncludes),
245
- nameIncludes: uniqueStrings(nameIncludes),
246
- description
247
- };
248
- }
249
-
250
- export function createNetworkProbe({
251
- id,
252
- required = false,
253
- minCount = 1,
254
- urlPatterns = [],
255
- description = ""
256
- } = {}) {
257
- if (!id) throw new Error("Network probe requires an id");
258
- return {
259
- type: "network",
260
- id,
261
- required: Boolean(required),
262
- minCount,
263
- urlPatterns,
264
- description
265
- };
266
- }
267
-
268
- export function createViewportCollapseProbe({
269
- id = "viewport_collapse",
270
- root = "frame",
271
- frameOwnerRoot = "frameOwner",
272
- required = true,
273
- repair = true,
274
- description = ""
275
- } = {}) {
276
- if (!id) throw new Error("Viewport collapse probe requires an id");
277
- return {
278
- type: "viewport",
279
- id,
280
- root,
281
- frameOwnerRoot,
282
- required: Boolean(required),
283
- repair: Boolean(repair),
284
- description
285
- };
286
- }
287
-
288
- export async function runSelectorProbe(client, roots, probe) {
289
- const nodeId = rootNodeId(roots, probe.root);
290
- if (!nodeId) {
291
- return {
292
- ...probe,
293
- ok: !probe.required,
294
- status: probe.required ? PROBE_STATUS.BLOCKED : PROBE_STATUS.OPTIONAL_ABSENT,
295
- count: 0,
296
- selector_counts: [],
297
- error: `Root not found: ${probe.root}`
298
- };
299
- }
300
-
301
- const selectorCounts = [];
302
- try {
303
- for (const selector of probe.selectors) {
304
- const nodeIds = await querySelectorAll(client, nodeId, selector);
305
- selectorCounts.push({
306
- selector,
307
- count: nodeIds.length
308
- });
309
- }
310
- } catch (error) {
311
- return {
312
- ...probe,
313
- ok: !probe.required,
314
- status: probe.required ? PROBE_STATUS.ERROR : PROBE_STATUS.OPTIONAL_ABSENT,
315
- count: 0,
316
- selector_counts: selectorCounts,
317
- error: error?.message || String(error)
318
- };
319
- }
320
-
321
- const count = selectorCounts.reduce((max, item) => Math.max(max, item.count), 0);
322
- const ok = count >= probe.minCount;
323
- return {
324
- ...probe,
325
- ok: probe.required ? ok : true,
326
- status: ok ? PROBE_STATUS.PASS : probe.required ? PROBE_STATUS.FAIL : PROBE_STATUS.OPTIONAL_ABSENT,
327
- count,
328
- selector_counts: selectorCounts,
329
- matched_selectors: selectorCounts.filter((item) => item.count > 0)
330
- };
331
- }
332
-
333
- export async function runAccessibilityProbe(client, probe) {
334
- try {
335
- const tree = await getAccessibilityTree(client);
336
- const nodes = tree?.nodes || [];
337
- const matches = nodes.filter((node) => {
338
- const role = String(node?.role?.value || "");
339
- const name = String(node?.name?.value || "");
340
- const roleOk = probe.roleIncludes.length === 0
341
- || probe.roleIncludes.some((value) => role.includes(value));
342
- const nameOk = probe.nameIncludes.length === 0
343
- || probe.nameIncludes.some((value) => name.includes(value));
344
- return roleOk && nameOk;
345
- });
346
- const ok = matches.length >= probe.minCount;
347
- return {
348
- ...probe,
349
- ok: probe.required ? ok : true,
350
- status: ok ? PROBE_STATUS.PASS : probe.required ? PROBE_STATUS.FAIL : PROBE_STATUS.OPTIONAL_ABSENT,
351
- count: matches.length,
352
- total_ax_nodes: nodes.length
353
- };
354
- } catch (error) {
355
- return {
356
- ...probe,
357
- ok: !probe.required,
358
- status: probe.required ? PROBE_STATUS.ERROR : PROBE_STATUS.OPTIONAL_ABSENT,
359
- count: 0,
360
- total_ax_nodes: 0,
361
- error: error?.message || String(error)
362
- };
363
- }
364
- }
365
-
366
- export function runNetworkProbe(networkEvents = [], probe) {
367
- const matches = networkEvents.filter((event) => {
368
- if (!probe.urlPatterns.length) return true;
369
- return stringMatchesAnyPattern(event?.url || event?.response?.url, probe.urlPatterns);
370
- });
371
- const ok = matches.length >= probe.minCount;
372
- return {
373
- ...probe,
374
- ok: probe.required ? ok : true,
375
- status: ok ? PROBE_STATUS.PASS : probe.required ? PROBE_STATUS.FAIL : PROBE_STATUS.OPTIONAL_ABSENT,
376
- count: matches.length,
377
- sample_urls: matches.slice(0, 5).map((event) => event?.url || event?.response?.url || "")
378
- };
379
- }
380
-
381
- export async function runViewportCollapseProbe(client, roots, probe) {
382
- const nodeId = rootNodeId(roots, probe.root);
383
- if (!nodeId) {
384
- return {
385
- ...probe,
386
- ok: !probe.required,
387
- status: probe.required ? PROBE_STATUS.BLOCKED : PROBE_STATUS.OPTIONAL_ABSENT,
388
- count: 0,
389
- collapsed: false,
390
- recovered: false,
391
- error: `Root not found: ${probe.root}`
392
- };
393
- }
394
-
395
- try {
396
- const health = await ensureHealthyViewport(client, {
397
- roots,
398
- root: probe.root,
399
- frameOwnerRoot: probe.frameOwnerRoot,
400
- reason: probe.id,
401
- repair: probe.repair
402
- });
403
- const ok = Boolean(health.ok);
404
- return {
405
- ...probe,
406
- ok: probe.required ? ok : true,
407
- status: ok ? PROBE_STATUS.PASS : probe.required ? PROBE_STATUS.FAIL : PROBE_STATUS.OPTIONAL_ABSENT,
408
- count: ok ? 1 : 0,
409
- collapsed: Boolean(health.collapsed),
410
- recovered: Boolean(health.recovered),
411
- viewport_health: compactViewportHealthResult(health),
412
- error: health.error || null
413
- };
414
- } catch (error) {
415
- return {
416
- ...probe,
417
- ok: !probe.required,
418
- status: probe.required ? PROBE_STATUS.ERROR : PROBE_STATUS.OPTIONAL_ABSENT,
419
- count: 0,
420
- collapsed: false,
421
- recovered: false,
422
- error: error?.message || String(error)
423
- };
424
- }
425
- }
426
-
427
- export function summarizeProbeResults(probes = []) {
428
- const required = probes.filter((probe) => probe.required);
429
- const blocked = required.filter((probe) => probe.status === PROBE_STATUS.BLOCKED);
430
- const failed = required.filter((probe) => !probe.ok && probe.status !== PROBE_STATUS.BLOCKED);
431
- const optionalAbsent = probes.filter((probe) => probe.status === PROBE_STATUS.OPTIONAL_ABSENT);
432
- const passed = probes.filter((probe) => probe.status === PROBE_STATUS.PASS);
433
-
434
- return {
435
- status: blocked.length
436
- ? HEALTH_STATUS.BLOCKED
437
- : failed.length
438
- ? HEALTH_STATUS.DEGRADED
439
- : HEALTH_STATUS.HEALTHY,
440
- required_count: required.length,
441
- passed_count: passed.length,
442
- failed_required_ids: failed.map((probe) => probe.id),
443
- blocked_required_ids: blocked.map((probe) => probe.id),
444
- optional_absent_ids: optionalAbsent.map((probe) => probe.id)
445
- };
446
- }
447
-
448
- export function buildDriftReport(probes = []) {
449
- return probes
450
- .filter((probe) => probe.required && !probe.ok)
451
- .map((probe) => ({
452
- probe_id: probe.id,
453
- probe_type: probe.type,
454
- status: probe.status,
455
- root: probe.root || null,
456
- expected_min_count: probe.minCount,
457
- observed_count: probe.count || 0,
458
- selectors: probe.selectors || [],
459
- viewport_health: probe.viewport_health || undefined,
460
- error: probe.error || null
461
- }));
462
- }
463
-
464
- export async function runSelfHealCheck({
465
- client,
466
- domain,
467
- roots = {},
468
- selectorProbes = [],
469
- accessibilityProbes = [],
470
- viewportProbes = [],
471
- networkProbes = [],
472
- networkEvents = []
473
- } = {}) {
474
- const selectorResults = [];
475
- for (const probe of selectorProbes) {
476
- selectorResults.push(await runSelectorProbe(client, roots, probe));
477
- }
478
-
479
- const accessibilityResults = [];
480
- for (const probe of accessibilityProbes) {
481
- accessibilityResults.push(await runAccessibilityProbe(client, probe));
482
- }
483
-
484
- const viewportResults = [];
485
- for (const probe of viewportProbes) {
486
- viewportResults.push(await runViewportCollapseProbe(client, roots, probe));
487
- }
488
-
489
- const networkResults = networkProbes.map((probe) => runNetworkProbe(networkEvents, probe));
490
- const probes = [...selectorResults, ...accessibilityResults, ...viewportResults, ...networkResults];
491
- const summary = summarizeProbeResults(probes);
492
-
493
- return {
494
- domain,
495
- status: summary.status,
496
- summary,
497
- probes,
498
- drift_report: buildDriftReport(probes)
499
- };
500
- }
501
-
502
- export function buildRecommendSelfHealConfig(rules = {}) {
503
- const iframeSelectors = selectorGroup(
504
- rules,
505
- "top",
506
- "recommend_iframe",
507
- FALLBACK_RECOMMEND_SELECTORS.top.recommend_iframe
508
- );
509
- const cardSelectors = mergeSelectorGroups(
510
- rules,
511
- "frame",
512
- [
513
- "latest_card_inner",
514
- "recommend_card_inner",
515
- "featured_card_anchor",
516
- "recommend_cards",
517
- "featured_cards",
518
- "latest_cards"
519
- ],
520
- FALLBACK_RECOMMEND_SELECTORS.frame.candidate_cards
521
- );
522
-
523
- return {
524
- domain: "recommend",
525
- targetHints: DOMAIN_TARGET_HINTS.recommend,
526
- iframeSelectors,
527
- selectorProbes: [
528
- createSelectorProbe({
529
- id: "recommend_iframe",
530
- root: "top",
531
- selectors: iframeSelectors,
532
- required: true,
533
- description: "Recommend iframe can be discovered from the top document"
534
- }),
535
- createSelectorProbe({
536
- id: "filter_trigger",
537
- root: "frame",
538
- selectors: selectorGroup(
539
- rules,
540
- "frame",
541
- "filter_trigger",
542
- FALLBACK_RECOMMEND_SELECTORS.frame.filter_trigger
543
- ),
544
- required: true,
545
- description: "Filter trigger is mounted in the recommend frame"
546
- }),
547
- createSelectorProbe({
548
- id: "candidate_cards",
549
- root: "frame",
550
- selectors: cardSelectors,
551
- required: true,
552
- description: "At least one recommend candidate card is visible"
553
- }),
554
- createSelectorProbe({
555
- id: "tab_items",
556
- root: "frame",
557
- selectors: selectorGroup(
558
- rules,
559
- "frame",
560
- "tab_items",
561
- FALLBACK_RECOMMEND_SELECTORS.frame.tab_items
562
- ),
563
- required: false,
564
- description: "Recommend tab controls are mounted when this layout exposes them"
565
- }),
566
- createSelectorProbe({
567
- id: "filter_panel",
568
- root: "frame",
569
- selectors: selectorGroup(
570
- rules,
571
- "frame",
572
- "filter_panel",
573
- FALLBACK_RECOMMEND_SELECTORS.frame.filter_panel
574
- ),
575
- required: false,
576
- description: "Filter panel is optional because it is absent until opened"
577
- }),
578
- createSelectorProbe({
579
- id: "detail_popup_top",
580
- root: "top",
581
- selectors: selectorGroup(
582
- rules,
583
- "detail",
584
- "popup",
585
- FALLBACK_RECOMMEND_SELECTORS.detail.popup
586
- ),
587
- required: false,
588
- description: "Candidate detail popup may be absent during idle health checks"
589
- }),
590
- createSelectorProbe({
591
- id: "detail_popup_frame",
592
- root: "frame",
593
- selectors: selectorGroup(
594
- rules,
595
- "detail",
596
- "popup",
597
- FALLBACK_RECOMMEND_SELECTORS.detail.popup
598
- ),
599
- required: false,
600
- description: "Candidate detail popup may mount inside the recommend frame"
601
- })
602
- ],
603
- viewportProbes: [
604
- createViewportCollapseProbe({
605
- id: "recommend_viewport_collapse",
606
- root: "frame",
607
- frameOwnerRoot: "frameOwner",
608
- required: true,
609
- repair: true,
610
- description: "Recommend frame/list viewport has not collapsed relative to the Chrome window"
611
- })
612
- ],
613
- accessibilityProbes: [
614
- createAccessibilityProbe({
615
- id: "accessibility_tree",
616
- required: true,
617
- minCount: 1,
618
- description: "Accessibility tree is readable without page script"
619
- })
620
- ],
621
- networkProbes: [
622
- createNetworkProbe({
623
- id: "zhipin_network_after_refresh",
624
- required: true,
625
- minCount: 1,
626
- urlPatterns: ["zhipin.com"],
627
- description: "A controlled refresh produced observable Boss network traffic"
628
- })
629
- ],
630
- repairActions: [
631
- {
632
- id: "page_reload",
633
- type: "page_reload",
634
- ignoreCache: false,
635
- waitMs: 2500,
636
- description: "Refresh the current page through Page.reload"
637
- }
638
- ]
639
- };
640
- }
641
-
642
- export function buildRecruitSelfHealConfig(rules = {}) {
643
- const iframeSelectors = selectorGroup(
644
- rules,
645
- "top",
646
- "search_iframe",
647
- FALLBACK_RECRUIT_SELECTORS.top.search_iframe
648
- );
649
- const cardSelectors = mergeSelectorGroups(
650
- rules,
651
- "frame",
652
- [
653
- "search_candidate_cards",
654
- "recruit_candidate_cards",
655
- "candidate_cards"
656
- ],
657
- FALLBACK_RECRUIT_SELECTORS.frame.candidate_cards
658
- );
659
-
660
- return {
661
- domain: "recruit",
662
- targetHints: DOMAIN_TARGET_HINTS.recruit,
663
- iframeSelectors,
664
- selectorProbes: [
665
- createSelectorProbe({
666
- id: "search_iframe",
667
- root: "top",
668
- selectors: iframeSelectors,
669
- required: true,
670
- description: "Search iframe can be discovered from the top document"
671
- }),
672
- createSelectorProbe({
673
- id: "candidate_cards",
674
- root: "frame",
675
- selectors: cardSelectors,
676
- required: true,
677
- description: "At least one search candidate card is visible"
678
- }),
679
- createSelectorProbe({
680
- id: "no_data_tip",
681
- root: "frame",
682
- selectors: selectorGroup(
683
- rules,
684
- "frame",
685
- "no_data",
686
- FALLBACK_RECRUIT_SELECTORS.frame.no_data
687
- ),
688
- required: false,
689
- description: "Search no-data state is optional and blocks candidate extraction if present"
690
- }),
691
- createSelectorProbe({
692
- id: "detail_popup_top",
693
- root: "top",
694
- selectors: selectorGroup(
695
- rules,
696
- "detail",
697
- "popup",
698
- FALLBACK_RECRUIT_SELECTORS.detail.popup
699
- ),
700
- required: false,
701
- description: "Candidate detail popup may be absent during idle health checks"
702
- }),
703
- createSelectorProbe({
704
- id: "detail_popup_frame",
705
- root: "frame",
706
- selectors: selectorGroup(
707
- rules,
708
- "detail",
709
- "popup",
710
- FALLBACK_RECRUIT_SELECTORS.detail.popup
711
- ),
712
- required: false,
713
- description: "Candidate detail popup may mount inside the search frame"
714
- })
715
- ],
716
- viewportProbes: [
717
- createViewportCollapseProbe({
718
- id: "recruit_viewport_collapse",
719
- root: "frame",
720
- frameOwnerRoot: "frameOwner",
721
- required: true,
722
- repair: true,
723
- description: "Search frame/list viewport has not collapsed relative to the Chrome window"
724
- })
725
- ],
726
- accessibilityProbes: [
727
- createAccessibilityProbe({
728
- id: "accessibility_tree",
729
- required: true,
730
- minCount: 1,
731
- description: "Accessibility tree is readable without page script"
732
- })
733
- ],
734
- networkProbes: [
735
- createNetworkProbe({
736
- id: "zhipin_network_after_refresh",
737
- required: true,
738
- minCount: 1,
739
- urlPatterns: ["zhipin.com"],
740
- description: "A controlled refresh produced observable Boss network traffic"
741
- })
742
- ],
743
- repairActions: [
744
- {
745
- id: "page_reload",
746
- type: "page_reload",
747
- ignoreCache: false,
748
- waitMs: 2500,
749
- description: "Refresh the current search page through Page.reload"
750
- }
751
- ]
752
- };
753
- }
754
-
755
- export function buildChatSelfHealConfig(rules = {}) {
756
- const cardSelectors = mergeSelectorGroups(
757
- rules,
758
- "top",
759
- [
760
- "chat_candidate_cards",
761
- "candidate_cards",
762
- "conversation_cards"
763
- ],
764
- FALLBACK_CHAT_SELECTORS.top.candidate_cards
765
- );
766
-
767
- return {
768
- domain: "chat",
769
- targetHints: DOMAIN_TARGET_HINTS.chat,
770
- selectorProbes: [
771
- createSelectorProbe({
772
- id: "candidate_cards",
773
- root: "top",
774
- selectors: cardSelectors,
775
- required: true,
776
- description: "At least one chat conversation candidate is visible"
777
- }),
778
- createSelectorProbe({
779
- id: "selected_candidate",
780
- root: "top",
781
- selectors: selectorGroup(
782
- rules,
783
- "top",
784
- "selected_candidate",
785
- FALLBACK_CHAT_SELECTORS.top.selected_candidate
786
- ),
787
- required: false,
788
- description: "A selected chat candidate is optional before extraction starts"
789
- }),
790
- createSelectorProbe({
791
- id: "online_resume_button",
792
- root: "top",
793
- selectors: selectorGroup(
794
- rules,
795
- "top",
796
- "online_resume_button",
797
- FALLBACK_CHAT_SELECTORS.top.online_resume_button
798
- ),
799
- required: false,
800
- description: "Online resume button appears after a candidate conversation is selected"
801
- }),
802
- createSelectorProbe({
803
- id: "resume_modal",
804
- root: "top",
805
- selectors: selectorGroup(
806
- rules,
807
- "detail",
808
- "resume_modal",
809
- FALLBACK_CHAT_SELECTORS.detail.resume_modal
810
- ),
811
- required: false,
812
- description: "Resume modal is optional during idle chat health checks"
813
- }),
814
- createSelectorProbe({
815
- id: "resume_iframe",
816
- root: "top",
817
- selectors: selectorGroup(
818
- rules,
819
- "detail",
820
- "resume_iframe",
821
- FALLBACK_CHAT_SELECTORS.detail.resume_iframe
822
- ),
823
- required: false,
824
- description: "Resume iframe appears after the online resume is opened"
825
- })
826
- ],
827
- viewportProbes: [
828
- createViewportCollapseProbe({
829
- id: "chat_viewport_collapse",
830
- root: "top",
831
- frameOwnerRoot: "top",
832
- required: true,
833
- repair: true,
834
- description: "Chat list viewport has not collapsed relative to the Chrome window"
835
- })
836
- ],
837
- accessibilityProbes: [
838
- createAccessibilityProbe({
839
- id: "accessibility_tree",
840
- required: true,
841
- minCount: 1,
842
- description: "Accessibility tree is readable without page script"
843
- })
844
- ],
845
- networkProbes: [
846
- createNetworkProbe({
847
- id: "zhipin_network_after_refresh",
848
- required: true,
849
- minCount: 1,
850
- urlPatterns: ["zhipin.com"],
851
- description: "A controlled refresh produced observable Boss network traffic"
852
- })
853
- ],
854
- repairActions: [
855
- {
856
- id: "page_reload",
857
- type: "page_reload",
858
- ignoreCache: false,
859
- waitMs: 2500,
860
- description: "Refresh the current chat page through Page.reload"
861
- }
862
- ]
863
- };
864
- }
865
-
866
- export async function resolveRecommendSelfHealRoots(client, config = buildRecommendSelfHealConfig()) {
867
- const topRoot = await getDocumentRoot(client);
868
- const iframe = await findIframeDocument(client, topRoot.nodeId, config.iframeSelectors);
869
- if (!iframe) {
870
- return {
871
- roots: {
872
- top: topRoot.nodeId
873
- },
874
- topRoot,
875
- iframe: null
876
- };
877
- }
878
-
879
- return {
880
- roots: {
881
- top: topRoot.nodeId,
882
- frame: iframe.documentNodeId,
883
- frameOwner: iframe.nodeId
884
- },
885
- topRoot,
886
- iframe
887
- };
888
- }
889
-
890
- export async function resolveRecruitSelfHealRoots(client, config = buildRecruitSelfHealConfig()) {
891
- const topRoot = await getDocumentRoot(client);
892
- const iframe = await findIframeDocument(client, topRoot.nodeId, config.iframeSelectors);
893
- if (!iframe) {
894
- return {
895
- roots: {
896
- top: topRoot.nodeId
897
- },
898
- topRoot,
899
- iframe: null
900
- };
901
- }
902
-
903
- return {
904
- roots: {
905
- top: topRoot.nodeId,
906
- frame: iframe.documentNodeId,
907
- frameOwner: iframe.nodeId
908
- },
909
- topRoot,
910
- iframe
911
- };
912
- }
913
-
914
- export async function resolveChatSelfHealRoots(client, _config = buildChatSelfHealConfig()) {
915
- const topRoot = await getDocumentRoot(client);
916
- return {
917
- roots: {
918
- top: topRoot.nodeId
919
- },
920
- topRoot,
921
- iframe: null
922
- };
923
- }
924
-
925
- export async function runRepairAction(client, action = {}) {
926
- if (action.type === "page_reload") {
927
- await client.Page.reload({ ignoreCache: Boolean(action.ignoreCache) });
928
- if (action.waitMs > 0) await sleep(action.waitMs);
929
- return {
930
- id: action.id || action.type,
931
- type: action.type,
932
- ok: true
933
- };
934
- }
935
-
936
- return {
937
- id: action.id || action.type || "unknown",
938
- type: action.type || "unknown",
939
- ok: false,
940
- error: `Unsupported repair action: ${action.type || "unknown"}`
941
- };
942
- }
943
-
944
- export function classifyBossTargets(targets = []) {
945
- const pageTargets = targets.filter((target) => target?.type === "page");
946
- const byDomain = {};
947
- for (const [domain, hints] of Object.entries(DOMAIN_TARGET_HINTS)) {
948
- const target = pageTargets.find((item) => {
949
- const url = String(item?.url || "");
950
- if (domain === "chat") {
951
- return hints.some((hint) => url.includes(hint))
952
- && !url.includes("/web/chat/recommend")
953
- && !url.includes("/web/chat/search");
954
- }
955
- return hints.some((hint) => url.includes(hint));
956
- });
957
- byDomain[domain] = target
958
- ? {
959
- status: "available",
960
- target: {
961
- id: target.id,
962
- type: target.type,
963
- url: target.url,
964
- title: target.title
965
- }
966
- }
967
- : {
968
- status: "blocked",
969
- reason: `No live ${domain} target is open in Chrome`
970
- };
971
- }
972
- return byDomain;
973
- }
1
+ import {
2
+ findIframeDocument,
3
+ getAccessibilityTree,
4
+ getDocumentRoot,
5
+ querySelectorAll,
6
+ sleep
7
+ } from "../browser/index.js";
8
+ import {
9
+ compactViewportHealthResult,
10
+ ensureHealthyViewport
11
+ } from "./viewport.js";
12
+
13
+ export {
14
+ buildViewportHealthDiagnostics,
15
+ compactViewportHealthResult,
16
+ compactViewportState,
17
+ createViewportRunGuard,
18
+ ensureHealthyViewport,
19
+ getCurrentWindowInfo,
20
+ isListViewportCollapsed,
21
+ readViewportState,
22
+ setWindowStateIfPossible,
23
+ toggleWindowStateForViewportRecovery,
24
+ VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH,
25
+ VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO,
26
+ VIEWPORT_COLLAPSE_RATIO_THRESHOLD
27
+ } from "./viewport.js";
28
+
29
+ export const PROBE_STATUS = Object.freeze({
30
+ PASS: "pass",
31
+ FAIL: "fail",
32
+ BLOCKED: "blocked",
33
+ OPTIONAL_ABSENT: "optional_absent",
34
+ ERROR: "error"
35
+ });
36
+
37
+ export const HEALTH_STATUS = Object.freeze({
38
+ HEALTHY: "healthy",
39
+ DEGRADED: "degraded",
40
+ BLOCKED: "blocked"
41
+ });
42
+
43
+ export const DOMAIN_TARGET_HINTS = Object.freeze({
44
+ recommend: ["/web/chat/recommend"],
45
+ recruit: ["/web/chat/search"],
46
+ chat: ["/web/chat/index", "/web/chat"]
47
+ });
48
+
49
+ const FALLBACK_RECOMMEND_SELECTORS = Object.freeze({
50
+ top: {
51
+ recommend_iframe: [
52
+ 'iframe[name="recommendFrame"]',
53
+ 'iframe[src*="/web/frame/recommend/"]',
54
+ "iframe"
55
+ ]
56
+ },
57
+ frame: {
58
+ filter_trigger: [
59
+ ".filter-label-wrap",
60
+ ".recommend-filter.op-filter"
61
+ ],
62
+ filter_panel: [
63
+ ".recommend-filter.op-filter .filter-panel",
64
+ ".recommend-filter .filter-panel",
65
+ ".filter-panel"
66
+ ],
67
+ tab_items: [
68
+ "li.tab-item[data-status]",
69
+ 'li[data-status][class*="tab"]'
70
+ ],
71
+ candidate_cards: [
72
+ ".candidate-card-wrap .card-inner[data-geek]",
73
+ ".candidate-card-wrap [data-geek]",
74
+ "ul.card-list > li.card-item",
75
+ ".card-inner[data-geekid]",
76
+ "li.geek-info-card",
77
+ "a[data-geekid]",
78
+ ".candidate-card-wrap"
79
+ ]
80
+ },
81
+ detail: {
82
+ popup: [
83
+ ".dialog-wrap.active",
84
+ ".boss-popup__wrapper",
85
+ ".boss-popup_wrapper",
86
+ ".boss-dialog_wrapper",
87
+ ".boss-dialog",
88
+ ".resume-item-detail",
89
+ ".geek-detail-modal",
90
+ '[class*="popup"][class*="wrapper"]',
91
+ '[class*="dialog"][class*="wrapper"]'
92
+ ]
93
+ }
94
+ });
95
+
96
+ const FALLBACK_RECRUIT_SELECTORS = Object.freeze({
97
+ top: {
98
+ search_iframe: [
99
+ 'iframe[name="searchFrame"]',
100
+ 'iframe[src*="/web/frame/search/"]',
101
+ "iframe"
102
+ ]
103
+ },
104
+ frame: {
105
+ candidate_cards: [
106
+ "li.geek-info-card a[data-jid]",
107
+ "li.geek-info-card a[data-geekid]",
108
+ ".geek-info-card a[data-jid]",
109
+ ".geek-info-card a[data-geekid]",
110
+ ".geek-info-card a",
111
+ "a[data-jid]",
112
+ "a[data-geekid]"
113
+ ],
114
+ no_data: [
115
+ "i.tip-nodata",
116
+ ".tip-nodata",
117
+ ".empty-tip",
118
+ ".empty-text"
119
+ ]
120
+ },
121
+ detail: {
122
+ popup: [
123
+ ".dialog-wrap.active",
124
+ ".boss-popup__wrapper",
125
+ ".boss-popup_wrapper",
126
+ ".boss-dialog_wrapper",
127
+ ".boss-dialog",
128
+ ".resume-item-detail",
129
+ ".geek-detail-modal",
130
+ ".resume-container",
131
+ '[class*="popup"][class*="wrapper"]',
132
+ '[class*="dialog"][class*="wrapper"]'
133
+ ]
134
+ }
135
+ });
136
+
137
+ const FALLBACK_CHAT_SELECTORS = Object.freeze({
138
+ top: {
139
+ candidate_cards: [
140
+ ".geek-item[data-id]",
141
+ 'div[role="listitem"] .geek-item[data-id]',
142
+ ".geek-item",
143
+ ".geek-item-wrap",
144
+ 'div[role="listitem"]'
145
+ ],
146
+ selected_candidate: [
147
+ ".geek-item.selected[data-id]",
148
+ ".geek-item.selected",
149
+ ".geek-item.active[data-id]",
150
+ ".geek-item.active"
151
+ ],
152
+ online_resume_button: [
153
+ "a.btn.resume-btn-online",
154
+ "a.resume-btn-online",
155
+ ".btn.resume-btn-online",
156
+ ".resume-btn-online"
157
+ ]
158
+ },
159
+ detail: {
160
+ resume_modal: [
161
+ ".boss-popup__wrapper",
162
+ ".new-chat-resume-dialog-main-ui",
163
+ ".dialog-wrap.active",
164
+ ".boss-dialog",
165
+ ".geek-detail-modal",
166
+ ".modal",
167
+ ".resume-container",
168
+ ".resume-content-wrap",
169
+ ".resume-common-wrap",
170
+ ".resume-detail",
171
+ ".resume-recommend"
172
+ ],
173
+ resume_iframe: [
174
+ 'iframe[src*="/web/frame/c-resume/"]',
175
+ 'iframe[src*="resume"]',
176
+ 'iframe[name*="resume"]'
177
+ ]
178
+ }
179
+ });
180
+
181
+ function uniqueStrings(values = []) {
182
+ return [...new Set(values.filter((value) => typeof value === "string" && value.trim()))];
183
+ }
184
+
185
+ function selectorGroup(rules, scope, name, fallback = []) {
186
+ return uniqueStrings(rules?.selectors?.[scope]?.[name] || fallback);
187
+ }
188
+
189
+ function mergeSelectorGroups(rules, scope, names = [], fallback = []) {
190
+ const selectors = names.flatMap((name) => rules?.selectors?.[scope]?.[name] || []);
191
+ return uniqueStrings(selectors.length ? selectors : fallback);
192
+ }
193
+
194
+ function rootNodeId(roots = {}, name) {
195
+ for (const root of [roots?.[name], roots?.rootNodes?.[name], roots?.roots?.[name]]) {
196
+ if (typeof root === "number" && root > 0) return root;
197
+ if (root?.nodeId) return root.nodeId;
198
+ if (root?.documentNodeId) return root.documentNodeId;
199
+ }
200
+ return 0;
201
+ }
202
+
203
+ function stringMatchesAnyPattern(value, patterns = []) {
204
+ const text = String(value || "");
205
+ return patterns.some((pattern) => {
206
+ if (pattern instanceof RegExp) return pattern.test(text);
207
+ return text.includes(String(pattern || ""));
208
+ });
209
+ }
210
+
211
+ export function createSelectorProbe({
212
+ id,
213
+ root = "frame",
214
+ selectors = [],
215
+ required = false,
216
+ minCount = 1,
217
+ description = ""
218
+ } = {}) {
219
+ if (!id) throw new Error("Selector probe requires an id");
220
+ return {
221
+ type: "selector",
222
+ id,
223
+ root,
224
+ selectors: uniqueStrings(selectors),
225
+ required: Boolean(required),
226
+ minCount,
227
+ description
228
+ };
229
+ }
230
+
231
+ export function createAccessibilityProbe({
232
+ id,
233
+ required = false,
234
+ minCount = 1,
235
+ roleIncludes = [],
236
+ nameIncludes = [],
237
+ description = ""
238
+ } = {}) {
239
+ if (!id) throw new Error("Accessibility probe requires an id");
240
+ return {
241
+ type: "accessibility",
242
+ id,
243
+ required: Boolean(required),
244
+ minCount,
245
+ roleIncludes: uniqueStrings(roleIncludes),
246
+ nameIncludes: uniqueStrings(nameIncludes),
247
+ description
248
+ };
249
+ }
250
+
251
+ export function createNetworkProbe({
252
+ id,
253
+ required = false,
254
+ minCount = 1,
255
+ urlPatterns = [],
256
+ description = ""
257
+ } = {}) {
258
+ if (!id) throw new Error("Network probe requires an id");
259
+ return {
260
+ type: "network",
261
+ id,
262
+ required: Boolean(required),
263
+ minCount,
264
+ urlPatterns,
265
+ description
266
+ };
267
+ }
268
+
269
+ export function createViewportCollapseProbe({
270
+ id = "viewport_collapse",
271
+ root = "frame",
272
+ frameOwnerRoot = "frameOwner",
273
+ required = true,
274
+ repair = true,
275
+ description = ""
276
+ } = {}) {
277
+ if (!id) throw new Error("Viewport collapse probe requires an id");
278
+ return {
279
+ type: "viewport",
280
+ id,
281
+ root,
282
+ frameOwnerRoot,
283
+ required: Boolean(required),
284
+ repair: Boolean(repair),
285
+ description
286
+ };
287
+ }
288
+
289
+ export async function runSelectorProbe(client, roots, probe) {
290
+ const nodeId = rootNodeId(roots, probe.root);
291
+ if (!nodeId) {
292
+ return {
293
+ ...probe,
294
+ ok: !probe.required,
295
+ status: probe.required ? PROBE_STATUS.BLOCKED : PROBE_STATUS.OPTIONAL_ABSENT,
296
+ count: 0,
297
+ selector_counts: [],
298
+ error: `Root not found: ${probe.root}`
299
+ };
300
+ }
301
+
302
+ const selectorCounts = [];
303
+ try {
304
+ for (const selector of probe.selectors) {
305
+ const nodeIds = await querySelectorAll(client, nodeId, selector);
306
+ selectorCounts.push({
307
+ selector,
308
+ count: nodeIds.length
309
+ });
310
+ }
311
+ } catch (error) {
312
+ return {
313
+ ...probe,
314
+ ok: !probe.required,
315
+ status: probe.required ? PROBE_STATUS.ERROR : PROBE_STATUS.OPTIONAL_ABSENT,
316
+ count: 0,
317
+ selector_counts: selectorCounts,
318
+ error: error?.message || String(error)
319
+ };
320
+ }
321
+
322
+ const count = selectorCounts.reduce((max, item) => Math.max(max, item.count), 0);
323
+ const ok = count >= probe.minCount;
324
+ return {
325
+ ...probe,
326
+ ok: probe.required ? ok : true,
327
+ status: ok ? PROBE_STATUS.PASS : probe.required ? PROBE_STATUS.FAIL : PROBE_STATUS.OPTIONAL_ABSENT,
328
+ count,
329
+ selector_counts: selectorCounts,
330
+ matched_selectors: selectorCounts.filter((item) => item.count > 0)
331
+ };
332
+ }
333
+
334
+ export async function runAccessibilityProbe(client, probe) {
335
+ try {
336
+ const tree = await getAccessibilityTree(client);
337
+ const nodes = tree?.nodes || [];
338
+ const matches = nodes.filter((node) => {
339
+ const role = String(node?.role?.value || "");
340
+ const name = String(node?.name?.value || "");
341
+ const roleOk = probe.roleIncludes.length === 0
342
+ || probe.roleIncludes.some((value) => role.includes(value));
343
+ const nameOk = probe.nameIncludes.length === 0
344
+ || probe.nameIncludes.some((value) => name.includes(value));
345
+ return roleOk && nameOk;
346
+ });
347
+ const ok = matches.length >= probe.minCount;
348
+ return {
349
+ ...probe,
350
+ ok: probe.required ? ok : true,
351
+ status: ok ? PROBE_STATUS.PASS : probe.required ? PROBE_STATUS.FAIL : PROBE_STATUS.OPTIONAL_ABSENT,
352
+ count: matches.length,
353
+ total_ax_nodes: nodes.length
354
+ };
355
+ } catch (error) {
356
+ return {
357
+ ...probe,
358
+ ok: !probe.required,
359
+ status: probe.required ? PROBE_STATUS.ERROR : PROBE_STATUS.OPTIONAL_ABSENT,
360
+ count: 0,
361
+ total_ax_nodes: 0,
362
+ error: error?.message || String(error)
363
+ };
364
+ }
365
+ }
366
+
367
+ export function runNetworkProbe(networkEvents = [], probe) {
368
+ const matches = networkEvents.filter((event) => {
369
+ if (!probe.urlPatterns.length) return true;
370
+ return stringMatchesAnyPattern(event?.url || event?.response?.url, probe.urlPatterns);
371
+ });
372
+ const ok = matches.length >= probe.minCount;
373
+ return {
374
+ ...probe,
375
+ ok: probe.required ? ok : true,
376
+ status: ok ? PROBE_STATUS.PASS : probe.required ? PROBE_STATUS.FAIL : PROBE_STATUS.OPTIONAL_ABSENT,
377
+ count: matches.length,
378
+ sample_urls: matches.slice(0, 5).map((event) => event?.url || event?.response?.url || "")
379
+ };
380
+ }
381
+
382
+ export async function runViewportCollapseProbe(client, roots, probe, {
383
+ reacquireRoots = null
384
+ } = {}) {
385
+ const nodeId = rootNodeId(roots, probe.root);
386
+ if (!nodeId) {
387
+ return {
388
+ ...probe,
389
+ ok: !probe.required,
390
+ status: probe.required ? PROBE_STATUS.BLOCKED : PROBE_STATUS.OPTIONAL_ABSENT,
391
+ count: 0,
392
+ collapsed: false,
393
+ recovered: false,
394
+ error: `Root not found: ${probe.root}`
395
+ };
396
+ }
397
+
398
+ try {
399
+ const health = await ensureHealthyViewport(client, {
400
+ roots,
401
+ root: probe.root,
402
+ frameOwnerRoot: probe.frameOwnerRoot,
403
+ reason: probe.id,
404
+ repair: probe.repair,
405
+ reacquireRoots
406
+ });
407
+ const ok = Boolean(health.ok);
408
+ return {
409
+ ...probe,
410
+ ok: probe.required ? ok : true,
411
+ status: ok ? PROBE_STATUS.PASS : probe.required ? PROBE_STATUS.FAIL : PROBE_STATUS.OPTIONAL_ABSENT,
412
+ count: ok ? 1 : 0,
413
+ collapsed: Boolean(health.collapsed),
414
+ recovered: Boolean(health.recovered),
415
+ viewport_health: compactViewportHealthResult(health),
416
+ error: health.error || null
417
+ };
418
+ } catch (error) {
419
+ return {
420
+ ...probe,
421
+ ok: !probe.required,
422
+ status: probe.required ? PROBE_STATUS.ERROR : PROBE_STATUS.OPTIONAL_ABSENT,
423
+ count: 0,
424
+ collapsed: false,
425
+ recovered: false,
426
+ error: error?.message || String(error)
427
+ };
428
+ }
429
+ }
430
+
431
+ export function summarizeProbeResults(probes = []) {
432
+ const required = probes.filter((probe) => probe.required);
433
+ const blocked = required.filter((probe) => probe.status === PROBE_STATUS.BLOCKED);
434
+ const failed = required.filter((probe) => !probe.ok && probe.status !== PROBE_STATUS.BLOCKED);
435
+ const optionalAbsent = probes.filter((probe) => probe.status === PROBE_STATUS.OPTIONAL_ABSENT);
436
+ const passed = probes.filter((probe) => probe.status === PROBE_STATUS.PASS);
437
+
438
+ return {
439
+ status: blocked.length
440
+ ? HEALTH_STATUS.BLOCKED
441
+ : failed.length
442
+ ? HEALTH_STATUS.DEGRADED
443
+ : HEALTH_STATUS.HEALTHY,
444
+ required_count: required.length,
445
+ passed_count: passed.length,
446
+ failed_required_ids: failed.map((probe) => probe.id),
447
+ blocked_required_ids: blocked.map((probe) => probe.id),
448
+ optional_absent_ids: optionalAbsent.map((probe) => probe.id)
449
+ };
450
+ }
451
+
452
+ export function buildDriftReport(probes = []) {
453
+ return probes
454
+ .filter((probe) => probe.required && !probe.ok)
455
+ .map((probe) => ({
456
+ probe_id: probe.id,
457
+ probe_type: probe.type,
458
+ status: probe.status,
459
+ root: probe.root || null,
460
+ expected_min_count: probe.minCount,
461
+ observed_count: probe.count || 0,
462
+ selectors: probe.selectors || [],
463
+ viewport_health: probe.viewport_health || undefined,
464
+ error: probe.error || null
465
+ }));
466
+ }
467
+
468
+ export async function runSelfHealCheck({
469
+ client,
470
+ domain,
471
+ roots = {},
472
+ selectorProbes = [],
473
+ accessibilityProbes = [],
474
+ viewportProbes = [],
475
+ networkProbes = [],
476
+ networkEvents = [],
477
+ reacquireRoots = null
478
+ } = {}) {
479
+ const selectorResults = [];
480
+ for (const probe of selectorProbes) {
481
+ selectorResults.push(await runSelectorProbe(client, roots, probe));
482
+ }
483
+
484
+ const accessibilityResults = [];
485
+ for (const probe of accessibilityProbes) {
486
+ accessibilityResults.push(await runAccessibilityProbe(client, probe));
487
+ }
488
+
489
+ const viewportResults = [];
490
+ const resolveViewportRoots = typeof reacquireRoots === "function"
491
+ ? reacquireRoots
492
+ : domain === "recommend"
493
+ ? async () => (await resolveRecommendSelfHealRoots(client)).roots
494
+ : domain === "recruit"
495
+ ? async () => (await resolveRecruitSelfHealRoots(client)).roots
496
+ : domain === "chat"
497
+ ? async () => (await resolveChatSelfHealRoots(client)).roots
498
+ : null;
499
+ for (const probe of viewportProbes) {
500
+ viewportResults.push(await runViewportCollapseProbe(client, roots, probe, {
501
+ reacquireRoots: resolveViewportRoots
502
+ }));
503
+ }
504
+
505
+ const networkResults = networkProbes.map((probe) => runNetworkProbe(networkEvents, probe));
506
+ const probes = [...selectorResults, ...accessibilityResults, ...viewportResults, ...networkResults];
507
+ const summary = summarizeProbeResults(probes);
508
+
509
+ return {
510
+ domain,
511
+ status: summary.status,
512
+ summary,
513
+ probes,
514
+ drift_report: buildDriftReport(probes)
515
+ };
516
+ }
517
+
518
+ export function buildRecommendSelfHealConfig(rules = {}) {
519
+ const iframeSelectors = selectorGroup(
520
+ rules,
521
+ "top",
522
+ "recommend_iframe",
523
+ FALLBACK_RECOMMEND_SELECTORS.top.recommend_iframe
524
+ );
525
+ const cardSelectors = mergeSelectorGroups(
526
+ rules,
527
+ "frame",
528
+ [
529
+ "latest_card_inner",
530
+ "recommend_card_inner",
531
+ "featured_card_anchor",
532
+ "recommend_cards",
533
+ "featured_cards",
534
+ "latest_cards"
535
+ ],
536
+ FALLBACK_RECOMMEND_SELECTORS.frame.candidate_cards
537
+ );
538
+
539
+ return {
540
+ domain: "recommend",
541
+ targetHints: DOMAIN_TARGET_HINTS.recommend,
542
+ iframeSelectors,
543
+ selectorProbes: [
544
+ createSelectorProbe({
545
+ id: "recommend_iframe",
546
+ root: "top",
547
+ selectors: iframeSelectors,
548
+ required: true,
549
+ description: "Recommend iframe can be discovered from the top document"
550
+ }),
551
+ createSelectorProbe({
552
+ id: "filter_trigger",
553
+ root: "frame",
554
+ selectors: selectorGroup(
555
+ rules,
556
+ "frame",
557
+ "filter_trigger",
558
+ FALLBACK_RECOMMEND_SELECTORS.frame.filter_trigger
559
+ ),
560
+ required: true,
561
+ description: "Filter trigger is mounted in the recommend frame"
562
+ }),
563
+ createSelectorProbe({
564
+ id: "candidate_cards",
565
+ root: "frame",
566
+ selectors: cardSelectors,
567
+ required: true,
568
+ description: "At least one recommend candidate card is visible"
569
+ }),
570
+ createSelectorProbe({
571
+ id: "tab_items",
572
+ root: "frame",
573
+ selectors: selectorGroup(
574
+ rules,
575
+ "frame",
576
+ "tab_items",
577
+ FALLBACK_RECOMMEND_SELECTORS.frame.tab_items
578
+ ),
579
+ required: false,
580
+ description: "Recommend tab controls are mounted when this layout exposes them"
581
+ }),
582
+ createSelectorProbe({
583
+ id: "filter_panel",
584
+ root: "frame",
585
+ selectors: selectorGroup(
586
+ rules,
587
+ "frame",
588
+ "filter_panel",
589
+ FALLBACK_RECOMMEND_SELECTORS.frame.filter_panel
590
+ ),
591
+ required: false,
592
+ description: "Filter panel is optional because it is absent until opened"
593
+ }),
594
+ createSelectorProbe({
595
+ id: "detail_popup_top",
596
+ root: "top",
597
+ selectors: selectorGroup(
598
+ rules,
599
+ "detail",
600
+ "popup",
601
+ FALLBACK_RECOMMEND_SELECTORS.detail.popup
602
+ ),
603
+ required: false,
604
+ description: "Candidate detail popup may be absent during idle health checks"
605
+ }),
606
+ createSelectorProbe({
607
+ id: "detail_popup_frame",
608
+ root: "frame",
609
+ selectors: selectorGroup(
610
+ rules,
611
+ "detail",
612
+ "popup",
613
+ FALLBACK_RECOMMEND_SELECTORS.detail.popup
614
+ ),
615
+ required: false,
616
+ description: "Candidate detail popup may mount inside the recommend frame"
617
+ })
618
+ ],
619
+ viewportProbes: [
620
+ createViewportCollapseProbe({
621
+ id: "recommend_viewport_collapse",
622
+ root: "frame",
623
+ frameOwnerRoot: "frameOwner",
624
+ required: true,
625
+ repair: true,
626
+ description: "Recommend frame/list viewport has not collapsed relative to the Chrome window"
627
+ })
628
+ ],
629
+ accessibilityProbes: [
630
+ createAccessibilityProbe({
631
+ id: "accessibility_tree",
632
+ required: true,
633
+ minCount: 1,
634
+ description: "Accessibility tree is readable without page script"
635
+ })
636
+ ],
637
+ networkProbes: [
638
+ createNetworkProbe({
639
+ id: "zhipin_network_after_refresh",
640
+ required: true,
641
+ minCount: 1,
642
+ urlPatterns: ["zhipin.com"],
643
+ description: "A controlled refresh produced observable Boss network traffic"
644
+ })
645
+ ],
646
+ repairActions: [
647
+ {
648
+ id: "page_reload",
649
+ type: "page_reload",
650
+ ignoreCache: false,
651
+ waitMs: 2500,
652
+ description: "Refresh the current page through Page.reload"
653
+ }
654
+ ]
655
+ };
656
+ }
657
+
658
+ export function buildRecruitSelfHealConfig(rules = {}) {
659
+ const iframeSelectors = selectorGroup(
660
+ rules,
661
+ "top",
662
+ "search_iframe",
663
+ FALLBACK_RECRUIT_SELECTORS.top.search_iframe
664
+ );
665
+ const cardSelectors = mergeSelectorGroups(
666
+ rules,
667
+ "frame",
668
+ [
669
+ "search_candidate_cards",
670
+ "recruit_candidate_cards",
671
+ "candidate_cards"
672
+ ],
673
+ FALLBACK_RECRUIT_SELECTORS.frame.candidate_cards
674
+ );
675
+
676
+ return {
677
+ domain: "recruit",
678
+ targetHints: DOMAIN_TARGET_HINTS.recruit,
679
+ iframeSelectors,
680
+ selectorProbes: [
681
+ createSelectorProbe({
682
+ id: "search_iframe",
683
+ root: "top",
684
+ selectors: iframeSelectors,
685
+ required: true,
686
+ description: "Search iframe can be discovered from the top document"
687
+ }),
688
+ createSelectorProbe({
689
+ id: "candidate_cards",
690
+ root: "frame",
691
+ selectors: cardSelectors,
692
+ required: true,
693
+ description: "At least one search candidate card is visible"
694
+ }),
695
+ createSelectorProbe({
696
+ id: "no_data_tip",
697
+ root: "frame",
698
+ selectors: selectorGroup(
699
+ rules,
700
+ "frame",
701
+ "no_data",
702
+ FALLBACK_RECRUIT_SELECTORS.frame.no_data
703
+ ),
704
+ required: false,
705
+ description: "Search no-data state is optional and blocks candidate extraction if present"
706
+ }),
707
+ createSelectorProbe({
708
+ id: "detail_popup_top",
709
+ root: "top",
710
+ selectors: selectorGroup(
711
+ rules,
712
+ "detail",
713
+ "popup",
714
+ FALLBACK_RECRUIT_SELECTORS.detail.popup
715
+ ),
716
+ required: false,
717
+ description: "Candidate detail popup may be absent during idle health checks"
718
+ }),
719
+ createSelectorProbe({
720
+ id: "detail_popup_frame",
721
+ root: "frame",
722
+ selectors: selectorGroup(
723
+ rules,
724
+ "detail",
725
+ "popup",
726
+ FALLBACK_RECRUIT_SELECTORS.detail.popup
727
+ ),
728
+ required: false,
729
+ description: "Candidate detail popup may mount inside the search frame"
730
+ })
731
+ ],
732
+ viewportProbes: [
733
+ createViewportCollapseProbe({
734
+ id: "recruit_viewport_collapse",
735
+ root: "frame",
736
+ frameOwnerRoot: "frameOwner",
737
+ required: true,
738
+ repair: true,
739
+ description: "Search frame/list viewport has not collapsed relative to the Chrome window"
740
+ })
741
+ ],
742
+ accessibilityProbes: [
743
+ createAccessibilityProbe({
744
+ id: "accessibility_tree",
745
+ required: true,
746
+ minCount: 1,
747
+ description: "Accessibility tree is readable without page script"
748
+ })
749
+ ],
750
+ networkProbes: [
751
+ createNetworkProbe({
752
+ id: "zhipin_network_after_refresh",
753
+ required: true,
754
+ minCount: 1,
755
+ urlPatterns: ["zhipin.com"],
756
+ description: "A controlled refresh produced observable Boss network traffic"
757
+ })
758
+ ],
759
+ repairActions: [
760
+ {
761
+ id: "page_reload",
762
+ type: "page_reload",
763
+ ignoreCache: false,
764
+ waitMs: 2500,
765
+ description: "Refresh the current search page through Page.reload"
766
+ }
767
+ ]
768
+ };
769
+ }
770
+
771
+ export function buildChatSelfHealConfig(rules = {}) {
772
+ const cardSelectors = mergeSelectorGroups(
773
+ rules,
774
+ "top",
775
+ [
776
+ "chat_candidate_cards",
777
+ "candidate_cards",
778
+ "conversation_cards"
779
+ ],
780
+ FALLBACK_CHAT_SELECTORS.top.candidate_cards
781
+ );
782
+
783
+ return {
784
+ domain: "chat",
785
+ targetHints: DOMAIN_TARGET_HINTS.chat,
786
+ selectorProbes: [
787
+ createSelectorProbe({
788
+ id: "candidate_cards",
789
+ root: "top",
790
+ selectors: cardSelectors,
791
+ required: true,
792
+ description: "At least one chat conversation candidate is visible"
793
+ }),
794
+ createSelectorProbe({
795
+ id: "selected_candidate",
796
+ root: "top",
797
+ selectors: selectorGroup(
798
+ rules,
799
+ "top",
800
+ "selected_candidate",
801
+ FALLBACK_CHAT_SELECTORS.top.selected_candidate
802
+ ),
803
+ required: false,
804
+ description: "A selected chat candidate is optional before extraction starts"
805
+ }),
806
+ createSelectorProbe({
807
+ id: "online_resume_button",
808
+ root: "top",
809
+ selectors: selectorGroup(
810
+ rules,
811
+ "top",
812
+ "online_resume_button",
813
+ FALLBACK_CHAT_SELECTORS.top.online_resume_button
814
+ ),
815
+ required: false,
816
+ description: "Online resume button appears after a candidate conversation is selected"
817
+ }),
818
+ createSelectorProbe({
819
+ id: "resume_modal",
820
+ root: "top",
821
+ selectors: selectorGroup(
822
+ rules,
823
+ "detail",
824
+ "resume_modal",
825
+ FALLBACK_CHAT_SELECTORS.detail.resume_modal
826
+ ),
827
+ required: false,
828
+ description: "Resume modal is optional during idle chat health checks"
829
+ }),
830
+ createSelectorProbe({
831
+ id: "resume_iframe",
832
+ root: "top",
833
+ selectors: selectorGroup(
834
+ rules,
835
+ "detail",
836
+ "resume_iframe",
837
+ FALLBACK_CHAT_SELECTORS.detail.resume_iframe
838
+ ),
839
+ required: false,
840
+ description: "Resume iframe appears after the online resume is opened"
841
+ })
842
+ ],
843
+ viewportProbes: [
844
+ createViewportCollapseProbe({
845
+ id: "chat_viewport_collapse",
846
+ root: "top",
847
+ frameOwnerRoot: "top",
848
+ required: true,
849
+ repair: true,
850
+ description: "Chat list viewport has not collapsed relative to the Chrome window"
851
+ })
852
+ ],
853
+ accessibilityProbes: [
854
+ createAccessibilityProbe({
855
+ id: "accessibility_tree",
856
+ required: true,
857
+ minCount: 1,
858
+ description: "Accessibility tree is readable without page script"
859
+ })
860
+ ],
861
+ networkProbes: [
862
+ createNetworkProbe({
863
+ id: "zhipin_network_after_refresh",
864
+ required: true,
865
+ minCount: 1,
866
+ urlPatterns: ["zhipin.com"],
867
+ description: "A controlled refresh produced observable Boss network traffic"
868
+ })
869
+ ],
870
+ repairActions: [
871
+ {
872
+ id: "page_reload",
873
+ type: "page_reload",
874
+ ignoreCache: false,
875
+ waitMs: 2500,
876
+ description: "Refresh the current chat page through Page.reload"
877
+ }
878
+ ]
879
+ };
880
+ }
881
+
882
+ export async function resolveRecommendSelfHealRoots(client, config = buildRecommendSelfHealConfig()) {
883
+ const topRoot = await getDocumentRoot(client);
884
+ const iframe = await findIframeDocument(client, topRoot.nodeId, config.iframeSelectors);
885
+ if (!iframe) {
886
+ return {
887
+ roots: {
888
+ top: topRoot.nodeId
889
+ },
890
+ topRoot,
891
+ iframe: null
892
+ };
893
+ }
894
+
895
+ return {
896
+ roots: {
897
+ top: topRoot.nodeId,
898
+ frame: iframe.documentNodeId,
899
+ frameOwner: iframe.nodeId
900
+ },
901
+ topRoot,
902
+ iframe
903
+ };
904
+ }
905
+
906
+ export async function resolveRecruitSelfHealRoots(client, config = buildRecruitSelfHealConfig()) {
907
+ const topRoot = await getDocumentRoot(client);
908
+ const iframe = await findIframeDocument(client, topRoot.nodeId, config.iframeSelectors);
909
+ if (!iframe) {
910
+ return {
911
+ roots: {
912
+ top: topRoot.nodeId
913
+ },
914
+ topRoot,
915
+ iframe: null
916
+ };
917
+ }
918
+
919
+ return {
920
+ roots: {
921
+ top: topRoot.nodeId,
922
+ frame: iframe.documentNodeId,
923
+ frameOwner: iframe.nodeId
924
+ },
925
+ topRoot,
926
+ iframe
927
+ };
928
+ }
929
+
930
+ export async function resolveChatSelfHealRoots(client, _config = buildChatSelfHealConfig()) {
931
+ const topRoot = await getDocumentRoot(client);
932
+ return {
933
+ roots: {
934
+ top: topRoot.nodeId
935
+ },
936
+ topRoot,
937
+ iframe: null
938
+ };
939
+ }
940
+
941
+ export async function runRepairAction(client, action = {}) {
942
+ if (action.type === "page_reload") {
943
+ await client.Page.reload({ ignoreCache: Boolean(action.ignoreCache) });
944
+ if (action.waitMs > 0) await sleep(action.waitMs);
945
+ return {
946
+ id: action.id || action.type,
947
+ type: action.type,
948
+ ok: true
949
+ };
950
+ }
951
+
952
+ return {
953
+ id: action.id || action.type || "unknown",
954
+ type: action.type || "unknown",
955
+ ok: false,
956
+ error: `Unsupported repair action: ${action.type || "unknown"}`
957
+ };
958
+ }
959
+
960
+ export function classifyBossTargets(targets = []) {
961
+ const pageTargets = targets.filter((target) => target?.type === "page");
962
+ const byDomain = {};
963
+ for (const [domain, hints] of Object.entries(DOMAIN_TARGET_HINTS)) {
964
+ const target = pageTargets.find((item) => {
965
+ const url = String(item?.url || "");
966
+ if (domain === "chat") {
967
+ return hints.some((hint) => url.includes(hint))
968
+ && !url.includes("/web/chat/recommend")
969
+ && !url.includes("/web/chat/search");
970
+ }
971
+ return hints.some((hint) => url.includes(hint));
972
+ });
973
+ byDomain[domain] = target
974
+ ? {
975
+ status: "available",
976
+ target: {
977
+ id: target.id,
978
+ type: target.type,
979
+ url: target.url,
980
+ title: target.title
981
+ }
982
+ }
983
+ : {
984
+ status: "blocked",
985
+ reason: `No live ${domain} target is open in Chrome`
986
+ };
987
+ }
988
+ return byDomain;
989
+ }