@joohw/boss-cli 0.3.5 → 0.4.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.
- package/README.md +11 -14
- package/dist/browser/agent_operating_indicator.js +51 -51
- package/dist/browser/human_delay.d.ts +2 -0
- package/dist/browser/human_delay.d.ts.map +1 -1
- package/dist/browser/human_delay.js +4 -0
- package/dist/browser/human_delay.js.map +1 -1
- package/dist/cli/cliRouter.js +10 -10
- package/dist/cli/cliRouter.js.map +1 -1
- package/dist/common/auth.js +49 -49
- package/dist/common/boss_paywall_popup.js +87 -87
- package/dist/common/boss_session_lock.d.ts +2 -0
- package/dist/common/boss_session_lock.d.ts.map +1 -0
- package/dist/common/boss_session_lock.js +127 -0
- package/dist/common/boss_session_lock.js.map +1 -0
- package/dist/common/boss_session_page.d.ts +2 -2
- package/dist/common/boss_session_page.d.ts.map +1 -1
- package/dist/common/boss_session_page.js +66 -74
- package/dist/common/boss_session_page.js.map +1 -1
- package/dist/common/boss_sidebar_nav.js +25 -26
- package/dist/common/boss_sidebar_nav.js.map +1 -1
- package/dist/common/c_resume_capture.d.ts +1 -0
- package/dist/common/c_resume_capture.d.ts.map +1 -1
- package/dist/common/c_resume_capture.js +69 -32
- package/dist/common/c_resume_capture.js.map +1 -1
- package/dist/ocr/resume_ocr.d.ts.map +1 -1
- package/dist/ocr/resume_ocr.js +4 -1
- package/dist/ocr/resume_ocr.js.map +1 -1
- package/dist/toolset/action.d.ts.map +1 -1
- package/dist/toolset/action.js +11 -7
- package/dist/toolset/action.js.map +1 -1
- package/dist/toolset/chat.d.ts.map +1 -1
- package/dist/toolset/chat.js +246 -230
- package/dist/toolset/chat.js.map +1 -1
- package/dist/toolset/deep-search.d.ts +1 -1
- package/dist/toolset/deep-search.d.ts.map +1 -1
- package/dist/toolset/deep-search.js +291 -281
- package/dist/toolset/deep-search.js.map +1 -1
- package/dist/toolset/greet.d.ts.map +1 -1
- package/dist/toolset/greet.js +2 -7
- package/dist/toolset/greet.js.map +1 -1
- package/dist/toolset/jd.d.ts.map +1 -1
- package/dist/toolset/jd.js +134 -142
- package/dist/toolset/jd.js.map +1 -1
- package/dist/toolset/list.d.ts.map +1 -1
- package/dist/toolset/list.js +52 -45
- package/dist/toolset/list.js.map +1 -1
- package/dist/toolset/preview.d.ts.map +1 -1
- package/dist/toolset/preview.js +7 -9
- package/dist/toolset/preview.js.map +1 -1
- package/dist/toolset/recommend.d.ts +1 -1
- package/dist/toolset/recommend.d.ts.map +1 -1
- package/dist/toolset/recommend.js +223 -210
- package/dist/toolset/recommend.js.map +1 -1
- package/dist/toolset/send.d.ts.map +1 -1
- package/dist/toolset/send.js +6 -9
- package/dist/toolset/send.js.map +1 -1
- package/package.json +65 -63
- package/dist/browser/auth.d.ts +0 -33
- package/dist/browser/auth.d.ts.map +0 -1
- package/dist/browser/auth.js +0 -137
- package/dist/browser/auth.js.map +0 -1
- package/dist/browser/c_resume_capture.d.ts +0 -11
- package/dist/browser/c_resume_capture.d.ts.map +0 -1
- package/dist/browser/c_resume_capture.js +0 -76
- package/dist/browser/c_resume_capture.js.map +0 -1
- package/dist/browser/chat.d.ts +0 -7
- package/dist/browser/chat.d.ts.map +0 -1
- package/dist/browser/chat.js +0 -155
- package/dist/browser/chat.js.map +0 -1
- package/dist/browser/withLoggedInPage.d.ts +0 -7
- package/dist/browser/withLoggedInPage.d.ts.map +0 -1
- package/dist/browser/withLoggedInPage.js +0 -19
- package/dist/browser/withLoggedInPage.js.map +0 -1
- package/dist/facebook-cli/index.d.ts +0 -2
- package/dist/facebook-cli/index.d.ts.map +0 -1
- package/dist/facebook-cli/index.js +0 -2
- package/dist/facebook-cli/index.js.map +0 -1
- package/dist/toolset/c_resume_capture.d.ts +0 -11
- package/dist/toolset/c_resume_capture.d.ts.map +0 -1
- package/dist/toolset/c_resume_capture.js +0 -76
- package/dist/toolset/c_resume_capture.js.map +0 -1
- package/dist/toolset/chat_action.d.ts +0 -7
- package/dist/toolset/chat_action.d.ts.map +0 -1
- package/dist/toolset/chat_action.js +0 -355
- package/dist/toolset/chat_action.js.map +0 -1
- package/dist/toolset/list_candidates.d.ts +0 -4
- package/dist/toolset/list_candidates.d.ts.map +0 -1
- package/dist/toolset/list_candidates.js +0 -120
- package/dist/toolset/list_candidates.js.map +0 -1
- package/dist/toolset/list_positions.d.ts +0 -9
- package/dist/toolset/list_positions.d.ts.map +0 -1
- package/dist/toolset/list_positions.js +0 -41
- package/dist/toolset/list_positions.js.map +0 -1
- package/dist/toolset/open_chat.d.ts +0 -3
- package/dist/toolset/open_chat.d.ts.map +0 -1
- package/dist/toolset/open_chat.js +0 -423
- package/dist/toolset/open_chat.js.map +0 -1
- package/dist/toolset/recommend_greet.d.ts +0 -2
- package/dist/toolset/recommend_greet.d.ts.map +0 -1
- package/dist/toolset/recommend_greet.js +0 -27
- package/dist/toolset/recommend_greet.js.map +0 -1
- package/dist/toolset/search.d.ts +0 -24
- package/dist/toolset/search.d.ts.map +0 -1
- package/dist/toolset/search.js +0 -682
- package/dist/toolset/search.js.map +0 -1
- package/dist/toolset/send_message.d.ts +0 -12
- package/dist/toolset/send_message.d.ts.map +0 -1
- package/dist/toolset/send_message.js +0 -214
- package/dist/toolset/send_message.js.map +0 -1
- package/dist/toolset/skill.d.ts +0 -13
- package/dist/toolset/skill.d.ts.map +0 -1
- package/dist/toolset/skill.js +0 -85
- package/dist/toolset/skill.js.map +0 -1
package/dist/toolset/search.js
DELETED
|
@@ -1,682 +0,0 @@
|
|
|
1
|
-
import { createWaitManualLoginRequiredText, sleepRandom, withBossSessionPage, } from '../browser/index.js';
|
|
2
|
-
import { clickBossSidebarMenuToPath } from '../common/boss_sidebar_nav.js';
|
|
3
|
-
const BOSS_CHAT_AI_FORM_URL = 'https://www.zhipin.com/web/chat/aiform';
|
|
4
|
-
const AI_FORM_SETTLE_MS = { min: 1600, max: 2600 };
|
|
5
|
-
export function isBossChatAiFormUrl(url) {
|
|
6
|
-
try {
|
|
7
|
-
const u = new URL(url);
|
|
8
|
-
if (!u.hostname.includes('zhipin.com')) {
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
11
|
-
const p = u.pathname.replace(/\/+$/, '') || '/';
|
|
12
|
-
return p === '/web/chat/aiform';
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
async function waitForAiFormReady(page) {
|
|
19
|
-
await page.waitForFunction(`(() => {
|
|
20
|
-
const root = document.querySelector(".ai-form-left");
|
|
21
|
-
const submit = document.querySelector(".ai-form-match-footer .btn-ai-match-v2");
|
|
22
|
-
const selected = document.querySelector(".job-dropmenu-select .job-main-text");
|
|
23
|
-
if (!root || !submit || !selected) {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
const text = (selected.textContent ?? "").replace(/\\s+/g, " ").trim();
|
|
27
|
-
return text.length > 0;
|
|
28
|
-
})()`, { timeout: 15_000 });
|
|
29
|
-
}
|
|
30
|
-
export async function ensureInDeepSearchPage(page) {
|
|
31
|
-
if (!isBossChatAiFormUrl(page.url())) {
|
|
32
|
-
throw new Error('当前不在深度搜索页(/web/chat/aiform),请先通过侧栏进入「深度搜索」。');
|
|
33
|
-
}
|
|
34
|
-
await waitForAiFormReady(page);
|
|
35
|
-
}
|
|
36
|
-
async function clickAddConditionInSection(page, titleKeyword) {
|
|
37
|
-
const titleLiteral = JSON.stringify(titleKeyword);
|
|
38
|
-
const clicked = (await page.evaluate(`((titleKeyword) => {
|
|
39
|
-
const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
|
|
40
|
-
function findFormSectionByTitle(kw) {
|
|
41
|
-
const h3s = Array.from(document.querySelectorAll(".form-content .form-content-title-h3"));
|
|
42
|
-
const h3 = h3s.find((el) => norm(el.textContent).includes(kw));
|
|
43
|
-
return h3 ? h3.closest(".form-content") : null;
|
|
44
|
-
}
|
|
45
|
-
const section = findFormSectionByTitle(titleKeyword);
|
|
46
|
-
if (!section) return false;
|
|
47
|
-
const header = section.querySelector(".form-content-header");
|
|
48
|
-
const titleBtn = header?.querySelector(".form-content-title-btn");
|
|
49
|
-
if (!(titleBtn instanceof HTMLElement)) return false;
|
|
50
|
-
if (!norm(titleBtn.textContent).includes("添加条件")) return false;
|
|
51
|
-
titleBtn.scrollIntoView({ block: "center", inline: "nearest" });
|
|
52
|
-
titleBtn.click();
|
|
53
|
-
return true;
|
|
54
|
-
})(${titleLiteral})`));
|
|
55
|
-
if (!clicked) {
|
|
56
|
-
throw new Error(`未找到「${titleKeyword}」区域的「添加条件」。`);
|
|
57
|
-
}
|
|
58
|
-
await sleepRandom(280, 520);
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* 在区块内找到第一个「当前为空」的条件行并填入 `text`。
|
|
62
|
-
* Vue 结构:首行往往只有 `.form-content-list-item-title`(placeholder 属性),点击后才挂载 input / contenteditable;
|
|
63
|
-
* 必须在点击后等待下一拍再填,不能在同一次 evaluate 里连点带填。
|
|
64
|
-
*/
|
|
65
|
-
async function fillFirstEmptyRowInSection(page, titleKeyword, text) {
|
|
66
|
-
const clickedIndex = await page.evaluate((kw) => {
|
|
67
|
-
function norm(v) {
|
|
68
|
-
return (v ?? '').replace(/\s+/g, ' ').trim();
|
|
69
|
-
}
|
|
70
|
-
function findFormSectionByTitle(keyword) {
|
|
71
|
-
const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
|
|
72
|
-
const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(keyword));
|
|
73
|
-
const section = h3 ? h3.closest('.form-content') : null;
|
|
74
|
-
if (!section?.querySelector('.form-content-list')) {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
return section;
|
|
78
|
-
}
|
|
79
|
-
function rowDisplayText(row) {
|
|
80
|
-
const word = row.querySelector('.form-content-word');
|
|
81
|
-
if (word)
|
|
82
|
-
return norm(word.textContent ?? '');
|
|
83
|
-
const inp = row.querySelector('input, textarea');
|
|
84
|
-
if (inp instanceof HTMLInputElement || inp instanceof HTMLTextAreaElement) {
|
|
85
|
-
return norm(inp.value);
|
|
86
|
-
}
|
|
87
|
-
const ce = row.querySelector('[contenteditable="true"]');
|
|
88
|
-
if (ce)
|
|
89
|
-
return norm(ce.textContent ?? '');
|
|
90
|
-
const titleEl = row.querySelector('.form-content-list-item-title');
|
|
91
|
-
if (titleEl)
|
|
92
|
-
return norm(titleEl.textContent ?? '');
|
|
93
|
-
return '';
|
|
94
|
-
}
|
|
95
|
-
function looksLikePlaceholder(t) {
|
|
96
|
-
if (!t)
|
|
97
|
-
return true;
|
|
98
|
-
return /^(请输入|点击输入|请填写|添加)/.test(t);
|
|
99
|
-
}
|
|
100
|
-
function rowEmpty(row) {
|
|
101
|
-
const t = rowDisplayText(row);
|
|
102
|
-
if (!t)
|
|
103
|
-
return true;
|
|
104
|
-
return looksLikePlaceholder(t);
|
|
105
|
-
}
|
|
106
|
-
const section = findFormSectionByTitle(kw);
|
|
107
|
-
if (!section)
|
|
108
|
-
return -1;
|
|
109
|
-
const list = section.querySelector('.form-content-list');
|
|
110
|
-
if (!list)
|
|
111
|
-
return -1;
|
|
112
|
-
const items = list.querySelectorAll('.form-content-list-item');
|
|
113
|
-
for (let i = 0; i < items.length; i++) {
|
|
114
|
-
const row = items[i];
|
|
115
|
-
if (!rowEmpty(row))
|
|
116
|
-
continue;
|
|
117
|
-
const titleEl = row.querySelector('.form-content-list-item-title');
|
|
118
|
-
if (!(titleEl instanceof HTMLElement))
|
|
119
|
-
continue;
|
|
120
|
-
titleEl.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
121
|
-
titleEl.click();
|
|
122
|
-
return i;
|
|
123
|
-
}
|
|
124
|
-
return -1;
|
|
125
|
-
}, titleKeyword);
|
|
126
|
-
if (clickedIndex < 0) {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
await sleepRandom(450, 900);
|
|
130
|
-
let filled = await page.evaluate((kw, rowIndex, value) => {
|
|
131
|
-
function norm(v) {
|
|
132
|
-
return (v ?? '').replace(/\s+/g, ' ').trim();
|
|
133
|
-
}
|
|
134
|
-
function findFormSectionByTitle(keyword) {
|
|
135
|
-
const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
|
|
136
|
-
const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(keyword));
|
|
137
|
-
const section = h3 ? h3.closest('.form-content') : null;
|
|
138
|
-
if (!section?.querySelector('.form-content-list')) {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
return section;
|
|
142
|
-
}
|
|
143
|
-
function setNativeValue(el, v) {
|
|
144
|
-
const tracker = el._valueTracker;
|
|
145
|
-
if (tracker && typeof tracker.setValue === 'function') {
|
|
146
|
-
tracker.setValue('');
|
|
147
|
-
}
|
|
148
|
-
const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
149
|
-
const desc = Object.getOwnPropertyDescriptor(proto, 'value');
|
|
150
|
-
if (desc?.set) {
|
|
151
|
-
desc.set.call(el, v);
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
el.value = v;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
function dispatchInputChain(el) {
|
|
158
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
159
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
160
|
-
}
|
|
161
|
-
function fillContentEditable(el, v) {
|
|
162
|
-
el.focus();
|
|
163
|
-
try {
|
|
164
|
-
document.execCommand('selectAll', false);
|
|
165
|
-
document.execCommand('insertText', false, v);
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
el.textContent = v;
|
|
169
|
-
}
|
|
170
|
-
dispatchInputChain(el);
|
|
171
|
-
return true;
|
|
172
|
-
}
|
|
173
|
-
const section = findFormSectionByTitle(kw);
|
|
174
|
-
if (!section)
|
|
175
|
-
return false;
|
|
176
|
-
const list = section.querySelector('.form-content-list');
|
|
177
|
-
if (!list)
|
|
178
|
-
return false;
|
|
179
|
-
const items = list.querySelectorAll('.form-content-list-item');
|
|
180
|
-
const row = items[rowIndex];
|
|
181
|
-
if (!row)
|
|
182
|
-
return false;
|
|
183
|
-
row.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
184
|
-
const inp = row.querySelector('input, textarea');
|
|
185
|
-
if (inp instanceof HTMLInputElement || inp instanceof HTMLTextAreaElement) {
|
|
186
|
-
inp.focus();
|
|
187
|
-
setNativeValue(inp, value);
|
|
188
|
-
dispatchInputChain(inp);
|
|
189
|
-
const word = row.querySelector('.form-content-word');
|
|
190
|
-
if (word instanceof HTMLElement) {
|
|
191
|
-
word.textContent = value;
|
|
192
|
-
}
|
|
193
|
-
inp.blur();
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
|
-
const editables = row.querySelectorAll('[contenteditable]');
|
|
197
|
-
for (let j = 0; j < editables.length; j++) {
|
|
198
|
-
const el = editables[j];
|
|
199
|
-
if (el instanceof HTMLElement && el.isContentEditable) {
|
|
200
|
-
fillContentEditable(el, value);
|
|
201
|
-
el.blur();
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
const titleEl = row.querySelector('.form-content-list-item-title');
|
|
206
|
-
if (titleEl instanceof HTMLElement && titleEl.isContentEditable) {
|
|
207
|
-
fillContentEditable(titleEl, value);
|
|
208
|
-
titleEl.blur();
|
|
209
|
-
return true;
|
|
210
|
-
}
|
|
211
|
-
const wordOnly = row.querySelector('.form-content-word');
|
|
212
|
-
if (wordOnly instanceof HTMLElement) {
|
|
213
|
-
wordOnly.textContent = value;
|
|
214
|
-
dispatchInputChain(wordOnly);
|
|
215
|
-
return true;
|
|
216
|
-
}
|
|
217
|
-
return false;
|
|
218
|
-
}, titleKeyword, clickedIndex, text);
|
|
219
|
-
if (filled) {
|
|
220
|
-
await sleepRandom(200, 450);
|
|
221
|
-
return true;
|
|
222
|
-
}
|
|
223
|
-
await page.evaluate((kw, rowIndex) => {
|
|
224
|
-
function norm(v) {
|
|
225
|
-
return (v ?? '').replace(/\s+/g, ' ').trim();
|
|
226
|
-
}
|
|
227
|
-
function findFormSectionByTitle(keyword) {
|
|
228
|
-
const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
|
|
229
|
-
const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(keyword));
|
|
230
|
-
const section = h3 ? h3.closest('.form-content') : null;
|
|
231
|
-
if (!section?.querySelector('.form-content-list')) {
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
return section;
|
|
235
|
-
}
|
|
236
|
-
const section = findFormSectionByTitle(kw);
|
|
237
|
-
const list = section?.querySelector('.form-content-list');
|
|
238
|
-
const items = list?.querySelectorAll('.form-content-list-item');
|
|
239
|
-
const row = items?.[rowIndex];
|
|
240
|
-
const inp = row?.querySelector('input, textarea');
|
|
241
|
-
if (inp instanceof HTMLElement) {
|
|
242
|
-
inp.focus();
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
const titleEl = row?.querySelector('.form-content-list-item-title');
|
|
246
|
-
if (titleEl instanceof HTMLElement) {
|
|
247
|
-
titleEl.focus();
|
|
248
|
-
titleEl.click();
|
|
249
|
-
}
|
|
250
|
-
}, titleKeyword, clickedIndex);
|
|
251
|
-
await sleepRandom(120, 280);
|
|
252
|
-
await page.keyboard.type(text, { delay: 12 });
|
|
253
|
-
await page.keyboard.press('Tab');
|
|
254
|
-
await sleepRandom(120, 260);
|
|
255
|
-
return true;
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* 已处理条数从 0 计:每条待填条件先尝试填入第一个空输入框;若无空框则点一次「添加条件」再填。
|
|
259
|
-
*/
|
|
260
|
-
async function applyLinesToSection(page, titleKeyword, lines) {
|
|
261
|
-
let processed = 0;
|
|
262
|
-
for (const raw of lines) {
|
|
263
|
-
const text = raw.trim();
|
|
264
|
-
if (!text) {
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
let ok = await fillFirstEmptyRowInSection(page, titleKeyword, text);
|
|
268
|
-
if (!ok) {
|
|
269
|
-
await clickAddConditionInSection(page, titleKeyword);
|
|
270
|
-
ok = await fillFirstEmptyRowInSection(page, titleKeyword, text);
|
|
271
|
-
}
|
|
272
|
-
if (!ok) {
|
|
273
|
-
throw new Error(`「${titleKeyword}」第 ${processed + 1} 条条件无法填入(无可用空行或「添加条件」后仍无空行)。`);
|
|
274
|
-
}
|
|
275
|
-
processed += 1;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
async function applyAiFormRequirementLists(page, opts) {
|
|
279
|
-
if (opts.core !== undefined) {
|
|
280
|
-
await applyLinesToSection(page, '核心要求', opts.core);
|
|
281
|
-
}
|
|
282
|
-
if (opts.bonus !== undefined) {
|
|
283
|
-
await applyLinesToSection(page, '加分项', opts.bonus);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
async function readSearchFormSnapshot(page) {
|
|
287
|
-
return (await page.evaluate(`(() => {
|
|
288
|
-
const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
|
|
289
|
-
function itemLineText(item) {
|
|
290
|
-
const word = item.querySelector(".form-content-word");
|
|
291
|
-
const w = word ? norm(word.textContent) : "";
|
|
292
|
-
if (w) return w;
|
|
293
|
-
const inp = item.querySelector("input, textarea");
|
|
294
|
-
if (inp && norm(inp.value)) return norm(inp.value);
|
|
295
|
-
const ce = item.querySelector("[contenteditable='true']");
|
|
296
|
-
if (ce) return norm(ce.textContent);
|
|
297
|
-
const titleEl = item.querySelector(".form-content-list-item-title");
|
|
298
|
-
if (titleEl) return norm(titleEl.textContent);
|
|
299
|
-
return "";
|
|
300
|
-
}
|
|
301
|
-
const selectedJob = norm(document.querySelector(".job-dropmenu-select .job-main-text")?.textContent);
|
|
302
|
-
const sections = Array.from(document.querySelectorAll(".form-content"));
|
|
303
|
-
const coreRequirements = [];
|
|
304
|
-
const bonusRequirements = [];
|
|
305
|
-
for (const section of sections) {
|
|
306
|
-
const title = norm(section.querySelector(".form-content-header .form-content-title-h3")?.textContent);
|
|
307
|
-
const items = section.querySelectorAll(".form-content-list-item");
|
|
308
|
-
const words = Array.from(items)
|
|
309
|
-
.map((item) => itemLineText(item))
|
|
310
|
-
.filter(Boolean);
|
|
311
|
-
if (title.includes("核心要求")) {
|
|
312
|
-
coreRequirements.push(...words);
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
if (title.includes("加分项")) {
|
|
316
|
-
bonusRequirements.push(...words);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
const remainingCountText = norm(document.querySelector(".ai-form-match-footer-text-count")?.textContent);
|
|
320
|
-
return {
|
|
321
|
-
selectedJob,
|
|
322
|
-
coreRequirements,
|
|
323
|
-
bonusRequirements,
|
|
324
|
-
remainingCountText,
|
|
325
|
-
};
|
|
326
|
-
})()`));
|
|
327
|
-
}
|
|
328
|
-
async function selectAiFormJob(page, keyword) {
|
|
329
|
-
const kw = keyword.trim();
|
|
330
|
-
if (!kw) {
|
|
331
|
-
throw new Error('岗位关键字不能为空。');
|
|
332
|
-
}
|
|
333
|
-
const kwLiteral = JSON.stringify(kw);
|
|
334
|
-
const opened = (await page.evaluate(`(() => {
|
|
335
|
-
const host = document.querySelector(".job-dropmenu-select");
|
|
336
|
-
if (!(host instanceof HTMLElement)) return false;
|
|
337
|
-
host.scrollIntoView({ block: "center", inline: "nearest" });
|
|
338
|
-
host.click();
|
|
339
|
-
return true;
|
|
340
|
-
})()`));
|
|
341
|
-
if (!opened) {
|
|
342
|
-
throw new Error('未找到深度搜索页岗位下拉(.job-dropmenu-select)。');
|
|
343
|
-
}
|
|
344
|
-
await sleepRandom(450, 900);
|
|
345
|
-
const searched = (await page.evaluate(`(() => {
|
|
346
|
-
const kw = ${kwLiteral};
|
|
347
|
-
const inputs = Array.from(
|
|
348
|
-
document.querySelectorAll(
|
|
349
|
-
".ui-dropmenu-list input[type='text'], .ui-dropmenu-list input, .job-dropmenu-options .chat-job-search, .job-dropmenu-popover .chat-job-search, .top-chat-search .chat-job-search, input.chat-job-search",
|
|
350
|
-
),
|
|
351
|
-
);
|
|
352
|
-
const input = inputs.find((el) => {
|
|
353
|
-
if (!(el instanceof HTMLInputElement)) return false;
|
|
354
|
-
const r = el.getBoundingClientRect();
|
|
355
|
-
return r.width > 0 && r.height > 0;
|
|
356
|
-
});
|
|
357
|
-
if (!input) return false;
|
|
358
|
-
input.focus();
|
|
359
|
-
input.value = kw;
|
|
360
|
-
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
361
|
-
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
362
|
-
return true;
|
|
363
|
-
})()`));
|
|
364
|
-
if (searched) {
|
|
365
|
-
await sleepRandom(520, 1080);
|
|
366
|
-
}
|
|
367
|
-
else {
|
|
368
|
-
await sleepRandom(200, 450);
|
|
369
|
-
}
|
|
370
|
-
const picked = (await page.evaluate(`(() => {
|
|
371
|
-
const kw = ${kwLiteral};
|
|
372
|
-
const norm = (v) => (v ?? "").replace(/\\s+/g, "").trim().toLowerCase();
|
|
373
|
-
const rows = Array.from(
|
|
374
|
-
document.querySelectorAll(
|
|
375
|
-
".job-dropmenu-list .job-dropmenu-item, .job-dropmenu-options .job-list .job-item, .job-dropmenu-popover .job-list .job-item, .job-dropmenu-options .job-item",
|
|
376
|
-
),
|
|
377
|
-
);
|
|
378
|
-
if (rows.length === 0) return { ok: false, reason: "empty" };
|
|
379
|
-
const target = rows.find((el) => {
|
|
380
|
-
const label = norm(
|
|
381
|
-
el.querySelector(".job-option-text, .label")?.textContent || el.textContent || "",
|
|
382
|
-
);
|
|
383
|
-
return label.includes(norm(kw));
|
|
384
|
-
});
|
|
385
|
-
if (!(target instanceof HTMLElement)) return { ok: false, reason: "not_found" };
|
|
386
|
-
const label = (
|
|
387
|
-
target.querySelector(".job-option-text, .label")?.textContent ?? target.textContent ?? ""
|
|
388
|
-
)
|
|
389
|
-
.replace(/\\s+/g, " ")
|
|
390
|
-
.trim();
|
|
391
|
-
target.scrollIntoView({ block: "center", inline: "nearest" });
|
|
392
|
-
target.click();
|
|
393
|
-
return { ok: true, label };
|
|
394
|
-
})()`));
|
|
395
|
-
if (!picked.ok) {
|
|
396
|
-
throw new Error(`未找到匹配岗位「${kw}」。`);
|
|
397
|
-
}
|
|
398
|
-
await sleepRandom(900, 1500);
|
|
399
|
-
return picked.label ?? kw;
|
|
400
|
-
}
|
|
401
|
-
export async function readDeepSearchGeekList(page) {
|
|
402
|
-
return (await page.evaluate(`(() => {
|
|
403
|
-
const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
|
|
404
|
-
const items = Array.from(
|
|
405
|
-
document.querySelectorAll(".geeks-box .geek-card-item, .geek-card-list .geek-card-item"),
|
|
406
|
-
);
|
|
407
|
-
return items
|
|
408
|
-
.map((item) => {
|
|
409
|
-
const chatLabel = norm(item.querySelector(".geek-chat")?.textContent);
|
|
410
|
-
if (chatLabel.includes("继续沟通")) {
|
|
411
|
-
return null;
|
|
412
|
-
}
|
|
413
|
-
const name = norm(item.querySelector(".geek-name")?.textContent);
|
|
414
|
-
const splits = Array.from(item.querySelectorAll(".geek-exp .split"))
|
|
415
|
-
.map((el) => norm(el.getAttribute("title") || el.textContent || ""))
|
|
416
|
-
.filter(Boolean);
|
|
417
|
-
const meta = splits.join(" · ");
|
|
418
|
-
const work = norm(item.querySelector(".geek-works span")?.textContent);
|
|
419
|
-
const edu = norm(item.querySelector(".geek-edus span")?.textContent);
|
|
420
|
-
const recEl = item.querySelector(".geek-recommend-text");
|
|
421
|
-
let reason = "";
|
|
422
|
-
if (recEl) {
|
|
423
|
-
reason = norm(recEl.textContent).replace(/^推荐理由\\s*/, "").trim();
|
|
424
|
-
}
|
|
425
|
-
return { name, meta, work, edu, reason };
|
|
426
|
-
})
|
|
427
|
-
.filter((x) => x !== null);
|
|
428
|
-
})()`));
|
|
429
|
-
}
|
|
430
|
-
async function waitForAiMatchSettled(page) {
|
|
431
|
-
await page
|
|
432
|
-
.waitForFunction(`(() =>
|
|
433
|
-
(document.querySelector(".ai-form-match-footer .light-flow-btn-content .text")?.textContent ?? "").includes(
|
|
434
|
-
"停止匹配",
|
|
435
|
-
))()`, { timeout: 10_000 })
|
|
436
|
-
.catch(() => { });
|
|
437
|
-
await page.waitForFunction(`(() =>
|
|
438
|
-
!(document.querySelector(".ai-form-match-footer .light-flow-btn-content .text")?.textContent ?? "").includes(
|
|
439
|
-
"停止匹配",
|
|
440
|
-
))()`, { timeout: 120_000 });
|
|
441
|
-
await sleepRandom(500, 900);
|
|
442
|
-
}
|
|
443
|
-
export function renderGeekListSection(title, items) {
|
|
444
|
-
const lines = [title, `共 ${items.length} 人`, ''];
|
|
445
|
-
items.forEach((g, i) => {
|
|
446
|
-
const n = i + 1;
|
|
447
|
-
lines.push(`${n}. ${g.name || '(无姓名)'}`);
|
|
448
|
-
if (g.meta) {
|
|
449
|
-
lines.push(` 概要:${g.meta}`);
|
|
450
|
-
}
|
|
451
|
-
if (g.work) {
|
|
452
|
-
lines.push(` 经历:${g.work}`);
|
|
453
|
-
}
|
|
454
|
-
if (g.edu) {
|
|
455
|
-
lines.push(` 教育:${g.edu}`);
|
|
456
|
-
}
|
|
457
|
-
if (g.reason) {
|
|
458
|
-
lines.push(` 推荐:${g.reason}`);
|
|
459
|
-
}
|
|
460
|
-
lines.push('');
|
|
461
|
-
});
|
|
462
|
-
return lines.join('\n').trimEnd();
|
|
463
|
-
}
|
|
464
|
-
export async function clickGreetDeepSearch(page, target) {
|
|
465
|
-
const targetLiteral = JSON.stringify(target.trim());
|
|
466
|
-
const result = (await page.evaluate(`(() => {
|
|
467
|
-
const raw = ${targetLiteral};
|
|
468
|
-
const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
|
|
469
|
-
const allCards = Array.from(
|
|
470
|
-
document.querySelectorAll(".geeks-box .geek-card-item, .geek-card-list .geek-card-item"),
|
|
471
|
-
);
|
|
472
|
-
if (allCards.length === 0) {
|
|
473
|
-
return { kind: "empty" };
|
|
474
|
-
}
|
|
475
|
-
const cards = allCards.filter((item) => {
|
|
476
|
-
const chatLabel = norm(item.querySelector(".geek-chat")?.textContent);
|
|
477
|
-
return !chatLabel.includes("继续沟通");
|
|
478
|
-
});
|
|
479
|
-
if (cards.length === 0) {
|
|
480
|
-
return { kind: "all_continue" };
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const idxNum = Number.parseInt(raw, 10);
|
|
484
|
-
let targetCard = null;
|
|
485
|
-
if (Number.isFinite(idxNum) && idxNum >= 1 && idxNum <= cards.length) {
|
|
486
|
-
targetCard = cards[idxNum - 1];
|
|
487
|
-
} else {
|
|
488
|
-
targetCard =
|
|
489
|
-
cards.find((item) => {
|
|
490
|
-
const name = norm(item.querySelector(".geek-name")?.textContent);
|
|
491
|
-
return name === raw || name.includes(raw);
|
|
492
|
-
}) ?? null;
|
|
493
|
-
}
|
|
494
|
-
if (!targetCard) {
|
|
495
|
-
return { kind: "not_found", target: raw };
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
const name = norm(targetCard.querySelector(".geek-name")?.textContent);
|
|
499
|
-
const btn =
|
|
500
|
-
targetCard.querySelector(".geek-chat .btn-ai-v2") ||
|
|
501
|
-
targetCard.querySelector(".geek-chat span.btn-ai-v2") ||
|
|
502
|
-
targetCard.querySelector(".geek-chat span[class*='btn-ai']");
|
|
503
|
-
if (!(btn instanceof HTMLElement)) {
|
|
504
|
-
return { kind: "no_btn", name };
|
|
505
|
-
}
|
|
506
|
-
const label = norm(btn.textContent);
|
|
507
|
-
if (!label.includes("打招呼")) {
|
|
508
|
-
return { kind: "not_greet", name, label };
|
|
509
|
-
}
|
|
510
|
-
const cls = btn.className ?? "";
|
|
511
|
-
const disabled = /disabled|forbid|ban/i.test(cls) || btn.getAttribute("disabled") !== null;
|
|
512
|
-
if (disabled) {
|
|
513
|
-
return { kind: "disabled", name };
|
|
514
|
-
}
|
|
515
|
-
btn.scrollIntoView({ block: "center", inline: "nearest" });
|
|
516
|
-
btn.click();
|
|
517
|
-
return { kind: "clicked", name };
|
|
518
|
-
})()`));
|
|
519
|
-
switch (result.kind) {
|
|
520
|
-
case 'empty':
|
|
521
|
-
throw new Error('深度搜索暂无候选人列表,请先执行「立即匹配」或使用 boss deep-search <岗位> 触发匹配。');
|
|
522
|
-
case 'all_continue':
|
|
523
|
-
throw new Error('当前列表均为「继续沟通」状态,已无待打招呼人选(与 boss deep-search 列表展示一致)。');
|
|
524
|
-
case 'not_found':
|
|
525
|
-
throw new Error(`未在可打招呼的深度搜索列表中找到目标:${result.target}(「继续沟通」人选已排除,请用 boss deep-search 核对序号与姓名)。`);
|
|
526
|
-
case 'no_btn':
|
|
527
|
-
throw new Error(`候选人 ${result.name} 缺少「打招呼」按钮,无法执行。`);
|
|
528
|
-
case 'not_greet':
|
|
529
|
-
throw new Error(`候选人 ${result.name} 当前按钮为「${result.label}」,无法执行打招呼。`);
|
|
530
|
-
case 'disabled':
|
|
531
|
-
throw new Error(`候选人 ${result.name} 的打招呼不可用(可能已打过招呼)。`);
|
|
532
|
-
case 'clicked':
|
|
533
|
-
return { message: `已对 ${result.name} 在深度搜索页点击「打招呼」。` };
|
|
534
|
-
default: {
|
|
535
|
-
const _x = result;
|
|
536
|
-
throw new Error(`未知结果:${String(_x)}`);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
async function clickMatchNow(page) {
|
|
541
|
-
const clicked = (await page.evaluate(`(() => {
|
|
542
|
-
function norm(v) {
|
|
543
|
-
return (v ?? "").replace(/\\s+/g, "").trim();
|
|
544
|
-
}
|
|
545
|
-
function isVisible(el) {
|
|
546
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
547
|
-
const st = window.getComputedStyle(el);
|
|
548
|
-
if (st.display === "none" || st.visibility === "hidden") return false;
|
|
549
|
-
const r = el.getBoundingClientRect();
|
|
550
|
-
return r.width > 0 && r.height > 0;
|
|
551
|
-
}
|
|
552
|
-
const buttons = Array.from(
|
|
553
|
-
document.querySelectorAll(".ai-form-match-footer .btn-ai-match-v2, .ai-form-match-footer-btn .btn-ai-common, .ai-form-match-footer-btn .light-flow-btn-content"),
|
|
554
|
-
).filter((el) => isVisible(el));
|
|
555
|
-
if (buttons.length === 0) {
|
|
556
|
-
return false;
|
|
557
|
-
}
|
|
558
|
-
const preferred = buttons.find((el) => norm(el.textContent).includes("立即匹配")) ?? buttons[0];
|
|
559
|
-
if (!(preferred instanceof HTMLElement)) {
|
|
560
|
-
return false;
|
|
561
|
-
}
|
|
562
|
-
preferred.scrollIntoView({ block: "center", inline: "nearest" });
|
|
563
|
-
preferred.click();
|
|
564
|
-
return true;
|
|
565
|
-
})()`));
|
|
566
|
-
if (!clicked) {
|
|
567
|
-
throw new Error('未找到“立即匹配”按钮,无法执行深度搜索。');
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
function renderFormSnapshotOnly(snap) {
|
|
571
|
-
const core = snap.coreRequirements.length > 0 ? snap.coreRequirements.join('|') : '(空)';
|
|
572
|
-
const bonus = snap.bonusRequirements.length > 0 ? snap.bonusRequirements.join('|') : '(空)';
|
|
573
|
-
return [
|
|
574
|
-
'已更新深度搜索表单(未触发「立即匹配」)。',
|
|
575
|
-
`职位:${snap.selectedJob || '未知职位'}`,
|
|
576
|
-
`核心要求(${snap.coreRequirements.length}):${core}`,
|
|
577
|
-
`加分项(${snap.bonusRequirements.length}):${bonus}`,
|
|
578
|
-
`今日匹配剩余:${snap.remainingCountText || '未知'}`,
|
|
579
|
-
`来源页面:${BOSS_CHAT_AI_FORM_URL}`,
|
|
580
|
-
].join('\n');
|
|
581
|
-
}
|
|
582
|
-
function renderMatchSummaryText(before, after) {
|
|
583
|
-
const core = before.coreRequirements.length > 0 ? before.coreRequirements.join('|') : '(空)';
|
|
584
|
-
const bonus = before.bonusRequirements.length > 0 ? before.bonusRequirements.join('|') : '(空)';
|
|
585
|
-
const remain = before.remainingCountText || '未知';
|
|
586
|
-
const remainAfter = after.remainingCountText || '未知';
|
|
587
|
-
const remainLine = remain === remainAfter ? `今日匹配剩余:${remain}` : `今日匹配剩余:${remain} -> ${remainAfter}`;
|
|
588
|
-
return [
|
|
589
|
-
'已进入深度搜索并触发“立即匹配”。',
|
|
590
|
-
`职位:${before.selectedJob || '未知职位'}`,
|
|
591
|
-
`核心要求(${before.coreRequirements.length}):${core}`,
|
|
592
|
-
`加分项(${before.bonusRequirements.length}):${bonus}`,
|
|
593
|
-
remainLine,
|
|
594
|
-
`来源页面:${BOSS_CHAT_AI_FORM_URL}`,
|
|
595
|
-
].join('\n');
|
|
596
|
-
}
|
|
597
|
-
export async function runBossSearchSet(opts) {
|
|
598
|
-
const jobKeyword = opts.jobKeyword?.trim();
|
|
599
|
-
const coreReq = opts.coreRequirements;
|
|
600
|
-
const bonusReq = opts.bonusRequirements;
|
|
601
|
-
const hasFormEdit = coreReq !== undefined || bonusReq !== undefined;
|
|
602
|
-
if (!jobKeyword && !hasFormEdit) {
|
|
603
|
-
throw new Error('请至少指定 --job/-j、--core/-c 或 --bonus/-b 之一。');
|
|
604
|
-
}
|
|
605
|
-
try {
|
|
606
|
-
return await withBossSessionPage(async (page) => {
|
|
607
|
-
const currentUrl = page.url();
|
|
608
|
-
if (!isBossChatAiFormUrl(currentUrl)) {
|
|
609
|
-
await clickBossSidebarMenuToPath(page, '深度搜索', '/web/chat/aiform');
|
|
610
|
-
await sleepRandom(AI_FORM_SETTLE_MS.min, AI_FORM_SETTLE_MS.max);
|
|
611
|
-
}
|
|
612
|
-
if (!isBossChatAiFormUrl(page.url())) {
|
|
613
|
-
throw new Error('通过侧边栏“深度搜索”进入页面失败,请确认已登录并可访问 /web/chat/aiform。');
|
|
614
|
-
}
|
|
615
|
-
await ensureInDeepSearchPage(page);
|
|
616
|
-
if (jobKeyword) {
|
|
617
|
-
await selectAiFormJob(page, jobKeyword);
|
|
618
|
-
await ensureInDeepSearchPage(page);
|
|
619
|
-
if (hasFormEdit) {
|
|
620
|
-
await sleepRandom(500, 900);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
if (hasFormEdit) {
|
|
624
|
-
await applyAiFormRequirementLists(page, {
|
|
625
|
-
core: coreReq,
|
|
626
|
-
bonus: bonusReq,
|
|
627
|
-
});
|
|
628
|
-
await ensureInDeepSearchPage(page);
|
|
629
|
-
}
|
|
630
|
-
const snap = await readSearchFormSnapshot(page);
|
|
631
|
-
return renderFormSnapshotOnly(snap);
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
catch (e) {
|
|
635
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
636
|
-
if (e instanceof Error && e.message.includes('浏览器会话尚未初始化')) {
|
|
637
|
-
throw new Error(createWaitManualLoginRequiredText('设置深度搜索条件'));
|
|
638
|
-
}
|
|
639
|
-
console.error(`[boss-cli] boss_search_set error: ${message}`);
|
|
640
|
-
throw new Error(`设置深度搜索条件失败:${message}`);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
export async function runBossSearch(opts = {}) {
|
|
644
|
-
const jobKeyword = opts.jobKeyword?.trim();
|
|
645
|
-
try {
|
|
646
|
-
return await withBossSessionPage(async (page) => {
|
|
647
|
-
const currentUrl = page.url();
|
|
648
|
-
if (!isBossChatAiFormUrl(currentUrl)) {
|
|
649
|
-
await clickBossSidebarMenuToPath(page, '深度搜索', '/web/chat/aiform');
|
|
650
|
-
await sleepRandom(AI_FORM_SETTLE_MS.min, AI_FORM_SETTLE_MS.max);
|
|
651
|
-
}
|
|
652
|
-
if (!isBossChatAiFormUrl(page.url())) {
|
|
653
|
-
throw new Error('通过侧边栏“深度搜索”进入页面失败,请确认已登录并可访问 /web/chat/aiform。');
|
|
654
|
-
}
|
|
655
|
-
await ensureInDeepSearchPage(page);
|
|
656
|
-
if (!jobKeyword) {
|
|
657
|
-
const geeks = await readDeepSearchGeekList(page);
|
|
658
|
-
return renderGeekListSection('深度搜索当前匹配结果(未触发立即匹配)', geeks);
|
|
659
|
-
}
|
|
660
|
-
await selectAiFormJob(page, jobKeyword);
|
|
661
|
-
await ensureInDeepSearchPage(page);
|
|
662
|
-
const before = await readSearchFormSnapshot(page);
|
|
663
|
-
await clickMatchNow(page);
|
|
664
|
-
await waitForAiMatchSettled(page);
|
|
665
|
-
await ensureInDeepSearchPage(page);
|
|
666
|
-
const after = await readSearchFormSnapshot(page);
|
|
667
|
-
await sleepRandom(600, 1200);
|
|
668
|
-
const geeks = await readDeepSearchGeekList(page);
|
|
669
|
-
const summary = renderMatchSummaryText(before, after);
|
|
670
|
-
return [summary, '', renderGeekListSection('匹配结果列表', geeks)].join('\n');
|
|
671
|
-
});
|
|
672
|
-
}
|
|
673
|
-
catch (e) {
|
|
674
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
675
|
-
if (e instanceof Error && e.message.includes('浏览器会话尚未初始化')) {
|
|
676
|
-
throw new Error(createWaitManualLoginRequiredText('执行深度搜索'));
|
|
677
|
-
}
|
|
678
|
-
console.error(`[boss-cli] boss_search error: ${message}`);
|
|
679
|
-
throw new Error(`执行深度搜索失败:${message}`);
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
//# sourceMappingURL=search.js.map
|