@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.22

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 -2
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/config/screening-config.example.json +33 -33
  4. package/package.json +8 -8
  5. package/scripts/install-macos.sh +280 -280
  6. package/scripts/postinstall.cjs +44 -44
  7. package/skills/boss-chat/README.md +42 -42
  8. package/skills/boss-chat/SKILL.md +106 -106
  9. package/skills/boss-recommend-pipeline/README.md +13 -13
  10. package/skills/boss-recommend-pipeline/SKILL.md +219 -214
  11. package/skills/boss-recruit-pipeline/README.md +19 -19
  12. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  13. package/src/chat-mcp.js +127 -127
  14. package/src/chat-runtime-config.js +775 -775
  15. package/src/cli.js +573 -573
  16. package/src/core/boss-cards/index.js +199 -199
  17. package/src/core/browser/index.js +2419 -2415
  18. package/src/core/capture/index.js +1201 -1201
  19. package/src/core/cv-acquisition/index.js +238 -238
  20. package/src/core/cv-capture-target/index.js +299 -299
  21. package/src/core/greet-quota/index.js +71 -71
  22. package/src/core/infinite-list/index.js +1326 -1326
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +32 -32
  25. package/src/core/run/timing.js +33 -33
  26. package/src/core/screening/index.js +2135 -2135
  27. package/src/core/self-heal/index.js +973 -973
  28. package/src/core/self-heal/viewport.js +564 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/cards.js +137 -137
  31. package/src/domains/chat/constants.js +9 -9
  32. package/src/domains/chat/detail.js +113 -113
  33. package/src/domains/chat/index.js +7 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +122 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +571 -571
  38. package/src/domains/common/account-rights-panel.js +314 -314
  39. package/src/domains/common/recovery-settle.js +159 -159
  40. package/src/domains/recommend/actions.js +472 -472
  41. package/src/domains/recommend/cards.js +243 -243
  42. package/src/domains/recommend/colleague-contact.js +333 -333
  43. package/src/domains/recommend/constants.js +228 -159
  44. package/src/domains/recommend/detail.js +650 -650
  45. package/src/domains/recommend/filters.js +748 -377
  46. package/src/domains/recommend/index.js +4 -3
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -0
  49. package/src/domains/recommend/refresh.js +504 -361
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +987 -854
  52. package/src/domains/recommend/scopes.js +246 -246
  53. package/src/domains/recruit/actions.js +277 -277
  54. package/src/domains/recruit/cards.js +74 -74
  55. package/src/domains/recruit/constants.js +236 -236
  56. package/src/domains/recruit/detail.js +588 -588
  57. package/src/domains/recruit/index.js +9 -9
  58. package/src/domains/recruit/instruction-parser.js +866 -866
  59. package/src/domains/recruit/refresh.js +45 -45
  60. package/src/domains/recruit/roots.js +68 -68
  61. package/src/domains/recruit/run-service.js +1620 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +13 -0
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +929 -915
  66. package/src/recommend-scheduler.js +496 -496
  67. package/src/recruit-mcp.js +2121 -2121
@@ -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
+ }