@viyzhu/boss-cli-fork 0.7.2-rc.1 → 0.7.3
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/AGENTS.md +85 -56
- package/LICENSE +674 -674
- package/README.md +284 -276
- package/dist/browser/cdp_browser.d.ts +170 -88
- package/dist/browser/cdp_browser.d.ts.map +1 -1
- package/dist/browser/cdp_browser.js +674 -400
- package/dist/browser/cdp_browser.js.map +1 -1
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/cli/cliRouter.js +61 -61
- package/dist/common/auth.js +49 -49
- package/dist/common/boss_modal.js +42 -42
- package/dist/common/boss_page_guards.js +247 -247
- package/dist/common/boss_paywall_popup.js +87 -87
- package/dist/common/boss_session_page.d.ts.map +1 -1
- package/dist/common/boss_session_page.js +16 -15
- package/dist/common/boss_session_page.js.map +1 -1
- package/dist/common/boss_sidebar_nav.js +24 -24
- package/dist/common/c_resume_capture.js +61 -61
- package/dist/toolset/action.d.ts.map +1 -1
- package/dist/toolset/action.js +237 -233
- package/dist/toolset/action.js.map +1 -1
- package/dist/toolset/chat.js +437 -437
- package/dist/toolset/deep-search.js +570 -570
- package/dist/toolset/jd.js +132 -132
- package/dist/toolset/list.js +48 -48
- package/dist/toolset/normal-search.js +158 -158
- package/dist/toolset/preview.d.ts.map +1 -1
- package/dist/toolset/preview.js +3 -3
- package/dist/toolset/preview.js.map +1 -1
- package/dist/toolset/recommend.js +207 -207
- package/package.json +69 -69
- package/skills/boss-frontend-analysis/SKILL.md +58 -58
- package/skills/boss-frontend-analysis/agents/openai.yaml +4 -4
- package/skills/boss-frontend-analysis/scripts/capture_boss_frontend.mjs +604 -604
package/dist/toolset/jd.js
CHANGED
|
@@ -7,30 +7,30 @@ import { JD_DIR } from '../config.js';
|
|
|
7
7
|
const BOSS_CHAT_JOB_LIST_URL = 'https://www.zhipin.com/web/chat/job/list';
|
|
8
8
|
const JD_PAGE_SETTLE_MS = { min: 3200, max: 5600 };
|
|
9
9
|
const JD_DETAIL_DEFAULT_WAIT_MS = 10_000;
|
|
10
|
-
const CLICK_JD_BACK_BUTTON_SCRIPT = `(() => {
|
|
11
|
-
const isVisible = (el) => {
|
|
12
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
13
|
-
const st = window.getComputedStyle(el);
|
|
14
|
-
if (st.display === "none" || st.visibility === "hidden") return false;
|
|
15
|
-
const r = el.getBoundingClientRect();
|
|
16
|
-
return r.width > 0 && r.height > 0;
|
|
17
|
-
};
|
|
18
|
-
const topNav = document.querySelector(".top-nav");
|
|
19
|
-
if (!(topNav instanceof HTMLElement)) {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
const backBtn = topNav.querySelector(".history-back-container .back-btn");
|
|
23
|
-
const icon = backBtn?.querySelector("i.iboss-right");
|
|
24
|
-
if (!(icon instanceof HTMLElement)) {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
if (!(backBtn instanceof HTMLElement) || !isVisible(backBtn)) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
backBtn.scrollIntoView({ block: "center", inline: "nearest" });
|
|
31
|
-
backBtn.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
32
|
-
backBtn.click();
|
|
33
|
-
return true;
|
|
10
|
+
const CLICK_JD_BACK_BUTTON_SCRIPT = `(() => {
|
|
11
|
+
const isVisible = (el) => {
|
|
12
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
13
|
+
const st = window.getComputedStyle(el);
|
|
14
|
+
if (st.display === "none" || st.visibility === "hidden") return false;
|
|
15
|
+
const r = el.getBoundingClientRect();
|
|
16
|
+
return r.width > 0 && r.height > 0;
|
|
17
|
+
};
|
|
18
|
+
const topNav = document.querySelector(".top-nav");
|
|
19
|
+
if (!(topNav instanceof HTMLElement)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const backBtn = topNav.querySelector(".history-back-container .back-btn");
|
|
23
|
+
const icon = backBtn?.querySelector("i.iboss-right");
|
|
24
|
+
if (!(icon instanceof HTMLElement)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (!(backBtn instanceof HTMLElement) || !isVisible(backBtn)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
backBtn.scrollIntoView({ block: "center", inline: "nearest" });
|
|
31
|
+
backBtn.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
32
|
+
backBtn.click();
|
|
33
|
+
return true;
|
|
34
34
|
})()`;
|
|
35
35
|
function isBossChatJobListUrl(url) {
|
|
36
36
|
try {
|
|
@@ -87,38 +87,38 @@ async function fileExists(target) {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
async function readJobsFromFrame(frame) {
|
|
90
|
-
return (await frame.evaluate(`(() => {
|
|
91
|
-
const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
|
|
92
|
-
const rows = Array.from(document.querySelectorAll(".job-jobInfo-warp"));
|
|
93
|
-
const jobs = rows.map((el) => {
|
|
94
|
-
const statusText = norm(el.querySelector(".job-status-wrapper .status-box")?.textContent);
|
|
95
|
-
const labelText = norm(el.querySelector(".job-title .label-common")?.textContent);
|
|
96
|
-
const meta = Array.from(el.querySelectorAll(".job-main-info-wrapper .info-labels span"))
|
|
97
|
-
.map((x) => norm(x.textContent))
|
|
98
|
-
.filter(Boolean);
|
|
99
|
-
const nums = Array.from(el.querySelectorAll(".job-about-num-wrapper .inner-box .num"))
|
|
100
|
-
.map((x) => norm(x.textContent));
|
|
101
|
-
return {
|
|
102
|
-
id: norm(el.getAttribute("data-id")),
|
|
103
|
-
title: norm(el.querySelector(".job-title a")?.textContent),
|
|
104
|
-
label: labelText,
|
|
105
|
-
status: statusText,
|
|
106
|
-
meta,
|
|
107
|
-
viewed: nums[0] || "0",
|
|
108
|
-
chatted: nums[1] || "0",
|
|
109
|
-
interested: nums[2] || "0",
|
|
110
|
-
};
|
|
111
|
-
});
|
|
112
|
-
const totalText = norm(document.querySelector(".total-num")?.textContent);
|
|
113
|
-
const bodyText = norm(document.body?.innerText ?? "");
|
|
114
|
-
return {
|
|
115
|
-
rows: rows.length,
|
|
116
|
-
totalText,
|
|
117
|
-
title: document.title || "",
|
|
118
|
-
url: location.href,
|
|
119
|
-
bodyPreview: bodyText.slice(0, 120),
|
|
120
|
-
jobs,
|
|
121
|
-
};
|
|
90
|
+
return (await frame.evaluate(`(() => {
|
|
91
|
+
const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
|
|
92
|
+
const rows = Array.from(document.querySelectorAll(".job-jobInfo-warp"));
|
|
93
|
+
const jobs = rows.map((el) => {
|
|
94
|
+
const statusText = norm(el.querySelector(".job-status-wrapper .status-box")?.textContent);
|
|
95
|
+
const labelText = norm(el.querySelector(".job-title .label-common")?.textContent);
|
|
96
|
+
const meta = Array.from(el.querySelectorAll(".job-main-info-wrapper .info-labels span"))
|
|
97
|
+
.map((x) => norm(x.textContent))
|
|
98
|
+
.filter(Boolean);
|
|
99
|
+
const nums = Array.from(el.querySelectorAll(".job-about-num-wrapper .inner-box .num"))
|
|
100
|
+
.map((x) => norm(x.textContent));
|
|
101
|
+
return {
|
|
102
|
+
id: norm(el.getAttribute("data-id")),
|
|
103
|
+
title: norm(el.querySelector(".job-title a")?.textContent),
|
|
104
|
+
label: labelText,
|
|
105
|
+
status: statusText,
|
|
106
|
+
meta,
|
|
107
|
+
viewed: nums[0] || "0",
|
|
108
|
+
chatted: nums[1] || "0",
|
|
109
|
+
interested: nums[2] || "0",
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
const totalText = norm(document.querySelector(".total-num")?.textContent);
|
|
113
|
+
const bodyText = norm(document.body?.innerText ?? "");
|
|
114
|
+
return {
|
|
115
|
+
rows: rows.length,
|
|
116
|
+
totalText,
|
|
117
|
+
title: document.title || "",
|
|
118
|
+
url: location.href,
|
|
119
|
+
bodyPreview: bodyText.slice(0, 120),
|
|
120
|
+
jobs,
|
|
121
|
+
};
|
|
122
122
|
})()`));
|
|
123
123
|
}
|
|
124
124
|
async function readJobsFromPageAnyFrame(page) {
|
|
@@ -185,88 +185,88 @@ async function waitForJobRowsReady(page, timeoutMs) {
|
|
|
185
185
|
async function clickEditForJob(frame, job) {
|
|
186
186
|
const targetId = JSON.stringify(job.id ?? '');
|
|
187
187
|
const targetTitle = JSON.stringify(job.title ?? '');
|
|
188
|
-
const clicked = (await frame.evaluate(`(() => {
|
|
189
|
-
const jobId = ${targetId};
|
|
190
|
-
const title = ${targetTitle};
|
|
191
|
-
const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
|
|
192
|
-
const rows = Array.from(document.querySelectorAll(".job-jobInfo-warp"));
|
|
193
|
-
let row = null;
|
|
194
|
-
if (jobId) {
|
|
195
|
-
row = rows.find((el) => norm(el.getAttribute("data-id")) === jobId) ?? null;
|
|
196
|
-
}
|
|
197
|
-
if (!row && title) {
|
|
198
|
-
row = rows.find((el) => {
|
|
199
|
-
const t = norm(el.querySelector(".job-title a")?.textContent);
|
|
200
|
-
return t === title;
|
|
201
|
-
}) ?? null;
|
|
202
|
-
}
|
|
203
|
-
if (!row) return false;
|
|
204
|
-
const editBtn = row.querySelector(".position-edit");
|
|
205
|
-
if (!(editBtn instanceof HTMLElement)) return false;
|
|
206
|
-
editBtn.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
207
|
-
editBtn.click();
|
|
208
|
-
return true;
|
|
188
|
+
const clicked = (await frame.evaluate(`(() => {
|
|
189
|
+
const jobId = ${targetId};
|
|
190
|
+
const title = ${targetTitle};
|
|
191
|
+
const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
|
|
192
|
+
const rows = Array.from(document.querySelectorAll(".job-jobInfo-warp"));
|
|
193
|
+
let row = null;
|
|
194
|
+
if (jobId) {
|
|
195
|
+
row = rows.find((el) => norm(el.getAttribute("data-id")) === jobId) ?? null;
|
|
196
|
+
}
|
|
197
|
+
if (!row && title) {
|
|
198
|
+
row = rows.find((el) => {
|
|
199
|
+
const t = norm(el.querySelector(".job-title a")?.textContent);
|
|
200
|
+
return t === title;
|
|
201
|
+
}) ?? null;
|
|
202
|
+
}
|
|
203
|
+
if (!row) return false;
|
|
204
|
+
const editBtn = row.querySelector(".position-edit");
|
|
205
|
+
if (!(editBtn instanceof HTMLElement)) return false;
|
|
206
|
+
editBtn.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
207
|
+
editBtn.click();
|
|
208
|
+
return true;
|
|
209
209
|
})()`));
|
|
210
210
|
if (!clicked) {
|
|
211
211
|
throw new Error(`未找到职位“${job.title}”的编辑入口`);
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
async function readJobDetailFromFrame(frame) {
|
|
215
|
-
const detail = (await frame.evaluate(`(() => {
|
|
216
|
-
const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
|
|
217
|
-
const root = document.querySelector(".job-edit-container.edit-job");
|
|
218
|
-
if (!root) {
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
const rows = Array.from(root.querySelectorAll(".form-row"));
|
|
222
|
-
const rowValueByTitle = {};
|
|
223
|
-
for (const row of rows) {
|
|
224
|
-
const title = norm(row.querySelector(".title")?.textContent);
|
|
225
|
-
if (!title) continue;
|
|
226
|
-
const content = norm(row.querySelector(".content")?.innerText);
|
|
227
|
-
if (content && !rowValueByTitle[title]) {
|
|
228
|
-
rowValueByTitle[title] = content;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
const salaryValues = Array.from(
|
|
232
|
-
root.querySelectorAll(".scope-selecter .scope-select .ui-select-selected-value"),
|
|
233
|
-
).map((el) => norm(el.textContent));
|
|
234
|
-
const keywordValues = Array.from(
|
|
235
|
-
root.querySelectorAll(".job-skill-content .job-skill-item, .job-skill-content .skill-tag, .job-skill-content .tag"),
|
|
236
|
-
)
|
|
237
|
-
.map((el) => norm(el.textContent))
|
|
238
|
-
.filter(Boolean);
|
|
239
|
-
const detail = {
|
|
240
|
-
pageTitle: document.title || "",
|
|
241
|
-
pageUrl: location.href,
|
|
242
|
-
company: norm(root.querySelector(".base-info .text-primary")?.textContent),
|
|
243
|
-
recruitmentType: norm(root.querySelector(".recruitment-type-wrap .ui-select-selected-value")?.textContent),
|
|
244
|
-
jobName:
|
|
245
|
-
norm(root.querySelector("input[name='jobName']")?.value) ||
|
|
246
|
-
norm(root.querySelector(".job-name input")?.value) ||
|
|
247
|
-
(rowValueByTitle["职位名称"] || ""),
|
|
248
|
-
description: norm(root.querySelector(".performance-row textarea")?.value),
|
|
249
|
-
overseas: norm(root.querySelector(".overseas-entry-container .chose-item.active")?.textContent),
|
|
250
|
-
jobCategory:
|
|
251
|
-
norm(root.querySelector("input[name='jobCategory']")?.value) ||
|
|
252
|
-
norm(root.querySelector(".job-category-tag-container")?.innerText),
|
|
253
|
-
experience:
|
|
254
|
-
norm(root.querySelector(".job-experience-row .experience-select .ui-select-selected-value")?.textContent) ||
|
|
255
|
-
(rowValueByTitle["经验"] || ""),
|
|
256
|
-
education:
|
|
257
|
-
norm(root.querySelector(".form-row .title")?.textContent?.includes("学历")
|
|
258
|
-
? root.querySelector(".form-row .experience-select .ui-select-selected-value")?.textContent
|
|
259
|
-
: "") ||
|
|
260
|
-
(rowValueByTitle["学历"] || ""),
|
|
261
|
-
salaryRange:
|
|
262
|
-
salaryValues.length >= 2 ? (salaryValues[0] + "-" + salaryValues[1]) : salaryValues.join("-"),
|
|
263
|
-
salaryMonths: norm(root.querySelector(".salaryMonth-select .ui-select-selected-value")?.textContent),
|
|
264
|
-
keywords: keywordValues.join("|"),
|
|
265
|
-
workLocation:
|
|
266
|
-
norm(root.querySelector(".job-address input.ipt")?.value) ||
|
|
267
|
-
(rowValueByTitle["工作地点"] || ""),
|
|
268
|
-
};
|
|
269
|
-
return detail;
|
|
215
|
+
const detail = (await frame.evaluate(`(() => {
|
|
216
|
+
const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
|
|
217
|
+
const root = document.querySelector(".job-edit-container.edit-job");
|
|
218
|
+
if (!root) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const rows = Array.from(root.querySelectorAll(".form-row"));
|
|
222
|
+
const rowValueByTitle = {};
|
|
223
|
+
for (const row of rows) {
|
|
224
|
+
const title = norm(row.querySelector(".title")?.textContent);
|
|
225
|
+
if (!title) continue;
|
|
226
|
+
const content = norm(row.querySelector(".content")?.innerText);
|
|
227
|
+
if (content && !rowValueByTitle[title]) {
|
|
228
|
+
rowValueByTitle[title] = content;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const salaryValues = Array.from(
|
|
232
|
+
root.querySelectorAll(".scope-selecter .scope-select .ui-select-selected-value"),
|
|
233
|
+
).map((el) => norm(el.textContent));
|
|
234
|
+
const keywordValues = Array.from(
|
|
235
|
+
root.querySelectorAll(".job-skill-content .job-skill-item, .job-skill-content .skill-tag, .job-skill-content .tag"),
|
|
236
|
+
)
|
|
237
|
+
.map((el) => norm(el.textContent))
|
|
238
|
+
.filter(Boolean);
|
|
239
|
+
const detail = {
|
|
240
|
+
pageTitle: document.title || "",
|
|
241
|
+
pageUrl: location.href,
|
|
242
|
+
company: norm(root.querySelector(".base-info .text-primary")?.textContent),
|
|
243
|
+
recruitmentType: norm(root.querySelector(".recruitment-type-wrap .ui-select-selected-value")?.textContent),
|
|
244
|
+
jobName:
|
|
245
|
+
norm(root.querySelector("input[name='jobName']")?.value) ||
|
|
246
|
+
norm(root.querySelector(".job-name input")?.value) ||
|
|
247
|
+
(rowValueByTitle["职位名称"] || ""),
|
|
248
|
+
description: norm(root.querySelector(".performance-row textarea")?.value),
|
|
249
|
+
overseas: norm(root.querySelector(".overseas-entry-container .chose-item.active")?.textContent),
|
|
250
|
+
jobCategory:
|
|
251
|
+
norm(root.querySelector("input[name='jobCategory']")?.value) ||
|
|
252
|
+
norm(root.querySelector(".job-category-tag-container")?.innerText),
|
|
253
|
+
experience:
|
|
254
|
+
norm(root.querySelector(".job-experience-row .experience-select .ui-select-selected-value")?.textContent) ||
|
|
255
|
+
(rowValueByTitle["经验"] || ""),
|
|
256
|
+
education:
|
|
257
|
+
norm(root.querySelector(".form-row .title")?.textContent?.includes("学历")
|
|
258
|
+
? root.querySelector(".form-row .experience-select .ui-select-selected-value")?.textContent
|
|
259
|
+
: "") ||
|
|
260
|
+
(rowValueByTitle["学历"] || ""),
|
|
261
|
+
salaryRange:
|
|
262
|
+
salaryValues.length >= 2 ? (salaryValues[0] + "-" + salaryValues[1]) : salaryValues.join("-"),
|
|
263
|
+
salaryMonths: norm(root.querySelector(".salaryMonth-select .ui-select-selected-value")?.textContent),
|
|
264
|
+
keywords: keywordValues.join("|"),
|
|
265
|
+
workLocation:
|
|
266
|
+
norm(root.querySelector(".job-address input.ipt")?.value) ||
|
|
267
|
+
(rowValueByTitle["工作地点"] || ""),
|
|
268
|
+
};
|
|
269
|
+
return detail;
|
|
270
270
|
})()`));
|
|
271
271
|
return detail;
|
|
272
272
|
}
|
package/dist/toolset/list.js
CHANGED
|
@@ -29,38 +29,38 @@ async function waitForCandidateListSettled(page, opts) {
|
|
|
29
29
|
}
|
|
30
30
|
async function clickChatFilterTab(page, label) {
|
|
31
31
|
const labelLiteral = JSON.stringify(label);
|
|
32
|
-
await page.evaluate(`(() => {
|
|
33
|
-
const targetText = ${labelLiteral};
|
|
34
|
-
const container = document.querySelector(".chat-message-filter-left");
|
|
35
|
-
if (!container) {
|
|
36
|
-
throw new Error("未找到聊天筛选容器:.chat-message-filter-left");
|
|
37
|
-
}
|
|
38
|
-
const spans = Array.from(container.querySelectorAll("span"));
|
|
39
|
-
const norm = (v) => (v ?? "").replace(/\\s+/g, "");
|
|
40
|
-
const target = spans.find((el) => norm(el.textContent).includes(targetText));
|
|
41
|
-
if (!target) {
|
|
42
|
-
const labels = spans.map((el) => norm(el.textContent)).filter(Boolean).join(",");
|
|
43
|
-
throw new Error("未找到聊天筛选项:" + targetText + ";当前筛选项:" + (labels || "空"));
|
|
44
|
-
}
|
|
45
|
-
target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
46
|
-
target.click();
|
|
32
|
+
await page.evaluate(`(() => {
|
|
33
|
+
const targetText = ${labelLiteral};
|
|
34
|
+
const container = document.querySelector(".chat-message-filter-left");
|
|
35
|
+
if (!container) {
|
|
36
|
+
throw new Error("未找到聊天筛选容器:.chat-message-filter-left");
|
|
37
|
+
}
|
|
38
|
+
const spans = Array.from(container.querySelectorAll("span"));
|
|
39
|
+
const norm = (v) => (v ?? "").replace(/\\s+/g, "");
|
|
40
|
+
const target = spans.find((el) => norm(el.textContent).includes(targetText));
|
|
41
|
+
if (!target) {
|
|
42
|
+
const labels = spans.map((el) => norm(el.textContent)).filter(Boolean).join(",");
|
|
43
|
+
throw new Error("未找到聊天筛选项:" + targetText + ";当前筛选项:" + (labels || "空"));
|
|
44
|
+
}
|
|
45
|
+
target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
46
|
+
target.click();
|
|
47
47
|
})()`);
|
|
48
48
|
}
|
|
49
49
|
async function waitForChatFilterTabSelected(page, label) {
|
|
50
50
|
const labelLiteral = JSON.stringify(label);
|
|
51
|
-
await page.waitForFunction(`(() => {
|
|
52
|
-
const targetText = ${labelLiteral};
|
|
53
|
-
const container = document.querySelector(".chat-message-filter-left");
|
|
54
|
-
if (!container) return false;
|
|
55
|
-
const norm = (v) => (v ?? "").replace(/\\s+/g, "");
|
|
56
|
-
const tabs = Array.from(container.querySelectorAll("span"));
|
|
57
|
-
const tab = tabs.find((el) => norm(el.textContent).includes(targetText));
|
|
58
|
-
if (!tab) return false;
|
|
59
|
-
const cls = String(tab.className || "");
|
|
60
|
-
const selectedByClass = /active|selected|current|checked/.test(cls);
|
|
61
|
-
const selectedByAria = tab.getAttribute("aria-selected") === "true";
|
|
62
|
-
const selectedByAncestor = !!tab.closest(".active, .selected, .current, .checked");
|
|
63
|
-
return selectedByClass || selectedByAria || selectedByAncestor;
|
|
51
|
+
await page.waitForFunction(`(() => {
|
|
52
|
+
const targetText = ${labelLiteral};
|
|
53
|
+
const container = document.querySelector(".chat-message-filter-left");
|
|
54
|
+
if (!container) return false;
|
|
55
|
+
const norm = (v) => (v ?? "").replace(/\\s+/g, "");
|
|
56
|
+
const tabs = Array.from(container.querySelectorAll("span"));
|
|
57
|
+
const tab = tabs.find((el) => norm(el.textContent).includes(targetText));
|
|
58
|
+
if (!tab) return false;
|
|
59
|
+
const cls = String(tab.className || "");
|
|
60
|
+
const selectedByClass = /active|selected|current|checked/.test(cls);
|
|
61
|
+
const selectedByAria = tab.getAttribute("aria-selected") === "true";
|
|
62
|
+
const selectedByAncestor = !!tab.closest(".active, .selected, .current, .checked");
|
|
63
|
+
return selectedByClass || selectedByAria || selectedByAncestor;
|
|
64
64
|
})()`, { timeout: 8_000 });
|
|
65
65
|
}
|
|
66
66
|
export async function ensureChatListReady(page, filter = 'all') {
|
|
@@ -69,11 +69,11 @@ export async function ensureChatListReady(page, filter = 'all') {
|
|
|
69
69
|
targetUrl: BOSS_CHAT_INDEX_URL,
|
|
70
70
|
matches: isBossChatIndexUrl,
|
|
71
71
|
});
|
|
72
|
-
await page.waitForFunction(`(() => {
|
|
73
|
-
const filter = document.querySelector(".chat-message-filter-left");
|
|
74
|
-
if (!filter) return false;
|
|
75
|
-
const tabs = Array.from(filter.querySelectorAll("span"));
|
|
76
|
-
return tabs.length >= 2;
|
|
72
|
+
await page.waitForFunction(`(() => {
|
|
73
|
+
const filter = document.querySelector(".chat-message-filter-left");
|
|
74
|
+
if (!filter) return false;
|
|
75
|
+
const tabs = Array.from(filter.querySelectorAll("span"));
|
|
76
|
+
return tabs.length >= 2;
|
|
77
77
|
})()`, { timeout: 15_000 });
|
|
78
78
|
const filterLabel = filter === 'unread' ? '未读' : '全部';
|
|
79
79
|
await clickChatFilterTab(page, filterLabel);
|
|
@@ -93,21 +93,21 @@ export async function runGetCandidateList(opts = {}) {
|
|
|
93
93
|
// 始终加载「全部」列表再在客户端按未读角标过滤:
|
|
94
94
|
// Boss v10718+ 改版后「未读」筛选 tab 点击失效/筛出空列表,不能依赖平台过滤。
|
|
95
95
|
await ensureChatListReady(page, 'all');
|
|
96
|
-
const items = (await page.evaluate(`(() => {
|
|
97
|
-
const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
|
|
98
|
-
return Array.from(document.querySelectorAll(".geek-item")).map((el) => {
|
|
99
|
-
const name = norm(el.querySelector(".geek-name")?.textContent);
|
|
100
|
-
const job = norm(el.querySelector(".source-job")?.textContent);
|
|
101
|
-
const time = norm(el.querySelector(".time")?.textContent);
|
|
102
|
-
const message = norm(el.querySelector(".push-text")?.textContent);
|
|
103
|
-
const badge = el.querySelector(".badge-count");
|
|
104
|
-
let unreadCount = 0;
|
|
105
|
-
if (badge) {
|
|
106
|
-
const digits = norm(badge.textContent).replace(/\\D/g, "");
|
|
107
|
-
if (digits) unreadCount = parseInt(digits, 10) || 0;
|
|
108
|
-
}
|
|
109
|
-
return { name, job, time, message, unreadCount };
|
|
110
|
-
});
|
|
96
|
+
const items = (await page.evaluate(`(() => {
|
|
97
|
+
const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
|
|
98
|
+
return Array.from(document.querySelectorAll(".geek-item")).map((el) => {
|
|
99
|
+
const name = norm(el.querySelector(".geek-name")?.textContent);
|
|
100
|
+
const job = norm(el.querySelector(".source-job")?.textContent);
|
|
101
|
+
const time = norm(el.querySelector(".time")?.textContent);
|
|
102
|
+
const message = norm(el.querySelector(".push-text")?.textContent);
|
|
103
|
+
const badge = el.querySelector(".badge-count");
|
|
104
|
+
let unreadCount = 0;
|
|
105
|
+
if (badge) {
|
|
106
|
+
const digits = norm(badge.textContent).replace(/\\D/g, "");
|
|
107
|
+
if (digits) unreadCount = parseInt(digits, 10) || 0;
|
|
108
|
+
}
|
|
109
|
+
return { name, job, time, message, unreadCount };
|
|
110
|
+
});
|
|
111
111
|
})()`));
|
|
112
112
|
const candidates = items.filter((it) => it.name);
|
|
113
113
|
const withUnread = candidates.filter((it) => it.unreadCount > 0).length;
|