@reconcrap/boss-recommend-mcp 2.0.46 → 2.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/bin/boss-recommend-mcp.js +4 -4
  2. package/config/screening-config.example.json +27 -27
  3. package/package.json +1 -1
  4. package/scripts/postinstall.cjs +44 -44
  5. package/skills/boss-chat/README.md +39 -39
  6. package/skills/boss-chat/SKILL.md +93 -93
  7. package/skills/boss-recommend-pipeline/README.md +12 -12
  8. package/skills/boss-recommend-pipeline/SKILL.md +180 -180
  9. package/skills/boss-recruit-pipeline/README.md +17 -17
  10. package/skills/boss-recruit-pipeline/SKILL.md +58 -58
  11. package/src/chat-mcp.js +1780 -1780
  12. package/src/chat-runtime-config.js +749 -749
  13. package/src/cli.js +3054 -3054
  14. package/src/core/boss-cards/index.js +199 -199
  15. package/src/core/browser/index.js +1453 -1453
  16. package/src/core/capture/index.js +1201 -1201
  17. package/src/core/cv-acquisition/index.js +238 -238
  18. package/src/core/cv-capture-target/index.js +299 -299
  19. package/src/core/greet-quota/index.js +54 -54
  20. package/src/core/infinite-list/index.js +1326 -1326
  21. package/src/core/reporting/legacy-csv.js +341 -341
  22. package/src/core/run/timing.js +33 -33
  23. package/src/core/screening/index.js +50 -3
  24. package/src/core/self-heal/index.js +973 -973
  25. package/src/core/self-heal/viewport.js +564 -564
  26. package/src/domains/chat/cards.js +137 -137
  27. package/src/domains/chat/constants.js +221 -221
  28. package/src/domains/chat/detail.js +1668 -1668
  29. package/src/domains/chat/index.js +7 -7
  30. package/src/domains/chat/jobs.js +592 -592
  31. package/src/domains/chat/page-guard.js +98 -98
  32. package/src/domains/chat/roots.js +56 -56
  33. package/src/domains/chat/run-service.js +1977 -1977
  34. package/src/domains/recommend/actions.js +457 -457
  35. package/src/domains/recommend/cards.js +243 -243
  36. package/src/domains/recommend/constants.js +165 -165
  37. package/src/domains/recommend/detail.js +25 -18
  38. package/src/domains/recommend/filters.js +610 -610
  39. package/src/domains/recommend/index.js +10 -10
  40. package/src/domains/recommend/jobs.js +316 -316
  41. package/src/domains/recommend/refresh.js +472 -472
  42. package/src/domains/recommend/roots.js +80 -80
  43. package/src/domains/recommend/run-service.js +27 -20
  44. package/src/domains/recommend/scopes.js +246 -246
  45. package/src/domains/recruit/actions.js +277 -277
  46. package/src/domains/recruit/cards.js +74 -74
  47. package/src/domains/recruit/constants.js +167 -167
  48. package/src/domains/recruit/detail.js +461 -461
  49. package/src/domains/recruit/index.js +9 -9
  50. package/src/domains/recruit/instruction-parser.js +451 -451
  51. package/src/domains/recruit/refresh.js +44 -44
  52. package/src/domains/recruit/roots.js +68 -68
  53. package/src/domains/recruit/run-service.js +1207 -1207
  54. package/src/domains/recruit/search.js +1202 -1202
  55. package/src/recommend-mcp.js +22 -22
  56. package/src/recruit-mcp.js +1338 -1338
@@ -1,457 +1,457 @@
1
- import {
2
- clickPoint,
3
- getFrameDocumentNodeId,
4
- getAttributesMap,
5
- getNodeBox,
6
- getOuterHTML,
7
- querySelectorAll,
8
- sleep
9
- } from "../../core/browser/index.js";
10
- import {
11
- htmlToText,
12
- normalizeText
13
- } from "../../core/screening/index.js";
14
- import {
15
- assertGreetQuotaAvailable,
16
- parseGreetQuota
17
- } from "../../core/greet-quota/index.js";
18
- import {
19
- FAVORITE_BUTTON_SELECTORS,
20
- GREET_BUTTON_RECOMMEND_SELECTORS
21
- } from "./constants.js";
22
- import { waitForRecommendDetail } from "./detail.js";
23
- import { getRecommendRoots } from "./roots.js";
24
-
25
- const POST_ACTIONS = new Set(["none", "favorite", "greet"]);
26
- const GREET_EXACT_LABEL_PATTERN = /^(?:打招呼|聊一聊|立即沟通(?:[\((]\d+\s*[//]\s*\d+[\))])?|沟通)$/i;
27
- export const RECOMMEND_DETAIL_ACTION_TEXT_SELECTORS = Object.freeze([
28
- "button",
29
- ".btn",
30
- '[role="button"]',
31
- "a",
32
- "span",
33
- "div"
34
- ]);
35
-
36
- function uniqueSelectors(...selectorGroups) {
37
- return [...new Set(selectorGroups.flat().filter(Boolean))];
38
- }
39
-
40
- function uniqueByNode(candidates = []) {
41
- const seen = new Set();
42
- const result = [];
43
- for (const item of candidates) {
44
- const key = `${item.kind}:${item.root}:${item.node_id}`;
45
- if (seen.has(key)) continue;
46
- seen.add(key);
47
- result.push(item);
48
- }
49
- return result;
50
- }
51
-
52
- function lowerText(...parts) {
53
- return normalizeText(parts.filter(Boolean).join(" ")).toLowerCase();
54
- }
55
-
56
- function hasActiveClass(text) {
57
- return /(?:^|\s)(?:active|curr|current|selected|checked)(?:\s|$)/i.test(text);
58
- }
59
-
60
- function hasDisabledSignal(text) {
61
- return /(?:^|\s)(?:disabled|disable|forbidden|is-disabled)(?:\s|$)/i.test(text);
62
- }
63
-
64
- function rectArea(control) {
65
- const rect = control?.rect || {};
66
- return Math.max(0, Number(rect.width) || 0) * Math.max(0, Number(rect.height) || 0);
67
- }
68
-
69
- function isCompactLabel(control, limit = 80) {
70
- const label = normalizeText(control?.label || "");
71
- return label.length > 0 && label.length <= limit;
72
- }
73
-
74
- function controlRank(control, exactLabelPattern) {
75
- const label = normalizeText(control?.label || "");
76
- const selector = String(control?.selector || "");
77
- const className = String(control?.class_name || "");
78
- let score = 0;
79
- if (exactLabelPattern.test(label)) score -= 1000;
80
- if (/button|\[role=|\.btn/.test(selector) || /btn|button/i.test(className)) score -= 250;
81
- if (selector === "div") score += 300;
82
- if (!isCompactLabel(control)) score += 500;
83
- score += Math.min(rectArea(control), 100000) / 1000;
84
- score += label.length / 10;
85
- return score;
86
- }
87
-
88
- function bestControl(controls, exactLabelPattern) {
89
- return [...controls].sort((left, right) => (
90
- controlRank(left, exactLabelPattern) - controlRank(right, exactLabelPattern)
91
- ))[0] || null;
92
- }
93
-
94
- export function normalizeRecommendPostAction(value) {
95
- const normalized = normalizeText(value).toLowerCase();
96
- if (["", "none", "skip", "no", "不执行", "无"].includes(normalized)) return "none";
97
- if (["favorite", "fav", "collect", "收藏", "感兴趣"].includes(normalized)) return "favorite";
98
- if (["greet", "chat", "打招呼", "直接沟通", "沟通"].includes(normalized)) return "greet";
99
- return POST_ACTIONS.has(normalized) ? normalized : "";
100
- }
101
-
102
- export function resolveRecommendPostAction({
103
- postAction = "none",
104
- greetCount = 0,
105
- maxGreetCount = null
106
- } = {}) {
107
- const requested = normalizeRecommendPostAction(postAction) || "none";
108
- const currentGreetCount = Number.isInteger(greetCount) && greetCount >= 0 ? greetCount : 0;
109
- const limit = Number.isInteger(maxGreetCount) && maxGreetCount > 0 ? maxGreetCount : null;
110
- if (requested === "greet" && limit !== null && currentGreetCount >= limit) {
111
- return {
112
- requested,
113
- effective: "favorite",
114
- reason: "greet_limit_reached",
115
- greet_count: currentGreetCount,
116
- max_greet_count: limit
117
- };
118
- }
119
- return {
120
- requested,
121
- effective: requested,
122
- reason: "requested_action",
123
- greet_count: currentGreetCount,
124
- max_greet_count: limit
125
- };
126
- }
127
-
128
- export function classifyFavoriteControl({
129
- outerHTML = "",
130
- attributes = {}
131
- } = {}) {
132
- const label = htmlToText(outerHTML);
133
- const labelText = normalizeText(label);
134
- const className = normalizeText(attributes.class || "");
135
- const title = normalizeText(attributes.title || attributes["aria-label"] || "");
136
- const combined = lowerText(className, title);
137
- const labelMatches = /^(?:收藏|已收藏|感兴趣|已感兴趣)$/.test(labelText);
138
- const classMatches = /favorite|collect|interest|like/.test(combined);
139
- const matches = labelMatches || classMatches;
140
- const active = (
141
- /已收藏|已感兴趣/.test(label)
142
- || /like-icon-active|favorite-active|collect-active/i.test(outerHTML)
143
- || hasActiveClass(className)
144
- );
145
- const disabled = (
146
- Object.prototype.hasOwnProperty.call(attributes, "disabled")
147
- || hasDisabledSignal(className)
148
- || /disabled/i.test(outerHTML)
149
- );
150
- return {
151
- kind: "favorite",
152
- matches,
153
- active,
154
- disabled,
155
- label: label || title || null,
156
- class_name: className || null
157
- };
158
- }
159
-
160
- export function classifyGreetControl({
161
- outerHTML = "",
162
- attributes = {}
163
- } = {}) {
164
- const label = htmlToText(outerHTML);
165
- const labelText = normalizeText(label);
166
- const className = normalizeText(attributes.class || "");
167
- const title = normalizeText(attributes.title || attributes["aria-label"] || "");
168
- const combined = lowerText(className, title);
169
- const continueChat = labelText.length <= 40 && /继续沟通/.test(labelText);
170
- const greetQuota = parseGreetQuota(labelText || title);
171
- const greetEntry = (
172
- GREET_EXACT_LABEL_PATTERN.test(labelText)
173
- || greetQuota.found
174
- || /greet/i.test(combined)
175
- );
176
- const disabled = (
177
- Object.prototype.hasOwnProperty.call(attributes, "disabled")
178
- || hasDisabledSignal(className)
179
- || /disabled/i.test(outerHTML)
180
- );
181
- return {
182
- kind: "greet",
183
- matches: greetEntry || continueChat,
184
- available: greetEntry && !continueChat && !disabled,
185
- continue_chat: continueChat,
186
- disabled,
187
- label: label || title || null,
188
- greet_quota: greetQuota.found ? greetQuota : null,
189
- class_name: className || null
190
- };
191
- }
192
-
193
- async function readActionNode(client, {
194
- root,
195
- selector,
196
- nodeId,
197
- kind
198
- }) {
199
- const [attributes, outerHTML] = await Promise.all([
200
- getAttributesMap(client, nodeId),
201
- getOuterHTML(client, nodeId)
202
- ]);
203
- let box = null;
204
- let visible = false;
205
- try {
206
- box = await getNodeBox(client, nodeId);
207
- visible = box.rect.width > 2 && box.rect.height > 2;
208
- } catch {}
209
- const classification = kind === "favorite"
210
- ? classifyFavoriteControl({ outerHTML, attributes })
211
- : classifyGreetControl({ outerHTML, attributes });
212
- return {
213
- kind,
214
- root: root.name,
215
- root_node_id: root.nodeId,
216
- selector,
217
- node_id: nodeId,
218
- visible,
219
- center: box?.center || null,
220
- rect: box?.rect || null,
221
- attributes,
222
- outer_html_length: outerHTML.length,
223
- html_preview: outerHTML.slice(0, 500),
224
- ...classification
225
- };
226
- }
227
-
228
- export async function collectRecommendActionControls(client, roots, {
229
- favoriteSelectors = FAVORITE_BUTTON_SELECTORS,
230
- greetSelectors = GREET_BUTTON_RECOMMEND_SELECTORS,
231
- detailTextFallback = false
232
- } = {}) {
233
- const candidates = [];
234
- const favoriteScanSelectors = detailTextFallback
235
- ? uniqueSelectors(favoriteSelectors, RECOMMEND_DETAIL_ACTION_TEXT_SELECTORS)
236
- : favoriteSelectors;
237
- const greetScanSelectors = detailTextFallback
238
- ? uniqueSelectors(greetSelectors, RECOMMEND_DETAIL_ACTION_TEXT_SELECTORS)
239
- : greetSelectors;
240
- for (const root of roots) {
241
- if (!root?.nodeId) continue;
242
- for (const [kind, selectors] of [
243
- ["favorite", favoriteScanSelectors],
244
- ["greet", greetScanSelectors]
245
- ]) {
246
- for (const selector of selectors) {
247
- const nodeIds = await querySelectorAll(client, root.nodeId, selector);
248
- for (const nodeId of nodeIds) {
249
- candidates.push(await readActionNode(client, {
250
- root,
251
- selector,
252
- nodeId,
253
- kind
254
- }));
255
- }
256
- }
257
- }
258
- }
259
- return uniqueByNode(candidates);
260
- }
261
-
262
- export function summarizeRecommendActionControls(controls = []) {
263
- const visibleControls = controls.filter((item) => item.visible && item.matches);
264
- const favoriteControls = visibleControls.filter((item) => item.kind === "favorite");
265
- const greetControls = visibleControls.filter((item) => item.kind === "greet");
266
- const favorite = bestControl(
267
- favoriteControls.filter((item) => item.matches),
268
- /^(?:收藏|已收藏|感兴趣|已感兴趣)$/i
269
- );
270
- const greet = bestControl(
271
- greetControls.filter((item) => item.available),
272
- GREET_EXACT_LABEL_PATTERN
273
- ) || bestControl(
274
- greetControls.filter((item) => item.continue_chat),
275
- /^继续沟通$/i
276
- );
277
- return {
278
- favorite: favorite
279
- ? {
280
- found: true,
281
- active: favorite.active,
282
- disabled: favorite.disabled,
283
- label: favorite.label,
284
- selector: favorite.selector,
285
- root: favorite.root,
286
- node_id: favorite.node_id,
287
- center: favorite.center
288
- }
289
- : { found: false },
290
- greet: greet
291
- ? {
292
- found: true,
293
- available: greet.available,
294
- continue_chat: greet.continue_chat,
295
- disabled: greet.disabled,
296
- label: greet.label,
297
- greet_quota: greet.greet_quota || null,
298
- selector: greet.selector,
299
- root: greet.root,
300
- node_id: greet.node_id,
301
- center: greet.center
302
- }
303
- : { found: false },
304
- counts: {
305
- total: controls.length,
306
- visible_matching: visibleControls.length,
307
- favorite: favoriteControls.length,
308
- greet: greetControls.length
309
- }
310
- };
311
- }
312
-
313
- export async function discoverRecommendActionControls(client, {
314
- roots = null,
315
- selectors = {},
316
- detailTextFallback = false
317
- } = {}) {
318
- const rootState = roots ? { roots } : await getRecommendRoots(client);
319
- const controls = await collectRecommendActionControls(client, rootState.roots, {
320
- ...selectors,
321
- detailTextFallback
322
- });
323
- return {
324
- controls,
325
- summary: summarizeRecommendActionControls(controls)
326
- };
327
- }
328
-
329
- export async function waitForRecommendActionControls(client, {
330
- timeoutMs = 6000,
331
- intervalMs = 250,
332
- requireAny = true,
333
- ...discoveryOptions
334
- } = {}) {
335
- const started = Date.now();
336
- let lastDiscovery = null;
337
- while (Date.now() - started <= timeoutMs) {
338
- lastDiscovery = await discoverRecommendActionControls(client, discoveryOptions);
339
- const hasControl = Boolean(
340
- lastDiscovery.summary.favorite.found
341
- || lastDiscovery.summary.greet.found
342
- );
343
- if (!requireAny || hasControl) {
344
- return {
345
- ...lastDiscovery,
346
- elapsed_ms: Date.now() - started,
347
- timed_out: false
348
- };
349
- }
350
- await sleep(intervalMs);
351
- }
352
- return {
353
- ...(lastDiscovery || { controls: [], summary: summarizeRecommendActionControls([]) }),
354
- elapsed_ms: Date.now() - started,
355
- timed_out: true
356
- };
357
- }
358
-
359
- export async function getRecommendDetailActionRoots(client, detailState) {
360
- const roots = [];
361
- if (detailState?.popup?.node_id) {
362
- roots.push({
363
- name: `${detailState.popup.root || "unknown"}:detail-popup`,
364
- nodeId: detailState.popup.node_id
365
- });
366
- }
367
- if (detailState?.resumeIframe?.node_id) {
368
- try {
369
- roots.push({
370
- name: `${detailState.resumeIframe.root || "unknown"}:resume-iframe-document`,
371
- nodeId: await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id)
372
- });
373
- } catch {
374
- roots.push({
375
- name: `${detailState.resumeIframe.root || "unknown"}:resume-iframe-node`,
376
- nodeId: detailState.resumeIframe.node_id
377
- });
378
- }
379
- }
380
- return roots;
381
- }
382
-
383
- export async function waitForRecommendDetailActionControls(client, {
384
- timeoutMs = 8000,
385
- intervalMs = 350,
386
- selectors = {},
387
- requireAny = true
388
- } = {}) {
389
- const started = Date.now();
390
- let lastDiscovery = null;
391
- let lastError = null;
392
- let lastRootCount = 0;
393
- while (Date.now() - started <= timeoutMs) {
394
- const detailState = await waitForRecommendDetail(client, {
395
- timeoutMs: Math.min(intervalMs, 500),
396
- intervalMs: 100
397
- });
398
- const roots = await getRecommendDetailActionRoots(client, detailState);
399
- lastRootCount = roots.length;
400
- if (roots.length) {
401
- try {
402
- lastDiscovery = await discoverRecommendActionControls(client, {
403
- roots,
404
- selectors,
405
- detailTextFallback: true
406
- });
407
- const hasControl = Boolean(
408
- lastDiscovery.summary.favorite.found
409
- || lastDiscovery.summary.greet.found
410
- );
411
- if (!requireAny || hasControl) {
412
- return {
413
- ...lastDiscovery,
414
- elapsed_ms: Date.now() - started,
415
- timed_out: false,
416
- detail_root_count: roots.length
417
- };
418
- }
419
- } catch (error) {
420
- lastError = error?.message || String(error);
421
- }
422
- }
423
- await sleep(intervalMs);
424
- }
425
- return {
426
- ...(lastDiscovery || { controls: [], summary: summarizeRecommendActionControls([]) }),
427
- elapsed_ms: Date.now() - started,
428
- timed_out: true,
429
- detail_root_count: lastRootCount,
430
- last_error: lastError
431
- };
432
- }
433
-
434
- export async function clickRecommendActionControl(client, control, {
435
- allowDisabled = false
436
- } = {}) {
437
- if (!control?.center) {
438
- throw new Error("Action control has no clickable center");
439
- }
440
- const greetQuota = control.kind === "greet"
441
- ? assertGreetQuotaAvailable(control.greet_quota || control.label || "")
442
- : null;
443
- if (control.disabled && !allowDisabled) {
444
- throw new Error(`Action control is disabled: ${control.kind}`);
445
- }
446
- await clickPoint(client, control.center.x, control.center.y);
447
- return {
448
- clicked: true,
449
- kind: control.kind,
450
- label: control.label,
451
- greet_quota: greetQuota?.found ? greetQuota : null,
452
- selector: control.selector,
453
- root: control.root,
454
- node_id: control.node_id,
455
- center: control.center
456
- };
457
- }
1
+ import {
2
+ clickPoint,
3
+ getFrameDocumentNodeId,
4
+ getAttributesMap,
5
+ getNodeBox,
6
+ getOuterHTML,
7
+ querySelectorAll,
8
+ sleep
9
+ } from "../../core/browser/index.js";
10
+ import {
11
+ htmlToText,
12
+ normalizeText
13
+ } from "../../core/screening/index.js";
14
+ import {
15
+ assertGreetQuotaAvailable,
16
+ parseGreetQuota
17
+ } from "../../core/greet-quota/index.js";
18
+ import {
19
+ FAVORITE_BUTTON_SELECTORS,
20
+ GREET_BUTTON_RECOMMEND_SELECTORS
21
+ } from "./constants.js";
22
+ import { waitForRecommendDetail } from "./detail.js";
23
+ import { getRecommendRoots } from "./roots.js";
24
+
25
+ const POST_ACTIONS = new Set(["none", "favorite", "greet"]);
26
+ const GREET_EXACT_LABEL_PATTERN = /^(?:打招呼|聊一聊|立即沟通(?:[\((]\d+\s*[//]\s*\d+[\))])?|沟通)$/i;
27
+ export const RECOMMEND_DETAIL_ACTION_TEXT_SELECTORS = Object.freeze([
28
+ "button",
29
+ ".btn",
30
+ '[role="button"]',
31
+ "a",
32
+ "span",
33
+ "div"
34
+ ]);
35
+
36
+ function uniqueSelectors(...selectorGroups) {
37
+ return [...new Set(selectorGroups.flat().filter(Boolean))];
38
+ }
39
+
40
+ function uniqueByNode(candidates = []) {
41
+ const seen = new Set();
42
+ const result = [];
43
+ for (const item of candidates) {
44
+ const key = `${item.kind}:${item.root}:${item.node_id}`;
45
+ if (seen.has(key)) continue;
46
+ seen.add(key);
47
+ result.push(item);
48
+ }
49
+ return result;
50
+ }
51
+
52
+ function lowerText(...parts) {
53
+ return normalizeText(parts.filter(Boolean).join(" ")).toLowerCase();
54
+ }
55
+
56
+ function hasActiveClass(text) {
57
+ return /(?:^|\s)(?:active|curr|current|selected|checked)(?:\s|$)/i.test(text);
58
+ }
59
+
60
+ function hasDisabledSignal(text) {
61
+ return /(?:^|\s)(?:disabled|disable|forbidden|is-disabled)(?:\s|$)/i.test(text);
62
+ }
63
+
64
+ function rectArea(control) {
65
+ const rect = control?.rect || {};
66
+ return Math.max(0, Number(rect.width) || 0) * Math.max(0, Number(rect.height) || 0);
67
+ }
68
+
69
+ function isCompactLabel(control, limit = 80) {
70
+ const label = normalizeText(control?.label || "");
71
+ return label.length > 0 && label.length <= limit;
72
+ }
73
+
74
+ function controlRank(control, exactLabelPattern) {
75
+ const label = normalizeText(control?.label || "");
76
+ const selector = String(control?.selector || "");
77
+ const className = String(control?.class_name || "");
78
+ let score = 0;
79
+ if (exactLabelPattern.test(label)) score -= 1000;
80
+ if (/button|\[role=|\.btn/.test(selector) || /btn|button/i.test(className)) score -= 250;
81
+ if (selector === "div") score += 300;
82
+ if (!isCompactLabel(control)) score += 500;
83
+ score += Math.min(rectArea(control), 100000) / 1000;
84
+ score += label.length / 10;
85
+ return score;
86
+ }
87
+
88
+ function bestControl(controls, exactLabelPattern) {
89
+ return [...controls].sort((left, right) => (
90
+ controlRank(left, exactLabelPattern) - controlRank(right, exactLabelPattern)
91
+ ))[0] || null;
92
+ }
93
+
94
+ export function normalizeRecommendPostAction(value) {
95
+ const normalized = normalizeText(value).toLowerCase();
96
+ if (["", "none", "skip", "no", "不执行", "无"].includes(normalized)) return "none";
97
+ if (["favorite", "fav", "collect", "收藏", "感兴趣"].includes(normalized)) return "favorite";
98
+ if (["greet", "chat", "打招呼", "直接沟通", "沟通"].includes(normalized)) return "greet";
99
+ return POST_ACTIONS.has(normalized) ? normalized : "";
100
+ }
101
+
102
+ export function resolveRecommendPostAction({
103
+ postAction = "none",
104
+ greetCount = 0,
105
+ maxGreetCount = null
106
+ } = {}) {
107
+ const requested = normalizeRecommendPostAction(postAction) || "none";
108
+ const currentGreetCount = Number.isInteger(greetCount) && greetCount >= 0 ? greetCount : 0;
109
+ const limit = Number.isInteger(maxGreetCount) && maxGreetCount > 0 ? maxGreetCount : null;
110
+ if (requested === "greet" && limit !== null && currentGreetCount >= limit) {
111
+ return {
112
+ requested,
113
+ effective: "favorite",
114
+ reason: "greet_limit_reached",
115
+ greet_count: currentGreetCount,
116
+ max_greet_count: limit
117
+ };
118
+ }
119
+ return {
120
+ requested,
121
+ effective: requested,
122
+ reason: "requested_action",
123
+ greet_count: currentGreetCount,
124
+ max_greet_count: limit
125
+ };
126
+ }
127
+
128
+ export function classifyFavoriteControl({
129
+ outerHTML = "",
130
+ attributes = {}
131
+ } = {}) {
132
+ const label = htmlToText(outerHTML);
133
+ const labelText = normalizeText(label);
134
+ const className = normalizeText(attributes.class || "");
135
+ const title = normalizeText(attributes.title || attributes["aria-label"] || "");
136
+ const combined = lowerText(className, title);
137
+ const labelMatches = /^(?:收藏|已收藏|感兴趣|已感兴趣)$/.test(labelText);
138
+ const classMatches = /favorite|collect|interest|like/.test(combined);
139
+ const matches = labelMatches || classMatches;
140
+ const active = (
141
+ /已收藏|已感兴趣/.test(label)
142
+ || /like-icon-active|favorite-active|collect-active/i.test(outerHTML)
143
+ || hasActiveClass(className)
144
+ );
145
+ const disabled = (
146
+ Object.prototype.hasOwnProperty.call(attributes, "disabled")
147
+ || hasDisabledSignal(className)
148
+ || /disabled/i.test(outerHTML)
149
+ );
150
+ return {
151
+ kind: "favorite",
152
+ matches,
153
+ active,
154
+ disabled,
155
+ label: label || title || null,
156
+ class_name: className || null
157
+ };
158
+ }
159
+
160
+ export function classifyGreetControl({
161
+ outerHTML = "",
162
+ attributes = {}
163
+ } = {}) {
164
+ const label = htmlToText(outerHTML);
165
+ const labelText = normalizeText(label);
166
+ const className = normalizeText(attributes.class || "");
167
+ const title = normalizeText(attributes.title || attributes["aria-label"] || "");
168
+ const combined = lowerText(className, title);
169
+ const continueChat = labelText.length <= 40 && /继续沟通/.test(labelText);
170
+ const greetQuota = parseGreetQuota(labelText || title);
171
+ const greetEntry = (
172
+ GREET_EXACT_LABEL_PATTERN.test(labelText)
173
+ || greetQuota.found
174
+ || /greet/i.test(combined)
175
+ );
176
+ const disabled = (
177
+ Object.prototype.hasOwnProperty.call(attributes, "disabled")
178
+ || hasDisabledSignal(className)
179
+ || /disabled/i.test(outerHTML)
180
+ );
181
+ return {
182
+ kind: "greet",
183
+ matches: greetEntry || continueChat,
184
+ available: greetEntry && !continueChat && !disabled,
185
+ continue_chat: continueChat,
186
+ disabled,
187
+ label: label || title || null,
188
+ greet_quota: greetQuota.found ? greetQuota : null,
189
+ class_name: className || null
190
+ };
191
+ }
192
+
193
+ async function readActionNode(client, {
194
+ root,
195
+ selector,
196
+ nodeId,
197
+ kind
198
+ }) {
199
+ const [attributes, outerHTML] = await Promise.all([
200
+ getAttributesMap(client, nodeId),
201
+ getOuterHTML(client, nodeId)
202
+ ]);
203
+ let box = null;
204
+ let visible = false;
205
+ try {
206
+ box = await getNodeBox(client, nodeId);
207
+ visible = box.rect.width > 2 && box.rect.height > 2;
208
+ } catch {}
209
+ const classification = kind === "favorite"
210
+ ? classifyFavoriteControl({ outerHTML, attributes })
211
+ : classifyGreetControl({ outerHTML, attributes });
212
+ return {
213
+ kind,
214
+ root: root.name,
215
+ root_node_id: root.nodeId,
216
+ selector,
217
+ node_id: nodeId,
218
+ visible,
219
+ center: box?.center || null,
220
+ rect: box?.rect || null,
221
+ attributes,
222
+ outer_html_length: outerHTML.length,
223
+ html_preview: outerHTML.slice(0, 500),
224
+ ...classification
225
+ };
226
+ }
227
+
228
+ export async function collectRecommendActionControls(client, roots, {
229
+ favoriteSelectors = FAVORITE_BUTTON_SELECTORS,
230
+ greetSelectors = GREET_BUTTON_RECOMMEND_SELECTORS,
231
+ detailTextFallback = false
232
+ } = {}) {
233
+ const candidates = [];
234
+ const favoriteScanSelectors = detailTextFallback
235
+ ? uniqueSelectors(favoriteSelectors, RECOMMEND_DETAIL_ACTION_TEXT_SELECTORS)
236
+ : favoriteSelectors;
237
+ const greetScanSelectors = detailTextFallback
238
+ ? uniqueSelectors(greetSelectors, RECOMMEND_DETAIL_ACTION_TEXT_SELECTORS)
239
+ : greetSelectors;
240
+ for (const root of roots) {
241
+ if (!root?.nodeId) continue;
242
+ for (const [kind, selectors] of [
243
+ ["favorite", favoriteScanSelectors],
244
+ ["greet", greetScanSelectors]
245
+ ]) {
246
+ for (const selector of selectors) {
247
+ const nodeIds = await querySelectorAll(client, root.nodeId, selector);
248
+ for (const nodeId of nodeIds) {
249
+ candidates.push(await readActionNode(client, {
250
+ root,
251
+ selector,
252
+ nodeId,
253
+ kind
254
+ }));
255
+ }
256
+ }
257
+ }
258
+ }
259
+ return uniqueByNode(candidates);
260
+ }
261
+
262
+ export function summarizeRecommendActionControls(controls = []) {
263
+ const visibleControls = controls.filter((item) => item.visible && item.matches);
264
+ const favoriteControls = visibleControls.filter((item) => item.kind === "favorite");
265
+ const greetControls = visibleControls.filter((item) => item.kind === "greet");
266
+ const favorite = bestControl(
267
+ favoriteControls.filter((item) => item.matches),
268
+ /^(?:收藏|已收藏|感兴趣|已感兴趣)$/i
269
+ );
270
+ const greet = bestControl(
271
+ greetControls.filter((item) => item.available),
272
+ GREET_EXACT_LABEL_PATTERN
273
+ ) || bestControl(
274
+ greetControls.filter((item) => item.continue_chat),
275
+ /^继续沟通$/i
276
+ );
277
+ return {
278
+ favorite: favorite
279
+ ? {
280
+ found: true,
281
+ active: favorite.active,
282
+ disabled: favorite.disabled,
283
+ label: favorite.label,
284
+ selector: favorite.selector,
285
+ root: favorite.root,
286
+ node_id: favorite.node_id,
287
+ center: favorite.center
288
+ }
289
+ : { found: false },
290
+ greet: greet
291
+ ? {
292
+ found: true,
293
+ available: greet.available,
294
+ continue_chat: greet.continue_chat,
295
+ disabled: greet.disabled,
296
+ label: greet.label,
297
+ greet_quota: greet.greet_quota || null,
298
+ selector: greet.selector,
299
+ root: greet.root,
300
+ node_id: greet.node_id,
301
+ center: greet.center
302
+ }
303
+ : { found: false },
304
+ counts: {
305
+ total: controls.length,
306
+ visible_matching: visibleControls.length,
307
+ favorite: favoriteControls.length,
308
+ greet: greetControls.length
309
+ }
310
+ };
311
+ }
312
+
313
+ export async function discoverRecommendActionControls(client, {
314
+ roots = null,
315
+ selectors = {},
316
+ detailTextFallback = false
317
+ } = {}) {
318
+ const rootState = roots ? { roots } : await getRecommendRoots(client);
319
+ const controls = await collectRecommendActionControls(client, rootState.roots, {
320
+ ...selectors,
321
+ detailTextFallback
322
+ });
323
+ return {
324
+ controls,
325
+ summary: summarizeRecommendActionControls(controls)
326
+ };
327
+ }
328
+
329
+ export async function waitForRecommendActionControls(client, {
330
+ timeoutMs = 6000,
331
+ intervalMs = 250,
332
+ requireAny = true,
333
+ ...discoveryOptions
334
+ } = {}) {
335
+ const started = Date.now();
336
+ let lastDiscovery = null;
337
+ while (Date.now() - started <= timeoutMs) {
338
+ lastDiscovery = await discoverRecommendActionControls(client, discoveryOptions);
339
+ const hasControl = Boolean(
340
+ lastDiscovery.summary.favorite.found
341
+ || lastDiscovery.summary.greet.found
342
+ );
343
+ if (!requireAny || hasControl) {
344
+ return {
345
+ ...lastDiscovery,
346
+ elapsed_ms: Date.now() - started,
347
+ timed_out: false
348
+ };
349
+ }
350
+ await sleep(intervalMs);
351
+ }
352
+ return {
353
+ ...(lastDiscovery || { controls: [], summary: summarizeRecommendActionControls([]) }),
354
+ elapsed_ms: Date.now() - started,
355
+ timed_out: true
356
+ };
357
+ }
358
+
359
+ export async function getRecommendDetailActionRoots(client, detailState) {
360
+ const roots = [];
361
+ if (detailState?.popup?.node_id) {
362
+ roots.push({
363
+ name: `${detailState.popup.root || "unknown"}:detail-popup`,
364
+ nodeId: detailState.popup.node_id
365
+ });
366
+ }
367
+ if (detailState?.resumeIframe?.node_id) {
368
+ try {
369
+ roots.push({
370
+ name: `${detailState.resumeIframe.root || "unknown"}:resume-iframe-document`,
371
+ nodeId: await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id)
372
+ });
373
+ } catch {
374
+ roots.push({
375
+ name: `${detailState.resumeIframe.root || "unknown"}:resume-iframe-node`,
376
+ nodeId: detailState.resumeIframe.node_id
377
+ });
378
+ }
379
+ }
380
+ return roots;
381
+ }
382
+
383
+ export async function waitForRecommendDetailActionControls(client, {
384
+ timeoutMs = 8000,
385
+ intervalMs = 350,
386
+ selectors = {},
387
+ requireAny = true
388
+ } = {}) {
389
+ const started = Date.now();
390
+ let lastDiscovery = null;
391
+ let lastError = null;
392
+ let lastRootCount = 0;
393
+ while (Date.now() - started <= timeoutMs) {
394
+ const detailState = await waitForRecommendDetail(client, {
395
+ timeoutMs: Math.min(intervalMs, 500),
396
+ intervalMs: 100
397
+ });
398
+ const roots = await getRecommendDetailActionRoots(client, detailState);
399
+ lastRootCount = roots.length;
400
+ if (roots.length) {
401
+ try {
402
+ lastDiscovery = await discoverRecommendActionControls(client, {
403
+ roots,
404
+ selectors,
405
+ detailTextFallback: true
406
+ });
407
+ const hasControl = Boolean(
408
+ lastDiscovery.summary.favorite.found
409
+ || lastDiscovery.summary.greet.found
410
+ );
411
+ if (!requireAny || hasControl) {
412
+ return {
413
+ ...lastDiscovery,
414
+ elapsed_ms: Date.now() - started,
415
+ timed_out: false,
416
+ detail_root_count: roots.length
417
+ };
418
+ }
419
+ } catch (error) {
420
+ lastError = error?.message || String(error);
421
+ }
422
+ }
423
+ await sleep(intervalMs);
424
+ }
425
+ return {
426
+ ...(lastDiscovery || { controls: [], summary: summarizeRecommendActionControls([]) }),
427
+ elapsed_ms: Date.now() - started,
428
+ timed_out: true,
429
+ detail_root_count: lastRootCount,
430
+ last_error: lastError
431
+ };
432
+ }
433
+
434
+ export async function clickRecommendActionControl(client, control, {
435
+ allowDisabled = false
436
+ } = {}) {
437
+ if (!control?.center) {
438
+ throw new Error("Action control has no clickable center");
439
+ }
440
+ const greetQuota = control.kind === "greet"
441
+ ? assertGreetQuotaAvailable(control.greet_quota || control.label || "")
442
+ : null;
443
+ if (control.disabled && !allowDisabled) {
444
+ throw new Error(`Action control is disabled: ${control.kind}`);
445
+ }
446
+ await clickPoint(client, control.center.x, control.center.y);
447
+ return {
448
+ clicked: true,
449
+ kind: control.kind,
450
+ label: control.label,
451
+ greet_quota: greetQuota?.found ? greetQuota : null,
452
+ selector: control.selector,
453
+ root: control.root,
454
+ node_id: control.node_id,
455
+ center: control.center
456
+ };
457
+ }