@reconcrap/boss-recommend-mcp 1.3.39 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +86 -33
  2. package/package.json +62 -9
  3. package/skills/boss-chat/SKILL.md +5 -4
  4. package/skills/boss-recommend-pipeline/SKILL.md +21 -31
  5. package/skills/boss-recruit-pipeline/README.md +17 -0
  6. package/skills/boss-recruit-pipeline/SKILL.md +55 -0
  7. package/src/chat-mcp.js +1333 -0
  8. package/src/chat-runtime-config.js +559 -0
  9. package/src/cli.js +1254 -225
  10. package/src/core/browser/index.js +378 -0
  11. package/src/core/capture/index.js +298 -0
  12. package/src/core/cv-acquisition/index.js +219 -0
  13. package/src/core/greet-quota/index.js +54 -0
  14. package/src/core/infinite-list/index.js +459 -0
  15. package/src/core/reporting/legacy-csv.js +332 -0
  16. package/src/core/run/index.js +286 -0
  17. package/src/core/screening/index.js +1166 -0
  18. package/src/core/self-heal/index.js +848 -0
  19. package/src/domains/chat/cards.js +129 -0
  20. package/src/domains/chat/constants.js +183 -0
  21. package/src/domains/chat/detail.js +1369 -0
  22. package/src/domains/chat/index.js +7 -0
  23. package/src/domains/chat/jobs.js +334 -0
  24. package/src/domains/chat/page-guard.js +88 -0
  25. package/src/domains/chat/roots.js +56 -0
  26. package/src/domains/chat/run-service.js +1101 -0
  27. package/src/domains/recommend/actions.js +457 -0
  28. package/src/domains/recommend/cards.js +228 -0
  29. package/src/domains/recommend/constants.js +141 -0
  30. package/src/domains/recommend/detail.js +341 -0
  31. package/src/domains/recommend/filters.js +581 -0
  32. package/src/domains/recommend/index.js +10 -0
  33. package/src/domains/recommend/jobs.js +232 -0
  34. package/src/domains/recommend/refresh.js +204 -0
  35. package/src/domains/recommend/roots.js +78 -0
  36. package/src/domains/recommend/run-service.js +903 -0
  37. package/src/domains/recommend/scopes.js +245 -0
  38. package/src/domains/recruit/actions.js +277 -0
  39. package/src/domains/recruit/cards.js +66 -0
  40. package/src/domains/recruit/constants.js +130 -0
  41. package/src/domains/recruit/detail.js +414 -0
  42. package/src/domains/recruit/index.js +9 -0
  43. package/src/domains/recruit/instruction-parser.js +451 -0
  44. package/src/domains/recruit/refresh.js +40 -0
  45. package/src/domains/recruit/roots.js +67 -0
  46. package/src/domains/recruit/run-service.js +580 -0
  47. package/src/domains/recruit/search.js +1149 -0
  48. package/src/index.js +578 -419
  49. package/src/recommend-mcp.js +1257 -0
  50. package/src/recruit-mcp.js +1035 -0
  51. package/src/adapters.js +0 -3079
  52. package/src/boss-chat.js +0 -1037
  53. package/src/pipeline.js +0 -2249
  54. package/src/recommend-healing-config.js +0 -131
  55. package/src/recommend-healing-rules.json +0 -261
  56. package/src/self-heal.js +0 -2237
  57. package/src/test-adapters-runtime.js +0 -628
  58. package/src/test-boss-chat.js +0 -3196
  59. package/src/test-index-async.js +0 -498
  60. package/src/test-parser.js +0 -742
  61. package/src/test-pipeline.js +0 -2703
  62. package/src/test-run-state.js +0 -152
  63. package/src/test-self-heal.js +0 -224
  64. package/vendor/boss-chat-cli/README.md +0 -134
  65. package/vendor/boss-chat-cli/package.json +0 -53
  66. package/vendor/boss-chat-cli/src/app.js +0 -1501
  67. package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
  68. package/vendor/boss-chat-cli/src/cli.js +0 -1713
  69. package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
  70. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
  71. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
  72. package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
  73. package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
  74. package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
  75. package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
  76. package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
  77. package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
  78. package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
  79. package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
  80. package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
  81. package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
  82. package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
  83. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -7072
  84. package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
  85. package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
  86. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2423
  87. package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
  88. package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
@@ -0,0 +1,848 @@
1
+ import {
2
+ findIframeDocument,
3
+ getAccessibilityTree,
4
+ getDocumentRoot,
5
+ querySelectorAll,
6
+ sleep
7
+ } from "../browser/index.js";
8
+
9
+ export const PROBE_STATUS = Object.freeze({
10
+ PASS: "pass",
11
+ FAIL: "fail",
12
+ BLOCKED: "blocked",
13
+ OPTIONAL_ABSENT: "optional_absent",
14
+ ERROR: "error"
15
+ });
16
+
17
+ export const HEALTH_STATUS = Object.freeze({
18
+ HEALTHY: "healthy",
19
+ DEGRADED: "degraded",
20
+ BLOCKED: "blocked"
21
+ });
22
+
23
+ export const DOMAIN_TARGET_HINTS = Object.freeze({
24
+ recommend: ["/web/chat/recommend"],
25
+ recruit: ["/web/chat/search"],
26
+ chat: ["/web/chat/index", "/web/chat"]
27
+ });
28
+
29
+ const FALLBACK_RECOMMEND_SELECTORS = Object.freeze({
30
+ top: {
31
+ recommend_iframe: [
32
+ 'iframe[name="recommendFrame"]',
33
+ 'iframe[src*="/web/frame/recommend/"]',
34
+ "iframe"
35
+ ]
36
+ },
37
+ frame: {
38
+ filter_trigger: [
39
+ ".filter-label-wrap",
40
+ ".recommend-filter.op-filter"
41
+ ],
42
+ filter_panel: [
43
+ ".recommend-filter.op-filter .filter-panel",
44
+ ".recommend-filter .filter-panel",
45
+ ".filter-panel"
46
+ ],
47
+ tab_items: [
48
+ "li.tab-item[data-status]",
49
+ 'li[data-status][class*="tab"]'
50
+ ],
51
+ candidate_cards: [
52
+ ".candidate-card-wrap .card-inner[data-geek]",
53
+ ".candidate-card-wrap [data-geek]",
54
+ "ul.card-list > li.card-item",
55
+ ".card-inner[data-geekid]",
56
+ "li.geek-info-card",
57
+ "a[data-geekid]",
58
+ ".candidate-card-wrap"
59
+ ]
60
+ },
61
+ detail: {
62
+ popup: [
63
+ ".dialog-wrap.active",
64
+ ".boss-popup__wrapper",
65
+ ".boss-popup_wrapper",
66
+ ".boss-dialog_wrapper",
67
+ ".boss-dialog",
68
+ ".resume-item-detail",
69
+ ".geek-detail-modal",
70
+ '[class*="popup"][class*="wrapper"]',
71
+ '[class*="dialog"][class*="wrapper"]'
72
+ ]
73
+ }
74
+ });
75
+
76
+ const FALLBACK_RECRUIT_SELECTORS = Object.freeze({
77
+ top: {
78
+ search_iframe: [
79
+ 'iframe[name="searchFrame"]',
80
+ 'iframe[src*="/web/frame/search/"]',
81
+ "iframe"
82
+ ]
83
+ },
84
+ frame: {
85
+ candidate_cards: [
86
+ "li.geek-info-card a[data-jid]",
87
+ "li.geek-info-card a[data-geekid]",
88
+ ".geek-info-card a[data-jid]",
89
+ ".geek-info-card a[data-geekid]",
90
+ ".geek-info-card a",
91
+ "a[data-jid]",
92
+ "a[data-geekid]"
93
+ ],
94
+ no_data: [
95
+ "i.tip-nodata",
96
+ ".tip-nodata",
97
+ ".empty-tip",
98
+ ".empty-text"
99
+ ]
100
+ },
101
+ detail: {
102
+ popup: [
103
+ ".dialog-wrap.active",
104
+ ".boss-popup__wrapper",
105
+ ".boss-popup_wrapper",
106
+ ".boss-dialog_wrapper",
107
+ ".boss-dialog",
108
+ ".resume-item-detail",
109
+ ".geek-detail-modal",
110
+ ".resume-container",
111
+ '[class*="popup"][class*="wrapper"]',
112
+ '[class*="dialog"][class*="wrapper"]'
113
+ ]
114
+ }
115
+ });
116
+
117
+ const FALLBACK_CHAT_SELECTORS = Object.freeze({
118
+ top: {
119
+ candidate_cards: [
120
+ ".geek-item[data-id]",
121
+ 'div[role="listitem"] .geek-item[data-id]',
122
+ ".geek-item",
123
+ ".geek-item-wrap",
124
+ 'div[role="listitem"]'
125
+ ],
126
+ selected_candidate: [
127
+ ".geek-item.selected[data-id]",
128
+ ".geek-item.selected",
129
+ ".geek-item.active[data-id]",
130
+ ".geek-item.active"
131
+ ],
132
+ online_resume_button: [
133
+ "a.btn.resume-btn-online",
134
+ "a.resume-btn-online",
135
+ ".btn.resume-btn-online",
136
+ ".resume-btn-online"
137
+ ]
138
+ },
139
+ detail: {
140
+ resume_modal: [
141
+ ".boss-popup__wrapper",
142
+ ".new-chat-resume-dialog-main-ui",
143
+ ".dialog-wrap.active",
144
+ ".boss-dialog",
145
+ ".geek-detail-modal",
146
+ ".modal",
147
+ ".resume-container",
148
+ ".resume-content-wrap",
149
+ ".resume-common-wrap",
150
+ ".resume-detail",
151
+ ".resume-recommend"
152
+ ],
153
+ resume_iframe: [
154
+ 'iframe[src*="/web/frame/c-resume/"]',
155
+ 'iframe[src*="resume"]',
156
+ 'iframe[name*="resume"]'
157
+ ]
158
+ }
159
+ });
160
+
161
+ function uniqueStrings(values = []) {
162
+ return [...new Set(values.filter((value) => typeof value === "string" && value.trim()))];
163
+ }
164
+
165
+ function selectorGroup(rules, scope, name, fallback = []) {
166
+ return uniqueStrings(rules?.selectors?.[scope]?.[name] || fallback);
167
+ }
168
+
169
+ function mergeSelectorGroups(rules, scope, names = [], fallback = []) {
170
+ const selectors = names.flatMap((name) => rules?.selectors?.[scope]?.[name] || []);
171
+ return uniqueStrings(selectors.length ? selectors : fallback);
172
+ }
173
+
174
+ function rootNodeId(roots = {}, name) {
175
+ const root = roots[name];
176
+ if (typeof root === "number") return root;
177
+ if (root?.nodeId) return root.nodeId;
178
+ if (root?.documentNodeId) return root.documentNodeId;
179
+ return 0;
180
+ }
181
+
182
+ function stringMatchesAnyPattern(value, patterns = []) {
183
+ const text = String(value || "");
184
+ return patterns.some((pattern) => {
185
+ if (pattern instanceof RegExp) return pattern.test(text);
186
+ return text.includes(String(pattern || ""));
187
+ });
188
+ }
189
+
190
+ export function createSelectorProbe({
191
+ id,
192
+ root = "frame",
193
+ selectors = [],
194
+ required = false,
195
+ minCount = 1,
196
+ description = ""
197
+ } = {}) {
198
+ if (!id) throw new Error("Selector probe requires an id");
199
+ return {
200
+ type: "selector",
201
+ id,
202
+ root,
203
+ selectors: uniqueStrings(selectors),
204
+ required: Boolean(required),
205
+ minCount,
206
+ description
207
+ };
208
+ }
209
+
210
+ export function createAccessibilityProbe({
211
+ id,
212
+ required = false,
213
+ minCount = 1,
214
+ roleIncludes = [],
215
+ nameIncludes = [],
216
+ description = ""
217
+ } = {}) {
218
+ if (!id) throw new Error("Accessibility probe requires an id");
219
+ return {
220
+ type: "accessibility",
221
+ id,
222
+ required: Boolean(required),
223
+ minCount,
224
+ roleIncludes: uniqueStrings(roleIncludes),
225
+ nameIncludes: uniqueStrings(nameIncludes),
226
+ description
227
+ };
228
+ }
229
+
230
+ export function createNetworkProbe({
231
+ id,
232
+ required = false,
233
+ minCount = 1,
234
+ urlPatterns = [],
235
+ description = ""
236
+ } = {}) {
237
+ if (!id) throw new Error("Network probe requires an id");
238
+ return {
239
+ type: "network",
240
+ id,
241
+ required: Boolean(required),
242
+ minCount,
243
+ urlPatterns,
244
+ description
245
+ };
246
+ }
247
+
248
+ export async function runSelectorProbe(client, roots, probe) {
249
+ const nodeId = rootNodeId(roots, probe.root);
250
+ if (!nodeId) {
251
+ return {
252
+ ...probe,
253
+ ok: !probe.required,
254
+ status: probe.required ? PROBE_STATUS.BLOCKED : PROBE_STATUS.OPTIONAL_ABSENT,
255
+ count: 0,
256
+ selector_counts: [],
257
+ error: `Root not found: ${probe.root}`
258
+ };
259
+ }
260
+
261
+ const selectorCounts = [];
262
+ try {
263
+ for (const selector of probe.selectors) {
264
+ const nodeIds = await querySelectorAll(client, nodeId, selector);
265
+ selectorCounts.push({
266
+ selector,
267
+ count: nodeIds.length
268
+ });
269
+ }
270
+ } catch (error) {
271
+ return {
272
+ ...probe,
273
+ ok: !probe.required,
274
+ status: probe.required ? PROBE_STATUS.ERROR : PROBE_STATUS.OPTIONAL_ABSENT,
275
+ count: 0,
276
+ selector_counts: selectorCounts,
277
+ error: error?.message || String(error)
278
+ };
279
+ }
280
+
281
+ const count = selectorCounts.reduce((max, item) => Math.max(max, item.count), 0);
282
+ const ok = count >= probe.minCount;
283
+ return {
284
+ ...probe,
285
+ ok: probe.required ? ok : true,
286
+ status: ok ? PROBE_STATUS.PASS : probe.required ? PROBE_STATUS.FAIL : PROBE_STATUS.OPTIONAL_ABSENT,
287
+ count,
288
+ selector_counts: selectorCounts,
289
+ matched_selectors: selectorCounts.filter((item) => item.count > 0)
290
+ };
291
+ }
292
+
293
+ export async function runAccessibilityProbe(client, probe) {
294
+ try {
295
+ const tree = await getAccessibilityTree(client);
296
+ const nodes = tree?.nodes || [];
297
+ const matches = nodes.filter((node) => {
298
+ const role = String(node?.role?.value || "");
299
+ const name = String(node?.name?.value || "");
300
+ const roleOk = probe.roleIncludes.length === 0
301
+ || probe.roleIncludes.some((value) => role.includes(value));
302
+ const nameOk = probe.nameIncludes.length === 0
303
+ || probe.nameIncludes.some((value) => name.includes(value));
304
+ return roleOk && nameOk;
305
+ });
306
+ const ok = matches.length >= probe.minCount;
307
+ return {
308
+ ...probe,
309
+ ok: probe.required ? ok : true,
310
+ status: ok ? PROBE_STATUS.PASS : probe.required ? PROBE_STATUS.FAIL : PROBE_STATUS.OPTIONAL_ABSENT,
311
+ count: matches.length,
312
+ total_ax_nodes: nodes.length
313
+ };
314
+ } catch (error) {
315
+ return {
316
+ ...probe,
317
+ ok: !probe.required,
318
+ status: probe.required ? PROBE_STATUS.ERROR : PROBE_STATUS.OPTIONAL_ABSENT,
319
+ count: 0,
320
+ total_ax_nodes: 0,
321
+ error: error?.message || String(error)
322
+ };
323
+ }
324
+ }
325
+
326
+ export function runNetworkProbe(networkEvents = [], probe) {
327
+ const matches = networkEvents.filter((event) => {
328
+ if (!probe.urlPatterns.length) return true;
329
+ return stringMatchesAnyPattern(event?.url || event?.response?.url, probe.urlPatterns);
330
+ });
331
+ const ok = matches.length >= probe.minCount;
332
+ return {
333
+ ...probe,
334
+ ok: probe.required ? ok : true,
335
+ status: ok ? PROBE_STATUS.PASS : probe.required ? PROBE_STATUS.FAIL : PROBE_STATUS.OPTIONAL_ABSENT,
336
+ count: matches.length,
337
+ sample_urls: matches.slice(0, 5).map((event) => event?.url || event?.response?.url || "")
338
+ };
339
+ }
340
+
341
+ export function summarizeProbeResults(probes = []) {
342
+ const required = probes.filter((probe) => probe.required);
343
+ const blocked = required.filter((probe) => probe.status === PROBE_STATUS.BLOCKED);
344
+ const failed = required.filter((probe) => !probe.ok && probe.status !== PROBE_STATUS.BLOCKED);
345
+ const optionalAbsent = probes.filter((probe) => probe.status === PROBE_STATUS.OPTIONAL_ABSENT);
346
+ const passed = probes.filter((probe) => probe.status === PROBE_STATUS.PASS);
347
+
348
+ return {
349
+ status: blocked.length
350
+ ? HEALTH_STATUS.BLOCKED
351
+ : failed.length
352
+ ? HEALTH_STATUS.DEGRADED
353
+ : HEALTH_STATUS.HEALTHY,
354
+ required_count: required.length,
355
+ passed_count: passed.length,
356
+ failed_required_ids: failed.map((probe) => probe.id),
357
+ blocked_required_ids: blocked.map((probe) => probe.id),
358
+ optional_absent_ids: optionalAbsent.map((probe) => probe.id)
359
+ };
360
+ }
361
+
362
+ export function buildDriftReport(probes = []) {
363
+ return probes
364
+ .filter((probe) => probe.required && !probe.ok)
365
+ .map((probe) => ({
366
+ probe_id: probe.id,
367
+ probe_type: probe.type,
368
+ status: probe.status,
369
+ root: probe.root || null,
370
+ expected_min_count: probe.minCount,
371
+ observed_count: probe.count || 0,
372
+ selectors: probe.selectors || [],
373
+ error: probe.error || null
374
+ }));
375
+ }
376
+
377
+ export async function runSelfHealCheck({
378
+ client,
379
+ domain,
380
+ roots = {},
381
+ selectorProbes = [],
382
+ accessibilityProbes = [],
383
+ networkProbes = [],
384
+ networkEvents = []
385
+ } = {}) {
386
+ const selectorResults = [];
387
+ for (const probe of selectorProbes) {
388
+ selectorResults.push(await runSelectorProbe(client, roots, probe));
389
+ }
390
+
391
+ const accessibilityResults = [];
392
+ for (const probe of accessibilityProbes) {
393
+ accessibilityResults.push(await runAccessibilityProbe(client, probe));
394
+ }
395
+
396
+ const networkResults = networkProbes.map((probe) => runNetworkProbe(networkEvents, probe));
397
+ const probes = [...selectorResults, ...accessibilityResults, ...networkResults];
398
+ const summary = summarizeProbeResults(probes);
399
+
400
+ return {
401
+ domain,
402
+ status: summary.status,
403
+ summary,
404
+ probes,
405
+ drift_report: buildDriftReport(probes)
406
+ };
407
+ }
408
+
409
+ export function buildRecommendSelfHealConfig(rules = {}) {
410
+ const iframeSelectors = selectorGroup(
411
+ rules,
412
+ "top",
413
+ "recommend_iframe",
414
+ FALLBACK_RECOMMEND_SELECTORS.top.recommend_iframe
415
+ );
416
+ const cardSelectors = mergeSelectorGroups(
417
+ rules,
418
+ "frame",
419
+ [
420
+ "latest_card_inner",
421
+ "recommend_card_inner",
422
+ "featured_card_anchor",
423
+ "recommend_cards",
424
+ "featured_cards",
425
+ "latest_cards"
426
+ ],
427
+ FALLBACK_RECOMMEND_SELECTORS.frame.candidate_cards
428
+ );
429
+
430
+ return {
431
+ domain: "recommend",
432
+ targetHints: DOMAIN_TARGET_HINTS.recommend,
433
+ iframeSelectors,
434
+ selectorProbes: [
435
+ createSelectorProbe({
436
+ id: "recommend_iframe",
437
+ root: "top",
438
+ selectors: iframeSelectors,
439
+ required: true,
440
+ description: "Recommend iframe can be discovered from the top document"
441
+ }),
442
+ createSelectorProbe({
443
+ id: "filter_trigger",
444
+ root: "frame",
445
+ selectors: selectorGroup(
446
+ rules,
447
+ "frame",
448
+ "filter_trigger",
449
+ FALLBACK_RECOMMEND_SELECTORS.frame.filter_trigger
450
+ ),
451
+ required: true,
452
+ description: "Filter trigger is mounted in the recommend frame"
453
+ }),
454
+ createSelectorProbe({
455
+ id: "candidate_cards",
456
+ root: "frame",
457
+ selectors: cardSelectors,
458
+ required: true,
459
+ description: "At least one recommend candidate card is visible"
460
+ }),
461
+ createSelectorProbe({
462
+ id: "tab_items",
463
+ root: "frame",
464
+ selectors: selectorGroup(
465
+ rules,
466
+ "frame",
467
+ "tab_items",
468
+ FALLBACK_RECOMMEND_SELECTORS.frame.tab_items
469
+ ),
470
+ required: false,
471
+ description: "Recommend tab controls are mounted when this layout exposes them"
472
+ }),
473
+ createSelectorProbe({
474
+ id: "filter_panel",
475
+ root: "frame",
476
+ selectors: selectorGroup(
477
+ rules,
478
+ "frame",
479
+ "filter_panel",
480
+ FALLBACK_RECOMMEND_SELECTORS.frame.filter_panel
481
+ ),
482
+ required: false,
483
+ description: "Filter panel is optional because it is absent until opened"
484
+ }),
485
+ createSelectorProbe({
486
+ id: "detail_popup_top",
487
+ root: "top",
488
+ selectors: selectorGroup(
489
+ rules,
490
+ "detail",
491
+ "popup",
492
+ FALLBACK_RECOMMEND_SELECTORS.detail.popup
493
+ ),
494
+ required: false,
495
+ description: "Candidate detail popup may be absent during idle health checks"
496
+ }),
497
+ createSelectorProbe({
498
+ id: "detail_popup_frame",
499
+ root: "frame",
500
+ selectors: selectorGroup(
501
+ rules,
502
+ "detail",
503
+ "popup",
504
+ FALLBACK_RECOMMEND_SELECTORS.detail.popup
505
+ ),
506
+ required: false,
507
+ description: "Candidate detail popup may mount inside the recommend frame"
508
+ })
509
+ ],
510
+ accessibilityProbes: [
511
+ createAccessibilityProbe({
512
+ id: "accessibility_tree",
513
+ required: true,
514
+ minCount: 1,
515
+ description: "Accessibility tree is readable without page script"
516
+ })
517
+ ],
518
+ networkProbes: [
519
+ createNetworkProbe({
520
+ id: "zhipin_network_after_refresh",
521
+ required: true,
522
+ minCount: 1,
523
+ urlPatterns: ["zhipin.com"],
524
+ description: "A controlled refresh produced observable Boss network traffic"
525
+ })
526
+ ],
527
+ repairActions: [
528
+ {
529
+ id: "page_reload",
530
+ type: "page_reload",
531
+ ignoreCache: false,
532
+ waitMs: 2500,
533
+ description: "Refresh the current page through Page.reload"
534
+ }
535
+ ]
536
+ };
537
+ }
538
+
539
+ export function buildRecruitSelfHealConfig(rules = {}) {
540
+ const iframeSelectors = selectorGroup(
541
+ rules,
542
+ "top",
543
+ "search_iframe",
544
+ FALLBACK_RECRUIT_SELECTORS.top.search_iframe
545
+ );
546
+ const cardSelectors = mergeSelectorGroups(
547
+ rules,
548
+ "frame",
549
+ [
550
+ "search_candidate_cards",
551
+ "recruit_candidate_cards",
552
+ "candidate_cards"
553
+ ],
554
+ FALLBACK_RECRUIT_SELECTORS.frame.candidate_cards
555
+ );
556
+
557
+ return {
558
+ domain: "recruit",
559
+ targetHints: DOMAIN_TARGET_HINTS.recruit,
560
+ iframeSelectors,
561
+ selectorProbes: [
562
+ createSelectorProbe({
563
+ id: "search_iframe",
564
+ root: "top",
565
+ selectors: iframeSelectors,
566
+ required: true,
567
+ description: "Search iframe can be discovered from the top document"
568
+ }),
569
+ createSelectorProbe({
570
+ id: "candidate_cards",
571
+ root: "frame",
572
+ selectors: cardSelectors,
573
+ required: true,
574
+ description: "At least one search candidate card is visible"
575
+ }),
576
+ createSelectorProbe({
577
+ id: "no_data_tip",
578
+ root: "frame",
579
+ selectors: selectorGroup(
580
+ rules,
581
+ "frame",
582
+ "no_data",
583
+ FALLBACK_RECRUIT_SELECTORS.frame.no_data
584
+ ),
585
+ required: false,
586
+ description: "Search no-data state is optional and blocks candidate extraction if present"
587
+ }),
588
+ createSelectorProbe({
589
+ id: "detail_popup_top",
590
+ root: "top",
591
+ selectors: selectorGroup(
592
+ rules,
593
+ "detail",
594
+ "popup",
595
+ FALLBACK_RECRUIT_SELECTORS.detail.popup
596
+ ),
597
+ required: false,
598
+ description: "Candidate detail popup may be absent during idle health checks"
599
+ }),
600
+ createSelectorProbe({
601
+ id: "detail_popup_frame",
602
+ root: "frame",
603
+ selectors: selectorGroup(
604
+ rules,
605
+ "detail",
606
+ "popup",
607
+ FALLBACK_RECRUIT_SELECTORS.detail.popup
608
+ ),
609
+ required: false,
610
+ description: "Candidate detail popup may mount inside the search frame"
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 search page through Page.reload"
637
+ }
638
+ ]
639
+ };
640
+ }
641
+
642
+ export function buildChatSelfHealConfig(rules = {}) {
643
+ const cardSelectors = mergeSelectorGroups(
644
+ rules,
645
+ "top",
646
+ [
647
+ "chat_candidate_cards",
648
+ "candidate_cards",
649
+ "conversation_cards"
650
+ ],
651
+ FALLBACK_CHAT_SELECTORS.top.candidate_cards
652
+ );
653
+
654
+ return {
655
+ domain: "chat",
656
+ targetHints: DOMAIN_TARGET_HINTS.chat,
657
+ selectorProbes: [
658
+ createSelectorProbe({
659
+ id: "candidate_cards",
660
+ root: "top",
661
+ selectors: cardSelectors,
662
+ required: true,
663
+ description: "At least one chat conversation candidate is visible"
664
+ }),
665
+ createSelectorProbe({
666
+ id: "selected_candidate",
667
+ root: "top",
668
+ selectors: selectorGroup(
669
+ rules,
670
+ "top",
671
+ "selected_candidate",
672
+ FALLBACK_CHAT_SELECTORS.top.selected_candidate
673
+ ),
674
+ required: false,
675
+ description: "A selected chat candidate is optional before extraction starts"
676
+ }),
677
+ createSelectorProbe({
678
+ id: "online_resume_button",
679
+ root: "top",
680
+ selectors: selectorGroup(
681
+ rules,
682
+ "top",
683
+ "online_resume_button",
684
+ FALLBACK_CHAT_SELECTORS.top.online_resume_button
685
+ ),
686
+ required: false,
687
+ description: "Online resume button appears after a candidate conversation is selected"
688
+ }),
689
+ createSelectorProbe({
690
+ id: "resume_modal",
691
+ root: "top",
692
+ selectors: selectorGroup(
693
+ rules,
694
+ "detail",
695
+ "resume_modal",
696
+ FALLBACK_CHAT_SELECTORS.detail.resume_modal
697
+ ),
698
+ required: false,
699
+ description: "Resume modal is optional during idle chat health checks"
700
+ }),
701
+ createSelectorProbe({
702
+ id: "resume_iframe",
703
+ root: "top",
704
+ selectors: selectorGroup(
705
+ rules,
706
+ "detail",
707
+ "resume_iframe",
708
+ FALLBACK_CHAT_SELECTORS.detail.resume_iframe
709
+ ),
710
+ required: false,
711
+ description: "Resume iframe appears after the online resume is opened"
712
+ })
713
+ ],
714
+ accessibilityProbes: [
715
+ createAccessibilityProbe({
716
+ id: "accessibility_tree",
717
+ required: true,
718
+ minCount: 1,
719
+ description: "Accessibility tree is readable without page script"
720
+ })
721
+ ],
722
+ networkProbes: [
723
+ createNetworkProbe({
724
+ id: "zhipin_network_after_refresh",
725
+ required: true,
726
+ minCount: 1,
727
+ urlPatterns: ["zhipin.com"],
728
+ description: "A controlled refresh produced observable Boss network traffic"
729
+ })
730
+ ],
731
+ repairActions: [
732
+ {
733
+ id: "page_reload",
734
+ type: "page_reload",
735
+ ignoreCache: false,
736
+ waitMs: 2500,
737
+ description: "Refresh the current chat page through Page.reload"
738
+ }
739
+ ]
740
+ };
741
+ }
742
+
743
+ export async function resolveRecommendSelfHealRoots(client, config = buildRecommendSelfHealConfig()) {
744
+ const topRoot = await getDocumentRoot(client);
745
+ const iframe = await findIframeDocument(client, topRoot.nodeId, config.iframeSelectors);
746
+ if (!iframe) {
747
+ return {
748
+ roots: {
749
+ top: topRoot.nodeId
750
+ },
751
+ topRoot,
752
+ iframe: null
753
+ };
754
+ }
755
+
756
+ return {
757
+ roots: {
758
+ top: topRoot.nodeId,
759
+ frame: iframe.documentNodeId
760
+ },
761
+ topRoot,
762
+ iframe
763
+ };
764
+ }
765
+
766
+ export async function resolveRecruitSelfHealRoots(client, config = buildRecruitSelfHealConfig()) {
767
+ const topRoot = await getDocumentRoot(client);
768
+ const iframe = await findIframeDocument(client, topRoot.nodeId, config.iframeSelectors);
769
+ if (!iframe) {
770
+ return {
771
+ roots: {
772
+ top: topRoot.nodeId
773
+ },
774
+ topRoot,
775
+ iframe: null
776
+ };
777
+ }
778
+
779
+ return {
780
+ roots: {
781
+ top: topRoot.nodeId,
782
+ frame: iframe.documentNodeId
783
+ },
784
+ topRoot,
785
+ iframe
786
+ };
787
+ }
788
+
789
+ export async function resolveChatSelfHealRoots(client, _config = buildChatSelfHealConfig()) {
790
+ const topRoot = await getDocumentRoot(client);
791
+ return {
792
+ roots: {
793
+ top: topRoot.nodeId
794
+ },
795
+ topRoot,
796
+ iframe: null
797
+ };
798
+ }
799
+
800
+ export async function runRepairAction(client, action = {}) {
801
+ if (action.type === "page_reload") {
802
+ await client.Page.reload({ ignoreCache: Boolean(action.ignoreCache) });
803
+ if (action.waitMs > 0) await sleep(action.waitMs);
804
+ return {
805
+ id: action.id || action.type,
806
+ type: action.type,
807
+ ok: true
808
+ };
809
+ }
810
+
811
+ return {
812
+ id: action.id || action.type || "unknown",
813
+ type: action.type || "unknown",
814
+ ok: false,
815
+ error: `Unsupported repair action: ${action.type || "unknown"}`
816
+ };
817
+ }
818
+
819
+ export function classifyBossTargets(targets = []) {
820
+ const pageTargets = targets.filter((target) => target?.type === "page");
821
+ const byDomain = {};
822
+ for (const [domain, hints] of Object.entries(DOMAIN_TARGET_HINTS)) {
823
+ const target = pageTargets.find((item) => {
824
+ const url = String(item?.url || "");
825
+ if (domain === "chat") {
826
+ return hints.some((hint) => url.includes(hint))
827
+ && !url.includes("/web/chat/recommend")
828
+ && !url.includes("/web/chat/search");
829
+ }
830
+ return hints.some((hint) => url.includes(hint));
831
+ });
832
+ byDomain[domain] = target
833
+ ? {
834
+ status: "available",
835
+ target: {
836
+ id: target.id,
837
+ type: target.type,
838
+ url: target.url,
839
+ title: target.title
840
+ }
841
+ }
842
+ : {
843
+ status: "blocked",
844
+ reason: `No live ${domain} target is open in Chrome`
845
+ };
846
+ }
847
+ return byDomain;
848
+ }