@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,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
+ }