@reconcrap/boss-recommend-mcp 1.1.11 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,7 @@ const RESUME_CAPTURE_RETRY_DELAY_MS = 1200;
15
15
  const MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES = 10;
16
16
  const PAGE_SCOPE_TAB_STATUS = {
17
17
  recommend: "0",
18
+ latest: "1",
18
19
  featured: "3"
19
20
  };
20
21
 
@@ -79,6 +80,7 @@ function normalizePageScope(value) {
79
80
  const normalized = normalizeText(value).toLowerCase();
80
81
  if (!normalized) return null;
81
82
  if (["recommend", "推荐", "推荐页", "推荐页面"].includes(normalized)) return "recommend";
83
+ if (["latest", "最新", "最新页", "最新页面"].includes(normalized)) return "latest";
82
84
  if (["featured", "精选", "精选页", "精选页面", "精选牛人"].includes(normalized)) return "featured";
83
85
  return null;
84
86
  }
@@ -91,6 +93,234 @@ function parseBoolean(value) {
91
93
  return null;
92
94
  }
93
95
 
96
+ function normalizeCliOptionToken(rawToken) {
97
+ const token = String(rawToken || "").trim();
98
+ if (!token) {
99
+ return { token: "", inlineValue: null };
100
+ }
101
+ const normalizedDashes = token.replace(/^[\u2010-\u2015\u2212\uFE58\uFE63\uFF0D]+/, "--");
102
+ const eqIndex = normalizedDashes.indexOf("=");
103
+ if (normalizedDashes.startsWith("--") && eqIndex > 2) {
104
+ return {
105
+ token: normalizedDashes.slice(0, eqIndex),
106
+ inlineValue: normalizedDashes.slice(eqIndex + 1)
107
+ };
108
+ }
109
+ return { token: normalizedDashes, inlineValue: null };
110
+ }
111
+
112
+ function parseFavoriteActionValue(rawValue) {
113
+ if (rawValue === null || rawValue === undefined) return null;
114
+ if (typeof rawValue === "boolean") return rawValue ? "add" : "del";
115
+ if (typeof rawValue === "number") {
116
+ if (rawValue === 1) return "add";
117
+ if (rawValue === 0) return "del";
118
+ }
119
+ const normalized = normalizeText(rawValue).toLowerCase();
120
+ if (!normalized) return null;
121
+ if (
122
+ ["1", "add", "favorite", "collect", "interested", "true", "yes", "on"].includes(normalized)
123
+ || /(?:^|[_\W])(add|favorite|collect|interest(?:ed)?)(?:$|[_\W])/.test(normalized)
124
+ ) {
125
+ return "add";
126
+ }
127
+ if (
128
+ ["0", "del", "delete", "remove", "cancel", "unfavorite", "uncollect", "false", "no", "off"].includes(normalized)
129
+ || /(?:^|[_\W])(del|delete|remove|cancel|unfavorite|uncollect|uninterest)(?:$|[_\W])/.test(normalized)
130
+ ) {
131
+ return "del";
132
+ }
133
+ return null;
134
+ }
135
+
136
+ function parseFavoriteActionFromObject(payload, visited = new Set()) {
137
+ if (!payload || typeof payload !== "object") return null;
138
+ if (visited.has(payload)) return null;
139
+ visited.add(payload);
140
+
141
+ if (Array.isArray(payload)) {
142
+ for (const item of payload) {
143
+ const action = parseFavoriteActionFromObject(item, visited);
144
+ if (action) return action;
145
+ }
146
+ return null;
147
+ }
148
+
149
+ const keys = Object.keys(payload);
150
+ const strongSignalKey = (key) => /p3|status|state|favorite|collect|interested|markstatus|isfavorite|iscollect/.test(key);
151
+ const weakSignalKey = (key) => /action|op|operation|type|mode|mark|interest/.test(key);
152
+ for (const key of keys) {
153
+ const value = payload[key];
154
+ const normalizedKey = normalizeText(key).toLowerCase();
155
+ if (strongSignalKey(normalizedKey)) {
156
+ const action = parseFavoriteActionValue(value);
157
+ if (action) return action;
158
+ }
159
+ }
160
+ for (const key of keys) {
161
+ const value = payload[key];
162
+ const normalizedKey = normalizeText(key).toLowerCase();
163
+ if (weakSignalKey(normalizedKey)) {
164
+ const action = parseFavoriteActionValue(value);
165
+ if (action) return action;
166
+ }
167
+ }
168
+
169
+ for (const key of keys) {
170
+ const value = payload[key];
171
+ if (value && typeof value === "object") {
172
+ const action = parseFavoriteActionFromObject(value, visited);
173
+ if (action) return action;
174
+ }
175
+ }
176
+ return null;
177
+ }
178
+
179
+ function parseFavoriteActionFromPostData(rawPostData) {
180
+ const postData = normalizeText(rawPostData);
181
+ if (!postData) return null;
182
+
183
+ try {
184
+ const parsed = JSON.parse(postData);
185
+ const action = parseFavoriteActionFromObject(parsed);
186
+ if (action) return action;
187
+ } catch {}
188
+
189
+ try {
190
+ const params = new URLSearchParams(postData);
191
+ const strongEntries = [];
192
+ const weakEntries = [];
193
+ for (const [key, value] of params.entries()) {
194
+ const normalizedKey = normalizeText(key).toLowerCase();
195
+ if (/p3|status|state|favorite|collect|interested|markstatus|isfavorite|iscollect/.test(normalizedKey)) {
196
+ strongEntries.push(value);
197
+ } else if (/action|op|operation|type|mode|mark|interest/.test(normalizedKey)) {
198
+ weakEntries.push(value);
199
+ }
200
+ }
201
+ for (const value of strongEntries) {
202
+ const action = parseFavoriteActionValue(value);
203
+ if (action) return action;
204
+ }
205
+ for (const value of weakEntries) {
206
+ const action = parseFavoriteActionValue(value);
207
+ if (action) return action;
208
+ }
209
+ } catch {}
210
+
211
+ const fallback = parseFavoriteActionValue(postData);
212
+ if (fallback) return fallback;
213
+
214
+ if (/star-interest-click/i.test(postData)) {
215
+ if (/(?:^|[?&"'\s])p3(?:["'\s:=]){1,3}1(?:$|[&"'\s,}])/i.test(postData)) return "add";
216
+ if (/(?:^|[?&"'\s])p3(?:["'\s:=]){1,3}0(?:$|[&"'\s,}])/i.test(postData)) return "del";
217
+ }
218
+ return null;
219
+ }
220
+
221
+ function parseFavoriteActionFromRequest(url, postData = "") {
222
+ const normalizedUrl = normalizeText(url).toLowerCase();
223
+ if (!normalizedUrl) return null;
224
+
225
+ if (/\/add(?:\/|$)|[?&](?:action|op|operation|type)=add\b|[?&](?:status|p3|favorite|collect|interested)=1\b/i.test(normalizedUrl)) {
226
+ return "add";
227
+ }
228
+ if (/\/del(?:\/|$)|[?&](?:action|op|operation|type)=del\b|[?&](?:status|p3|favorite|collect|interested)=0\b/i.test(normalizedUrl)) {
229
+ return "del";
230
+ }
231
+
232
+ return parseFavoriteActionFromPostData(postData);
233
+ }
234
+
235
+ function parseFavoriteActionFromActionLog(postData = "") {
236
+ const raw = normalizeText(postData);
237
+ if (!raw) return null;
238
+ try {
239
+ const payload = JSON.parse(raw);
240
+ if (normalizeText(payload?.action).toLowerCase() !== "star-interest-click") return null;
241
+ return parseFavoriteActionValue(payload?.p3);
242
+ } catch {}
243
+
244
+ try {
245
+ const params = new URLSearchParams(raw);
246
+ const actionName = normalizeText(params.get("action")).toLowerCase();
247
+ if (actionName !== "star-interest-click") return null;
248
+ return parseFavoriteActionValue(params.get("p3"));
249
+ } catch {}
250
+ return null;
251
+ }
252
+
253
+ function parseFavoriteActionFromKnownRequest(url, postData = "") {
254
+ const normalizedUrl = normalizeText(url).toLowerCase();
255
+ if (!normalizedUrl) return null;
256
+
257
+ if (normalizedUrl.includes("usermark")) {
258
+ if (/\/add(?:\/|$)|[?&](?:action|op|operation|type)=add\b/i.test(normalizedUrl)) {
259
+ return "add";
260
+ }
261
+ if (/\/del(?:\/|$)|[?&](?:action|op|operation|type)=del\b/i.test(normalizedUrl)) {
262
+ return "del";
263
+ }
264
+ return null;
265
+ }
266
+
267
+ if (normalizedUrl.includes("actionlog/common.json")) {
268
+ return parseFavoriteActionFromActionLog(postData);
269
+ }
270
+
271
+ return null;
272
+ }
273
+
274
+ function parseFavoriteActionFromWsPayload(payload, depth = 0) {
275
+ if (depth > 3 || payload === null || payload === undefined) return null;
276
+
277
+ if (typeof payload === "object") {
278
+ if (normalizeText(payload?.action).toLowerCase() === "star-interest-click") {
279
+ const strictAction = parseFavoriteActionValue(payload?.p3);
280
+ if (strictAction) return strictAction;
281
+ }
282
+ const nestedCandidates = [
283
+ payload.data,
284
+ payload.payload,
285
+ payload.body,
286
+ payload.message,
287
+ payload.msg
288
+ ];
289
+ for (const nested of nestedCandidates) {
290
+ const action = parseFavoriteActionFromWsPayload(nested, depth + 1);
291
+ if (action) return action;
292
+ }
293
+ return null;
294
+ }
295
+
296
+ const text = normalizeText(payload);
297
+ if (!text) return null;
298
+
299
+ try {
300
+ const parsed = JSON.parse(text);
301
+ const action = parseFavoriteActionFromWsPayload(parsed, depth + 1);
302
+ if (action) return action;
303
+ } catch {}
304
+
305
+ const actionFromActionLog = parseFavoriteActionFromActionLog(text);
306
+ if (actionFromActionLog) return actionFromActionLog;
307
+ if (/usermark/i.test(text)) {
308
+ if (/\/add(?:\/|$)|[?&](?:action|op|operation|type)=add\b/i.test(text)) return "add";
309
+ if (/\/del(?:\/|$)|[?&](?:action|op|operation|type)=del\b/i.test(text)) return "del";
310
+ }
311
+
312
+ return null;
313
+ }
314
+
315
+ function isRecoverablePostActionError(error, action) {
316
+ const normalizedAction = normalizeText(action).toLowerCase();
317
+ const normalizedCode = normalizeText(error?.code).toUpperCase();
318
+ if (!normalizedAction || !normalizedCode) return false;
319
+ if (normalizedAction === "favorite" && normalizedCode === "FAVORITE_BUTTON_FAILED") return true;
320
+ if (normalizedAction === "greet" && normalizedCode === "GREET_BUTTON_FAILED") return true;
321
+ return false;
322
+ }
323
+
94
324
  function loadCalibrationPosition(filePath) {
95
325
  try {
96
326
  const resolved = path.resolve(String(filePath || ""));
@@ -162,69 +392,71 @@ function parseArgs(argv) {
162
392
  };
163
393
 
164
394
  for (let index = 0; index < argv.length; index += 1) {
165
- const token = argv[index];
395
+ const normalizedToken = normalizeCliOptionToken(argv[index]);
396
+ const token = normalizedToken.token;
166
397
  const next = argv[index + 1];
167
- if (token === "--baseurl" && next) {
168
- parsed.baseUrl = next;
398
+ const inlineValue = normalizedToken.inlineValue;
399
+ if ((token === "--baseurl" || token === "--base-url") && (inlineValue || next)) {
400
+ parsed.baseUrl = inlineValue || next;
169
401
  parsed.__provided.baseUrl = true;
170
- index += 1;
171
- } else if (token === "--apikey" && next) {
172
- parsed.apiKey = next;
402
+ if (!inlineValue) index += 1;
403
+ } else if ((token === "--apikey" || token === "--api-key") && (inlineValue || next)) {
404
+ parsed.apiKey = inlineValue || next;
173
405
  parsed.__provided.apiKey = true;
174
- index += 1;
175
- } else if (token === "--model" && next) {
176
- parsed.model = next;
406
+ if (!inlineValue) index += 1;
407
+ } else if (token === "--model" && (inlineValue || next)) {
408
+ parsed.model = inlineValue || next;
177
409
  parsed.__provided.model = true;
178
- index += 1;
179
- } else if (token === "--openai-organization" && next) {
180
- parsed.openaiOrganization = next;
181
- index += 1;
182
- } else if (token === "--openai-project" && next) {
183
- parsed.openaiProject = next;
184
- index += 1;
185
- } else if (token === "--criteria" && next) {
186
- parsed.criteria = next;
410
+ if (!inlineValue) index += 1;
411
+ } else if (token === "--openai-organization" && (inlineValue || next)) {
412
+ parsed.openaiOrganization = inlineValue || next;
413
+ if (!inlineValue) index += 1;
414
+ } else if (token === "--openai-project" && (inlineValue || next)) {
415
+ parsed.openaiProject = inlineValue || next;
416
+ if (!inlineValue) index += 1;
417
+ } else if (token === "--criteria" && (inlineValue || next)) {
418
+ parsed.criteria = inlineValue || next;
187
419
  parsed.__provided.criteria = true;
188
- index += 1;
189
- } else if (token === "--targetCount" && next) {
190
- parsed.targetCount = parsePositiveInteger(next);
420
+ if (!inlineValue) index += 1;
421
+ } else if ((token === "--targetCount" || token === "--target-count") && (inlineValue || next)) {
422
+ parsed.targetCount = parsePositiveInteger(inlineValue || next);
191
423
  parsed.__provided.targetCount = true;
192
- index += 1;
193
- } else if (token === "--max-greet-count" && next) {
194
- parsed.maxGreetCount = parsePositiveInteger(next);
424
+ if (!inlineValue) index += 1;
425
+ } else if ((token === "--max-greet-count" || token === "--maxGreetCount") && (inlineValue || next)) {
426
+ parsed.maxGreetCount = parsePositiveInteger(inlineValue || next);
195
427
  parsed.__provided.maxGreetCount = true;
196
- index += 1;
197
- } else if (token === "--page-scope" && next) {
198
- parsed.pageScope = normalizePageScope(next) || "recommend";
428
+ if (!inlineValue) index += 1;
429
+ } else if ((token === "--page-scope" || token === "--pageScope" || token === "--page_scope") && (inlineValue || next)) {
430
+ parsed.pageScope = normalizePageScope(inlineValue || next) || "recommend";
199
431
  parsed.__provided.pageScope = true;
200
- index += 1;
201
- } else if (token === "--calibration" && next) {
202
- parsed.calibrationPath = path.resolve(next);
432
+ if (!inlineValue) index += 1;
433
+ } else if ((token === "--calibration" || token === "--calibration-path") && (inlineValue || next)) {
434
+ parsed.calibrationPath = path.resolve(inlineValue || next);
203
435
  parsed.__provided.calibrationPath = true;
204
- index += 1;
205
- } else if (token === "--port" && next) {
206
- parsed.port = parsePositiveInteger(next) || DEFAULT_PORT;
436
+ if (!inlineValue) index += 1;
437
+ } else if (token === "--port" && (inlineValue || next)) {
438
+ parsed.port = parsePositiveInteger(inlineValue || next) || DEFAULT_PORT;
207
439
  parsed.__provided.port = true;
208
- index += 1;
209
- } else if (token === "--output" && next) {
210
- parsed.output = path.resolve(next);
211
- index += 1;
212
- } else if (token === "--checkpoint-path" && next) {
213
- parsed.checkpointPath = path.resolve(next);
214
- index += 1;
215
- } else if (token === "--pause-control-path" && next) {
216
- parsed.pauseControlPath = path.resolve(next);
217
- index += 1;
440
+ if (!inlineValue) index += 1;
441
+ } else if (token === "--output" && (inlineValue || next)) {
442
+ parsed.output = path.resolve(inlineValue || next);
443
+ if (!inlineValue) index += 1;
444
+ } else if (token === "--checkpoint-path" && (inlineValue || next)) {
445
+ parsed.checkpointPath = path.resolve(inlineValue || next);
446
+ if (!inlineValue) index += 1;
447
+ } else if (token === "--pause-control-path" && (inlineValue || next)) {
448
+ parsed.pauseControlPath = path.resolve(inlineValue || next);
449
+ if (!inlineValue) index += 1;
218
450
  } else if (token === "--resume") {
219
451
  parsed.resume = true;
220
- } else if (token === "--post-action" && next) {
221
- parsed.postAction = normalizePostAction(next);
452
+ } else if ((token === "--post-action" || token === "--postAction") && (inlineValue || next)) {
453
+ parsed.postAction = normalizePostAction(inlineValue || next);
222
454
  parsed.__provided.postAction = true;
223
- index += 1;
224
- } else if (token === "--post-action-confirmed" && next) {
225
- parsed.postActionConfirmed = parseBoolean(next);
455
+ if (!inlineValue) index += 1;
456
+ } else if ((token === "--post-action-confirmed" || token === "--postActionConfirmed") && (inlineValue || next)) {
457
+ parsed.postActionConfirmed = parseBoolean(inlineValue || next);
226
458
  parsed.__provided.postActionConfirmed = true;
227
- index += 1;
459
+ if (!inlineValue) index += 1;
228
460
  } else if (token === "--help" || token === "-h") {
229
461
  parsed.help = true;
230
462
  }
@@ -544,6 +776,7 @@ function buildListCandidatesExpr(processedKeys) {
544
776
  const processed = new Set(processedKeys || []);
545
777
  const cards = Array.from(doc.querySelectorAll('ul.card-list > li.card-item'));
546
778
  const featuredCards = Array.from(doc.querySelectorAll('li.geek-info-card'));
779
+ const latestCards = Array.from(doc.querySelectorAll('.candidate-card-wrap'));
547
780
  const textOf = (el) => String(el ? el.textContent : '').replace(/\s+/g, ' ').trim();
548
781
  const tabs = Array.from(doc.querySelectorAll('li.tab-item[data-status], li[data-status][class*="tab"]'));
549
782
  const activeTab = tabs.find((node) => /(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(String(node.className || ''))) || null;
@@ -608,19 +841,71 @@ function buildListCandidatesExpr(processedKeys) {
608
841
  layout: 'featured'
609
842
  };
610
843
  }).filter(Boolean);
844
+ const latestCandidates = latestCards.map((card, index) => {
845
+ const inner = card.querySelector('.card-inner[data-geek]') || card.querySelector('[data-geek]');
846
+ if (!inner) return null;
847
+ const geekId = inner.getAttribute('data-geek');
848
+ if (!geekId) return null;
849
+ const rect = card.getBoundingClientRect();
850
+ const nameEl = card.querySelector('.name, .name-wrap .name, .name-wrap');
851
+ const tags = Array.from(card.querySelectorAll('.base-info span, .edu-wrap span, .desc span, .tag-wrap span, .tag-item'))
852
+ .map((item) => textOf(item))
853
+ .filter(Boolean);
854
+ const latestWork = card.querySelector('.timeline-wrap.work-exps .timeline-item');
855
+ const workSpans = latestWork
856
+ ? Array.from(latestWork.querySelectorAll('.join-text-wrap.content span')).map((item) => textOf(item)).filter(Boolean)
857
+ : [];
858
+ return {
859
+ found: true,
860
+ index,
861
+ key: geekId,
862
+ geek_id: geekId,
863
+ name: textOf(nameEl),
864
+ school: tags[0] || '',
865
+ major: tags[1] || '',
866
+ degree: tags[2] || '',
867
+ last_company: workSpans[0] || '',
868
+ last_position: workSpans[1] || '',
869
+ x: frameRect.left + rect.left + Math.min(Math.max(rect.width / 2, 80), Math.max(rect.width - 40, 80)),
870
+ y: frameRect.top + rect.top + Math.min(Math.max(rect.height / 2, 24), Math.max(rect.height - 12, 24)),
871
+ width: rect.width,
872
+ height: rect.height,
873
+ layout: 'latest'
874
+ };
875
+ }).filter(Boolean);
611
876
  const inferredStatus = activeStatus
612
- || (featuredCandidates.length > 0 && recommendCandidates.length === 0 ? '3' : recommendCandidates.length > 0 ? '0' : '');
877
+ || (
878
+ featuredCandidates.length > 0 && recommendCandidates.length === 0 && latestCandidates.length === 0
879
+ ? '3'
880
+ : latestCandidates.length > 0 && recommendCandidates.length === 0 && featuredCandidates.length === 0
881
+ ? '1'
882
+ : recommendCandidates.length > 0 && featuredCandidates.length === 0 && latestCandidates.length === 0
883
+ ? '0'
884
+ : ''
885
+ );
613
886
  const activeLayout = inferredStatus === '3'
614
887
  ? 'featured'
615
- : inferredStatus === '0'
616
- ? 'recommend'
617
- : (featuredCandidates.length > 0 && recommendCandidates.length === 0 ? 'featured' : 'recommend');
618
- const candidates = activeLayout === 'featured' ? featuredCandidates : recommendCandidates;
888
+ : inferredStatus === '1'
889
+ ? 'latest'
890
+ : inferredStatus === '0'
891
+ ? 'recommend'
892
+ : (
893
+ featuredCandidates.length > 0 && recommendCandidates.length === 0 && latestCandidates.length === 0
894
+ ? 'featured'
895
+ : latestCandidates.length > 0 && featuredCandidates.length === 0 && recommendCandidates.length === 0
896
+ ? 'latest'
897
+ : 'recommend'
898
+ );
899
+ const candidates = activeLayout === 'featured'
900
+ ? featuredCandidates
901
+ : activeLayout === 'latest'
902
+ ? latestCandidates
903
+ : recommendCandidates;
619
904
  return {
620
905
  ok: true,
621
906
  candidates: candidates.filter((candidate) => !processed.has(candidate.key)),
622
907
  candidate_count: candidates.length,
623
- total_cards: activeLayout === 'featured' ? featuredCards.length : cards.length,
908
+ total_cards: activeLayout === 'featured' ? featuredCards.length : activeLayout === 'latest' ? latestCards.length : cards.length,
624
909
  active_tab_status: inferredStatus || null,
625
910
  layout: activeLayout
626
911
  };
@@ -641,16 +926,28 @@ const jsGetListState = `(() => {
641
926
  const candidateCards = cards.filter((card) => card.querySelector('.card-inner[data-geekid]'));
642
927
  const featuredCards = Array.from(doc.querySelectorAll('li.geek-info-card'));
643
928
  const featuredCandidates = featuredCards.filter((card) => card.querySelector('a[data-geekid]'));
929
+ const latestCards = Array.from(doc.querySelectorAll('.candidate-card-wrap'));
930
+ const latestCandidates = latestCards.filter((card) => card.querySelector('.card-inner[data-geek], [data-geek]'));
644
931
  const tabs = Array.from(doc.querySelectorAll('li.tab-item[data-status], li[data-status][class*="tab"]'));
645
932
  const activeTab = tabs.find((node) => /(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(String(node.className || ''))) || null;
646
933
  const activeTabStatus = activeTab ? String(activeTab.getAttribute('data-status') || '') : '';
647
934
  const inferredStatus = activeTabStatus
648
- || (featuredCandidates.length > 0 && candidateCards.length === 0 ? '3' : candidateCards.length > 0 ? '0' : '');
935
+ || (
936
+ featuredCandidates.length > 0 && candidateCards.length === 0 && latestCandidates.length === 0
937
+ ? '3'
938
+ : latestCandidates.length > 0 && candidateCards.length === 0 && featuredCandidates.length === 0
939
+ ? '1'
940
+ : candidateCards.length > 0 && featuredCandidates.length === 0 && latestCandidates.length === 0
941
+ ? '0'
942
+ : ''
943
+ );
649
944
  const effectiveCount = inferredStatus === '3'
650
945
  ? featuredCandidates.length
651
- : inferredStatus === '0'
652
- ? candidateCards.length
653
- : Math.max(candidateCards.length, featuredCandidates.length);
946
+ : inferredStatus === '1'
947
+ ? latestCandidates.length
948
+ : inferredStatus === '0'
949
+ ? candidateCards.length
950
+ : Math.max(candidateCards.length, featuredCandidates.length, latestCandidates.length);
654
951
  return {
655
952
  ok: true,
656
953
  scrollTop: body ? body.scrollTop : 0,
@@ -668,7 +965,8 @@ const jsGetListState = `(() => {
668
965
  candidateCount: effectiveCount,
669
966
  recommendCandidateCount: candidateCards.length,
670
967
  featuredCandidateCount: featuredCandidates.length,
671
- totalCards: Math.max(cards.length, featuredCards.length),
968
+ latestCandidateCount: latestCandidates.length,
969
+ totalCards: Math.max(cards.length, featuredCards.length, latestCards.length),
672
970
  activeTabStatus: inferredStatus || null
673
971
  };
674
972
  })()`;
@@ -684,12 +982,21 @@ const jsScrollList = `(() => {
684
982
  const body = doc.body;
685
983
  const recommendCards = Array.from(doc.querySelectorAll('ul.card-list > li.card-item')).filter((card) => card.querySelector('.card-inner[data-geekid]'));
686
984
  const featuredCards = Array.from(doc.querySelectorAll('li.geek-info-card')).filter((card) => card.querySelector('a[data-geekid]'));
985
+ const latestCards = Array.from(doc.querySelectorAll('.candidate-card-wrap')).filter((card) => card.querySelector('.card-inner[data-geek], [data-geek]'));
687
986
  const tabs = Array.from(doc.querySelectorAll('li.tab-item[data-status], li[data-status][class*="tab"]'));
688
987
  const activeTab = tabs.find((node) => /(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(String(node.className || ''))) || null;
689
988
  const activeStatus = activeTab ? String(activeTab.getAttribute('data-status') || '') : '';
690
989
  const inferredStatus = activeStatus
691
- || (featuredCards.length > 0 && recommendCards.length === 0 ? '3' : recommendCards.length > 0 ? '0' : '');
692
- const activeCards = inferredStatus === '3' ? featuredCards : recommendCards;
990
+ || (
991
+ featuredCards.length > 0 && recommendCards.length === 0 && latestCards.length === 0
992
+ ? '3'
993
+ : latestCards.length > 0 && recommendCards.length === 0 && featuredCards.length === 0
994
+ ? '1'
995
+ : recommendCards.length > 0 && featuredCards.length === 0 && latestCards.length === 0
996
+ ? '0'
997
+ : ''
998
+ );
999
+ const activeCards = inferredStatus === '3' ? featuredCards : inferredStatus === '1' ? latestCards : recommendCards;
693
1000
  const lastCard = activeCards[activeCards.length - 1];
694
1001
  const before = {
695
1002
  scrollTop: body ? body.scrollTop : 0,
@@ -1546,6 +1853,8 @@ class RecommendScreenCli {
1546
1853
  this.latestResumeNetworkPayload = null;
1547
1854
  this.favoriteActionEvents = [];
1548
1855
  this.favoriteClickPendingSince = 0;
1856
+ this.favoriteNetworkTraces = [];
1857
+ this.webSocketByRequestId = new Map();
1549
1858
  this.resumeSourceStats = {
1550
1859
  network: 0,
1551
1860
  image_fallback: 0
@@ -1640,6 +1949,7 @@ class RecommendScreenCli {
1640
1949
 
1641
1950
  markFavoriteClickPending() {
1642
1951
  this.favoriteClickPendingSince = Date.now();
1952
+ this.favoriteNetworkTraces = [];
1643
1953
  }
1644
1954
 
1645
1955
  consumeFavoriteActionResult(since = 0) {
@@ -1656,6 +1966,30 @@ class RecommendScreenCli {
1656
1966
  return matched.action || null;
1657
1967
  }
1658
1968
 
1969
+ recordFavoriteNetworkTrace(entry) {
1970
+ const trace = {
1971
+ ts: Date.now(),
1972
+ ...entry
1973
+ };
1974
+ this.favoriteNetworkTraces.push(trace);
1975
+ if (this.favoriteNetworkTraces.length > 60) {
1976
+ this.favoriteNetworkTraces = this.favoriteNetworkTraces.slice(-60);
1977
+ }
1978
+ }
1979
+
1980
+ summarizeFavoriteNetworkTrace(since = 0) {
1981
+ const timestamp = Number.isFinite(since) ? since : 0;
1982
+ return this.favoriteNetworkTraces
1983
+ .filter((item) => Number(item?.ts || 0) >= timestamp)
1984
+ .slice(-12)
1985
+ .map((item) => {
1986
+ if (item.kind === "ws") {
1987
+ return `[ws:${item.direction}] ${item.url || "unknown"} payload=${item.payload || ""}`;
1988
+ }
1989
+ return `[http] ${item.method || "GET"} ${item.url || ""} body=${item.postData || ""}`;
1990
+ });
1991
+ }
1992
+
1659
1993
  cacheResumeNetworkPayload(payload, fallbackGeekId = null) {
1660
1994
  if (!payload || typeof payload !== "object") return;
1661
1995
  const geekDetail = payload.geekDetail || payload;
@@ -1734,31 +2068,55 @@ class RecommendScreenCli {
1734
2068
 
1735
2069
  if (this.favoriteClickPendingSince <= 0) return;
1736
2070
  const requestTs = Date.now();
1737
- if (requestTs < this.favoriteClickPendingSince - 1000) return;
1738
-
1739
- if (url.includes("userMark")) {
1740
- const action = /\/add(?:\/|$)|[?&]action=add/i.test(url)
1741
- ? "add"
1742
- : /\/del(?:\/|$)|[?&]action=del/i.test(url)
1743
- ? "del"
1744
- : null;
1745
- if (action) {
1746
- this.favoriteActionEvents.push({ action, ts: requestTs, source: "userMark", url });
1747
- }
1748
- return;
1749
- }
2071
+ if (requestTs < this.favoriteClickPendingSince) return;
2072
+ const method = normalizeText(params?.request?.method || "").toUpperCase() || "GET";
2073
+ const postData = params?.request?.postData || "";
2074
+ this.recordFavoriteNetworkTrace({
2075
+ ts: requestTs,
2076
+ kind: "http",
2077
+ method,
2078
+ url: url.slice(0, 240),
2079
+ postData: normalizeText(postData).slice(0, 200)
2080
+ });
2081
+ const action = parseFavoriteActionFromKnownRequest(url, postData);
2082
+ if (!action) return;
2083
+ const source = url.includes("userMark")
2084
+ ? "userMark"
2085
+ : url.includes("actionLog/common.json")
2086
+ ? "actionLog"
2087
+ : "favorite";
2088
+ this.favoriteActionEvents.push({ action, ts: requestTs, source, url });
2089
+ }
1750
2090
 
1751
- if (url.includes("actionLog/common.json")) {
1752
- try {
1753
- const payload = JSON.parse(params?.request?.postData || "{}");
1754
- if (payload?.action === "star-interest-click") {
1755
- const action = Number(payload?.p3) === 1 ? "add" : Number(payload?.p3) === 0 ? "del" : null;
1756
- if (action) {
1757
- this.favoriteActionEvents.push({ action, ts: requestTs, source: "actionLog", url });
1758
- }
1759
- }
1760
- } catch {}
1761
- }
2091
+ handleNetworkWebSocketCreated(params) {
2092
+ const requestId = normalizeText(params?.requestId || "");
2093
+ if (!requestId) return;
2094
+ const url = normalizeText(params?.url || "");
2095
+ this.webSocketByRequestId.set(requestId, url || "");
2096
+ }
2097
+
2098
+ handleNetworkWebSocketFrame(params, direction = "sent") {
2099
+ if (this.favoriteClickPendingSince <= 0) return;
2100
+ const ts = Date.now();
2101
+ if (ts < this.favoriteClickPendingSince) return;
2102
+ const requestId = normalizeText(params?.requestId || "");
2103
+ const payloadData = normalizeText(params?.response?.payloadData || "");
2104
+ const wsUrl = this.webSocketByRequestId.get(requestId) || "";
2105
+ this.recordFavoriteNetworkTrace({
2106
+ ts,
2107
+ kind: "ws",
2108
+ direction,
2109
+ url: wsUrl ? wsUrl.slice(0, 240) : requestId ? `ws:${requestId}` : "ws",
2110
+ payload: payloadData.slice(0, 200)
2111
+ });
2112
+ const action = parseFavoriteActionFromWsPayload(payloadData);
2113
+ if (!action) return;
2114
+ this.favoriteActionEvents.push({
2115
+ action,
2116
+ ts,
2117
+ source: `websocket_${direction}`,
2118
+ url: wsUrl || (requestId ? `ws:${requestId}` : "websocket")
2119
+ });
1762
2120
  }
1763
2121
 
1764
2122
  async handleNetworkLoadingFinished(params) {
@@ -1921,6 +2279,27 @@ class RecommendScreenCli {
1921
2279
  } catch {}
1922
2280
  });
1923
2281
  }
2282
+ if (typeof this.Network.webSocketCreated === "function") {
2283
+ this.Network.webSocketCreated((params) => {
2284
+ try {
2285
+ this.handleNetworkWebSocketCreated(params);
2286
+ } catch {}
2287
+ });
2288
+ }
2289
+ if (typeof this.Network.webSocketFrameSent === "function") {
2290
+ this.Network.webSocketFrameSent((params) => {
2291
+ try {
2292
+ this.handleNetworkWebSocketFrame(params, "sent");
2293
+ } catch {}
2294
+ });
2295
+ }
2296
+ if (typeof this.Network.webSocketFrameReceived === "function") {
2297
+ this.Network.webSocketFrameReceived((params) => {
2298
+ try {
2299
+ this.handleNetworkWebSocketFrame(params, "received");
2300
+ } catch {}
2301
+ });
2302
+ }
1924
2303
  if (typeof this.Network.loadingFinished === "function") {
1925
2304
  this.Network.loadingFinished((params) => {
1926
2305
  this.handleNetworkLoadingFinished(params).catch(() => {});
@@ -2444,7 +2823,8 @@ class RecommendScreenCli {
2444
2823
  async favoriteCandidate(options = {}) {
2445
2824
  if (this.args.pageScope === "featured") {
2446
2825
  if (options.alreadyInterested === true) {
2447
- return { actionTaken: "already_favorited" };
2826
+ log("[FAVORITE] network profile indicates alreadyInterested=true,跳过点击以避免误取消收藏。");
2827
+ return { actionTaken: "already_favorited", source: "network_profile" };
2448
2828
  }
2449
2829
  if (!this.featuredCalibration?.position) {
2450
2830
  throw this.buildError(
@@ -2459,6 +2839,7 @@ class RecommendScreenCli {
2459
2839
 
2460
2840
  const base = this.featuredCalibration.position;
2461
2841
  const maxClicks = 5;
2842
+ let detectedAlreadyFavoritedByNetwork = false;
2462
2843
  for (let clickIndex = 0; clickIndex < maxClicks; clickIndex += 1) {
2463
2844
  const clickStartedAt = Date.now();
2464
2845
  this.markFavoriteClickPending();
@@ -2474,22 +2855,38 @@ class RecommendScreenCli {
2474
2855
  }
2475
2856
 
2476
2857
  let sawDel = false;
2477
- for (let index = 0; index < 10; index += 1) {
2858
+ for (let index = 0; index < 14; index += 1) {
2478
2859
  await sleep(humanDelay(260, 80));
2479
2860
  const networkAction = this.consumeFavoriteActionResult(clickStartedAt);
2480
2861
  if (networkAction === "add") {
2481
- return { actionTaken: "favorite" };
2862
+ return detectedAlreadyFavoritedByNetwork
2863
+ ? { actionTaken: "already_favorited", re_favorited: true }
2864
+ : { actionTaken: "favorite" };
2482
2865
  }
2483
2866
  if (networkAction === "del") {
2867
+ detectedAlreadyFavoritedByNetwork = true;
2868
+ log("[FAVORITE] 检测到 network=del,推断该人选原本已收藏,继续点击恢复为收藏状态。");
2484
2869
  sawDel = true;
2485
2870
  break;
2486
2871
  }
2487
2872
  }
2488
2873
  if (!sawDel && clickIndex === maxClicks - 1) {
2874
+ const traceSummary = this.summarizeFavoriteNetworkTrace(clickStartedAt);
2875
+ if (traceSummary.length > 0) {
2876
+ log(`[FAVORITE_NETWORK_TRACE] ${traceSummary.join(" | ")}`);
2877
+ } else {
2878
+ log("[FAVORITE_NETWORK_TRACE] 点击后未捕获到可识别的 HTTP/WS 网络信号。");
2879
+ }
2489
2880
  break;
2490
2881
  }
2491
2882
  }
2492
2883
 
2884
+ if (detectedAlreadyFavoritedByNetwork) {
2885
+ throw this.buildError(
2886
+ "FAVORITE_BUTTON_FAILED",
2887
+ "精选页检测到 network del(原本已收藏),但后续未检测到 network add(恢复收藏)成功信号。"
2888
+ );
2889
+ }
2493
2890
  throw this.buildError("FAVORITE_BUTTON_FAILED", "精选页收藏未检测到 network add 成功信号。");
2494
2891
  }
2495
2892
 
@@ -2700,6 +3097,10 @@ class RecommendScreenCli {
2700
3097
  if (!this.args.baseUrl || !this.args.apiKey || !this.args.model) {
2701
3098
  throw this.buildError("SCREEN_CONFIG_ERROR", "Missing baseUrl/apiKey/model", false);
2702
3099
  }
3100
+ log(
3101
+ `[ARGS] page_scope=${this.args.pageScope} target_count=${this.args.targetCount ?? "none"} ` +
3102
+ `post_action=${this.args.postAction || "unset"} port=${this.args.port}`
3103
+ );
2703
3104
 
2704
3105
  if (!(this.args.postActionConfirmed === true && this.args.postAction)) {
2705
3106
  this.args.postAction = await promptPostAction();
@@ -2888,23 +3289,41 @@ class RecommendScreenCli {
2888
3289
  effectiveAction = "favorite";
2889
3290
  this.greetLimitFallbackCount += 1;
2890
3291
  }
2891
- const actionResult = effectiveAction === "favorite"
2892
- ? await this.favoriteCandidate({
2893
- alreadyInterested: networkCandidateInfo?.alreadyInterested === true
2894
- })
2895
- : effectiveAction === "greet"
2896
- ? await this.greetCandidate()
2897
- : { actionTaken: "none" };
3292
+ let actionResult = { actionTaken: "none" };
3293
+ try {
3294
+ actionResult = effectiveAction === "favorite"
3295
+ ? await this.favoriteCandidate({
3296
+ alreadyInterested: networkCandidateInfo?.alreadyInterested === true
3297
+ })
3298
+ : effectiveAction === "greet"
3299
+ ? await this.greetCandidate()
3300
+ : { actionTaken: "none" };
3301
+ } catch (postActionError) {
3302
+ if (!isRecoverablePostActionError(postActionError, effectiveAction)) {
3303
+ throw postActionError;
3304
+ }
3305
+ log(`[POST_ACTION_WARN] ${effectiveAction} 失败,继续写入通过候选人: ${postActionError.message || postActionError}`);
3306
+ actionResult = {
3307
+ actionTaken: `${effectiveAction}_failed`,
3308
+ errorCode: postActionError.code || "POST_ACTION_FAILED",
3309
+ errorMessage: normalizeText(postActionError.message || "post action failed")
3310
+ };
3311
+ }
2898
3312
  if (actionResult.actionTaken === "greet") {
2899
3313
  this.greetCount += 1;
2900
3314
  }
3315
+ const screeningReason = normalizeText(screening.reason || screening.summary || "");
3316
+ const actionErrorMessage = normalizeText(actionResult.errorMessage || "");
3317
+ const mergedReason = actionErrorMessage
3318
+ ? `${screeningReason}${screeningReason ? " | " : ""}[${effectiveAction}失败] ${actionErrorMessage}`
3319
+ : screeningReason;
2901
3320
  this.passedCandidates.push({
2902
3321
  name: candidateProfile.name,
2903
3322
  school: candidateProfile.school,
2904
3323
  major: candidateProfile.major,
2905
3324
  company: candidateProfile.company,
2906
3325
  position: candidateProfile.position,
2907
- reason: screening.reason || screening.summary || "",
3326
+ reason: mergedReason,
2908
3327
  action: actionResult.actionTaken,
2909
3328
  geekId: nextCandidate.geek_id,
2910
3329
  summary: screening.summary,
@@ -3019,11 +3438,11 @@ async function main() {
3019
3438
  const args = parseArgs(process.argv.slice(2));
3020
3439
  if (args.help) {
3021
3440
  console.log(JSON.stringify({
3022
- status: "COMPLETED",
3023
- result: {
3024
- usage: "node boss-recommend-screen-cli.cjs --criteria \"有 MCP 开发经验\" --post-action <favorite|greet|none> --max-greet-count 10 --post-action-confirmed true --baseurl <url> --apikey <key> --model <model> --page-scope recommend|featured --calibration <favorite-calibration.json> --port 9222 --output <csv-path> --checkpoint-path <checkpoint.json> --pause-control-path <pause-control.json> [--resume]"
3025
- }
3026
- }));
3441
+ status: "COMPLETED",
3442
+ result: {
3443
+ usage: "node boss-recommend-screen-cli.cjs --criteria \"有 MCP 开发经验\" --post-action <favorite|greet|none> --max-greet-count 10 --post-action-confirmed true --baseurl <url> --apikey <key> --model <model> --page-scope recommend|latest|featured --calibration <favorite-calibration.json> --port 9222 --output <csv-path> --checkpoint-path <checkpoint.json> --pause-control-path <pause-control.json> [--resume]"
3444
+ }
3445
+ }));
3027
3446
  return;
3028
3447
  }
3029
3448
 
@@ -3060,8 +3479,13 @@ if (require.main === module) {
3060
3479
  __testables: {
3061
3480
  MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES,
3062
3481
  RESUME_CAPTURE_MAX_ATTEMPTS,
3063
- RESUME_CAPTURE_WAIT_MS
3482
+ RESUME_CAPTURE_WAIT_MS,
3483
+ parseFavoriteActionFromPostData,
3484
+ parseFavoriteActionFromRequest,
3485
+ parseFavoriteActionFromKnownRequest,
3486
+ parseFavoriteActionFromActionLog,
3487
+ parseFavoriteActionFromWsPayload,
3488
+ isRecoverablePostActionError
3064
3489
  }
3065
3490
  };
3066
3491
  }
3067
-