@reconcrap/boss-recommend-mcp 2.0.47 → 2.0.49
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/bin/boss-recommend-mcp.js +4 -4
- package/config/screening-config.example.json +27 -27
- package/package.json +1 -1
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +39 -39
- package/skills/boss-chat/SKILL.md +93 -93
- package/skills/boss-recommend-pipeline/README.md +12 -12
- package/skills/boss-recommend-pipeline/SKILL.md +180 -180
- package/skills/boss-recruit-pipeline/README.md +17 -17
- package/skills/boss-recruit-pipeline/SKILL.md +58 -58
- package/src/chat-mcp.js +1780 -1780
- package/src/chat-runtime-config.js +749 -749
- package/src/cli.js +3054 -3054
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +1586 -1453
- package/src/core/capture/index.js +1201 -1201
- package/src/core/cv-acquisition/index.js +238 -238
- package/src/core/cv-capture-target/index.js +299 -299
- package/src/core/greet-quota/index.js +54 -54
- package/src/core/infinite-list/index.js +1326 -1326
- package/src/core/reporting/legacy-csv.js +341 -341
- package/src/core/run/timing.js +33 -33
- package/src/core/self-heal/index.js +973 -973
- package/src/core/self-heal/viewport.js +564 -564
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +221 -221
- package/src/domains/chat/detail.js +1668 -1668
- package/src/domains/chat/index.js +7 -7
- package/src/domains/chat/jobs.js +592 -592
- package/src/domains/chat/page-guard.js +98 -98
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +1977 -1977
- package/src/domains/recommend/actions.js +457 -457
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/constants.js +165 -165
- package/src/domains/recommend/detail.js +1 -1
- package/src/domains/recommend/filters.js +610 -610
- package/src/domains/recommend/index.js +10 -10
- package/src/domains/recommend/jobs.js +378 -316
- package/src/domains/recommend/refresh.js +491 -472
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +50 -29
- package/src/domains/recommend/scopes.js +246 -246
- package/src/domains/recruit/actions.js +277 -277
- package/src/domains/recruit/cards.js +74 -74
- package/src/domains/recruit/constants.js +167 -167
- package/src/domains/recruit/detail.js +461 -461
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +451 -451
- package/src/domains/recruit/refresh.js +44 -44
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1207 -1207
- package/src/domains/recruit/search.js +1202 -1202
- package/src/recommend-mcp.js +22 -22
- package/src/recruit-mcp.js +1338 -1338
package/src/domains/chat/jobs.js
CHANGED
|
@@ -1,592 +1,592 @@
|
|
|
1
|
-
import {
|
|
2
|
-
clickNodeCenter,
|
|
3
|
-
clickPoint,
|
|
4
|
-
DETERMINISTIC_CLICK_OPTIONS,
|
|
5
|
-
getAttributesMap,
|
|
6
|
-
getNodeBox,
|
|
7
|
-
getOuterHTML,
|
|
8
|
-
pressKey,
|
|
9
|
-
querySelector,
|
|
10
|
-
querySelectorAll,
|
|
11
|
-
sleep
|
|
12
|
-
} from "../../core/browser/index.js";
|
|
13
|
-
import {
|
|
14
|
-
htmlToText,
|
|
15
|
-
normalizeText
|
|
16
|
-
} from "../../core/screening/index.js";
|
|
17
|
-
import {
|
|
18
|
-
CHAT_JOB_FALLBACK_SELECTORS,
|
|
19
|
-
CHAT_JOB_LABEL_SELECTORS,
|
|
20
|
-
CHAT_JOB_OPTION_SELECTORS,
|
|
21
|
-
CHAT_JOB_TRIGGER_SELECTORS
|
|
22
|
-
} from "./constants.js";
|
|
23
|
-
import { getChatRoots } from "./roots.js";
|
|
24
|
-
|
|
25
|
-
function isActiveClass(className = "") {
|
|
26
|
-
return /\b(active|selected|current)\b/i.test(String(className || ""));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function normalizeJobText(value) {
|
|
30
|
-
return normalizeText(value).replace(/\s+_/g, " _").replace(/_\s+/g, "_ ");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function freshTopRootNodeId(client, fallbackNodeId) {
|
|
34
|
-
try {
|
|
35
|
-
const rootState = await getChatRoots(client);
|
|
36
|
-
return rootState.rootNodes.top || fallbackNodeId;
|
|
37
|
-
} catch {
|
|
38
|
-
return fallbackNodeId;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function safeQuerySelector(client, rootNodeId, selector) {
|
|
43
|
-
try {
|
|
44
|
-
return await querySelector(client, rootNodeId, selector);
|
|
45
|
-
} catch {
|
|
46
|
-
return 0;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function safeQuerySelectorAll(client, rootNodeId, selector) {
|
|
51
|
-
try {
|
|
52
|
-
return await querySelectorAll(client, rootNodeId, selector);
|
|
53
|
-
} catch {
|
|
54
|
-
return [];
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function readNodeText(client, nodeId) {
|
|
59
|
-
const outerHTML = await getOuterHTML(client, nodeId);
|
|
60
|
-
return {
|
|
61
|
-
outerHTML,
|
|
62
|
-
text: normalizeJobText(htmlToText(outerHTML))
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function readSelectedJobLabel(client, rootNodeId) {
|
|
67
|
-
for (const selector of CHAT_JOB_LABEL_SELECTORS) {
|
|
68
|
-
const nodeId = await safeQuerySelector(client, rootNodeId, selector);
|
|
69
|
-
if (!nodeId) continue;
|
|
70
|
-
try {
|
|
71
|
-
const { text } = await readNodeText(client, nodeId);
|
|
72
|
-
if (text) return { selector, label: text };
|
|
73
|
-
} catch {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return { selector: "", label: "" };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function readOptionNode(client, nodeId, index, { selector, source }) {
|
|
81
|
-
const [attributes, textResult] = await Promise.all([
|
|
82
|
-
getAttributesMap(client, nodeId),
|
|
83
|
-
readNodeText(client, nodeId)
|
|
84
|
-
]);
|
|
85
|
-
const label = normalizeJobText(attributes.title || textResult.text);
|
|
86
|
-
if (!label) return null;
|
|
87
|
-
const rawValue = normalizeText(attributes.value || attributes["data-value"] || attributes["data-id"] || "");
|
|
88
|
-
return {
|
|
89
|
-
node_id: nodeId,
|
|
90
|
-
index,
|
|
91
|
-
label,
|
|
92
|
-
title: label,
|
|
93
|
-
value: rawValue || label,
|
|
94
|
-
active: isActiveClass(attributes.class),
|
|
95
|
-
is_all: rawValue === "-1" || /^(全部职位|全部岗位|全部)$/u.test(label),
|
|
96
|
-
source,
|
|
97
|
-
selector
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function readClickableOptionNode(client, nodeId, index, { selector, source }) {
|
|
102
|
-
const option = await readOptionNode(client, nodeId, index, { selector, source });
|
|
103
|
-
if (!option) return null;
|
|
104
|
-
try {
|
|
105
|
-
const box = await getNodeBox(client, nodeId);
|
|
106
|
-
option.center = box.center;
|
|
107
|
-
option.rect = box.rect;
|
|
108
|
-
option.visible = box.rect.width > 2 && box.rect.height > 2;
|
|
109
|
-
} catch {
|
|
110
|
-
option.center = null;
|
|
111
|
-
option.rect = null;
|
|
112
|
-
option.visible = false;
|
|
113
|
-
}
|
|
114
|
-
return option;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async function readOptionsForSelector(client, rootNodeId, selector, { source }) {
|
|
118
|
-
const nodeIds = await safeQuerySelectorAll(client, rootNodeId, selector);
|
|
119
|
-
const options = [];
|
|
120
|
-
for (const nodeId of nodeIds) {
|
|
121
|
-
let option = null;
|
|
122
|
-
try {
|
|
123
|
-
option = await readClickableOptionNode(client, nodeId, options.length + 1, {
|
|
124
|
-
selector,
|
|
125
|
-
source
|
|
126
|
-
});
|
|
127
|
-
} catch {
|
|
128
|
-
option = null;
|
|
129
|
-
}
|
|
130
|
-
if (option) options.push(option);
|
|
131
|
-
}
|
|
132
|
-
return options;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function dedupeJobOptions(options = []) {
|
|
136
|
-
const seen = new Set();
|
|
137
|
-
const deduped = [];
|
|
138
|
-
for (const option of options) {
|
|
139
|
-
const key = `${normalizeText(option.value).toLowerCase()}|${normalizeText(option.label).toLowerCase()}`;
|
|
140
|
-
if (seen.has(key)) continue;
|
|
141
|
-
seen.add(key);
|
|
142
|
-
deduped.push({
|
|
143
|
-
...option,
|
|
144
|
-
index: deduped.length + 1
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
return deduped;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export async function readChatJobOptions(client, rootNodeId, {
|
|
151
|
-
timeoutMs = 12000,
|
|
152
|
-
intervalMs = 300
|
|
153
|
-
} = {}) {
|
|
154
|
-
const started = Date.now();
|
|
155
|
-
let selected = { selector: "", label: "" };
|
|
156
|
-
let lastPrimary = {
|
|
157
|
-
selector: "",
|
|
158
|
-
source: "chat-job-list",
|
|
159
|
-
options: []
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
while (Date.now() - started <= timeoutMs) {
|
|
163
|
-
selected = await readSelectedJobLabel(client, rootNodeId);
|
|
164
|
-
for (const selector of CHAT_JOB_OPTION_SELECTORS) {
|
|
165
|
-
const options = await readOptionsForSelector(client, rootNodeId, selector, {
|
|
166
|
-
source: "chat-job-list"
|
|
167
|
-
});
|
|
168
|
-
if (options.length) {
|
|
169
|
-
lastPrimary = {
|
|
170
|
-
selector,
|
|
171
|
-
source: "chat-job-list",
|
|
172
|
-
options: dedupeJobOptions(options)
|
|
173
|
-
};
|
|
174
|
-
return {
|
|
175
|
-
selector,
|
|
176
|
-
source: "chat-job-list",
|
|
177
|
-
selected_label: selected.label || "",
|
|
178
|
-
selected_selector: selected.selector || "",
|
|
179
|
-
job_options: lastPrimary.options
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
await sleep(intervalMs);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const fallbackOptions = [];
|
|
187
|
-
for (const selector of CHAT_JOB_FALLBACK_SELECTORS) {
|
|
188
|
-
const options = await readOptionsForSelector(client, rootNodeId, selector, {
|
|
189
|
-
source: "conversation-source-job"
|
|
190
|
-
});
|
|
191
|
-
fallbackOptions.push(...options);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const dedupedFallback = dedupeJobOptions(fallbackOptions);
|
|
195
|
-
if (dedupedFallback.length) {
|
|
196
|
-
return {
|
|
197
|
-
selector: CHAT_JOB_FALLBACK_SELECTORS.join(", "),
|
|
198
|
-
source: "conversation-source-job",
|
|
199
|
-
selected_label: selected.label || "",
|
|
200
|
-
selected_selector: selected.selector || "",
|
|
201
|
-
job_options: dedupedFallback
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
selector: lastPrimary.selector,
|
|
207
|
-
source: lastPrimary.source,
|
|
208
|
-
selected_label: selected.label || "",
|
|
209
|
-
selected_selector: selected.selector || "",
|
|
210
|
-
job_options: []
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function matchJobOption(option, jobLabel = "") {
|
|
215
|
-
const requested = normalizeJobText(jobLabel).toLowerCase();
|
|
216
|
-
if (!requested) return false;
|
|
217
|
-
return [
|
|
218
|
-
option.value,
|
|
219
|
-
option.label,
|
|
220
|
-
option.title
|
|
221
|
-
].map((value) => normalizeJobText(value).toLowerCase()).some((value) => (
|
|
222
|
-
value === requested
|
|
223
|
-
|| value.includes(requested)
|
|
224
|
-
|| requested.includes(value)
|
|
225
|
-
));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function activeMatchingJobOption(options = [], jobLabel = "") {
|
|
229
|
-
return (options || []).find((option) => option.active && matchJobOption(option, jobLabel)) || null;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function selectedLabelMatches(label = "", jobLabel = "") {
|
|
233
|
-
const normalized = normalizeJobText(label);
|
|
234
|
-
return Boolean(normalized && matchJobOption({ label: normalized, value: normalized, title: normalized }, jobLabel));
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async function clickFirstVisible(client, rootNodeId, selectors = []) {
|
|
238
|
-
for (const selector of selectors) {
|
|
239
|
-
const nodeIds = await safeQuerySelectorAll(client, rootNodeId, selector);
|
|
240
|
-
for (const nodeId of nodeIds) {
|
|
241
|
-
try {
|
|
242
|
-
const box = await getNodeBox(client, nodeId);
|
|
243
|
-
if (box.rect.width <= 2 || box.rect.height <= 2) continue;
|
|
244
|
-
await clickPoint(client, box.center.x, box.center.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
245
|
-
return {
|
|
246
|
-
clicked: true,
|
|
247
|
-
selector,
|
|
248
|
-
node_id: nodeId,
|
|
249
|
-
center: box.center
|
|
250
|
-
};
|
|
251
|
-
} catch {}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return {
|
|
255
|
-
clicked: false,
|
|
256
|
-
selector: "",
|
|
257
|
-
node_id: 0
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async function openChatJobDropdown(client, rootNodeId, {
|
|
262
|
-
timeoutMs = 12000,
|
|
263
|
-
intervalMs = 300,
|
|
264
|
-
settleMs = 800
|
|
265
|
-
} = {}) {
|
|
266
|
-
const started = Date.now();
|
|
267
|
-
const triedPoints = new Set();
|
|
268
|
-
const attempts = [];
|
|
269
|
-
const initialClose = await closeChatJobDropdownQuietly(client, rootNodeId, Math.min(settleMs, 300));
|
|
270
|
-
for (const selector of CHAT_JOB_TRIGGER_SELECTORS) {
|
|
271
|
-
const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
|
|
272
|
-
const nodeIds = await safeQuerySelectorAll(client, currentRootNodeId, selector);
|
|
273
|
-
for (const nodeId of nodeIds) {
|
|
274
|
-
try {
|
|
275
|
-
const box = await getNodeBox(client, nodeId);
|
|
276
|
-
if (box.rect.width <= 2 || box.rect.height <= 2) continue;
|
|
277
|
-
const y = box.center.y;
|
|
278
|
-
const xCandidates = [
|
|
279
|
-
["center", box.center.x],
|
|
280
|
-
["right_12", box.rect.x + box.rect.width - 12],
|
|
281
|
-
["right_44", box.rect.x + box.rect.width - 44],
|
|
282
|
-
["right_64", box.rect.x + box.rect.width - 64]
|
|
283
|
-
].filter(([, x]) => x > box.rect.x + 4 && x < box.rect.x + box.rect.width - 4);
|
|
284
|
-
for (const [pointName, x] of xCandidates) {
|
|
285
|
-
const pointKey = `${nodeId}:${Math.round(x)}:${Math.round(y)}`;
|
|
286
|
-
if (triedPoints.has(pointKey)) continue;
|
|
287
|
-
triedPoints.add(pointKey);
|
|
288
|
-
await clickPoint(client, x, y, DETERMINISTIC_CLICK_OPTIONS);
|
|
289
|
-
if (settleMs > 0) await sleep(Math.min(settleMs, 800));
|
|
290
|
-
const remaining = Math.max(300, timeoutMs - (Date.now() - started));
|
|
291
|
-
const optionsResult = await waitForChatJobOptions(client, currentRootNodeId, {
|
|
292
|
-
timeoutMs: Math.min(remaining, 1800),
|
|
293
|
-
intervalMs,
|
|
294
|
-
requireVisible: true
|
|
295
|
-
});
|
|
296
|
-
const visibleCount = (optionsResult.job_options || []).filter((option) => option.visible).length;
|
|
297
|
-
const attempt = {
|
|
298
|
-
clicked: true,
|
|
299
|
-
selector,
|
|
300
|
-
node_id: nodeId,
|
|
301
|
-
point: pointName,
|
|
302
|
-
center: { x, y },
|
|
303
|
-
visible_option_count: visibleCount,
|
|
304
|
-
initial_close: initialClose
|
|
305
|
-
};
|
|
306
|
-
attempts.push(attempt);
|
|
307
|
-
if (visibleCount > 0) {
|
|
308
|
-
return {
|
|
309
|
-
...attempt,
|
|
310
|
-
attempts,
|
|
311
|
-
options_result: optionsResult
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
if (Date.now() - started > timeoutMs) break;
|
|
315
|
-
}
|
|
316
|
-
} catch (error) {
|
|
317
|
-
attempts.push({
|
|
318
|
-
clicked: false,
|
|
319
|
-
selector,
|
|
320
|
-
node_id: nodeId,
|
|
321
|
-
error: error?.message || String(error)
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
if (Date.now() - started > timeoutMs) break;
|
|
325
|
-
}
|
|
326
|
-
if (Date.now() - started > timeoutMs) break;
|
|
327
|
-
}
|
|
328
|
-
return {
|
|
329
|
-
clicked: attempts.some((attempt) => attempt.clicked),
|
|
330
|
-
selector: attempts.find((attempt) => attempt.clicked)?.selector || "",
|
|
331
|
-
node_id: attempts.find((attempt) => attempt.clicked)?.node_id || 0,
|
|
332
|
-
attempts,
|
|
333
|
-
options_result: null
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
async function waitForChatJobOptions(client, rootNodeId, {
|
|
338
|
-
timeoutMs = 12000,
|
|
339
|
-
intervalMs = 300,
|
|
340
|
-
requireVisible = false
|
|
341
|
-
} = {}) {
|
|
342
|
-
const started = Date.now();
|
|
343
|
-
let latest = null;
|
|
344
|
-
while (Date.now() - started <= timeoutMs) {
|
|
345
|
-
const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
|
|
346
|
-
latest = await readChatJobOptions(client, currentRootNodeId, {
|
|
347
|
-
timeoutMs: Math.min(intervalMs, 300),
|
|
348
|
-
intervalMs
|
|
349
|
-
});
|
|
350
|
-
const options = latest.job_options || [];
|
|
351
|
-
if (options.length && (!requireVisible || options.some((option) => option.visible))) {
|
|
352
|
-
return latest;
|
|
353
|
-
}
|
|
354
|
-
await sleep(intervalMs);
|
|
355
|
-
}
|
|
356
|
-
return latest || {
|
|
357
|
-
selector: "",
|
|
358
|
-
source: "chat-job-list",
|
|
359
|
-
selected_label: "",
|
|
360
|
-
job_options: []
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
async function waitForSelectedChatJob(client, rootNodeId, jobLabel = "", {
|
|
365
|
-
timeoutMs = 5000,
|
|
366
|
-
intervalMs = 300
|
|
367
|
-
} = {}) {
|
|
368
|
-
const started = Date.now();
|
|
369
|
-
let latest = null;
|
|
370
|
-
while (Date.now() - started <= timeoutMs) {
|
|
371
|
-
const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
|
|
372
|
-
latest = await readChatJobOptions(client, currentRootNodeId, {
|
|
373
|
-
timeoutMs: Math.min(intervalMs, 300),
|
|
374
|
-
intervalMs
|
|
375
|
-
});
|
|
376
|
-
if (
|
|
377
|
-
selectedLabelMatches(latest.selected_label, jobLabel)
|
|
378
|
-
|| activeMatchingJobOption(latest.job_options || [], jobLabel)
|
|
379
|
-
) {
|
|
380
|
-
return {
|
|
381
|
-
verified: true,
|
|
382
|
-
result: latest
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
await sleep(intervalMs);
|
|
386
|
-
}
|
|
387
|
-
return {
|
|
388
|
-
verified: false,
|
|
389
|
-
result: latest
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function visibleChatJobOptions(client, rootNodeId) {
|
|
394
|
-
const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
|
|
395
|
-
const visible = [];
|
|
396
|
-
for (const selector of CHAT_JOB_OPTION_SELECTORS) {
|
|
397
|
-
const nodeIds = await safeQuerySelectorAll(client, currentRootNodeId, selector);
|
|
398
|
-
for (const nodeId of nodeIds) {
|
|
399
|
-
try {
|
|
400
|
-
const box = await getNodeBox(client, nodeId);
|
|
401
|
-
if (box.rect.width > 2 && box.rect.height > 2) {
|
|
402
|
-
visible.push({
|
|
403
|
-
selector,
|
|
404
|
-
node_id: nodeId,
|
|
405
|
-
center: box.center,
|
|
406
|
-
rect: box.rect
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
} catch {
|
|
410
|
-
// Hidden job options are normal when the dropdown is closed.
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return visible;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
export async function closeChatJobDropdown(client, rootNodeId, {
|
|
418
|
-
settleMs = 180
|
|
419
|
-
} = {}) {
|
|
420
|
-
const before = await visibleChatJobOptions(client, rootNodeId);
|
|
421
|
-
if (!before.length) {
|
|
422
|
-
return {
|
|
423
|
-
ok: true,
|
|
424
|
-
closed: false,
|
|
425
|
-
reason: "already_closed",
|
|
426
|
-
visible_before_count: 0,
|
|
427
|
-
visible_after_count: 0
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
if (typeof client?.Input?.dispatchKeyEvent !== "function") {
|
|
431
|
-
return {
|
|
432
|
-
ok: false,
|
|
433
|
-
closed: false,
|
|
434
|
-
reason: "dispatch_key_unavailable",
|
|
435
|
-
visible_before_count: before.length,
|
|
436
|
-
visible_after_count: before.length
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
await pressKey(client, "Escape", {
|
|
440
|
-
code: "Escape",
|
|
441
|
-
windowsVirtualKeyCode: 27,
|
|
442
|
-
nativeVirtualKeyCode: 27
|
|
443
|
-
});
|
|
444
|
-
if (settleMs > 0) await sleep(settleMs);
|
|
445
|
-
const after = await visibleChatJobOptions(client, rootNodeId);
|
|
446
|
-
return {
|
|
447
|
-
ok: after.length === 0,
|
|
448
|
-
closed: after.length === 0,
|
|
449
|
-
reason: after.length ? "still_visible_after_escape" : "escape",
|
|
450
|
-
visible_before_count: before.length,
|
|
451
|
-
visible_after_count: after.length,
|
|
452
|
-
first_visible_before: before[0] || null,
|
|
453
|
-
first_visible_after: after[0] || null
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
async function closeChatJobDropdownQuietly(client, rootNodeId, settleMs = 180) {
|
|
458
|
-
try {
|
|
459
|
-
return await closeChatJobDropdown(client, rootNodeId, { settleMs });
|
|
460
|
-
} catch (error) {
|
|
461
|
-
return {
|
|
462
|
-
ok: false,
|
|
463
|
-
closed: false,
|
|
464
|
-
reason: "close_failed",
|
|
465
|
-
error: error?.message || String(error)
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
export async function selectChatJob(client, rootNodeId, {
|
|
471
|
-
jobLabel = "",
|
|
472
|
-
timeoutMs = 12000,
|
|
473
|
-
intervalMs = 300,
|
|
474
|
-
settleMs = 800
|
|
475
|
-
} = {}) {
|
|
476
|
-
const requested = normalizeJobText(jobLabel);
|
|
477
|
-
if (!requested) {
|
|
478
|
-
return {
|
|
479
|
-
selected: false,
|
|
480
|
-
reason: "missing_job_label"
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
let currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
|
|
485
|
-
let optionsResult = await readChatJobOptions(client, currentRootNodeId, {
|
|
486
|
-
timeoutMs: Math.min(timeoutMs, 1500),
|
|
487
|
-
intervalMs
|
|
488
|
-
});
|
|
489
|
-
let matched = (optionsResult.job_options || []).find((option) => matchJobOption(option, requested)) || null;
|
|
490
|
-
if (
|
|
491
|
-
matched
|
|
492
|
-
&& (
|
|
493
|
-
matched.active
|
|
494
|
-
|| selectedLabelMatches(optionsResult.selected_label, matched.label)
|
|
495
|
-
|| selectedLabelMatches(optionsResult.selected_label, requested)
|
|
496
|
-
)
|
|
497
|
-
) {
|
|
498
|
-
const menuClose = await closeChatJobDropdownQuietly(client, currentRootNodeId, Math.min(settleMs, 300));
|
|
499
|
-
return {
|
|
500
|
-
selected: true,
|
|
501
|
-
verified: true,
|
|
502
|
-
already_current: true,
|
|
503
|
-
requested,
|
|
504
|
-
selected_option: matched,
|
|
505
|
-
options: optionsResult.job_options || [],
|
|
506
|
-
selected_label: optionsResult.selected_label || matched.label,
|
|
507
|
-
menu_close: menuClose
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
if (!matched || !matched.visible) {
|
|
512
|
-
const triggerRootNodeId = await freshTopRootNodeId(client, currentRootNodeId);
|
|
513
|
-
const trigger = await openChatJobDropdown(client, triggerRootNodeId, {
|
|
514
|
-
timeoutMs,
|
|
515
|
-
intervalMs,
|
|
516
|
-
settleMs
|
|
517
|
-
});
|
|
518
|
-
currentRootNodeId = await freshTopRootNodeId(client, triggerRootNodeId);
|
|
519
|
-
optionsResult = trigger.options_result || await waitForChatJobOptions(client, currentRootNodeId, {
|
|
520
|
-
timeoutMs,
|
|
521
|
-
intervalMs,
|
|
522
|
-
requireVisible: true
|
|
523
|
-
});
|
|
524
|
-
matched = (optionsResult.job_options || []).find((option) => matchJobOption(option, requested)) || null;
|
|
525
|
-
if (!matched || !matched.visible) {
|
|
526
|
-
const menuClose = await closeChatJobDropdownQuietly(client, currentRootNodeId, Math.min(settleMs, 300));
|
|
527
|
-
return {
|
|
528
|
-
selected: false,
|
|
529
|
-
reason: matched ? "job_option_not_visible" : "job_option_not_found",
|
|
530
|
-
requested,
|
|
531
|
-
trigger,
|
|
532
|
-
options: optionsResult.job_options || [],
|
|
533
|
-
selected_label_before: optionsResult.selected_label || "",
|
|
534
|
-
menu_close: menuClose
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
if (matched.active || normalizeJobText(optionsResult.selected_label).toLowerCase() === normalizeJobText(matched.label).toLowerCase()) {
|
|
540
|
-
const menuClose = await closeChatJobDropdownQuietly(client, currentRootNodeId, Math.min(settleMs, 300));
|
|
541
|
-
return {
|
|
542
|
-
selected: true,
|
|
543
|
-
verified: true,
|
|
544
|
-
already_current: true,
|
|
545
|
-
requested,
|
|
546
|
-
selected_option: matched,
|
|
547
|
-
options: optionsResult.job_options || [],
|
|
548
|
-
selected_label: optionsResult.selected_label || matched.label,
|
|
549
|
-
menu_close: menuClose
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
if (matched.center) {
|
|
554
|
-
await clickPoint(client, matched.center.x, matched.center.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
555
|
-
} else {
|
|
556
|
-
await clickNodeCenter(client, matched.node_id, {
|
|
557
|
-
...DETERMINISTIC_CLICK_OPTIONS,
|
|
558
|
-
scrollIntoView: true
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
if (settleMs > 0) await sleep(settleMs);
|
|
562
|
-
|
|
563
|
-
const afterRootNodeId = await freshTopRootNodeId(client, currentRootNodeId);
|
|
564
|
-
const verification = await waitForSelectedChatJob(client, afterRootNodeId, matched.label, {
|
|
565
|
-
timeoutMs: Math.min(timeoutMs, 5000),
|
|
566
|
-
intervalMs
|
|
567
|
-
});
|
|
568
|
-
const after = verification.result || {
|
|
569
|
-
selected_label: "",
|
|
570
|
-
job_options: []
|
|
571
|
-
};
|
|
572
|
-
const afterMatch = (after.job_options || []).find((option) => matchJobOption(option, matched.label)) || matched;
|
|
573
|
-
const selectedLabel = normalizeJobText(after.selected_label || "");
|
|
574
|
-
const activeMatch = activeMatchingJobOption(after.job_options || [], matched.label);
|
|
575
|
-
const verified = Boolean(verification.verified || selectedLabelMatches(selectedLabel, matched.label) || activeMatch);
|
|
576
|
-
const menuClose = await closeChatJobDropdownQuietly(client, afterRootNodeId, Math.min(settleMs, 300));
|
|
577
|
-
|
|
578
|
-
return {
|
|
579
|
-
selected: verified,
|
|
580
|
-
verified,
|
|
581
|
-
already_current: false,
|
|
582
|
-
reason: verified ? "verified" : "job_selection_not_verified",
|
|
583
|
-
requested,
|
|
584
|
-
selected_option: afterMatch,
|
|
585
|
-
active_option: activeMatch,
|
|
586
|
-
options: after.job_options || optionsResult.job_options || [],
|
|
587
|
-
selected_label: selectedLabel,
|
|
588
|
-
before: optionsResult,
|
|
589
|
-
after,
|
|
590
|
-
menu_close: menuClose
|
|
591
|
-
};
|
|
592
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
clickNodeCenter,
|
|
3
|
+
clickPoint,
|
|
4
|
+
DETERMINISTIC_CLICK_OPTIONS,
|
|
5
|
+
getAttributesMap,
|
|
6
|
+
getNodeBox,
|
|
7
|
+
getOuterHTML,
|
|
8
|
+
pressKey,
|
|
9
|
+
querySelector,
|
|
10
|
+
querySelectorAll,
|
|
11
|
+
sleep
|
|
12
|
+
} from "../../core/browser/index.js";
|
|
13
|
+
import {
|
|
14
|
+
htmlToText,
|
|
15
|
+
normalizeText
|
|
16
|
+
} from "../../core/screening/index.js";
|
|
17
|
+
import {
|
|
18
|
+
CHAT_JOB_FALLBACK_SELECTORS,
|
|
19
|
+
CHAT_JOB_LABEL_SELECTORS,
|
|
20
|
+
CHAT_JOB_OPTION_SELECTORS,
|
|
21
|
+
CHAT_JOB_TRIGGER_SELECTORS
|
|
22
|
+
} from "./constants.js";
|
|
23
|
+
import { getChatRoots } from "./roots.js";
|
|
24
|
+
|
|
25
|
+
function isActiveClass(className = "") {
|
|
26
|
+
return /\b(active|selected|current)\b/i.test(String(className || ""));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeJobText(value) {
|
|
30
|
+
return normalizeText(value).replace(/\s+_/g, " _").replace(/_\s+/g, "_ ");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function freshTopRootNodeId(client, fallbackNodeId) {
|
|
34
|
+
try {
|
|
35
|
+
const rootState = await getChatRoots(client);
|
|
36
|
+
return rootState.rootNodes.top || fallbackNodeId;
|
|
37
|
+
} catch {
|
|
38
|
+
return fallbackNodeId;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function safeQuerySelector(client, rootNodeId, selector) {
|
|
43
|
+
try {
|
|
44
|
+
return await querySelector(client, rootNodeId, selector);
|
|
45
|
+
} catch {
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function safeQuerySelectorAll(client, rootNodeId, selector) {
|
|
51
|
+
try {
|
|
52
|
+
return await querySelectorAll(client, rootNodeId, selector);
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function readNodeText(client, nodeId) {
|
|
59
|
+
const outerHTML = await getOuterHTML(client, nodeId);
|
|
60
|
+
return {
|
|
61
|
+
outerHTML,
|
|
62
|
+
text: normalizeJobText(htmlToText(outerHTML))
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function readSelectedJobLabel(client, rootNodeId) {
|
|
67
|
+
for (const selector of CHAT_JOB_LABEL_SELECTORS) {
|
|
68
|
+
const nodeId = await safeQuerySelector(client, rootNodeId, selector);
|
|
69
|
+
if (!nodeId) continue;
|
|
70
|
+
try {
|
|
71
|
+
const { text } = await readNodeText(client, nodeId);
|
|
72
|
+
if (text) return { selector, label: text };
|
|
73
|
+
} catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { selector: "", label: "" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function readOptionNode(client, nodeId, index, { selector, source }) {
|
|
81
|
+
const [attributes, textResult] = await Promise.all([
|
|
82
|
+
getAttributesMap(client, nodeId),
|
|
83
|
+
readNodeText(client, nodeId)
|
|
84
|
+
]);
|
|
85
|
+
const label = normalizeJobText(attributes.title || textResult.text);
|
|
86
|
+
if (!label) return null;
|
|
87
|
+
const rawValue = normalizeText(attributes.value || attributes["data-value"] || attributes["data-id"] || "");
|
|
88
|
+
return {
|
|
89
|
+
node_id: nodeId,
|
|
90
|
+
index,
|
|
91
|
+
label,
|
|
92
|
+
title: label,
|
|
93
|
+
value: rawValue || label,
|
|
94
|
+
active: isActiveClass(attributes.class),
|
|
95
|
+
is_all: rawValue === "-1" || /^(全部职位|全部岗位|全部)$/u.test(label),
|
|
96
|
+
source,
|
|
97
|
+
selector
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function readClickableOptionNode(client, nodeId, index, { selector, source }) {
|
|
102
|
+
const option = await readOptionNode(client, nodeId, index, { selector, source });
|
|
103
|
+
if (!option) return null;
|
|
104
|
+
try {
|
|
105
|
+
const box = await getNodeBox(client, nodeId);
|
|
106
|
+
option.center = box.center;
|
|
107
|
+
option.rect = box.rect;
|
|
108
|
+
option.visible = box.rect.width > 2 && box.rect.height > 2;
|
|
109
|
+
} catch {
|
|
110
|
+
option.center = null;
|
|
111
|
+
option.rect = null;
|
|
112
|
+
option.visible = false;
|
|
113
|
+
}
|
|
114
|
+
return option;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function readOptionsForSelector(client, rootNodeId, selector, { source }) {
|
|
118
|
+
const nodeIds = await safeQuerySelectorAll(client, rootNodeId, selector);
|
|
119
|
+
const options = [];
|
|
120
|
+
for (const nodeId of nodeIds) {
|
|
121
|
+
let option = null;
|
|
122
|
+
try {
|
|
123
|
+
option = await readClickableOptionNode(client, nodeId, options.length + 1, {
|
|
124
|
+
selector,
|
|
125
|
+
source
|
|
126
|
+
});
|
|
127
|
+
} catch {
|
|
128
|
+
option = null;
|
|
129
|
+
}
|
|
130
|
+
if (option) options.push(option);
|
|
131
|
+
}
|
|
132
|
+
return options;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function dedupeJobOptions(options = []) {
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
const deduped = [];
|
|
138
|
+
for (const option of options) {
|
|
139
|
+
const key = `${normalizeText(option.value).toLowerCase()}|${normalizeText(option.label).toLowerCase()}`;
|
|
140
|
+
if (seen.has(key)) continue;
|
|
141
|
+
seen.add(key);
|
|
142
|
+
deduped.push({
|
|
143
|
+
...option,
|
|
144
|
+
index: deduped.length + 1
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return deduped;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function readChatJobOptions(client, rootNodeId, {
|
|
151
|
+
timeoutMs = 12000,
|
|
152
|
+
intervalMs = 300
|
|
153
|
+
} = {}) {
|
|
154
|
+
const started = Date.now();
|
|
155
|
+
let selected = { selector: "", label: "" };
|
|
156
|
+
let lastPrimary = {
|
|
157
|
+
selector: "",
|
|
158
|
+
source: "chat-job-list",
|
|
159
|
+
options: []
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
while (Date.now() - started <= timeoutMs) {
|
|
163
|
+
selected = await readSelectedJobLabel(client, rootNodeId);
|
|
164
|
+
for (const selector of CHAT_JOB_OPTION_SELECTORS) {
|
|
165
|
+
const options = await readOptionsForSelector(client, rootNodeId, selector, {
|
|
166
|
+
source: "chat-job-list"
|
|
167
|
+
});
|
|
168
|
+
if (options.length) {
|
|
169
|
+
lastPrimary = {
|
|
170
|
+
selector,
|
|
171
|
+
source: "chat-job-list",
|
|
172
|
+
options: dedupeJobOptions(options)
|
|
173
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
selector,
|
|
176
|
+
source: "chat-job-list",
|
|
177
|
+
selected_label: selected.label || "",
|
|
178
|
+
selected_selector: selected.selector || "",
|
|
179
|
+
job_options: lastPrimary.options
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
await sleep(intervalMs);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const fallbackOptions = [];
|
|
187
|
+
for (const selector of CHAT_JOB_FALLBACK_SELECTORS) {
|
|
188
|
+
const options = await readOptionsForSelector(client, rootNodeId, selector, {
|
|
189
|
+
source: "conversation-source-job"
|
|
190
|
+
});
|
|
191
|
+
fallbackOptions.push(...options);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const dedupedFallback = dedupeJobOptions(fallbackOptions);
|
|
195
|
+
if (dedupedFallback.length) {
|
|
196
|
+
return {
|
|
197
|
+
selector: CHAT_JOB_FALLBACK_SELECTORS.join(", "),
|
|
198
|
+
source: "conversation-source-job",
|
|
199
|
+
selected_label: selected.label || "",
|
|
200
|
+
selected_selector: selected.selector || "",
|
|
201
|
+
job_options: dedupedFallback
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
selector: lastPrimary.selector,
|
|
207
|
+
source: lastPrimary.source,
|
|
208
|
+
selected_label: selected.label || "",
|
|
209
|
+
selected_selector: selected.selector || "",
|
|
210
|
+
job_options: []
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function matchJobOption(option, jobLabel = "") {
|
|
215
|
+
const requested = normalizeJobText(jobLabel).toLowerCase();
|
|
216
|
+
if (!requested) return false;
|
|
217
|
+
return [
|
|
218
|
+
option.value,
|
|
219
|
+
option.label,
|
|
220
|
+
option.title
|
|
221
|
+
].map((value) => normalizeJobText(value).toLowerCase()).some((value) => (
|
|
222
|
+
value === requested
|
|
223
|
+
|| value.includes(requested)
|
|
224
|
+
|| requested.includes(value)
|
|
225
|
+
));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function activeMatchingJobOption(options = [], jobLabel = "") {
|
|
229
|
+
return (options || []).find((option) => option.active && matchJobOption(option, jobLabel)) || null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function selectedLabelMatches(label = "", jobLabel = "") {
|
|
233
|
+
const normalized = normalizeJobText(label);
|
|
234
|
+
return Boolean(normalized && matchJobOption({ label: normalized, value: normalized, title: normalized }, jobLabel));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function clickFirstVisible(client, rootNodeId, selectors = []) {
|
|
238
|
+
for (const selector of selectors) {
|
|
239
|
+
const nodeIds = await safeQuerySelectorAll(client, rootNodeId, selector);
|
|
240
|
+
for (const nodeId of nodeIds) {
|
|
241
|
+
try {
|
|
242
|
+
const box = await getNodeBox(client, nodeId);
|
|
243
|
+
if (box.rect.width <= 2 || box.rect.height <= 2) continue;
|
|
244
|
+
await clickPoint(client, box.center.x, box.center.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
245
|
+
return {
|
|
246
|
+
clicked: true,
|
|
247
|
+
selector,
|
|
248
|
+
node_id: nodeId,
|
|
249
|
+
center: box.center
|
|
250
|
+
};
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
clicked: false,
|
|
256
|
+
selector: "",
|
|
257
|
+
node_id: 0
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function openChatJobDropdown(client, rootNodeId, {
|
|
262
|
+
timeoutMs = 12000,
|
|
263
|
+
intervalMs = 300,
|
|
264
|
+
settleMs = 800
|
|
265
|
+
} = {}) {
|
|
266
|
+
const started = Date.now();
|
|
267
|
+
const triedPoints = new Set();
|
|
268
|
+
const attempts = [];
|
|
269
|
+
const initialClose = await closeChatJobDropdownQuietly(client, rootNodeId, Math.min(settleMs, 300));
|
|
270
|
+
for (const selector of CHAT_JOB_TRIGGER_SELECTORS) {
|
|
271
|
+
const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
|
|
272
|
+
const nodeIds = await safeQuerySelectorAll(client, currentRootNodeId, selector);
|
|
273
|
+
for (const nodeId of nodeIds) {
|
|
274
|
+
try {
|
|
275
|
+
const box = await getNodeBox(client, nodeId);
|
|
276
|
+
if (box.rect.width <= 2 || box.rect.height <= 2) continue;
|
|
277
|
+
const y = box.center.y;
|
|
278
|
+
const xCandidates = [
|
|
279
|
+
["center", box.center.x],
|
|
280
|
+
["right_12", box.rect.x + box.rect.width - 12],
|
|
281
|
+
["right_44", box.rect.x + box.rect.width - 44],
|
|
282
|
+
["right_64", box.rect.x + box.rect.width - 64]
|
|
283
|
+
].filter(([, x]) => x > box.rect.x + 4 && x < box.rect.x + box.rect.width - 4);
|
|
284
|
+
for (const [pointName, x] of xCandidates) {
|
|
285
|
+
const pointKey = `${nodeId}:${Math.round(x)}:${Math.round(y)}`;
|
|
286
|
+
if (triedPoints.has(pointKey)) continue;
|
|
287
|
+
triedPoints.add(pointKey);
|
|
288
|
+
await clickPoint(client, x, y, DETERMINISTIC_CLICK_OPTIONS);
|
|
289
|
+
if (settleMs > 0) await sleep(Math.min(settleMs, 800));
|
|
290
|
+
const remaining = Math.max(300, timeoutMs - (Date.now() - started));
|
|
291
|
+
const optionsResult = await waitForChatJobOptions(client, currentRootNodeId, {
|
|
292
|
+
timeoutMs: Math.min(remaining, 1800),
|
|
293
|
+
intervalMs,
|
|
294
|
+
requireVisible: true
|
|
295
|
+
});
|
|
296
|
+
const visibleCount = (optionsResult.job_options || []).filter((option) => option.visible).length;
|
|
297
|
+
const attempt = {
|
|
298
|
+
clicked: true,
|
|
299
|
+
selector,
|
|
300
|
+
node_id: nodeId,
|
|
301
|
+
point: pointName,
|
|
302
|
+
center: { x, y },
|
|
303
|
+
visible_option_count: visibleCount,
|
|
304
|
+
initial_close: initialClose
|
|
305
|
+
};
|
|
306
|
+
attempts.push(attempt);
|
|
307
|
+
if (visibleCount > 0) {
|
|
308
|
+
return {
|
|
309
|
+
...attempt,
|
|
310
|
+
attempts,
|
|
311
|
+
options_result: optionsResult
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (Date.now() - started > timeoutMs) break;
|
|
315
|
+
}
|
|
316
|
+
} catch (error) {
|
|
317
|
+
attempts.push({
|
|
318
|
+
clicked: false,
|
|
319
|
+
selector,
|
|
320
|
+
node_id: nodeId,
|
|
321
|
+
error: error?.message || String(error)
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
if (Date.now() - started > timeoutMs) break;
|
|
325
|
+
}
|
|
326
|
+
if (Date.now() - started > timeoutMs) break;
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
clicked: attempts.some((attempt) => attempt.clicked),
|
|
330
|
+
selector: attempts.find((attempt) => attempt.clicked)?.selector || "",
|
|
331
|
+
node_id: attempts.find((attempt) => attempt.clicked)?.node_id || 0,
|
|
332
|
+
attempts,
|
|
333
|
+
options_result: null
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function waitForChatJobOptions(client, rootNodeId, {
|
|
338
|
+
timeoutMs = 12000,
|
|
339
|
+
intervalMs = 300,
|
|
340
|
+
requireVisible = false
|
|
341
|
+
} = {}) {
|
|
342
|
+
const started = Date.now();
|
|
343
|
+
let latest = null;
|
|
344
|
+
while (Date.now() - started <= timeoutMs) {
|
|
345
|
+
const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
|
|
346
|
+
latest = await readChatJobOptions(client, currentRootNodeId, {
|
|
347
|
+
timeoutMs: Math.min(intervalMs, 300),
|
|
348
|
+
intervalMs
|
|
349
|
+
});
|
|
350
|
+
const options = latest.job_options || [];
|
|
351
|
+
if (options.length && (!requireVisible || options.some((option) => option.visible))) {
|
|
352
|
+
return latest;
|
|
353
|
+
}
|
|
354
|
+
await sleep(intervalMs);
|
|
355
|
+
}
|
|
356
|
+
return latest || {
|
|
357
|
+
selector: "",
|
|
358
|
+
source: "chat-job-list",
|
|
359
|
+
selected_label: "",
|
|
360
|
+
job_options: []
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function waitForSelectedChatJob(client, rootNodeId, jobLabel = "", {
|
|
365
|
+
timeoutMs = 5000,
|
|
366
|
+
intervalMs = 300
|
|
367
|
+
} = {}) {
|
|
368
|
+
const started = Date.now();
|
|
369
|
+
let latest = null;
|
|
370
|
+
while (Date.now() - started <= timeoutMs) {
|
|
371
|
+
const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
|
|
372
|
+
latest = await readChatJobOptions(client, currentRootNodeId, {
|
|
373
|
+
timeoutMs: Math.min(intervalMs, 300),
|
|
374
|
+
intervalMs
|
|
375
|
+
});
|
|
376
|
+
if (
|
|
377
|
+
selectedLabelMatches(latest.selected_label, jobLabel)
|
|
378
|
+
|| activeMatchingJobOption(latest.job_options || [], jobLabel)
|
|
379
|
+
) {
|
|
380
|
+
return {
|
|
381
|
+
verified: true,
|
|
382
|
+
result: latest
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
await sleep(intervalMs);
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
verified: false,
|
|
389
|
+
result: latest
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function visibleChatJobOptions(client, rootNodeId) {
|
|
394
|
+
const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
|
|
395
|
+
const visible = [];
|
|
396
|
+
for (const selector of CHAT_JOB_OPTION_SELECTORS) {
|
|
397
|
+
const nodeIds = await safeQuerySelectorAll(client, currentRootNodeId, selector);
|
|
398
|
+
for (const nodeId of nodeIds) {
|
|
399
|
+
try {
|
|
400
|
+
const box = await getNodeBox(client, nodeId);
|
|
401
|
+
if (box.rect.width > 2 && box.rect.height > 2) {
|
|
402
|
+
visible.push({
|
|
403
|
+
selector,
|
|
404
|
+
node_id: nodeId,
|
|
405
|
+
center: box.center,
|
|
406
|
+
rect: box.rect
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
// Hidden job options are normal when the dropdown is closed.
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return visible;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export async function closeChatJobDropdown(client, rootNodeId, {
|
|
418
|
+
settleMs = 180
|
|
419
|
+
} = {}) {
|
|
420
|
+
const before = await visibleChatJobOptions(client, rootNodeId);
|
|
421
|
+
if (!before.length) {
|
|
422
|
+
return {
|
|
423
|
+
ok: true,
|
|
424
|
+
closed: false,
|
|
425
|
+
reason: "already_closed",
|
|
426
|
+
visible_before_count: 0,
|
|
427
|
+
visible_after_count: 0
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
if (typeof client?.Input?.dispatchKeyEvent !== "function") {
|
|
431
|
+
return {
|
|
432
|
+
ok: false,
|
|
433
|
+
closed: false,
|
|
434
|
+
reason: "dispatch_key_unavailable",
|
|
435
|
+
visible_before_count: before.length,
|
|
436
|
+
visible_after_count: before.length
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
await pressKey(client, "Escape", {
|
|
440
|
+
code: "Escape",
|
|
441
|
+
windowsVirtualKeyCode: 27,
|
|
442
|
+
nativeVirtualKeyCode: 27
|
|
443
|
+
});
|
|
444
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
445
|
+
const after = await visibleChatJobOptions(client, rootNodeId);
|
|
446
|
+
return {
|
|
447
|
+
ok: after.length === 0,
|
|
448
|
+
closed: after.length === 0,
|
|
449
|
+
reason: after.length ? "still_visible_after_escape" : "escape",
|
|
450
|
+
visible_before_count: before.length,
|
|
451
|
+
visible_after_count: after.length,
|
|
452
|
+
first_visible_before: before[0] || null,
|
|
453
|
+
first_visible_after: after[0] || null
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function closeChatJobDropdownQuietly(client, rootNodeId, settleMs = 180) {
|
|
458
|
+
try {
|
|
459
|
+
return await closeChatJobDropdown(client, rootNodeId, { settleMs });
|
|
460
|
+
} catch (error) {
|
|
461
|
+
return {
|
|
462
|
+
ok: false,
|
|
463
|
+
closed: false,
|
|
464
|
+
reason: "close_failed",
|
|
465
|
+
error: error?.message || String(error)
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export async function selectChatJob(client, rootNodeId, {
|
|
471
|
+
jobLabel = "",
|
|
472
|
+
timeoutMs = 12000,
|
|
473
|
+
intervalMs = 300,
|
|
474
|
+
settleMs = 800
|
|
475
|
+
} = {}) {
|
|
476
|
+
const requested = normalizeJobText(jobLabel);
|
|
477
|
+
if (!requested) {
|
|
478
|
+
return {
|
|
479
|
+
selected: false,
|
|
480
|
+
reason: "missing_job_label"
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
let currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
|
|
485
|
+
let optionsResult = await readChatJobOptions(client, currentRootNodeId, {
|
|
486
|
+
timeoutMs: Math.min(timeoutMs, 1500),
|
|
487
|
+
intervalMs
|
|
488
|
+
});
|
|
489
|
+
let matched = (optionsResult.job_options || []).find((option) => matchJobOption(option, requested)) || null;
|
|
490
|
+
if (
|
|
491
|
+
matched
|
|
492
|
+
&& (
|
|
493
|
+
matched.active
|
|
494
|
+
|| selectedLabelMatches(optionsResult.selected_label, matched.label)
|
|
495
|
+
|| selectedLabelMatches(optionsResult.selected_label, requested)
|
|
496
|
+
)
|
|
497
|
+
) {
|
|
498
|
+
const menuClose = await closeChatJobDropdownQuietly(client, currentRootNodeId, Math.min(settleMs, 300));
|
|
499
|
+
return {
|
|
500
|
+
selected: true,
|
|
501
|
+
verified: true,
|
|
502
|
+
already_current: true,
|
|
503
|
+
requested,
|
|
504
|
+
selected_option: matched,
|
|
505
|
+
options: optionsResult.job_options || [],
|
|
506
|
+
selected_label: optionsResult.selected_label || matched.label,
|
|
507
|
+
menu_close: menuClose
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (!matched || !matched.visible) {
|
|
512
|
+
const triggerRootNodeId = await freshTopRootNodeId(client, currentRootNodeId);
|
|
513
|
+
const trigger = await openChatJobDropdown(client, triggerRootNodeId, {
|
|
514
|
+
timeoutMs,
|
|
515
|
+
intervalMs,
|
|
516
|
+
settleMs
|
|
517
|
+
});
|
|
518
|
+
currentRootNodeId = await freshTopRootNodeId(client, triggerRootNodeId);
|
|
519
|
+
optionsResult = trigger.options_result || await waitForChatJobOptions(client, currentRootNodeId, {
|
|
520
|
+
timeoutMs,
|
|
521
|
+
intervalMs,
|
|
522
|
+
requireVisible: true
|
|
523
|
+
});
|
|
524
|
+
matched = (optionsResult.job_options || []).find((option) => matchJobOption(option, requested)) || null;
|
|
525
|
+
if (!matched || !matched.visible) {
|
|
526
|
+
const menuClose = await closeChatJobDropdownQuietly(client, currentRootNodeId, Math.min(settleMs, 300));
|
|
527
|
+
return {
|
|
528
|
+
selected: false,
|
|
529
|
+
reason: matched ? "job_option_not_visible" : "job_option_not_found",
|
|
530
|
+
requested,
|
|
531
|
+
trigger,
|
|
532
|
+
options: optionsResult.job_options || [],
|
|
533
|
+
selected_label_before: optionsResult.selected_label || "",
|
|
534
|
+
menu_close: menuClose
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (matched.active || normalizeJobText(optionsResult.selected_label).toLowerCase() === normalizeJobText(matched.label).toLowerCase()) {
|
|
540
|
+
const menuClose = await closeChatJobDropdownQuietly(client, currentRootNodeId, Math.min(settleMs, 300));
|
|
541
|
+
return {
|
|
542
|
+
selected: true,
|
|
543
|
+
verified: true,
|
|
544
|
+
already_current: true,
|
|
545
|
+
requested,
|
|
546
|
+
selected_option: matched,
|
|
547
|
+
options: optionsResult.job_options || [],
|
|
548
|
+
selected_label: optionsResult.selected_label || matched.label,
|
|
549
|
+
menu_close: menuClose
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (matched.center) {
|
|
554
|
+
await clickPoint(client, matched.center.x, matched.center.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
555
|
+
} else {
|
|
556
|
+
await clickNodeCenter(client, matched.node_id, {
|
|
557
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
558
|
+
scrollIntoView: true
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
562
|
+
|
|
563
|
+
const afterRootNodeId = await freshTopRootNodeId(client, currentRootNodeId);
|
|
564
|
+
const verification = await waitForSelectedChatJob(client, afterRootNodeId, matched.label, {
|
|
565
|
+
timeoutMs: Math.min(timeoutMs, 5000),
|
|
566
|
+
intervalMs
|
|
567
|
+
});
|
|
568
|
+
const after = verification.result || {
|
|
569
|
+
selected_label: "",
|
|
570
|
+
job_options: []
|
|
571
|
+
};
|
|
572
|
+
const afterMatch = (after.job_options || []).find((option) => matchJobOption(option, matched.label)) || matched;
|
|
573
|
+
const selectedLabel = normalizeJobText(after.selected_label || "");
|
|
574
|
+
const activeMatch = activeMatchingJobOption(after.job_options || [], matched.label);
|
|
575
|
+
const verified = Boolean(verification.verified || selectedLabelMatches(selectedLabel, matched.label) || activeMatch);
|
|
576
|
+
const menuClose = await closeChatJobDropdownQuietly(client, afterRootNodeId, Math.min(settleMs, 300));
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
selected: verified,
|
|
580
|
+
verified,
|
|
581
|
+
already_current: false,
|
|
582
|
+
reason: verified ? "verified" : "job_selection_not_verified",
|
|
583
|
+
requested,
|
|
584
|
+
selected_option: afterMatch,
|
|
585
|
+
active_option: activeMatch,
|
|
586
|
+
options: after.job_options || optionsResult.job_options || [],
|
|
587
|
+
selected_label: selectedLabel,
|
|
588
|
+
before: optionsResult,
|
|
589
|
+
after,
|
|
590
|
+
menu_close: menuClose
|
|
591
|
+
};
|
|
592
|
+
}
|