@reconcrap/boss-recommend-mcp 1.3.27 → 1.3.28

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.27",
3
+ "version": "1.3.28",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -1089,6 +1089,16 @@ async function testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime() {
1089
1089
  artifactRootDir: os.tmpdir(),
1090
1090
  resumeOpenCooldownMs: 0,
1091
1091
  });
1092
+ app.waitForCandidateList = async ({ reason } = {}) => {
1093
+ calls.push(`waitForCandidateList:${reason || "unknown"}`);
1094
+ return {
1095
+ ready: true,
1096
+ waitedMs: 0,
1097
+ attempts: 1,
1098
+ listItemCount: 1,
1099
+ lastError: "",
1100
+ };
1101
+ };
1092
1102
  app.processCustomer = async (_customer, _profile, _runId, options = {}) => {
1093
1103
  calls.push(`processCustomer:${options.skipCardClick === true ? "skip" : "click"}`);
1094
1104
  return {
@@ -1110,13 +1120,13 @@ async function testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime() {
1110
1120
  llm: { model: "gpt-test" },
1111
1121
  });
1112
1122
 
1113
- assert.deepEqual(calls.slice(0, 5), [
1123
+ assert.deepEqual(calls.slice(0, 4), [
1114
1124
  "ensureReady",
1115
1125
  "activatePrimaryChatLabel:全部",
1116
1126
  "selectJob:算法工程师",
1117
1127
  "activateUnreadFilter",
1118
- "primeConversationByFirstCandidate:1",
1119
1128
  ]);
1129
+ assert.equal(calls.includes("primeConversationByFirstCandidate:1"), true);
1120
1130
  assert.equal(calls.includes("processCustomer:skip"), true);
1121
1131
  assert.equal(summary.inspected, 1);
1122
1132
  assert.equal(summary.skipped, 1);
@@ -1209,6 +1219,22 @@ async function testBossChatAppShouldRestoreListContextAfterRecovery() {
1209
1219
  artifactRootDir: os.tmpdir(),
1210
1220
  resumeOpenCooldownMs: 0,
1211
1221
  });
1222
+ app.waitForCandidateList = async ({ reason } = {}) => {
1223
+ calls.push(`waitForCandidateList:${reason || "unknown"}`);
1224
+ return {
1225
+ ready:
1226
+ reason === "initial-context-restore" ||
1227
+ reason === "post-recovery-context-restore",
1228
+ waitedMs: 0,
1229
+ attempts: 1,
1230
+ listItemCount:
1231
+ reason === "initial-context-restore" ||
1232
+ reason === "post-recovery-context-restore"
1233
+ ? 1
1234
+ : 0,
1235
+ lastError: "",
1236
+ };
1237
+ };
1212
1238
  app.processCustomer = async (_customer, _profile, _runId, options = {}) => {
1213
1239
  calls.push(`processCustomer:${options.skipCardClick === true ? "skip" : "click"}`);
1214
1240
  return {
@@ -1236,12 +1262,127 @@ async function testBossChatAppShouldRestoreListContextAfterRecovery() {
1236
1262
  assert.equal(calls[recoverIndex + 1], "activatePrimaryChatLabel:全部");
1237
1263
  assert.equal(calls[recoverIndex + 2], "selectJob:算法工程师");
1238
1264
  assert.equal(calls[recoverIndex + 3], "activateUnreadFilter");
1239
- assert.equal(calls[recoverIndex + 4], "primeConversationByFirstCandidate:2");
1265
+ assert.equal(calls[recoverIndex + 4], "waitForCandidateList:post-recovery-context-restore");
1266
+ assert.equal(calls[recoverIndex + 5], "primeConversationByFirstCandidate:2");
1240
1267
  assert.equal(calls.includes("processCustomer:skip"), true);
1241
1268
  assert.equal(summary.inspected, 1);
1242
1269
  assert.equal(summary.skipped, 1);
1243
1270
  }
1244
1271
 
1272
+ async function testBossChatAppShouldWaitForCandidateListBeforePriming() {
1273
+ const calls = [];
1274
+ let pageStateCall = 0;
1275
+ const page = {
1276
+ async ensureReady() {
1277
+ calls.push("ensureReady");
1278
+ return { hasListContainer: false, listItemCount: 0 };
1279
+ },
1280
+ async activatePrimaryChatLabel(label) {
1281
+ calls.push(`activatePrimaryChatLabel:${label}`);
1282
+ return { changed: false, verified: true, activeLabel: label };
1283
+ },
1284
+ async selectJob(jobSelection) {
1285
+ calls.push(`selectJob:${jobSelection.label}`);
1286
+ return jobSelection;
1287
+ },
1288
+ async activateUnreadFilter() {
1289
+ calls.push("activateUnreadFilter");
1290
+ return { changed: true, verified: true, activeLabel: "未读" };
1291
+ },
1292
+ async getPageState() {
1293
+ pageStateCall += 1;
1294
+ calls.push(`getPageState:${pageStateCall}`);
1295
+ return {
1296
+ href: "https://www.zhipin.com/web/chat/index",
1297
+ readyState: "complete",
1298
+ hasListContainer: pageStateCall >= 3,
1299
+ listItemCount: pageStateCall >= 3 ? 2 : 0,
1300
+ };
1301
+ },
1302
+ async primeConversationByFirstCandidate() {
1303
+ calls.push("primeConversationByFirstCandidate:1");
1304
+ return {
1305
+ candidate: {
1306
+ customerId: "1003",
1307
+ name: "候选人C",
1308
+ sourceJob: "算法工程师",
1309
+ domIndex: 0,
1310
+ },
1311
+ totalVisibleCandidates: 2,
1312
+ readyState: {
1313
+ hasOnlineResume: true,
1314
+ hasAskResume: true,
1315
+ hasAttachmentResume: false,
1316
+ },
1317
+ };
1318
+ },
1319
+ async getLoadedCustomers() {
1320
+ calls.push("getLoadedCustomers:1");
1321
+ return [];
1322
+ },
1323
+ async closeResumeModalDomOnce() {
1324
+ return {
1325
+ closed: true,
1326
+ method: "already-closed",
1327
+ finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
1328
+ };
1329
+ },
1330
+ };
1331
+ const stateStore = {
1332
+ async load() {},
1333
+ hasAny() {
1334
+ return false;
1335
+ },
1336
+ async record() {},
1337
+ };
1338
+ const app = new BossChatApp({
1339
+ page,
1340
+ llmClient: {},
1341
+ interaction: {
1342
+ async sleepRange() {},
1343
+ async maybeRest() {},
1344
+ },
1345
+ resumeCaptureService: {},
1346
+ stateStore,
1347
+ reportStore: {
1348
+ async write() {
1349
+ return "report.json";
1350
+ },
1351
+ },
1352
+ logger: { log() {} },
1353
+ dryRun: true,
1354
+ artifactRootDir: os.tmpdir(),
1355
+ resumeOpenCooldownMs: 0,
1356
+ });
1357
+ app.processCustomer = async (_customer, _profile, _runId, options = {}) => {
1358
+ calls.push(`processCustomer:${options.skipCardClick === true ? "skip" : "click"}`);
1359
+ return {
1360
+ name: "候选人C",
1361
+ passed: false,
1362
+ requested: false,
1363
+ reason: "skip",
1364
+ error: "",
1365
+ artifacts: {},
1366
+ };
1367
+ };
1368
+
1369
+ const summary = await app.run({
1370
+ screeningCriteria: "有 AI 项目经验",
1371
+ targetCount: 1,
1372
+ startFrom: "unread",
1373
+ jobSelection: { label: "算法工程师", value: "job-1" },
1374
+ chrome: { port: 9222 },
1375
+ llm: { model: "gpt-test" },
1376
+ });
1377
+
1378
+ const primeIndex = calls.indexOf("primeConversationByFirstCandidate:1");
1379
+ const thirdStateIndex = calls.indexOf("getPageState:3");
1380
+ assert.equal(thirdStateIndex >= 0, true);
1381
+ assert.equal(primeIndex > thirdStateIndex, true);
1382
+ assert.equal(summary.inspected, 1);
1383
+ assert.equal(summary.skipped, 1);
1384
+ }
1385
+
1245
1386
  async function testBossChatAppShouldPersistEvidenceArtifacts() {
1246
1387
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-artifacts-"));
1247
1388
  await mkdir(tempDir, { recursive: true });
@@ -1379,6 +1520,7 @@ async function main() {
1379
1520
  await testBossChatLlmShouldApplyThinkingDefaultsAndOverrides();
1380
1521
  await testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime();
1381
1522
  await testBossChatAppShouldRestoreListContextAfterRecovery();
1523
+ await testBossChatAppShouldWaitForCandidateListBeforePriming();
1382
1524
  await testBossChatAppShouldPersistEvidenceArtifacts();
1383
1525
  console.log("boss-chat tests passed");
1384
1526
  }
@@ -64,6 +64,9 @@ function hasResumeRequestSentMessage(state = {}) {
64
64
  return recent.some((item) => normalizeText(item).includes('简历请求已发送'));
65
65
  }
66
66
 
67
+ const CANDIDATE_LIST_WAIT_AFTER_CONTEXT_MS = 5000;
68
+ const CANDIDATE_LIST_WAIT_POLL_MS = 500;
69
+
67
70
  export class BossChatApp {
68
71
  constructor({
69
72
  page,
@@ -152,6 +155,65 @@ export class BossChatApp {
152
155
  : this.page.activateUnreadFilter();
153
156
  }
154
157
 
158
+ async waitForCandidateList({
159
+ reason = 'unknown',
160
+ maxWaitMs = CANDIDATE_LIST_WAIT_AFTER_CONTEXT_MS,
161
+ pollMs = CANDIDATE_LIST_WAIT_POLL_MS,
162
+ } = {}) {
163
+ const startedAt = Date.now();
164
+ let attempts = 0;
165
+ let lastState = null;
166
+ let lastError = '';
167
+
168
+ while (Date.now() - startedAt <= maxWaitMs) {
169
+ attempts += 1;
170
+ try {
171
+ if (typeof this.page.getPageState === 'function') {
172
+ lastState = await this.page.getPageState();
173
+ if (Number(lastState?.listItemCount || 0) > 0) {
174
+ return {
175
+ ready: true,
176
+ waitedMs: Date.now() - startedAt,
177
+ attempts,
178
+ listItemCount: Number(lastState?.listItemCount || 0),
179
+ lastState,
180
+ lastError,
181
+ };
182
+ }
183
+ } else if (typeof this.page.getLoadedCustomers === 'function') {
184
+ const customers = await this.page.getLoadedCustomers();
185
+ if (Array.isArray(customers) && customers.length > 0) {
186
+ return {
187
+ ready: true,
188
+ waitedMs: Date.now() - startedAt,
189
+ attempts,
190
+ listItemCount: customers.length,
191
+ lastState,
192
+ lastError,
193
+ };
194
+ }
195
+ }
196
+ } catch (error) {
197
+ lastError = String(error?.message || error || '');
198
+ }
199
+
200
+ if (Date.now() - startedAt >= maxWaitMs) {
201
+ break;
202
+ }
203
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
204
+ }
205
+
206
+ return {
207
+ ready: false,
208
+ waitedMs: Date.now() - startedAt,
209
+ attempts,
210
+ listItemCount: Number(lastState?.listItemCount || 0),
211
+ lastState,
212
+ lastError,
213
+ reason,
214
+ };
215
+ }
216
+
155
217
  async run(profile) {
156
218
  const startedAt = new Date().toISOString();
157
219
  const runId = runToken(new Date());
@@ -168,8 +230,34 @@ export class BossChatApp {
168
230
  } catch (error) {
169
231
  this.logger.log(`页面就绪检查告警:${error?.message || error},将继续执行预热恢复流程。`);
170
232
  }
171
- const filterResult = await this.restoreListContext(profile);
233
+ let filterResult = await this.restoreListContext(profile);
172
234
  await this.interaction.sleepRange(420, 160);
235
+ let initialListWait = await this.waitForCandidateList({
236
+ reason: 'initial-context-restore',
237
+ });
238
+ if (initialListWait.ready) {
239
+ this.logger.log(
240
+ `候选人列表已就绪:reason=initial-context-restore | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount}`,
241
+ );
242
+ } else {
243
+ this.logger.log(
244
+ `候选人列表等待超时:reason=initial-context-restore | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount} | lastError=${initialListWait.lastError || 'n/a'},继续尝试预热。`,
245
+ );
246
+ filterResult = await this.restoreListContext(profile);
247
+ await this.interaction.sleepRange(420, 160);
248
+ initialListWait = await this.waitForCandidateList({
249
+ reason: 'initial-context-restore-reapply',
250
+ });
251
+ if (initialListWait.ready) {
252
+ this.logger.log(
253
+ `候选人列表二次恢复成功:reason=initial-context-restore-reapply | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount}`,
254
+ );
255
+ } else {
256
+ this.logger.log(
257
+ `候选人列表二次等待仍超时:reason=initial-context-restore-reapply | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount} | lastError=${initialListWait.lastError || 'n/a'},继续尝试预热。`,
258
+ );
259
+ }
260
+ }
173
261
  this.logger.log('预热步骤:准备点击首位人选初始化聊天容器...');
174
262
  let primedCustomer = null;
175
263
 
@@ -269,13 +357,22 @@ export class BossChatApp {
269
357
  message,
270
358
  )
271
359
  ) {
360
+ const delayedListWait = await this.waitForCandidateList({
361
+ reason: `main-loop:${message}`,
362
+ });
363
+ if (delayedListWait.ready) {
364
+ this.logger.log(
365
+ `候选人列表延迟恢复成功:reason=main-loop:${message} | waited=${delayedListWait.waitedMs}ms | attempts=${delayedListWait.attempts} | count=${delayedListWait.listItemCount},继续重试扫描。`,
366
+ );
367
+ continue;
368
+ }
272
369
  try {
273
370
  const recover = await this.page.recoverToChatIndex();
274
371
  this.logger.log(
275
372
  `页面恢复:changed=${recover.changed} | href=${recover.href || 'unknown'},准备重新预热并继续。`,
276
373
  );
277
374
  await this.interaction.sleepRange(900, 220);
278
- const recoveredFilterResult = await this.restoreListContext(profile);
375
+ let recoveredFilterResult = await this.restoreListContext(profile);
279
376
  this.logger.log(
280
377
  `恢复后列表上下文:岗位=${profile.jobSelection?.label || profile.jobSelection?.value || '未知'};列表范围: ${filterLabel}${
281
378
  recoveredFilterResult.changed
@@ -285,6 +382,41 @@ export class BossChatApp {
285
382
  : '(已在目标筛选)'
286
383
  }${recoveredFilterResult?.activeLabel ? ` | active=${recoveredFilterResult.activeLabel}` : ''}`,
287
384
  );
385
+ let recoveredListWait = await this.waitForCandidateList({
386
+ reason: 'post-recovery-context-restore',
387
+ });
388
+ if (recoveredListWait.ready) {
389
+ this.logger.log(
390
+ `恢复后候选人列表已就绪:reason=post-recovery-context-restore | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount}`,
391
+ );
392
+ } else {
393
+ this.logger.log(
394
+ `恢复后候选人列表等待超时:reason=post-recovery-context-restore | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount} | lastError=${recoveredListWait.lastError || 'n/a'},继续尝试预热。`,
395
+ );
396
+ recoveredFilterResult = await this.restoreListContext(profile);
397
+ this.logger.log(
398
+ `恢复后二次应用列表上下文:岗位=${profile.jobSelection?.label || profile.jobSelection?.value || '未知'};列表范围: ${filterLabel}${
399
+ recoveredFilterResult.changed
400
+ ? recoveredFilterResult.verified === false
401
+ ? '(已尝试切换,未验证 active)'
402
+ : '(已切换)'
403
+ : '(已在目标筛选)'
404
+ }${recoveredFilterResult?.activeLabel ? ` | active=${recoveredFilterResult.activeLabel}` : ''}`,
405
+ );
406
+ await this.interaction.sleepRange(420, 160);
407
+ recoveredListWait = await this.waitForCandidateList({
408
+ reason: 'post-recovery-context-restore-reapply',
409
+ });
410
+ if (recoveredListWait.ready) {
411
+ this.logger.log(
412
+ `恢复后二次候选人列表恢复成功:reason=post-recovery-context-restore-reapply | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount}`,
413
+ );
414
+ } else {
415
+ this.logger.log(
416
+ `恢复后二次候选人列表等待仍超时:reason=post-recovery-context-restore-reapply | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount} | lastError=${recoveredListWait.lastError || 'n/a'},继续尝试预热。`,
417
+ );
418
+ }
419
+ }
288
420
  const prime = await this.page.primeConversationByFirstCandidate();
289
421
  const candidate = prime?.candidate || {};
290
422
  const candidateBase = {