@viyzhu/boss-cli-fork 0.6.8

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 (159) hide show
  1. package/AGENTS.md +30 -0
  2. package/LICENSE +674 -0
  3. package/README.md +223 -0
  4. package/dist/browser/browser_session.d.ts +27 -0
  5. package/dist/browser/browser_session.d.ts.map +1 -0
  6. package/dist/browser/browser_session.js +246 -0
  7. package/dist/browser/browser_session.js.map +1 -0
  8. package/dist/browser/cdp_browser.d.ts +50 -0
  9. package/dist/browser/cdp_browser.d.ts.map +1 -0
  10. package/dist/browser/cdp_browser.js +315 -0
  11. package/dist/browser/cdp_browser.js.map +1 -0
  12. package/dist/browser/human_delay.d.ts +117 -0
  13. package/dist/browser/human_delay.d.ts.map +1 -0
  14. package/dist/browser/human_delay.js +71 -0
  15. package/dist/browser/human_delay.js.map +1 -0
  16. package/dist/browser/index.d.ts +9 -0
  17. package/dist/browser/index.d.ts.map +1 -0
  18. package/dist/browser/index.js +9 -0
  19. package/dist/browser/index.js.map +1 -0
  20. package/dist/browser/timing.d.ts +7 -0
  21. package/dist/browser/timing.d.ts.map +1 -0
  22. package/dist/browser/timing.js +34 -0
  23. package/dist/browser/timing.js.map +1 -0
  24. package/dist/browser/viewport_temp.d.ts +15 -0
  25. package/dist/browser/viewport_temp.d.ts.map +1 -0
  26. package/dist/browser/viewport_temp.js +78 -0
  27. package/dist/browser/viewport_temp.js.map +1 -0
  28. package/dist/cli/banner.d.ts +2 -0
  29. package/dist/cli/banner.d.ts.map +1 -0
  30. package/dist/cli/banner.js +69 -0
  31. package/dist/cli/banner.js.map +1 -0
  32. package/dist/cli/cliRouter.d.ts +9 -0
  33. package/dist/cli/cliRouter.d.ts.map +1 -0
  34. package/dist/cli/cliRouter.js +604 -0
  35. package/dist/cli/cliRouter.js.map +1 -0
  36. package/dist/cli/index.d.ts +3 -0
  37. package/dist/cli/index.d.ts.map +1 -0
  38. package/dist/cli/index.js +34 -0
  39. package/dist/cli/index.js.map +1 -0
  40. package/dist/cli/version.d.ts +29 -0
  41. package/dist/cli/version.d.ts.map +1 -0
  42. package/dist/cli/version.js +180 -0
  43. package/dist/cli/version.js.map +1 -0
  44. package/dist/common/auth.d.ts +44 -0
  45. package/dist/common/auth.d.ts.map +1 -0
  46. package/dist/common/auth.js +168 -0
  47. package/dist/common/auth.js.map +1 -0
  48. package/dist/common/baidu_user_env.d.ts +7 -0
  49. package/dist/common/baidu_user_env.d.ts.map +1 -0
  50. package/dist/common/baidu_user_env.js +56 -0
  51. package/dist/common/baidu_user_env.js.map +1 -0
  52. package/dist/common/boss_availability.d.ts +5 -0
  53. package/dist/common/boss_availability.d.ts.map +1 -0
  54. package/dist/common/boss_availability.js +245 -0
  55. package/dist/common/boss_availability.js.map +1 -0
  56. package/dist/common/boss_modal.d.ts +14 -0
  57. package/dist/common/boss_modal.d.ts.map +1 -0
  58. package/dist/common/boss_modal.js +81 -0
  59. package/dist/common/boss_modal.js.map +1 -0
  60. package/dist/common/boss_page_guards.d.ts +14 -0
  61. package/dist/common/boss_page_guards.d.ts.map +1 -0
  62. package/dist/common/boss_page_guards.js +596 -0
  63. package/dist/common/boss_page_guards.js.map +1 -0
  64. package/dist/common/boss_paywall_popup.d.ts +28 -0
  65. package/dist/common/boss_paywall_popup.d.ts.map +1 -0
  66. package/dist/common/boss_paywall_popup.js +158 -0
  67. package/dist/common/boss_paywall_popup.js.map +1 -0
  68. package/dist/common/boss_session_lock.d.ts +2 -0
  69. package/dist/common/boss_session_lock.d.ts.map +1 -0
  70. package/dist/common/boss_session_lock.js +127 -0
  71. package/dist/common/boss_session_lock.js.map +1 -0
  72. package/dist/common/boss_session_page.d.ts +27 -0
  73. package/dist/common/boss_session_page.d.ts.map +1 -0
  74. package/dist/common/boss_session_page.js +173 -0
  75. package/dist/common/boss_session_page.js.map +1 -0
  76. package/dist/common/boss_sidebar_nav.d.ts +7 -0
  77. package/dist/common/boss_sidebar_nav.d.ts.map +1 -0
  78. package/dist/common/boss_sidebar_nav.js +53 -0
  79. package/dist/common/boss_sidebar_nav.js.map +1 -0
  80. package/dist/common/c_resume_capture.d.ts +19 -0
  81. package/dist/common/c_resume_capture.d.ts.map +1 -0
  82. package/dist/common/c_resume_capture.js +185 -0
  83. package/dist/common/c_resume_capture.js.map +1 -0
  84. package/dist/common/ensure_page.d.ts +9 -0
  85. package/dist/common/ensure_page.d.ts.map +1 -0
  86. package/dist/common/ensure_page.js +14 -0
  87. package/dist/common/ensure_page.js.map +1 -0
  88. package/dist/config.d.ts +22 -0
  89. package/dist/config.d.ts.map +1 -0
  90. package/dist/config.js +57 -0
  91. package/dist/config.js.map +1 -0
  92. package/dist/ocr/baidu_ocr.d.ts +13 -0
  93. package/dist/ocr/baidu_ocr.d.ts.map +1 -0
  94. package/dist/ocr/baidu_ocr.js +80 -0
  95. package/dist/ocr/baidu_ocr.js.map +1 -0
  96. package/dist/ocr/index.d.ts +3 -0
  97. package/dist/ocr/index.d.ts.map +1 -0
  98. package/dist/ocr/index.js +3 -0
  99. package/dist/ocr/index.js.map +1 -0
  100. package/dist/ocr/resume_ocr.d.ts +13 -0
  101. package/dist/ocr/resume_ocr.d.ts.map +1 -0
  102. package/dist/ocr/resume_ocr.js +38 -0
  103. package/dist/ocr/resume_ocr.js.map +1 -0
  104. package/dist/toolset/action.d.ts +12 -0
  105. package/dist/toolset/action.d.ts.map +1 -0
  106. package/dist/toolset/action.js +521 -0
  107. package/dist/toolset/action.js.map +1 -0
  108. package/dist/toolset/baidu_credentials.d.ts +5 -0
  109. package/dist/toolset/baidu_credentials.d.ts.map +1 -0
  110. package/dist/toolset/baidu_credentials.js +24 -0
  111. package/dist/toolset/baidu_credentials.js.map +1 -0
  112. package/dist/toolset/chat.d.ts +10 -0
  113. package/dist/toolset/chat.d.ts.map +1 -0
  114. package/dist/toolset/chat.js +763 -0
  115. package/dist/toolset/chat.js.map +1 -0
  116. package/dist/toolset/deep-search.d.ts +35 -0
  117. package/dist/toolset/deep-search.d.ts.map +1 -0
  118. package/dist/toolset/deep-search.js +1505 -0
  119. package/dist/toolset/deep-search.js.map +1 -0
  120. package/dist/toolset/greet.d.ts +6 -0
  121. package/dist/toolset/greet.d.ts.map +1 -0
  122. package/dist/toolset/greet.js +81 -0
  123. package/dist/toolset/greet.js.map +1 -0
  124. package/dist/toolset/index.d.ts +48 -0
  125. package/dist/toolset/index.d.ts.map +1 -0
  126. package/dist/toolset/index.js +71 -0
  127. package/dist/toolset/index.js.map +1 -0
  128. package/dist/toolset/jd.d.ts +10 -0
  129. package/dist/toolset/jd.d.ts.map +1 -0
  130. package/dist/toolset/jd.js +406 -0
  131. package/dist/toolset/jd.js.map +1 -0
  132. package/dist/toolset/list.d.ts +6 -0
  133. package/dist/toolset/list.d.ts.map +1 -0
  134. package/dist/toolset/list.js +146 -0
  135. package/dist/toolset/list.js.map +1 -0
  136. package/dist/toolset/login.d.ts +6 -0
  137. package/dist/toolset/login.d.ts.map +1 -0
  138. package/dist/toolset/login.js +76 -0
  139. package/dist/toolset/login.js.map +1 -0
  140. package/dist/toolset/normal-search.d.ts +12 -0
  141. package/dist/toolset/normal-search.d.ts.map +1 -0
  142. package/dist/toolset/normal-search.js +301 -0
  143. package/dist/toolset/normal-search.js.map +1 -0
  144. package/dist/toolset/preview.d.ts +5 -0
  145. package/dist/toolset/preview.d.ts.map +1 -0
  146. package/dist/toolset/preview.js +100 -0
  147. package/dist/toolset/preview.js.map +1 -0
  148. package/dist/toolset/recommend.d.ts +36 -0
  149. package/dist/toolset/recommend.d.ts.map +1 -0
  150. package/dist/toolset/recommend.js +428 -0
  151. package/dist/toolset/recommend.js.map +1 -0
  152. package/dist/toolset/send.d.ts +7 -0
  153. package/dist/toolset/send.d.ts.map +1 -0
  154. package/dist/toolset/send.js +52 -0
  155. package/dist/toolset/send.js.map +1 -0
  156. package/package.json +69 -0
  157. package/skills/boss-frontend-analysis/SKILL.md +58 -0
  158. package/skills/boss-frontend-analysis/agents/openai.yaml +4 -0
  159. package/skills/boss-frontend-analysis/scripts/capture_boss_frontend.mjs +604 -0
@@ -0,0 +1,1505 @@
1
+ import { JOB_SEARCH_ACTION_GAP_MS, JOB_SELECT_ACTION_GAP_MS, RESUME_PREVIEW_OPEN_GAP_MS, selectAllModifierKey, sleepRandom, } from '../browser/index.js';
2
+ import { withBossSessionPage } from '../common/boss_session_page.js';
3
+ import { ensurePage } from '../common/ensure_page.js';
4
+ const BOSS_CHAT_AI_FORM_URL = 'https://www.zhipin.com/web/chat/aiform';
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: 18_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 ensureDeepSearchPageReady(page) {
37
+ await ensurePage(page, {
38
+ name: '深度搜索页',
39
+ targetUrl: BOSS_CHAT_AI_FORM_URL,
40
+ matches: isBossChatAiFormUrl,
41
+ });
42
+ await waitForAiFormReady(page);
43
+ }
44
+ async function clickAddConditionInSection(page, titleKeyword) {
45
+ const titleLiteral = JSON.stringify(titleKeyword);
46
+ const clicked = (await page.evaluate(`((titleKeyword) => {
47
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
48
+ function findFormSectionByTitle(kw) {
49
+ const h3s = Array.from(document.querySelectorAll(".form-content .form-content-title-h3"));
50
+ const h3 = h3s.find((el) => norm(el.textContent).includes(kw));
51
+ return h3 ? h3.closest(".form-content") : null;
52
+ }
53
+ const section = findFormSectionByTitle(titleKeyword);
54
+ if (!section) return false;
55
+ const header = section.querySelector(".form-content-header");
56
+ const titleBtn = header?.querySelector(".form-content-title-btn");
57
+ if (!(titleBtn instanceof HTMLElement)) return false;
58
+ if (!norm(titleBtn.textContent).includes("添加条件")) return false;
59
+ titleBtn.scrollIntoView({ block: "center", inline: "nearest" });
60
+ titleBtn.click();
61
+ return true;
62
+ })(${titleLiteral})`));
63
+ if (!clicked) {
64
+ throw new Error(`未找到「${titleKeyword}」区域的「添加条件」。`);
65
+ }
66
+ await sleepRandom(280, 520);
67
+ }
68
+ /** 等待指定区块内第 idx 行已出现在 DOM 中(岗位切换等可能导致列表晚于表单壳渲染)。 */
69
+ async function waitForRequirementRowPresent(page, titleKeyword, rowIndex, timeoutMs = 5000) {
70
+ try {
71
+ await page.waitForFunction((kw, idx) => {
72
+ function norm(v) {
73
+ return (v ?? '').replace(/\s+/g, ' ').trim();
74
+ }
75
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
76
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
77
+ const section = h3 ? h3.closest('.form-content') : null;
78
+ if (!section)
79
+ return false;
80
+ const list = section.querySelector('.form-content-list');
81
+ if (!list)
82
+ return false;
83
+ const items = list.querySelectorAll('.form-content-list-item');
84
+ if (idx < 0 || idx >= items.length)
85
+ return false;
86
+ return !!items[idx].querySelector('.form-content-list-item-title');
87
+ }, { timeout: timeoutMs }, titleKeyword, rowIndex);
88
+ return true;
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ }
94
+ /** 深度搜索条件行:Boss 用 `.form-content-word` 展示文案(挂载很快,超时不宜过长)。 */
95
+ async function waitForFormContentWordInRow(page, titleKeyword, rowIndex, timeoutMs = 1000) {
96
+ try {
97
+ await page.waitForFunction((kw, idx) => {
98
+ function norm(v) {
99
+ return (v ?? '').replace(/\s+/g, ' ').trim();
100
+ }
101
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
102
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
103
+ const section = h3 ? h3.closest('.form-content') : null;
104
+ if (!section)
105
+ return false;
106
+ const list = section.querySelector('.form-content-list');
107
+ if (!list)
108
+ return false;
109
+ const items = list.querySelectorAll('.form-content-list-item');
110
+ if (idx < 0 || idx >= items.length)
111
+ return false;
112
+ return !!items[idx].querySelector('.form-content-word');
113
+ }, { timeout: timeoutMs }, titleKeyword, rowIndex);
114
+ return true;
115
+ }
116
+ catch {
117
+ return false;
118
+ }
119
+ }
120
+ function normFormText(s) {
121
+ return s.replace(/\s+/g, ' ').trim();
122
+ }
123
+ /** 校验用:优先读 `.auto-resize-textarea-wrapper` 内 textarea/input,再读其它 input、最后 `.form-content-word`。 */
124
+ async function readRowRequirementShownText(page, titleKeyword, rowIndex) {
125
+ return page.evaluate((kw, idx) => {
126
+ function norm(v) {
127
+ return (v ?? '').replace(/\s+/g, ' ').trim();
128
+ }
129
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
130
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
131
+ const section = h3 ? h3.closest('.form-content') : null;
132
+ if (!section)
133
+ return '';
134
+ const list = section.querySelector('.form-content-list');
135
+ if (!list)
136
+ return '';
137
+ const items = list.querySelectorAll('.form-content-list-item');
138
+ if (idx < 0 || idx >= items.length)
139
+ return '';
140
+ const row = items[idx];
141
+ const wrap = row.querySelector('.auto-resize-textarea-wrapper');
142
+ if (wrap) {
143
+ const ta = wrap.querySelector('textarea, input');
144
+ if (ta instanceof HTMLTextAreaElement) {
145
+ return norm(ta.value);
146
+ }
147
+ if (ta instanceof HTMLInputElement && ta.type !== 'hidden') {
148
+ return norm(ta.value);
149
+ }
150
+ }
151
+ const scope = row.querySelector('.form-content-list-item-content') ?? row;
152
+ const inp = scope.querySelector('input, textarea');
153
+ if (inp instanceof HTMLInputElement && inp.type !== 'hidden') {
154
+ return norm(inp.value);
155
+ }
156
+ if (inp instanceof HTMLTextAreaElement) {
157
+ return norm(inp.value);
158
+ }
159
+ const word = row.querySelector('.form-content-list-item-title .form-content-word') ||
160
+ row.querySelector('.form-content-word');
161
+ return norm(word?.textContent ?? '');
162
+ }, titleKeyword, rowIndex);
163
+ }
164
+ /** 点击行内 `.form-content-list-item-content`,Boss 会在内部挂载 `.auto-resize-textarea-wrapper` + textarea。 */
165
+ async function clickFormContentListItemContent(page, titleKeyword, rowIndex) {
166
+ return page.evaluate((kw, idx) => {
167
+ function norm(v) {
168
+ return (v ?? '').replace(/\s+/g, ' ').trim();
169
+ }
170
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
171
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
172
+ const section = h3 ? h3.closest('.form-content') : null;
173
+ if (!section)
174
+ return false;
175
+ const items = section.querySelectorAll('.form-content-list .form-content-list-item');
176
+ if (idx < 0 || idx >= items.length)
177
+ return false;
178
+ const row = items[idx];
179
+ const content = row.querySelector('.form-content-list-item-content');
180
+ if (!(content instanceof HTMLElement))
181
+ return false;
182
+ content.scrollIntoView({ block: 'center', inline: 'nearest' });
183
+ content.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
184
+ content.click();
185
+ return true;
186
+ }, titleKeyword, rowIndex);
187
+ }
188
+ async function waitForAutoResizeTextareaInRow(page, titleKeyword, rowIndex, timeoutMs = 1800) {
189
+ try {
190
+ await page.waitForFunction((kw, idx) => {
191
+ function norm(v) {
192
+ return (v ?? '').replace(/\s+/g, ' ').trim();
193
+ }
194
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
195
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
196
+ const section = h3 ? h3.closest('.form-content') : null;
197
+ if (!section)
198
+ return false;
199
+ const items = section.querySelectorAll('.form-content-list .form-content-list-item');
200
+ if (idx < 0 || idx >= items.length)
201
+ return false;
202
+ const row = items[idx];
203
+ const wrap = row.querySelector('.auto-resize-textarea-wrapper');
204
+ if (!wrap)
205
+ return false;
206
+ const ta = wrap.querySelector('textarea, input');
207
+ if (ta instanceof HTMLTextAreaElement)
208
+ return true;
209
+ if (ta instanceof HTMLInputElement && ta.type !== 'hidden' && ta.type !== 'button' && ta.type !== 'submit') {
210
+ return true;
211
+ }
212
+ return false;
213
+ }, { timeout: timeoutMs }, titleKeyword, rowIndex);
214
+ return true;
215
+ }
216
+ catch {
217
+ return false;
218
+ }
219
+ }
220
+ async function fillAutoResizeTextareaInRow(page, titleKeyword, rowIndex, value) {
221
+ return page.evaluate((kw, idx, v) => {
222
+ function norm(s) {
223
+ return (s ?? '').replace(/\s+/g, ' ').trim();
224
+ }
225
+ function setNativeValue(el, val) {
226
+ const tracker = el._valueTracker;
227
+ if (tracker && typeof tracker.setValue === 'function') {
228
+ tracker.setValue('');
229
+ }
230
+ const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
231
+ const desc = Object.getOwnPropertyDescriptor(proto, 'value');
232
+ if (desc?.set) {
233
+ desc.set.call(el, val);
234
+ }
235
+ else {
236
+ el.value = val;
237
+ }
238
+ }
239
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
240
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
241
+ const section = h3 ? h3.closest('.form-content') : null;
242
+ if (!section)
243
+ return false;
244
+ const items = section.querySelectorAll('.form-content-list .form-content-list-item');
245
+ if (idx < 0 || idx >= items.length)
246
+ return false;
247
+ const row = items[idx];
248
+ const wrap = row.querySelector('.auto-resize-textarea-wrapper');
249
+ if (!wrap)
250
+ return false;
251
+ const el = wrap.querySelector('textarea, input');
252
+ if (!(el instanceof HTMLTextAreaElement) && !(el instanceof HTMLInputElement))
253
+ return false;
254
+ if (el instanceof HTMLInputElement && (el.type === 'hidden' || el.type === 'button' || el.type === 'submit')) {
255
+ return false;
256
+ }
257
+ el.focus();
258
+ setNativeValue(el, v);
259
+ el.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true, data: v }));
260
+ el.dispatchEvent(new Event('change', { bubbles: true }));
261
+ const titleWrap = row.querySelector('.form-content-list-item-title');
262
+ titleWrap?.classList.remove('error');
263
+ el.blur();
264
+ return true;
265
+ }, titleKeyword, rowIndex, value);
266
+ }
267
+ /** 用视口坐标点击 `.form-content-word` 中心,确保焦点落在真实文案节点上(比 programatic focus 更稳)。 */
268
+ async function clickFormContentWordCenter(page, titleKeyword, rowIndex) {
269
+ const pos = await page.evaluate((kw, idx) => {
270
+ function norm(v) {
271
+ return (v ?? '').replace(/\s+/g, ' ').trim();
272
+ }
273
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
274
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
275
+ const section = h3 ? h3.closest('.form-content') : null;
276
+ if (!section)
277
+ return null;
278
+ const list = section.querySelector('.form-content-list');
279
+ if (!list)
280
+ return null;
281
+ const items = list.querySelectorAll('.form-content-list-item');
282
+ if (idx < 0 || idx >= items.length)
283
+ return null;
284
+ const row = items[idx];
285
+ const word = row.querySelector('.form-content-list-item-title .form-content-word') ||
286
+ row.querySelector('.form-content-word');
287
+ if (!(word instanceof HTMLElement))
288
+ return null;
289
+ word.scrollIntoView({ block: 'center', inline: 'nearest' });
290
+ const r = word.getBoundingClientRect();
291
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
292
+ }, titleKeyword, rowIndex);
293
+ if (!pos || !(pos.x > 0 && pos.y > 0)) {
294
+ return false;
295
+ }
296
+ await page.mouse.click(pos.x, pos.y);
297
+ return true;
298
+ }
299
+ async function tryFillRowViaDomEvaluate(page, titleKeyword, rowIndex, value) {
300
+ return page.evaluate((kw, idx, v) => {
301
+ function norm(s) {
302
+ return (s ?? '').replace(/\s+/g, ' ').trim();
303
+ }
304
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
305
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
306
+ const section = h3 ? h3.closest('.form-content') : null;
307
+ if (!section)
308
+ return false;
309
+ const list = section.querySelector('.form-content-list');
310
+ if (!list)
311
+ return false;
312
+ const items = list.querySelectorAll('.form-content-list-item');
313
+ const row = items[idx];
314
+ if (!row)
315
+ return false;
316
+ row.scrollIntoView({ block: 'center', inline: 'nearest' });
317
+ const scope = row.querySelector('.form-content-list-item-content') ?? row;
318
+ function walkInput(root) {
319
+ const direct = root.querySelectorAll('input, textarea');
320
+ for (let i = 0; i < direct.length; i++) {
321
+ const el = direct[i];
322
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
323
+ if (el.type === 'hidden' || el.type === 'submit' || el.type === 'button')
324
+ continue;
325
+ return el;
326
+ }
327
+ }
328
+ for (const el of Array.from(root.querySelectorAll('*'))) {
329
+ if (el.shadowRoot) {
330
+ const f = walkInput(el.shadowRoot);
331
+ if (f)
332
+ return f;
333
+ }
334
+ }
335
+ return null;
336
+ }
337
+ const inp = walkInput(scope);
338
+ if (inp) {
339
+ const tracker = inp._valueTracker;
340
+ if (tracker && typeof tracker.setValue === 'function')
341
+ tracker.setValue('');
342
+ const proto = inp instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
343
+ const desc = Object.getOwnPropertyDescriptor(proto, 'value');
344
+ if (desc?.set)
345
+ desc.set.call(inp, v);
346
+ else
347
+ inp.value = v;
348
+ inp.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true, data: v }));
349
+ inp.dispatchEvent(new Event('change', { bubbles: true }));
350
+ return true;
351
+ }
352
+ const word = row.querySelector('.form-content-list-item-title .form-content-word') ||
353
+ row.querySelector('.form-content-word');
354
+ if (word instanceof HTMLElement) {
355
+ word.focus();
356
+ word.click();
357
+ try {
358
+ document.execCommand('selectAll', false);
359
+ document.execCommand('insertText', false, v);
360
+ }
361
+ catch {
362
+ /* 由外层 CDP/键盘兜底 */
363
+ }
364
+ return true;
365
+ }
366
+ return false;
367
+ }, titleKeyword, rowIndex, value);
368
+ }
369
+ /** 必须与目标文案一致(规范化空白后);不用 includes,避免整段终端/回显误匹配。 */
370
+ function wordTextMatchesExpected(shown, expected) {
371
+ return normFormText(shown) === normFormText(expected);
372
+ }
373
+ /**
374
+ * 直接改 DOM:文案在 `.form-content-list-item-title` > `.form-content-word` 内,无原生 input。
375
+ * 同步写 inner 文本并向外层派发 input/change,并尝试去掉 error 态 class。
376
+ */
377
+ async function setFormContentWordDirect(page, titleKeyword, rowIndex, value) {
378
+ return page.evaluate((kw, idx, v) => {
379
+ function norm(s) {
380
+ return (s ?? '').replace(/\s+/g, ' ').trim();
381
+ }
382
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
383
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
384
+ const section = h3 ? h3.closest('.form-content') : null;
385
+ if (!section)
386
+ return false;
387
+ const list = section.querySelector('.form-content-list');
388
+ if (!list)
389
+ return false;
390
+ const items = list.querySelectorAll('.form-content-list-item');
391
+ if (idx < 0 || idx >= items.length)
392
+ return false;
393
+ const row = items[idx];
394
+ const titleWrap = row.querySelector('.form-content-list-item-title');
395
+ let word = row.querySelector('.form-content-list-item-title .form-content-word') ||
396
+ row.querySelector('.form-content-word');
397
+ if (!(word instanceof HTMLElement)) {
398
+ if (titleWrap instanceof HTMLElement) {
399
+ word = document.createElement('div');
400
+ word.className = 'form-content-word';
401
+ titleWrap.appendChild(word);
402
+ }
403
+ else {
404
+ return false;
405
+ }
406
+ }
407
+ word.textContent = v;
408
+ if (titleWrap instanceof HTMLElement) {
409
+ titleWrap.classList.remove('error');
410
+ }
411
+ function fire(el) {
412
+ try {
413
+ el.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true, data: v, inputType: 'insertText' }));
414
+ }
415
+ catch {
416
+ el.dispatchEvent(new Event('input', { bubbles: true }));
417
+ }
418
+ el.dispatchEvent(new Event('change', { bubbles: true }));
419
+ }
420
+ fire(word);
421
+ if (titleWrap instanceof HTMLElement) {
422
+ fire(titleWrap);
423
+ }
424
+ return true;
425
+ }, titleKeyword, rowIndex, value);
426
+ }
427
+ /**
428
+ * 在区块内按行下标写入一条条件(有内容则直接覆盖)。
429
+ * Boss:先点 `.form-content-list-item-content`,再填 `.auto-resize-textarea-wrapper` 内 textarea;失败再改 word div / 键盘。
430
+ */
431
+ async function fillRowAtIndexInSection(page, titleKeyword, rowIndex, text) {
432
+ const rowPresent = await waitForRequirementRowPresent(page, titleKeyword, rowIndex);
433
+ if (!rowPresent) {
434
+ return false;
435
+ }
436
+ const contentClicked = await clickFormContentListItemContent(page, titleKeyword, rowIndex);
437
+ let clickedIndex = rowIndex;
438
+ if (!contentClicked) {
439
+ clickedIndex = await page.evaluate((kw, idx) => {
440
+ function norm(v) {
441
+ return (v ?? '').replace(/\s+/g, ' ').trim();
442
+ }
443
+ function findFormSectionByTitle(keyword) {
444
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
445
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(keyword));
446
+ const section = h3 ? h3.closest('.form-content') : null;
447
+ if (!section?.querySelector('.form-content-list')) {
448
+ return null;
449
+ }
450
+ return section;
451
+ }
452
+ const section = findFormSectionByTitle(kw);
453
+ if (!section)
454
+ return -1;
455
+ const list = section.querySelector('.form-content-list');
456
+ if (!list)
457
+ return -1;
458
+ const items = list.querySelectorAll('.form-content-list-item');
459
+ if (idx < 0 || idx >= items.length)
460
+ return -1;
461
+ const row = items[idx];
462
+ const titleEl = row.querySelector('.form-content-list-item-title');
463
+ if (!(titleEl instanceof HTMLElement))
464
+ return -1;
465
+ titleEl.scrollIntoView({ block: 'center', inline: 'nearest' });
466
+ titleEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
467
+ titleEl.click();
468
+ return idx;
469
+ }, titleKeyword, rowIndex);
470
+ if (clickedIndex < 0) {
471
+ return false;
472
+ }
473
+ }
474
+ await sleepRandom(120, 280);
475
+ const tryVerify = async () => {
476
+ await sleepRandom(40, 100);
477
+ const shown = await readRowRequirementShownText(page, titleKeyword, clickedIndex);
478
+ return wordTextMatchesExpected(shown, text);
479
+ };
480
+ let hasAutoResize = await waitForAutoResizeTextareaInRow(page, titleKeyword, clickedIndex, 1800);
481
+ if (!hasAutoResize && contentClicked) {
482
+ await clickFormContentListItemContent(page, titleKeyword, clickedIndex);
483
+ await sleepRandom(80, 160);
484
+ hasAutoResize = await waitForAutoResizeTextareaInRow(page, titleKeyword, clickedIndex, 1200);
485
+ }
486
+ if (hasAutoResize) {
487
+ await fillAutoResizeTextareaInRow(page, titleKeyword, clickedIndex, text);
488
+ if (await tryVerify()) {
489
+ await page.keyboard.press('Tab').catch(() => { });
490
+ return true;
491
+ }
492
+ await page.evaluate((kw, idx) => {
493
+ function norm(v) {
494
+ return (v ?? '').replace(/\s+/g, ' ').trim();
495
+ }
496
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
497
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
498
+ const section = h3 ? h3.closest('.form-content') : null;
499
+ if (!section)
500
+ return;
501
+ const items = section.querySelectorAll('.form-content-list .form-content-list-item');
502
+ const row = items[idx];
503
+ const wrap = row?.querySelector('.auto-resize-textarea-wrapper');
504
+ const ta = wrap?.querySelector('textarea, input');
505
+ if (ta instanceof HTMLElement) {
506
+ ta.focus();
507
+ ta.click();
508
+ }
509
+ }, titleKeyword, clickedIndex);
510
+ await sleepRandom(50, 120);
511
+ await fillAutoResizeTextareaInRow(page, titleKeyword, clickedIndex, text);
512
+ if (await tryVerify()) {
513
+ await page.keyboard.press('Tab').catch(() => { });
514
+ return true;
515
+ }
516
+ }
517
+ let hasWord = await waitForFormContentWordInRow(page, titleKeyword, clickedIndex, 2000);
518
+ if (!hasWord) {
519
+ await page.evaluate((kw, idx) => {
520
+ function norm(v) {
521
+ return (v ?? '').replace(/\s+/g, ' ').trim();
522
+ }
523
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
524
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
525
+ const section = h3 ? h3.closest('.form-content') : null;
526
+ if (!section)
527
+ return;
528
+ const list = section.querySelector('.form-content-list');
529
+ if (!list)
530
+ return;
531
+ const items = list.querySelectorAll('.form-content-list-item');
532
+ const row = items[idx];
533
+ const titleEl = row?.querySelector('.form-content-list-item-title');
534
+ if (titleEl instanceof HTMLElement) {
535
+ titleEl.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true, view: window }));
536
+ titleEl.click();
537
+ }
538
+ }, titleKeyword, clickedIndex);
539
+ await sleepRandom(120, 240);
540
+ hasWord = await waitForFormContentWordInRow(page, titleKeyword, clickedIndex, 1200);
541
+ }
542
+ if (await setFormContentWordDirect(page, titleKeyword, clickedIndex, text)) {
543
+ if (await tryVerify()) {
544
+ await page.keyboard.press('Tab').catch(() => { });
545
+ return true;
546
+ }
547
+ }
548
+ await page.evaluate(() => new Promise((resolve) => {
549
+ requestAnimationFrame(() => {
550
+ requestAnimationFrame(() => resolve());
551
+ });
552
+ }));
553
+ if (await setFormContentWordDirect(page, titleKeyword, clickedIndex, text)) {
554
+ if (await tryVerify()) {
555
+ await page.keyboard.press('Tab').catch(() => { });
556
+ return true;
557
+ }
558
+ }
559
+ await tryFillRowViaDomEvaluate(page, titleKeyword, clickedIndex, text);
560
+ if (await tryVerify()) {
561
+ await page.keyboard.press('Tab').catch(() => { });
562
+ return true;
563
+ }
564
+ if (hasWord) {
565
+ await clickFormContentWordCenter(page, titleKeyword, clickedIndex);
566
+ await sleepRandom(60, 120);
567
+ await page.evaluate((kw, idx) => {
568
+ function norm(v) {
569
+ return (v ?? '').replace(/\s+/g, ' ').trim();
570
+ }
571
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
572
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
573
+ const section = h3 ? h3.closest('.form-content') : null;
574
+ if (!section)
575
+ return;
576
+ const items = section.querySelectorAll('.form-content-list .form-content-list-item');
577
+ const row = items[idx];
578
+ const word = row?.querySelector('.form-content-list-item-title .form-content-word') ||
579
+ row?.querySelector('.form-content-word');
580
+ if (word instanceof HTMLElement) {
581
+ word.focus();
582
+ }
583
+ }, titleKeyword, clickedIndex);
584
+ }
585
+ else {
586
+ await page.evaluate((kw, idx) => {
587
+ function norm(v) {
588
+ return (v ?? '').replace(/\s+/g, ' ').trim();
589
+ }
590
+ const h3s = Array.from(document.querySelectorAll('.form-content .form-content-title-h3'));
591
+ const h3 = h3s.find((el) => norm(el.textContent ?? '').includes(kw));
592
+ const section = h3 ? h3.closest('.form-content') : null;
593
+ if (!section)
594
+ return;
595
+ const items = section.querySelectorAll('.form-content-list .form-content-list-item');
596
+ const row = items[idx];
597
+ const titleEl = row?.querySelector('.form-content-list-item-title');
598
+ if (titleEl instanceof HTMLElement) {
599
+ titleEl.scrollIntoView({ block: 'center', inline: 'nearest' });
600
+ titleEl.focus();
601
+ titleEl.click();
602
+ }
603
+ }, titleKeyword, clickedIndex);
604
+ await sleepRandom(60, 100);
605
+ }
606
+ const selectAllMod = selectAllModifierKey();
607
+ await page.keyboard.down(selectAllMod);
608
+ await page.keyboard.press('KeyA');
609
+ await page.keyboard.up(selectAllMod);
610
+ await sleepRandom(40, 80);
611
+ await page.keyboard.type(text, { delay: 12 });
612
+ await page.keyboard.press('Tab');
613
+ await sleepRandom(50, 100);
614
+ if (await tryVerify()) {
615
+ return true;
616
+ }
617
+ return false;
618
+ }
619
+ /**
620
+ * 每条条件按顺序对应第 0、1、2… 行:有行则覆盖内容;行不够则点「添加条件」再填。
621
+ */
622
+ async function applyLinesToSection(page, titleKeyword, lines) {
623
+ let processed = 0;
624
+ let nextRowIndex = 0;
625
+ for (const raw of lines) {
626
+ const text = raw.trim();
627
+ if (!text) {
628
+ continue;
629
+ }
630
+ let ok = await fillRowAtIndexInSection(page, titleKeyword, nextRowIndex, text);
631
+ if (!ok) {
632
+ await clickAddConditionInSection(page, titleKeyword);
633
+ ok = await fillRowAtIndexInSection(page, titleKeyword, nextRowIndex, text);
634
+ }
635
+ if (!ok) {
636
+ throw new Error(`「${titleKeyword}」第 ${processed + 1} 条条件无法填入(该区块无可用行或「添加条件」后仍失败)。`);
637
+ }
638
+ processed += 1;
639
+ nextRowIndex += 1;
640
+ }
641
+ }
642
+ async function ensureRequirementGroupRow(page, titleKeyword, rowIndex) {
643
+ const titleLiteral = JSON.stringify(titleKeyword);
644
+ for (let round = 0; round < 5; round++) {
645
+ const state = (await page.evaluate(`(() => {
646
+ const titleKeyword = ${titleLiteral};
647
+ const rowIndex = ${rowIndex};
648
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
649
+ function getGroup(title) {
650
+ const headers = Array.from(document.querySelectorAll(".form-content-header"));
651
+ const header = headers.find((el) => {
652
+ const h = norm(el.querySelector(".form-content-title-h3")?.textContent);
653
+ return h.includes(title);
654
+ });
655
+ if (!(header instanceof HTMLElement)) return null;
656
+ let node = header.nextElementSibling;
657
+ while (node && !node.classList.contains("form-content-header")) {
658
+ if (node.classList.contains("form-content-list")) return { header, list: node };
659
+ node = node.nextElementSibling;
660
+ }
661
+ return null;
662
+ }
663
+ const group = getGroup(titleKeyword);
664
+ if (!group) return { ok: false, reason: "missing_group", count: 0, x: 0, y: 0 };
665
+ const count = group.list.querySelectorAll(".form-content-list-item").length;
666
+ if (count > rowIndex) return { ok: true, count, x: 0, y: 0 };
667
+ const add = group.header.querySelector(".form-content-title-btn");
668
+ if (!(add instanceof HTMLElement)) return { ok: false, reason: "missing_add_button", count, x: 0, y: 0 };
669
+ const disabled =
670
+ add.getAttribute("aria-disabled") === "true" ||
671
+ add.classList.contains("disabled") ||
672
+ add.classList.contains("is-disabled");
673
+ if (disabled) return { ok: false, reason: "add_button_disabled", count, x: 0, y: 0 };
674
+ add.scrollIntoView({ block: "center", inline: "nearest" });
675
+ const rect = add.getBoundingClientRect();
676
+ return { ok: true, count, x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
677
+ })()`));
678
+ if (!state.ok) {
679
+ throw new Error(`未找到「${titleKeyword}」分组或添加条件按钮:${state.reason ?? 'unknown'}`);
680
+ }
681
+ if (state.count > rowIndex) {
682
+ return;
683
+ }
684
+ await page.keyboard.press('Tab').catch(() => { });
685
+ await sleepRandom(80, 140);
686
+ await page.mouse.click(state.x, state.y);
687
+ try {
688
+ await page.waitForFunction(`(() => {
689
+ const titleKeyword = ${titleLiteral};
690
+ const rowIndex = ${rowIndex};
691
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
692
+ const headers = Array.from(document.querySelectorAll(".form-content-header"));
693
+ const header = headers.find((el) => {
694
+ const h = norm(el.querySelector(".form-content-title-h3")?.textContent);
695
+ return h.includes(titleKeyword);
696
+ });
697
+ if (!header) return false;
698
+ let node = header.nextElementSibling;
699
+ while (node && !node.classList.contains("form-content-header")) {
700
+ if (node.classList.contains("form-content-list")) {
701
+ return node.querySelectorAll(".form-content-list-item").length > rowIndex;
702
+ }
703
+ node = node.nextElementSibling;
704
+ }
705
+ return false;
706
+ })()`, { timeout: 1600 });
707
+ return;
708
+ }
709
+ catch {
710
+ await sleepRandom(180, 260);
711
+ }
712
+ }
713
+ throw new Error(`「${titleKeyword}」第 ${rowIndex + 1} 条条件未能创建。`);
714
+ }
715
+ async function fillRequirementGroupRow(page, titleKeyword, rowIndex, value) {
716
+ const titleLiteral = JSON.stringify(titleKeyword);
717
+ const valueLiteral = JSON.stringify(value);
718
+ const clicked = (await page.evaluate(`(() => {
719
+ const titleKeyword = ${titleLiteral};
720
+ const rowIndex = ${rowIndex};
721
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
722
+ function getList(title) {
723
+ const headers = Array.from(document.querySelectorAll(".form-content-header"));
724
+ const header = headers.find((el) => {
725
+ const h = norm(el.querySelector(".form-content-title-h3")?.textContent);
726
+ return h.includes(title);
727
+ });
728
+ if (!header) return null;
729
+ let node = header.nextElementSibling;
730
+ while (node && !node.classList.contains("form-content-header")) {
731
+ if (node.classList.contains("form-content-list")) return node;
732
+ node = node.nextElementSibling;
733
+ }
734
+ return null;
735
+ }
736
+ const list = getList(titleKeyword);
737
+ const row = list?.querySelectorAll(".form-content-list-item")[rowIndex];
738
+ if (!(row instanceof HTMLElement)) return false;
739
+ const target =
740
+ row.querySelector('[contenteditable="true"].auto-resize-textarea, [contenteditable="true"]') ||
741
+ row.querySelector(".form-content-list-item-content, .form-content-list-item-title") ||
742
+ row;
743
+ if (!(target instanceof HTMLElement)) return false;
744
+ target.scrollIntoView({ block: "center", inline: "nearest" });
745
+ target.click();
746
+ return true;
747
+ })()`));
748
+ if (!clicked) {
749
+ return false;
750
+ }
751
+ await sleepRandom(120, 220);
752
+ const selectAllMod = selectAllModifierKey();
753
+ await page.keyboard.down(selectAllMod);
754
+ await page.keyboard.press('KeyA');
755
+ await page.keyboard.up(selectAllMod);
756
+ await sleepRandom(40, 80);
757
+ await page.keyboard.type(value, { delay: 12 });
758
+ await page.keyboard.press('Tab');
759
+ await sleepRandom(120, 220);
760
+ const typedShown = (await page.evaluate(`(() => {
761
+ const titleKeyword = ${titleLiteral};
762
+ const rowIndex = ${rowIndex};
763
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
764
+ function getList(title) {
765
+ const headers = Array.from(document.querySelectorAll(".form-content-header"));
766
+ const header = headers.find((el) => {
767
+ const h = norm(el.querySelector(".form-content-title-h3")?.textContent);
768
+ return h.includes(title);
769
+ });
770
+ if (!header) return null;
771
+ let node = header.nextElementSibling;
772
+ while (node && !node.classList.contains("form-content-header")) {
773
+ if (node.classList.contains("form-content-list")) return node;
774
+ node = node.nextElementSibling;
775
+ }
776
+ return null;
777
+ }
778
+ const list = getList(titleKeyword);
779
+ const row = list?.querySelectorAll(".form-content-list-item")[rowIndex];
780
+ if (!(row instanceof HTMLElement)) return "";
781
+ const word = row.querySelector(".form-content-word");
782
+ const editable = row.querySelector('[contenteditable="true"]');
783
+ const rowInput = row.querySelector("textarea, input");
784
+ if (word && norm(word.textContent)) return norm(word.textContent);
785
+ if (editable && norm(editable.textContent)) return norm(editable.textContent);
786
+ if (rowInput && norm(rowInput.value)) return norm(rowInput.value);
787
+ return norm(row.textContent);
788
+ })()`));
789
+ if (normFormText(typedShown) === normFormText(value)) {
790
+ return true;
791
+ }
792
+ const shown = (await page.evaluate(`(() => {
793
+ const titleKeyword = ${titleLiteral};
794
+ const rowIndex = ${rowIndex};
795
+ const value = ${valueLiteral};
796
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
797
+ function getList(title) {
798
+ const headers = Array.from(document.querySelectorAll(".form-content-header"));
799
+ const header = headers.find((el) => {
800
+ const h = norm(el.querySelector(".form-content-title-h3")?.textContent);
801
+ return h.includes(title);
802
+ });
803
+ if (!header) return null;
804
+ let node = header.nextElementSibling;
805
+ while (node && !node.classList.contains("form-content-header")) {
806
+ if (node.classList.contains("form-content-list")) return node;
807
+ node = node.nextElementSibling;
808
+ }
809
+ return null;
810
+ }
811
+ function setNativeValue(el, val) {
812
+ const tracker = el._valueTracker;
813
+ if (tracker && typeof tracker.setValue === "function") tracker.setValue("");
814
+ const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
815
+ const desc = Object.getOwnPropertyDescriptor(proto, "value");
816
+ if (desc?.set) desc.set.call(el, val);
817
+ else el.value = val;
818
+ }
819
+ function fire(el) {
820
+ try {
821
+ el.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true, data: value, inputType: "insertText" }));
822
+ } catch {
823
+ el.dispatchEvent(new Event("input", { bubbles: true }));
824
+ }
825
+ el.dispatchEvent(new Event("change", { bubbles: true }));
826
+ }
827
+ const list = getList(titleKeyword);
828
+ const row = list?.querySelectorAll(".form-content-list-item")[rowIndex];
829
+ if (!(row instanceof HTMLElement)) return "";
830
+ const editable = row.querySelector('[contenteditable="true"]');
831
+ let input = row.querySelector("textarea, input");
832
+ if (input instanceof HTMLInputElement && (input.type === "hidden" || input.type === "button" || input.type === "submit")) {
833
+ input = null;
834
+ }
835
+ if (editable instanceof HTMLElement) {
836
+ editable.focus();
837
+ editable.textContent = value;
838
+ fire(editable);
839
+ editable.blur();
840
+ } else if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) {
841
+ input.focus();
842
+ setNativeValue(input, value);
843
+ fire(input);
844
+ input.blur();
845
+ } else {
846
+ const title = row.querySelector(".form-content-list-item-title");
847
+ if (!(title instanceof HTMLElement)) return "";
848
+ let word = title.querySelector(".form-content-word");
849
+ if (!(word instanceof HTMLElement)) {
850
+ word = document.createElement("div");
851
+ word.className = "form-content-word";
852
+ title.appendChild(word);
853
+ }
854
+ word.textContent = value;
855
+ title.classList.remove("error");
856
+ fire(word);
857
+ fire(title);
858
+ }
859
+ fire(row);
860
+ fire(list);
861
+ const word = row.querySelector(".form-content-word");
862
+ const shownEditable = row.querySelector('[contenteditable="true"]');
863
+ const rowInput = row.querySelector("textarea, input");
864
+ if (word && norm(word.textContent)) return norm(word.textContent);
865
+ if (shownEditable && norm(shownEditable.textContent)) return norm(shownEditable.textContent);
866
+ if (rowInput && norm(rowInput.value)) return norm(rowInput.value);
867
+ return norm(row.textContent);
868
+ })()`));
869
+ return normFormText(shown) === normFormText(value);
870
+ }
871
+ async function readRequirementGroupRowCount(page, titleKeyword) {
872
+ const titleLiteral = JSON.stringify(titleKeyword);
873
+ return (await page.evaluate(`(() => {
874
+ const titleKeyword = ${titleLiteral};
875
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
876
+ const headers = Array.from(document.querySelectorAll(".form-content-header"));
877
+ const header = headers.find((el) => {
878
+ const h = norm(el.querySelector(".form-content-title-h3")?.textContent);
879
+ return h.includes(titleKeyword);
880
+ });
881
+ if (!header) return -1;
882
+ let node = header.nextElementSibling;
883
+ while (node && !node.classList.contains("form-content-header")) {
884
+ if (node.classList.contains("form-content-list")) {
885
+ return node.querySelectorAll(".form-content-list-item").length;
886
+ }
887
+ node = node.nextElementSibling;
888
+ }
889
+ return -1;
890
+ })()`));
891
+ }
892
+ async function removeRequirementGroupRowsAfter(page, titleKeyword, desiredCount) {
893
+ const titleLiteral = JSON.stringify(titleKeyword);
894
+ for (let count = await readRequirementGroupRowCount(page, titleKeyword); count > desiredCount; count -= 1) {
895
+ if (count < 0) {
896
+ throw new Error(`未找到「${titleKeyword}」分组。`);
897
+ }
898
+ const targetIndex = count - 1;
899
+ const pos = (await page.evaluate(`(() => {
900
+ const titleKeyword = ${titleLiteral};
901
+ const targetIndex = ${targetIndex};
902
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
903
+ const headers = Array.from(document.querySelectorAll(".form-content-header"));
904
+ const header = headers.find((el) => {
905
+ const h = norm(el.querySelector(".form-content-title-h3")?.textContent);
906
+ return h.includes(titleKeyword);
907
+ });
908
+ if (!header) return null;
909
+ let node = header.nextElementSibling;
910
+ while (node && !node.classList.contains("form-content-header")) {
911
+ if (node.classList.contains("form-content-list")) {
912
+ const row = node.querySelectorAll(".form-content-list-item")[targetIndex];
913
+ const btn = row?.querySelector(".form-content-list-item-btn");
914
+ if (!(btn instanceof HTMLElement)) return null;
915
+ btn.scrollIntoView({ block: "center", inline: "nearest" });
916
+ const rect = btn.getBoundingClientRect();
917
+ return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
918
+ }
919
+ node = node.nextElementSibling;
920
+ }
921
+ return null;
922
+ })()`));
923
+ if (!pos) {
924
+ throw new Error(`「${titleKeyword}」第 ${targetIndex + 1} 条条件未找到删除按钮。`);
925
+ }
926
+ await page.mouse.click(pos.x, pos.y);
927
+ await page.waitForFunction(`(() => {
928
+ const titleKeyword = ${titleLiteral};
929
+ const expectedCount = ${targetIndex};
930
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
931
+ const headers = Array.from(document.querySelectorAll(".form-content-header"));
932
+ const header = headers.find((el) => {
933
+ const h = norm(el.querySelector(".form-content-title-h3")?.textContent);
934
+ return h.includes(titleKeyword);
935
+ });
936
+ if (!header) return false;
937
+ let node = header.nextElementSibling;
938
+ while (node && !node.classList.contains("form-content-header")) {
939
+ if (node.classList.contains("form-content-list")) {
940
+ const items = Array.from(node.querySelectorAll(".form-content-list-item"));
941
+ if (items.length === expectedCount) return true;
942
+ if (expectedCount === 0 && items.length === 1) {
943
+ const item = items[0];
944
+ const word = norm(item.querySelector(".form-content-word")?.textContent);
945
+ const input = norm(item.querySelector("input, textarea")?.value);
946
+ const editable = norm(item.querySelector('[contenteditable="true"]')?.textContent);
947
+ return !word && !input && !editable;
948
+ }
949
+ return false;
950
+ }
951
+ node = node.nextElementSibling;
952
+ }
953
+ return false;
954
+ })()`, { timeout: 1800 });
955
+ }
956
+ }
957
+ async function applyLinesToRequirementGroup(page, titleKeyword, lines) {
958
+ let nextRowIndex = 0;
959
+ for (const raw of lines) {
960
+ const text = raw.trim();
961
+ if (!text) {
962
+ continue;
963
+ }
964
+ await ensureRequirementGroupRow(page, titleKeyword, nextRowIndex);
965
+ const ok = await fillRequirementGroupRow(page, titleKeyword, nextRowIndex, text);
966
+ if (!ok) {
967
+ throw new Error(`「${titleKeyword}」第 ${nextRowIndex + 1} 条条件未能写入:${text}`);
968
+ }
969
+ nextRowIndex += 1;
970
+ }
971
+ await removeRequirementGroupRowsAfter(page, titleKeyword, nextRowIndex);
972
+ }
973
+ async function applyAiFormRequirementLists(page, opts) {
974
+ if (opts.core !== undefined) {
975
+ await applyLinesToRequirementGroup(page, '核心要求', opts.core);
976
+ }
977
+ if (opts.bonus !== undefined) {
978
+ await applyLinesToRequirementGroup(page, '加分项', opts.bonus);
979
+ }
980
+ }
981
+ async function readSearchFormSnapshot(page) {
982
+ return (await page.evaluate(`(() => {
983
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
984
+ function parseCount(text) {
985
+ const m = String(text || "").match(/\\d+/);
986
+ return m ? Number(m[0]) : null;
987
+ }
988
+ function getGroupList(titleKeyword) {
989
+ const headers = Array.from(document.querySelectorAll(".form-content-header"));
990
+ const header = headers.find((el) => {
991
+ const title = norm(el.querySelector(".form-content-title-h3")?.textContent);
992
+ return title.includes(titleKeyword);
993
+ });
994
+ if (!header) return null;
995
+ let node = header.nextElementSibling;
996
+ while (node && !node.classList.contains("form-content-header")) {
997
+ if (node.classList.contains("form-content-list")) return node;
998
+ node = node.nextElementSibling;
999
+ }
1000
+ return null;
1001
+ }
1002
+ function itemLineText(item, groupTitle) {
1003
+ const word = item.querySelector(".form-content-word");
1004
+ const w = word ? norm(word.textContent) : "";
1005
+ if (w) return w;
1006
+ const inp = item.querySelector("input, textarea");
1007
+ if (inp && norm(inp.value)) return norm(inp.value);
1008
+ const ce = item.querySelector("[contenteditable='true']");
1009
+ if (ce) return norm(ce.textContent);
1010
+ const titleEl = item.querySelector(".form-content-list-item-title");
1011
+ const title = titleEl ? norm(titleEl.textContent) : "";
1012
+ if (!title) return "";
1013
+ if (title.includes("请至少输入") || title.includes("可输入")) return "";
1014
+ if (groupTitle && title.includes(groupTitle)) return "";
1015
+ return title;
1016
+ }
1017
+ function readGroup(titleKeyword) {
1018
+ const list = getGroupList(titleKeyword);
1019
+ if (!list) return [];
1020
+ return Array.from(list.querySelectorAll(".form-content-list-item"))
1021
+ .map((item) => itemLineText(item, titleKeyword))
1022
+ .filter(Boolean);
1023
+ }
1024
+ const selectedJob = norm(document.querySelector(".job-dropmenu-select .job-main-text")?.textContent);
1025
+ const remainingCountText = norm(document.querySelector(".ai-form-match-footer-text-count")?.textContent);
1026
+ const btn = document.querySelector(".ai-form-match-footer .btn-ai-match-v2");
1027
+ const btnText = norm(btn?.textContent);
1028
+ let matchButtonDisabled = true;
1029
+ if (btn instanceof HTMLElement) {
1030
+ const cls = String(btn.className || "");
1031
+ const style = getComputedStyle(btn);
1032
+ matchButtonDisabled =
1033
+ btn.getAttribute("disabled") !== null ||
1034
+ /disabled|forbid|ban/i.test(cls) ||
1035
+ style.pointerEvents === "none" ||
1036
+ Number(style.opacity) < 0.3;
1037
+ }
1038
+ return {
1039
+ selectedJob,
1040
+ coreRequirements: readGroup("核心要求"),
1041
+ bonusRequirements: readGroup("加分项"),
1042
+ remainingCountText,
1043
+ remainingCount: parseCount(remainingCountText),
1044
+ matchButtonText: btnText || "未知",
1045
+ matchButtonDisabled,
1046
+ };
1047
+ })()`));
1048
+ }
1049
+ async function waitForAiFormJobDropdownReady(page) {
1050
+ await page.waitForFunction(`(() => {
1051
+ const input = Array.from(
1052
+ document.querySelectorAll(
1053
+ ".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",
1054
+ ),
1055
+ ).find((el) => {
1056
+ if (!(el instanceof HTMLInputElement)) return false;
1057
+ const rect = el.getBoundingClientRect();
1058
+ const style = window.getComputedStyle(el);
1059
+ return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
1060
+ });
1061
+ return !!input;
1062
+ })()`, { timeout: 8_000 });
1063
+ }
1064
+ async function waitForAiFormJobSearchResults(page, keyword) {
1065
+ await page.waitForFunction(`((kw) => {
1066
+ const norm = (v) => (v ?? "").replace(/\\s+/g, "").trim().toLowerCase();
1067
+ const rows = Array.from(
1068
+ document.querySelectorAll(
1069
+ ".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",
1070
+ ),
1071
+ );
1072
+ if (rows.length === 0) return false;
1073
+ return rows.some((el) => {
1074
+ const label = norm(el.querySelector(".job-option-text, .label")?.textContent || el.textContent || "");
1075
+ return label.includes(norm(kw));
1076
+ });
1077
+ })`, { timeout: 10_000 }, keyword);
1078
+ }
1079
+ async function waitForAiFormJobSelected(page, expectedLabel) {
1080
+ await page.waitForFunction(`((label) => {
1081
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
1082
+ const selected = norm(document.querySelector(".job-dropmenu-select .job-main-text")?.textContent);
1083
+ return !!selected && selected === label;
1084
+ })`, { timeout: 10_000 }, expectedLabel);
1085
+ await ensureInDeepSearchPage(page);
1086
+ }
1087
+ export async function selectAiFormJob(page, keyword) {
1088
+ const kw = keyword.trim();
1089
+ if (!kw) {
1090
+ throw new Error('岗位关键字不能为空。');
1091
+ }
1092
+ const kwLiteral = JSON.stringify(kw);
1093
+ const opened = (await page.evaluate(`(() => {
1094
+ const host = document.querySelector(".job-dropmenu-select");
1095
+ if (!(host instanceof HTMLElement)) return false;
1096
+ host.scrollIntoView({ block: "center", inline: "nearest" });
1097
+ host.click();
1098
+ return true;
1099
+ })()`));
1100
+ if (!opened) {
1101
+ throw new Error('未找到深度搜索页岗位下拉(.job-dropmenu-select)。');
1102
+ }
1103
+ await sleepRandom(JOB_SELECT_ACTION_GAP_MS.min, JOB_SELECT_ACTION_GAP_MS.max);
1104
+ await waitForAiFormJobDropdownReady(page);
1105
+ const searched = (await page.evaluate(`(() => {
1106
+ const kw = ${kwLiteral};
1107
+ const inputs = Array.from(
1108
+ document.querySelectorAll(
1109
+ ".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",
1110
+ ),
1111
+ );
1112
+ const input = inputs.find((el) => {
1113
+ if (!(el instanceof HTMLInputElement)) return false;
1114
+ const r = el.getBoundingClientRect();
1115
+ return r.width > 0 && r.height > 0;
1116
+ });
1117
+ if (!input) return false;
1118
+ input.focus();
1119
+ input.value = kw;
1120
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1121
+ input.dispatchEvent(new Event("change", { bubbles: true }));
1122
+ return true;
1123
+ })()`));
1124
+ if (searched) {
1125
+ await sleepRandom(JOB_SEARCH_ACTION_GAP_MS.min, JOB_SEARCH_ACTION_GAP_MS.max);
1126
+ await waitForAiFormJobSearchResults(page, kw);
1127
+ }
1128
+ const picked = (await page.evaluate(`(() => {
1129
+ const kw = ${kwLiteral};
1130
+ const norm = (v) => (v ?? "").replace(/\\s+/g, "").trim().toLowerCase();
1131
+ const rows = Array.from(
1132
+ document.querySelectorAll(
1133
+ ".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",
1134
+ ),
1135
+ );
1136
+ if (rows.length === 0) return { ok: false, reason: "empty" };
1137
+ const target = rows.find((el) => {
1138
+ const label = norm(
1139
+ el.querySelector(".job-option-text, .label")?.textContent || el.textContent || "",
1140
+ );
1141
+ return label.includes(norm(kw));
1142
+ });
1143
+ if (!(target instanceof HTMLElement)) return { ok: false, reason: "not_found" };
1144
+ const label = (
1145
+ target.querySelector(".job-option-text, .label")?.textContent ?? target.textContent ?? ""
1146
+ )
1147
+ .replace(/\\s+/g, " ")
1148
+ .trim();
1149
+ target.scrollIntoView({ block: "center", inline: "nearest" });
1150
+ target.click();
1151
+ return { ok: true, label };
1152
+ })()`));
1153
+ if (!picked.ok) {
1154
+ throw new Error(`未找到匹配岗位「${kw}」。`);
1155
+ }
1156
+ const label = picked.label ?? kw;
1157
+ await sleepRandom(JOB_SELECT_ACTION_GAP_MS.min, JOB_SELECT_ACTION_GAP_MS.max);
1158
+ await waitForAiFormJobSelected(page, label);
1159
+ return label;
1160
+ }
1161
+ /** 深度搜索页当前选中的岗位文案(无则「默认」) */
1162
+ export async function readAiFormSelectedJobLabel(page) {
1163
+ return (await page.evaluate(`(() => {
1164
+ const t = (document.querySelector(".job-dropmenu-select .job-main-text")?.textContent ?? "")
1165
+ .replace(/\\s+/g, " ")
1166
+ .trim();
1167
+ return t.length > 0 ? t : "默认";
1168
+ })()`));
1169
+ }
1170
+ /**
1171
+ * 在深度搜索(aiform)主文档中按姓名打开在线简历预览(与 {@link clickGreetDeepSearch} 同一卡片集合,排除「继续沟通」)。
1172
+ */
1173
+ export async function openDeepSearchResumePreview(page, target) {
1174
+ const raw = target.trim();
1175
+ const targetLiteral = JSON.stringify(raw);
1176
+ const opened = (await page.evaluate(`(() => {
1177
+ const raw = ${targetLiteral};
1178
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
1179
+ const allCards = Array.from(
1180
+ document.querySelectorAll(".geeks-box .geek-card-item, .geek-card-list .geek-card-item"),
1181
+ );
1182
+ if (allCards.length === 0) return false;
1183
+ const cards = allCards.filter((item) => {
1184
+ const chatLabel = norm(item.querySelector(".geek-chat")?.textContent);
1185
+ return !chatLabel.includes("继续沟通");
1186
+ });
1187
+ if (cards.length === 0) return false;
1188
+ const targetCard =
1189
+ cards.find((item) => {
1190
+ const name = norm(item.querySelector(".geek-name")?.textContent);
1191
+ return name === raw || name.includes(raw);
1192
+ }) ?? null;
1193
+ if (!targetCard) return false;
1194
+
1195
+ function tryOpen(el) {
1196
+ if (!(el instanceof HTMLElement)) return false;
1197
+ if (el.classList.contains("disabled")) return false;
1198
+ const st = window.getComputedStyle(el);
1199
+ if (st.pointerEvents === "none" || Number(st.opacity) < 0.3) return false;
1200
+ el.scrollIntoView({ block: "center", inline: "nearest" });
1201
+ el.click();
1202
+ return true;
1203
+ }
1204
+
1205
+ const nameEl = targetCard.querySelector(".geek-name");
1206
+ if (nameEl instanceof HTMLElement) {
1207
+ nameEl.scrollIntoView({ block: "center", inline: "nearest" });
1208
+ nameEl.click();
1209
+ return true;
1210
+ }
1211
+
1212
+ const resumeOnline = targetCard.querySelector("a.resume-btn-online");
1213
+ if (tryOpen(resumeOnline)) return true;
1214
+ const hrefResume = targetCard.querySelector('a[href*="c-resume"], a[href*="frame/c-resume"]');
1215
+ if (tryOpen(hrefResume)) return true;
1216
+
1217
+ const links = Array.from(targetCard.querySelectorAll("a, button, .btn")).filter((node) => {
1218
+ const t = norm(node.textContent);
1219
+ return /在线简历|查看简历|简历预览|预览/.test(t);
1220
+ });
1221
+ if (links.length > 0 && tryOpen(links[0])) return true;
1222
+
1223
+ const geekInfo = targetCard.querySelector(".geek-info, .geek-card-main, .card-content");
1224
+ if (geekInfo instanceof HTMLElement) {
1225
+ geekInfo.scrollIntoView({ block: "center", inline: "nearest" });
1226
+ geekInfo.click();
1227
+ return true;
1228
+ }
1229
+
1230
+ return false;
1231
+ })()`));
1232
+ if (opened) {
1233
+ await sleepRandom(RESUME_PREVIEW_OPEN_GAP_MS.min, RESUME_PREVIEW_OPEN_GAP_MS.max);
1234
+ }
1235
+ return opened;
1236
+ }
1237
+ export async function readDeepSearchGeekList(page) {
1238
+ return (await page.evaluate(`(() => {
1239
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
1240
+ const items = Array.from(
1241
+ document.querySelectorAll(".geeks-box .geek-card-item, .geek-card-list .geek-card-item"),
1242
+ );
1243
+ return items
1244
+ .map((item) => {
1245
+ const chatLabel = norm(item.querySelector(".geek-chat")?.textContent);
1246
+ if (chatLabel.includes("继续沟通")) {
1247
+ return null;
1248
+ }
1249
+ const name = norm(item.querySelector(".geek-name")?.textContent);
1250
+ const splits = Array.from(item.querySelectorAll(".geek-exp .split"))
1251
+ .map((el) => norm(el.getAttribute("title") || el.textContent || ""))
1252
+ .filter(Boolean);
1253
+ const meta = splits.join(" · ");
1254
+ const work = norm(item.querySelector(".geek-works span")?.textContent);
1255
+ const edu = norm(item.querySelector(".geek-edus span")?.textContent);
1256
+ const recEl = item.querySelector(".geek-recommend-text");
1257
+ let reason = "";
1258
+ if (recEl) {
1259
+ reason = norm(recEl.textContent).replace(/^推荐理由\\s*/, "").trim();
1260
+ }
1261
+ return { name, meta, work, edu, reason };
1262
+ })
1263
+ .filter((x) => x !== null);
1264
+ })()`));
1265
+ }
1266
+ export function renderGeekListSection(title, items) {
1267
+ const lines = [title, `共 ${items.length} 人`, ''];
1268
+ items.forEach((g, i) => {
1269
+ const n = i + 1;
1270
+ lines.push(`${n}. ${g.name || '(无姓名)'}`);
1271
+ if (g.meta) {
1272
+ lines.push(` 概要:${g.meta}`);
1273
+ }
1274
+ if (g.work) {
1275
+ lines.push(` 经历:${g.work}`);
1276
+ }
1277
+ if (g.edu) {
1278
+ lines.push(` 教育:${g.edu}`);
1279
+ }
1280
+ if (g.reason) {
1281
+ lines.push(` 推荐:${g.reason}`);
1282
+ }
1283
+ lines.push('');
1284
+ });
1285
+ return lines.join('\n').trimEnd();
1286
+ }
1287
+ export async function clickGreetDeepSearch(page, target) {
1288
+ const targetLiteral = JSON.stringify(target.trim());
1289
+ const result = (await page.evaluate(`(() => {
1290
+ const raw = ${targetLiteral};
1291
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
1292
+ const allCards = Array.from(
1293
+ document.querySelectorAll(".geeks-box .geek-card-item, .geek-card-list .geek-card-item"),
1294
+ );
1295
+ if (allCards.length === 0) {
1296
+ return { kind: "empty" };
1297
+ }
1298
+ const cards = allCards.filter((item) => {
1299
+ const chatLabel = norm(item.querySelector(".geek-chat")?.textContent);
1300
+ return !chatLabel.includes("继续沟通");
1301
+ });
1302
+ if (cards.length === 0) {
1303
+ return { kind: "all_continue" };
1304
+ }
1305
+ const targetCard =
1306
+ cards.find((item) => {
1307
+ const name = norm(item.querySelector(".geek-name")?.textContent);
1308
+ return name === raw || name.includes(raw);
1309
+ }) ?? null;
1310
+ if (!targetCard) {
1311
+ return { kind: "not_found", target: raw };
1312
+ }
1313
+
1314
+ const name = norm(targetCard.querySelector(".geek-name")?.textContent);
1315
+ const btn =
1316
+ targetCard.querySelector(".geek-chat .btn-ai-v2") ||
1317
+ targetCard.querySelector(".geek-chat span.btn-ai-v2") ||
1318
+ targetCard.querySelector(".geek-chat span[class*='btn-ai']");
1319
+ if (!(btn instanceof HTMLElement)) {
1320
+ return { kind: "no_btn", name };
1321
+ }
1322
+ const label = norm(btn.textContent);
1323
+ if (!label.includes("打招呼")) {
1324
+ return { kind: "not_greet", name, label };
1325
+ }
1326
+ const cls = btn.className ?? "";
1327
+ const disabled = /disabled|forbid|ban/i.test(cls) || btn.getAttribute("disabled") !== null;
1328
+ if (disabled) {
1329
+ return { kind: "disabled", name };
1330
+ }
1331
+ btn.scrollIntoView({ block: "center", inline: "nearest" });
1332
+ btn.click();
1333
+ return { kind: "clicked", name };
1334
+ })()`));
1335
+ switch (result.kind) {
1336
+ case 'empty':
1337
+ throw new Error('深度搜索暂无候选人列表,请先在页面点击「立即匹配」后再试。');
1338
+ case 'all_continue':
1339
+ throw new Error('当前列表均为「继续沟通」状态,已无待打招呼人选(与 boss deep-search 列表展示一致)。');
1340
+ case 'not_found':
1341
+ throw new Error(`未在可打招呼的深度搜索列表中找到目标:${result.target}(「继续沟通」人选已排除,请用 boss deep-search 核对姓名)。`);
1342
+ case 'no_btn':
1343
+ throw new Error(`候选人 ${result.name} 缺少「打招呼」按钮,无法执行。`);
1344
+ case 'not_greet':
1345
+ throw new Error(`候选人 ${result.name} 当前按钮为「${result.label}」,无法执行打招呼。`);
1346
+ case 'disabled':
1347
+ throw new Error(`候选人 ${result.name} 的打招呼不可用(可能已打过招呼)。`);
1348
+ case 'clicked':
1349
+ return { message: `已对 ${result.name} 在深度搜索页点击「打招呼」。` };
1350
+ default: {
1351
+ const _x = result;
1352
+ throw new Error(`未知结果:${String(_x)}`);
1353
+ }
1354
+ }
1355
+ }
1356
+ function renderFormSnapshotOnly(snap) {
1357
+ const core = snap.coreRequirements.length > 0 ? snap.coreRequirements.join('|') : '(空)';
1358
+ const bonus = snap.bonusRequirements.length > 0 ? snap.bonusRequirements.join('|') : '(空)';
1359
+ return [
1360
+ '已更新深度搜索表单(未触发「立即匹配」)。',
1361
+ `职位:${snap.selectedJob || '未知职位'}`,
1362
+ `核心要求(${snap.coreRequirements.length}):${core}`,
1363
+ `加分项(${snap.bonusRequirements.length}):${bonus}`,
1364
+ `今日匹配剩余:${snap.remainingCountText || '未知'}`,
1365
+ `来源页面:${BOSS_CHAT_AI_FORM_URL}`,
1366
+ ].join('\n');
1367
+ }
1368
+ function renderSearchFormSummary(snap, actionLine) {
1369
+ const core = snap.coreRequirements.length > 0 ? snap.coreRequirements.join(';') : '(空)';
1370
+ const bonus = snap.bonusRequirements.length > 0 ? snap.bonusRequirements.join(';') : '(空)';
1371
+ return [
1372
+ actionLine,
1373
+ `职位:${snap.selectedJob || '未知职位'}`,
1374
+ `核心要求(${snap.coreRequirements.length}):${core}`,
1375
+ `加分项(${snap.bonusRequirements.length}):${bonus}`,
1376
+ `今日匹配剩余:${snap.remainingCountText || '未知'}`,
1377
+ `匹配按钮:${snap.matchButtonText}${snap.matchButtonDisabled ? '(不可用)' : '(可用)'}`,
1378
+ ].join('\n');
1379
+ }
1380
+ function renderDeepSearchOutput(snap, geeks, actionLine, listTitle = '推荐简历') {
1381
+ return [
1382
+ renderSearchFormSummary(snap, actionLine),
1383
+ '',
1384
+ renderGeekListSection(listTitle, geeks),
1385
+ ].join('\n');
1386
+ }
1387
+ async function triggerAiFormMatch(page) {
1388
+ const before = await readSearchFormSnapshot(page);
1389
+ if (before.coreRequirements.length === 0) {
1390
+ throw new Error('深度搜索至少需要 1 条核心要求;请先使用 --core 添加核心要求。');
1391
+ }
1392
+ if (before.remainingCount !== null && before.remainingCount <= 0) {
1393
+ throw new Error(`今日匹配次数已用完:${before.remainingCountText || '0次'}`);
1394
+ }
1395
+ const beforeCountLiteral = JSON.stringify(before.remainingCountText);
1396
+ const clicked = (await page.evaluate(`(() => {
1397
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
1398
+ const btn = document.querySelector(".ai-form-match-footer .btn-ai-match-v2");
1399
+ if (!(btn instanceof HTMLElement)) return { ok: false, reason: "missing_button" };
1400
+ const cls = String(btn.className || "");
1401
+ const style = getComputedStyle(btn);
1402
+ const disabled =
1403
+ btn.getAttribute("disabled") !== null ||
1404
+ /disabled|forbid|ban/i.test(cls) ||
1405
+ style.pointerEvents === "none" ||
1406
+ Number(style.opacity) < 0.3;
1407
+ if (disabled) return { ok: false, reason: "disabled", text: norm(btn.textContent) };
1408
+ btn.scrollIntoView({ block: "center", inline: "nearest" });
1409
+ btn.click();
1410
+ return { ok: true, text: norm(btn.textContent) };
1411
+ })()`));
1412
+ if (!clicked.ok) {
1413
+ throw new Error(`无法点击「立即匹配」:${clicked.reason ?? 'unknown'}${clicked.text ? `(${clicked.text})` : ''}`);
1414
+ }
1415
+ await sleepRandom(900, 1400);
1416
+ await page.waitForFunction(`(() => {
1417
+ const beforeCount = ${beforeCountLiteral};
1418
+ const norm = (v) => (v ?? "").replace(/\\s+/g, " ").trim();
1419
+ const btnText = norm(document.querySelector(".ai-form-match-footer .btn-ai-match-v2")?.textContent);
1420
+ const countText = norm(document.querySelector(".ai-form-match-footer-text-count")?.textContent);
1421
+ const cards = document.querySelectorAll(".geeks-box .geek-card-item, .geek-card-list .geek-card-item").length;
1422
+ const stillMatching = /停止匹配|匹配中|加载中|生成中/.test(btnText);
1423
+ return !stillMatching && cards > 0 && (!beforeCount || (countText && countText !== beforeCount));
1424
+ })()`, { timeout: 35_000 });
1425
+ await ensureInDeepSearchPage(page);
1426
+ }
1427
+ export async function runBossSearchSet(opts) {
1428
+ const jobKeyword = opts.jobKeyword?.trim();
1429
+ const coreReq = opts.coreRequirements;
1430
+ const bonusReq = opts.bonusRequirements;
1431
+ const hasFormEdit = coreReq !== undefined || bonusReq !== undefined;
1432
+ if (!jobKeyword && !hasFormEdit) {
1433
+ throw new Error('请至少指定 --job/-j、--core/-c 或 --bonus/-b 之一。');
1434
+ }
1435
+ try {
1436
+ return await withBossSessionPage(async (page) => {
1437
+ await ensureDeepSearchPageReady(page);
1438
+ if (jobKeyword) {
1439
+ await selectAiFormJob(page, jobKeyword);
1440
+ await ensureInDeepSearchPage(page);
1441
+ if (hasFormEdit) {
1442
+ await ensureInDeepSearchPage(page);
1443
+ }
1444
+ }
1445
+ if (hasFormEdit) {
1446
+ await applyAiFormRequirementLists(page, {
1447
+ core: coreReq,
1448
+ bonus: bonusReq,
1449
+ });
1450
+ await ensureInDeepSearchPage(page);
1451
+ }
1452
+ const snap = await readSearchFormSnapshot(page);
1453
+ return renderFormSnapshotOnly(snap);
1454
+ });
1455
+ }
1456
+ catch (e) {
1457
+ const message = e instanceof Error ? e.message : String(e);
1458
+ console.error(`[boss-cli] boss_search_set error: ${message}`);
1459
+ throw new Error(`设置深度搜索条件失败:${message}`);
1460
+ }
1461
+ }
1462
+ export async function runBossSearch(opts = {}) {
1463
+ const jobKeyword = opts.jobKeyword?.trim();
1464
+ const coreReq = opts.coreRequirements;
1465
+ const bonusReq = opts.bonusRequirements;
1466
+ const hasFormEdit = coreReq !== undefined || bonusReq !== undefined;
1467
+ const shouldMatch = opts.match === true;
1468
+ try {
1469
+ return await withBossSessionPage(async (page) => {
1470
+ await ensureDeepSearchPageReady(page);
1471
+ if (jobKeyword) {
1472
+ await selectAiFormJob(page, jobKeyword);
1473
+ await ensureInDeepSearchPage(page);
1474
+ }
1475
+ if (hasFormEdit) {
1476
+ await applyAiFormRequirementLists(page, {
1477
+ core: coreReq,
1478
+ bonus: bonusReq,
1479
+ });
1480
+ await ensureInDeepSearchPage(page);
1481
+ }
1482
+ if (shouldMatch) {
1483
+ await triggerAiFormMatch(page);
1484
+ }
1485
+ const snap = await readSearchFormSnapshot(page);
1486
+ const actionLine = shouldMatch
1487
+ ? '深度搜索:已触发「立即匹配」。'
1488
+ : hasFormEdit
1489
+ ? '深度搜索:已更新表单,未触发「立即匹配」。'
1490
+ : '深度搜索:当前表单(未触发「立即匹配」)。';
1491
+ if (!shouldMatch) {
1492
+ return renderSearchFormSummary(snap, actionLine);
1493
+ }
1494
+ const geeks = (await readDeepSearchGeekList(page)).slice(0, 20);
1495
+ const listTitle = shouldMatch ? '本次新增推荐简历(最新20条)' : '推荐简历';
1496
+ return renderDeepSearchOutput(snap, geeks, actionLine, listTitle);
1497
+ });
1498
+ }
1499
+ catch (e) {
1500
+ const message = e instanceof Error ? e.message : String(e);
1501
+ console.error(`[boss-cli] boss_search error: ${message}`);
1502
+ throw new Error(`读取深度搜索列表失败:${message}`);
1503
+ }
1504
+ }
1505
+ //# sourceMappingURL=deep-search.js.map