@reconcrap/boss-recommend-mcp 1.3.39 → 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.
- package/README.md +53 -33
- package/package.json +61 -9
- package/skills/boss-recommend-pipeline/SKILL.md +4 -0
- package/src/chat-mcp.js +1333 -0
- package/src/chat-runtime-config.js +559 -0
- package/src/cli.js +1095 -196
- package/src/core/browser/index.js +378 -0
- package/src/core/capture/index.js +298 -0
- package/src/core/cv-acquisition/index.js +219 -0
- package/src/core/greet-quota/index.js +54 -0
- package/src/core/infinite-list/index.js +459 -0
- package/src/core/reporting/legacy-csv.js +332 -0
- package/src/core/run/index.js +286 -0
- package/src/core/screening/index.js +1166 -0
- package/src/core/self-heal/index.js +848 -0
- package/src/domains/chat/cards.js +129 -0
- package/src/domains/chat/constants.js +183 -0
- package/src/domains/chat/detail.js +1369 -0
- package/src/domains/chat/index.js +7 -0
- package/src/domains/chat/jobs.js +334 -0
- package/src/domains/chat/page-guard.js +88 -0
- package/src/domains/chat/roots.js +56 -0
- package/src/domains/chat/run-service.js +1101 -0
- package/src/domains/recommend/actions.js +457 -0
- package/src/domains/recommend/cards.js +228 -0
- package/src/domains/recommend/constants.js +141 -0
- package/src/domains/recommend/detail.js +341 -0
- package/src/domains/recommend/filters.js +581 -0
- package/src/domains/recommend/index.js +10 -0
- package/src/domains/recommend/jobs.js +232 -0
- package/src/domains/recommend/refresh.js +204 -0
- package/src/domains/recommend/roots.js +78 -0
- package/src/domains/recommend/run-service.js +903 -0
- package/src/domains/recommend/scopes.js +245 -0
- package/src/domains/recruit/actions.js +277 -0
- package/src/domains/recruit/cards.js +67 -0
- package/src/domains/recruit/constants.js +130 -0
- package/src/domains/recruit/detail.js +414 -0
- package/src/domains/recruit/index.js +9 -0
- package/src/domains/recruit/instruction-parser.js +451 -0
- package/src/domains/recruit/refresh.js +40 -0
- package/src/domains/recruit/roots.js +68 -0
- package/src/domains/recruit/run-service.js +580 -0
- package/src/domains/recruit/search.js +1149 -0
- package/src/index.js +578 -419
- package/src/recommend-mcp.js +1257 -0
- package/src/recruit-mcp.js +1035 -0
- package/src/adapters.js +0 -3079
- package/src/boss-chat.js +0 -1037
- package/src/pipeline.js +0 -2249
- package/src/recommend-healing-config.js +0 -131
- package/src/recommend-healing-rules.json +0 -261
- package/src/self-heal.js +0 -2237
- package/src/test-adapters-runtime.js +0 -628
- package/src/test-boss-chat.js +0 -3196
- package/src/test-index-async.js +0 -498
- package/src/test-parser.js +0 -742
- package/src/test-pipeline.js +0 -2703
- package/src/test-run-state.js +0 -152
- package/src/test-self-heal.js +0 -224
- package/vendor/boss-chat-cli/README.md +0 -134
- package/vendor/boss-chat-cli/package.json +0 -53
- package/vendor/boss-chat-cli/src/app.js +0 -1501
- package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
- package/vendor/boss-chat-cli/src/cli.js +0 -1713
- package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
- package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
- package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
- package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
- package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
- package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
- package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
- package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
- package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
- package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
- package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
- package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
- package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
- package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -7072
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
- package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2423
- package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
- 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
|
+
}
|