@joohw/boss-cli 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/AGENTS.md +25 -25
  2. package/README.md +34 -19
  3. package/dist/browser/agent_operating_indicator.d.ts +9 -0
  4. package/dist/browser/agent_operating_indicator.d.ts.map +1 -0
  5. package/dist/browser/agent_operating_indicator.js +92 -0
  6. package/dist/browser/agent_operating_indicator.js.map +1 -0
  7. package/dist/browser/c_resume_capture.d.ts +11 -0
  8. package/dist/browser/c_resume_capture.d.ts.map +1 -0
  9. package/dist/browser/c_resume_capture.js +76 -0
  10. package/dist/browser/c_resume_capture.js.map +1 -0
  11. package/dist/browser/chat.d.ts +3 -3
  12. package/dist/browser/chat.d.ts.map +1 -1
  13. package/dist/browser/chat.js +20 -4
  14. package/dist/browser/chat.js.map +1 -1
  15. package/dist/browser/human_delay.d.ts +5 -0
  16. package/dist/browser/human_delay.d.ts.map +1 -1
  17. package/dist/browser/human_delay.js +5 -0
  18. package/dist/browser/human_delay.js.map +1 -1
  19. package/dist/browser/index.d.ts +3 -1
  20. package/dist/browser/index.d.ts.map +1 -1
  21. package/dist/browser/index.js +3 -1
  22. package/dist/browser/index.js.map +1 -1
  23. package/dist/browser/viewport_temp.d.ts +15 -0
  24. package/dist/browser/viewport_temp.d.ts.map +1 -0
  25. package/dist/browser/viewport_temp.js +78 -0
  26. package/dist/browser/viewport_temp.js.map +1 -0
  27. package/dist/cli/banner.js +1 -1
  28. package/dist/cli/cliRouter.d.ts.map +1 -1
  29. package/dist/cli/cliRouter.js +32 -13
  30. package/dist/cli/cliRouter.js.map +1 -1
  31. package/dist/common/boss_paywall_popup.d.ts +16 -0
  32. package/dist/common/boss_paywall_popup.d.ts.map +1 -0
  33. package/dist/common/boss_paywall_popup.js +140 -0
  34. package/dist/common/boss_paywall_popup.js.map +1 -0
  35. package/dist/common/boss_sidebar_nav.d.ts +6 -0
  36. package/dist/common/boss_sidebar_nav.d.ts.map +1 -0
  37. package/dist/common/boss_sidebar_nav.js +37 -0
  38. package/dist/common/boss_sidebar_nav.js.map +1 -0
  39. package/dist/common/c_resume_capture.d.ts +11 -0
  40. package/dist/common/c_resume_capture.d.ts.map +1 -0
  41. package/dist/common/c_resume_capture.js +76 -0
  42. package/dist/common/c_resume_capture.js.map +1 -0
  43. package/dist/toolset/action.d.ts +1 -1
  44. package/dist/toolset/action.d.ts.map +1 -1
  45. package/dist/toolset/action.js +192 -152
  46. package/dist/toolset/action.js.map +1 -1
  47. package/dist/toolset/c_resume_capture.d.ts +11 -0
  48. package/dist/toolset/c_resume_capture.d.ts.map +1 -0
  49. package/dist/toolset/c_resume_capture.js +76 -0
  50. package/dist/toolset/c_resume_capture.js.map +1 -0
  51. package/dist/toolset/chat.d.ts.map +1 -1
  52. package/dist/toolset/chat.js +4 -3
  53. package/dist/toolset/chat.js.map +1 -1
  54. package/dist/toolset/deep-search.d.ts +24 -0
  55. package/dist/toolset/deep-search.d.ts.map +1 -0
  56. package/dist/toolset/deep-search.js +1039 -0
  57. package/dist/toolset/deep-search.js.map +1 -0
  58. package/dist/toolset/greet.d.ts.map +1 -1
  59. package/dist/toolset/greet.js +30 -10
  60. package/dist/toolset/greet.js.map +1 -1
  61. package/dist/toolset/index.d.ts +13 -1
  62. package/dist/toolset/index.d.ts.map +1 -1
  63. package/dist/toolset/index.js +13 -7
  64. package/dist/toolset/index.js.map +1 -1
  65. package/dist/toolset/jd.d.ts.map +1 -1
  66. package/dist/toolset/jd.js +4 -35
  67. package/dist/toolset/jd.js.map +1 -1
  68. package/dist/toolset/list.d.ts +6 -0
  69. package/dist/toolset/list.d.ts.map +1 -1
  70. package/dist/toolset/list.js +36 -60
  71. package/dist/toolset/list.js.map +1 -1
  72. package/dist/toolset/login.d.ts.map +1 -1
  73. package/dist/toolset/login.js +2 -5
  74. package/dist/toolset/login.js.map +1 -1
  75. package/dist/toolset/recommend.d.ts +12 -0
  76. package/dist/toolset/recommend.d.ts.map +1 -1
  77. package/dist/toolset/recommend.js +144 -45
  78. package/dist/toolset/recommend.js.map +1 -1
  79. package/dist/toolset/search.d.ts +23 -1
  80. package/dist/toolset/search.d.ts.map +1 -1
  81. package/dist/toolset/search.js +556 -48
  82. package/dist/toolset/search.js.map +1 -1
  83. package/dist/toolset/send.js +2 -2
  84. package/dist/toolset/send.js.map +1 -1
  85. package/package.json +2 -2
@@ -0,0 +1,1039 @@
1
+ import process from 'node:process';
2
+ import { createWaitManualLoginRequiredText, sleepRandom, withBossSessionPage, } from '../browser/index.js';
3
+ import { clickBossSidebarMenuToPath } from '../common/boss_sidebar_nav.js';
4
+ const BOSS_CHAT_AI_FORM_URL = 'https://www.zhipin.com/web/chat/aiform';
5
+ const AI_FORM_SETTLE_MS = { min: 1600, max: 2600 };
6
+ export function isBossChatAiFormUrl(url) {
7
+ try {
8
+ const u = new URL(url);
9
+ if (!u.hostname.includes('zhipin.com')) {
10
+ return false;
11
+ }
12
+ const p = u.pathname.replace(/\/+$/, '') || '/';
13
+ return p === '/web/chat/aiform';
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ async function waitForAiFormReady(page) {
20
+ await page.waitForFunction(`(() => {
21
+ const root = document.querySelector(".ai-form-left");
22
+ const submit = document.querySelector(".ai-form-match-footer .btn-ai-match-v2");
23
+ const selected = document.querySelector(".job-dropmenu-select .job-main-text");
24
+ if (!root || !submit || !selected) {
25
+ return false;
26
+ }
27
+ const text = (selected.textContent ?? "").replace(/\\s+/g, " ").trim();
28
+ return text.length > 0;
29
+ })()`, { timeout: 15_000 });
30
+ }
31
+ export async function ensureInDeepSearchPage(page) {
32
+ if (!isBossChatAiFormUrl(page.url())) {
33
+ throw new Error('当前不在深度搜索页(/web/chat/aiform),请先通过侧栏进入「深度搜索」。');
34
+ }
35
+ await waitForAiFormReady(page);
36
+ }
37
+ async function clickAddConditionInSection(page, titleKeyword) {
38
+ const titleLiteral = JSON.stringify(titleKeyword);
39
+ const clicked = (await page.evaluate(`((titleKeyword) => {
40
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
41
+ function findFormSectionByTitle(kw) {
42
+ const h3s = Array.from(document.querySelectorAll(".form-content .form-content-title-h3"));
43
+ const h3 = h3s.find((el) => norm(el.textContent).includes(kw));
44
+ return h3 ? h3.closest(".form-content") : null;
45
+ }
46
+ const section = findFormSectionByTitle(titleKeyword);
47
+ if (!section) return false;
48
+ const header = section.querySelector(".form-content-header");
49
+ const titleBtn = header?.querySelector(".form-content-title-btn");
50
+ if (!(titleBtn instanceof HTMLElement)) return false;
51
+ if (!norm(titleBtn.textContent).includes("添加条件")) return false;
52
+ titleBtn.scrollIntoView({ block: "center", inline: "nearest" });
53
+ titleBtn.click();
54
+ return true;
55
+ })(${titleLiteral})`));
56
+ if (!clicked) {
57
+ throw new Error(`未找到「${titleKeyword}」区域的「添加条件」。`);
58
+ }
59
+ await sleepRandom(280, 520);
60
+ }
61
+ /** 等待指定区块内第 idx 行已出现在 DOM 中(岗位切换等可能导致列表晚于表单壳渲染)。 */
62
+ async function waitForRequirementRowPresent(page, titleKeyword, rowIndex, timeoutMs = 5000) {
63
+ try {
64
+ await page.waitForFunction((kw, idx) => {
65
+ function norm(v) {
66
+ return (v ?? '').replace(/\s+/g, ' ').trim();
67
+ }
68
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
69
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
70
+ const section = h3 ? h3.closest('.form-content') : null;
71
+ if (!section)
72
+ return false;
73
+ const list = section.querySelector('.form-content-list');
74
+ if (!list)
75
+ return false;
76
+ const items = list.querySelectorAll('.form-content-list-item');
77
+ if (idx < 0 || idx >= items.length)
78
+ return false;
79
+ return !!items[idx].querySelector('.form-content-list-item-title');
80
+ }, { timeout: timeoutMs }, titleKeyword, rowIndex);
81
+ return true;
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
87
+ /** 深度搜索条件行:Boss 用 `.form-content-word` 展示文案(挂载很快,超时不宜过长)。 */
88
+ async function waitForFormContentWordInRow(page, titleKeyword, rowIndex, timeoutMs = 1000) {
89
+ try {
90
+ await page.waitForFunction((kw, idx) => {
91
+ function norm(v) {
92
+ return (v ?? '').replace(/\s+/g, ' ').trim();
93
+ }
94
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
95
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
96
+ const section = h3 ? h3.closest('.form-content') : null;
97
+ if (!section)
98
+ return false;
99
+ const list = section.querySelector('.form-content-list');
100
+ if (!list)
101
+ return false;
102
+ const items = list.querySelectorAll('.form-content-list-item');
103
+ if (idx < 0 || idx >= items.length)
104
+ return false;
105
+ return !!items[idx].querySelector('.form-content-word');
106
+ }, { timeout: timeoutMs }, titleKeyword, rowIndex);
107
+ return true;
108
+ }
109
+ catch {
110
+ return false;
111
+ }
112
+ }
113
+ function normFormText(s) {
114
+ return s.replace(/\s+/g, ' ').trim();
115
+ }
116
+ /** 校验用:优先读 `.auto-resize-textarea-wrapper` 内 textarea/input,再读其它 input、最后 `.form-content-word`。 */
117
+ async function readRowRequirementShownText(page, titleKeyword, rowIndex) {
118
+ return page.evaluate((kw, idx) => {
119
+ function norm(v) {
120
+ return (v ?? '').replace(/\s+/g, ' ').trim();
121
+ }
122
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
123
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
124
+ const section = h3 ? h3.closest('.form-content') : null;
125
+ if (!section)
126
+ return '';
127
+ const list = section.querySelector('.form-content-list');
128
+ if (!list)
129
+ return '';
130
+ const items = list.querySelectorAll('.form-content-list-item');
131
+ if (idx < 0 || idx >= items.length)
132
+ return '';
133
+ const row = items[idx];
134
+ const wrap = row.querySelector('.auto-resize-textarea-wrapper');
135
+ if (wrap) {
136
+ const ta = wrap.querySelector('textarea, input');
137
+ if (ta instanceof HTMLTextAreaElement) {
138
+ return norm(ta.value);
139
+ }
140
+ if (ta instanceof HTMLInputElement && ta.type !== 'hidden') {
141
+ return norm(ta.value);
142
+ }
143
+ }
144
+ const scope = row.querySelector('.form-content-list-item-content') ?? row;
145
+ const inp = scope.querySelector('input, textarea');
146
+ if (inp instanceof HTMLInputElement && inp.type !== 'hidden') {
147
+ return norm(inp.value);
148
+ }
149
+ if (inp instanceof HTMLTextAreaElement) {
150
+ return norm(inp.value);
151
+ }
152
+ const word = row.querySelector('.form-content-list-item-title .form-content-word') ||
153
+ row.querySelector('.form-content-word');
154
+ return norm(word?.textContent ?? '');
155
+ }, titleKeyword, rowIndex);
156
+ }
157
+ /** 点击行内 `.form-content-list-item-content`,Boss 会在内部挂载 `.auto-resize-textarea-wrapper` + textarea。 */
158
+ async function clickFormContentListItemContent(page, titleKeyword, rowIndex) {
159
+ return page.evaluate((kw, idx) => {
160
+ function norm(v) {
161
+ return (v ?? '').replace(/\s+/g, ' ').trim();
162
+ }
163
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
164
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
165
+ const section = h3 ? h3.closest('.form-content') : null;
166
+ if (!section)
167
+ return false;
168
+ const items = section.querySelectorAll('.form-content-list .form-content-list-item');
169
+ if (idx < 0 || idx >= items.length)
170
+ return false;
171
+ const row = items[idx];
172
+ const content = row.querySelector('.form-content-list-item-content');
173
+ if (!(content instanceof HTMLElement))
174
+ return false;
175
+ content.scrollIntoView({ block: 'center', inline: 'nearest' });
176
+ content.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
177
+ content.click();
178
+ return true;
179
+ }, titleKeyword, rowIndex);
180
+ }
181
+ async function waitForAutoResizeTextareaInRow(page, titleKeyword, rowIndex, timeoutMs = 1800) {
182
+ try {
183
+ await page.waitForFunction((kw, idx) => {
184
+ function norm(v) {
185
+ return (v ?? '').replace(/\s+/g, ' ').trim();
186
+ }
187
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
188
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
189
+ const section = h3 ? h3.closest('.form-content') : null;
190
+ if (!section)
191
+ return false;
192
+ const items = section.querySelectorAll('.form-content-list .form-content-list-item');
193
+ if (idx < 0 || idx >= items.length)
194
+ return false;
195
+ const row = items[idx];
196
+ const wrap = row.querySelector('.auto-resize-textarea-wrapper');
197
+ if (!wrap)
198
+ return false;
199
+ const ta = wrap.querySelector('textarea, input');
200
+ if (ta instanceof HTMLTextAreaElement)
201
+ return true;
202
+ if (ta instanceof HTMLInputElement && ta.type !== 'hidden' && ta.type !== 'button' && ta.type !== 'submit') {
203
+ return true;
204
+ }
205
+ return false;
206
+ }, { timeout: timeoutMs }, titleKeyword, rowIndex);
207
+ return true;
208
+ }
209
+ catch {
210
+ return false;
211
+ }
212
+ }
213
+ async function fillAutoResizeTextareaInRow(page, titleKeyword, rowIndex, value) {
214
+ return page.evaluate((kw, idx, v) => {
215
+ function norm(s) {
216
+ return (s ?? '').replace(/\s+/g, ' ').trim();
217
+ }
218
+ function setNativeValue(el, val) {
219
+ const tracker = el._valueTracker;
220
+ if (tracker && typeof tracker.setValue === 'function') {
221
+ tracker.setValue('');
222
+ }
223
+ const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
224
+ const desc = Object.getOwnPropertyDescriptor(proto, 'value');
225
+ if (desc?.set) {
226
+ desc.set.call(el, val);
227
+ }
228
+ else {
229
+ el.value = val;
230
+ }
231
+ }
232
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
233
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
234
+ const section = h3 ? h3.closest('.form-content') : null;
235
+ if (!section)
236
+ return false;
237
+ const items = section.querySelectorAll('.form-content-list .form-content-list-item');
238
+ if (idx < 0 || idx >= items.length)
239
+ return false;
240
+ const row = items[idx];
241
+ const wrap = row.querySelector('.auto-resize-textarea-wrapper');
242
+ if (!wrap)
243
+ return false;
244
+ const el = wrap.querySelector('textarea, input');
245
+ if (!(el instanceof HTMLTextAreaElement) && !(el instanceof HTMLInputElement))
246
+ return false;
247
+ if (el instanceof HTMLInputElement && (el.type === 'hidden' || el.type === 'button' || el.type === 'submit')) {
248
+ return false;
249
+ }
250
+ el.focus();
251
+ setNativeValue(el, v);
252
+ el.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true, data: v }));
253
+ el.dispatchEvent(new Event('change', { bubbles: true }));
254
+ const titleWrap = row.querySelector('.form-content-list-item-title');
255
+ titleWrap?.classList.remove('error');
256
+ el.blur();
257
+ return true;
258
+ }, titleKeyword, rowIndex, value);
259
+ }
260
+ /** 用视口坐标点击 `.form-content-word` 中心,确保焦点落在真实文案节点上(比 programatic focus 更稳)。 */
261
+ async function clickFormContentWordCenter(page, titleKeyword, rowIndex) {
262
+ const pos = await page.evaluate((kw, idx) => {
263
+ function norm(v) {
264
+ return (v ?? '').replace(/\s+/g, ' ').trim();
265
+ }
266
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
267
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
268
+ const section = h3 ? h3.closest('.form-content') : null;
269
+ if (!section)
270
+ return null;
271
+ const list = section.querySelector('.form-content-list');
272
+ if (!list)
273
+ return null;
274
+ const items = list.querySelectorAll('.form-content-list-item');
275
+ if (idx < 0 || idx >= items.length)
276
+ return null;
277
+ const row = items[idx];
278
+ const word = row.querySelector('.form-content-list-item-title .form-content-word') ||
279
+ row.querySelector('.form-content-word');
280
+ if (!(word instanceof HTMLElement))
281
+ return null;
282
+ word.scrollIntoView({ block: 'center', inline: 'nearest' });
283
+ const r = word.getBoundingClientRect();
284
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
285
+ }, titleKeyword, rowIndex);
286
+ if (!pos || !(pos.x > 0 && pos.y > 0)) {
287
+ return false;
288
+ }
289
+ await page.mouse.click(pos.x, pos.y);
290
+ return true;
291
+ }
292
+ async function tryFillRowViaDomEvaluate(page, titleKeyword, rowIndex, value) {
293
+ return page.evaluate((kw, idx, v) => {
294
+ function norm(s) {
295
+ return (s ?? '').replace(/\s+/g, ' ').trim();
296
+ }
297
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
298
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
299
+ const section = h3 ? h3.closest('.form-content') : null;
300
+ if (!section)
301
+ return false;
302
+ const list = section.querySelector('.form-content-list');
303
+ if (!list)
304
+ return false;
305
+ const items = list.querySelectorAll('.form-content-list-item');
306
+ const row = items[idx];
307
+ if (!row)
308
+ return false;
309
+ row.scrollIntoView({ block: 'center', inline: 'nearest' });
310
+ const scope = row.querySelector('.form-content-list-item-content') ?? row;
311
+ function walkInput(root) {
312
+ const direct = root.querySelectorAll('input, textarea');
313
+ for (let i = 0; i < direct.length; i++) {
314
+ const el = direct[i];
315
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
316
+ if (el.type === 'hidden' || el.type === 'submit' || el.type === 'button')
317
+ continue;
318
+ return el;
319
+ }
320
+ }
321
+ for (const el of Array.from(root.querySelectorAll('*'))) {
322
+ if (el.shadowRoot) {
323
+ const f = walkInput(el.shadowRoot);
324
+ if (f)
325
+ return f;
326
+ }
327
+ }
328
+ return null;
329
+ }
330
+ const inp = walkInput(scope);
331
+ if (inp) {
332
+ const tracker = inp._valueTracker;
333
+ if (tracker && typeof tracker.setValue === 'function')
334
+ tracker.setValue('');
335
+ const proto = inp instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
336
+ const desc = Object.getOwnPropertyDescriptor(proto, 'value');
337
+ if (desc?.set)
338
+ desc.set.call(inp, v);
339
+ else
340
+ inp.value = v;
341
+ inp.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true, data: v }));
342
+ inp.dispatchEvent(new Event('change', { bubbles: true }));
343
+ return true;
344
+ }
345
+ const word = row.querySelector('.form-content-list-item-title .form-content-word') ||
346
+ row.querySelector('.form-content-word');
347
+ if (word instanceof HTMLElement) {
348
+ word.focus();
349
+ word.click();
350
+ try {
351
+ document.execCommand('selectAll', false);
352
+ document.execCommand('insertText', false, v);
353
+ }
354
+ catch {
355
+ /* 由外层 CDP/键盘兜底 */
356
+ }
357
+ return true;
358
+ }
359
+ return false;
360
+ }, titleKeyword, rowIndex, value);
361
+ }
362
+ /** 必须与目标文案一致(规范化空白后);不用 includes,避免整段终端/回显误匹配。 */
363
+ function wordTextMatchesExpected(shown, expected) {
364
+ return normFormText(shown) === normFormText(expected);
365
+ }
366
+ /**
367
+ * 直接改 DOM:文案在 `.form-content-list-item-title` > `.form-content-word` 内,无原生 input。
368
+ * 同步写 inner 文本并向外层派发 input/change,并尝试去掉 error 态 class。
369
+ */
370
+ async function setFormContentWordDirect(page, titleKeyword, rowIndex, value) {
371
+ return page.evaluate((kw, idx, v) => {
372
+ function norm(s) {
373
+ return (s ?? '').replace(/\s+/g, ' ').trim();
374
+ }
375
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
376
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
377
+ const section = h3 ? h3.closest('.form-content') : null;
378
+ if (!section)
379
+ return false;
380
+ const list = section.querySelector('.form-content-list');
381
+ if (!list)
382
+ return false;
383
+ const items = list.querySelectorAll('.form-content-list-item');
384
+ if (idx < 0 || idx >= items.length)
385
+ return false;
386
+ const row = items[idx];
387
+ const titleWrap = row.querySelector('.form-content-list-item-title');
388
+ let word = row.querySelector('.form-content-list-item-title .form-content-word') ||
389
+ row.querySelector('.form-content-word');
390
+ if (!(word instanceof HTMLElement)) {
391
+ if (titleWrap instanceof HTMLElement) {
392
+ word = document.createElement('div');
393
+ word.className = 'form-content-word';
394
+ titleWrap.appendChild(word);
395
+ }
396
+ else {
397
+ return false;
398
+ }
399
+ }
400
+ word.textContent = v;
401
+ if (titleWrap instanceof HTMLElement) {
402
+ titleWrap.classList.remove('error');
403
+ }
404
+ function fire(el) {
405
+ try {
406
+ el.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true, data: v, inputType: 'insertText' }));
407
+ }
408
+ catch {
409
+ el.dispatchEvent(new Event('input', { bubbles: true }));
410
+ }
411
+ el.dispatchEvent(new Event('change', { bubbles: true }));
412
+ }
413
+ fire(word);
414
+ if (titleWrap instanceof HTMLElement) {
415
+ fire(titleWrap);
416
+ }
417
+ return true;
418
+ }, titleKeyword, rowIndex, value);
419
+ }
420
+ /**
421
+ * 在区块内按行下标写入一条条件(有内容则直接覆盖)。
422
+ * Boss:先点 `.form-content-list-item-content`,再填 `.auto-resize-textarea-wrapper` 内 textarea;失败再改 word div / 键盘。
423
+ */
424
+ async function fillRowAtIndexInSection(page, titleKeyword, rowIndex, text) {
425
+ const rowPresent = await waitForRequirementRowPresent(page, titleKeyword, rowIndex);
426
+ if (!rowPresent) {
427
+ return false;
428
+ }
429
+ const contentClicked = await clickFormContentListItemContent(page, titleKeyword, rowIndex);
430
+ let clickedIndex = rowIndex;
431
+ if (!contentClicked) {
432
+ clickedIndex = await page.evaluate((kw, idx) => {
433
+ function norm(v) {
434
+ return (v ?? '').replace(/\s+/g, ' ').trim();
435
+ }
436
+ function findFormSectionByTitle(keyword) {
437
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
438
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(keyword));
439
+ const section = h3 ? h3.closest('.form-content') : null;
440
+ if (!section?.querySelector('.form-content-list')) {
441
+ return null;
442
+ }
443
+ return section;
444
+ }
445
+ const section = findFormSectionByTitle(kw);
446
+ if (!section)
447
+ return -1;
448
+ const list = section.querySelector('.form-content-list');
449
+ if (!list)
450
+ return -1;
451
+ const items = list.querySelectorAll('.form-content-list-item');
452
+ if (idx < 0 || idx >= items.length)
453
+ return -1;
454
+ const row = items[idx];
455
+ const titleEl = row.querySelector('.form-content-list-item-title');
456
+ if (!(titleEl instanceof HTMLElement))
457
+ return -1;
458
+ titleEl.scrollIntoView({ block: 'center', inline: 'nearest' });
459
+ titleEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
460
+ titleEl.click();
461
+ return idx;
462
+ }, titleKeyword, rowIndex);
463
+ if (clickedIndex < 0) {
464
+ return false;
465
+ }
466
+ }
467
+ await sleepRandom(120, 280);
468
+ const tryVerify = async () => {
469
+ await sleepRandom(40, 100);
470
+ const shown = await readRowRequirementShownText(page, titleKeyword, clickedIndex);
471
+ return wordTextMatchesExpected(shown, text);
472
+ };
473
+ let hasAutoResize = await waitForAutoResizeTextareaInRow(page, titleKeyword, clickedIndex, 1800);
474
+ if (!hasAutoResize && contentClicked) {
475
+ await clickFormContentListItemContent(page, titleKeyword, clickedIndex);
476
+ await sleepRandom(80, 160);
477
+ hasAutoResize = await waitForAutoResizeTextareaInRow(page, titleKeyword, clickedIndex, 1200);
478
+ }
479
+ if (hasAutoResize) {
480
+ await fillAutoResizeTextareaInRow(page, titleKeyword, clickedIndex, text);
481
+ if (await tryVerify()) {
482
+ await page.keyboard.press('Tab').catch(() => { });
483
+ return true;
484
+ }
485
+ await page.evaluate((kw, idx) => {
486
+ function norm(v) {
487
+ return (v ?? '').replace(/\s+/g, ' ').trim();
488
+ }
489
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
490
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
491
+ const section = h3 ? h3.closest('.form-content') : null;
492
+ if (!section)
493
+ return;
494
+ const items = section.querySelectorAll('.form-content-list .form-content-list-item');
495
+ const row = items[idx];
496
+ const wrap = row?.querySelector('.auto-resize-textarea-wrapper');
497
+ const ta = wrap?.querySelector('textarea, input');
498
+ if (ta instanceof HTMLElement) {
499
+ ta.focus();
500
+ ta.click();
501
+ }
502
+ }, titleKeyword, clickedIndex);
503
+ await sleepRandom(50, 120);
504
+ await fillAutoResizeTextareaInRow(page, titleKeyword, clickedIndex, text);
505
+ if (await tryVerify()) {
506
+ await page.keyboard.press('Tab').catch(() => { });
507
+ return true;
508
+ }
509
+ }
510
+ let hasWord = await waitForFormContentWordInRow(page, titleKeyword, clickedIndex, 2000);
511
+ if (!hasWord) {
512
+ await page.evaluate((kw, idx) => {
513
+ function norm(v) {
514
+ return (v ?? '').replace(/\s+/g, ' ').trim();
515
+ }
516
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
517
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
518
+ const section = h3 ? h3.closest('.form-content') : null;
519
+ if (!section)
520
+ return;
521
+ const list = section.querySelector('.form-content-list');
522
+ if (!list)
523
+ return;
524
+ const items = list.querySelectorAll('.form-content-list-item');
525
+ const row = items[idx];
526
+ const titleEl = row?.querySelector('.form-content-list-item-title');
527
+ if (titleEl instanceof HTMLElement) {
528
+ titleEl.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true, view: window }));
529
+ titleEl.click();
530
+ }
531
+ }, titleKeyword, clickedIndex);
532
+ await sleepRandom(120, 240);
533
+ hasWord = await waitForFormContentWordInRow(page, titleKeyword, clickedIndex, 1200);
534
+ }
535
+ if (await setFormContentWordDirect(page, titleKeyword, clickedIndex, text)) {
536
+ if (await tryVerify()) {
537
+ await page.keyboard.press('Tab').catch(() => { });
538
+ return true;
539
+ }
540
+ }
541
+ await page.evaluate(() => new Promise((resolve) => {
542
+ requestAnimationFrame(() => {
543
+ requestAnimationFrame(() => resolve());
544
+ });
545
+ }));
546
+ if (await setFormContentWordDirect(page, titleKeyword, clickedIndex, text)) {
547
+ if (await tryVerify()) {
548
+ await page.keyboard.press('Tab').catch(() => { });
549
+ return true;
550
+ }
551
+ }
552
+ await tryFillRowViaDomEvaluate(page, titleKeyword, clickedIndex, text);
553
+ if (await tryVerify()) {
554
+ await page.keyboard.press('Tab').catch(() => { });
555
+ return true;
556
+ }
557
+ if (hasWord) {
558
+ await clickFormContentWordCenter(page, titleKeyword, clickedIndex);
559
+ await sleepRandom(60, 120);
560
+ await page.evaluate((kw, idx) => {
561
+ function norm(v) {
562
+ return (v ?? '').replace(/\s+/g, ' ').trim();
563
+ }
564
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
565
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
566
+ const section = h3 ? h3.closest('.form-content') : null;
567
+ if (!section)
568
+ return;
569
+ const items = section.querySelectorAll('.form-content-list .form-content-list-item');
570
+ const row = items[idx];
571
+ const word = row?.querySelector('.form-content-list-item-title .form-content-word') ||
572
+ row?.querySelector('.form-content-word');
573
+ if (word instanceof HTMLElement) {
574
+ word.focus();
575
+ }
576
+ }, titleKeyword, clickedIndex);
577
+ }
578
+ else {
579
+ await page.evaluate((kw, idx) => {
580
+ function norm(v) {
581
+ return (v ?? '').replace(/\s+/g, ' ').trim();
582
+ }
583
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
584
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
585
+ const section = h3 ? h3.closest('.form-content') : null;
586
+ if (!section)
587
+ return;
588
+ const items = section.querySelectorAll('.form-content-list .form-content-list-item');
589
+ const row = items[idx];
590
+ const titleEl = row?.querySelector('.form-content-list-item-title');
591
+ if (titleEl instanceof HTMLElement) {
592
+ titleEl.scrollIntoView({ block: 'center', inline: 'nearest' });
593
+ titleEl.focus();
594
+ titleEl.click();
595
+ }
596
+ }, titleKeyword, clickedIndex);
597
+ await sleepRandom(60, 100);
598
+ }
599
+ const selectAllMod = process.platform === 'darwin' ? 'Meta' : 'Control';
600
+ await page.keyboard.down(selectAllMod);
601
+ await page.keyboard.press('KeyA');
602
+ await page.keyboard.up(selectAllMod);
603
+ await sleepRandom(40, 80);
604
+ await page.keyboard.type(text, { delay: 12 });
605
+ await page.keyboard.press('Tab');
606
+ await sleepRandom(50, 100);
607
+ if (await tryVerify()) {
608
+ return true;
609
+ }
610
+ return false;
611
+ }
612
+ /**
613
+ * 每条条件按顺序对应第 0、1、2… 行:有行则覆盖内容;行不够则点「添加条件」再填。
614
+ */
615
+ async function applyLinesToSection(page, titleKeyword, lines) {
616
+ let processed = 0;
617
+ let nextRowIndex = 0;
618
+ for (const raw of lines) {
619
+ const text = raw.trim();
620
+ if (!text) {
621
+ continue;
622
+ }
623
+ let ok = await fillRowAtIndexInSection(page, titleKeyword, nextRowIndex, text);
624
+ if (!ok) {
625
+ await clickAddConditionInSection(page, titleKeyword);
626
+ ok = await fillRowAtIndexInSection(page, titleKeyword, nextRowIndex, text);
627
+ }
628
+ if (!ok) {
629
+ throw new Error(`「${titleKeyword}」第 ${processed + 1} 条条件无法填入(该区块无可用行或「添加条件」后仍失败)。`);
630
+ }
631
+ processed += 1;
632
+ nextRowIndex += 1;
633
+ }
634
+ }
635
+ async function applyAiFormRequirementLists(page, opts) {
636
+ if (opts.core !== undefined) {
637
+ await applyLinesToSection(page, '核心要求', opts.core);
638
+ }
639
+ if (opts.bonus !== undefined) {
640
+ await applyLinesToSection(page, '加分项', opts.bonus);
641
+ }
642
+ }
643
+ async function readSearchFormSnapshot(page) {
644
+ return (await page.evaluate(`(() => {
645
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
646
+ function itemLineText(item) {
647
+ const word = item.querySelector(".form-content-word");
648
+ const w = word ? norm(word.textContent) : "";
649
+ if (w) return w;
650
+ const inp = item.querySelector("input, textarea");
651
+ if (inp && norm(inp.value)) return norm(inp.value);
652
+ const ce = item.querySelector("[contenteditable='true']");
653
+ if (ce) return norm(ce.textContent);
654
+ const titleEl = item.querySelector(".form-content-list-item-title");
655
+ if (titleEl) return norm(titleEl.textContent);
656
+ return "";
657
+ }
658
+ const selectedJob = norm(document.querySelector(".job-dropmenu-select .job-main-text")?.textContent);
659
+ const sections = Array.from(document.querySelectorAll(".form-content"));
660
+ const coreRequirements = [];
661
+ const bonusRequirements = [];
662
+ for (const section of sections) {
663
+ const title = norm(section.querySelector(".form-content-header .form-content-title-h3")?.textContent);
664
+ const items = section.querySelectorAll(".form-content-list-item");
665
+ const words = Array.from(items)
666
+ .map((item) => itemLineText(item))
667
+ .filter(Boolean);
668
+ if (title.includes("核心要求")) {
669
+ coreRequirements.push(...words);
670
+ continue;
671
+ }
672
+ if (title.includes("加分项")) {
673
+ bonusRequirements.push(...words);
674
+ }
675
+ }
676
+ const remainingCountText = norm(document.querySelector(".ai-form-match-footer-text-count")?.textContent);
677
+ return {
678
+ selectedJob,
679
+ coreRequirements,
680
+ bonusRequirements,
681
+ remainingCountText,
682
+ };
683
+ })()`));
684
+ }
685
+ async function selectAiFormJob(page, keyword) {
686
+ const kw = keyword.trim();
687
+ if (!kw) {
688
+ throw new Error('岗位关键字不能为空。');
689
+ }
690
+ const kwLiteral = JSON.stringify(kw);
691
+ const opened = (await page.evaluate(`(() => {
692
+ const host = document.querySelector(".job-dropmenu-select");
693
+ if (!(host instanceof HTMLElement)) return false;
694
+ host.scrollIntoView({ block: "center", inline: "nearest" });
695
+ host.click();
696
+ return true;
697
+ })()`));
698
+ if (!opened) {
699
+ throw new Error('未找到深度搜索页岗位下拉(.job-dropmenu-select)。');
700
+ }
701
+ await sleepRandom(450, 900);
702
+ const searched = (await page.evaluate(`(() => {
703
+ const kw = ${kwLiteral};
704
+ const inputs = Array.from(
705
+ document.querySelectorAll(
706
+ ".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",
707
+ ),
708
+ );
709
+ const input = inputs.find((el) => {
710
+ if (!(el instanceof HTMLInputElement)) return false;
711
+ const r = el.getBoundingClientRect();
712
+ return r.width > 0 && r.height > 0;
713
+ });
714
+ if (!input) return false;
715
+ input.focus();
716
+ input.value = kw;
717
+ input.dispatchEvent(new Event("input", { bubbles: true }));
718
+ input.dispatchEvent(new Event("change", { bubbles: true }));
719
+ return true;
720
+ })()`));
721
+ if (searched) {
722
+ await sleepRandom(520, 1080);
723
+ }
724
+ else {
725
+ await sleepRandom(200, 450);
726
+ }
727
+ const picked = (await page.evaluate(`(() => {
728
+ const kw = ${kwLiteral};
729
+ const norm = (v) => (v ?? "").replace(/\\s+/g, "").trim().toLowerCase();
730
+ const rows = Array.from(
731
+ document.querySelectorAll(
732
+ ".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",
733
+ ),
734
+ );
735
+ if (rows.length === 0) return { ok: false, reason: "empty" };
736
+ const target = rows.find((el) => {
737
+ const label = norm(
738
+ el.querySelector(".job-option-text, .label")?.textContent || el.textContent || "",
739
+ );
740
+ return label.includes(norm(kw));
741
+ });
742
+ if (!(target instanceof HTMLElement)) return { ok: false, reason: "not_found" };
743
+ const label = (
744
+ target.querySelector(".job-option-text, .label")?.textContent ?? target.textContent ?? ""
745
+ )
746
+ .replace(/\\s+/g, " ")
747
+ .trim();
748
+ target.scrollIntoView({ block: "center", inline: "nearest" });
749
+ target.click();
750
+ return { ok: true, label };
751
+ })()`));
752
+ if (!picked.ok) {
753
+ throw new Error(`未找到匹配岗位「${kw}」。`);
754
+ }
755
+ await sleepRandom(900, 1500);
756
+ return picked.label ?? kw;
757
+ }
758
+ export async function readDeepSearchGeekList(page) {
759
+ return (await page.evaluate(`(() => {
760
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
761
+ const items = Array.from(
762
+ document.querySelectorAll(".geeks-box .geek-card-item, .geek-card-list .geek-card-item"),
763
+ );
764
+ return items
765
+ .map((item) => {
766
+ const chatLabel = norm(item.querySelector(".geek-chat")?.textContent);
767
+ if (chatLabel.includes("继续沟通")) {
768
+ return null;
769
+ }
770
+ const name = norm(item.querySelector(".geek-name")?.textContent);
771
+ const splits = Array.from(item.querySelectorAll(".geek-exp .split"))
772
+ .map((el) => norm(el.getAttribute("title") || el.textContent || ""))
773
+ .filter(Boolean);
774
+ const meta = splits.join(" · ");
775
+ const work = norm(item.querySelector(".geek-works span")?.textContent);
776
+ const edu = norm(item.querySelector(".geek-edus span")?.textContent);
777
+ const recEl = item.querySelector(".geek-recommend-text");
778
+ let reason = "";
779
+ if (recEl) {
780
+ reason = norm(recEl.textContent).replace(/^推荐理由\\s*/, "").trim();
781
+ }
782
+ return { name, meta, work, edu, reason };
783
+ })
784
+ .filter((x) => x !== null);
785
+ })()`));
786
+ }
787
+ async function waitForAiMatchSettled(page) {
788
+ await page
789
+ .waitForFunction(`(() =>
790
+ (document.querySelector(".ai-form-match-footer .light-flow-btn-content .text")?.textContent ?? "").includes(
791
+ "停止匹配",
792
+ ))()`, { timeout: 10_000 })
793
+ .catch(() => { });
794
+ await page.waitForFunction(`(() =>
795
+ !(document.querySelector(".ai-form-match-footer .light-flow-btn-content .text")?.textContent ?? "").includes(
796
+ "停止匹配",
797
+ ))()`, { timeout: 120_000 });
798
+ await sleepRandom(500, 900);
799
+ }
800
+ export function renderGeekListSection(title, items) {
801
+ const lines = [title, `共 ${items.length} 人`, ''];
802
+ items.forEach((g, i) => {
803
+ const n = i + 1;
804
+ lines.push(`${n}. ${g.name || '(无姓名)'}`);
805
+ if (g.meta) {
806
+ lines.push(` 概要:${g.meta}`);
807
+ }
808
+ if (g.work) {
809
+ lines.push(` 经历:${g.work}`);
810
+ }
811
+ if (g.edu) {
812
+ lines.push(` 教育:${g.edu}`);
813
+ }
814
+ if (g.reason) {
815
+ lines.push(` 推荐:${g.reason}`);
816
+ }
817
+ lines.push('');
818
+ });
819
+ return lines.join('\n').trimEnd();
820
+ }
821
+ export async function clickGreetDeepSearch(page, target) {
822
+ const targetLiteral = JSON.stringify(target.trim());
823
+ const result = (await page.evaluate(`(() => {
824
+ const raw = ${targetLiteral};
825
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
826
+ const allCards = Array.from(
827
+ document.querySelectorAll(".geeks-box .geek-card-item, .geek-card-list .geek-card-item"),
828
+ );
829
+ if (allCards.length === 0) {
830
+ return { kind: "empty" };
831
+ }
832
+ const cards = allCards.filter((item) => {
833
+ const chatLabel = norm(item.querySelector(".geek-chat")?.textContent);
834
+ return !chatLabel.includes("继续沟通");
835
+ });
836
+ if (cards.length === 0) {
837
+ return { kind: "all_continue" };
838
+ }
839
+
840
+ const idxNum = Number.parseInt(raw, 10);
841
+ let targetCard = null;
842
+ if (Number.isFinite(idxNum) && idxNum >= 1 && idxNum <= cards.length) {
843
+ targetCard = cards[idxNum - 1];
844
+ } else {
845
+ targetCard =
846
+ cards.find((item) => {
847
+ const name = norm(item.querySelector(".geek-name")?.textContent);
848
+ return name === raw || name.includes(raw);
849
+ }) ?? null;
850
+ }
851
+ if (!targetCard) {
852
+ return { kind: "not_found", target: raw };
853
+ }
854
+
855
+ const name = norm(targetCard.querySelector(".geek-name")?.textContent);
856
+ const btn =
857
+ targetCard.querySelector(".geek-chat .btn-ai-v2") ||
858
+ targetCard.querySelector(".geek-chat span.btn-ai-v2") ||
859
+ targetCard.querySelector(".geek-chat span[class*='btn-ai']");
860
+ if (!(btn instanceof HTMLElement)) {
861
+ return { kind: "no_btn", name };
862
+ }
863
+ const label = norm(btn.textContent);
864
+ if (!label.includes("打招呼")) {
865
+ return { kind: "not_greet", name, label };
866
+ }
867
+ const cls = btn.className ?? "";
868
+ const disabled = /disabled|forbid|ban/i.test(cls) || btn.getAttribute("disabled") !== null;
869
+ if (disabled) {
870
+ return { kind: "disabled", name };
871
+ }
872
+ btn.scrollIntoView({ block: "center", inline: "nearest" });
873
+ btn.click();
874
+ return { kind: "clicked", name };
875
+ })()`));
876
+ switch (result.kind) {
877
+ case 'empty':
878
+ throw new Error('深度搜索暂无候选人列表,请先执行「立即匹配」或使用 boss deep-search <岗位> 触发匹配。');
879
+ case 'all_continue':
880
+ throw new Error('当前列表均为「继续沟通」状态,已无待打招呼人选(与 boss deep-search 列表展示一致)。');
881
+ case 'not_found':
882
+ throw new Error(`未在可打招呼的深度搜索列表中找到目标:${result.target}(「继续沟通」人选已排除,请用 boss deep-search 核对序号与姓名)。`);
883
+ case 'no_btn':
884
+ throw new Error(`候选人 ${result.name} 缺少「打招呼」按钮,无法执行。`);
885
+ case 'not_greet':
886
+ throw new Error(`候选人 ${result.name} 当前按钮为「${result.label}」,无法执行打招呼。`);
887
+ case 'disabled':
888
+ throw new Error(`候选人 ${result.name} 的打招呼不可用(可能已打过招呼)。`);
889
+ case 'clicked':
890
+ return { message: `已对 ${result.name} 在深度搜索页点击「打招呼」。` };
891
+ default: {
892
+ const _x = result;
893
+ throw new Error(`未知结果:${String(_x)}`);
894
+ }
895
+ }
896
+ }
897
+ async function clickMatchNow(page) {
898
+ const clicked = (await page.evaluate(`(() => {
899
+ function norm(v) {
900
+ return (v ?? "").replace(/\\s+/g, "").trim();
901
+ }
902
+ function isVisible(el) {
903
+ if (!(el instanceof HTMLElement)) return false;
904
+ const st = window.getComputedStyle(el);
905
+ if (st.display === "none" || st.visibility === "hidden") return false;
906
+ const r = el.getBoundingClientRect();
907
+ return r.width > 0 && r.height > 0;
908
+ }
909
+ const buttons = Array.from(
910
+ 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"),
911
+ ).filter((el) => isVisible(el));
912
+ if (buttons.length === 0) {
913
+ return false;
914
+ }
915
+ const preferred = buttons.find((el) => norm(el.textContent).includes("立即匹配")) ?? buttons[0];
916
+ if (!(preferred instanceof HTMLElement)) {
917
+ return false;
918
+ }
919
+ preferred.scrollIntoView({ block: "center", inline: "nearest" });
920
+ preferred.click();
921
+ return true;
922
+ })()`));
923
+ if (!clicked) {
924
+ throw new Error('未找到“立即匹配”按钮,无法执行深度搜索。');
925
+ }
926
+ }
927
+ function renderFormSnapshotOnly(snap) {
928
+ const core = snap.coreRequirements.length > 0 ? snap.coreRequirements.join('|') : '(空)';
929
+ const bonus = snap.bonusRequirements.length > 0 ? snap.bonusRequirements.join('|') : '(空)';
930
+ return [
931
+ '已更新深度搜索表单(未触发「立即匹配」)。',
932
+ `职位:${snap.selectedJob || '未知职位'}`,
933
+ `核心要求(${snap.coreRequirements.length}):${core}`,
934
+ `加分项(${snap.bonusRequirements.length}):${bonus}`,
935
+ `今日匹配剩余:${snap.remainingCountText || '未知'}`,
936
+ `来源页面:${BOSS_CHAT_AI_FORM_URL}`,
937
+ ].join('\n');
938
+ }
939
+ function renderMatchSummaryText(before, after) {
940
+ const core = before.coreRequirements.length > 0 ? before.coreRequirements.join('|') : '(空)';
941
+ const bonus = before.bonusRequirements.length > 0 ? before.bonusRequirements.join('|') : '(空)';
942
+ const remain = before.remainingCountText || '未知';
943
+ const remainAfter = after.remainingCountText || '未知';
944
+ const remainLine = remain === remainAfter ? `今日匹配剩余:${remain}` : `今日匹配剩余:${remain} -> ${remainAfter}`;
945
+ return [
946
+ '已进入深度搜索并触发“立即匹配”。',
947
+ `职位:${before.selectedJob || '未知职位'}`,
948
+ `核心要求(${before.coreRequirements.length}):${core}`,
949
+ `加分项(${before.bonusRequirements.length}):${bonus}`,
950
+ remainLine,
951
+ `来源页面:${BOSS_CHAT_AI_FORM_URL}`,
952
+ ].join('\n');
953
+ }
954
+ export async function runBossSearchSet(opts) {
955
+ const jobKeyword = opts.jobKeyword?.trim();
956
+ const coreReq = opts.coreRequirements;
957
+ const bonusReq = opts.bonusRequirements;
958
+ const hasFormEdit = coreReq !== undefined || bonusReq !== undefined;
959
+ if (!jobKeyword && !hasFormEdit) {
960
+ throw new Error('请至少指定 --job/-j、--core/-c 或 --bonus/-b 之一。');
961
+ }
962
+ try {
963
+ return await withBossSessionPage(async (page) => {
964
+ const currentUrl = page.url();
965
+ if (!isBossChatAiFormUrl(currentUrl)) {
966
+ await clickBossSidebarMenuToPath(page, '深度搜索', '/web/chat/aiform');
967
+ await sleepRandom(AI_FORM_SETTLE_MS.min, AI_FORM_SETTLE_MS.max);
968
+ }
969
+ if (!isBossChatAiFormUrl(page.url())) {
970
+ throw new Error('通过侧边栏“深度搜索”进入页面失败,请确认已登录并可访问 /web/chat/aiform。');
971
+ }
972
+ await ensureInDeepSearchPage(page);
973
+ if (jobKeyword) {
974
+ await selectAiFormJob(page, jobKeyword);
975
+ await ensureInDeepSearchPage(page);
976
+ if (hasFormEdit) {
977
+ await sleepRandom(500, 900);
978
+ }
979
+ }
980
+ if (hasFormEdit) {
981
+ await applyAiFormRequirementLists(page, {
982
+ core: coreReq,
983
+ bonus: bonusReq,
984
+ });
985
+ await ensureInDeepSearchPage(page);
986
+ }
987
+ const snap = await readSearchFormSnapshot(page);
988
+ return renderFormSnapshotOnly(snap);
989
+ });
990
+ }
991
+ catch (e) {
992
+ const message = e instanceof Error ? e.message : String(e);
993
+ if (e instanceof Error && e.message.includes('浏览器会话尚未初始化')) {
994
+ throw new Error(createWaitManualLoginRequiredText('设置深度搜索条件'));
995
+ }
996
+ console.error(`[boss-cli] boss_search_set error: ${message}`);
997
+ throw new Error(`设置深度搜索条件失败:${message}`);
998
+ }
999
+ }
1000
+ export async function runBossSearch(opts = {}) {
1001
+ const jobKeyword = opts.jobKeyword?.trim();
1002
+ try {
1003
+ return await withBossSessionPage(async (page) => {
1004
+ const currentUrl = page.url();
1005
+ if (!isBossChatAiFormUrl(currentUrl)) {
1006
+ await clickBossSidebarMenuToPath(page, '深度搜索', '/web/chat/aiform');
1007
+ await sleepRandom(AI_FORM_SETTLE_MS.min, AI_FORM_SETTLE_MS.max);
1008
+ }
1009
+ if (!isBossChatAiFormUrl(page.url())) {
1010
+ throw new Error('通过侧边栏“深度搜索”进入页面失败,请确认已登录并可访问 /web/chat/aiform。');
1011
+ }
1012
+ await ensureInDeepSearchPage(page);
1013
+ if (!jobKeyword) {
1014
+ const geeks = await readDeepSearchGeekList(page);
1015
+ return renderGeekListSection('深度搜索当前匹配结果(未触发立即匹配)', geeks);
1016
+ }
1017
+ await selectAiFormJob(page, jobKeyword);
1018
+ await ensureInDeepSearchPage(page);
1019
+ const before = await readSearchFormSnapshot(page);
1020
+ await clickMatchNow(page);
1021
+ await waitForAiMatchSettled(page);
1022
+ await ensureInDeepSearchPage(page);
1023
+ const after = await readSearchFormSnapshot(page);
1024
+ await sleepRandom(600, 1200);
1025
+ const geeks = await readDeepSearchGeekList(page);
1026
+ const summary = renderMatchSummaryText(before, after);
1027
+ return [summary, '', renderGeekListSection('匹配结果列表', geeks)].join('\n');
1028
+ });
1029
+ }
1030
+ catch (e) {
1031
+ const message = e instanceof Error ? e.message : String(e);
1032
+ if (e instanceof Error && e.message.includes('浏览器会话尚未初始化')) {
1033
+ throw new Error(createWaitManualLoginRequiredText('执行深度搜索'));
1034
+ }
1035
+ console.error(`[boss-cli] boss_search error: ${message}`);
1036
+ throw new Error(`执行深度搜索失败:${message}`);
1037
+ }
1038
+ }
1039
+ //# sourceMappingURL=deep-search.js.map