@reconcrap/boss-recommend-mcp 2.1.14 → 2.1.16
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 +34 -5
- package/package.json +8 -7
- package/skills/boss-chat/README.md +2 -2
- package/skills/boss-chat/SKILL.md +7 -7
- package/skills/boss-recruit-pipeline/SKILL.md +23 -1
- package/src/chat-mcp.js +127 -88
- package/src/core/greet-quota/index.js +17 -0
- package/src/core/reporting/legacy-csv.js +5 -1
- package/src/domains/chat/detail.js +79 -47
- package/src/domains/chat/run-service.js +400 -158
- package/src/domains/recommend/colleague-contact.js +333 -0
- package/src/domains/recommend/index.js +1 -0
- package/src/domains/recommend/run-service.js +166 -77
- package/src/domains/recruit/constants.js +69 -0
- package/src/domains/recruit/instruction-parser.js +403 -86
- package/src/domains/recruit/run-service.js +320 -11
- package/src/domains/recruit/search.js +2118 -306
- package/src/index.js +38 -23
- package/src/parser.js +45 -2
- package/src/recommend-mcp.js +92 -18
- package/src/recruit-mcp.js +236 -3
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clickNodeCenter,
|
|
3
|
+
getNodeBox,
|
|
4
|
+
getOuterHTML,
|
|
5
|
+
querySelectorAll,
|
|
6
|
+
sleep
|
|
7
|
+
} from "../../core/browser/index.js";
|
|
8
|
+
import { htmlToText } from "../../core/screening/index.js";
|
|
9
|
+
|
|
10
|
+
const COLLEAGUE_SECTION_SELECTOR = ".colleague-collaboration";
|
|
11
|
+
const COLLEAGUE_TAB_SELECTOR = ".colleague-collaboration .tab-hd";
|
|
12
|
+
const SELECTED_TAB_SELECTOR = ".colleague-collaboration .tab-hd .selected";
|
|
13
|
+
const SECTION_SELECTED_TAB_SELECTOR = ".tab-hd .selected";
|
|
14
|
+
const TAB_CANDIDATE_SELECTOR = ".tab-hd span, .tab-hd div, .tab-hd *";
|
|
15
|
+
const ROW_SELECTOR = ".colleague-collaboration .record-item.mate-log-item";
|
|
16
|
+
const ROW_CONTENT_SELECTOR = ".colleague-collaboration .record-item.mate-log-item .content";
|
|
17
|
+
const DETAIL_PANE_SELECTOR = ".resume-item-detail";
|
|
18
|
+
|
|
19
|
+
function normalizeText(value) {
|
|
20
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function dateOnly(value) {
|
|
24
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
25
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
26
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function daysBetween(left, right) {
|
|
30
|
+
const leftDate = dateOnly(left);
|
|
31
|
+
const rightDate = dateOnly(right);
|
|
32
|
+
if (!leftDate || !rightDate) return null;
|
|
33
|
+
return Math.floor((leftDate.getTime() - rightDate.getTime()) / 86400000);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatLocalDate(date) {
|
|
37
|
+
const parsed = dateOnly(date);
|
|
38
|
+
if (!parsed) return null;
|
|
39
|
+
const year = parsed.getFullYear();
|
|
40
|
+
const month = String(parsed.getMonth() + 1).padStart(2, "0");
|
|
41
|
+
const day = String(parsed.getDate()).padStart(2, "0");
|
|
42
|
+
return `${year}-${month}-${day}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeDate(year, month, day) {
|
|
46
|
+
const parsed = new Date(year, month - 1, day);
|
|
47
|
+
if (
|
|
48
|
+
parsed.getFullYear() !== year
|
|
49
|
+
|| parsed.getMonth() !== month - 1
|
|
50
|
+
|| parsed.getDate() !== day
|
|
51
|
+
) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return parsed;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseColleagueContactDate(text, {
|
|
58
|
+
referenceDate = new Date()
|
|
59
|
+
} = {}) {
|
|
60
|
+
const raw = normalizeText(text);
|
|
61
|
+
if (!raw) return null;
|
|
62
|
+
const today = dateOnly(referenceDate) || dateOnly(new Date());
|
|
63
|
+
const relativeDays = raw.match(/(\d+)\s*天前/);
|
|
64
|
+
if (relativeDays) {
|
|
65
|
+
const days = Number.parseInt(relativeDays[1], 10);
|
|
66
|
+
if (Number.isFinite(days) && days >= 0) {
|
|
67
|
+
const date = new Date(today);
|
|
68
|
+
date.setDate(date.getDate() - days);
|
|
69
|
+
return date;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (/今天/.test(raw)) return today;
|
|
73
|
+
if (/昨天/.test(raw)) {
|
|
74
|
+
const date = new Date(today);
|
|
75
|
+
date.setDate(date.getDate() - 1);
|
|
76
|
+
return date;
|
|
77
|
+
}
|
|
78
|
+
if (/前天/.test(raw)) {
|
|
79
|
+
const date = new Date(today);
|
|
80
|
+
date.setDate(date.getDate() - 2);
|
|
81
|
+
return date;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const full = raw.match(/(20\d{2})[.\-\/](\d{1,2})[.\-\/](\d{1,2})/);
|
|
85
|
+
if (full) {
|
|
86
|
+
return makeDate(
|
|
87
|
+
Number.parseInt(full[1], 10),
|
|
88
|
+
Number.parseInt(full[2], 10),
|
|
89
|
+
Number.parseInt(full[3], 10)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const partial = raw.match(/(?:^|\D)(\d{1,2})[.\-\/](\d{1,2})(?:\D|$)/);
|
|
94
|
+
if (partial) {
|
|
95
|
+
const reference = dateOnly(referenceDate) || new Date();
|
|
96
|
+
let date = makeDate(
|
|
97
|
+
reference.getFullYear(),
|
|
98
|
+
Number.parseInt(partial[1], 10),
|
|
99
|
+
Number.parseInt(partial[2], 10)
|
|
100
|
+
);
|
|
101
|
+
if (date && daysBetween(date, reference) > 7) {
|
|
102
|
+
date = makeDate(
|
|
103
|
+
reference.getFullYear() - 1,
|
|
104
|
+
Number.parseInt(partial[1], 10),
|
|
105
|
+
Number.parseInt(partial[2], 10)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return date;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function isDateWithinWindow(date, {
|
|
115
|
+
referenceDate = new Date(),
|
|
116
|
+
windowDays = 14
|
|
117
|
+
} = {}) {
|
|
118
|
+
const diff = daysBetween(referenceDate, date);
|
|
119
|
+
return Number.isFinite(diff) && diff >= 0 && diff <= windowDays;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function textForNode(client, nodeId) {
|
|
123
|
+
return htmlToText(await getOuterHTML(client, nodeId));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function queryAcrossRoots(client, roots, selector) {
|
|
127
|
+
const matches = [];
|
|
128
|
+
for (const root of roots || []) {
|
|
129
|
+
if (!root?.nodeId) continue;
|
|
130
|
+
const nodeIds = await querySelectorAll(client, root.nodeId, selector).catch(() => []);
|
|
131
|
+
for (const nodeId of nodeIds) {
|
|
132
|
+
matches.push({
|
|
133
|
+
root: root.name,
|
|
134
|
+
root_node_id: root.nodeId,
|
|
135
|
+
selector,
|
|
136
|
+
node_id: nodeId
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return matches;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function tabIsColleague(text) {
|
|
144
|
+
return /同事沟通进度/.test(normalizeText(text));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function ensureColleagueTabSelected(client, sectionNodeId) {
|
|
148
|
+
const selectedIds = await querySelectorAll(client, sectionNodeId, SECTION_SELECTED_TAB_SELECTOR).catch(() => []);
|
|
149
|
+
for (const nodeId of selectedIds) {
|
|
150
|
+
const text = normalizeText(await textForNode(client, nodeId).catch(() => ""));
|
|
151
|
+
if (tabIsColleague(text)) {
|
|
152
|
+
return {
|
|
153
|
+
selected: true,
|
|
154
|
+
changed: false,
|
|
155
|
+
selected_text: text
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const candidateIds = await querySelectorAll(client, sectionNodeId, TAB_CANDIDATE_SELECTOR).catch(() => []);
|
|
161
|
+
for (const nodeId of candidateIds) {
|
|
162
|
+
const text = normalizeText(await textForNode(client, nodeId).catch(() => ""));
|
|
163
|
+
if (!tabIsColleague(text)) continue;
|
|
164
|
+
const box = await clickNodeCenter(client, nodeId, { scrollIntoView: true });
|
|
165
|
+
await sleep(500);
|
|
166
|
+
return {
|
|
167
|
+
selected: true,
|
|
168
|
+
changed: true,
|
|
169
|
+
selected_text: text,
|
|
170
|
+
click_box: {
|
|
171
|
+
rect: box.rect,
|
|
172
|
+
center: box.center
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
selected: false,
|
|
179
|
+
changed: false,
|
|
180
|
+
selected_text: selectedIds.length
|
|
181
|
+
? normalizeText(await textForNode(client, selectedIds[0]).catch(() => ""))
|
|
182
|
+
: ""
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function readContactRows(client, roots) {
|
|
187
|
+
const rowMatches = await queryAcrossRoots(client, roots, ROW_CONTENT_SELECTOR);
|
|
188
|
+
const fallbackMatches = rowMatches.length ? [] : await queryAcrossRoots(client, roots, ROW_SELECTOR);
|
|
189
|
+
const matches = rowMatches.length ? rowMatches : fallbackMatches;
|
|
190
|
+
const rows = [];
|
|
191
|
+
const seen = new Set();
|
|
192
|
+
for (const match of matches) {
|
|
193
|
+
const text = normalizeText(await textForNode(client, match.node_id).catch(() => ""));
|
|
194
|
+
if (!text || seen.has(text)) continue;
|
|
195
|
+
seen.add(text);
|
|
196
|
+
rows.push({
|
|
197
|
+
text,
|
|
198
|
+
root: match.root,
|
|
199
|
+
selector: match.selector,
|
|
200
|
+
node_id: match.node_id
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return rows;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function scrollDetailPaneForRows(client, roots, sectionNodeId, {
|
|
207
|
+
maxScrolls = 4,
|
|
208
|
+
settleMs = 350
|
|
209
|
+
} = {}) {
|
|
210
|
+
const detailPanes = await queryAcrossRoots(client, roots, DETAIL_PANE_SELECTOR);
|
|
211
|
+
const targetNodeId = detailPanes[0]?.node_id || sectionNodeId;
|
|
212
|
+
let box = null;
|
|
213
|
+
try {
|
|
214
|
+
box = await getNodeBox(client, targetNodeId);
|
|
215
|
+
} catch {
|
|
216
|
+
try {
|
|
217
|
+
box = await getNodeBox(client, sectionNodeId);
|
|
218
|
+
} catch {
|
|
219
|
+
return { scrolls: 0, reason: "scroll_target_box_unavailable" };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
let scrolls = 0;
|
|
223
|
+
for (let index = 0; index < maxScrolls; index += 1) {
|
|
224
|
+
await client.Input.dispatchMouseEvent({
|
|
225
|
+
type: "mouseWheel",
|
|
226
|
+
x: box.center.x,
|
|
227
|
+
y: box.center.y,
|
|
228
|
+
deltaY: 680,
|
|
229
|
+
deltaX: 0
|
|
230
|
+
});
|
|
231
|
+
scrolls += 1;
|
|
232
|
+
await sleep(settleMs);
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
scrolls,
|
|
236
|
+
target_selector: detailPanes[0]?.selector || COLLEAGUE_SECTION_SELECTOR
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function waitForColleagueSections(client, roots, {
|
|
241
|
+
timeoutMs = 1000,
|
|
242
|
+
intervalMs = 150
|
|
243
|
+
} = {}) {
|
|
244
|
+
const started = Date.now();
|
|
245
|
+
let sections = [];
|
|
246
|
+
do {
|
|
247
|
+
sections = await queryAcrossRoots(client, roots, COLLEAGUE_SECTION_SELECTOR);
|
|
248
|
+
if (sections.length) return sections;
|
|
249
|
+
if (Date.now() - started >= timeoutMs) break;
|
|
250
|
+
await sleep(intervalMs);
|
|
251
|
+
} while (Date.now() - started <= timeoutMs);
|
|
252
|
+
return sections;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function inspectRecentColleagueContact(client, detailState, {
|
|
256
|
+
referenceDate = new Date(),
|
|
257
|
+
windowDays = 14,
|
|
258
|
+
scroll = true,
|
|
259
|
+
sectionWaitMs = 1000,
|
|
260
|
+
sectionPollMs = 150
|
|
261
|
+
} = {}) {
|
|
262
|
+
const roots = detailState?.roots || [];
|
|
263
|
+
const sections = await waitForColleagueSections(client, roots, {
|
|
264
|
+
timeoutMs: sectionWaitMs,
|
|
265
|
+
intervalMs: sectionPollMs
|
|
266
|
+
});
|
|
267
|
+
if (!sections.length) {
|
|
268
|
+
return {
|
|
269
|
+
checked: true,
|
|
270
|
+
panel_found: false,
|
|
271
|
+
recent: false,
|
|
272
|
+
reason: "panel_missing",
|
|
273
|
+
window_days: windowDays,
|
|
274
|
+
rows: []
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const section = sections[0];
|
|
279
|
+
const tabHeader = await queryAcrossRoots(client, roots, COLLEAGUE_TAB_SELECTOR);
|
|
280
|
+
const tab = await ensureColleagueTabSelected(client, section.node_id);
|
|
281
|
+
if (!tab.selected) {
|
|
282
|
+
return {
|
|
283
|
+
checked: true,
|
|
284
|
+
panel_found: true,
|
|
285
|
+
recent: false,
|
|
286
|
+
reason: "colleague_tab_unavailable",
|
|
287
|
+
window_days: windowDays,
|
|
288
|
+
section_root: section.root,
|
|
289
|
+
tab_header_found: tabHeader.length > 0,
|
|
290
|
+
selected_tab_text: tab.selected_text,
|
|
291
|
+
rows: []
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let rows = await readContactRows(client, roots);
|
|
296
|
+
let scroll_probe = null;
|
|
297
|
+
if (scroll) {
|
|
298
|
+
scroll_probe = await scrollDetailPaneForRows(client, roots, section.node_id);
|
|
299
|
+
const afterScrollRows = await readContactRows(client, roots);
|
|
300
|
+
const byText = new Map(rows.map((row) => [row.text, row]));
|
|
301
|
+
for (const row of afterScrollRows) {
|
|
302
|
+
if (!byText.has(row.text)) byText.set(row.text, row);
|
|
303
|
+
}
|
|
304
|
+
rows = Array.from(byText.values());
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const parsedRows = rows.map((row) => {
|
|
308
|
+
const parsedDate = parseColleagueContactDate(row.text, { referenceDate });
|
|
309
|
+
return {
|
|
310
|
+
...row,
|
|
311
|
+
parsed_date: parsedDate ? formatLocalDate(parsedDate) : null,
|
|
312
|
+
within_window: parsedDate
|
|
313
|
+
? isDateWithinWindow(parsedDate, { referenceDate, windowDays })
|
|
314
|
+
: false
|
|
315
|
+
};
|
|
316
|
+
});
|
|
317
|
+
const matched = parsedRows.find((row) => row.within_window) || null;
|
|
318
|
+
return {
|
|
319
|
+
checked: true,
|
|
320
|
+
panel_found: true,
|
|
321
|
+
recent: Boolean(matched),
|
|
322
|
+
reason: matched ? "recent_colleague_contact_found" : "no_recent_colleague_contact",
|
|
323
|
+
window_days: windowDays,
|
|
324
|
+
section_root: section.root,
|
|
325
|
+
tab_header_found: tabHeader.length > 0,
|
|
326
|
+
selected_tab_text: tab.selected_text,
|
|
327
|
+
tab_changed: tab.changed,
|
|
328
|
+
matched_row: matched,
|
|
329
|
+
row_count: parsedRows.length,
|
|
330
|
+
rows: parsedRows,
|
|
331
|
+
scroll_probe
|
|
332
|
+
};
|
|
333
|
+
}
|