@reconcrap/boss-recommend-mcp 1.3.26 → 1.3.27

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": "1.3.26",
3
+ "version": "1.3.27",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -1015,6 +1015,233 @@ async function testBossChatLlmShouldApplyThinkingDefaultsAndOverrides() {
1015
1015
  assert.deepEqual(responsesPayload.reasoning, { effort: "low" });
1016
1016
  }
1017
1017
 
1018
+ async function testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime() {
1019
+ const calls = [];
1020
+ const page = {
1021
+ async ensureReady() {
1022
+ calls.push("ensureReady");
1023
+ return { hasListContainer: true, listItemCount: 1 };
1024
+ },
1025
+ async activatePrimaryChatLabel(label) {
1026
+ calls.push(`activatePrimaryChatLabel:${label}`);
1027
+ return { changed: false, verified: true, activeLabel: label };
1028
+ },
1029
+ async selectJob(jobSelection) {
1030
+ calls.push(`selectJob:${jobSelection.label}`);
1031
+ return jobSelection;
1032
+ },
1033
+ async activateUnreadFilter() {
1034
+ calls.push("activateUnreadFilter");
1035
+ return { changed: false, verified: true, activeLabel: "未读" };
1036
+ },
1037
+ async primeConversationByFirstCandidate() {
1038
+ calls.push("primeConversationByFirstCandidate:1");
1039
+ return {
1040
+ candidate: {
1041
+ customerId: "1001",
1042
+ name: "候选人A",
1043
+ sourceJob: "算法工程师",
1044
+ domIndex: 0,
1045
+ },
1046
+ totalVisibleCandidates: 1,
1047
+ readyState: {
1048
+ hasOnlineResume: true,
1049
+ hasAskResume: true,
1050
+ hasAttachmentResume: false,
1051
+ },
1052
+ };
1053
+ },
1054
+ async getLoadedCustomers() {
1055
+ calls.push("getLoadedCustomers:1");
1056
+ return [];
1057
+ },
1058
+ async closeResumeModalDomOnce() {
1059
+ return {
1060
+ closed: true,
1061
+ method: "already-closed",
1062
+ finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
1063
+ };
1064
+ },
1065
+ };
1066
+ const stateStore = {
1067
+ async load() {},
1068
+ hasAny() {
1069
+ return false;
1070
+ },
1071
+ async record() {},
1072
+ };
1073
+ const app = new BossChatApp({
1074
+ page,
1075
+ llmClient: {},
1076
+ interaction: {
1077
+ async sleepRange() {},
1078
+ async maybeRest() {},
1079
+ },
1080
+ resumeCaptureService: {},
1081
+ stateStore,
1082
+ reportStore: {
1083
+ async write() {
1084
+ return "report.json";
1085
+ },
1086
+ },
1087
+ logger: { log() {} },
1088
+ dryRun: true,
1089
+ artifactRootDir: os.tmpdir(),
1090
+ resumeOpenCooldownMs: 0,
1091
+ });
1092
+ app.processCustomer = async (_customer, _profile, _runId, options = {}) => {
1093
+ calls.push(`processCustomer:${options.skipCardClick === true ? "skip" : "click"}`);
1094
+ return {
1095
+ name: "候选人A",
1096
+ passed: false,
1097
+ requested: false,
1098
+ reason: "skip",
1099
+ error: "",
1100
+ artifacts: {},
1101
+ };
1102
+ };
1103
+
1104
+ const summary = await app.run({
1105
+ screeningCriteria: "有 AI 项目经验",
1106
+ targetCount: 1,
1107
+ startFrom: "unread",
1108
+ jobSelection: { label: "算法工程师", value: "job-1" },
1109
+ chrome: { port: 9222 },
1110
+ llm: { model: "gpt-test" },
1111
+ });
1112
+
1113
+ assert.deepEqual(calls.slice(0, 5), [
1114
+ "ensureReady",
1115
+ "activatePrimaryChatLabel:全部",
1116
+ "selectJob:算法工程师",
1117
+ "activateUnreadFilter",
1118
+ "primeConversationByFirstCandidate:1",
1119
+ ]);
1120
+ assert.equal(calls.includes("processCustomer:skip"), true);
1121
+ assert.equal(summary.inspected, 1);
1122
+ assert.equal(summary.skipped, 1);
1123
+ }
1124
+
1125
+ async function testBossChatAppShouldRestoreListContextAfterRecovery() {
1126
+ const calls = [];
1127
+ let primeCount = 0;
1128
+ let loadedCount = 0;
1129
+ const page = {
1130
+ async ensureReady() {
1131
+ return { hasListContainer: true, listItemCount: 1 };
1132
+ },
1133
+ async activatePrimaryChatLabel(label) {
1134
+ calls.push(`activatePrimaryChatLabel:${label}`);
1135
+ return { changed: false, verified: true, activeLabel: label };
1136
+ },
1137
+ async selectJob(jobSelection) {
1138
+ calls.push(`selectJob:${jobSelection.label}`);
1139
+ return jobSelection;
1140
+ },
1141
+ async activateUnreadFilter() {
1142
+ calls.push("activateUnreadFilter");
1143
+ return { changed: false, verified: true, activeLabel: "未读" };
1144
+ },
1145
+ async primeConversationByFirstCandidate() {
1146
+ primeCount += 1;
1147
+ calls.push(`primeConversationByFirstCandidate:${primeCount}`);
1148
+ if (primeCount === 1) {
1149
+ throw new Error("NO_FIRST_CANDIDATE");
1150
+ }
1151
+ return {
1152
+ candidate: {
1153
+ customerId: "1002",
1154
+ name: "候选人B",
1155
+ sourceJob: "算法工程师",
1156
+ domIndex: 0,
1157
+ },
1158
+ totalVisibleCandidates: 1,
1159
+ readyState: {
1160
+ hasOnlineResume: true,
1161
+ hasAskResume: true,
1162
+ hasAttachmentResume: false,
1163
+ },
1164
+ };
1165
+ },
1166
+ async getLoadedCustomers() {
1167
+ loadedCount += 1;
1168
+ calls.push(`getLoadedCustomers:${loadedCount}`);
1169
+ if (loadedCount === 1) {
1170
+ throw new Error("CHAT_CARD_LIST_NOT_FOUND");
1171
+ }
1172
+ return [];
1173
+ },
1174
+ async recoverToChatIndex() {
1175
+ calls.push("recoverToChatIndex");
1176
+ return { changed: true, href: "https://www.zhipin.com/web/chat/index" };
1177
+ },
1178
+ async closeResumeModalDomOnce() {
1179
+ return {
1180
+ closed: true,
1181
+ method: "already-closed",
1182
+ finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
1183
+ };
1184
+ },
1185
+ };
1186
+ const stateStore = {
1187
+ async load() {},
1188
+ hasAny() {
1189
+ return false;
1190
+ },
1191
+ async record() {},
1192
+ };
1193
+ const app = new BossChatApp({
1194
+ page,
1195
+ llmClient: {},
1196
+ interaction: {
1197
+ async sleepRange() {},
1198
+ async maybeRest() {},
1199
+ },
1200
+ resumeCaptureService: {},
1201
+ stateStore,
1202
+ reportStore: {
1203
+ async write() {
1204
+ return "report.json";
1205
+ },
1206
+ },
1207
+ logger: { log() {} },
1208
+ dryRun: true,
1209
+ artifactRootDir: os.tmpdir(),
1210
+ resumeOpenCooldownMs: 0,
1211
+ });
1212
+ app.processCustomer = async (_customer, _profile, _runId, options = {}) => {
1213
+ calls.push(`processCustomer:${options.skipCardClick === true ? "skip" : "click"}`);
1214
+ return {
1215
+ name: "候选人B",
1216
+ passed: false,
1217
+ requested: false,
1218
+ reason: "skip",
1219
+ error: "",
1220
+ artifacts: {},
1221
+ };
1222
+ };
1223
+
1224
+ const summary = await app.run({
1225
+ screeningCriteria: "有 AI 项目经验",
1226
+ targetCount: 1,
1227
+ startFrom: "unread",
1228
+ jobSelection: { label: "算法工程师", value: "job-1" },
1229
+ chrome: { port: 9222 },
1230
+ llm: { model: "gpt-test" },
1231
+ });
1232
+
1233
+ assert.equal(calls.filter((item) => item === "activatePrimaryChatLabel:全部").length, 2);
1234
+ const recoverIndex = calls.indexOf("recoverToChatIndex");
1235
+ assert.equal(recoverIndex >= 0, true);
1236
+ assert.equal(calls[recoverIndex + 1], "activatePrimaryChatLabel:全部");
1237
+ assert.equal(calls[recoverIndex + 2], "selectJob:算法工程师");
1238
+ assert.equal(calls[recoverIndex + 3], "activateUnreadFilter");
1239
+ assert.equal(calls[recoverIndex + 4], "primeConversationByFirstCandidate:2");
1240
+ assert.equal(calls.includes("processCustomer:skip"), true);
1241
+ assert.equal(summary.inspected, 1);
1242
+ assert.equal(summary.skipped, 1);
1243
+ }
1244
+
1018
1245
  async function testBossChatAppShouldPersistEvidenceArtifacts() {
1019
1246
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-artifacts-"));
1020
1247
  await mkdir(tempDir, { recursive: true });
@@ -1150,6 +1377,8 @@ async function main() {
1150
1377
  testBossChatLlmEvidenceGateShouldDemoteUnmatchedEvidence();
1151
1378
  await testBossChatLlmTextChunkFallbackShouldWork();
1152
1379
  await testBossChatLlmShouldApplyThinkingDefaultsAndOverrides();
1380
+ await testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime();
1381
+ await testBossChatAppShouldRestoreListContextAfterRecovery();
1153
1382
  await testBossChatAppShouldPersistEvidenceArtifacts();
1154
1383
  console.log("boss-chat tests passed");
1155
1384
  }
@@ -142,6 +142,16 @@ export class BossChatApp {
142
142
  } catch {}
143
143
  }
144
144
 
145
+ async restoreListContext(profile) {
146
+ if (typeof this.page.activatePrimaryChatLabel === 'function') {
147
+ await this.page.activatePrimaryChatLabel('全部');
148
+ }
149
+ await this.page.selectJob(profile.jobSelection);
150
+ return profile.startFrom === 'all'
151
+ ? this.page.activateAllFilter()
152
+ : this.page.activateUnreadFilter();
153
+ }
154
+
145
155
  async run(profile) {
146
156
  const startedAt = new Date().toISOString();
147
157
  const runId = runToken(new Date());
@@ -158,12 +168,7 @@ export class BossChatApp {
158
168
  } catch (error) {
159
169
  this.logger.log(`页面就绪检查告警:${error?.message || error},将继续执行预热恢复流程。`);
160
170
  }
161
- await this.page.selectJob(profile.jobSelection);
162
-
163
- const filterResult =
164
- startFrom === 'all'
165
- ? await this.page.activateAllFilter()
166
- : await this.page.activateUnreadFilter();
171
+ const filterResult = await this.restoreListContext(profile);
167
172
  await this.interaction.sleepRange(420, 160);
168
173
  this.logger.log('预热步骤:准备点击首位人选初始化聊天容器...');
169
174
  let primedCustomer = null;
@@ -270,6 +275,16 @@ export class BossChatApp {
270
275
  `页面恢复:changed=${recover.changed} | href=${recover.href || 'unknown'},准备重新预热并继续。`,
271
276
  );
272
277
  await this.interaction.sleepRange(900, 220);
278
+ const recoveredFilterResult = await this.restoreListContext(profile);
279
+ this.logger.log(
280
+ `恢复后列表上下文:岗位=${profile.jobSelection?.label || profile.jobSelection?.value || '未知'};列表范围: ${filterLabel}${
281
+ recoveredFilterResult.changed
282
+ ? recoveredFilterResult.verified === false
283
+ ? '(已尝试切换,未验证 active)'
284
+ : '(已切换)'
285
+ : '(已在目标筛选)'
286
+ }${recoveredFilterResult?.activeLabel ? ` | active=${recoveredFilterResult.activeLabel}` : ''}`,
287
+ );
273
288
  const prime = await this.page.primeConversationByFirstCandidate();
274
289
  const candidate = prime?.candidate || {};
275
290
  const candidateBase = {
@@ -276,6 +276,63 @@ async function browserActivateFilterTab(label) {
276
276
  };
277
277
  }
278
278
 
279
+ async function browserActivatePrimaryChatLabel(label) {
280
+ const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
281
+ const isVisible = (node) => {
282
+ if (!(node instanceof HTMLElement)) return false;
283
+ const rect = node.getBoundingClientRect();
284
+ if (rect.width <= 2 || rect.height <= 2) return false;
285
+ const style = getComputedStyle(node);
286
+ return style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || '1') > 0.01;
287
+ };
288
+ const matchesLabel = (node) => {
289
+ const text = normalize(node?.textContent || '');
290
+ return text === label || text.startsWith(`${label}(`);
291
+ };
292
+ const hasActiveState = (node) =>
293
+ node instanceof HTMLElement &&
294
+ (
295
+ node.classList.contains('active') ||
296
+ node.classList.contains('selected') ||
297
+ node.getAttribute('aria-selected') === 'true' ||
298
+ node.getAttribute('data-active') === 'true'
299
+ );
300
+ const getLabels = () =>
301
+ Array.from(document.querySelectorAll('.label-list .chat-label-item, .chat-label-item'))
302
+ .filter((node) => node instanceof HTMLElement && isVisible(node));
303
+ const getActiveLabel = () => {
304
+ const activeNode = getLabels().find((node) => hasActiveState(node));
305
+ return normalize(activeNode?.textContent || '');
306
+ };
307
+
308
+ const labels = getLabels();
309
+ const candidates = labels.filter((node) => matchesLabel(node));
310
+ if (candidates.length === 0) {
311
+ return { ok: false, error: `PRIMARY_CHAT_LABEL_NOT_FOUND:${label}` };
312
+ }
313
+
314
+ const activeLabelBefore = getActiveLabel();
315
+ if (candidates.some((node) => hasActiveState(node)) || matchesLabel({ textContent: activeLabelBefore })) {
316
+ return { ok: true, changed: false, verified: true, activeLabel: activeLabelBefore || label };
317
+ }
318
+
319
+ candidates[0].click();
320
+ await new Promise((resolve) => window.setTimeout(resolve, 420));
321
+
322
+ const refreshedCandidates = getLabels().filter((node) => matchesLabel(node));
323
+ const activeLabelAfter = getActiveLabel();
324
+ const verified =
325
+ matchesLabel({ textContent: activeLabelAfter }) ||
326
+ refreshedCandidates.some((node) => hasActiveState(node));
327
+
328
+ return {
329
+ ok: true,
330
+ changed: true,
331
+ verified,
332
+ activeLabel: activeLabelAfter || '',
333
+ };
334
+ }
335
+
279
336
  function browserGetLoadedCustomers() {
280
337
  const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
281
338
  const isScrollable = (el) =>
@@ -2317,6 +2374,14 @@ export class BossChatPage {
2317
2374
  return result;
2318
2375
  }
2319
2376
 
2377
+ async activatePrimaryChatLabel(label = '全部') {
2378
+ const result = await this.chromeClient.callFunction(browserActivatePrimaryChatLabel, label);
2379
+ if (!result?.ok) {
2380
+ throw new Error(result?.error || `ACTIVATE_PRIMARY_CHAT_LABEL_FAILED:${label}`);
2381
+ }
2382
+ return result;
2383
+ }
2384
+
2320
2385
  async getLoadedCustomers() {
2321
2386
  let lastError = 'GET_LOADED_CUSTOMERS_FAILED';
2322
2387
  for (let attempt = 0; attempt < 6; attempt += 1) {