@reconcrap/boss-recommend-mcp 2.0.47 → 2.0.48

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 (53) hide show
  1. package/bin/boss-recommend-mcp.js +4 -4
  2. package/config/screening-config.example.json +27 -27
  3. package/package.json +1 -1
  4. package/scripts/postinstall.cjs +44 -44
  5. package/skills/boss-chat/README.md +39 -39
  6. package/skills/boss-chat/SKILL.md +93 -93
  7. package/skills/boss-recommend-pipeline/README.md +12 -12
  8. package/skills/boss-recommend-pipeline/SKILL.md +180 -180
  9. package/skills/boss-recruit-pipeline/README.md +17 -17
  10. package/skills/boss-recruit-pipeline/SKILL.md +58 -58
  11. package/src/chat-mcp.js +1780 -1780
  12. package/src/chat-runtime-config.js +749 -749
  13. package/src/cli.js +3054 -3054
  14. package/src/core/boss-cards/index.js +199 -199
  15. package/src/core/browser/index.js +1586 -1453
  16. package/src/core/capture/index.js +1201 -1201
  17. package/src/core/cv-acquisition/index.js +238 -238
  18. package/src/core/cv-capture-target/index.js +299 -299
  19. package/src/core/greet-quota/index.js +54 -54
  20. package/src/core/infinite-list/index.js +1326 -1326
  21. package/src/core/reporting/legacy-csv.js +341 -341
  22. package/src/core/run/timing.js +33 -33
  23. package/src/core/self-heal/index.js +973 -973
  24. package/src/core/self-heal/viewport.js +564 -564
  25. package/src/domains/chat/cards.js +137 -137
  26. package/src/domains/chat/constants.js +221 -221
  27. package/src/domains/chat/detail.js +1668 -1668
  28. package/src/domains/chat/index.js +7 -7
  29. package/src/domains/chat/jobs.js +592 -592
  30. package/src/domains/chat/page-guard.js +98 -98
  31. package/src/domains/chat/roots.js +56 -56
  32. package/src/domains/chat/run-service.js +1977 -1977
  33. package/src/domains/recommend/actions.js +457 -457
  34. package/src/domains/recommend/cards.js +243 -243
  35. package/src/domains/recommend/constants.js +165 -165
  36. package/src/domains/recommend/filters.js +610 -610
  37. package/src/domains/recommend/index.js +10 -10
  38. package/src/domains/recommend/jobs.js +316 -316
  39. package/src/domains/recommend/refresh.js +472 -472
  40. package/src/domains/recommend/roots.js +80 -80
  41. package/src/domains/recommend/scopes.js +246 -246
  42. package/src/domains/recruit/actions.js +277 -277
  43. package/src/domains/recruit/cards.js +74 -74
  44. package/src/domains/recruit/constants.js +167 -167
  45. package/src/domains/recruit/detail.js +461 -461
  46. package/src/domains/recruit/index.js +9 -9
  47. package/src/domains/recruit/instruction-parser.js +451 -451
  48. package/src/domains/recruit/refresh.js +44 -44
  49. package/src/domains/recruit/roots.js +68 -68
  50. package/src/domains/recruit/run-service.js +1207 -1207
  51. package/src/domains/recruit/search.js +1202 -1202
  52. package/src/recommend-mcp.js +22 -22
  53. package/src/recruit-mcp.js +1338 -1338
@@ -1,973 +1,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
- 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
+ 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
+ }