@reconcrap/boss-recommend-mcp 2.0.53 → 2.0.55

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.
@@ -0,0 +1,314 @@
1
+ import {
2
+ clickPoint,
3
+ DETERMINISTIC_CLICK_OPTIONS,
4
+ getDocumentRoot,
5
+ getNodeBox,
6
+ pressKey,
7
+ querySelectorAll,
8
+ sleep
9
+ } from "../../core/browser/index.js";
10
+
11
+ export const BOSS_ACCOUNT_RIGHTS_PANEL_TEXT_QUERIES = Object.freeze([
12
+ "我的权益",
13
+ "VVIP账号-精选版专享权益",
14
+ "全部账号权益使用量",
15
+ "职位发布权益总量",
16
+ "每日使用权益总量"
17
+ ]);
18
+
19
+ export const BOSS_ACCOUNT_RIGHTS_PANEL_CLOSE_SELECTORS = Object.freeze([
20
+ ".boss-popup__close",
21
+ ".boss-dialog__close",
22
+ ".side-panel-close",
23
+ ".drawer-close",
24
+ ".panel-close",
25
+ ".popup-close",
26
+ ".modal-close",
27
+ ".dialog-close",
28
+ ".close-btn",
29
+ ".icon-close",
30
+ "[class*=\"close\"]",
31
+ '[aria-label*="关闭"]',
32
+ '[title*="关闭"]'
33
+ ]);
34
+
35
+ async function performDomTextSearch(client, query, {
36
+ limit = 6
37
+ } = {}) {
38
+ if (typeof client?.DOM?.performSearch !== "function"
39
+ || typeof client?.DOM?.getSearchResults !== "function") {
40
+ return [];
41
+ }
42
+
43
+ const searchOnce = async () => {
44
+ let searchId = "";
45
+ try {
46
+ const search = await client.DOM.performSearch({
47
+ query,
48
+ includeUserAgentShadowDOM: true
49
+ });
50
+ searchId = search?.searchId || "";
51
+ const resultCount = Math.min(Number(search?.resultCount) || 0, Math.max(0, Number(limit) || 0));
52
+ if (!searchId || resultCount <= 0) return [];
53
+ const results = await client.DOM.getSearchResults({
54
+ searchId,
55
+ fromIndex: 0,
56
+ toIndex: resultCount
57
+ });
58
+ return results?.nodeIds || [];
59
+ } catch {
60
+ return [];
61
+ } finally {
62
+ if (searchId && typeof client?.DOM?.discardSearchResults === "function") {
63
+ try {
64
+ await client.DOM.discardSearchResults({ searchId });
65
+ } catch {
66
+ // Best-effort cleanup only.
67
+ }
68
+ }
69
+ }
70
+ };
71
+
72
+ const firstPass = await searchOnce();
73
+ if (!firstPass.length) return [];
74
+ const firstPassNodeIds = firstPass.filter((nodeId) => Number(nodeId) > 0);
75
+ if (firstPassNodeIds.length || typeof client?.DOM?.getDocument !== "function") return firstPassNodeIds;
76
+
77
+ try {
78
+ await client.DOM.getDocument({
79
+ depth: -1,
80
+ pierce: true
81
+ });
82
+ } catch {
83
+ return firstPassNodeIds;
84
+ }
85
+ return (await searchOnce()).filter((nodeId) => Number(nodeId) > 0);
86
+ }
87
+
88
+ export async function findBossAccountRightsBlockingPanel(client, {
89
+ textQueries = BOSS_ACCOUNT_RIGHTS_PANEL_TEXT_QUERIES
90
+ } = {}) {
91
+ for (const query of textQueries || []) {
92
+ const nodeIds = await performDomTextSearch(client, query);
93
+ for (const nodeId of nodeIds) {
94
+ try {
95
+ const box = await getNodeBox(client, nodeId);
96
+ if (box?.rect?.width > 2 && box?.rect?.height > 2) {
97
+ return {
98
+ open: true,
99
+ reason: "account_rights_panel_text_visible",
100
+ query,
101
+ node_id: nodeId,
102
+ rect: box.rect,
103
+ center: box.center
104
+ };
105
+ }
106
+ } catch {
107
+ // Hidden or stale text hits are ignored.
108
+ }
109
+ }
110
+ }
111
+ return {
112
+ open: false
113
+ };
114
+ }
115
+
116
+ export function accountRightsPanelOutsideClickPoint(probe = {}) {
117
+ const rect = probe?.rect || {};
118
+ // Click the empty lower-left sidebar area. It is outside the rights drawer
119
+ // and avoids top nav, chat rows, message controls, and candidate actions.
120
+ const x = 84;
121
+ const y = Number.isFinite(Number(rect.y))
122
+ ? Math.max(560, Math.min(680, Number(rect.y) + 600))
123
+ : 660;
124
+ return {
125
+ x,
126
+ y,
127
+ mode: "empty-lower-left-sidebar"
128
+ };
129
+ }
130
+
131
+ async function resolveBlockingPanelRoots(client, {
132
+ roots = null,
133
+ rootState = null,
134
+ resolveRoots = null
135
+ } = {}) {
136
+ if (Array.isArray(roots) && roots.some((root) => root?.nodeId)) {
137
+ return roots.filter((root) => root?.nodeId);
138
+ }
139
+ if (Array.isArray(rootState?.roots) && rootState.roots.some((root) => root?.nodeId)) {
140
+ return rootState.roots.filter((root) => root?.nodeId);
141
+ }
142
+ if (typeof resolveRoots === "function") {
143
+ try {
144
+ const resolved = await resolveRoots(client);
145
+ if (Array.isArray(resolved)) return resolved.filter((root) => root?.nodeId);
146
+ if (Array.isArray(resolved?.roots)) return resolved.roots.filter((root) => root?.nodeId);
147
+ } catch {
148
+ // Fall through to the top document. The rights panel lives there.
149
+ }
150
+ }
151
+ try {
152
+ const topRoot = await getDocumentRoot(client);
153
+ return [{ name: "top", nodeId: topRoot.nodeId }];
154
+ } catch {
155
+ return [];
156
+ }
157
+ }
158
+
159
+ async function findVisibleCloseTarget(client, roots, selectors) {
160
+ let fallback = null;
161
+ for (const root of roots || []) {
162
+ if (!root?.nodeId) continue;
163
+ for (const selector of selectors || []) {
164
+ let nodeIds = [];
165
+ try {
166
+ nodeIds = await querySelectorAll(client, root.nodeId, selector);
167
+ } catch {
168
+ nodeIds = [];
169
+ }
170
+ for (const nodeId of nodeIds) {
171
+ const target = {
172
+ root: root.name,
173
+ root_node_id: root.nodeId,
174
+ selector,
175
+ node_id: nodeId
176
+ };
177
+ if (!fallback) fallback = target;
178
+ try {
179
+ const box = await getNodeBox(client, nodeId);
180
+ if (box?.rect?.width > 2 && box?.rect?.height > 2) {
181
+ return {
182
+ ...target,
183
+ center: box.center,
184
+ rect: box.rect
185
+ };
186
+ }
187
+ } catch {
188
+ // Stale close candidates are ignored.
189
+ }
190
+ }
191
+ }
192
+ }
193
+ return fallback;
194
+ }
195
+
196
+ async function pressEscape(client) {
197
+ await pressKey(client, "Escape", {
198
+ code: "Escape",
199
+ windowsVirtualKeyCode: 27,
200
+ nativeVirtualKeyCode: 27
201
+ });
202
+ }
203
+
204
+ export async function closeBossAccountRightsBlockingPanel(client, {
205
+ attemptsLimit = 2,
206
+ closeSelectors = BOSS_ACCOUNT_RIGHTS_PANEL_CLOSE_SELECTORS,
207
+ resolveRoots = null,
208
+ roots = null,
209
+ rootState = null,
210
+ textQueries = BOSS_ACCOUNT_RIGHTS_PANEL_TEXT_QUERIES,
211
+ waitMs = 700
212
+ } = {}) {
213
+ const attempts = [];
214
+ let probe = await findBossAccountRightsBlockingPanel(client, { textQueries });
215
+ if (!probe.open) {
216
+ return {
217
+ closed: true,
218
+ already_closed: true,
219
+ probe,
220
+ attempts
221
+ };
222
+ }
223
+
224
+ for (let index = 0; index < attemptsLimit; index += 1) {
225
+ const outsidePoint = accountRightsPanelOutsideClickPoint(probe);
226
+ try {
227
+ await clickPoint(client, outsidePoint.x, outsidePoint.y, DETERMINISTIC_CLICK_OPTIONS);
228
+ attempts.push({
229
+ mode: "outside-click",
230
+ point: outsidePoint
231
+ });
232
+ } catch (error) {
233
+ attempts.push({
234
+ mode: "outside-click-error",
235
+ point: outsidePoint,
236
+ error: error?.message || String(error)
237
+ });
238
+ }
239
+ await sleep(waitMs);
240
+
241
+ probe = await findBossAccountRightsBlockingPanel(client, { textQueries });
242
+ if (!probe.open) {
243
+ return {
244
+ closed: true,
245
+ already_closed: false,
246
+ probe,
247
+ attempts
248
+ };
249
+ }
250
+
251
+ const resolvedRoots = await resolveBlockingPanelRoots(client, { roots, rootState, resolveRoots });
252
+ const closeTarget = await findVisibleCloseTarget(client, resolvedRoots, closeSelectors);
253
+ if (closeTarget) {
254
+ try {
255
+ if (closeTarget.center) {
256
+ await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
257
+ }
258
+ attempts.push({
259
+ mode: "close-selector",
260
+ selector: closeTarget.selector,
261
+ root: closeTarget.root
262
+ });
263
+ } catch (error) {
264
+ attempts.push({
265
+ mode: "close-selector-error",
266
+ selector: closeTarget.selector,
267
+ root: closeTarget.root,
268
+ error: error?.message || String(error)
269
+ });
270
+ }
271
+ await sleep(waitMs);
272
+
273
+ probe = await findBossAccountRightsBlockingPanel(client, { textQueries });
274
+ if (!probe.open) {
275
+ return {
276
+ closed: true,
277
+ already_closed: false,
278
+ probe,
279
+ attempts
280
+ };
281
+ }
282
+ }
283
+
284
+ try {
285
+ await pressEscape(client);
286
+ attempts.push({ mode: closeTarget ? "Escape-fallback" : "Escape" });
287
+ } catch (error) {
288
+ attempts.push({
289
+ mode: "Escape-error",
290
+ error: error?.message || String(error)
291
+ });
292
+ }
293
+ await sleep(waitMs);
294
+
295
+ probe = await findBossAccountRightsBlockingPanel(client, { textQueries });
296
+ if (!probe.open) {
297
+ return {
298
+ closed: true,
299
+ already_closed: false,
300
+ probe,
301
+ attempts
302
+ };
303
+ }
304
+ }
305
+
306
+ return {
307
+ closed: false,
308
+ already_closed: false,
309
+ reason: "account_rights_panel_still_visible_after_close_attempts",
310
+ probe,
311
+ attempts
312
+ };
313
+ }
314
+