@reconcrap/boss-recommend-mcp 1.3.38 → 2.0.0

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 (85) hide show
  1. package/README.md +53 -33
  2. package/package.json +61 -9
  3. package/skills/boss-recommend-pipeline/SKILL.md +4 -0
  4. package/src/chat-mcp.js +1333 -0
  5. package/src/chat-runtime-config.js +559 -0
  6. package/src/cli.js +1095 -196
  7. package/src/core/browser/index.js +378 -0
  8. package/src/core/capture/index.js +298 -0
  9. package/src/core/cv-acquisition/index.js +219 -0
  10. package/src/core/greet-quota/index.js +54 -0
  11. package/src/core/infinite-list/index.js +459 -0
  12. package/src/core/reporting/legacy-csv.js +332 -0
  13. package/src/core/run/index.js +286 -0
  14. package/src/core/screening/index.js +1166 -0
  15. package/src/core/self-heal/index.js +848 -0
  16. package/src/domains/chat/cards.js +129 -0
  17. package/src/domains/chat/constants.js +183 -0
  18. package/src/domains/chat/detail.js +1369 -0
  19. package/src/domains/chat/index.js +7 -0
  20. package/src/domains/chat/jobs.js +334 -0
  21. package/src/domains/chat/page-guard.js +88 -0
  22. package/src/domains/chat/roots.js +56 -0
  23. package/src/domains/chat/run-service.js +1101 -0
  24. package/src/domains/recommend/actions.js +457 -0
  25. package/src/domains/recommend/cards.js +228 -0
  26. package/src/domains/recommend/constants.js +141 -0
  27. package/src/domains/recommend/detail.js +341 -0
  28. package/src/domains/recommend/filters.js +581 -0
  29. package/src/domains/recommend/index.js +10 -0
  30. package/src/domains/recommend/jobs.js +232 -0
  31. package/src/domains/recommend/refresh.js +204 -0
  32. package/src/domains/recommend/roots.js +78 -0
  33. package/src/domains/recommend/run-service.js +903 -0
  34. package/src/domains/recommend/scopes.js +245 -0
  35. package/src/domains/recruit/actions.js +277 -0
  36. package/src/domains/recruit/cards.js +67 -0
  37. package/src/domains/recruit/constants.js +130 -0
  38. package/src/domains/recruit/detail.js +414 -0
  39. package/src/domains/recruit/index.js +9 -0
  40. package/src/domains/recruit/instruction-parser.js +451 -0
  41. package/src/domains/recruit/refresh.js +40 -0
  42. package/src/domains/recruit/roots.js +68 -0
  43. package/src/domains/recruit/run-service.js +580 -0
  44. package/src/domains/recruit/search.js +1149 -0
  45. package/src/index.js +578 -419
  46. package/src/recommend-mcp.js +1257 -0
  47. package/src/recruit-mcp.js +1035 -0
  48. package/src/adapters.js +0 -3079
  49. package/src/boss-chat.js +0 -1037
  50. package/src/pipeline.js +0 -2249
  51. package/src/recommend-healing-config.js +0 -131
  52. package/src/recommend-healing-rules.json +0 -261
  53. package/src/self-heal.js +0 -2237
  54. package/src/test-adapters-runtime.js +0 -628
  55. package/src/test-boss-chat.js +0 -3196
  56. package/src/test-index-async.js +0 -498
  57. package/src/test-parser.js +0 -742
  58. package/src/test-pipeline.js +0 -2703
  59. package/src/test-run-state.js +0 -152
  60. package/src/test-self-heal.js +0 -224
  61. package/vendor/boss-chat-cli/README.md +0 -134
  62. package/vendor/boss-chat-cli/package.json +0 -53
  63. package/vendor/boss-chat-cli/src/app.js +0 -1501
  64. package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
  65. package/vendor/boss-chat-cli/src/cli.js +0 -1713
  66. package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
  67. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
  68. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
  69. package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
  70. package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
  71. package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
  72. package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
  73. package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
  74. package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
  75. package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
  76. package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
  77. package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
  78. package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
  79. package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
  80. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -6927
  81. package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
  82. package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
  83. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2294
  84. package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
  85. 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
+ }