@reconcrap/boss-recommend-mcp 1.3.38 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -33
- package/package.json +61 -9
- package/skills/boss-recommend-pipeline/SKILL.md +4 -0
- package/src/chat-mcp.js +1333 -0
- package/src/chat-runtime-config.js +559 -0
- package/src/cli.js +1095 -196
- package/src/core/browser/index.js +378 -0
- package/src/core/capture/index.js +298 -0
- package/src/core/cv-acquisition/index.js +219 -0
- package/src/core/greet-quota/index.js +54 -0
- package/src/core/infinite-list/index.js +459 -0
- package/src/core/reporting/legacy-csv.js +332 -0
- package/src/core/run/index.js +286 -0
- package/src/core/screening/index.js +1166 -0
- package/src/core/self-heal/index.js +848 -0
- package/src/domains/chat/cards.js +129 -0
- package/src/domains/chat/constants.js +183 -0
- package/src/domains/chat/detail.js +1369 -0
- package/src/domains/chat/index.js +7 -0
- package/src/domains/chat/jobs.js +334 -0
- package/src/domains/chat/page-guard.js +88 -0
- package/src/domains/chat/roots.js +56 -0
- package/src/domains/chat/run-service.js +1101 -0
- package/src/domains/recommend/actions.js +457 -0
- package/src/domains/recommend/cards.js +228 -0
- package/src/domains/recommend/constants.js +141 -0
- package/src/domains/recommend/detail.js +341 -0
- package/src/domains/recommend/filters.js +581 -0
- package/src/domains/recommend/index.js +10 -0
- package/src/domains/recommend/jobs.js +232 -0
- package/src/domains/recommend/refresh.js +204 -0
- package/src/domains/recommend/roots.js +78 -0
- package/src/domains/recommend/run-service.js +903 -0
- package/src/domains/recommend/scopes.js +245 -0
- package/src/domains/recruit/actions.js +277 -0
- package/src/domains/recruit/cards.js +67 -0
- package/src/domains/recruit/constants.js +130 -0
- package/src/domains/recruit/detail.js +414 -0
- package/src/domains/recruit/index.js +9 -0
- package/src/domains/recruit/instruction-parser.js +451 -0
- package/src/domains/recruit/refresh.js +40 -0
- package/src/domains/recruit/roots.js +68 -0
- package/src/domains/recruit/run-service.js +580 -0
- package/src/domains/recruit/search.js +1149 -0
- package/src/index.js +578 -419
- package/src/recommend-mcp.js +1257 -0
- package/src/recruit-mcp.js +1035 -0
- package/src/adapters.js +0 -3079
- package/src/boss-chat.js +0 -1037
- package/src/pipeline.js +0 -2249
- package/src/recommend-healing-config.js +0 -131
- package/src/recommend-healing-rules.json +0 -261
- package/src/self-heal.js +0 -2237
- package/src/test-adapters-runtime.js +0 -628
- package/src/test-boss-chat.js +0 -3196
- package/src/test-index-async.js +0 -498
- package/src/test-parser.js +0 -742
- package/src/test-pipeline.js +0 -2703
- package/src/test-run-state.js +0 -152
- package/src/test-self-heal.js +0 -224
- package/vendor/boss-chat-cli/README.md +0 -134
- package/vendor/boss-chat-cli/package.json +0 -53
- package/vendor/boss-chat-cli/src/app.js +0 -1501
- package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
- package/vendor/boss-chat-cli/src/cli.js +0 -1713
- package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
- package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
- package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
- package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
- package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
- package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
- package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
- package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
- package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
- package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
- package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
- package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
- package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
- package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -6927
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
- package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2294
- package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
- package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
|
@@ -1,3562 +0,0 @@
|
|
|
1
|
-
import { createCustomerKey } from '../utils/customer-key.js';
|
|
2
|
-
|
|
3
|
-
const CHAT_URL_TOKEN = '/web/chat/index';
|
|
4
|
-
|
|
5
|
-
function normalizeText(value) {
|
|
6
|
-
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function browserGetPageState() {
|
|
10
|
-
const querySelectors = (selectors) => {
|
|
11
|
-
for (const selector of selectors) {
|
|
12
|
-
const found = document.querySelector(selector);
|
|
13
|
-
if (found) return found;
|
|
14
|
-
}
|
|
15
|
-
return null;
|
|
16
|
-
};
|
|
17
|
-
const listContainer = querySelectors([
|
|
18
|
-
'.user-list.b-scroll-stable',
|
|
19
|
-
'.chat-user .user-list',
|
|
20
|
-
'.chat-user .user-container > div > div:nth-child(2)',
|
|
21
|
-
'.chat-user .user-container [class*="list"]',
|
|
22
|
-
]);
|
|
23
|
-
const listItems = document.querySelectorAll('div[role="listitem"]');
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
href: window.location.href,
|
|
27
|
-
readyState: document.readyState,
|
|
28
|
-
hasListContainer: Boolean(listContainer),
|
|
29
|
-
listItemCount: listItems.length,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function browserGetCurrentHref() {
|
|
34
|
-
return { href: window.location.href };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function browserNavigateToChatIndex(options = {}) {
|
|
38
|
-
const chatUrl = 'https://www.zhipin.com/web/chat/index';
|
|
39
|
-
const force = options?.force === true;
|
|
40
|
-
if (force || !String(window.location.href || '').includes('/web/chat/index')) {
|
|
41
|
-
window.location.assign(chatUrl);
|
|
42
|
-
return { ok: true, changed: true, href: chatUrl };
|
|
43
|
-
}
|
|
44
|
-
return { ok: true, changed: false, href: window.location.href };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function browserListJobs() {
|
|
48
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
49
|
-
const isVisible = (el) => {
|
|
50
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
51
|
-
const style = getComputedStyle(el);
|
|
52
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
const rect = el.getBoundingClientRect();
|
|
56
|
-
return rect.width > 2 && rect.height > 2;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const triggerSelectors = [
|
|
60
|
-
'.chat-job-select',
|
|
61
|
-
'.chat-job-selector',
|
|
62
|
-
'.job-selecter',
|
|
63
|
-
'.job-selector',
|
|
64
|
-
'.job-select-wrap',
|
|
65
|
-
'.job-select',
|
|
66
|
-
'.job-select-box',
|
|
67
|
-
'.job-wrap',
|
|
68
|
-
'.chat-job-name',
|
|
69
|
-
'.top-chat-search',
|
|
70
|
-
];
|
|
71
|
-
|
|
72
|
-
for (const selector of triggerSelectors) {
|
|
73
|
-
const trigger = document.querySelector(selector);
|
|
74
|
-
if (trigger && isVisible(trigger)) {
|
|
75
|
-
trigger.click();
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
await new Promise((resolve) => window.setTimeout(resolve, 180));
|
|
81
|
-
|
|
82
|
-
const items = Array.from(
|
|
83
|
-
document.querySelectorAll('.ui-dropmenu-list li[value], .dropmenu-list li[value]'),
|
|
84
|
-
);
|
|
85
|
-
const jobs = [];
|
|
86
|
-
const seen = new Set();
|
|
87
|
-
for (const item of items) {
|
|
88
|
-
const label = normalize(item.textContent || '');
|
|
89
|
-
const value = normalize(item.getAttribute('value') || item.dataset?.value || '');
|
|
90
|
-
if (!label) continue;
|
|
91
|
-
const key = `${value}__${label}`;
|
|
92
|
-
if (seen.has(key)) continue;
|
|
93
|
-
seen.add(key);
|
|
94
|
-
jobs.push({
|
|
95
|
-
value: value || null,
|
|
96
|
-
label,
|
|
97
|
-
active: item.classList.contains('active') || item.classList.contains('curr'),
|
|
98
|
-
visible: isVisible(item),
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
ok: true,
|
|
104
|
-
jobs,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function browserSelectJob(jobSelection) {
|
|
109
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
110
|
-
const isVisible = (el) => {
|
|
111
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
112
|
-
const style = getComputedStyle(el);
|
|
113
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
const rect = el.getBoundingClientRect();
|
|
117
|
-
return rect.width > 2 && rect.height > 2;
|
|
118
|
-
};
|
|
119
|
-
const targetValue = normalize(jobSelection?.value || '');
|
|
120
|
-
const targetLabel = normalize(jobSelection?.label || '');
|
|
121
|
-
|
|
122
|
-
const triggerSelectors = [
|
|
123
|
-
'.chat-job-select',
|
|
124
|
-
'.chat-job-selector',
|
|
125
|
-
'.job-selecter',
|
|
126
|
-
'.job-selector',
|
|
127
|
-
'.job-select-wrap',
|
|
128
|
-
'.job-select',
|
|
129
|
-
'.job-select-box',
|
|
130
|
-
'.job-wrap',
|
|
131
|
-
'.chat-job-name',
|
|
132
|
-
'.top-chat-search',
|
|
133
|
-
];
|
|
134
|
-
for (const selector of triggerSelectors) {
|
|
135
|
-
const trigger = document.querySelector(selector);
|
|
136
|
-
if (trigger && isVisible(trigger)) {
|
|
137
|
-
trigger.click();
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
await new Promise((resolve) => window.setTimeout(resolve, 180));
|
|
142
|
-
|
|
143
|
-
const items = Array.from(
|
|
144
|
-
document.querySelectorAll('.ui-dropmenu-list li[value], .dropmenu-list li[value]'),
|
|
145
|
-
);
|
|
146
|
-
if (items.length === 0) {
|
|
147
|
-
return { ok: false, error: 'JOB_OPTIONS_NOT_FOUND' };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const target = items.find((item) => {
|
|
151
|
-
const value = normalize(item.getAttribute('value') || item.dataset?.value || '');
|
|
152
|
-
const label = normalize(item.textContent || '');
|
|
153
|
-
return (targetValue && value === targetValue) || (targetLabel && label === targetLabel);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
if (!target) {
|
|
157
|
-
return { ok: false, error: 'JOB_OPTION_NOT_FOUND' };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
target.click();
|
|
161
|
-
await new Promise((resolve) => window.setTimeout(resolve, 280));
|
|
162
|
-
|
|
163
|
-
const refreshed = Array.from(
|
|
164
|
-
document.querySelectorAll('.ui-dropmenu-list li[value], .dropmenu-list li[value]'),
|
|
165
|
-
);
|
|
166
|
-
const selected = refreshed.find((item) => item.classList.contains('active') || item.classList.contains('curr'));
|
|
167
|
-
const selectedLabel = normalize(selected?.textContent || '');
|
|
168
|
-
const selectedValue = normalize(selected?.getAttribute('value') || selected?.dataset?.value || '');
|
|
169
|
-
const matched =
|
|
170
|
-
(targetValue && selectedValue === targetValue) || (targetLabel && selectedLabel === targetLabel);
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
ok: true,
|
|
174
|
-
matched,
|
|
175
|
-
selected: {
|
|
176
|
-
value: selectedValue || null,
|
|
177
|
-
label: selectedLabel || null,
|
|
178
|
-
},
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function browserActivateFilterTab(label) {
|
|
183
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
184
|
-
const isVisible = (node) => {
|
|
185
|
-
if (!(node instanceof HTMLElement)) return false;
|
|
186
|
-
const rect = node.getBoundingClientRect();
|
|
187
|
-
if (rect.width <= 2 || rect.height <= 2) return false;
|
|
188
|
-
const style = getComputedStyle(node);
|
|
189
|
-
return style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || '1') > 0.01;
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const getMessageFilterCandidates = () => {
|
|
193
|
-
const containers = [
|
|
194
|
-
...Array.from(document.querySelectorAll('.chat-message-filter-left')),
|
|
195
|
-
...Array.from(document.querySelectorAll('.chat-message-filter')),
|
|
196
|
-
].filter(isVisible);
|
|
197
|
-
for (const container of containers) {
|
|
198
|
-
const scoped = Array.from(container.querySelectorAll('span,button,a,div'))
|
|
199
|
-
.filter((node) => isVisible(node))
|
|
200
|
-
.filter((node) => ['全部', '未读'].includes(normalize(node.textContent || '')));
|
|
201
|
-
if (scoped.length > 0) {
|
|
202
|
-
return scoped;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return [];
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
const getActiveFilterLabel = () => {
|
|
209
|
-
const activeNode = Array.from(
|
|
210
|
-
document.querySelectorAll(
|
|
211
|
-
'.chat-message-filter-left span.active, .chat-message-filter span.active, .chat-message-filter-left .active, .chat-message-filter .active',
|
|
212
|
-
),
|
|
213
|
-
).find((node) => isVisible(node));
|
|
214
|
-
return normalize(activeNode?.textContent || '');
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
const hasActiveState = (node) => {
|
|
218
|
-
let current = node;
|
|
219
|
-
let depth = 0;
|
|
220
|
-
while (current instanceof HTMLElement && depth < 4) {
|
|
221
|
-
if (
|
|
222
|
-
current.classList.contains('active') ||
|
|
223
|
-
current.classList.contains('curr') ||
|
|
224
|
-
current.getAttribute('aria-selected') === 'true' ||
|
|
225
|
-
current.getAttribute('data-active') === 'true'
|
|
226
|
-
) {
|
|
227
|
-
return true;
|
|
228
|
-
}
|
|
229
|
-
current = current.parentElement;
|
|
230
|
-
depth += 1;
|
|
231
|
-
}
|
|
232
|
-
return false;
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
const messageFilterCandidates = getMessageFilterCandidates();
|
|
236
|
-
const candidates = (
|
|
237
|
-
messageFilterCandidates.length > 0
|
|
238
|
-
? messageFilterCandidates
|
|
239
|
-
: Array.from(document.querySelectorAll('span,button,div,li,a,[role="tab"]'))
|
|
240
|
-
).filter((node) => normalize(node.textContent || '') === label && isVisible(node));
|
|
241
|
-
|
|
242
|
-
if (candidates.length === 0) {
|
|
243
|
-
return { ok: false, error: `FILTER_TAB_NOT_FOUND:${label}` };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const active = candidates.find((node) => hasActiveState(node));
|
|
247
|
-
const activeLabelBefore = getActiveFilterLabel();
|
|
248
|
-
if (active || activeLabelBefore === label) {
|
|
249
|
-
return { ok: true, changed: false, verified: true, activeLabel: activeLabelBefore || label };
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const target = candidates[0];
|
|
253
|
-
const parentFilterItem = target.closest('.chat-message-filter-left span, .chat-message-filter-left [class*="item"]');
|
|
254
|
-
const clickable =
|
|
255
|
-
parentFilterItem ||
|
|
256
|
-
target.closest('button,[role="tab"],a,li,[class*="tab"],[class*="filter"],div') ||
|
|
257
|
-
target;
|
|
258
|
-
|
|
259
|
-
clickable.click();
|
|
260
|
-
await new Promise((resolve) => window.setTimeout(resolve, 380));
|
|
261
|
-
|
|
262
|
-
const refreshedCandidates = getMessageFilterCandidates();
|
|
263
|
-
const refreshed = (
|
|
264
|
-
refreshedCandidates.length > 0
|
|
265
|
-
? refreshedCandidates
|
|
266
|
-
: Array.from(document.querySelectorAll('span,button,div,li,a,[role="tab"]'))
|
|
267
|
-
).filter((node) => normalize(node.textContent || '') === label && isVisible(node));
|
|
268
|
-
const activeLabelAfter = getActiveFilterLabel();
|
|
269
|
-
const verified = activeLabelAfter === label || refreshed.some((node) => hasActiveState(node));
|
|
270
|
-
|
|
271
|
-
return {
|
|
272
|
-
ok: true,
|
|
273
|
-
changed: true,
|
|
274
|
-
verified,
|
|
275
|
-
activeLabel: activeLabelAfter || '',
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
async function browserActivatePrimaryChatLabel(label) {
|
|
280
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
281
|
-
const isVisible = (node) => {
|
|
282
|
-
if (!(node instanceof HTMLElement)) return false;
|
|
283
|
-
const rect = node.getBoundingClientRect();
|
|
284
|
-
if (rect.width <= 2 || rect.height <= 2) return false;
|
|
285
|
-
const style = getComputedStyle(node);
|
|
286
|
-
return style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || '1') > 0.01;
|
|
287
|
-
};
|
|
288
|
-
const matchesLabel = (node) => {
|
|
289
|
-
const text = normalize(node?.textContent || '');
|
|
290
|
-
return text === label || text.startsWith(`${label}(`);
|
|
291
|
-
};
|
|
292
|
-
const hasActiveState = (node) =>
|
|
293
|
-
node instanceof HTMLElement &&
|
|
294
|
-
(
|
|
295
|
-
node.classList.contains('active') ||
|
|
296
|
-
node.classList.contains('selected') ||
|
|
297
|
-
node.getAttribute('aria-selected') === 'true' ||
|
|
298
|
-
node.getAttribute('data-active') === 'true'
|
|
299
|
-
);
|
|
300
|
-
const getLabels = () =>
|
|
301
|
-
Array.from(document.querySelectorAll('.label-list .chat-label-item, .chat-label-item'))
|
|
302
|
-
.filter((node) => node instanceof HTMLElement && isVisible(node));
|
|
303
|
-
const getActiveLabel = () => {
|
|
304
|
-
const activeNode = getLabels().find((node) => hasActiveState(node));
|
|
305
|
-
return normalize(activeNode?.textContent || '');
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
const labels = getLabels();
|
|
309
|
-
const candidates = labels.filter((node) => matchesLabel(node));
|
|
310
|
-
if (candidates.length === 0) {
|
|
311
|
-
return { ok: false, error: `PRIMARY_CHAT_LABEL_NOT_FOUND:${label}` };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const activeLabelBefore = getActiveLabel();
|
|
315
|
-
if (candidates.some((node) => hasActiveState(node)) || matchesLabel({ textContent: activeLabelBefore })) {
|
|
316
|
-
return { ok: true, changed: false, verified: true, activeLabel: activeLabelBefore || label };
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
candidates[0].click();
|
|
320
|
-
await new Promise((resolve) => window.setTimeout(resolve, 420));
|
|
321
|
-
|
|
322
|
-
const refreshedCandidates = getLabels().filter((node) => matchesLabel(node));
|
|
323
|
-
const activeLabelAfter = getActiveLabel();
|
|
324
|
-
const verified =
|
|
325
|
-
matchesLabel({ textContent: activeLabelAfter }) ||
|
|
326
|
-
refreshedCandidates.some((node) => hasActiveState(node));
|
|
327
|
-
|
|
328
|
-
return {
|
|
329
|
-
ok: true,
|
|
330
|
-
changed: true,
|
|
331
|
-
verified,
|
|
332
|
-
activeLabel: activeLabelAfter || '',
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function browserGetLoadedCustomers() {
|
|
337
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
338
|
-
const isScrollable = (el) =>
|
|
339
|
-
el instanceof HTMLElement &&
|
|
340
|
-
Number(el.scrollHeight || 0) > Number(el.clientHeight || 0) + 16 &&
|
|
341
|
-
Number(el.clientHeight || 0) > 80;
|
|
342
|
-
const querySelectors = (selectors) => {
|
|
343
|
-
for (const selector of selectors) {
|
|
344
|
-
const found = document.querySelector(selector);
|
|
345
|
-
if (found) return found;
|
|
346
|
-
}
|
|
347
|
-
return null;
|
|
348
|
-
};
|
|
349
|
-
let listContainer = querySelectors([
|
|
350
|
-
'.user-list.b-scroll-stable',
|
|
351
|
-
'.chat-user .user-list',
|
|
352
|
-
'.chat-user .user-container > div > div:nth-child(2)',
|
|
353
|
-
'.chat-user .user-container [class*="list"]',
|
|
354
|
-
]);
|
|
355
|
-
|
|
356
|
-
if (!listContainer) {
|
|
357
|
-
const firstCard = document.querySelector('div[role="listitem"]');
|
|
358
|
-
if (firstCard instanceof HTMLElement) {
|
|
359
|
-
let current = firstCard.parentElement;
|
|
360
|
-
let depth = 0;
|
|
361
|
-
let best = null;
|
|
362
|
-
while (current && depth < 24) {
|
|
363
|
-
if (isScrollable(current)) {
|
|
364
|
-
best = current;
|
|
365
|
-
if (/user|list|chat/i.test(String(current.className || ''))) {
|
|
366
|
-
break;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
current = current.parentElement;
|
|
370
|
-
depth += 1;
|
|
371
|
-
}
|
|
372
|
-
listContainer = best || firstCard.parentElement;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const cardSource = listContainer || document;
|
|
377
|
-
const cards = Array.from(cardSource.querySelectorAll('div[role="listitem"]'))
|
|
378
|
-
.filter((node) => node instanceof HTMLElement)
|
|
379
|
-
.map((card, domIndex) => {
|
|
380
|
-
const text = normalize(card.textContent || '');
|
|
381
|
-
const lines = text.split(/\n+/).map(normalize).filter(Boolean);
|
|
382
|
-
const rect = card.getBoundingClientRect();
|
|
383
|
-
const geekItem = card.querySelector('.geek-item[data-id], .geek-item');
|
|
384
|
-
const nameNode = card.querySelector('.geek-name,[class*="name"]');
|
|
385
|
-
const sourceJobNode = card.querySelector('.source-job');
|
|
386
|
-
|
|
387
|
-
return {
|
|
388
|
-
domIndex,
|
|
389
|
-
customerId:
|
|
390
|
-
geekItem?.getAttribute('data-id') ||
|
|
391
|
-
card.getAttribute('key') ||
|
|
392
|
-
card.getAttribute('data-key') ||
|
|
393
|
-
card.dataset.id ||
|
|
394
|
-
card.id ||
|
|
395
|
-
'',
|
|
396
|
-
name: normalize(nameNode?.textContent || lines[1] || lines[0] || ''),
|
|
397
|
-
sourceJob: normalize(sourceJobNode?.textContent || ''),
|
|
398
|
-
textSnippet: text.slice(0, 300),
|
|
399
|
-
rect: {
|
|
400
|
-
left: rect.left,
|
|
401
|
-
top: rect.top,
|
|
402
|
-
width: rect.width,
|
|
403
|
-
height: rect.height,
|
|
404
|
-
right: rect.right,
|
|
405
|
-
bottom: rect.bottom,
|
|
406
|
-
},
|
|
407
|
-
visible:
|
|
408
|
-
rect.width > 0 &&
|
|
409
|
-
rect.height > 0 &&
|
|
410
|
-
rect.bottom > listContainer.getBoundingClientRect().top &&
|
|
411
|
-
rect.top < listContainer.getBoundingClientRect().bottom,
|
|
412
|
-
};
|
|
413
|
-
})
|
|
414
|
-
.filter((card) => card.textSnippet);
|
|
415
|
-
|
|
416
|
-
if (cards.length === 0) {
|
|
417
|
-
return {
|
|
418
|
-
ok: false,
|
|
419
|
-
error: 'CHAT_CARD_LIST_NOT_FOUND',
|
|
420
|
-
customers: [],
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
return {
|
|
425
|
-
ok: true,
|
|
426
|
-
customers: cards,
|
|
427
|
-
scroll: {
|
|
428
|
-
top: Number(listContainer?.scrollTop || 0),
|
|
429
|
-
height: Number(listContainer?.scrollHeight || 0),
|
|
430
|
-
clientHeight: Number(listContainer?.clientHeight || 0),
|
|
431
|
-
},
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function browserPrimeConversationByFirstCandidate() {
|
|
436
|
-
const isVisible = (el) => {
|
|
437
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
438
|
-
const style = getComputedStyle(el);
|
|
439
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
440
|
-
return false;
|
|
441
|
-
}
|
|
442
|
-
const rect = el.getBoundingClientRect();
|
|
443
|
-
return rect.width > 2 && rect.height > 2;
|
|
444
|
-
};
|
|
445
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
446
|
-
const items = Array.from(document.querySelectorAll('div[role="listitem"]')).filter((node) => node instanceof HTMLElement);
|
|
447
|
-
const target = items.find((item) => isVisible(item)) || items[0];
|
|
448
|
-
if (!(target instanceof HTMLElement)) {
|
|
449
|
-
return { ok: false, error: 'NO_FIRST_CANDIDATE' };
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
target.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
453
|
-
const clickTarget = target.querySelector('.geek-item[data-id], .geek-item, .geek-item-wrap') || target;
|
|
454
|
-
if (!(clickTarget instanceof HTMLElement)) {
|
|
455
|
-
return { ok: false, error: 'FIRST_CANDIDATE_NOT_CLICKABLE' };
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const geekItem = target.querySelector('.geek-item[data-id], .geek-item');
|
|
459
|
-
const name = normalize(
|
|
460
|
-
target.querySelector('.geek-name,[class*="name"]')?.textContent ||
|
|
461
|
-
'',
|
|
462
|
-
);
|
|
463
|
-
const sourceJob = normalize(target.querySelector('.source-job')?.textContent || '');
|
|
464
|
-
const customerId = normalize(
|
|
465
|
-
geekItem?.getAttribute('data-id') ||
|
|
466
|
-
target.getAttribute('key') ||
|
|
467
|
-
target.getAttribute('data-key') ||
|
|
468
|
-
target.dataset.id ||
|
|
469
|
-
'',
|
|
470
|
-
);
|
|
471
|
-
const domIndex = items.indexOf(target);
|
|
472
|
-
|
|
473
|
-
clickTarget.click();
|
|
474
|
-
return {
|
|
475
|
-
ok: true,
|
|
476
|
-
text: normalize(target.textContent || '').slice(0, 120),
|
|
477
|
-
candidate: {
|
|
478
|
-
name: name || null,
|
|
479
|
-
sourceJob: sourceJob || null,
|
|
480
|
-
customerId: customerId || null,
|
|
481
|
-
domIndex,
|
|
482
|
-
},
|
|
483
|
-
totalVisibleCandidates: items.length,
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function browserCenterCandidateInList(options = {}) {
|
|
488
|
-
const domIndex = Number(options.domIndex);
|
|
489
|
-
const drift = Number(options.drift || 0);
|
|
490
|
-
const clamp = (value, low, high) => Math.max(low, Math.min(high, value));
|
|
491
|
-
const isScrollable = (el) =>
|
|
492
|
-
el instanceof HTMLElement &&
|
|
493
|
-
Number(el.scrollHeight || 0) > Number(el.clientHeight || 0) + 16 &&
|
|
494
|
-
Number(el.clientHeight || 0) > 80;
|
|
495
|
-
const querySelectors = (selectors) => {
|
|
496
|
-
for (const selector of selectors) {
|
|
497
|
-
const found = document.querySelector(selector);
|
|
498
|
-
if (found) return found;
|
|
499
|
-
}
|
|
500
|
-
return null;
|
|
501
|
-
};
|
|
502
|
-
let listContainer = querySelectors([
|
|
503
|
-
'.user-list.b-scroll-stable',
|
|
504
|
-
'.chat-user .user-list',
|
|
505
|
-
'.chat-user .user-container > div > div:nth-child(2)',
|
|
506
|
-
'.chat-user .user-container [class*="list"]',
|
|
507
|
-
]);
|
|
508
|
-
|
|
509
|
-
if (!listContainer) {
|
|
510
|
-
const firstCard = document.querySelector('div[role="listitem"]');
|
|
511
|
-
if (firstCard instanceof HTMLElement) {
|
|
512
|
-
let current = firstCard.parentElement;
|
|
513
|
-
let depth = 0;
|
|
514
|
-
let best = null;
|
|
515
|
-
while (current && depth < 24) {
|
|
516
|
-
if (isScrollable(current)) {
|
|
517
|
-
best = current;
|
|
518
|
-
if (/user|list|chat/i.test(String(current.className || ''))) {
|
|
519
|
-
break;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
current = current.parentElement;
|
|
523
|
-
depth += 1;
|
|
524
|
-
}
|
|
525
|
-
listContainer = best || firstCard.parentElement;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (!listContainer) {
|
|
530
|
-
return { ok: false, error: 'CHAT_LIST_CONTAINER_NOT_FOUND' };
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const cards = Array.from(listContainer.querySelectorAll('div[role="listitem"]'));
|
|
534
|
-
const card = cards[domIndex];
|
|
535
|
-
if (!(card instanceof HTMLElement)) {
|
|
536
|
-
return { ok: false, error: `CARD_NOT_FOUND:${domIndex}` };
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
const beforeTop = Number(listContainer.scrollTop || 0);
|
|
540
|
-
const listRect = listContainer.getBoundingClientRect();
|
|
541
|
-
const cardRect = card.getBoundingClientRect();
|
|
542
|
-
const listCenter = listRect.top + listRect.height / 2;
|
|
543
|
-
const cardCenter = cardRect.top + cardRect.height / 2;
|
|
544
|
-
const delta = cardCenter - listCenter + drift;
|
|
545
|
-
const maxScroll = Math.max(0, listContainer.scrollHeight - listContainer.clientHeight);
|
|
546
|
-
const targetScroll = clamp(beforeTop + delta, 0, maxScroll);
|
|
547
|
-
listContainer.scrollTop = targetScroll;
|
|
548
|
-
listContainer.dispatchEvent(new Event('scroll', { bubbles: true }));
|
|
549
|
-
|
|
550
|
-
const rect = card.getBoundingClientRect();
|
|
551
|
-
return {
|
|
552
|
-
ok: true,
|
|
553
|
-
beforeTop,
|
|
554
|
-
afterTop: Number(listContainer.scrollTop || 0),
|
|
555
|
-
rect: {
|
|
556
|
-
left: rect.left,
|
|
557
|
-
top: rect.top,
|
|
558
|
-
width: rect.width,
|
|
559
|
-
height: rect.height,
|
|
560
|
-
right: rect.right,
|
|
561
|
-
bottom: rect.bottom,
|
|
562
|
-
},
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function browserActivateCandidate(options = {}) {
|
|
567
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
568
|
-
const domIndex = Number(options.domIndex);
|
|
569
|
-
const drift = Number(options.drift || 0);
|
|
570
|
-
const targetId = normalize(options.customerId || '');
|
|
571
|
-
const targetName = normalize(options.name || '');
|
|
572
|
-
const clamp = (value, low, high) => Math.max(low, Math.min(high, value));
|
|
573
|
-
const isScrollable = (el) =>
|
|
574
|
-
el instanceof HTMLElement &&
|
|
575
|
-
Number(el.scrollHeight || 0) > Number(el.clientHeight || 0) + 16 &&
|
|
576
|
-
Number(el.clientHeight || 0) > 80;
|
|
577
|
-
const querySelectors = (selectors) => {
|
|
578
|
-
for (const selector of selectors) {
|
|
579
|
-
const found = document.querySelector(selector);
|
|
580
|
-
if (found) return found;
|
|
581
|
-
}
|
|
582
|
-
return null;
|
|
583
|
-
};
|
|
584
|
-
let listContainer = querySelectors([
|
|
585
|
-
'.user-list.b-scroll-stable',
|
|
586
|
-
'.chat-user .user-list',
|
|
587
|
-
'.chat-user .user-container > div > div:nth-child(2)',
|
|
588
|
-
'.chat-user .user-container [class*="list"]',
|
|
589
|
-
]);
|
|
590
|
-
|
|
591
|
-
if (!listContainer) {
|
|
592
|
-
const firstCard = document.querySelector('div[role="listitem"]');
|
|
593
|
-
if (firstCard instanceof HTMLElement) {
|
|
594
|
-
let current = firstCard.parentElement;
|
|
595
|
-
let depth = 0;
|
|
596
|
-
let best = null;
|
|
597
|
-
while (current && depth < 24) {
|
|
598
|
-
if (isScrollable(current)) {
|
|
599
|
-
best = current;
|
|
600
|
-
if (/user|list|chat/i.test(String(current.className || ''))) {
|
|
601
|
-
break;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
current = current.parentElement;
|
|
605
|
-
depth += 1;
|
|
606
|
-
}
|
|
607
|
-
listContainer = best || firstCard.parentElement;
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
if (!listContainer) {
|
|
612
|
-
return { ok: false, error: 'CHAT_LIST_CONTAINER_NOT_FOUND' };
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const cards = Array.from(listContainer.querySelectorAll('div[role="listitem"]')).filter(
|
|
616
|
-
(node) => node instanceof HTMLElement,
|
|
617
|
-
);
|
|
618
|
-
const resolveCardMeta = (card) => {
|
|
619
|
-
const geekItem = card.querySelector('.geek-item[data-id], .geek-item');
|
|
620
|
-
const customerId = normalize(
|
|
621
|
-
geekItem?.getAttribute('data-id') ||
|
|
622
|
-
card.getAttribute('key') ||
|
|
623
|
-
card.getAttribute('data-key') ||
|
|
624
|
-
card.dataset.id ||
|
|
625
|
-
card.id ||
|
|
626
|
-
'',
|
|
627
|
-
);
|
|
628
|
-
const name = normalize(card.querySelector('.geek-name,[class*="name"]')?.textContent || '');
|
|
629
|
-
return { customerId, name };
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
let card = null;
|
|
633
|
-
if (targetId) {
|
|
634
|
-
card = cards.find((item) => {
|
|
635
|
-
const meta = resolveCardMeta(item);
|
|
636
|
-
return (
|
|
637
|
-
meta.customerId === targetId ||
|
|
638
|
-
(meta.customerId && targetId.endsWith(meta.customerId)) ||
|
|
639
|
-
(targetId && meta.customerId.endsWith(targetId))
|
|
640
|
-
);
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
if (!card && targetName) {
|
|
644
|
-
card = cards.find((item) => resolveCardMeta(item).name === targetName);
|
|
645
|
-
}
|
|
646
|
-
if (!card && Number.isFinite(domIndex)) {
|
|
647
|
-
card = cards[domIndex] || null;
|
|
648
|
-
}
|
|
649
|
-
if (!(card instanceof HTMLElement)) {
|
|
650
|
-
return { ok: false, error: 'TARGET_CANDIDATE_NOT_FOUND' };
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
const beforeTop = Number(listContainer.scrollTop || 0);
|
|
654
|
-
const listRect = listContainer.getBoundingClientRect();
|
|
655
|
-
const cardRect = card.getBoundingClientRect();
|
|
656
|
-
const listCenter = listRect.top + listRect.height / 2;
|
|
657
|
-
const cardCenter = cardRect.top + cardRect.height / 2;
|
|
658
|
-
const delta = cardCenter - listCenter + drift;
|
|
659
|
-
const maxScroll = Math.max(0, listContainer.scrollHeight - listContainer.clientHeight);
|
|
660
|
-
const targetScroll = clamp(beforeTop + delta, 0, maxScroll);
|
|
661
|
-
listContainer.scrollTop = targetScroll;
|
|
662
|
-
listContainer.dispatchEvent(new Event('scroll', { bubbles: true }));
|
|
663
|
-
|
|
664
|
-
const clickTarget = card.querySelector('.geek-item[data-id], .geek-item, .geek-item-wrap') || card;
|
|
665
|
-
if (!(clickTarget instanceof HTMLElement)) {
|
|
666
|
-
return { ok: false, error: 'TARGET_CANDIDATE_NOT_CLICKABLE' };
|
|
667
|
-
}
|
|
668
|
-
clickTarget.click();
|
|
669
|
-
const rect = card.getBoundingClientRect();
|
|
670
|
-
const meta = resolveCardMeta(card);
|
|
671
|
-
return {
|
|
672
|
-
ok: true,
|
|
673
|
-
rect: {
|
|
674
|
-
left: rect.left,
|
|
675
|
-
top: rect.top,
|
|
676
|
-
width: rect.width,
|
|
677
|
-
height: rect.height,
|
|
678
|
-
right: rect.right,
|
|
679
|
-
bottom: rect.bottom,
|
|
680
|
-
},
|
|
681
|
-
resolved: {
|
|
682
|
-
customerId: meta.customerId,
|
|
683
|
-
name: meta.name,
|
|
684
|
-
domIndex: cards.indexOf(card),
|
|
685
|
-
},
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
function browserScrollCustomerList(options = {}) {
|
|
690
|
-
const ratio = Number(options.ratio || 0.72);
|
|
691
|
-
const clamp = (value, low, high) => Math.max(low, Math.min(high, value));
|
|
692
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
693
|
-
const isVisible = (el) => {
|
|
694
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
695
|
-
const style = getComputedStyle(el);
|
|
696
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
697
|
-
return false;
|
|
698
|
-
}
|
|
699
|
-
const rect = el.getBoundingClientRect();
|
|
700
|
-
return rect.width > 2 && rect.height > 2;
|
|
701
|
-
};
|
|
702
|
-
const isOverflowScrollable = (el) => {
|
|
703
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
704
|
-
const style = getComputedStyle(el);
|
|
705
|
-
return /(auto|scroll|overlay)/i.test(String(style.overflowY || ''));
|
|
706
|
-
};
|
|
707
|
-
const findBestScrollableContainer = (seedCard) => {
|
|
708
|
-
const candidates = [];
|
|
709
|
-
const pushCandidate = (node) => {
|
|
710
|
-
if (node instanceof HTMLElement) candidates.push(node);
|
|
711
|
-
};
|
|
712
|
-
if (seedCard instanceof HTMLElement) {
|
|
713
|
-
let current = seedCard.parentElement;
|
|
714
|
-
let depth = 0;
|
|
715
|
-
while (current && depth < 30) {
|
|
716
|
-
pushCandidate(current);
|
|
717
|
-
current = current.parentElement;
|
|
718
|
-
depth += 1;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
const unique = Array.from(new Set(candidates));
|
|
722
|
-
let best = null;
|
|
723
|
-
let bestScore = -Infinity;
|
|
724
|
-
for (const node of unique) {
|
|
725
|
-
const scrollRange = Number(node.scrollHeight || 0) - Number(node.clientHeight || 0);
|
|
726
|
-
const styleBonus = isOverflowScrollable(node) ? 80 : 0;
|
|
727
|
-
const classBonus = /user|list|chat/i.test(String(node.className || '')) ? 24 : 0;
|
|
728
|
-
const score = scrollRange + styleBonus + classBonus;
|
|
729
|
-
if (score > bestScore) {
|
|
730
|
-
best = node;
|
|
731
|
-
bestScore = score;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
return best;
|
|
735
|
-
};
|
|
736
|
-
const isScrollable = (el) =>
|
|
737
|
-
el instanceof HTMLElement &&
|
|
738
|
-
Number(el.scrollHeight || 0) > Number(el.clientHeight || 0) + 16 &&
|
|
739
|
-
Number(el.clientHeight || 0) > 80;
|
|
740
|
-
const querySelectors = (selectors) => {
|
|
741
|
-
for (const selector of selectors) {
|
|
742
|
-
const found = document.querySelector(selector);
|
|
743
|
-
if (found) return found;
|
|
744
|
-
}
|
|
745
|
-
return null;
|
|
746
|
-
};
|
|
747
|
-
let listContainer = querySelectors([
|
|
748
|
-
'.user-list.b-scroll-stable',
|
|
749
|
-
'.chat-user .user-list',
|
|
750
|
-
'.chat-user .user-container > div > div:nth-child(2)',
|
|
751
|
-
'.chat-user .user-container [class*="list"]',
|
|
752
|
-
]);
|
|
753
|
-
|
|
754
|
-
if (!listContainer) {
|
|
755
|
-
const firstCard = document.querySelector('div[role="listitem"]');
|
|
756
|
-
if (firstCard instanceof HTMLElement) {
|
|
757
|
-
let current = firstCard.parentElement;
|
|
758
|
-
let depth = 0;
|
|
759
|
-
let best = null;
|
|
760
|
-
while (current && depth < 24) {
|
|
761
|
-
if (isScrollable(current)) {
|
|
762
|
-
best = current;
|
|
763
|
-
if (/user|list|chat/i.test(String(current.className || ''))) {
|
|
764
|
-
break;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
current = current.parentElement;
|
|
768
|
-
depth += 1;
|
|
769
|
-
}
|
|
770
|
-
listContainer = best || firstCard.parentElement;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
if (!listContainer) {
|
|
775
|
-
return { ok: false, error: 'CHAT_LIST_CONTAINER_NOT_FOUND' };
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
const findNoMoreTips = () => {
|
|
779
|
-
const host =
|
|
780
|
-
listContainer.closest('.chat-user, .user-container, .chat-container, .chat-main') || document;
|
|
781
|
-
const tips = Array.from(host.querySelectorAll('div[role="tfoot"] .load-tips, p.load-tips')).find((node) => {
|
|
782
|
-
if (!(node instanceof HTMLElement)) return false;
|
|
783
|
-
const text = normalize(node.textContent || '');
|
|
784
|
-
return text.includes('没有更多了') && isVisible(node);
|
|
785
|
-
});
|
|
786
|
-
return {
|
|
787
|
-
detected: Boolean(tips),
|
|
788
|
-
text: normalize(tips?.textContent || ''),
|
|
789
|
-
};
|
|
790
|
-
};
|
|
791
|
-
|
|
792
|
-
const firstCard = listContainer.querySelector('div[role="listitem"]') || document.querySelector('div[role="listitem"]');
|
|
793
|
-
if (firstCard instanceof HTMLElement) {
|
|
794
|
-
const best = findBestScrollableContainer(firstCard);
|
|
795
|
-
if (best instanceof HTMLElement) {
|
|
796
|
-
listContainer = best;
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
const noMoreBefore = findNoMoreTips();
|
|
801
|
-
const before = {
|
|
802
|
-
top: Number(listContainer.scrollTop || 0),
|
|
803
|
-
height: Number(listContainer.scrollHeight || 0),
|
|
804
|
-
clientHeight: Number(listContainer.clientHeight || 0),
|
|
805
|
-
cardCount: Number(listContainer.querySelectorAll('div[role="listitem"]').length || 0),
|
|
806
|
-
};
|
|
807
|
-
const amount = Math.max(120, Math.round(before.clientHeight * Math.max(0.35, Math.min(0.95, ratio))));
|
|
808
|
-
const maxScroll = Math.max(0, before.height - before.clientHeight);
|
|
809
|
-
if (maxScroll > 0) {
|
|
810
|
-
listContainer.scrollTop = clamp(before.top + amount, 0, maxScroll);
|
|
811
|
-
} else {
|
|
812
|
-
const cards = Array.from(listContainer.querySelectorAll('div[role="listitem"]')).filter(
|
|
813
|
-
(node) => node instanceof HTMLElement,
|
|
814
|
-
);
|
|
815
|
-
const tail = cards[cards.length - 1];
|
|
816
|
-
if (tail instanceof HTMLElement) {
|
|
817
|
-
tail.scrollIntoView({ block: 'end', inline: 'nearest' });
|
|
818
|
-
try {
|
|
819
|
-
listContainer.dispatchEvent(
|
|
820
|
-
new WheelEvent('wheel', {
|
|
821
|
-
deltaY: amount,
|
|
822
|
-
bubbles: true,
|
|
823
|
-
cancelable: true,
|
|
824
|
-
}),
|
|
825
|
-
);
|
|
826
|
-
} catch {}
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
listContainer.dispatchEvent(new Event('scroll', { bubbles: true }));
|
|
830
|
-
|
|
831
|
-
const after = {
|
|
832
|
-
top: Number(listContainer.scrollTop || 0),
|
|
833
|
-
height: Number(listContainer.scrollHeight || 0),
|
|
834
|
-
clientHeight: Number(listContainer.clientHeight || 0),
|
|
835
|
-
cardCount: Number(listContainer.querySelectorAll('div[role="listitem"]').length || 0),
|
|
836
|
-
};
|
|
837
|
-
const noMoreAfter = findNoMoreTips();
|
|
838
|
-
const atBottom = after.height <= after.clientHeight + 2 || after.top >= Math.max(0, after.height - after.clientHeight - 2);
|
|
839
|
-
|
|
840
|
-
return {
|
|
841
|
-
ok: true,
|
|
842
|
-
before,
|
|
843
|
-
after,
|
|
844
|
-
atBottom,
|
|
845
|
-
noMoreDetectedBefore: noMoreBefore.detected,
|
|
846
|
-
noMoreDetectedAfter: noMoreAfter.detected,
|
|
847
|
-
noMoreTextBefore: noMoreBefore.text,
|
|
848
|
-
noMoreTextAfter: noMoreAfter.text,
|
|
849
|
-
didScroll:
|
|
850
|
-
before.top !== after.top ||
|
|
851
|
-
before.height !== after.height ||
|
|
852
|
-
before.cardCount !== after.cardCount,
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
function browserConversationReadyState() {
|
|
857
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
858
|
-
const isAskResumeText = (text) => {
|
|
859
|
-
const normalized = normalize(text);
|
|
860
|
-
if (!normalized) return false;
|
|
861
|
-
return (
|
|
862
|
-
normalized === '求简历' ||
|
|
863
|
-
normalized === '索要简历' ||
|
|
864
|
-
normalized === '求附件简历' ||
|
|
865
|
-
normalized.includes('求简历') ||
|
|
866
|
-
normalized.includes('索要简历') ||
|
|
867
|
-
normalized.includes('附件简历')
|
|
868
|
-
);
|
|
869
|
-
};
|
|
870
|
-
const isVisible = (el) => {
|
|
871
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
872
|
-
const style = getComputedStyle(el);
|
|
873
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
874
|
-
return false;
|
|
875
|
-
}
|
|
876
|
-
const rect = el.getBoundingClientRect();
|
|
877
|
-
return rect.width > 2 && rect.height > 2;
|
|
878
|
-
};
|
|
879
|
-
const isDisabled = (el) => {
|
|
880
|
-
const classText = String(el?.className || '').toLowerCase();
|
|
881
|
-
const ariaDisabled = String(el?.getAttribute?.('aria-disabled') || '').toLowerCase() === 'true';
|
|
882
|
-
const disabledAttr = Boolean(el?.hasAttribute?.('disabled'));
|
|
883
|
-
return classText.includes('disabled') || ariaDisabled || disabledAttr;
|
|
884
|
-
};
|
|
885
|
-
const isDisabledDeep = (el) => {
|
|
886
|
-
if (!(el instanceof HTMLElement)) return true;
|
|
887
|
-
let current = el;
|
|
888
|
-
let depth = 0;
|
|
889
|
-
while (current && depth < 5) {
|
|
890
|
-
if (isDisabled(current)) return true;
|
|
891
|
-
current = current.parentElement;
|
|
892
|
-
depth += 1;
|
|
893
|
-
}
|
|
894
|
-
return false;
|
|
895
|
-
};
|
|
896
|
-
const resolveAttachmentButton = () => {
|
|
897
|
-
const candidates = Array.from(
|
|
898
|
-
document.querySelectorAll(
|
|
899
|
-
'.resume-btn-file, .btn.resume-btn-file, [class*="resume-btn-file"]',
|
|
900
|
-
),
|
|
901
|
-
).filter((el) => isVisible(el));
|
|
902
|
-
const match = candidates.find((el) => {
|
|
903
|
-
const text = normalize(el.textContent || '');
|
|
904
|
-
if (!text) return false;
|
|
905
|
-
if (!text.includes('附件简历')) return false;
|
|
906
|
-
if (text.includes('求附件简历')) return false;
|
|
907
|
-
return true;
|
|
908
|
-
});
|
|
909
|
-
return match || null;
|
|
910
|
-
};
|
|
911
|
-
const onlineResume = Array.from(
|
|
912
|
-
document.querySelectorAll(
|
|
913
|
-
'a.btn.resume-btn-online, a.resume-btn-online, .resume-btn-online, .btn.resume-btn-online',
|
|
914
|
-
),
|
|
915
|
-
).find((el) => {
|
|
916
|
-
if (!isVisible(el)) return false;
|
|
917
|
-
if (!normalize(el.textContent || '').includes('在线简历')) return false;
|
|
918
|
-
return !isDisabled(el);
|
|
919
|
-
});
|
|
920
|
-
const attachmentResume = resolveAttachmentButton();
|
|
921
|
-
const askResume = Array.from(document.querySelectorAll('span.operate-btn, button, a, span')).find(
|
|
922
|
-
(el) => isVisible(el) && isAskResumeText(el.textContent || ''),
|
|
923
|
-
);
|
|
924
|
-
const editor = document.querySelector(
|
|
925
|
-
'#boss-chat-editor-input, .conversation-editor #boss-chat-editor-input, .conversation-editor .boss-chat-editor-input',
|
|
926
|
-
);
|
|
927
|
-
const activeSubmit = Array.from(
|
|
928
|
-
document.querySelectorAll(
|
|
929
|
-
'.conversation-editor .submit.active, .conversation-editor .submit-content .submit.active, .submit.active',
|
|
930
|
-
),
|
|
931
|
-
).find((node) => isVisible(node));
|
|
932
|
-
const anySubmit = Array.from(
|
|
933
|
-
document.querySelectorAll(
|
|
934
|
-
'.conversation-editor .submit-content .submit, .conversation-editor .submit, .submit-content .submit, .submit',
|
|
935
|
-
),
|
|
936
|
-
).find((node) => isVisible(node) && normalize(node.textContent || '').includes('发送'));
|
|
937
|
-
const resumeState = browserIsResumeModalOpen();
|
|
938
|
-
const detailState = browserCollectCandidateDetailSnapshot();
|
|
939
|
-
const attachmentResumeEnabled = Boolean(attachmentResume) && !isDisabledDeep(attachmentResume);
|
|
940
|
-
return {
|
|
941
|
-
hasOnlineResume: Boolean(onlineResume),
|
|
942
|
-
hasAskResume: Boolean(askResume),
|
|
943
|
-
onlineResumeClass: String(onlineResume?.className || ''),
|
|
944
|
-
hasAttachmentResume: Boolean(attachmentResume),
|
|
945
|
-
attachmentResumeEnabled,
|
|
946
|
-
attachmentResumeClass: String(attachmentResume?.className || ''),
|
|
947
|
-
resumeModalOpen:
|
|
948
|
-
Boolean(resumeState?.open) ||
|
|
949
|
-
Number(resumeState?.iframeCount || 0) > 0 ||
|
|
950
|
-
Number(resumeState?.scopeCount || 0) > 0,
|
|
951
|
-
candidateDetailOpen:
|
|
952
|
-
Boolean(detailState?.open) ||
|
|
953
|
-
Number(detailState?.panelCount || 0) > 0 ||
|
|
954
|
-
Number(detailState?.closeCount || 0) > 0,
|
|
955
|
-
panelsClosed:
|
|
956
|
-
!(
|
|
957
|
-
Boolean(resumeState?.open) ||
|
|
958
|
-
Number(resumeState?.iframeCount || 0) > 0 ||
|
|
959
|
-
Number(resumeState?.scopeCount || 0) > 0 ||
|
|
960
|
-
Boolean(detailState?.open) ||
|
|
961
|
-
Number(detailState?.panelCount || 0) > 0 ||
|
|
962
|
-
Number(detailState?.closeCount || 0) > 0
|
|
963
|
-
),
|
|
964
|
-
editorVisible: editor instanceof HTMLElement && isVisible(editor),
|
|
965
|
-
activeSubmit: Boolean(activeSubmit),
|
|
966
|
-
hasAnySubmit: Boolean(anySubmit),
|
|
967
|
-
messageInputReady:
|
|
968
|
-
editor instanceof HTMLElement &&
|
|
969
|
-
isVisible(editor) &&
|
|
970
|
-
(Boolean(activeSubmit) || Boolean(anySubmit)),
|
|
971
|
-
};
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
function browserNormalizeVisibleText(value) {
|
|
975
|
-
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
function browserIsVisibleElement(el, minimum = 2) {
|
|
979
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
980
|
-
const style = getComputedStyle(el);
|
|
981
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
982
|
-
return false;
|
|
983
|
-
}
|
|
984
|
-
const rect = el.getBoundingClientRect();
|
|
985
|
-
return rect.width > minimum && rect.height > minimum;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
function browserRectToJson(rect) {
|
|
989
|
-
return {
|
|
990
|
-
left: rect.left,
|
|
991
|
-
top: rect.top,
|
|
992
|
-
width: rect.width,
|
|
993
|
-
height: rect.height,
|
|
994
|
-
right: rect.right,
|
|
995
|
-
bottom: rect.bottom,
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
function browserCollectCandidateDetailSnapshot() {
|
|
1000
|
-
const normalize = browserNormalizeVisibleText;
|
|
1001
|
-
const isVisible = browserIsVisibleElement;
|
|
1002
|
-
const rectToJson = browserRectToJson;
|
|
1003
|
-
const viewportWidth = Math.max(document.documentElement?.clientWidth || 0, window.innerWidth || 0);
|
|
1004
|
-
const viewportHeight = Math.max(document.documentElement?.clientHeight || 0, window.innerHeight || 0);
|
|
1005
|
-
const panelSelectors = [
|
|
1006
|
-
'.base-info-single-top-detail',
|
|
1007
|
-
'.resume-detail-wrap',
|
|
1008
|
-
'.geek-card-detail',
|
|
1009
|
-
'.candidate-detail-wrap',
|
|
1010
|
-
'.chat-detail-wrap',
|
|
1011
|
-
'.new-resume-online-main-ui',
|
|
1012
|
-
'.resume-recommend.resume-common-wrap',
|
|
1013
|
-
'.resume-recommend',
|
|
1014
|
-
'.boss-dialog__body',
|
|
1015
|
-
];
|
|
1016
|
-
const overlaySelectors = [
|
|
1017
|
-
'.dialog-wrap.active',
|
|
1018
|
-
'.dialog-wrap',
|
|
1019
|
-
'.boss-dialog.active',
|
|
1020
|
-
'.boss-dialog',
|
|
1021
|
-
'.boss-popup__wrapper',
|
|
1022
|
-
'.v-modal',
|
|
1023
|
-
'.modal-mask',
|
|
1024
|
-
'.geek-detail-modal',
|
|
1025
|
-
'.modal',
|
|
1026
|
-
];
|
|
1027
|
-
const closeButtons = Array.from(document.querySelectorAll('.close-btn')).filter((el) => isVisible(el));
|
|
1028
|
-
const contentEntries = [];
|
|
1029
|
-
const overlayEntries = [];
|
|
1030
|
-
const contentSeen = new Set();
|
|
1031
|
-
const overlaySeen = new Set();
|
|
1032
|
-
|
|
1033
|
-
const pushEntry = (entries, seen, node, source, { minWidth = 240, minHeight = 160 } = {}) => {
|
|
1034
|
-
if (!(node instanceof HTMLElement) || !isVisible(node)) return;
|
|
1035
|
-
const rect = node.getBoundingClientRect();
|
|
1036
|
-
if (rect.width < minWidth || rect.height < minHeight) return;
|
|
1037
|
-
const key = `${Math.round(rect.left)}:${Math.round(rect.top)}:${Math.round(rect.width)}:${Math.round(rect.height)}:${normalize(node.className || '')}`;
|
|
1038
|
-
if (seen.has(key)) return;
|
|
1039
|
-
seen.add(key);
|
|
1040
|
-
entries.push({ node, rect, source });
|
|
1041
|
-
};
|
|
1042
|
-
|
|
1043
|
-
const hasContentHint = (node, rect, source = '') => {
|
|
1044
|
-
const classText = normalize(node.className || '').toLowerCase();
|
|
1045
|
-
const text = normalize(node.textContent || '').slice(0, 240).toLowerCase();
|
|
1046
|
-
const containsClose = closeButtons.some((button) => node.contains(button));
|
|
1047
|
-
const anchoredRight =
|
|
1048
|
-
rect.left >= viewportWidth * 0.35 ||
|
|
1049
|
-
rect.right >= viewportWidth * 0.68;
|
|
1050
|
-
const hasKnownContentClass =
|
|
1051
|
-
classText.includes('base-info-single-top-detail') ||
|
|
1052
|
-
classText.includes('resume-detail-wrap') ||
|
|
1053
|
-
classText.includes('candidate-detail') ||
|
|
1054
|
-
classText.includes('chat-detail') ||
|
|
1055
|
-
classText.includes('geek-card-detail') ||
|
|
1056
|
-
classText.includes('new-resume-online-main-ui') ||
|
|
1057
|
-
classText.includes('resume-common-wrap') ||
|
|
1058
|
-
classText.includes('boss-dialog__body');
|
|
1059
|
-
const hasDetailHint =
|
|
1060
|
-
text.includes('在线简历') ||
|
|
1061
|
-
text.includes('附件简历') ||
|
|
1062
|
-
text.includes('牛人分析器') ||
|
|
1063
|
-
text.includes('活跃');
|
|
1064
|
-
const notFullScreen =
|
|
1065
|
-
rect.width < viewportWidth * 0.96 ||
|
|
1066
|
-
rect.height < viewportHeight * 0.96;
|
|
1067
|
-
return (
|
|
1068
|
-
containsClose ||
|
|
1069
|
-
hasKnownContentClass ||
|
|
1070
|
-
hasDetailHint ||
|
|
1071
|
-
(source === 'close-ancestor' && (anchoredRight || notFullScreen))
|
|
1072
|
-
);
|
|
1073
|
-
};
|
|
1074
|
-
|
|
1075
|
-
const hasOverlayHint = (node, rect) => {
|
|
1076
|
-
const classText = normalize(node.className || '').toLowerCase();
|
|
1077
|
-
const hasOverlayClass =
|
|
1078
|
-
classText.includes('dialog-wrap') ||
|
|
1079
|
-
classText.includes('boss-dialog') ||
|
|
1080
|
-
classText.includes('popup') ||
|
|
1081
|
-
classText.includes('modal') ||
|
|
1082
|
-
classText.includes('mask') ||
|
|
1083
|
-
classText.includes('overlay');
|
|
1084
|
-
const coversViewport =
|
|
1085
|
-
rect.left <= 8 &&
|
|
1086
|
-
rect.top <= 8 &&
|
|
1087
|
-
rect.width >= viewportWidth * 0.85 &&
|
|
1088
|
-
rect.height >= viewportHeight * 0.85;
|
|
1089
|
-
return hasOverlayClass || coversViewport;
|
|
1090
|
-
};
|
|
1091
|
-
|
|
1092
|
-
for (const selector of panelSelectors) {
|
|
1093
|
-
for (const node of Array.from(document.querySelectorAll(selector))) {
|
|
1094
|
-
pushEntry(contentEntries, contentSeen, node, `selector:${selector}`);
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
for (const selector of overlaySelectors) {
|
|
1099
|
-
for (const node of Array.from(document.querySelectorAll(selector))) {
|
|
1100
|
-
pushEntry(overlayEntries, overlaySeen, node, `selector:${selector}`, {
|
|
1101
|
-
minWidth: 180,
|
|
1102
|
-
minHeight: 120,
|
|
1103
|
-
});
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
for (const closeButton of closeButtons) {
|
|
1108
|
-
let current = closeButton.parentElement;
|
|
1109
|
-
let depth = 0;
|
|
1110
|
-
while (current instanceof HTMLElement && depth < 12) {
|
|
1111
|
-
const rect = current.getBoundingClientRect();
|
|
1112
|
-
if (hasContentHint(current, rect, 'close-ancestor')) {
|
|
1113
|
-
pushEntry(contentEntries, contentSeen, current, 'close-ancestor');
|
|
1114
|
-
}
|
|
1115
|
-
if (hasOverlayHint(current, rect)) {
|
|
1116
|
-
pushEntry(overlayEntries, overlaySeen, current, 'close-ancestor', {
|
|
1117
|
-
minWidth: 180,
|
|
1118
|
-
minHeight: 120,
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
current = current.parentElement;
|
|
1122
|
-
depth += 1;
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
const scoredContent = contentEntries
|
|
1127
|
-
.map((entry) => {
|
|
1128
|
-
const classText = normalize(entry.node.className || '').toLowerCase();
|
|
1129
|
-
const text = normalize(entry.node.textContent || '').slice(0, 240).toLowerCase();
|
|
1130
|
-
const containsClose = closeButtons.some((button) => entry.node.contains(button));
|
|
1131
|
-
const anchoredRight =
|
|
1132
|
-
entry.rect.left >= viewportWidth * 0.35 ||
|
|
1133
|
-
entry.rect.right >= viewportWidth * 0.68;
|
|
1134
|
-
const hasKnownContentClass =
|
|
1135
|
-
classText.includes('base-info-single-top-detail') ||
|
|
1136
|
-
classText.includes('resume-detail-wrap') ||
|
|
1137
|
-
classText.includes('candidate-detail') ||
|
|
1138
|
-
classText.includes('chat-detail') ||
|
|
1139
|
-
classText.includes('geek-card-detail') ||
|
|
1140
|
-
classText.includes('new-resume-online-main-ui') ||
|
|
1141
|
-
classText.includes('resume-common-wrap') ||
|
|
1142
|
-
classText.includes('boss-dialog__body');
|
|
1143
|
-
const hasDetailHint =
|
|
1144
|
-
text.includes('在线简历') ||
|
|
1145
|
-
text.includes('附件简历') ||
|
|
1146
|
-
text.includes('牛人分析器') ||
|
|
1147
|
-
text.includes('活跃');
|
|
1148
|
-
const resemblesFullscreen =
|
|
1149
|
-
entry.rect.left <= 8 &&
|
|
1150
|
-
entry.rect.top <= 8 &&
|
|
1151
|
-
entry.rect.width >= viewportWidth * 0.92 &&
|
|
1152
|
-
entry.rect.height >= viewportHeight * 0.92;
|
|
1153
|
-
|
|
1154
|
-
let score = 0;
|
|
1155
|
-
if (containsClose) score += 220;
|
|
1156
|
-
if (anchoredRight) score += 150;
|
|
1157
|
-
if (hasKnownContentClass) score += 200;
|
|
1158
|
-
if (hasDetailHint) score += 80;
|
|
1159
|
-
if (entry.source === 'close-ancestor') score += 40;
|
|
1160
|
-
if (entry.rect.width <= viewportWidth * 0.82) score += 70;
|
|
1161
|
-
if (entry.rect.height <= viewportHeight * 0.98) score += 20;
|
|
1162
|
-
if (resemblesFullscreen) score -= 220;
|
|
1163
|
-
score += Math.min(140, Math.floor((entry.rect.width * entry.rect.height) / 18000));
|
|
1164
|
-
|
|
1165
|
-
return {
|
|
1166
|
-
...entry,
|
|
1167
|
-
score,
|
|
1168
|
-
};
|
|
1169
|
-
})
|
|
1170
|
-
.filter((entry) => entry.score > 0)
|
|
1171
|
-
.sort((a, b) => b.score - a.score);
|
|
1172
|
-
|
|
1173
|
-
const scoredOverlay = overlayEntries
|
|
1174
|
-
.map((entry) => {
|
|
1175
|
-
const classText = normalize(entry.node.className || '').toLowerCase();
|
|
1176
|
-
const coversViewport =
|
|
1177
|
-
entry.rect.left <= 8 &&
|
|
1178
|
-
entry.rect.top <= 8 &&
|
|
1179
|
-
entry.rect.width >= viewportWidth * 0.85 &&
|
|
1180
|
-
entry.rect.height >= viewportHeight * 0.85;
|
|
1181
|
-
let score = 0;
|
|
1182
|
-
if (classText.includes('dialog-wrap')) score += 160;
|
|
1183
|
-
if (classText.includes('boss-dialog')) score += 120;
|
|
1184
|
-
if (classText.includes('modal') || classText.includes('overlay') || classText.includes('mask')) {
|
|
1185
|
-
score += 80;
|
|
1186
|
-
}
|
|
1187
|
-
if (coversViewport) score += 180;
|
|
1188
|
-
if (entry.source === 'close-ancestor') score += 20;
|
|
1189
|
-
return {
|
|
1190
|
-
...entry,
|
|
1191
|
-
score,
|
|
1192
|
-
};
|
|
1193
|
-
})
|
|
1194
|
-
.filter((entry) => entry.score > 0)
|
|
1195
|
-
.sort((a, b) => b.score - a.score);
|
|
1196
|
-
|
|
1197
|
-
const topContent = scoredContent[0] || null;
|
|
1198
|
-
const topOverlay = scoredOverlay[0] || null;
|
|
1199
|
-
const topContentNode = topContent?.node || null;
|
|
1200
|
-
const topOverlayNode = topOverlay?.node || null;
|
|
1201
|
-
const closeButton =
|
|
1202
|
-
closeButtons.find((button) => topContentNode instanceof HTMLElement && topContentNode.contains(button)) ||
|
|
1203
|
-
closeButtons[0] ||
|
|
1204
|
-
null;
|
|
1205
|
-
const topPanel = topContent || topOverlay;
|
|
1206
|
-
const topPanelNode = topPanel?.node || null;
|
|
1207
|
-
|
|
1208
|
-
return {
|
|
1209
|
-
open: Boolean(topContent || topOverlay || closeButton),
|
|
1210
|
-
panelCount: scoredContent.length,
|
|
1211
|
-
closeCount: closeButtons.length,
|
|
1212
|
-
topPanelClass: normalize(topPanelNode?.className || ''),
|
|
1213
|
-
topPanelScore: Number(topPanel?.score || 0),
|
|
1214
|
-
panelRect: topContent ? rectToJson(topContent.rect) : topOverlay ? rectToJson(topOverlay.rect) : null,
|
|
1215
|
-
closeRect: closeButton ? rectToJson(closeButton.getBoundingClientRect()) : null,
|
|
1216
|
-
overlayClass: normalize(topOverlayNode?.className || ''),
|
|
1217
|
-
overlayRect: topOverlay ? rectToJson(topOverlay.rect) : null,
|
|
1218
|
-
contentClass: normalize(topContentNode?.className || ''),
|
|
1219
|
-
contentRect: topContent ? rectToJson(topContent.rect) : null,
|
|
1220
|
-
closeButton,
|
|
1221
|
-
};
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
function browserFindCandidateDetailOutsideClickPoint() {
|
|
1225
|
-
const state = browserCollectCandidateDetailSnapshot();
|
|
1226
|
-
const viewportWidth = Math.max(document.documentElement?.clientWidth || 0, window.innerWidth || 0);
|
|
1227
|
-
const viewportHeight = Math.max(document.documentElement?.clientHeight || 0, window.innerHeight || 0);
|
|
1228
|
-
const contentRect = state?.contentRect;
|
|
1229
|
-
if (!state?.open || !contentRect) {
|
|
1230
|
-
return {
|
|
1231
|
-
ok: false,
|
|
1232
|
-
error: 'CANDIDATE_DETAIL_OUTSIDE_POINT_NOT_FOUND',
|
|
1233
|
-
state,
|
|
1234
|
-
};
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
1238
|
-
const candidates = [];
|
|
1239
|
-
const safeMidY = clamp(contentRect.top + contentRect.height / 2, 18, viewportHeight - 18);
|
|
1240
|
-
const safeMidX = clamp(contentRect.left + contentRect.width / 2, 18, viewportWidth - 18);
|
|
1241
|
-
|
|
1242
|
-
if (contentRect.left >= 32) {
|
|
1243
|
-
candidates.push({
|
|
1244
|
-
strategy: 'left-gap',
|
|
1245
|
-
x: clamp(Math.min(contentRect.left - 18, contentRect.left / 2), 18, viewportWidth - 18),
|
|
1246
|
-
y: safeMidY,
|
|
1247
|
-
});
|
|
1248
|
-
}
|
|
1249
|
-
if (viewportWidth - contentRect.right >= 32) {
|
|
1250
|
-
candidates.push({
|
|
1251
|
-
strategy: 'right-gap',
|
|
1252
|
-
x: clamp(Math.max(contentRect.right + 18, contentRect.right + (viewportWidth - contentRect.right) / 2), 18, viewportWidth - 18),
|
|
1253
|
-
y: safeMidY,
|
|
1254
|
-
});
|
|
1255
|
-
}
|
|
1256
|
-
if (contentRect.top >= 32) {
|
|
1257
|
-
candidates.push({
|
|
1258
|
-
strategy: 'top-gap',
|
|
1259
|
-
x: safeMidX,
|
|
1260
|
-
y: clamp(Math.min(contentRect.top - 18, contentRect.top / 2), 18, viewportHeight - 18),
|
|
1261
|
-
});
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
const point = candidates.find((candidate) => {
|
|
1265
|
-
const outsideHorizontally = candidate.x < contentRect.left - 6 || candidate.x > contentRect.right + 6;
|
|
1266
|
-
const outsideVertically = candidate.y < contentRect.top - 6 || candidate.y > contentRect.bottom + 6;
|
|
1267
|
-
return outsideHorizontally || outsideVertically;
|
|
1268
|
-
});
|
|
1269
|
-
|
|
1270
|
-
if (!point) {
|
|
1271
|
-
return {
|
|
1272
|
-
ok: false,
|
|
1273
|
-
error: 'CANDIDATE_DETAIL_OUTSIDE_POINT_NOT_FOUND',
|
|
1274
|
-
state,
|
|
1275
|
-
};
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
return {
|
|
1279
|
-
ok: true,
|
|
1280
|
-
strategy: point.strategy,
|
|
1281
|
-
point: {
|
|
1282
|
-
x: Math.round(point.x),
|
|
1283
|
-
y: Math.round(point.y),
|
|
1284
|
-
},
|
|
1285
|
-
state,
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
function browserOpenOnlineResume(options = {}) {
|
|
1290
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
1291
|
-
const isVisible = (el) => {
|
|
1292
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
1293
|
-
const style = getComputedStyle(el);
|
|
1294
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
1295
|
-
return false;
|
|
1296
|
-
}
|
|
1297
|
-
const rect = el.getBoundingClientRect();
|
|
1298
|
-
return rect.width > 2 && rect.height > 2;
|
|
1299
|
-
};
|
|
1300
|
-
const selectors = [
|
|
1301
|
-
'a.btn.resume-btn-online',
|
|
1302
|
-
'a.resume-btn-online',
|
|
1303
|
-
'.resume-btn-online',
|
|
1304
|
-
'.btn.resume-btn-online',
|
|
1305
|
-
];
|
|
1306
|
-
let target = null;
|
|
1307
|
-
let selectorUsed = '';
|
|
1308
|
-
for (const selector of selectors) {
|
|
1309
|
-
const node = Array.from(document.querySelectorAll(selector)).find(
|
|
1310
|
-
(el) => {
|
|
1311
|
-
if (!isVisible(el)) return false;
|
|
1312
|
-
if (!normalize(el.textContent || '').includes('在线简历')) return false;
|
|
1313
|
-
const classText = String(el.className || '').toLowerCase();
|
|
1314
|
-
const ariaDisabled = String(el.getAttribute('aria-disabled') || '').toLowerCase() === 'true';
|
|
1315
|
-
const disabledAttr = el.hasAttribute('disabled');
|
|
1316
|
-
return !classText.includes('disabled') && !ariaDisabled && !disabledAttr;
|
|
1317
|
-
},
|
|
1318
|
-
);
|
|
1319
|
-
if (node) {
|
|
1320
|
-
target = node;
|
|
1321
|
-
selectorUsed = selector;
|
|
1322
|
-
break;
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
if (!target) {
|
|
1326
|
-
return { ok: false, error: 'ONLINE_RESUME_BUTTON_NOT_FOUND' };
|
|
1327
|
-
}
|
|
1328
|
-
if (options?.click === true) {
|
|
1329
|
-
try {
|
|
1330
|
-
target.click();
|
|
1331
|
-
const clickedRect = target.getBoundingClientRect();
|
|
1332
|
-
return {
|
|
1333
|
-
ok: true,
|
|
1334
|
-
clicked: true,
|
|
1335
|
-
by: 'dom-target-click',
|
|
1336
|
-
selector: selectorUsed || 'resume-btn-online',
|
|
1337
|
-
rect: {
|
|
1338
|
-
left: clickedRect.left,
|
|
1339
|
-
top: clickedRect.top,
|
|
1340
|
-
width: clickedRect.width,
|
|
1341
|
-
height: clickedRect.height,
|
|
1342
|
-
right: clickedRect.right,
|
|
1343
|
-
bottom: clickedRect.bottom,
|
|
1344
|
-
},
|
|
1345
|
-
};
|
|
1346
|
-
} catch (error) {
|
|
1347
|
-
return {
|
|
1348
|
-
ok: false,
|
|
1349
|
-
error: `ONLINE_RESUME_DOM_CLICK_FAILED:${error?.message || error}`,
|
|
1350
|
-
};
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
const rect = target.getBoundingClientRect();
|
|
1354
|
-
return {
|
|
1355
|
-
ok: true,
|
|
1356
|
-
selector: selectorUsed || 'resume-btn-online',
|
|
1357
|
-
rect: {
|
|
1358
|
-
left: rect.left,
|
|
1359
|
-
top: rect.top,
|
|
1360
|
-
width: rect.width,
|
|
1361
|
-
height: rect.height,
|
|
1362
|
-
right: rect.right,
|
|
1363
|
-
bottom: rect.bottom,
|
|
1364
|
-
},
|
|
1365
|
-
};
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
function browserIsCandidateDetailOpen() {
|
|
1369
|
-
return browserCollectCandidateDetailSnapshot();
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
function browserCloseCandidateDetailDomOnce() {
|
|
1373
|
-
const serializeSnapshot = (snapshot = {}) => ({
|
|
1374
|
-
open: Boolean(snapshot?.open),
|
|
1375
|
-
panelCount: Number(snapshot?.panelCount || 0),
|
|
1376
|
-
closeCount: Number(snapshot?.closeCount || 0),
|
|
1377
|
-
topPanelClass: String(snapshot?.topPanelClass || ''),
|
|
1378
|
-
topPanelScore: Number(snapshot?.topPanelScore || 0),
|
|
1379
|
-
panelRect: snapshot?.panelRect || null,
|
|
1380
|
-
closeRect: snapshot?.closeRect || null,
|
|
1381
|
-
overlayClass: String(snapshot?.overlayClass || ''),
|
|
1382
|
-
overlayRect: snapshot?.overlayRect || null,
|
|
1383
|
-
contentClass: String(snapshot?.contentClass || ''),
|
|
1384
|
-
contentRect: snapshot?.contentRect || null,
|
|
1385
|
-
});
|
|
1386
|
-
|
|
1387
|
-
const snapshot = browserCollectCandidateDetailSnapshot();
|
|
1388
|
-
if (!snapshot?.open || !(snapshot.closeButton instanceof HTMLElement)) {
|
|
1389
|
-
return {
|
|
1390
|
-
ok: false,
|
|
1391
|
-
error: 'CANDIDATE_DETAIL_CLOSE_BUTTON_NOT_FOUND',
|
|
1392
|
-
state: serializeSnapshot(snapshot),
|
|
1393
|
-
};
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
try {
|
|
1397
|
-
snapshot.closeButton.click();
|
|
1398
|
-
const rect = snapshot.closeButton.getBoundingClientRect();
|
|
1399
|
-
return {
|
|
1400
|
-
ok: true,
|
|
1401
|
-
selector: '.close-btn',
|
|
1402
|
-
method: 'dom-click-once',
|
|
1403
|
-
rect: {
|
|
1404
|
-
left: rect.left,
|
|
1405
|
-
top: rect.top,
|
|
1406
|
-
width: rect.width,
|
|
1407
|
-
height: rect.height,
|
|
1408
|
-
right: rect.right,
|
|
1409
|
-
bottom: rect.bottom,
|
|
1410
|
-
},
|
|
1411
|
-
state: serializeSnapshot(snapshot),
|
|
1412
|
-
};
|
|
1413
|
-
} catch (error) {
|
|
1414
|
-
return {
|
|
1415
|
-
ok: false,
|
|
1416
|
-
error: `CANDIDATE_DETAIL_DOM_CLOSE_FAILED:${error?.message || error}`,
|
|
1417
|
-
state: serializeSnapshot(snapshot),
|
|
1418
|
-
};
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
function browserCloseResumeModalDomOnce() {
|
|
1423
|
-
const isVisible = (el) => {
|
|
1424
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
1425
|
-
const style = getComputedStyle(el);
|
|
1426
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
1427
|
-
return false;
|
|
1428
|
-
}
|
|
1429
|
-
const rect = el.getBoundingClientRect();
|
|
1430
|
-
return rect.width > 2 && rect.height > 2;
|
|
1431
|
-
};
|
|
1432
|
-
|
|
1433
|
-
const selectors = [
|
|
1434
|
-
'.boss-popup__close',
|
|
1435
|
-
'.boss-dialog__close',
|
|
1436
|
-
'.dialog-close',
|
|
1437
|
-
'.modal-close',
|
|
1438
|
-
'.icon-close',
|
|
1439
|
-
];
|
|
1440
|
-
|
|
1441
|
-
for (const selector of selectors) {
|
|
1442
|
-
const target = Array.from(document.querySelectorAll(selector)).find((el) => isVisible(el));
|
|
1443
|
-
if (target instanceof HTMLElement) {
|
|
1444
|
-
target.click();
|
|
1445
|
-
const rect = target.getBoundingClientRect();
|
|
1446
|
-
return {
|
|
1447
|
-
ok: true,
|
|
1448
|
-
selector,
|
|
1449
|
-
method: 'dom-click-once',
|
|
1450
|
-
rect: {
|
|
1451
|
-
left: rect.left,
|
|
1452
|
-
top: rect.top,
|
|
1453
|
-
width: rect.width,
|
|
1454
|
-
height: rect.height,
|
|
1455
|
-
right: rect.right,
|
|
1456
|
-
bottom: rect.bottom,
|
|
1457
|
-
},
|
|
1458
|
-
};
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
return { ok: false, error: 'RESUME_CLOSE_BUTTON_NOT_FOUND' };
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
function browserGetResumeRateLimitWarning() {
|
|
1466
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
1467
|
-
const isVisible = (el) => {
|
|
1468
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
1469
|
-
const style = getComputedStyle(el);
|
|
1470
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
1471
|
-
return false;
|
|
1472
|
-
}
|
|
1473
|
-
const rect = el.getBoundingClientRect();
|
|
1474
|
-
return rect.width > 2 && rect.height > 2;
|
|
1475
|
-
};
|
|
1476
|
-
const patterns = ['查看太频繁', '操作太频繁', '频繁', '稍后再试'];
|
|
1477
|
-
const nodes = Array.from(document.querySelectorAll('div,span,p,section,article')).filter(isVisible);
|
|
1478
|
-
for (const node of nodes) {
|
|
1479
|
-
const text = normalize(node.textContent || '');
|
|
1480
|
-
if (!text || text.length > 120) continue;
|
|
1481
|
-
if (patterns.some((pattern) => text.includes(pattern))) {
|
|
1482
|
-
return { hit: true, text };
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
return { hit: false, text: '' };
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
function browserIsResumeModalOpen() {
|
|
1489
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
1490
|
-
const isVisible = (el) => {
|
|
1491
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
1492
|
-
const style = getComputedStyle(el);
|
|
1493
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
1494
|
-
return false;
|
|
1495
|
-
}
|
|
1496
|
-
const rect = el.getBoundingClientRect();
|
|
1497
|
-
return rect.width > 2 && rect.height > 2;
|
|
1498
|
-
};
|
|
1499
|
-
const closeSelectors = [
|
|
1500
|
-
'.boss-popup__close',
|
|
1501
|
-
'.boss-dialog__close',
|
|
1502
|
-
'.dialog-close',
|
|
1503
|
-
'.modal-close',
|
|
1504
|
-
'.icon-close',
|
|
1505
|
-
];
|
|
1506
|
-
const wrapperSelectors = [
|
|
1507
|
-
'.boss-popup__wrapper',
|
|
1508
|
-
'.new-chat-resume-dialog-main-ui',
|
|
1509
|
-
'.boss-dialog',
|
|
1510
|
-
'.dialog-wrap.active',
|
|
1511
|
-
'.geek-detail-modal',
|
|
1512
|
-
'.modal',
|
|
1513
|
-
];
|
|
1514
|
-
const frameSelectors = [
|
|
1515
|
-
'iframe[src*="/web/frame/c-resume/"]',
|
|
1516
|
-
'iframe[src*="resume"]',
|
|
1517
|
-
'iframe[name*="resume"]',
|
|
1518
|
-
];
|
|
1519
|
-
const resumePanelSelectors = [
|
|
1520
|
-
'.resume-content-wrap',
|
|
1521
|
-
'.resume-common-wrap',
|
|
1522
|
-
'.resume-detail',
|
|
1523
|
-
'.resume-recommend',
|
|
1524
|
-
'.iframe-resume-detail',
|
|
1525
|
-
'canvas#resume',
|
|
1526
|
-
];
|
|
1527
|
-
const wrappers = Array.from(document.querySelectorAll(wrapperSelectors.join(','))).filter(isVisible);
|
|
1528
|
-
const resumeIframes = Array.from(document.querySelectorAll(frameSelectors.join(','))).filter(isVisible);
|
|
1529
|
-
|
|
1530
|
-
const scoreScope = (scope) => {
|
|
1531
|
-
const rect = scope.getBoundingClientRect();
|
|
1532
|
-
const isLarge = rect.width >= 320 && rect.height >= 220;
|
|
1533
|
-
const hasClose = closeSelectors.some((selector) =>
|
|
1534
|
-
Array.from(scope.querySelectorAll(selector)).some((node) => isVisible(node)),
|
|
1535
|
-
);
|
|
1536
|
-
const hasResumeIframe = frameSelectors.some((selector) =>
|
|
1537
|
-
Array.from(scope.querySelectorAll(selector)).some((node) => isVisible(node)),
|
|
1538
|
-
);
|
|
1539
|
-
const hasResumePanel = resumePanelSelectors.some((selector) =>
|
|
1540
|
-
Array.from(scope.querySelectorAll(selector)).some((node) => isVisible(node)),
|
|
1541
|
-
);
|
|
1542
|
-
const classText = normalize(scope.className || '').toLowerCase();
|
|
1543
|
-
const text = normalize(scope.textContent || '').slice(0, 240).toLowerCase();
|
|
1544
|
-
const hasResumeClass = classText.includes('resume');
|
|
1545
|
-
const hasResumeText = text.includes('在线简历') || text.includes('附件简历') || text.includes('简历');
|
|
1546
|
-
const scopeStyle = getComputedStyle(scope);
|
|
1547
|
-
const isLeaving =
|
|
1548
|
-
/\bv-leave\b/.test(classText) ||
|
|
1549
|
-
/\bleave-active\b/.test(classText) ||
|
|
1550
|
-
/\bleaving\b/.test(classText) ||
|
|
1551
|
-
scopeStyle.pointerEvents === 'none';
|
|
1552
|
-
const hasResumeHint = hasResumeIframe || hasResumePanel || hasResumeClass || hasResumeText;
|
|
1553
|
-
|
|
1554
|
-
let score = 0;
|
|
1555
|
-
if (isLarge) score += 120;
|
|
1556
|
-
if (hasClose) score += 100;
|
|
1557
|
-
if (hasResumeClass) score += 80;
|
|
1558
|
-
if (hasResumeText) score += 40;
|
|
1559
|
-
if (hasResumePanel) score += 180;
|
|
1560
|
-
if (hasResumeIframe) score += 280;
|
|
1561
|
-
const zIndex = Number.parseInt(getComputedStyle(scope).zIndex || '0', 10);
|
|
1562
|
-
if (Number.isFinite(zIndex)) score += Math.max(0, Math.min(zIndex, 1000)) / 10;
|
|
1563
|
-
score += Math.min(120, Math.floor((rect.width * rect.height) / 9000));
|
|
1564
|
-
|
|
1565
|
-
const isResumeScope =
|
|
1566
|
-
hasResumeIframe ||
|
|
1567
|
-
(hasResumeHint && isLarge);
|
|
1568
|
-
const finalScope =
|
|
1569
|
-
isResumeScope && !(isLeaving && !hasResumeIframe && !hasClose);
|
|
1570
|
-
|
|
1571
|
-
return {
|
|
1572
|
-
scope,
|
|
1573
|
-
score,
|
|
1574
|
-
hasClose,
|
|
1575
|
-
hasResumeIframe,
|
|
1576
|
-
hasResumePanel,
|
|
1577
|
-
hasResumeClass,
|
|
1578
|
-
hasResumeText,
|
|
1579
|
-
isLarge,
|
|
1580
|
-
isResumeScope: finalScope,
|
|
1581
|
-
};
|
|
1582
|
-
};
|
|
1583
|
-
|
|
1584
|
-
const scoped = wrappers.map(scoreScope).filter((item) => item.isResumeScope).sort((a, b) => b.score - a.score);
|
|
1585
|
-
const closeCount = scoped.reduce((total, item) => {
|
|
1586
|
-
let local = 0;
|
|
1587
|
-
for (const selector of closeSelectors) {
|
|
1588
|
-
local += Array.from(item.scope.querySelectorAll(selector)).filter(isVisible).length;
|
|
1589
|
-
}
|
|
1590
|
-
return total + local;
|
|
1591
|
-
}, 0);
|
|
1592
|
-
const top = scoped[0];
|
|
1593
|
-
|
|
1594
|
-
return {
|
|
1595
|
-
open: scoped.length > 0 || resumeIframes.length > 0,
|
|
1596
|
-
scopeCount: scoped.length,
|
|
1597
|
-
iframeCount: resumeIframes.length,
|
|
1598
|
-
closeCount,
|
|
1599
|
-
topScopeClass: normalize(top?.scope?.className || ''),
|
|
1600
|
-
topScopeScore: Number(top?.score || 0),
|
|
1601
|
-
};
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
function browserCloseResumeModalBySelector() {
|
|
1605
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
1606
|
-
const isVisible = (el) => {
|
|
1607
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
1608
|
-
const style = getComputedStyle(el);
|
|
1609
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
1610
|
-
return false;
|
|
1611
|
-
}
|
|
1612
|
-
const rect = el.getBoundingClientRect();
|
|
1613
|
-
return rect.width > 2 && rect.height > 2;
|
|
1614
|
-
};
|
|
1615
|
-
const wrapperSelectors = [
|
|
1616
|
-
'.boss-popup__wrapper',
|
|
1617
|
-
'.new-chat-resume-dialog-main-ui',
|
|
1618
|
-
'.boss-dialog',
|
|
1619
|
-
'.dialog-wrap.active',
|
|
1620
|
-
'.geek-detail-modal',
|
|
1621
|
-
'.modal',
|
|
1622
|
-
];
|
|
1623
|
-
const frameSelectors = [
|
|
1624
|
-
'iframe[src*="/web/frame/c-resume/"]',
|
|
1625
|
-
'iframe[src*="resume"]',
|
|
1626
|
-
'iframe[name*="resume"]',
|
|
1627
|
-
];
|
|
1628
|
-
const resumePanelSelectors = [
|
|
1629
|
-
'.resume-content-wrap',
|
|
1630
|
-
'.resume-common-wrap',
|
|
1631
|
-
'.resume-detail',
|
|
1632
|
-
'.resume-recommend',
|
|
1633
|
-
'.iframe-resume-detail',
|
|
1634
|
-
'canvas#resume',
|
|
1635
|
-
];
|
|
1636
|
-
const closeSelectors = [
|
|
1637
|
-
'.boss-popup__close',
|
|
1638
|
-
'.boss-dialog__close',
|
|
1639
|
-
'.dialog-close',
|
|
1640
|
-
'.modal-close',
|
|
1641
|
-
'.icon-close',
|
|
1642
|
-
];
|
|
1643
|
-
|
|
1644
|
-
const scoreScope = (scope) => {
|
|
1645
|
-
const rect = scope.getBoundingClientRect();
|
|
1646
|
-
const isLarge = rect.width >= 320 && rect.height >= 220;
|
|
1647
|
-
const hasClose = closeSelectors.some((selector) =>
|
|
1648
|
-
Array.from(scope.querySelectorAll(selector)).some((node) => isVisible(node)),
|
|
1649
|
-
);
|
|
1650
|
-
const hasResumeIframe = frameSelectors.some((selector) =>
|
|
1651
|
-
Array.from(scope.querySelectorAll(selector)).some((node) => isVisible(node)),
|
|
1652
|
-
);
|
|
1653
|
-
const hasResumePanel = resumePanelSelectors.some((selector) =>
|
|
1654
|
-
Array.from(scope.querySelectorAll(selector)).some((node) => isVisible(node)),
|
|
1655
|
-
);
|
|
1656
|
-
const classText = normalize(scope.className || '').toLowerCase();
|
|
1657
|
-
const text = normalize(scope.textContent || '').slice(0, 240).toLowerCase();
|
|
1658
|
-
const hasResumeClass = classText.includes('resume');
|
|
1659
|
-
const hasResumeText = text.includes('在线简历') || text.includes('附件简历') || text.includes('简历');
|
|
1660
|
-
const scopeStyle = getComputedStyle(scope);
|
|
1661
|
-
const isLeaving =
|
|
1662
|
-
/\bv-leave\b/.test(classText) ||
|
|
1663
|
-
/\bleave-active\b/.test(classText) ||
|
|
1664
|
-
/\bleaving\b/.test(classText) ||
|
|
1665
|
-
scopeStyle.pointerEvents === 'none';
|
|
1666
|
-
|
|
1667
|
-
let score = 0;
|
|
1668
|
-
if (isLarge) score += 120;
|
|
1669
|
-
if (hasClose) score += 100;
|
|
1670
|
-
if (hasResumeClass) score += 80;
|
|
1671
|
-
if (hasResumeText) score += 40;
|
|
1672
|
-
if (hasResumePanel) score += 180;
|
|
1673
|
-
if (hasResumeIframe) score += 280;
|
|
1674
|
-
score += Math.min(120, Math.floor((rect.width * rect.height) / 9000));
|
|
1675
|
-
|
|
1676
|
-
const isResumeScope =
|
|
1677
|
-
hasResumeIframe ||
|
|
1678
|
-
((hasResumePanel || hasResumeClass || hasResumeText) && hasClose && isLarge);
|
|
1679
|
-
const finalScope =
|
|
1680
|
-
isResumeScope && !(isLeaving && !hasResumeIframe && !hasClose);
|
|
1681
|
-
|
|
1682
|
-
return {
|
|
1683
|
-
scope,
|
|
1684
|
-
score,
|
|
1685
|
-
isResumeScope: finalScope,
|
|
1686
|
-
};
|
|
1687
|
-
};
|
|
1688
|
-
|
|
1689
|
-
const scopes = Array.from(document.querySelectorAll(wrapperSelectors.join(',')))
|
|
1690
|
-
.filter(isVisible)
|
|
1691
|
-
.map(scoreScope)
|
|
1692
|
-
.filter((item) => item.isResumeScope)
|
|
1693
|
-
.sort((a, b) => b.score - a.score);
|
|
1694
|
-
|
|
1695
|
-
for (const entry of scopes) {
|
|
1696
|
-
for (const selector of closeSelectors) {
|
|
1697
|
-
const nodes = Array.from(entry.scope.querySelectorAll(selector));
|
|
1698
|
-
for (const node of nodes) {
|
|
1699
|
-
if (!isVisible(node)) continue;
|
|
1700
|
-
try {
|
|
1701
|
-
node.click();
|
|
1702
|
-
return {
|
|
1703
|
-
ok: true,
|
|
1704
|
-
method: 'selector',
|
|
1705
|
-
selector,
|
|
1706
|
-
scopeClass: normalize(entry.scope.className || ''),
|
|
1707
|
-
scopeScore: Number(entry.score || 0),
|
|
1708
|
-
};
|
|
1709
|
-
} catch {}
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
return { ok: false, error: 'RESUME_CLOSE_TARGET_NOT_FOUND', scopeCount: scopes.length };
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
function browserFindResumeCloseRect() {
|
|
1718
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
1719
|
-
const isVisible = (el) => {
|
|
1720
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
1721
|
-
const style = getComputedStyle(el);
|
|
1722
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
1723
|
-
return false;
|
|
1724
|
-
}
|
|
1725
|
-
const rect = el.getBoundingClientRect();
|
|
1726
|
-
return rect.width > 2 && rect.height > 2;
|
|
1727
|
-
};
|
|
1728
|
-
const wrapperSelectors = [
|
|
1729
|
-
'.boss-popup__wrapper',
|
|
1730
|
-
'.new-chat-resume-dialog-main-ui',
|
|
1731
|
-
'.boss-dialog',
|
|
1732
|
-
'.dialog-wrap.active',
|
|
1733
|
-
'.geek-detail-modal',
|
|
1734
|
-
'.modal',
|
|
1735
|
-
];
|
|
1736
|
-
const frameSelectors = [
|
|
1737
|
-
'iframe[src*="/web/frame/c-resume/"]',
|
|
1738
|
-
'iframe[src*="resume"]',
|
|
1739
|
-
'iframe[name*="resume"]',
|
|
1740
|
-
];
|
|
1741
|
-
const resumePanelSelectors = [
|
|
1742
|
-
'.resume-content-wrap',
|
|
1743
|
-
'.resume-common-wrap',
|
|
1744
|
-
'.resume-detail',
|
|
1745
|
-
'.resume-recommend',
|
|
1746
|
-
'.iframe-resume-detail',
|
|
1747
|
-
'canvas#resume',
|
|
1748
|
-
];
|
|
1749
|
-
const selectors = [
|
|
1750
|
-
'.boss-popup__close',
|
|
1751
|
-
'.boss-dialog__close',
|
|
1752
|
-
'.dialog-close',
|
|
1753
|
-
'.modal-close',
|
|
1754
|
-
'.icon-close',
|
|
1755
|
-
];
|
|
1756
|
-
|
|
1757
|
-
const scoreScope = (scope) => {
|
|
1758
|
-
const rect = scope.getBoundingClientRect();
|
|
1759
|
-
const isLarge = rect.width >= 320 && rect.height >= 220;
|
|
1760
|
-
const hasClose = selectors.some((selector) =>
|
|
1761
|
-
Array.from(scope.querySelectorAll(selector)).some((node) => isVisible(node)),
|
|
1762
|
-
);
|
|
1763
|
-
const hasResumeIframe = frameSelectors.some((selector) =>
|
|
1764
|
-
Array.from(scope.querySelectorAll(selector)).some((node) => isVisible(node)),
|
|
1765
|
-
);
|
|
1766
|
-
const hasResumePanel = resumePanelSelectors.some((selector) =>
|
|
1767
|
-
Array.from(scope.querySelectorAll(selector)).some((node) => isVisible(node)),
|
|
1768
|
-
);
|
|
1769
|
-
const classText = normalize(scope.className || '').toLowerCase();
|
|
1770
|
-
const text = normalize(scope.textContent || '').slice(0, 240).toLowerCase();
|
|
1771
|
-
const hasResumeClass = classText.includes('resume');
|
|
1772
|
-
const hasResumeText = text.includes('在线简历') || text.includes('附件简历') || text.includes('简历');
|
|
1773
|
-
const scopeStyle = getComputedStyle(scope);
|
|
1774
|
-
const isLeaving =
|
|
1775
|
-
/\bv-leave\b/.test(classText) ||
|
|
1776
|
-
/\bleave-active\b/.test(classText) ||
|
|
1777
|
-
/\bleaving\b/.test(classText) ||
|
|
1778
|
-
scopeStyle.pointerEvents === 'none';
|
|
1779
|
-
let score = 0;
|
|
1780
|
-
if (isLarge) score += 120;
|
|
1781
|
-
if (hasClose) score += 100;
|
|
1782
|
-
if (hasResumeClass) score += 80;
|
|
1783
|
-
if (hasResumeText) score += 40;
|
|
1784
|
-
if (hasResumePanel) score += 180;
|
|
1785
|
-
if (hasResumeIframe) score += 280;
|
|
1786
|
-
score += Math.min(120, Math.floor((rect.width * rect.height) / 9000));
|
|
1787
|
-
const isResumeScope =
|
|
1788
|
-
hasResumeIframe ||
|
|
1789
|
-
((hasResumePanel || hasResumeClass || hasResumeText) && hasClose && isLarge);
|
|
1790
|
-
const finalScope =
|
|
1791
|
-
isResumeScope && !(isLeaving && !hasResumeIframe && !hasClose);
|
|
1792
|
-
return { scope, score, isResumeScope: finalScope };
|
|
1793
|
-
};
|
|
1794
|
-
|
|
1795
|
-
const scopes = Array.from(document.querySelectorAll(wrapperSelectors.join(',')))
|
|
1796
|
-
.filter(isVisible)
|
|
1797
|
-
.map(scoreScope)
|
|
1798
|
-
.filter((item) => item.isResumeScope)
|
|
1799
|
-
.sort((a, b) => b.score - a.score);
|
|
1800
|
-
|
|
1801
|
-
for (const entry of scopes) {
|
|
1802
|
-
for (const selector of selectors) {
|
|
1803
|
-
const nodes = Array.from(entry.scope.querySelectorAll(selector));
|
|
1804
|
-
for (const node of nodes) {
|
|
1805
|
-
if (!isVisible(node)) continue;
|
|
1806
|
-
const rect = node.getBoundingClientRect();
|
|
1807
|
-
return {
|
|
1808
|
-
ok: true,
|
|
1809
|
-
selector,
|
|
1810
|
-
scopeClass: normalize(entry.scope.className || ''),
|
|
1811
|
-
scopeScore: Number(entry.score || 0),
|
|
1812
|
-
rect: {
|
|
1813
|
-
left: rect.left,
|
|
1814
|
-
top: rect.top,
|
|
1815
|
-
width: rect.width,
|
|
1816
|
-
height: rect.height,
|
|
1817
|
-
right: rect.right,
|
|
1818
|
-
bottom: rect.bottom,
|
|
1819
|
-
},
|
|
1820
|
-
};
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
return {
|
|
1826
|
-
ok: false,
|
|
1827
|
-
error: 'RESUME_CLOSE_RECT_NOT_FOUND',
|
|
1828
|
-
scopeCount: scopes.length,
|
|
1829
|
-
};
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
function browserCloseAnyPopupBySelector() {
|
|
1833
|
-
const isVisible = (el) => {
|
|
1834
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
1835
|
-
const style = getComputedStyle(el);
|
|
1836
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
1837
|
-
return false;
|
|
1838
|
-
}
|
|
1839
|
-
const rect = el.getBoundingClientRect();
|
|
1840
|
-
return rect.width > 2 && rect.height > 2;
|
|
1841
|
-
};
|
|
1842
|
-
const wrappers = Array.from(
|
|
1843
|
-
document.querySelectorAll('.boss-popup__wrapper, .dialog-wrap.active, .boss-dialog, .geek-detail-modal, .modal'),
|
|
1844
|
-
).filter((node) => {
|
|
1845
|
-
if (!isVisible(node)) return false;
|
|
1846
|
-
const rect = node.getBoundingClientRect();
|
|
1847
|
-
return rect.width > 220 && rect.height > 160;
|
|
1848
|
-
});
|
|
1849
|
-
const closeSelectors = ['.boss-popup__close', '.boss-dialog__close', '.dialog-close', '.modal-close', '.icon-close'];
|
|
1850
|
-
|
|
1851
|
-
for (const wrapper of wrappers) {
|
|
1852
|
-
for (const selector of closeSelectors) {
|
|
1853
|
-
const target = Array.from(wrapper.querySelectorAll(selector)).find((node) => isVisible(node));
|
|
1854
|
-
if (target instanceof HTMLElement) {
|
|
1855
|
-
target.click();
|
|
1856
|
-
return { ok: true, selector, method: 'any-popup-selector' };
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
return { ok: false, error: 'ANY_POPUP_CLOSE_NOT_FOUND' };
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
function browserFindAnyPopupCloseRect() {
|
|
1864
|
-
const isVisible = (el) => {
|
|
1865
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
1866
|
-
const style = getComputedStyle(el);
|
|
1867
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
1868
|
-
return false;
|
|
1869
|
-
}
|
|
1870
|
-
const rect = el.getBoundingClientRect();
|
|
1871
|
-
return rect.width > 2 && rect.height > 2;
|
|
1872
|
-
};
|
|
1873
|
-
const wrappers = Array.from(
|
|
1874
|
-
document.querySelectorAll('.boss-popup__wrapper, .dialog-wrap.active, .boss-dialog, .geek-detail-modal, .modal'),
|
|
1875
|
-
).filter((node) => {
|
|
1876
|
-
if (!isVisible(node)) return false;
|
|
1877
|
-
const rect = node.getBoundingClientRect();
|
|
1878
|
-
return rect.width > 220 && rect.height > 160;
|
|
1879
|
-
});
|
|
1880
|
-
const closeSelectors = ['.boss-popup__close', '.boss-dialog__close', '.dialog-close', '.modal-close', '.icon-close'];
|
|
1881
|
-
|
|
1882
|
-
for (const wrapper of wrappers) {
|
|
1883
|
-
for (const selector of closeSelectors) {
|
|
1884
|
-
const target = Array.from(wrapper.querySelectorAll(selector)).find((node) => isVisible(node));
|
|
1885
|
-
if (target instanceof HTMLElement) {
|
|
1886
|
-
const rect = target.getBoundingClientRect();
|
|
1887
|
-
return {
|
|
1888
|
-
ok: true,
|
|
1889
|
-
selector,
|
|
1890
|
-
rect: {
|
|
1891
|
-
left: rect.left,
|
|
1892
|
-
top: rect.top,
|
|
1893
|
-
width: rect.width,
|
|
1894
|
-
height: rect.height,
|
|
1895
|
-
right: rect.right,
|
|
1896
|
-
bottom: rect.bottom,
|
|
1897
|
-
},
|
|
1898
|
-
};
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
return { ok: false, error: 'ANY_POPUP_CLOSE_RECT_NOT_FOUND' };
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
function browserAnyPopupVisible() {
|
|
1906
|
-
const isVisible = (el) => {
|
|
1907
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
1908
|
-
const style = getComputedStyle(el);
|
|
1909
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
1910
|
-
return false;
|
|
1911
|
-
}
|
|
1912
|
-
const rect = el.getBoundingClientRect();
|
|
1913
|
-
return rect.width > 220 && rect.height > 160;
|
|
1914
|
-
};
|
|
1915
|
-
const wrappers = Array.from(
|
|
1916
|
-
document.querySelectorAll('.boss-popup__wrapper, .dialog-wrap.active, .boss-dialog, .geek-detail-modal, .modal'),
|
|
1917
|
-
).filter(isVisible);
|
|
1918
|
-
return { visible: wrappers.length > 0, count: wrappers.length };
|
|
1919
|
-
}
|
|
1920
|
-
|
|
1921
|
-
browserConversationReadyState.helpers = [
|
|
1922
|
-
browserIsResumeModalOpen,
|
|
1923
|
-
browserNormalizeVisibleText,
|
|
1924
|
-
browserIsVisibleElement,
|
|
1925
|
-
browserRectToJson,
|
|
1926
|
-
browserCollectCandidateDetailSnapshot,
|
|
1927
|
-
];
|
|
1928
|
-
|
|
1929
|
-
browserFindCandidateDetailOutsideClickPoint.helpers = [
|
|
1930
|
-
browserNormalizeVisibleText,
|
|
1931
|
-
browserIsVisibleElement,
|
|
1932
|
-
browserRectToJson,
|
|
1933
|
-
browserCollectCandidateDetailSnapshot,
|
|
1934
|
-
];
|
|
1935
|
-
|
|
1936
|
-
browserIsCandidateDetailOpen.helpers = [
|
|
1937
|
-
browserNormalizeVisibleText,
|
|
1938
|
-
browserIsVisibleElement,
|
|
1939
|
-
browserRectToJson,
|
|
1940
|
-
browserCollectCandidateDetailSnapshot,
|
|
1941
|
-
];
|
|
1942
|
-
|
|
1943
|
-
browserCloseCandidateDetailDomOnce.helpers = [
|
|
1944
|
-
browserNormalizeVisibleText,
|
|
1945
|
-
browserIsVisibleElement,
|
|
1946
|
-
browserRectToJson,
|
|
1947
|
-
browserCollectCandidateDetailSnapshot,
|
|
1948
|
-
];
|
|
1949
|
-
|
|
1950
|
-
function browserSetEditorMessage(message) {
|
|
1951
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
1952
|
-
const text = String(message || '').trim();
|
|
1953
|
-
if (!text) {
|
|
1954
|
-
return { ok: false, error: 'CHAT_MESSAGE_EMPTY' };
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
const isVisible = (el) => {
|
|
1958
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
1959
|
-
const style = getComputedStyle(el);
|
|
1960
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
1961
|
-
return false;
|
|
1962
|
-
}
|
|
1963
|
-
const rect = el.getBoundingClientRect();
|
|
1964
|
-
return rect.width > 2 && rect.height > 2;
|
|
1965
|
-
};
|
|
1966
|
-
|
|
1967
|
-
const editor = document.querySelector(
|
|
1968
|
-
'#boss-chat-editor-input, .conversation-editor #boss-chat-editor-input, .conversation-editor .boss-chat-editor-input',
|
|
1969
|
-
);
|
|
1970
|
-
if (!(editor instanceof HTMLElement) || !isVisible(editor)) {
|
|
1971
|
-
return { ok: false, error: 'CHAT_EDITOR_NOT_FOUND' };
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
editor.click();
|
|
1975
|
-
editor.focus();
|
|
1976
|
-
|
|
1977
|
-
const selection = window.getSelection();
|
|
1978
|
-
if (selection) {
|
|
1979
|
-
const range = document.createRange();
|
|
1980
|
-
range.selectNodeContents(editor);
|
|
1981
|
-
range.collapse(false);
|
|
1982
|
-
selection.removeAllRanges();
|
|
1983
|
-
selection.addRange(range);
|
|
1984
|
-
}
|
|
1985
|
-
|
|
1986
|
-
editor.textContent = '';
|
|
1987
|
-
let inserted = false;
|
|
1988
|
-
try {
|
|
1989
|
-
inserted = document.execCommand('insertText', false, text);
|
|
1990
|
-
} catch {}
|
|
1991
|
-
|
|
1992
|
-
if (!inserted) {
|
|
1993
|
-
editor.textContent = text;
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
try {
|
|
1997
|
-
editor.dispatchEvent(
|
|
1998
|
-
new InputEvent('input', {
|
|
1999
|
-
bubbles: true,
|
|
2000
|
-
cancelable: true,
|
|
2001
|
-
data: text,
|
|
2002
|
-
inputType: 'insertText',
|
|
2003
|
-
}),
|
|
2004
|
-
);
|
|
2005
|
-
} catch {
|
|
2006
|
-
editor.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
|
|
2007
|
-
}
|
|
2008
|
-
editor.dispatchEvent(new KeyboardEvent('keyup', { key: 'a', bubbles: true }));
|
|
2009
|
-
editor.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }));
|
|
2010
|
-
editor.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2011
|
-
|
|
2012
|
-
const activeSubmit = Array.from(
|
|
2013
|
-
document.querySelectorAll(
|
|
2014
|
-
'.conversation-editor .submit.active, .conversation-editor .submit-content .submit.active, .submit.active',
|
|
2015
|
-
),
|
|
2016
|
-
).some((node) => isVisible(node));
|
|
2017
|
-
|
|
2018
|
-
return {
|
|
2019
|
-
ok: true,
|
|
2020
|
-
value: normalize(editor.textContent || ''),
|
|
2021
|
-
activeSubmit,
|
|
2022
|
-
};
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
async function browserSendMessage(args = {}) {
|
|
2026
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
2027
|
-
const expectedText = normalize(args?.expectedText || '');
|
|
2028
|
-
const isVisible = (el) => {
|
|
2029
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
2030
|
-
const style = getComputedStyle(el);
|
|
2031
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
2032
|
-
return false;
|
|
2033
|
-
}
|
|
2034
|
-
const rect = el.getBoundingClientRect();
|
|
2035
|
-
return rect.width > 2 && rect.height > 2;
|
|
2036
|
-
};
|
|
2037
|
-
|
|
2038
|
-
const findEditor = () =>
|
|
2039
|
-
document.querySelector(
|
|
2040
|
-
'#boss-chat-editor-input, .conversation-editor #boss-chat-editor-input, .conversation-editor .boss-chat-editor-input',
|
|
2041
|
-
);
|
|
2042
|
-
const getEditorText = () => normalize(findEditor()?.textContent || '');
|
|
2043
|
-
const snapshot = () => {
|
|
2044
|
-
const activeSubmit = Array.from(
|
|
2045
|
-
document.querySelectorAll(
|
|
2046
|
-
'.conversation-editor .submit.active, .conversation-editor .submit-content .submit.active, .submit.active',
|
|
2047
|
-
),
|
|
2048
|
-
).find((node) => isVisible(node));
|
|
2049
|
-
const anySubmit = Array.from(
|
|
2050
|
-
document.querySelectorAll(
|
|
2051
|
-
'.conversation-editor .submit-content .submit, .conversation-editor .submit, .submit-content .submit, .submit',
|
|
2052
|
-
),
|
|
2053
|
-
).find((node) => isVisible(node) && normalize(node.textContent || '').includes('发送'));
|
|
2054
|
-
const editorText = getEditorText();
|
|
2055
|
-
return {
|
|
2056
|
-
editorText,
|
|
2057
|
-
hasExpected: expectedText ? editorText.includes(expectedText) : editorText.length > 0,
|
|
2058
|
-
activeSubmit: Boolean(activeSubmit),
|
|
2059
|
-
hasAnySubmit: Boolean(anySubmit),
|
|
2060
|
-
};
|
|
2061
|
-
};
|
|
2062
|
-
|
|
2063
|
-
const pressEnter = (target) => {
|
|
2064
|
-
if (!(target instanceof HTMLElement)) return;
|
|
2065
|
-
const init = {
|
|
2066
|
-
key: 'Enter',
|
|
2067
|
-
code: 'Enter',
|
|
2068
|
-
keyCode: 13,
|
|
2069
|
-
which: 13,
|
|
2070
|
-
bubbles: true,
|
|
2071
|
-
cancelable: true,
|
|
2072
|
-
};
|
|
2073
|
-
target.dispatchEvent(new KeyboardEvent('keydown', init));
|
|
2074
|
-
target.dispatchEvent(new KeyboardEvent('keypress', init));
|
|
2075
|
-
target.dispatchEvent(new KeyboardEvent('keyup', init));
|
|
2076
|
-
};
|
|
2077
|
-
|
|
2078
|
-
const editor = findEditor();
|
|
2079
|
-
if (!(editor instanceof HTMLElement) || !isVisible(editor)) {
|
|
2080
|
-
return { ok: false, error: 'CHAT_EDITOR_NOT_FOUND' };
|
|
2081
|
-
}
|
|
2082
|
-
editor.click();
|
|
2083
|
-
editor.focus();
|
|
2084
|
-
|
|
2085
|
-
const before = snapshot();
|
|
2086
|
-
if (!before.hasExpected) {
|
|
2087
|
-
return {
|
|
2088
|
-
ok: false,
|
|
2089
|
-
error: 'CHAT_EDITOR_TEXT_MISSING',
|
|
2090
|
-
editorBefore: before.editorText,
|
|
2091
|
-
};
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
const methods = [];
|
|
2095
|
-
const clickActive = () => {
|
|
2096
|
-
const activeSubmit = Array.from(
|
|
2097
|
-
document.querySelectorAll(
|
|
2098
|
-
'.conversation-editor .submit.active, .conversation-editor .submit-content .submit.active, .submit.active',
|
|
2099
|
-
),
|
|
2100
|
-
).find((node) => isVisible(node) && normalize(node.textContent || '').includes('发送'));
|
|
2101
|
-
if (activeSubmit instanceof HTMLElement) {
|
|
2102
|
-
activeSubmit.click();
|
|
2103
|
-
methods.push('click-submit-active');
|
|
2104
|
-
return true;
|
|
2105
|
-
}
|
|
2106
|
-
return false;
|
|
2107
|
-
};
|
|
2108
|
-
const clickAny = () => {
|
|
2109
|
-
const anySubmit = Array.from(
|
|
2110
|
-
document.querySelectorAll(
|
|
2111
|
-
'.conversation-editor .submit-content .submit, .conversation-editor .submit, .submit-content .submit, .submit',
|
|
2112
|
-
),
|
|
2113
|
-
).find((node) => isVisible(node) && normalize(node.textContent || '').includes('发送'));
|
|
2114
|
-
if (anySubmit instanceof HTMLElement) {
|
|
2115
|
-
anySubmit.click();
|
|
2116
|
-
methods.push('click-submit');
|
|
2117
|
-
return true;
|
|
2118
|
-
}
|
|
2119
|
-
return false;
|
|
2120
|
-
};
|
|
2121
|
-
|
|
2122
|
-
clickActive() || clickAny();
|
|
2123
|
-
await new Promise((resolve) => window.setTimeout(resolve, 320));
|
|
2124
|
-
let after = snapshot();
|
|
2125
|
-
if (!after.editorText) {
|
|
2126
|
-
return {
|
|
2127
|
-
ok: true,
|
|
2128
|
-
sent: true,
|
|
2129
|
-
method: methods.join('+') || 'click-submit',
|
|
2130
|
-
cleared: true,
|
|
2131
|
-
editorAfter: '',
|
|
2132
|
-
};
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
pressEnter(editor);
|
|
2136
|
-
methods.push('editor-enter');
|
|
2137
|
-
await new Promise((resolve) => window.setTimeout(resolve, 320));
|
|
2138
|
-
after = snapshot();
|
|
2139
|
-
if (!after.editorText) {
|
|
2140
|
-
return {
|
|
2141
|
-
ok: true,
|
|
2142
|
-
sent: true,
|
|
2143
|
-
method: methods.join('+'),
|
|
2144
|
-
cleared: true,
|
|
2145
|
-
editorAfter: '',
|
|
2146
|
-
};
|
|
2147
|
-
}
|
|
2148
|
-
|
|
2149
|
-
pressEnter(document.activeElement || editor);
|
|
2150
|
-
methods.push('active-enter');
|
|
2151
|
-
await new Promise((resolve) => window.setTimeout(resolve, 320));
|
|
2152
|
-
after = snapshot();
|
|
2153
|
-
|
|
2154
|
-
return {
|
|
2155
|
-
ok: true,
|
|
2156
|
-
sent: !after.editorText,
|
|
2157
|
-
method: methods.join('+') || 'none',
|
|
2158
|
-
cleared: !after.editorText,
|
|
2159
|
-
editorAfter: after.editorText,
|
|
2160
|
-
activeSubmitAfter: after.activeSubmit,
|
|
2161
|
-
};
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
|
-
function browserClickAskResume() {
|
|
2165
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
2166
|
-
const getActionLabel = (el) =>
|
|
2167
|
-
normalize([
|
|
2168
|
-
el?.textContent || '',
|
|
2169
|
-
el?.getAttribute?.('aria-label') || '',
|
|
2170
|
-
el?.getAttribute?.('title') || '',
|
|
2171
|
-
el?.getAttribute?.('data-title') || '',
|
|
2172
|
-
].join(' '));
|
|
2173
|
-
const isAskResumeText = (text) => {
|
|
2174
|
-
const normalized = normalize(text);
|
|
2175
|
-
if (!normalized) return false;
|
|
2176
|
-
return (
|
|
2177
|
-
normalized === '求简历' ||
|
|
2178
|
-
normalized === '索要简历' ||
|
|
2179
|
-
normalized === '求附件简历' ||
|
|
2180
|
-
normalized.includes('求简历') ||
|
|
2181
|
-
normalized.includes('索要简历') ||
|
|
2182
|
-
normalized.includes('附件简历')
|
|
2183
|
-
);
|
|
2184
|
-
};
|
|
2185
|
-
const isRequestedText = (text) => {
|
|
2186
|
-
const normalized = normalize(text);
|
|
2187
|
-
return (
|
|
2188
|
-
normalized === '已求简历' ||
|
|
2189
|
-
normalized === '已申请' ||
|
|
2190
|
-
normalized === '已发送' ||
|
|
2191
|
-
normalized.includes('已求简历') ||
|
|
2192
|
-
normalized.includes('已申请') ||
|
|
2193
|
-
normalized.includes('已发送')
|
|
2194
|
-
);
|
|
2195
|
-
};
|
|
2196
|
-
const isVisible = (el) => {
|
|
2197
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
2198
|
-
const style = getComputedStyle(el);
|
|
2199
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
2200
|
-
return false;
|
|
2201
|
-
}
|
|
2202
|
-
const rect = el.getBoundingClientRect();
|
|
2203
|
-
return rect.width > 2 && rect.height > 2;
|
|
2204
|
-
};
|
|
2205
|
-
const isDisabled = (el) => {
|
|
2206
|
-
const classText = String(el.className || '').toLowerCase();
|
|
2207
|
-
const ariaDisabled = String(el.getAttribute('aria-disabled') || '').toLowerCase() === 'true';
|
|
2208
|
-
const disabledAttr = el.hasAttribute('disabled');
|
|
2209
|
-
return classText.includes('disabled') || ariaDisabled || disabledAttr;
|
|
2210
|
-
};
|
|
2211
|
-
const toClickable = (node) => {
|
|
2212
|
-
let current = node;
|
|
2213
|
-
for (let depth = 0; current && depth < 5; depth += 1) {
|
|
2214
|
-
if (!(current instanceof HTMLElement)) break;
|
|
2215
|
-
const tag = String(current.tagName || '').toUpperCase();
|
|
2216
|
-
const classText = String(current.className || '').toLowerCase();
|
|
2217
|
-
const role = String(current.getAttribute('role') || '').toLowerCase();
|
|
2218
|
-
if (tag === 'BUTTON' || tag === 'A' || role === 'button' || classText.includes('btn')) {
|
|
2219
|
-
return current;
|
|
2220
|
-
}
|
|
2221
|
-
current = current.parentElement;
|
|
2222
|
-
}
|
|
2223
|
-
return node;
|
|
2224
|
-
};
|
|
2225
|
-
const alreadyRequested = Array.from(document.querySelectorAll('span.operate-btn')).find(
|
|
2226
|
-
(el) =>
|
|
2227
|
-
isVisible(el) &&
|
|
2228
|
-
(isRequestedText(getActionLabel(el)) || (isAskResumeText(getActionLabel(el)) && isDisabled(el))) &&
|
|
2229
|
-
isDisabled(el),
|
|
2230
|
-
);
|
|
2231
|
-
if (alreadyRequested) {
|
|
2232
|
-
return {
|
|
2233
|
-
ok: true,
|
|
2234
|
-
alreadyRequested: true,
|
|
2235
|
-
text: normalize(alreadyRequested.textContent || ''),
|
|
2236
|
-
className: String(alreadyRequested.className || ''),
|
|
2237
|
-
};
|
|
2238
|
-
}
|
|
2239
|
-
const exactOperate = Array.from(document.querySelectorAll('span.operate-btn')).find(
|
|
2240
|
-
(el) =>
|
|
2241
|
-
isVisible(el) &&
|
|
2242
|
-
isAskResumeText(getActionLabel(el)) &&
|
|
2243
|
-
!isRequestedText(getActionLabel(el)) &&
|
|
2244
|
-
!isDisabled(el),
|
|
2245
|
-
);
|
|
2246
|
-
const fallbackOperate = Array.from(
|
|
2247
|
-
document.querySelectorAll('span.operate-btn, .operate-btn, [class*="operate"], [class*="resume"], button, a, span'),
|
|
2248
|
-
).find(
|
|
2249
|
-
(el) =>
|
|
2250
|
-
isVisible(el) &&
|
|
2251
|
-
isAskResumeText(getActionLabel(el)) &&
|
|
2252
|
-
!isRequestedText(getActionLabel(el)) &&
|
|
2253
|
-
!isDisabled(el),
|
|
2254
|
-
);
|
|
2255
|
-
const target = exactOperate || fallbackOperate || null;
|
|
2256
|
-
if (!target) {
|
|
2257
|
-
return { ok: false, error: 'ASK_RESUME_BUTTON_NOT_FOUND' };
|
|
2258
|
-
}
|
|
2259
|
-
const clickable = toClickable(target);
|
|
2260
|
-
clickable.click();
|
|
2261
|
-
return {
|
|
2262
|
-
ok: true,
|
|
2263
|
-
alreadyRequested: false,
|
|
2264
|
-
text: normalize(target.textContent || ''),
|
|
2265
|
-
className: String(target.className || ''),
|
|
2266
|
-
clickedTag: String(clickable?.tagName || ''),
|
|
2267
|
-
clickedClassName: String(clickable?.className || ''),
|
|
2268
|
-
};
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
function browserClickConfirmRequestResume() {
|
|
2272
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
2273
|
-
const isVisible = (el) => {
|
|
2274
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
2275
|
-
const style = getComputedStyle(el);
|
|
2276
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
2277
|
-
return false;
|
|
2278
|
-
}
|
|
2279
|
-
const rect = el.getBoundingClientRect();
|
|
2280
|
-
return rect.width > 2 && rect.height > 2;
|
|
2281
|
-
};
|
|
2282
|
-
const isConfirmText = (text) => {
|
|
2283
|
-
const normalized = normalize(text);
|
|
2284
|
-
return (
|
|
2285
|
-
normalized === '确定' ||
|
|
2286
|
-
normalized === '确认' ||
|
|
2287
|
-
normalized === '提交' ||
|
|
2288
|
-
normalized === '发送' ||
|
|
2289
|
-
normalized === '继续'
|
|
2290
|
-
);
|
|
2291
|
-
};
|
|
2292
|
-
const toClickable = (node) => {
|
|
2293
|
-
let current = node;
|
|
2294
|
-
for (let depth = 0; current && depth < 5; depth += 1) {
|
|
2295
|
-
if (!(current instanceof HTMLElement)) break;
|
|
2296
|
-
const tag = String(current.tagName || '').toUpperCase();
|
|
2297
|
-
const classText = String(current.className || '').toLowerCase();
|
|
2298
|
-
const role = String(current.getAttribute('role') || '').toLowerCase();
|
|
2299
|
-
if (tag === 'BUTTON' || tag === 'A' || role === 'button' || classText.includes('btn')) {
|
|
2300
|
-
return current;
|
|
2301
|
-
}
|
|
2302
|
-
current = current.parentElement;
|
|
2303
|
-
}
|
|
2304
|
-
return node;
|
|
2305
|
-
};
|
|
2306
|
-
const wrapperSelectors = '.boss-popup__wrapper, .boss-dialog, .dialog-wrap.active, .modal';
|
|
2307
|
-
const wrappers = Array.from(document.querySelectorAll(wrapperSelectors))
|
|
2308
|
-
.filter((el) => isVisible(el))
|
|
2309
|
-
.sort((left, right) => {
|
|
2310
|
-
const leftStyle = getComputedStyle(left);
|
|
2311
|
-
const rightStyle = getComputedStyle(right);
|
|
2312
|
-
const leftZ = Number.parseInt(leftStyle.zIndex || '0', 10);
|
|
2313
|
-
const rightZ = Number.parseInt(rightStyle.zIndex || '0', 10);
|
|
2314
|
-
if (leftZ !== rightZ) return rightZ - leftZ;
|
|
2315
|
-
const leftRect = left.getBoundingClientRect();
|
|
2316
|
-
const rightRect = right.getBoundingClientRect();
|
|
2317
|
-
return rightRect.width * rightRect.height - leftRect.width * leftRect.height;
|
|
2318
|
-
});
|
|
2319
|
-
const findConfirmInScope = (scope) =>
|
|
2320
|
-
Array.from(
|
|
2321
|
-
scope.querySelectorAll(
|
|
2322
|
-
'span.boss-btn-primary.boss-btn, .boss-btn-primary.boss-btn, .boss-popup__wrapper .boss-btn-primary, .boss-dialog .boss-btn-primary, .boss-btn-primary, button, a, span',
|
|
2323
|
-
),
|
|
2324
|
-
).find((el) => isVisible(el) && isConfirmText(el.textContent || ''));
|
|
2325
|
-
let target = null;
|
|
2326
|
-
for (const wrapper of wrappers) {
|
|
2327
|
-
target = findConfirmInScope(wrapper);
|
|
2328
|
-
if (target) break;
|
|
2329
|
-
}
|
|
2330
|
-
if (!target) {
|
|
2331
|
-
target = Array.from(document.querySelectorAll('.boss-btn-primary, button, a, span')).find(
|
|
2332
|
-
(el) => isVisible(el) && isConfirmText(el.textContent || ''),
|
|
2333
|
-
);
|
|
2334
|
-
}
|
|
2335
|
-
if (!target) {
|
|
2336
|
-
return { ok: false, error: 'CONFIRM_BUTTON_NOT_FOUND' };
|
|
2337
|
-
}
|
|
2338
|
-
const clickable = toClickable(target);
|
|
2339
|
-
clickable.click();
|
|
2340
|
-
return {
|
|
2341
|
-
ok: true,
|
|
2342
|
-
text: normalize(target.textContent || ''),
|
|
2343
|
-
className: String(target.className || ''),
|
|
2344
|
-
clickedTag: String(clickable?.tagName || ''),
|
|
2345
|
-
clickedClassName: String(clickable?.className || ''),
|
|
2346
|
-
};
|
|
2347
|
-
}
|
|
2348
|
-
|
|
2349
|
-
function browserGetRequestResumeUiState() {
|
|
2350
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
2351
|
-
const getActionLabel = (el) =>
|
|
2352
|
-
normalize([
|
|
2353
|
-
el?.textContent || '',
|
|
2354
|
-
el?.getAttribute?.('aria-label') || '',
|
|
2355
|
-
el?.getAttribute?.('title') || '',
|
|
2356
|
-
el?.getAttribute?.('data-title') || '',
|
|
2357
|
-
].join(' '));
|
|
2358
|
-
const isAskResumeText = (text) => {
|
|
2359
|
-
const normalized = normalize(text);
|
|
2360
|
-
if (!normalized) return false;
|
|
2361
|
-
return (
|
|
2362
|
-
normalized === '求简历' ||
|
|
2363
|
-
normalized === '索要简历' ||
|
|
2364
|
-
normalized === '求附件简历' ||
|
|
2365
|
-
normalized.includes('求简历') ||
|
|
2366
|
-
normalized.includes('索要简历') ||
|
|
2367
|
-
normalized.includes('附件简历')
|
|
2368
|
-
);
|
|
2369
|
-
};
|
|
2370
|
-
const isVisible = (el) => {
|
|
2371
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
2372
|
-
const style = getComputedStyle(el);
|
|
2373
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
2374
|
-
return false;
|
|
2375
|
-
}
|
|
2376
|
-
const rect = el.getBoundingClientRect();
|
|
2377
|
-
return rect.width > 2 && rect.height > 2;
|
|
2378
|
-
};
|
|
2379
|
-
const isDisabled = (el) => {
|
|
2380
|
-
const classText = String(el.className || '').toLowerCase();
|
|
2381
|
-
const ariaDisabled = String(el.getAttribute('aria-disabled') || '').toLowerCase() === 'true';
|
|
2382
|
-
const disabledAttr = el.hasAttribute('disabled');
|
|
2383
|
-
return classText.includes('disabled') || ariaDisabled || disabledAttr;
|
|
2384
|
-
};
|
|
2385
|
-
const isRequestedText = (text) => {
|
|
2386
|
-
const normalized = normalize(text);
|
|
2387
|
-
return (
|
|
2388
|
-
normalized === '已求简历' ||
|
|
2389
|
-
normalized === '已申请' ||
|
|
2390
|
-
normalized === '已发送' ||
|
|
2391
|
-
normalized.includes('已求简历') ||
|
|
2392
|
-
normalized.includes('已申请') ||
|
|
2393
|
-
normalized.includes('已发送')
|
|
2394
|
-
);
|
|
2395
|
-
};
|
|
2396
|
-
const isConfirmText = (text) => {
|
|
2397
|
-
const normalized = normalize(text);
|
|
2398
|
-
return (
|
|
2399
|
-
normalized === '确定' ||
|
|
2400
|
-
normalized === '确认' ||
|
|
2401
|
-
normalized === '提交' ||
|
|
2402
|
-
normalized === '发送' ||
|
|
2403
|
-
normalized === '继续'
|
|
2404
|
-
);
|
|
2405
|
-
};
|
|
2406
|
-
|
|
2407
|
-
const askNodePreferred = Array.from(
|
|
2408
|
-
document.querySelectorAll('span.operate-btn, .operate-btn, [class*="operate"], button, a, span'),
|
|
2409
|
-
).find((el) => isVisible(el) && isAskResumeText(getActionLabel(el)));
|
|
2410
|
-
const askNodeFallback = Array.from(document.querySelectorAll('button, a, span')).find(
|
|
2411
|
-
(el) => isVisible(el) && isAskResumeText(getActionLabel(el)),
|
|
2412
|
-
);
|
|
2413
|
-
const askNode = askNodePreferred || askNodeFallback || null;
|
|
2414
|
-
const askDisabled = Boolean(askNode && isDisabled(askNode));
|
|
2415
|
-
const requestedOperateNode = Array.from(document.querySelectorAll('span.operate-btn')).find(
|
|
2416
|
-
(el) =>
|
|
2417
|
-
isVisible(el) &&
|
|
2418
|
-
(isRequestedText(getActionLabel(el)) || isAskResumeText(getActionLabel(el))) &&
|
|
2419
|
-
isDisabled(el),
|
|
2420
|
-
);
|
|
2421
|
-
const hasDisabledOperateAsk = Boolean(requestedOperateNode);
|
|
2422
|
-
const explicitRequestedNode = Array.from(
|
|
2423
|
-
document.querySelectorAll(
|
|
2424
|
-
'.operate-btn, [class*="operate"], [class*="resume"], span, button, a, div',
|
|
2425
|
-
),
|
|
2426
|
-
).find((el) => {
|
|
2427
|
-
const text = normalize(el.textContent || '');
|
|
2428
|
-
if (!isVisible(el)) return false;
|
|
2429
|
-
if (!text) return false;
|
|
2430
|
-
if (!isRequestedText(text)) return false;
|
|
2431
|
-
const classText = String(el.className || '').toLowerCase();
|
|
2432
|
-
return classText.includes('operate') || classText.includes('resume') || classText.includes('btn');
|
|
2433
|
-
});
|
|
2434
|
-
const wrapperSelectors = '.boss-popup__wrapper, .boss-dialog, .dialog-wrap.active, .modal';
|
|
2435
|
-
const wrappers = Array.from(document.querySelectorAll(wrapperSelectors)).filter((el) => isVisible(el));
|
|
2436
|
-
let confirmNode = null;
|
|
2437
|
-
for (const wrapper of wrappers) {
|
|
2438
|
-
confirmNode = Array.from(
|
|
2439
|
-
wrapper.querySelectorAll(
|
|
2440
|
-
'span.boss-btn-primary.boss-btn, .boss-btn-primary.boss-btn, .boss-popup__wrapper .boss-btn-primary, .boss-dialog .boss-btn-primary, .boss-btn-primary, button, a, span',
|
|
2441
|
-
),
|
|
2442
|
-
).find((el) => isVisible(el) && isConfirmText(el.textContent || ''));
|
|
2443
|
-
if (confirmNode) break;
|
|
2444
|
-
}
|
|
2445
|
-
if (!confirmNode) {
|
|
2446
|
-
confirmNode = Array.from(document.querySelectorAll('.boss-btn-primary, button, a, span')).find(
|
|
2447
|
-
(el) => isVisible(el) && isConfirmText(el.textContent || ''),
|
|
2448
|
-
);
|
|
2449
|
-
}
|
|
2450
|
-
|
|
2451
|
-
return {
|
|
2452
|
-
hasAskResume: Boolean(askNode),
|
|
2453
|
-
askDisabled,
|
|
2454
|
-
hasDisabledOperateAsk,
|
|
2455
|
-
hasRequestedMark: hasDisabledOperateAsk || Boolean(explicitRequestedNode) || askDisabled,
|
|
2456
|
-
hasConfirm: Boolean(confirmNode),
|
|
2457
|
-
};
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
|
-
function browserGetResumeRequestMessageState() {
|
|
2461
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
2462
|
-
const isVisible = (el) => {
|
|
2463
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
2464
|
-
const style = getComputedStyle(el);
|
|
2465
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
2466
|
-
return false;
|
|
2467
|
-
}
|
|
2468
|
-
const rect = el.getBoundingClientRect();
|
|
2469
|
-
return rect.width > 2 && rect.height > 2;
|
|
2470
|
-
};
|
|
2471
|
-
|
|
2472
|
-
const container = document.querySelector('.chat-message-list');
|
|
2473
|
-
if (!(container instanceof HTMLElement)) {
|
|
2474
|
-
return {
|
|
2475
|
-
ok: false,
|
|
2476
|
-
error: 'CHAT_MESSAGE_LIST_NOT_FOUND',
|
|
2477
|
-
count: 0,
|
|
2478
|
-
lastText: '',
|
|
2479
|
-
recent: [],
|
|
2480
|
-
};
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
|
-
const candidates = Array.from(
|
|
2484
|
-
container.querySelectorAll('.item-system .text span, .item-system .text, .item-system span'),
|
|
2485
|
-
).filter((node) => isVisible(node));
|
|
2486
|
-
const matched = [];
|
|
2487
|
-
for (const node of candidates) {
|
|
2488
|
-
const text = normalize(node.textContent || '');
|
|
2489
|
-
if (!text) continue;
|
|
2490
|
-
if (text.includes('简历请求已发送')) {
|
|
2491
|
-
matched.push(text);
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
|
|
2495
|
-
return {
|
|
2496
|
-
ok: true,
|
|
2497
|
-
count: matched.length,
|
|
2498
|
-
lastText: matched.length > 0 ? matched[matched.length - 1] : '',
|
|
2499
|
-
recent: matched.slice(-3),
|
|
2500
|
-
};
|
|
2501
|
-
}
|
|
2502
|
-
|
|
2503
|
-
function browserExtractResumeProfileFromModal() {
|
|
2504
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
2505
|
-
const isVisible = (el) => {
|
|
2506
|
-
if (!(el instanceof HTMLElement)) return false;
|
|
2507
|
-
const style = getComputedStyle(el);
|
|
2508
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
2509
|
-
return false;
|
|
2510
|
-
}
|
|
2511
|
-
const rect = el.getBoundingClientRect();
|
|
2512
|
-
return rect.width > 2 && rect.height > 2;
|
|
2513
|
-
};
|
|
2514
|
-
const isNoiseText = (text) => {
|
|
2515
|
-
const normalized = normalize(text);
|
|
2516
|
-
return (
|
|
2517
|
-
normalized.includes('其他名企大厂经历牛人') ||
|
|
2518
|
-
normalized.includes('相似牛人') ||
|
|
2519
|
-
normalized.includes('推荐牛人') ||
|
|
2520
|
-
normalized.includes('匿名牛人')
|
|
2521
|
-
);
|
|
2522
|
-
};
|
|
2523
|
-
const stripNoiseText = (text) => {
|
|
2524
|
-
let cleaned = normalize(text);
|
|
2525
|
-
const noisePhrases = ['其他名企大厂经历牛人', '相似牛人', '推荐牛人', '匿名牛人'];
|
|
2526
|
-
for (const phrase of noisePhrases) {
|
|
2527
|
-
cleaned = cleaned.split(phrase).join(' ');
|
|
2528
|
-
}
|
|
2529
|
-
return normalize(cleaned);
|
|
2530
|
-
};
|
|
2531
|
-
const pickSectionText = (section) => {
|
|
2532
|
-
if (!section) return '';
|
|
2533
|
-
return stripNoiseText(section.innerText || section.textContent || '');
|
|
2534
|
-
};
|
|
2535
|
-
const pickFirstText = (scope, selectors) => {
|
|
2536
|
-
for (const selector of selectors) {
|
|
2537
|
-
let nodes = [];
|
|
2538
|
-
try {
|
|
2539
|
-
nodes = Array.from(scope.querySelectorAll(selector)).slice(0, 12);
|
|
2540
|
-
} catch {
|
|
2541
|
-
nodes = [];
|
|
2542
|
-
}
|
|
2543
|
-
for (const node of nodes) {
|
|
2544
|
-
if (!isVisible(node)) continue;
|
|
2545
|
-
const text = normalize(node.textContent || '');
|
|
2546
|
-
if (!text || isNoiseText(text)) continue;
|
|
2547
|
-
return text;
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
|
-
return '';
|
|
2551
|
-
};
|
|
2552
|
-
const pickList = (scope, selectors, maxItems = 5) => {
|
|
2553
|
-
const items = [];
|
|
2554
|
-
const seen = new Set();
|
|
2555
|
-
for (const selector of selectors) {
|
|
2556
|
-
let nodes = [];
|
|
2557
|
-
try {
|
|
2558
|
-
nodes = Array.from(scope.querySelectorAll(selector)).slice(0, 20);
|
|
2559
|
-
} catch {
|
|
2560
|
-
nodes = [];
|
|
2561
|
-
}
|
|
2562
|
-
for (const node of nodes) {
|
|
2563
|
-
if (!isVisible(node)) continue;
|
|
2564
|
-
const text = normalize(node.textContent || '');
|
|
2565
|
-
if (!text || isNoiseText(text)) continue;
|
|
2566
|
-
const key = text.toLowerCase();
|
|
2567
|
-
if (seen.has(key)) continue;
|
|
2568
|
-
seen.add(key);
|
|
2569
|
-
items.push(text);
|
|
2570
|
-
if (items.length >= maxItems) return items;
|
|
2571
|
-
}
|
|
2572
|
-
}
|
|
2573
|
-
return items;
|
|
2574
|
-
};
|
|
2575
|
-
|
|
2576
|
-
const wrappers = Array.from(
|
|
2577
|
-
document.querySelectorAll('.dialog-wrap.active, .boss-popup__wrapper, .boss-dialog, .geek-detail-modal, .modal'),
|
|
2578
|
-
).filter((el) => isVisible(el));
|
|
2579
|
-
const scope = wrappers[0] || document;
|
|
2580
|
-
const root =
|
|
2581
|
-
scope.querySelector('.resume-center-side .resume-detail-wrap') ||
|
|
2582
|
-
scope.querySelector('.resume-detail-wrap') ||
|
|
2583
|
-
scope.querySelector('.resume-center-side') ||
|
|
2584
|
-
scope.querySelector('.resume-detail') ||
|
|
2585
|
-
document.querySelector('.resume-center-side .resume-detail-wrap') ||
|
|
2586
|
-
document.querySelector('.resume-detail-wrap') ||
|
|
2587
|
-
null;
|
|
2588
|
-
|
|
2589
|
-
if (!root) {
|
|
2590
|
-
return {
|
|
2591
|
-
ok: false,
|
|
2592
|
-
error: 'RESUME_PROFILE_ROOT_NOT_FOUND',
|
|
2593
|
-
name: '',
|
|
2594
|
-
primarySchool: '',
|
|
2595
|
-
major: '',
|
|
2596
|
-
company: '',
|
|
2597
|
-
position: '',
|
|
2598
|
-
schools: [],
|
|
2599
|
-
majors: [],
|
|
2600
|
-
resumeText: '',
|
|
2601
|
-
evidenceCorpus: '',
|
|
2602
|
-
};
|
|
2603
|
-
}
|
|
2604
|
-
|
|
2605
|
-
const educationSection =
|
|
2606
|
-
root.querySelector('.resume-section.geek-education-experience-wrap') ||
|
|
2607
|
-
root.querySelector('.geek-education-experience-wrap') ||
|
|
2608
|
-
root.querySelector('.resume-section[class*="education"]') ||
|
|
2609
|
-
root;
|
|
2610
|
-
const workSection =
|
|
2611
|
-
root.querySelector('.resume-section.geek-work-experience-wrap') ||
|
|
2612
|
-
root.querySelector('.geek-work-experience-wrap') ||
|
|
2613
|
-
root.querySelector('.resume-section[class*="work"]') ||
|
|
2614
|
-
root;
|
|
2615
|
-
const projectSection =
|
|
2616
|
-
root.querySelector('.resume-section.geek-project-experience-wrap') ||
|
|
2617
|
-
root.querySelector('.geek-project-experience-wrap') ||
|
|
2618
|
-
root.querySelector('.resume-section[class*="project"]') ||
|
|
2619
|
-
null;
|
|
2620
|
-
const skillSection =
|
|
2621
|
-
root.querySelector('.resume-section.geek-skill-wrap') ||
|
|
2622
|
-
root.querySelector('.geek-skill-wrap') ||
|
|
2623
|
-
root.querySelector('.resume-section[class*="skill"]') ||
|
|
2624
|
-
null;
|
|
2625
|
-
const baseSection =
|
|
2626
|
-
root.querySelector('.resume-section.geek-base-info-wrap') ||
|
|
2627
|
-
root.querySelector('.geek-base-info-wrap') ||
|
|
2628
|
-
root;
|
|
2629
|
-
|
|
2630
|
-
const schools = pickList(educationSection, [
|
|
2631
|
-
'.school-name',
|
|
2632
|
-
'.school-info .name-wrap .school-name',
|
|
2633
|
-
'.edu-wrap .school-name',
|
|
2634
|
-
]);
|
|
2635
|
-
const majors = pickList(educationSection, [
|
|
2636
|
-
'.major',
|
|
2637
|
-
'.school-name-wrap .major',
|
|
2638
|
-
'.edu-wrap .major',
|
|
2639
|
-
]);
|
|
2640
|
-
|
|
2641
|
-
const name = pickFirstText(baseSection, [
|
|
2642
|
-
'.name-wrap .name',
|
|
2643
|
-
'.geek-name .name',
|
|
2644
|
-
'.name',
|
|
2645
|
-
]);
|
|
2646
|
-
const primarySchool = schools[0] || '';
|
|
2647
|
-
const major = majors[0] || '';
|
|
2648
|
-
const company = pickFirstText(workSection, [
|
|
2649
|
-
'.company-name-wrap .name',
|
|
2650
|
-
'.company-name',
|
|
2651
|
-
'.helper-company-query-wrap .name',
|
|
2652
|
-
]);
|
|
2653
|
-
const position = pickFirstText(workSection, [
|
|
2654
|
-
'.position span',
|
|
2655
|
-
'.position',
|
|
2656
|
-
]);
|
|
2657
|
-
const baseText = pickSectionText(baseSection);
|
|
2658
|
-
const educationText = pickSectionText(educationSection);
|
|
2659
|
-
const workText = pickSectionText(workSection);
|
|
2660
|
-
const projectText = pickSectionText(projectSection);
|
|
2661
|
-
const skillText = pickSectionText(skillSection);
|
|
2662
|
-
const evidenceCorpus = stripNoiseText(root.innerText || root.textContent || '');
|
|
2663
|
-
const resumeText = [
|
|
2664
|
-
baseText ? `基础信息: ${baseText}` : '',
|
|
2665
|
-
educationText ? `教育经历: ${educationText}` : '',
|
|
2666
|
-
workText ? `工作经历: ${workText}` : '',
|
|
2667
|
-
projectText ? `项目经历: ${projectText}` : '',
|
|
2668
|
-
skillText ? `技能信息: ${skillText}` : '',
|
|
2669
|
-
]
|
|
2670
|
-
.filter(Boolean)
|
|
2671
|
-
.join('\n');
|
|
2672
|
-
|
|
2673
|
-
return {
|
|
2674
|
-
ok: true,
|
|
2675
|
-
name,
|
|
2676
|
-
primarySchool,
|
|
2677
|
-
major,
|
|
2678
|
-
company,
|
|
2679
|
-
position,
|
|
2680
|
-
schools,
|
|
2681
|
-
majors,
|
|
2682
|
-
resumeText: resumeText || evidenceCorpus || '',
|
|
2683
|
-
evidenceCorpus: evidenceCorpus || resumeText || '',
|
|
2684
|
-
debug: {
|
|
2685
|
-
rootClass: String(root.className || ''),
|
|
2686
|
-
educationClass: String(educationSection?.className || ''),
|
|
2687
|
-
workClass: String(workSection?.className || ''),
|
|
2688
|
-
wrapperClass: String(scope?.className || ''),
|
|
2689
|
-
resumeTextLength: Number((resumeText || '').length),
|
|
2690
|
-
evidenceCorpusLength: Number((evidenceCorpus || '').length),
|
|
2691
|
-
},
|
|
2692
|
-
};
|
|
2693
|
-
}
|
|
2694
|
-
|
|
2695
|
-
function browserGetActiveCandidateSnapshot() {
|
|
2696
|
-
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
2697
|
-
const active =
|
|
2698
|
-
document.querySelector('.geek-item.selected[data-id]') ||
|
|
2699
|
-
document.querySelector('.geek-item.selected');
|
|
2700
|
-
if (!(active instanceof HTMLElement)) {
|
|
2701
|
-
return {
|
|
2702
|
-
hasActive: false,
|
|
2703
|
-
customerId: '',
|
|
2704
|
-
name: '',
|
|
2705
|
-
domIndex: -1,
|
|
2706
|
-
};
|
|
2707
|
-
}
|
|
2708
|
-
const listItem = active.closest('div[role="listitem"]');
|
|
2709
|
-
const all = Array.from(document.querySelectorAll('div[role="listitem"]'));
|
|
2710
|
-
const name = normalize(listItem?.querySelector('.geek-name,[class*="name"]')?.textContent || '');
|
|
2711
|
-
const customerId = normalize(
|
|
2712
|
-
active.getAttribute('data-id') ||
|
|
2713
|
-
listItem?.getAttribute('key') ||
|
|
2714
|
-
listItem?.getAttribute('data-key') ||
|
|
2715
|
-
active.id ||
|
|
2716
|
-
'',
|
|
2717
|
-
);
|
|
2718
|
-
return {
|
|
2719
|
-
hasActive: true,
|
|
2720
|
-
customerId,
|
|
2721
|
-
name,
|
|
2722
|
-
domIndex: listItem ? all.indexOf(listItem) : -1,
|
|
2723
|
-
};
|
|
2724
|
-
}
|
|
2725
|
-
|
|
2726
|
-
export class BossChatPage {
|
|
2727
|
-
constructor(chromeClient) {
|
|
2728
|
-
this.chromeClient = chromeClient;
|
|
2729
|
-
}
|
|
2730
|
-
|
|
2731
|
-
static targetMatcher(target) {
|
|
2732
|
-
return target?.type === 'page' && String(target.url || '').includes(CHAT_URL_TOKEN);
|
|
2733
|
-
}
|
|
2734
|
-
|
|
2735
|
-
async getPageState() {
|
|
2736
|
-
return this.chromeClient.callFunction(browserGetPageState);
|
|
2737
|
-
}
|
|
2738
|
-
|
|
2739
|
-
async ensureOnChatPage() {
|
|
2740
|
-
const pageState = await this.getPageState();
|
|
2741
|
-
if (!pageState?.href?.includes(CHAT_URL_TOKEN)) {
|
|
2742
|
-
throw new Error('ACTIVE_TAB_IS_NOT_BOSS_CHAT_PAGE');
|
|
2743
|
-
}
|
|
2744
|
-
return pageState;
|
|
2745
|
-
}
|
|
2746
|
-
|
|
2747
|
-
async ensureReady() {
|
|
2748
|
-
const pageState = await this.ensureOnChatPage();
|
|
2749
|
-
if (!pageState.hasListContainer && Number(pageState.listItemCount || 0) <= 0) {
|
|
2750
|
-
throw new Error('CHAT_LIST_CONTAINER_NOT_FOUND');
|
|
2751
|
-
}
|
|
2752
|
-
return pageState;
|
|
2753
|
-
}
|
|
2754
|
-
|
|
2755
|
-
async recoverToChatIndex(options = {}) {
|
|
2756
|
-
const maxAttempts = options.maxAttempts || 20;
|
|
2757
|
-
const delayMs = options.delayMs || 500;
|
|
2758
|
-
const forceNavigate = options.forceNavigate === true;
|
|
2759
|
-
const waitForReadyState = options.waitForReadyState || 'complete';
|
|
2760
|
-
const hrefResult = await this.chromeClient.callFunction(browserGetCurrentHref);
|
|
2761
|
-
if (!forceNavigate && String(hrefResult?.href || '').includes(CHAT_URL_TOKEN)) {
|
|
2762
|
-
return { changed: false, href: hrefResult?.href || '' };
|
|
2763
|
-
}
|
|
2764
|
-
|
|
2765
|
-
await this.chromeClient.callFunction(browserNavigateToChatIndex, { force: forceNavigate });
|
|
2766
|
-
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
2767
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2768
|
-
const state = await this.getPageState();
|
|
2769
|
-
const onChatPage = String(state?.href || '').includes(CHAT_URL_TOKEN);
|
|
2770
|
-
const ready = !waitForReadyState || String(state?.readyState || '').toLowerCase() === String(waitForReadyState).toLowerCase();
|
|
2771
|
-
if (onChatPage && ready) {
|
|
2772
|
-
return { changed: true, href: state.href };
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
throw new Error('RECOVER_TO_CHAT_INDEX_TIMEOUT');
|
|
2777
|
-
}
|
|
2778
|
-
|
|
2779
|
-
async listJobs() {
|
|
2780
|
-
const result = await this.chromeClient.callFunction(browserListJobs);
|
|
2781
|
-
if (!result?.ok) {
|
|
2782
|
-
throw new Error(result?.error || 'LIST_JOBS_FAILED');
|
|
2783
|
-
}
|
|
2784
|
-
return result.jobs || [];
|
|
2785
|
-
}
|
|
2786
|
-
|
|
2787
|
-
async selectJob(jobSelection) {
|
|
2788
|
-
const result = await this.chromeClient.callFunction(browserSelectJob, jobSelection);
|
|
2789
|
-
if (!result?.ok) {
|
|
2790
|
-
throw new Error(result?.error || 'SELECT_JOB_FAILED');
|
|
2791
|
-
}
|
|
2792
|
-
if (!result.matched) {
|
|
2793
|
-
throw new Error('JOB_SELECTION_NOT_APPLIED');
|
|
2794
|
-
}
|
|
2795
|
-
return result.selected;
|
|
2796
|
-
}
|
|
2797
|
-
|
|
2798
|
-
async activateUnreadFilter() {
|
|
2799
|
-
const result = await this.chromeClient.callFunction(browserActivateFilterTab, '未读');
|
|
2800
|
-
if (!result?.ok) {
|
|
2801
|
-
throw new Error(result?.error || 'ACTIVATE_UNREAD_FILTER_FAILED');
|
|
2802
|
-
}
|
|
2803
|
-
return result;
|
|
2804
|
-
}
|
|
2805
|
-
|
|
2806
|
-
async activateAllFilter() {
|
|
2807
|
-
const result = await this.chromeClient.callFunction(browserActivateFilterTab, '全部');
|
|
2808
|
-
if (!result?.ok) {
|
|
2809
|
-
throw new Error(result?.error || 'ACTIVATE_ALL_FILTER_FAILED');
|
|
2810
|
-
}
|
|
2811
|
-
return result;
|
|
2812
|
-
}
|
|
2813
|
-
|
|
2814
|
-
async activatePrimaryChatLabel(label = '全部') {
|
|
2815
|
-
const result = await this.chromeClient.callFunction(browserActivatePrimaryChatLabel, label);
|
|
2816
|
-
if (!result?.ok) {
|
|
2817
|
-
throw new Error(result?.error || `ACTIVATE_PRIMARY_CHAT_LABEL_FAILED:${label}`);
|
|
2818
|
-
}
|
|
2819
|
-
return result;
|
|
2820
|
-
}
|
|
2821
|
-
|
|
2822
|
-
async getLoadedCustomers() {
|
|
2823
|
-
let lastError = 'GET_LOADED_CUSTOMERS_FAILED';
|
|
2824
|
-
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
2825
|
-
const result = await this.chromeClient.callFunction(browserGetLoadedCustomers);
|
|
2826
|
-
if (result?.ok) {
|
|
2827
|
-
return result.customers.map((customer) => ({
|
|
2828
|
-
...customer,
|
|
2829
|
-
customerKey: createCustomerKey(customer),
|
|
2830
|
-
}));
|
|
2831
|
-
}
|
|
2832
|
-
lastError = result?.error || lastError;
|
|
2833
|
-
await new Promise((resolve) => setTimeout(resolve, 180 + attempt * 70));
|
|
2834
|
-
}
|
|
2835
|
-
throw new Error(lastError);
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
|
-
async centerCustomerCard(domIndex, drift = 0) {
|
|
2839
|
-
const result = await this.chromeClient.callFunction(browserCenterCandidateInList, {
|
|
2840
|
-
domIndex,
|
|
2841
|
-
drift,
|
|
2842
|
-
});
|
|
2843
|
-
if (!result?.ok) {
|
|
2844
|
-
throw new Error(result?.error || `CENTER_CUSTOMER_CARD_FAILED:${domIndex}`);
|
|
2845
|
-
}
|
|
2846
|
-
return result.rect;
|
|
2847
|
-
}
|
|
2848
|
-
|
|
2849
|
-
async activateCandidate(customer, drift = 0) {
|
|
2850
|
-
const result = await this.chromeClient.callFunction(browserActivateCandidate, {
|
|
2851
|
-
domIndex: customer?.domIndex,
|
|
2852
|
-
customerId: customer?.customerId || '',
|
|
2853
|
-
name: customer?.name || '',
|
|
2854
|
-
drift,
|
|
2855
|
-
});
|
|
2856
|
-
if (!result?.ok) {
|
|
2857
|
-
throw new Error(result?.error || 'ACTIVATE_CANDIDATE_FAILED');
|
|
2858
|
-
}
|
|
2859
|
-
return result;
|
|
2860
|
-
}
|
|
2861
|
-
|
|
2862
|
-
async scrollCustomerList(ratio = 0.72) {
|
|
2863
|
-
const result = await this.chromeClient.callFunction(browserScrollCustomerList, { ratio });
|
|
2864
|
-
if (!result?.ok) {
|
|
2865
|
-
throw new Error(result?.error || 'SCROLL_CUSTOMER_LIST_FAILED');
|
|
2866
|
-
}
|
|
2867
|
-
return result;
|
|
2868
|
-
}
|
|
2869
|
-
|
|
2870
|
-
async waitForConversationReady(options = {}) {
|
|
2871
|
-
const maxAttempts = options.maxAttempts || 12;
|
|
2872
|
-
const delayMs = options.delayMs || 260;
|
|
2873
|
-
const requirePanelsClosed = options.requirePanelsClosed === true;
|
|
2874
|
-
|
|
2875
|
-
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
2876
|
-
const state = await this.chromeClient.callFunction(browserConversationReadyState);
|
|
2877
|
-
const hasActionControls =
|
|
2878
|
-
Boolean(state?.hasOnlineResume) ||
|
|
2879
|
-
Boolean(state?.hasAskResume) ||
|
|
2880
|
-
Boolean(state?.hasAttachmentResume);
|
|
2881
|
-
const panelsReady = requirePanelsClosed ? Boolean(state?.panelsClosed) : true;
|
|
2882
|
-
const editorReady = requirePanelsClosed
|
|
2883
|
-
? Boolean(state?.editorVisible) && (Boolean(state?.activeSubmit) || Boolean(state?.hasAnySubmit))
|
|
2884
|
-
: true;
|
|
2885
|
-
if (hasActionControls && panelsReady && editorReady) {
|
|
2886
|
-
return state;
|
|
2887
|
-
}
|
|
2888
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2889
|
-
}
|
|
2890
|
-
throw new Error(requirePanelsClosed ? 'CONVERSATION_PANEL_NOT_READY_OR_BLOCKED' : 'CONVERSATION_PANEL_NOT_READY');
|
|
2891
|
-
}
|
|
2892
|
-
|
|
2893
|
-
async waitForCandidateActivated(customer, options = {}) {
|
|
2894
|
-
const maxAttempts = options.maxAttempts || 12;
|
|
2895
|
-
const delayMs = options.delayMs || 220;
|
|
2896
|
-
const expectedId = String(customer?.customerId || '').trim();
|
|
2897
|
-
const expectedName = String(customer?.name || '').trim();
|
|
2898
|
-
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
2899
|
-
const state = await this.chromeClient.callFunction(browserGetActiveCandidateSnapshot);
|
|
2900
|
-
if (state?.hasActive) {
|
|
2901
|
-
const activeId = String(state.customerId || '').trim();
|
|
2902
|
-
const activeName = String(state.name || '').trim();
|
|
2903
|
-
const idMatched =
|
|
2904
|
-
expectedId &&
|
|
2905
|
-
(activeId === expectedId || activeId.endsWith(expectedId) || expectedId.endsWith(activeId));
|
|
2906
|
-
const nameMatched = expectedName && activeName && activeName === expectedName;
|
|
2907
|
-
if (idMatched || nameMatched) {
|
|
2908
|
-
return { ...state, matched: true };
|
|
2909
|
-
}
|
|
2910
|
-
}
|
|
2911
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2912
|
-
}
|
|
2913
|
-
const finalState = await this.chromeClient.callFunction(browserGetActiveCandidateSnapshot);
|
|
2914
|
-
return {
|
|
2915
|
-
...(finalState || {}),
|
|
2916
|
-
matched: false,
|
|
2917
|
-
expectedId,
|
|
2918
|
-
expectedName,
|
|
2919
|
-
};
|
|
2920
|
-
}
|
|
2921
|
-
|
|
2922
|
-
async primeConversationByFirstCandidate(options = {}) {
|
|
2923
|
-
const clickResult = await this.chromeClient.callFunction(browserPrimeConversationByFirstCandidate);
|
|
2924
|
-
if (!clickResult?.ok) {
|
|
2925
|
-
throw new Error(clickResult?.error || 'PRIME_CONVERSATION_FAILED');
|
|
2926
|
-
}
|
|
2927
|
-
await new Promise((resolve) => setTimeout(resolve, options.delayMs || 700));
|
|
2928
|
-
const readyState = await this.waitForConversationReady({
|
|
2929
|
-
maxAttempts: options.maxAttempts || 12,
|
|
2930
|
-
delayMs: options.pollMs || 240,
|
|
2931
|
-
});
|
|
2932
|
-
return {
|
|
2933
|
-
...clickResult,
|
|
2934
|
-
readyState,
|
|
2935
|
-
};
|
|
2936
|
-
}
|
|
2937
|
-
|
|
2938
|
-
async openOnlineResume() {
|
|
2939
|
-
const isModalLikelyOpen = (state) =>
|
|
2940
|
-
Boolean(state?.open) ||
|
|
2941
|
-
Number(state?.iframeCount || 0) > 0 ||
|
|
2942
|
-
(Number(state?.scopeCount || 0) > 0 && Number(state?.closeCount || 0) > 0);
|
|
2943
|
-
|
|
2944
|
-
const preState = await this.getResumeModalState();
|
|
2945
|
-
if (isModalLikelyOpen(preState)) {
|
|
2946
|
-
return {
|
|
2947
|
-
clicked: false,
|
|
2948
|
-
detectedOpen: true,
|
|
2949
|
-
by: 'already-open',
|
|
2950
|
-
};
|
|
2951
|
-
}
|
|
2952
|
-
|
|
2953
|
-
const result = await this.chromeClient.callFunction(browserOpenOnlineResume, { click: true });
|
|
2954
|
-
if (!result?.ok) {
|
|
2955
|
-
throw new Error(
|
|
2956
|
-
`OPEN_ONLINE_RESUME_FAILED(selector=n/a,error=${result?.error || 'n/a'})`,
|
|
2957
|
-
);
|
|
2958
|
-
}
|
|
2959
|
-
|
|
2960
|
-
await new Promise((resolve) => setTimeout(resolve, 360));
|
|
2961
|
-
const state = await this.getResumeModalState();
|
|
2962
|
-
if (isModalLikelyOpen(state)) {
|
|
2963
|
-
return { ...result, clicked: true, detectedOpen: true, by: 'dom-target-click-once' };
|
|
2964
|
-
}
|
|
2965
|
-
|
|
2966
|
-
return {
|
|
2967
|
-
...result,
|
|
2968
|
-
clicked: true,
|
|
2969
|
-
detectedOpen: false,
|
|
2970
|
-
by: 'dom-target-click-once-no-modal',
|
|
2971
|
-
};
|
|
2972
|
-
}
|
|
2973
|
-
|
|
2974
|
-
async closeResumeModalDomOnce() {
|
|
2975
|
-
const stateBefore = await this.getResumeModalState();
|
|
2976
|
-
const openBefore =
|
|
2977
|
-
Boolean(stateBefore?.open) ||
|
|
2978
|
-
Number(stateBefore?.iframeCount || 0) > 0 ||
|
|
2979
|
-
Number(stateBefore?.scopeCount || 0) > 0;
|
|
2980
|
-
if (!openBefore) {
|
|
2981
|
-
return {
|
|
2982
|
-
closed: true,
|
|
2983
|
-
method: 'already-closed',
|
|
2984
|
-
finalState: stateBefore,
|
|
2985
|
-
};
|
|
2986
|
-
}
|
|
2987
|
-
|
|
2988
|
-
const result = await this.chromeClient.callFunction(browserCloseResumeModalDomOnce);
|
|
2989
|
-
if (!result?.ok) {
|
|
2990
|
-
const finalState = await this.getResumeModalState();
|
|
2991
|
-
return {
|
|
2992
|
-
closed: false,
|
|
2993
|
-
method: `dom-close-miss:${result?.error || 'unknown'}`,
|
|
2994
|
-
finalState,
|
|
2995
|
-
};
|
|
2996
|
-
}
|
|
2997
|
-
|
|
2998
|
-
await new Promise((resolve) => setTimeout(resolve, 420));
|
|
2999
|
-
let finalState = await this.getResumeModalState();
|
|
3000
|
-
let openAfter =
|
|
3001
|
-
Boolean(finalState?.open) ||
|
|
3002
|
-
Number(finalState?.iframeCount || 0) > 0 ||
|
|
3003
|
-
Number(finalState?.scopeCount || 0) > 0;
|
|
3004
|
-
const classText = String(finalState?.topScopeClass || '');
|
|
3005
|
-
if (openAfter && /\bv-leave\b|\bleave-active\b|\bleaving\b/i.test(classText)) {
|
|
3006
|
-
await new Promise((resolve) => setTimeout(resolve, 760));
|
|
3007
|
-
finalState = await this.getResumeModalState();
|
|
3008
|
-
openAfter =
|
|
3009
|
-
Boolean(finalState?.open) ||
|
|
3010
|
-
Number(finalState?.iframeCount || 0) > 0 ||
|
|
3011
|
-
Number(finalState?.scopeCount || 0) > 0;
|
|
3012
|
-
}
|
|
3013
|
-
return {
|
|
3014
|
-
closed: !openAfter,
|
|
3015
|
-
method: `dom-close-once:${result.selector || 'unknown'}`,
|
|
3016
|
-
finalState,
|
|
3017
|
-
};
|
|
3018
|
-
}
|
|
3019
|
-
|
|
3020
|
-
async isResumeModalOpen() {
|
|
3021
|
-
const result = await this.chromeClient.callFunction(browserIsResumeModalOpen);
|
|
3022
|
-
return Boolean(result?.open);
|
|
3023
|
-
}
|
|
3024
|
-
|
|
3025
|
-
async getResumeRateLimitWarning() {
|
|
3026
|
-
const result = await this.chromeClient.callFunction(browserGetResumeRateLimitWarning);
|
|
3027
|
-
return {
|
|
3028
|
-
hit: Boolean(result?.hit),
|
|
3029
|
-
text: String(result?.text || ''),
|
|
3030
|
-
};
|
|
3031
|
-
}
|
|
3032
|
-
|
|
3033
|
-
async getResumeModalState() {
|
|
3034
|
-
const result = await this.chromeClient.callFunction(browserIsResumeModalOpen);
|
|
3035
|
-
return {
|
|
3036
|
-
open: Boolean(result?.open),
|
|
3037
|
-
scopeCount: Number(result?.scopeCount || 0),
|
|
3038
|
-
iframeCount: Number(result?.iframeCount || 0),
|
|
3039
|
-
closeCount: Number(result?.closeCount || 0),
|
|
3040
|
-
topScopeClass: String(result?.topScopeClass || ''),
|
|
3041
|
-
topScopeScore: Number(result?.topScopeScore || 0),
|
|
3042
|
-
};
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
|
-
async isCandidateDetailOpen() {
|
|
3046
|
-
const result = await this.chromeClient.callFunction(browserIsCandidateDetailOpen);
|
|
3047
|
-
return Boolean(result?.open);
|
|
3048
|
-
}
|
|
3049
|
-
|
|
3050
|
-
async getCandidateDetailState() {
|
|
3051
|
-
const result = await this.chromeClient.callFunction(browserIsCandidateDetailOpen);
|
|
3052
|
-
return {
|
|
3053
|
-
open: Boolean(result?.open),
|
|
3054
|
-
panelCount: Number(result?.panelCount || 0),
|
|
3055
|
-
closeCount: Number(result?.closeCount || 0),
|
|
3056
|
-
topPanelClass: String(result?.topPanelClass || ''),
|
|
3057
|
-
topPanelScore: Number(result?.topPanelScore || 0),
|
|
3058
|
-
panelRect: result?.panelRect || null,
|
|
3059
|
-
closeRect: result?.closeRect || null,
|
|
3060
|
-
overlayClass: String(result?.overlayClass || ''),
|
|
3061
|
-
overlayRect: result?.overlayRect || null,
|
|
3062
|
-
contentClass: String(result?.contentClass || ''),
|
|
3063
|
-
contentRect: result?.contentRect || null,
|
|
3064
|
-
};
|
|
3065
|
-
}
|
|
3066
|
-
|
|
3067
|
-
async closeCandidateDetailDomOnce() {
|
|
3068
|
-
const stateBefore = await this.getCandidateDetailState();
|
|
3069
|
-
const drawerOpen = (state) =>
|
|
3070
|
-
Boolean(state?.open) ||
|
|
3071
|
-
Number(state?.panelCount || 0) > 0 ||
|
|
3072
|
-
Number(state?.closeCount || 0) > 0;
|
|
3073
|
-
const openBefore = drawerOpen(stateBefore);
|
|
3074
|
-
if (!openBefore) {
|
|
3075
|
-
return {
|
|
3076
|
-
closed: true,
|
|
3077
|
-
method: 'already-closed',
|
|
3078
|
-
finalState: stateBefore,
|
|
3079
|
-
};
|
|
3080
|
-
}
|
|
3081
|
-
|
|
3082
|
-
const result = await this.chromeClient.callFunction(browserCloseCandidateDetailDomOnce);
|
|
3083
|
-
if (!result?.ok) {
|
|
3084
|
-
const finalState = await this.getCandidateDetailState();
|
|
3085
|
-
return {
|
|
3086
|
-
closed: false,
|
|
3087
|
-
method: `dom-close-miss:${result?.error || 'unknown'}`,
|
|
3088
|
-
finalState,
|
|
3089
|
-
};
|
|
3090
|
-
}
|
|
3091
|
-
|
|
3092
|
-
let finalState = await this.getCandidateDetailState();
|
|
3093
|
-
let openAfter = drawerOpen(finalState);
|
|
3094
|
-
for (let attempt = 0; openAfter && attempt < 8; attempt += 1) {
|
|
3095
|
-
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
3096
|
-
finalState = await this.getCandidateDetailState();
|
|
3097
|
-
openAfter = drawerOpen(finalState);
|
|
3098
|
-
}
|
|
3099
|
-
return {
|
|
3100
|
-
closed: !openAfter,
|
|
3101
|
-
method: `dom-close-once:${result.selector || '.close-btn'}`,
|
|
3102
|
-
finalState,
|
|
3103
|
-
};
|
|
3104
|
-
}
|
|
3105
|
-
|
|
3106
|
-
async clickCandidateDetailOutside() {
|
|
3107
|
-
const result = await this.chromeClient.callFunction(browserFindCandidateDetailOutsideClickPoint);
|
|
3108
|
-
if (!result?.ok || !result?.point) {
|
|
3109
|
-
const finalState = await this.getCandidateDetailState();
|
|
3110
|
-
return {
|
|
3111
|
-
clicked: false,
|
|
3112
|
-
method: `outside-click-miss:${result?.error || 'unknown'}`,
|
|
3113
|
-
finalState,
|
|
3114
|
-
};
|
|
3115
|
-
}
|
|
3116
|
-
|
|
3117
|
-
const point = result.point;
|
|
3118
|
-
const rect = {
|
|
3119
|
-
left: Math.max(0, Number(point.x || 0) - 3),
|
|
3120
|
-
top: Math.max(0, Number(point.y || 0) - 3),
|
|
3121
|
-
width: 6,
|
|
3122
|
-
height: 6,
|
|
3123
|
-
};
|
|
3124
|
-
await this.clickRect(rect);
|
|
3125
|
-
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
3126
|
-
const finalState = await this.getCandidateDetailState();
|
|
3127
|
-
return {
|
|
3128
|
-
clicked: true,
|
|
3129
|
-
method: `outside-click:${result?.strategy || 'unknown'}`,
|
|
3130
|
-
finalState,
|
|
3131
|
-
};
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
async closeCandidateDetail({ maxAttempts = 4, ensureDismiss = false } = {}) {
|
|
3135
|
-
const drawerOpen = (state) =>
|
|
3136
|
-
Boolean(state?.open) ||
|
|
3137
|
-
Number(state?.panelCount || 0) > 0 ||
|
|
3138
|
-
Number(state?.closeCount || 0) > 0;
|
|
3139
|
-
const methods = [];
|
|
3140
|
-
for (let index = 0; index < maxAttempts; index += 1) {
|
|
3141
|
-
const state = await this.getCandidateDetailState();
|
|
3142
|
-
if (!drawerOpen(state) && !ensureDismiss) {
|
|
3143
|
-
return {
|
|
3144
|
-
closed: true,
|
|
3145
|
-
method: methods.join('+') || 'already-closed',
|
|
3146
|
-
finalState: state,
|
|
3147
|
-
};
|
|
3148
|
-
}
|
|
3149
|
-
|
|
3150
|
-
const selectorResult = await this.chromeClient.callFunction(browserCloseCandidateDetailDomOnce);
|
|
3151
|
-
if (selectorResult?.ok) {
|
|
3152
|
-
methods.push(`selector:${selectorResult.selector || '.close-btn'}`);
|
|
3153
|
-
} else {
|
|
3154
|
-
methods.push(`selector-miss:${selectorResult?.error || 'unknown'}`);
|
|
3155
|
-
}
|
|
3156
|
-
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
3157
|
-
|
|
3158
|
-
let midState = await this.getCandidateDetailState();
|
|
3159
|
-
if (!drawerOpen(midState)) {
|
|
3160
|
-
return {
|
|
3161
|
-
closed: true,
|
|
3162
|
-
method: methods.join('+'),
|
|
3163
|
-
finalState: midState,
|
|
3164
|
-
};
|
|
3165
|
-
}
|
|
3166
|
-
|
|
3167
|
-
await this.chromeClient.pressEscape();
|
|
3168
|
-
methods.push('escape');
|
|
3169
|
-
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
3170
|
-
|
|
3171
|
-
midState = await this.getCandidateDetailState();
|
|
3172
|
-
if (!drawerOpen(midState)) {
|
|
3173
|
-
return {
|
|
3174
|
-
closed: true,
|
|
3175
|
-
method: methods.join('+'),
|
|
3176
|
-
finalState: midState,
|
|
3177
|
-
};
|
|
3178
|
-
}
|
|
3179
|
-
|
|
3180
|
-
const outsideResult = await this.clickCandidateDetailOutside();
|
|
3181
|
-
methods.push(outsideResult.method || 'outside-click:unknown');
|
|
3182
|
-
midState = outsideResult.finalState || await this.getCandidateDetailState();
|
|
3183
|
-
if (!drawerOpen(midState)) {
|
|
3184
|
-
return {
|
|
3185
|
-
closed: true,
|
|
3186
|
-
method: methods.join('+'),
|
|
3187
|
-
finalState: midState,
|
|
3188
|
-
};
|
|
3189
|
-
}
|
|
3190
|
-
|
|
3191
|
-
if (ensureDismiss && index >= 1) {
|
|
3192
|
-
const finalSweep = await this.getCandidateDetailState();
|
|
3193
|
-
if (!drawerOpen(finalSweep)) {
|
|
3194
|
-
return {
|
|
3195
|
-
closed: true,
|
|
3196
|
-
method: methods.join('+'),
|
|
3197
|
-
finalState: finalSweep,
|
|
3198
|
-
};
|
|
3199
|
-
}
|
|
3200
|
-
}
|
|
3201
|
-
}
|
|
3202
|
-
|
|
3203
|
-
const finalState = await this.getCandidateDetailState();
|
|
3204
|
-
if (!drawerOpen(finalState)) {
|
|
3205
|
-
return {
|
|
3206
|
-
closed: true,
|
|
3207
|
-
method: methods.join('+') || 'fallback',
|
|
3208
|
-
finalState,
|
|
3209
|
-
};
|
|
3210
|
-
}
|
|
3211
|
-
return {
|
|
3212
|
-
closed: false,
|
|
3213
|
-
method: methods.join('+') || 'failed',
|
|
3214
|
-
finalState,
|
|
3215
|
-
};
|
|
3216
|
-
}
|
|
3217
|
-
|
|
3218
|
-
async waitForResumeModalOpen(options = {}) {
|
|
3219
|
-
const maxAttempts = options.maxAttempts || 30;
|
|
3220
|
-
const delayMs = options.delayMs || 300;
|
|
3221
|
-
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
3222
|
-
const state = await this.getResumeModalState();
|
|
3223
|
-
const appearsOpen =
|
|
3224
|
-
Boolean(state.open) ||
|
|
3225
|
-
Number(state.iframeCount || 0) > 0 ||
|
|
3226
|
-
(Number(state.scopeCount || 0) > 0 && Number(state.closeCount || 0) > 0);
|
|
3227
|
-
if (appearsOpen && attempt >= 1) {
|
|
3228
|
-
return true;
|
|
3229
|
-
}
|
|
3230
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
3231
|
-
}
|
|
3232
|
-
const finalState = await this.getResumeModalState();
|
|
3233
|
-
throw new Error(
|
|
3234
|
-
`RESUME_MODAL_OPEN_TIMEOUT(scope=${finalState.scopeCount},iframe=${finalState.iframeCount},close=${finalState.closeCount},class=${finalState.topScopeClass || 'n/a'})`,
|
|
3235
|
-
);
|
|
3236
|
-
}
|
|
3237
|
-
|
|
3238
|
-
async closeResumeModal({ maxAttempts = 12, ensureDismiss = false } = {}) {
|
|
3239
|
-
const overlayOpen = (state) =>
|
|
3240
|
-
Boolean(state?.open) ||
|
|
3241
|
-
Number(state?.iframeCount || 0) > 0 ||
|
|
3242
|
-
Number(state?.scopeCount || 0) > 0 ||
|
|
3243
|
-
Number(state?.closeCount || 0) > 0;
|
|
3244
|
-
const methods = [];
|
|
3245
|
-
for (let index = 0; index < maxAttempts; index += 1) {
|
|
3246
|
-
const state = await this.getResumeModalState();
|
|
3247
|
-
if (!overlayOpen(state) && !ensureDismiss) {
|
|
3248
|
-
return {
|
|
3249
|
-
closed: true,
|
|
3250
|
-
method: methods.join('+') || 'already-closed',
|
|
3251
|
-
finalState: state,
|
|
3252
|
-
};
|
|
3253
|
-
}
|
|
3254
|
-
|
|
3255
|
-
const selectorResult = await this.chromeClient.callFunction(browserCloseResumeModalBySelector);
|
|
3256
|
-
if (selectorResult?.ok) {
|
|
3257
|
-
methods.push(
|
|
3258
|
-
`${selectorResult?.method || 'selector'}:${selectorResult?.selector || 'unknown'}`,
|
|
3259
|
-
);
|
|
3260
|
-
} else {
|
|
3261
|
-
methods.push(`selector-miss:${selectorResult?.error || 'unknown'}`);
|
|
3262
|
-
const anySelector = await this.chromeClient.callFunction(browserCloseAnyPopupBySelector);
|
|
3263
|
-
if (anySelector?.ok) {
|
|
3264
|
-
methods.push(`any-selector:${anySelector.selector || 'unknown'}`);
|
|
3265
|
-
} else {
|
|
3266
|
-
methods.push(`any-selector-miss:${anySelector?.error || 'unknown'}`);
|
|
3267
|
-
}
|
|
3268
|
-
}
|
|
3269
|
-
await new Promise((resolve) => setTimeout(resolve, 260));
|
|
3270
|
-
|
|
3271
|
-
let midState = await this.getResumeModalState();
|
|
3272
|
-
if (!overlayOpen(midState)) {
|
|
3273
|
-
return {
|
|
3274
|
-
closed: true,
|
|
3275
|
-
method: methods.join('+'),
|
|
3276
|
-
finalState: midState,
|
|
3277
|
-
};
|
|
3278
|
-
}
|
|
3279
|
-
|
|
3280
|
-
await this.chromeClient.pressEscape();
|
|
3281
|
-
methods.push('escape');
|
|
3282
|
-
await new Promise((resolve) => setTimeout(resolve, 260));
|
|
3283
|
-
|
|
3284
|
-
midState = await this.getResumeModalState();
|
|
3285
|
-
if (!overlayOpen(midState)) {
|
|
3286
|
-
return {
|
|
3287
|
-
closed: true,
|
|
3288
|
-
method: methods.join('+'),
|
|
3289
|
-
finalState: midState,
|
|
3290
|
-
};
|
|
3291
|
-
}
|
|
3292
|
-
|
|
3293
|
-
if (index % 2 === 0) {
|
|
3294
|
-
const closeRect = await this.chromeClient.callFunction(browserFindResumeCloseRect);
|
|
3295
|
-
if (closeRect?.ok && closeRect.rect) {
|
|
3296
|
-
await this.clickRect(closeRect.rect);
|
|
3297
|
-
methods.push(`rect:${closeRect.selector || 'unknown'}`);
|
|
3298
|
-
await new Promise((resolve) => setTimeout(resolve, 260));
|
|
3299
|
-
} else {
|
|
3300
|
-
methods.push(`rect-miss:${closeRect?.error || 'unknown'}`);
|
|
3301
|
-
const anyRect = await this.chromeClient.callFunction(browserFindAnyPopupCloseRect);
|
|
3302
|
-
if (anyRect?.ok && anyRect.rect) {
|
|
3303
|
-
await this.clickRect(anyRect.rect);
|
|
3304
|
-
methods.push(`any-rect:${anyRect.selector || 'unknown'}`);
|
|
3305
|
-
await new Promise((resolve) => setTimeout(resolve, 260));
|
|
3306
|
-
} else {
|
|
3307
|
-
methods.push(`any-rect-miss:${anyRect?.error || 'unknown'}`);
|
|
3308
|
-
}
|
|
3309
|
-
}
|
|
3310
|
-
}
|
|
3311
|
-
|
|
3312
|
-
if (ensureDismiss && index >= 1) {
|
|
3313
|
-
const finalSweep = await this.getResumeModalState();
|
|
3314
|
-
if (!overlayOpen(finalSweep)) {
|
|
3315
|
-
return {
|
|
3316
|
-
closed: true,
|
|
3317
|
-
method: methods.join('+'),
|
|
3318
|
-
finalState: finalSweep,
|
|
3319
|
-
};
|
|
3320
|
-
}
|
|
3321
|
-
}
|
|
3322
|
-
}
|
|
3323
|
-
|
|
3324
|
-
const finalState = await this.getResumeModalState();
|
|
3325
|
-
if (!overlayOpen(finalState)) {
|
|
3326
|
-
return {
|
|
3327
|
-
closed: true,
|
|
3328
|
-
method: methods.join('+') || 'fallback',
|
|
3329
|
-
finalState,
|
|
3330
|
-
};
|
|
3331
|
-
}
|
|
3332
|
-
return {
|
|
3333
|
-
closed: false,
|
|
3334
|
-
method: methods.join('+') || 'failed',
|
|
3335
|
-
finalState,
|
|
3336
|
-
};
|
|
3337
|
-
}
|
|
3338
|
-
|
|
3339
|
-
async closeAllPopupsHard({ maxAttempts = 10 } = {}) {
|
|
3340
|
-
const methods = [];
|
|
3341
|
-
for (let index = 0; index < maxAttempts; index += 1) {
|
|
3342
|
-
const visible = await this.chromeClient.callFunction(browserAnyPopupVisible);
|
|
3343
|
-
if (!visible?.visible) {
|
|
3344
|
-
return {
|
|
3345
|
-
closed: true,
|
|
3346
|
-
method: methods.join('+') || 'already-closed',
|
|
3347
|
-
finalVisibleCount: Number(visible?.count || 0),
|
|
3348
|
-
};
|
|
3349
|
-
}
|
|
3350
|
-
|
|
3351
|
-
const bySelector = await this.chromeClient.callFunction(browserCloseAnyPopupBySelector);
|
|
3352
|
-
if (bySelector?.ok) {
|
|
3353
|
-
methods.push(`any-selector:${bySelector.selector || 'unknown'}`);
|
|
3354
|
-
} else {
|
|
3355
|
-
methods.push(`any-selector-miss:${bySelector?.error || 'unknown'}`);
|
|
3356
|
-
}
|
|
3357
|
-
|
|
3358
|
-
await this.chromeClient.pressEscape();
|
|
3359
|
-
methods.push('escape');
|
|
3360
|
-
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
3361
|
-
|
|
3362
|
-
const byRect = await this.chromeClient.callFunction(browserFindAnyPopupCloseRect);
|
|
3363
|
-
if (byRect?.ok && byRect.rect) {
|
|
3364
|
-
await this.clickRect(byRect.rect);
|
|
3365
|
-
methods.push(`any-rect:${byRect.selector || 'unknown'}`);
|
|
3366
|
-
} else {
|
|
3367
|
-
methods.push(`any-rect-miss:${byRect?.error || 'unknown'}`);
|
|
3368
|
-
}
|
|
3369
|
-
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
3370
|
-
}
|
|
3371
|
-
|
|
3372
|
-
const finalVisible = await this.chromeClient.callFunction(browserAnyPopupVisible);
|
|
3373
|
-
return {
|
|
3374
|
-
closed: !Boolean(finalVisible?.visible),
|
|
3375
|
-
method: methods.join('+') || 'failed',
|
|
3376
|
-
finalVisibleCount: Number(finalVisible?.count || 0),
|
|
3377
|
-
};
|
|
3378
|
-
}
|
|
3379
|
-
|
|
3380
|
-
async clickRect(rect) {
|
|
3381
|
-
const centerX = rect.left + rect.width / 2;
|
|
3382
|
-
const centerY = rect.top + rect.height / 2;
|
|
3383
|
-
const targetX = Math.round(centerX + (Math.random() - 0.5) * Math.min(6, rect.width * 0.2));
|
|
3384
|
-
const targetY = Math.round(centerY + (Math.random() - 0.5) * Math.min(6, rect.height * 0.2));
|
|
3385
|
-
await this.chromeClient.Input.dispatchMouseEvent({
|
|
3386
|
-
type: 'mouseMoved',
|
|
3387
|
-
x: targetX,
|
|
3388
|
-
y: targetY,
|
|
3389
|
-
});
|
|
3390
|
-
await this.chromeClient.Input.dispatchMouseEvent({
|
|
3391
|
-
type: 'mousePressed',
|
|
3392
|
-
x: targetX,
|
|
3393
|
-
y: targetY,
|
|
3394
|
-
button: 'left',
|
|
3395
|
-
clickCount: 1,
|
|
3396
|
-
});
|
|
3397
|
-
await this.chromeClient.Input.dispatchMouseEvent({
|
|
3398
|
-
type: 'mouseReleased',
|
|
3399
|
-
x: targetX,
|
|
3400
|
-
y: targetY,
|
|
3401
|
-
button: 'left',
|
|
3402
|
-
clickCount: 1,
|
|
3403
|
-
});
|
|
3404
|
-
}
|
|
3405
|
-
|
|
3406
|
-
async setEditorMessage(message) {
|
|
3407
|
-
const result = await this.chromeClient.callFunction(browserSetEditorMessage, message);
|
|
3408
|
-
if (!result?.ok) {
|
|
3409
|
-
throw new Error(result?.error || 'SET_EDITOR_MESSAGE_FAILED');
|
|
3410
|
-
}
|
|
3411
|
-
return {
|
|
3412
|
-
value: String(result.value || ''),
|
|
3413
|
-
activeSubmit: Boolean(result?.activeSubmit),
|
|
3414
|
-
};
|
|
3415
|
-
}
|
|
3416
|
-
|
|
3417
|
-
async sendMessage(expectedText = '') {
|
|
3418
|
-
let result = await this.chromeClient.callFunction(browserSendMessage, { expectedText });
|
|
3419
|
-
if (!result?.ok) {
|
|
3420
|
-
throw new Error(result?.error || 'SEND_MESSAGE_FAILED');
|
|
3421
|
-
}
|
|
3422
|
-
if (!result?.sent) {
|
|
3423
|
-
await this.chromeClient.pressEnter();
|
|
3424
|
-
await new Promise((resolve) => setTimeout(resolve, 280));
|
|
3425
|
-
result = await this.chromeClient.callFunction(browserSendMessage, { expectedText });
|
|
3426
|
-
if (!result?.ok) {
|
|
3427
|
-
throw new Error(result?.error || 'SEND_MESSAGE_FAILED');
|
|
3428
|
-
}
|
|
3429
|
-
}
|
|
3430
|
-
return result;
|
|
3431
|
-
}
|
|
3432
|
-
|
|
3433
|
-
async clickAskResume() {
|
|
3434
|
-
let lastResult = null;
|
|
3435
|
-
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
3436
|
-
const result = await this.chromeClient.callFunction(browserClickAskResume);
|
|
3437
|
-
lastResult = result;
|
|
3438
|
-
if (result?.ok) {
|
|
3439
|
-
return result;
|
|
3440
|
-
}
|
|
3441
|
-
if (result?.error !== 'ASK_RESUME_BUTTON_NOT_FOUND') {
|
|
3442
|
-
break;
|
|
3443
|
-
}
|
|
3444
|
-
await new Promise((resolve) => setTimeout(resolve, 220 + attempt * 80));
|
|
3445
|
-
}
|
|
3446
|
-
throw new Error(lastResult?.error || 'CLICK_ASK_RESUME_FAILED');
|
|
3447
|
-
}
|
|
3448
|
-
|
|
3449
|
-
async clickConfirmRequestResume() {
|
|
3450
|
-
for (let attempt = 0; attempt < 14; attempt += 1) {
|
|
3451
|
-
const beforeState = await this.chromeClient.callFunction(browserGetRequestResumeUiState);
|
|
3452
|
-
if (beforeState?.hasDisabledOperateAsk) {
|
|
3453
|
-
return {
|
|
3454
|
-
ok: true,
|
|
3455
|
-
confirmed: false,
|
|
3456
|
-
requestedVerified: true,
|
|
3457
|
-
assumedRequested: true,
|
|
3458
|
-
uiState: beforeState,
|
|
3459
|
-
};
|
|
3460
|
-
}
|
|
3461
|
-
|
|
3462
|
-
const result = await this.chromeClient.callFunction(browserClickConfirmRequestResume);
|
|
3463
|
-
if (result?.ok) {
|
|
3464
|
-
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
3465
|
-
}
|
|
3466
|
-
|
|
3467
|
-
const uiState = await this.chromeClient.callFunction(browserGetRequestResumeUiState);
|
|
3468
|
-
if (uiState?.hasDisabledOperateAsk) {
|
|
3469
|
-
return {
|
|
3470
|
-
ok: true,
|
|
3471
|
-
confirmed: Boolean(result?.ok),
|
|
3472
|
-
requestedVerified: true,
|
|
3473
|
-
assumedRequested: !Boolean(result?.ok),
|
|
3474
|
-
uiState,
|
|
3475
|
-
};
|
|
3476
|
-
}
|
|
3477
|
-
|
|
3478
|
-
if (
|
|
3479
|
-
attempt % 3 === 2 &&
|
|
3480
|
-
uiState?.hasAskResume &&
|
|
3481
|
-
!uiState?.askDisabled &&
|
|
3482
|
-
!uiState?.hasConfirm
|
|
3483
|
-
) {
|
|
3484
|
-
await this.chromeClient.callFunction(browserClickAskResume);
|
|
3485
|
-
}
|
|
3486
|
-
await new Promise((resolve) => setTimeout(resolve, 260));
|
|
3487
|
-
}
|
|
3488
|
-
const finalUiState = await this.chromeClient.callFunction(browserGetRequestResumeUiState);
|
|
3489
|
-
if (finalUiState?.hasDisabledOperateAsk) {
|
|
3490
|
-
return {
|
|
3491
|
-
ok: true,
|
|
3492
|
-
confirmed: false,
|
|
3493
|
-
requestedVerified: true,
|
|
3494
|
-
assumedRequested: true,
|
|
3495
|
-
uiState: finalUiState,
|
|
3496
|
-
};
|
|
3497
|
-
}
|
|
3498
|
-
throw new Error(
|
|
3499
|
-
`CLICK_CONFIRM_REQUEST_RESUME_FAILED(state=${JSON.stringify(finalUiState || {})})`,
|
|
3500
|
-
);
|
|
3501
|
-
}
|
|
3502
|
-
|
|
3503
|
-
async getResumeRequestMessageState() {
|
|
3504
|
-
const result = await this.chromeClient.callFunction(browserGetResumeRequestMessageState);
|
|
3505
|
-
return {
|
|
3506
|
-
ok: Boolean(result?.ok),
|
|
3507
|
-
error: String(result?.error || ''),
|
|
3508
|
-
count: Number(result?.count || 0),
|
|
3509
|
-
lastText: String(result?.lastText || ''),
|
|
3510
|
-
recent: Array.isArray(result?.recent) ? result.recent.map((item) => String(item || '')) : [],
|
|
3511
|
-
};
|
|
3512
|
-
}
|
|
3513
|
-
|
|
3514
|
-
async getResumeProfileFromDom() {
|
|
3515
|
-
const result = await this.chromeClient.callFunction(browserExtractResumeProfileFromModal);
|
|
3516
|
-
return {
|
|
3517
|
-
ok: Boolean(result?.ok),
|
|
3518
|
-
error: String(result?.error || ''),
|
|
3519
|
-
name: String(result?.name || ''),
|
|
3520
|
-
primarySchool: String(result?.primarySchool || ''),
|
|
3521
|
-
major: String(result?.major || ''),
|
|
3522
|
-
company: String(result?.company || ''),
|
|
3523
|
-
position: String(result?.position || ''),
|
|
3524
|
-
schools: Array.isArray(result?.schools) ? result.schools.map((item) => String(item || '')) : [],
|
|
3525
|
-
majors: Array.isArray(result?.majors) ? result.majors.map((item) => String(item || '')) : [],
|
|
3526
|
-
debug: result?.debug && typeof result.debug === 'object' ? result.debug : {},
|
|
3527
|
-
};
|
|
3528
|
-
}
|
|
3529
|
-
|
|
3530
|
-
async waitForResumeRequestMessage({ baselineCount = 0, timeoutMs = 6500, pollMs = 260 } = {}) {
|
|
3531
|
-
const start = Date.now();
|
|
3532
|
-
let latest = null;
|
|
3533
|
-
const hasSentMessage = (state = {}) => {
|
|
3534
|
-
const lastText = String(state?.lastText || '');
|
|
3535
|
-
const recent = Array.isArray(state?.recent) ? state.recent : [];
|
|
3536
|
-
if (lastText.includes('简历请求已发送')) return true;
|
|
3537
|
-
return recent.some((item) => String(item || '').includes('简历请求已发送'));
|
|
3538
|
-
};
|
|
3539
|
-
while (Date.now() - start < timeoutMs) {
|
|
3540
|
-
const state = await this.getResumeRequestMessageState();
|
|
3541
|
-
latest = state;
|
|
3542
|
-
if (state.count > baselineCount || hasSentMessage(state)) {
|
|
3543
|
-
return {
|
|
3544
|
-
observed: true,
|
|
3545
|
-
state,
|
|
3546
|
-
};
|
|
3547
|
-
}
|
|
3548
|
-
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
3549
|
-
}
|
|
3550
|
-
|
|
3551
|
-
return {
|
|
3552
|
-
observed: false,
|
|
3553
|
-
state: latest || {
|
|
3554
|
-
ok: false,
|
|
3555
|
-
error: 'RESUME_REQUEST_MESSAGE_STATE_EMPTY',
|
|
3556
|
-
count: 0,
|
|
3557
|
-
lastText: '',
|
|
3558
|
-
recent: [],
|
|
3559
|
-
},
|
|
3560
|
-
};
|
|
3561
|
-
}
|
|
3562
|
-
}
|