@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,7 @@
1
+ export * from "./constants.js";
2
+ export * from "./roots.js";
3
+ export * from "./cards.js";
4
+ export * from "./jobs.js";
5
+ export * from "./page-guard.js";
6
+ export * from "./detail.js";
7
+ export * from "./run-service.js";
@@ -0,0 +1,334 @@
1
+ import {
2
+ clickNodeCenter,
3
+ clickPoint,
4
+ getAttributesMap,
5
+ getNodeBox,
6
+ getOuterHTML,
7
+ querySelector,
8
+ querySelectorAll,
9
+ sleep
10
+ } from "../../core/browser/index.js";
11
+ import {
12
+ htmlToText,
13
+ normalizeText
14
+ } from "../../core/screening/index.js";
15
+ import {
16
+ CHAT_JOB_FALLBACK_SELECTORS,
17
+ CHAT_JOB_LABEL_SELECTORS,
18
+ CHAT_JOB_OPTION_SELECTORS,
19
+ CHAT_JOB_TRIGGER_SELECTORS
20
+ } from "./constants.js";
21
+ import { getChatRoots } from "./roots.js";
22
+
23
+ function isActiveClass(className = "") {
24
+ return /\b(active|selected|current)\b/i.test(String(className || ""));
25
+ }
26
+
27
+ function normalizeJobText(value) {
28
+ return normalizeText(value).replace(/\s+_/g, " _").replace(/_\s+/g, "_ ");
29
+ }
30
+
31
+ async function freshTopRootNodeId(client, fallbackNodeId) {
32
+ try {
33
+ const rootState = await getChatRoots(client);
34
+ return rootState.rootNodes.top || fallbackNodeId;
35
+ } catch {
36
+ return fallbackNodeId;
37
+ }
38
+ }
39
+
40
+ async function safeQuerySelector(client, rootNodeId, selector) {
41
+ try {
42
+ return await querySelector(client, rootNodeId, selector);
43
+ } catch {
44
+ return 0;
45
+ }
46
+ }
47
+
48
+ async function safeQuerySelectorAll(client, rootNodeId, selector) {
49
+ try {
50
+ return await querySelectorAll(client, rootNodeId, selector);
51
+ } catch {
52
+ return [];
53
+ }
54
+ }
55
+
56
+ async function readNodeText(client, nodeId) {
57
+ const outerHTML = await getOuterHTML(client, nodeId);
58
+ return {
59
+ outerHTML,
60
+ text: normalizeJobText(htmlToText(outerHTML))
61
+ };
62
+ }
63
+
64
+ async function readSelectedJobLabel(client, rootNodeId) {
65
+ for (const selector of CHAT_JOB_LABEL_SELECTORS) {
66
+ const nodeId = await safeQuerySelector(client, rootNodeId, selector);
67
+ if (!nodeId) continue;
68
+ try {
69
+ const { text } = await readNodeText(client, nodeId);
70
+ if (text) return { selector, label: text };
71
+ } catch {
72
+ continue;
73
+ }
74
+ }
75
+ return { selector: "", label: "" };
76
+ }
77
+
78
+ async function readOptionNode(client, nodeId, index, { selector, source }) {
79
+ const [attributes, textResult] = await Promise.all([
80
+ getAttributesMap(client, nodeId),
81
+ readNodeText(client, nodeId)
82
+ ]);
83
+ const label = normalizeJobText(attributes.title || textResult.text);
84
+ if (!label) return null;
85
+ const rawValue = normalizeText(attributes.value || attributes["data-value"] || attributes["data-id"] || "");
86
+ return {
87
+ node_id: nodeId,
88
+ index,
89
+ label,
90
+ title: label,
91
+ value: rawValue || label,
92
+ active: isActiveClass(attributes.class),
93
+ is_all: rawValue === "-1" || /^(全部职位|全部岗位|全部)$/u.test(label),
94
+ source,
95
+ selector
96
+ };
97
+ }
98
+
99
+ async function readClickableOptionNode(client, nodeId, index, { selector, source }) {
100
+ const option = await readOptionNode(client, nodeId, index, { selector, source });
101
+ if (!option) return null;
102
+ try {
103
+ const box = await getNodeBox(client, nodeId);
104
+ option.center = box.center;
105
+ option.rect = box.rect;
106
+ option.visible = box.rect.width > 2 && box.rect.height > 2;
107
+ } catch {
108
+ option.center = null;
109
+ option.rect = null;
110
+ option.visible = false;
111
+ }
112
+ return option;
113
+ }
114
+
115
+ async function readOptionsForSelector(client, rootNodeId, selector, { source }) {
116
+ const nodeIds = await safeQuerySelectorAll(client, rootNodeId, selector);
117
+ const options = [];
118
+ for (const nodeId of nodeIds) {
119
+ let option = null;
120
+ try {
121
+ option = await readClickableOptionNode(client, nodeId, options.length + 1, {
122
+ selector,
123
+ source
124
+ });
125
+ } catch {
126
+ option = null;
127
+ }
128
+ if (option) options.push(option);
129
+ }
130
+ return options;
131
+ }
132
+
133
+ function dedupeJobOptions(options = []) {
134
+ const seen = new Set();
135
+ const deduped = [];
136
+ for (const option of options) {
137
+ const key = `${normalizeText(option.value).toLowerCase()}|${normalizeText(option.label).toLowerCase()}`;
138
+ if (seen.has(key)) continue;
139
+ seen.add(key);
140
+ deduped.push({
141
+ ...option,
142
+ index: deduped.length + 1
143
+ });
144
+ }
145
+ return deduped;
146
+ }
147
+
148
+ export async function readChatJobOptions(client, rootNodeId, {
149
+ timeoutMs = 12000,
150
+ intervalMs = 300
151
+ } = {}) {
152
+ const started = Date.now();
153
+ let selected = { selector: "", label: "" };
154
+ let lastPrimary = {
155
+ selector: "",
156
+ source: "chat-job-list",
157
+ options: []
158
+ };
159
+
160
+ while (Date.now() - started <= timeoutMs) {
161
+ selected = await readSelectedJobLabel(client, rootNodeId);
162
+ for (const selector of CHAT_JOB_OPTION_SELECTORS) {
163
+ const options = await readOptionsForSelector(client, rootNodeId, selector, {
164
+ source: "chat-job-list"
165
+ });
166
+ if (options.length) {
167
+ lastPrimary = {
168
+ selector,
169
+ source: "chat-job-list",
170
+ options: dedupeJobOptions(options)
171
+ };
172
+ return {
173
+ selector,
174
+ source: "chat-job-list",
175
+ selected_label: selected.label || "",
176
+ selected_selector: selected.selector || "",
177
+ job_options: lastPrimary.options
178
+ };
179
+ }
180
+ }
181
+ await sleep(intervalMs);
182
+ }
183
+
184
+ const fallbackOptions = [];
185
+ for (const selector of CHAT_JOB_FALLBACK_SELECTORS) {
186
+ const options = await readOptionsForSelector(client, rootNodeId, selector, {
187
+ source: "conversation-source-job"
188
+ });
189
+ fallbackOptions.push(...options);
190
+ }
191
+
192
+ const dedupedFallback = dedupeJobOptions(fallbackOptions);
193
+ if (dedupedFallback.length) {
194
+ return {
195
+ selector: CHAT_JOB_FALLBACK_SELECTORS.join(", "),
196
+ source: "conversation-source-job",
197
+ selected_label: selected.label || "",
198
+ selected_selector: selected.selector || "",
199
+ job_options: dedupedFallback
200
+ };
201
+ }
202
+
203
+ return {
204
+ selector: lastPrimary.selector,
205
+ source: lastPrimary.source,
206
+ selected_label: selected.label || "",
207
+ selected_selector: selected.selector || "",
208
+ job_options: []
209
+ };
210
+ }
211
+
212
+ function matchJobOption(option, jobLabel = "") {
213
+ const requested = normalizeJobText(jobLabel).toLowerCase();
214
+ if (!requested) return false;
215
+ return [
216
+ option.value,
217
+ option.label,
218
+ option.title
219
+ ].map((value) => normalizeJobText(value).toLowerCase()).some((value) => (
220
+ value === requested
221
+ || value.includes(requested)
222
+ || requested.includes(value)
223
+ ));
224
+ }
225
+
226
+ async function clickFirstVisible(client, rootNodeId, selectors = []) {
227
+ for (const selector of selectors) {
228
+ const nodeIds = await safeQuerySelectorAll(client, rootNodeId, selector);
229
+ for (const nodeId of nodeIds) {
230
+ try {
231
+ const box = await getNodeBox(client, nodeId);
232
+ if (box.rect.width <= 2 || box.rect.height <= 2) continue;
233
+ await clickPoint(client, box.center.x, box.center.y);
234
+ return {
235
+ clicked: true,
236
+ selector,
237
+ node_id: nodeId,
238
+ center: box.center
239
+ };
240
+ } catch {}
241
+ }
242
+ }
243
+ return {
244
+ clicked: false,
245
+ selector: "",
246
+ node_id: 0
247
+ };
248
+ }
249
+
250
+ export async function selectChatJob(client, rootNodeId, {
251
+ jobLabel = "",
252
+ timeoutMs = 12000,
253
+ intervalMs = 300,
254
+ settleMs = 800
255
+ } = {}) {
256
+ const requested = normalizeJobText(jobLabel);
257
+ if (!requested) {
258
+ return {
259
+ selected: false,
260
+ reason: "missing_job_label"
261
+ };
262
+ }
263
+
264
+ let currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
265
+ let optionsResult = await readChatJobOptions(client, currentRootNodeId, {
266
+ timeoutMs: Math.min(timeoutMs, 1500),
267
+ intervalMs
268
+ });
269
+ let matched = (optionsResult.job_options || []).find((option) => matchJobOption(option, requested)) || null;
270
+ if (!matched || !matched.visible) {
271
+ const triggerRootNodeId = await freshTopRootNodeId(client, currentRootNodeId);
272
+ const trigger = await clickFirstVisible(client, triggerRootNodeId, CHAT_JOB_TRIGGER_SELECTORS);
273
+ if (settleMs > 0) await sleep(settleMs);
274
+ currentRootNodeId = await freshTopRootNodeId(client, triggerRootNodeId);
275
+ optionsResult = await readChatJobOptions(client, currentRootNodeId, {
276
+ timeoutMs,
277
+ intervalMs
278
+ });
279
+ matched = (optionsResult.job_options || []).find((option) => matchJobOption(option, requested)) || null;
280
+ if (!matched || !matched.visible) {
281
+ return {
282
+ selected: false,
283
+ reason: matched ? "job_option_not_visible" : "job_option_not_found",
284
+ requested,
285
+ trigger,
286
+ options: optionsResult.job_options || [],
287
+ selected_label_before: optionsResult.selected_label || ""
288
+ };
289
+ }
290
+ }
291
+
292
+ if (matched.active || normalizeJobText(optionsResult.selected_label).toLowerCase() === normalizeJobText(matched.label).toLowerCase()) {
293
+ return {
294
+ selected: true,
295
+ already_current: true,
296
+ requested,
297
+ selected_option: matched,
298
+ options: optionsResult.job_options || [],
299
+ selected_label: optionsResult.selected_label || matched.label
300
+ };
301
+ }
302
+
303
+ if (matched.center) {
304
+ await clickPoint(client, matched.center.x, matched.center.y);
305
+ } else {
306
+ await clickNodeCenter(client, matched.node_id, {
307
+ scrollIntoView: true
308
+ });
309
+ }
310
+ if (settleMs > 0) await sleep(settleMs);
311
+
312
+ const afterRootNodeId = await freshTopRootNodeId(client, currentRootNodeId);
313
+ const after = await readChatJobOptions(client, afterRootNodeId, {
314
+ timeoutMs: Math.min(timeoutMs, 3000),
315
+ intervalMs
316
+ });
317
+ const afterMatch = (after.job_options || []).find((option) => matchJobOption(option, matched.label)) || matched;
318
+ const selectedLabel = normalizeJobText(after.selected_label || afterMatch.label || "");
319
+ const verified = selectedLabel
320
+ ? matchJobOption({ label: selectedLabel, value: selectedLabel, title: selectedLabel }, matched.label)
321
+ : true;
322
+
323
+ return {
324
+ selected: true,
325
+ verified,
326
+ already_current: false,
327
+ requested,
328
+ selected_option: afterMatch,
329
+ options: after.job_options || optionsResult.job_options || [],
330
+ selected_label: selectedLabel,
331
+ before: optionsResult,
332
+ after
333
+ };
334
+ }
@@ -0,0 +1,88 @@
1
+ import {
2
+ getMainFrameUrl,
3
+ waitForMainFrameUrl
4
+ } from "../../core/browser/index.js";
5
+ import { CHAT_TARGET_URL } from "./constants.js";
6
+
7
+ export const CHAT_FORBIDDEN_TOP_LEVEL_RESUME_CODE = "CHAT_FORBIDDEN_TOP_LEVEL_RESUME_NAVIGATION";
8
+
9
+ export function isChatShellUrl(url = "") {
10
+ const value = String(url || "");
11
+ return /https?:\/\/[^/]*zhipin\.com\/web\/chat\/index(?:[/?#]|$)/i.test(value)
12
+ || /https?:\/\/[^/]*zhipin\.com\/web\/chat\/index$/i.test(value);
13
+ }
14
+
15
+ export function isForbiddenChatResumeTopLevelUrl(url = "") {
16
+ return /https?:\/\/[^/]*zhipin\.com\/web\/frame\/c-resume\/?/i.test(String(url || ""));
17
+ }
18
+
19
+ export async function getChatTopLevelState(client) {
20
+ let url = "";
21
+ let error = null;
22
+ try {
23
+ url = await getMainFrameUrl(client);
24
+ } catch (err) {
25
+ error = err?.message || String(err);
26
+ }
27
+ return {
28
+ url,
29
+ is_chat_shell: isChatShellUrl(url),
30
+ is_forbidden_resume_top_level: isForbiddenChatResumeTopLevelUrl(url),
31
+ error
32
+ };
33
+ }
34
+
35
+ export function makeForbiddenChatResumeNavigationError(pageState, message = "") {
36
+ const error = new Error(message || `Chat tab navigated to forbidden top-level resume URL: ${pageState?.url || "unknown"}`);
37
+ error.code = CHAT_FORBIDDEN_TOP_LEVEL_RESUME_CODE;
38
+ error.page_state = pageState || null;
39
+ return error;
40
+ }
41
+
42
+ export function isForbiddenChatResumeNavigationError(error) {
43
+ return error?.code === CHAT_FORBIDDEN_TOP_LEVEL_RESUME_CODE
44
+ || /CHAT_FORBIDDEN_TOP_LEVEL_RESUME_NAVIGATION/i.test(String(error?.message || error || ""));
45
+ }
46
+
47
+ export async function assertChatShellNotResumeTopLevel(client, {
48
+ context = "chat"
49
+ } = {}) {
50
+ const state = await getChatTopLevelState(client);
51
+ if (state.is_forbidden_resume_top_level) {
52
+ throw makeForbiddenChatResumeNavigationError(
53
+ state,
54
+ `CHAT_FORBIDDEN_TOP_LEVEL_RESUME_NAVIGATION during ${context}: ${state.url}`
55
+ );
56
+ }
57
+ return state;
58
+ }
59
+
60
+ export async function recoverChatShell(client, {
61
+ targetUrl = CHAT_TARGET_URL,
62
+ timeoutMs = 60000,
63
+ intervalMs = 500
64
+ } = {}) {
65
+ const before = await getChatTopLevelState(client);
66
+ if (before.is_chat_shell) {
67
+ return {
68
+ recovered: false,
69
+ before,
70
+ after: before,
71
+ navigate_url: null
72
+ };
73
+ }
74
+
75
+ await client.Page.navigate({ url: targetUrl });
76
+ const waited = await waitForMainFrameUrl(client, isChatShellUrl, {
77
+ timeoutMs,
78
+ intervalMs
79
+ });
80
+ const after = await getChatTopLevelState(client);
81
+ return {
82
+ recovered: waited.ok && after.is_chat_shell,
83
+ before,
84
+ after,
85
+ wait: waited,
86
+ navigate_url: targetUrl
87
+ };
88
+ }
@@ -0,0 +1,56 @@
1
+ import {
2
+ getDocumentRoot,
3
+ querySelector,
4
+ sleep
5
+ } from "../../core/browser/index.js";
6
+
7
+ export async function getChatRoots(client) {
8
+ const topRoot = await getDocumentRoot(client);
9
+ return {
10
+ topRoot,
11
+ roots: [
12
+ { name: "top", nodeId: topRoot.nodeId }
13
+ ],
14
+ rootNodes: {
15
+ top: topRoot.nodeId
16
+ }
17
+ };
18
+ }
19
+
20
+ export async function waitForChatRoots(client, {
21
+ timeoutMs = 12000,
22
+ intervalMs = 300
23
+ } = {}) {
24
+ const started = Date.now();
25
+ let lastState = null;
26
+ let lastError = null;
27
+ while (Date.now() - started <= timeoutMs) {
28
+ try {
29
+ lastState = await getChatRoots(client);
30
+ if (lastState?.rootNodes?.top) return lastState;
31
+ } catch (error) {
32
+ lastError = error;
33
+ }
34
+ await sleep(intervalMs);
35
+ }
36
+ if (lastError && !lastState) throw lastError;
37
+ return lastState;
38
+ }
39
+
40
+ export async function queryFirstAcrossChatRoots(client, roots, selectors) {
41
+ for (const root of roots) {
42
+ if (!root?.nodeId) continue;
43
+ for (const selector of selectors) {
44
+ const nodeId = await querySelector(client, root.nodeId, selector);
45
+ if (nodeId) {
46
+ return {
47
+ root: root.name,
48
+ root_node_id: root.nodeId,
49
+ selector,
50
+ node_id: nodeId
51
+ };
52
+ }
53
+ }
54
+ }
55
+ return null;
56
+ }