@reconcrap/boss-recommend-mcp 2.0.54 → 2.0.56

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "2.0.54",
3
+ "version": "2.0.56",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -1,4 +1,9 @@
1
- export const CHAT_TARGET_URL = "https://www.zhipin.com/web/chat/index";
1
+ import {
2
+ BOSS_ACCOUNT_RIGHTS_PANEL_CLOSE_SELECTORS,
3
+ BOSS_ACCOUNT_RIGHTS_PANEL_TEXT_QUERIES
4
+ } from "../common/account-rights-panel.js";
5
+
6
+ export const CHAT_TARGET_URL = "https://www.zhipin.com/web/chat/index";
2
7
 
3
8
  export const CHAT_CARD_SELECTORS = Object.freeze([
4
9
  ".geek-item[data-id]",
@@ -208,29 +213,9 @@ export const CHAT_RESUME_CLOSE_SELECTORS = Object.freeze([
208
213
  '[title*="关闭"]'
209
214
  ]);
210
215
 
211
- export const CHAT_BLOCKING_PANEL_TEXT_QUERIES = Object.freeze([
212
- "我的权益",
213
- "VVIP账号-精选版专享权益",
214
- "全部账号权益使用量",
215
- "职位发布权益总量",
216
- "每日使用权益总量"
217
- ]);
218
-
219
- export const CHAT_BLOCKING_PANEL_CLOSE_SELECTORS = Object.freeze([
220
- ".boss-popup__close",
221
- ".boss-dialog__close",
222
- ".side-panel-close",
223
- ".drawer-close",
224
- ".panel-close",
225
- ".popup-close",
226
- ".modal-close",
227
- ".dialog-close",
228
- ".close-btn",
229
- ".icon-close",
230
- "[class*=\"close\"]",
231
- '[aria-label*="关闭"]',
232
- '[title*="关闭"]'
233
- ]);
216
+ export const CHAT_BLOCKING_PANEL_TEXT_QUERIES = BOSS_ACCOUNT_RIGHTS_PANEL_TEXT_QUERIES;
217
+
218
+ export const CHAT_BLOCKING_PANEL_CLOSE_SELECTORS = BOSS_ACCOUNT_RIGHTS_PANEL_CLOSE_SELECTORS;
234
219
 
235
220
  export const CHAT_PROFILE_NETWORK_PATTERNS = Object.freeze([
236
221
  /\/wapi\/zpjob\/view\/geek\/info(?:\/v2)?\b/i,
@@ -12,13 +12,17 @@ import {
12
12
  querySelectorAll,
13
13
  sleep
14
14
  } from "../../core/browser/index.js";
15
- import {
16
- buildScreeningCandidateFromDetail,
17
- htmlToText
18
- } from "../../core/screening/index.js";
19
- import {
20
- CHAT_ACTIVE_CANDIDATE_SELECTORS,
21
- CHAT_ASK_RESUME_BUTTON_SELECTORS,
15
+ import {
16
+ buildScreeningCandidateFromDetail,
17
+ htmlToText
18
+ } from "../../core/screening/index.js";
19
+ import {
20
+ closeBossAccountRightsBlockingPanel,
21
+ findBossAccountRightsBlockingPanel
22
+ } from "../common/account-rights-panel.js";
23
+ import {
24
+ CHAT_ACTIVE_CANDIDATE_SELECTORS,
25
+ CHAT_ASK_RESUME_BUTTON_SELECTORS,
22
26
  CHAT_ATTACHMENT_RESUME_BUTTON_SELECTORS,
23
27
  CHAT_BLOCKING_PANEL_CLOSE_SELECTORS,
24
28
  CHAT_BLOCKING_PANEL_TEXT_QUERIES,
@@ -735,185 +739,29 @@ export async function quickChatResumeModalOpenProbe(client, {
735
739
  };
736
740
  }
737
741
 
738
- async function performDomTextSearch(client, query, {
739
- limit = 6
740
- } = {}) {
741
- if (typeof client?.DOM?.performSearch !== "function"
742
- || typeof client?.DOM?.getSearchResults !== "function") {
743
- return [];
744
- }
745
- const searchOnce = async () => {
746
- let searchId = "";
747
- try {
748
- const search = await client.DOM.performSearch({
749
- query,
750
- includeUserAgentShadowDOM: true
751
- });
752
- searchId = search?.searchId || "";
753
- const resultCount = Math.min(Number(search?.resultCount) || 0, Math.max(0, Number(limit) || 0));
754
- if (!searchId || resultCount <= 0) return [];
755
- const results = await client.DOM.getSearchResults({
756
- searchId,
757
- fromIndex: 0,
758
- toIndex: resultCount
759
- });
760
- return results?.nodeIds || [];
761
- } catch {
762
- return [];
763
- } finally {
764
- if (searchId && typeof client?.DOM?.discardSearchResults === "function") {
765
- try {
766
- await client.DOM.discardSearchResults({ searchId });
767
- } catch {
768
- // Best-effort cleanup only.
769
- }
770
- }
771
- }
772
- };
773
-
774
- const firstPass = await searchOnce();
775
- if (!firstPass.length) return [];
776
- const firstPassNodeIds = firstPass.filter((nodeId) => Number(nodeId) > 0);
777
- if (firstPassNodeIds.length || typeof client?.DOM?.getDocument !== "function") return firstPassNodeIds;
778
- try {
779
- await client.DOM.getDocument({
780
- depth: -1,
781
- pierce: true
782
- });
783
- } catch {
784
- return firstPassNodeIds;
785
- }
786
- return (await searchOnce()).filter((nodeId) => Number(nodeId) > 0);
787
- }
788
-
789
- export async function findChatBlockingPanel(client, {
790
- textQueries = CHAT_BLOCKING_PANEL_TEXT_QUERIES
791
- } = {}) {
792
- for (const query of textQueries || []) {
793
- const nodeIds = await performDomTextSearch(client, query);
794
- for (const nodeId of nodeIds) {
795
- try {
796
- const box = await getNodeBox(client, nodeId);
797
- if (box?.rect?.width > 2 && box?.rect?.height > 2) {
798
- return {
799
- open: true,
800
- reason: "blocking_panel_text_visible",
801
- query,
802
- node_id: nodeId,
803
- rect: box.rect,
804
- center: box.center
805
- };
806
- }
807
- } catch {
808
- // Hidden or stale text hits are ignored.
809
- }
810
- }
811
- }
812
- return {
813
- open: false
814
- };
815
- }
816
-
817
- function blockingPanelOutsideClickPoint(probe = {}) {
818
- const rect = probe?.rect || {};
819
- // Click the empty lower-left sidebar area. It is outside the rights drawer
820
- // and avoids top nav, chat rows, message controls, and candidate actions.
821
- const x = 84;
822
- const y = Number.isFinite(Number(rect.y))
823
- ? Math.max(560, Math.min(680, Number(rect.y) + 600))
824
- : 660;
825
- return {
826
- x,
827
- y,
828
- mode: "empty-lower-left-sidebar"
829
- };
830
- }
831
-
832
- export async function closeChatBlockingPanels(client, {
833
- attemptsLimit = 2
834
- } = {}) {
835
- const attempts = [];
836
- let probe = await findChatBlockingPanel(client);
837
- if (!probe.open) {
838
- return {
839
- closed: true,
840
- already_closed: true,
841
- probe,
842
- attempts
843
- };
844
- }
845
-
846
- for (let index = 0; index < attemptsLimit; index += 1) {
847
- const outsidePoint = blockingPanelOutsideClickPoint(probe);
848
- try {
849
- await clickPoint(client, outsidePoint.x, outsidePoint.y, DETERMINISTIC_CLICK_OPTIONS);
850
- attempts.push({
851
- mode: "outside-click",
852
- point: outsidePoint
853
- });
854
- } catch (error) {
855
- attempts.push({
856
- mode: "outside-click-error",
857
- point: outsidePoint,
858
- error: error?.message || String(error)
859
- });
860
- }
861
- await sleep(700);
862
-
863
- probe = await findChatBlockingPanel(client);
864
- if (!probe.open) {
865
- return {
866
- closed: true,
867
- already_closed: false,
868
- probe,
869
- attempts
870
- };
871
- }
872
-
873
- const rootState = await getChatRoots(client);
874
- const closeTarget = await findVisibleTarget(client, rootState.roots, CHAT_BLOCKING_PANEL_CLOSE_SELECTORS);
875
- if (closeTarget) {
876
- try {
877
- await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
878
- attempts.push({
879
- mode: "close-selector",
880
- selector: closeTarget.selector,
881
- root: closeTarget.root
882
- });
883
- } catch (error) {
884
- attempts.push({
885
- mode: "close-selector-error",
886
- selector: closeTarget.selector,
887
- root: closeTarget.root,
888
- error: error?.message || String(error)
889
- });
890
- }
891
- } else {
892
- await pressEscape(client);
893
- attempts.push({ mode: "Escape" });
894
- }
895
- await sleep(700);
896
-
897
- probe = await findChatBlockingPanel(client);
898
- if (!probe.open) {
899
- return {
900
- closed: true,
901
- already_closed: false,
902
- probe,
903
- attempts
904
- };
905
- }
906
- }
907
-
908
- return {
909
- closed: false,
910
- already_closed: false,
911
- probe,
912
- attempts
913
- };
914
- }
915
-
916
- export async function readChatResumeHtml(client, resumeState) {
742
+ export async function findChatBlockingPanel(client, {
743
+ textQueries = CHAT_BLOCKING_PANEL_TEXT_QUERIES
744
+ } = {}) {
745
+ const panel = await findBossAccountRightsBlockingPanel(client, { textQueries });
746
+ return panel.open
747
+ ? { ...panel, reason: "blocking_panel_text_visible" }
748
+ : panel;
749
+ }
750
+
751
+ export async function closeChatBlockingPanels(client, {
752
+ attemptsLimit = 2,
753
+ closeSelectors = CHAT_BLOCKING_PANEL_CLOSE_SELECTORS,
754
+ textQueries = CHAT_BLOCKING_PANEL_TEXT_QUERIES
755
+ } = {}) {
756
+ return closeBossAccountRightsBlockingPanel(client, {
757
+ attemptsLimit,
758
+ closeSelectors,
759
+ resolveRoots: getChatRoots,
760
+ textQueries
761
+ });
762
+ }
763
+
764
+ export async function readChatResumeHtml(client, resumeState) {
917
765
  let popupHTML = "";
918
766
  let contentHTML = "";
919
767
  let resumeIframeHTML = "";
@@ -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
+
@@ -108,6 +108,23 @@ export const DETAIL_RESUME_IFRAME_SELECTORS = Object.freeze([
108
108
  'iframe[name*="resume"]'
109
109
  ]);
110
110
 
111
+ export const RECOMMEND_AVATAR_PREVIEW_SELECTORS = Object.freeze([
112
+ ".boss-dialog__wrapper.avatar-preview",
113
+ ".avatar-preview",
114
+ ".dialog-wrap.active .avatar-preview",
115
+ ".figure-preview"
116
+ ]);
117
+
118
+ export const RECOMMEND_AVATAR_PREVIEW_CLOSE_SELECTORS = Object.freeze([
119
+ ".avatar-preview .boss-popup__close",
120
+ ".dialog-wrap.active .avatar-preview .boss-popup__close",
121
+ ".dialog-wrap.active .boss-popup__close",
122
+ ".boss-dialog__wrapper.avatar-preview .boss-popup__close",
123
+ ".boss-popup__close",
124
+ ".icon-close",
125
+ '[class*="close"]'
126
+ ]);
127
+
111
128
  export const DETAIL_CLOSE_SELECTORS = Object.freeze([
112
129
  ".boss-popup__close",
113
130
  ".popup-close",