@reconcrap/boss-recommend-mcp 1.3.28 → 1.3.29
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
package/src/test-boss-chat.js
CHANGED
|
@@ -484,6 +484,106 @@ async function testBossChatRecoverToChatIndexShouldForceNavigateAndWaitForComple
|
|
|
484
484
|
);
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
async function testBossChatPageShouldFallbackToEscapeWhenClosingCandidateDetail() {
|
|
488
|
+
const calls = [];
|
|
489
|
+
const mouseEvents = [];
|
|
490
|
+
let stateIndex = 0;
|
|
491
|
+
const states = [
|
|
492
|
+
{
|
|
493
|
+
open: true,
|
|
494
|
+
panelCount: 1,
|
|
495
|
+
closeCount: 1,
|
|
496
|
+
topPanelClass: "base-info-single-top-detail",
|
|
497
|
+
topPanelScore: 520,
|
|
498
|
+
panelRect: {
|
|
499
|
+
left: 940,
|
|
500
|
+
top: 60,
|
|
501
|
+
width: 360,
|
|
502
|
+
height: 720,
|
|
503
|
+
right: 1300,
|
|
504
|
+
bottom: 780
|
|
505
|
+
},
|
|
506
|
+
closeRect: {
|
|
507
|
+
left: 1274,
|
|
508
|
+
top: 12,
|
|
509
|
+
width: 30,
|
|
510
|
+
height: 30,
|
|
511
|
+
right: 1304,
|
|
512
|
+
bottom: 42
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
open: true,
|
|
517
|
+
panelCount: 1,
|
|
518
|
+
closeCount: 1,
|
|
519
|
+
topPanelClass: "base-info-single-top-detail",
|
|
520
|
+
topPanelScore: 520,
|
|
521
|
+
panelRect: {
|
|
522
|
+
left: 940,
|
|
523
|
+
top: 60,
|
|
524
|
+
width: 360,
|
|
525
|
+
height: 720,
|
|
526
|
+
right: 1300,
|
|
527
|
+
bottom: 780
|
|
528
|
+
},
|
|
529
|
+
closeRect: {
|
|
530
|
+
left: 1274,
|
|
531
|
+
top: 12,
|
|
532
|
+
width: 30,
|
|
533
|
+
height: 30,
|
|
534
|
+
right: 1304,
|
|
535
|
+
bottom: 42
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
open: false,
|
|
540
|
+
panelCount: 0,
|
|
541
|
+
closeCount: 0,
|
|
542
|
+
topPanelClass: "",
|
|
543
|
+
topPanelScore: 0,
|
|
544
|
+
panelRect: null,
|
|
545
|
+
closeRect: null
|
|
546
|
+
}
|
|
547
|
+
];
|
|
548
|
+
|
|
549
|
+
const fakeChromeClient = {
|
|
550
|
+
Input: {
|
|
551
|
+
async dispatchMouseEvent(payload) {
|
|
552
|
+
mouseEvents.push(payload);
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
async pressEscape() {
|
|
556
|
+
calls.push("pressEscape");
|
|
557
|
+
},
|
|
558
|
+
async callFunction(fn) {
|
|
559
|
+
calls.push(fn.name);
|
|
560
|
+
if (fn.name === "browserIsCandidateDetailOpen") {
|
|
561
|
+
const value = states[Math.min(stateIndex, states.length - 1)];
|
|
562
|
+
stateIndex += 1;
|
|
563
|
+
return value;
|
|
564
|
+
}
|
|
565
|
+
if (fn.name === "browserCloseCandidateDetailDomOnce") {
|
|
566
|
+
return {
|
|
567
|
+
ok: true,
|
|
568
|
+
selector: ".close-btn",
|
|
569
|
+
method: "dom-click-once"
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
throw new Error(`unexpected function: ${fn.name}`);
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const page = new BossChatPage(fakeChromeClient);
|
|
577
|
+
const result = await page.closeCandidateDetail({
|
|
578
|
+
maxAttempts: 1,
|
|
579
|
+
ensureDismiss: true
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
assert.equal(result.closed, true);
|
|
583
|
+
assert.equal(calls.includes("pressEscape"), true);
|
|
584
|
+
assert.equal(mouseEvents.length > 0, true);
|
|
585
|
+
}
|
|
586
|
+
|
|
487
587
|
async function testBossChatMcpToolsShouldValidateAndRoute() {
|
|
488
588
|
await withBossChatWorkspace(async (workspaceRoot) => {
|
|
489
589
|
const toolsResponse = await handleRequest({
|
|
@@ -1132,6 +1232,122 @@ async function testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime() {
|
|
|
1132
1232
|
assert.equal(summary.skipped, 1);
|
|
1133
1233
|
}
|
|
1134
1234
|
|
|
1235
|
+
async function testBossChatAppShouldCloseCandidateDetailDuringRunCleanup() {
|
|
1236
|
+
const calls = [];
|
|
1237
|
+
const page = {
|
|
1238
|
+
async ensureReady() {
|
|
1239
|
+
calls.push("ensureReady");
|
|
1240
|
+
return { hasListContainer: true, listItemCount: 1 };
|
|
1241
|
+
},
|
|
1242
|
+
async activatePrimaryChatLabel(label) {
|
|
1243
|
+
calls.push(`activatePrimaryChatLabel:${label}`);
|
|
1244
|
+
return { changed: false, verified: true, activeLabel: label };
|
|
1245
|
+
},
|
|
1246
|
+
async selectJob(jobSelection) {
|
|
1247
|
+
calls.push(`selectJob:${jobSelection.label}`);
|
|
1248
|
+
return jobSelection;
|
|
1249
|
+
},
|
|
1250
|
+
async activateUnreadFilter() {
|
|
1251
|
+
calls.push("activateUnreadFilter");
|
|
1252
|
+
return { changed: false, verified: true, activeLabel: "未读" };
|
|
1253
|
+
},
|
|
1254
|
+
async primeConversationByFirstCandidate() {
|
|
1255
|
+
calls.push("primeConversationByFirstCandidate:1");
|
|
1256
|
+
return {
|
|
1257
|
+
candidate: {
|
|
1258
|
+
customerId: "1008",
|
|
1259
|
+
name: "候选人清理",
|
|
1260
|
+
sourceJob: "算法工程师",
|
|
1261
|
+
domIndex: 0
|
|
1262
|
+
},
|
|
1263
|
+
totalVisibleCandidates: 1,
|
|
1264
|
+
readyState: {
|
|
1265
|
+
hasOnlineResume: true,
|
|
1266
|
+
hasAskResume: true,
|
|
1267
|
+
hasAttachmentResume: false
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
},
|
|
1271
|
+
async getLoadedCustomers() {
|
|
1272
|
+
calls.push("getLoadedCustomers:1");
|
|
1273
|
+
return [];
|
|
1274
|
+
},
|
|
1275
|
+
async closeResumeModalDomOnce() {
|
|
1276
|
+
calls.push("closeResumeModalDomOnce");
|
|
1277
|
+
return {
|
|
1278
|
+
closed: true,
|
|
1279
|
+
method: "already-closed",
|
|
1280
|
+
finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" }
|
|
1281
|
+
};
|
|
1282
|
+
},
|
|
1283
|
+
async closeCandidateDetailDomOnce() {
|
|
1284
|
+
calls.push("closeCandidateDetailDomOnce");
|
|
1285
|
+
return {
|
|
1286
|
+
closed: true,
|
|
1287
|
+
method: "dom-close-once:.close-btn",
|
|
1288
|
+
finalState: { panelCount: 0, closeCount: 0, topPanelClass: "" }
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
const stateStore = {
|
|
1293
|
+
async load() {},
|
|
1294
|
+
hasAny() {
|
|
1295
|
+
return false;
|
|
1296
|
+
},
|
|
1297
|
+
async record() {}
|
|
1298
|
+
};
|
|
1299
|
+
const app = new BossChatApp({
|
|
1300
|
+
page,
|
|
1301
|
+
llmClient: {},
|
|
1302
|
+
interaction: {
|
|
1303
|
+
async sleepRange() {},
|
|
1304
|
+
async maybeRest() {}
|
|
1305
|
+
},
|
|
1306
|
+
resumeCaptureService: {},
|
|
1307
|
+
stateStore,
|
|
1308
|
+
reportStore: {
|
|
1309
|
+
async write() {
|
|
1310
|
+
return "report.json";
|
|
1311
|
+
}
|
|
1312
|
+
},
|
|
1313
|
+
logger: { log() {} },
|
|
1314
|
+
dryRun: true,
|
|
1315
|
+
artifactRootDir: os.tmpdir(),
|
|
1316
|
+
resumeOpenCooldownMs: 0
|
|
1317
|
+
});
|
|
1318
|
+
app.waitForCandidateList = async ({ reason } = {}) => {
|
|
1319
|
+
calls.push(`waitForCandidateList:${reason || "unknown"}`);
|
|
1320
|
+
return {
|
|
1321
|
+
ready: true,
|
|
1322
|
+
waitedMs: 0,
|
|
1323
|
+
attempts: 1,
|
|
1324
|
+
listItemCount: 1,
|
|
1325
|
+
lastError: ""
|
|
1326
|
+
};
|
|
1327
|
+
};
|
|
1328
|
+
app.processCustomer = async () => ({
|
|
1329
|
+
name: "候选人清理",
|
|
1330
|
+
passed: false,
|
|
1331
|
+
requested: false,
|
|
1332
|
+
reason: "skip",
|
|
1333
|
+
error: "",
|
|
1334
|
+
artifacts: {}
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
const summary = await app.run({
|
|
1338
|
+
screeningCriteria: "有 AI 项目经验",
|
|
1339
|
+
targetCount: 1,
|
|
1340
|
+
startFrom: "unread",
|
|
1341
|
+
jobSelection: { label: "算法工程师", value: "job-1" },
|
|
1342
|
+
chrome: { port: 9222 },
|
|
1343
|
+
llm: { model: "gpt-test" }
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
assert.equal(summary.inspected, 1);
|
|
1347
|
+
assert.equal(calls.includes("closeCandidateDetailDomOnce"), true);
|
|
1348
|
+
assert.equal(calls.lastIndexOf("closeCandidateDetailDomOnce") > calls.indexOf("getLoadedCustomers:1"), true);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1135
1351
|
async function testBossChatAppShouldRestoreListContextAfterRecovery() {
|
|
1136
1352
|
const calls = [];
|
|
1137
1353
|
let primeCount = 0;
|
|
@@ -1509,6 +1725,7 @@ async function main() {
|
|
|
1509
1725
|
await testBossChatPrepareShouldRetryWhenChatPageIsNotReady();
|
|
1510
1726
|
await testBossChatPageShouldTreatBlankChatShellAsOnChatPage();
|
|
1511
1727
|
await testBossChatRecoverToChatIndexShouldForceNavigateAndWaitForCompleteLoad();
|
|
1728
|
+
await testBossChatPageShouldFallbackToEscapeWhenClosingCandidateDetail();
|
|
1512
1729
|
await testBossChatMcpToolsShouldValidateAndRoute();
|
|
1513
1730
|
await testBossChatCliShouldSupportRunAndFollowUpParsing();
|
|
1514
1731
|
await testVendorBossChatCliShouldWaitForHydratedChatShell();
|
|
@@ -1519,6 +1736,7 @@ async function main() {
|
|
|
1519
1736
|
await testBossChatLlmTextChunkFallbackShouldWork();
|
|
1520
1737
|
await testBossChatLlmShouldApplyThinkingDefaultsAndOverrides();
|
|
1521
1738
|
await testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime();
|
|
1739
|
+
await testBossChatAppShouldCloseCandidateDetailDuringRunCleanup();
|
|
1522
1740
|
await testBossChatAppShouldRestoreListContextAfterRecovery();
|
|
1523
1741
|
await testBossChatAppShouldWaitForCandidateListBeforePriming();
|
|
1524
1742
|
await testBossChatAppShouldPersistEvidenceArtifacts();
|
|
@@ -214,6 +214,46 @@ export class BossChatApp {
|
|
|
214
214
|
};
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
async cleanupPanels({
|
|
218
|
+
resumeMaxAttempts = 6,
|
|
219
|
+
detailMaxAttempts = 4,
|
|
220
|
+
ensureDismiss = true,
|
|
221
|
+
} = {}) {
|
|
222
|
+
const resume =
|
|
223
|
+
typeof this.page.closeResumeModalDomOnce === 'function'
|
|
224
|
+
? await this.page.closeResumeModalDomOnce()
|
|
225
|
+
: await this.page.closeResumeModal({
|
|
226
|
+
maxAttempts: resumeMaxAttempts,
|
|
227
|
+
ensureDismiss,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
let detail = {
|
|
231
|
+
closed: true,
|
|
232
|
+
method: 'unsupported',
|
|
233
|
+
finalState: {
|
|
234
|
+
panelCount: 0,
|
|
235
|
+
closeCount: 0,
|
|
236
|
+
topPanelClass: '',
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
if (typeof this.page.closeCandidateDetailDomOnce === 'function') {
|
|
240
|
+
detail = await this.page.closeCandidateDetailDomOnce();
|
|
241
|
+
if (!detail.closed && typeof this.page.closeCandidateDetail === 'function') {
|
|
242
|
+
detail = await this.page.closeCandidateDetail({
|
|
243
|
+
maxAttempts: detailMaxAttempts,
|
|
244
|
+
ensureDismiss,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
} else if (typeof this.page.closeCandidateDetail === 'function') {
|
|
248
|
+
detail = await this.page.closeCandidateDetail({
|
|
249
|
+
maxAttempts: detailMaxAttempts,
|
|
250
|
+
ensureDismiss,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { resume, detail };
|
|
255
|
+
}
|
|
256
|
+
|
|
217
257
|
async run(profile) {
|
|
218
258
|
const startedAt = new Date().toISOString();
|
|
219
259
|
const runId = runToken(new Date());
|
|
@@ -576,12 +616,13 @@ export class BossChatApp {
|
|
|
576
616
|
}
|
|
577
617
|
|
|
578
618
|
try {
|
|
579
|
-
const finalClose =
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
619
|
+
const finalClose = await this.cleanupPanels({
|
|
620
|
+
resumeMaxAttempts: 6,
|
|
621
|
+
detailMaxAttempts: 4,
|
|
622
|
+
ensureDismiss: true,
|
|
623
|
+
});
|
|
583
624
|
this.logger.log(
|
|
584
|
-
|
|
625
|
+
`运行收尾关闭弹层:resumeClosed=${finalClose.resume.closed} | resumeMethod=${finalClose.resume.method} | detailClosed=${finalClose.detail.closed} | detailMethod=${finalClose.detail.method}`,
|
|
585
626
|
);
|
|
586
627
|
} catch (cleanupError) {
|
|
587
628
|
this.logger.log(`运行收尾清理告警:${cleanupError?.message || cleanupError}`);
|
|
@@ -614,13 +655,17 @@ export class BossChatApp {
|
|
|
614
655
|
let modalOpened = false;
|
|
615
656
|
try {
|
|
616
657
|
this.logger.log(`候选人开始:${customer.name || '未知'} (${customer.customerKey})`);
|
|
617
|
-
const preClose =
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
658
|
+
const preClose = await this.cleanupPanels({
|
|
659
|
+
resumeMaxAttempts: 4,
|
|
660
|
+
detailMaxAttempts: 3,
|
|
661
|
+
ensureDismiss: true,
|
|
662
|
+
});
|
|
663
|
+
if (
|
|
664
|
+
preClose.resume.method !== 'already-closed' ||
|
|
665
|
+
preClose.detail.method !== 'already-closed'
|
|
666
|
+
) {
|
|
622
667
|
this.logger.log(
|
|
623
|
-
|
|
668
|
+
`候选人开始前清理残留面板:resumeClosed=${preClose.resume.closed} | resumeMethod=${preClose.resume.method} | detailClosed=${preClose.detail.closed} | detailMethod=${preClose.detail.method}`,
|
|
624
669
|
);
|
|
625
670
|
}
|
|
626
671
|
if (!skipCardClick) {
|
|
@@ -980,6 +1025,24 @@ export class BossChatApp {
|
|
|
980
1025
|
}
|
|
981
1026
|
}
|
|
982
1027
|
|
|
1028
|
+
const finalPanels = await this.cleanupPanels({
|
|
1029
|
+
resumeMaxAttempts: 4,
|
|
1030
|
+
detailMaxAttempts: 4,
|
|
1031
|
+
ensureDismiss: true,
|
|
1032
|
+
});
|
|
1033
|
+
baseResult.artifacts.finalResumeCloseMethod = finalPanels.resume.method;
|
|
1034
|
+
baseResult.artifacts.finalResumeClosed = finalPanels.resume.closed;
|
|
1035
|
+
baseResult.artifacts.finalDetailCloseMethod = finalPanels.detail.method;
|
|
1036
|
+
baseResult.artifacts.finalDetailClosed = finalPanels.detail.closed;
|
|
1037
|
+
if (
|
|
1038
|
+
finalPanels.resume.method !== 'already-closed' ||
|
|
1039
|
+
finalPanels.detail.method !== 'already-closed'
|
|
1040
|
+
) {
|
|
1041
|
+
this.logger.log(
|
|
1042
|
+
`候选人收尾清理:resumeClosed=${finalPanels.resume.closed} | resumeMethod=${finalPanels.resume.method} | detailClosed=${finalPanels.detail.closed} | detailMethod=${finalPanels.detail.method}`,
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
983
1046
|
await this.stateStore.record(baseResult.customerKey, baseResult, baseAliases);
|
|
984
1047
|
return baseResult;
|
|
985
1048
|
} catch (error) {
|
|
@@ -987,16 +1050,19 @@ export class BossChatApp {
|
|
|
987
1050
|
throw error;
|
|
988
1051
|
}
|
|
989
1052
|
|
|
990
|
-
if (modalOpened) {
|
|
1053
|
+
if (modalOpened || typeof this.page.closeCandidateDetailDomOnce === 'function' || typeof this.page.closeCandidateDetail === 'function') {
|
|
991
1054
|
try {
|
|
992
|
-
const closeResult =
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
baseResult.artifacts.
|
|
1055
|
+
const closeResult = await this.cleanupPanels({
|
|
1056
|
+
resumeMaxAttempts: 6,
|
|
1057
|
+
detailMaxAttempts: 4,
|
|
1058
|
+
ensureDismiss: true,
|
|
1059
|
+
});
|
|
1060
|
+
baseResult.artifacts.resumeCloseMethod = closeResult.resume.method;
|
|
1061
|
+
baseResult.artifacts.resumeClosed = closeResult.resume.closed;
|
|
1062
|
+
baseResult.artifacts.finalDetailCloseMethod = closeResult.detail.method;
|
|
1063
|
+
baseResult.artifacts.finalDetailClosed = closeResult.detail.closed;
|
|
998
1064
|
this.logger.log(
|
|
999
|
-
|
|
1065
|
+
`异常后关闭面板结果:resumeClosed=${closeResult.resume.closed} | resumeMethod=${closeResult.resume.method} | resumeScope=${closeResult?.resume?.finalState?.scopeCount ?? 'n/a'} | resumeIframe=${closeResult?.resume?.finalState?.iframeCount ?? 'n/a'} | resumeClose=${closeResult?.resume?.finalState?.closeCount ?? 'n/a'} | resumeClass=${closeResult?.resume?.finalState?.topScopeClass || 'n/a'} | detailClosed=${closeResult.detail.closed} | detailMethod=${closeResult.detail.method} | detailPanels=${closeResult?.detail?.finalState?.panelCount ?? 'n/a'} | detailClose=${closeResult?.detail?.finalState?.closeCount ?? 'n/a'} | detailClass=${closeResult?.detail?.finalState?.topPanelClass || 'n/a'}`,
|
|
1000
1066
|
);
|
|
1001
1067
|
} catch {}
|
|
1002
1068
|
}
|
|
@@ -1011,6 +1011,272 @@ function browserOpenOnlineResume(options = {}) {
|
|
|
1011
1011
|
};
|
|
1012
1012
|
}
|
|
1013
1013
|
|
|
1014
|
+
function browserIsCandidateDetailOpen() {
|
|
1015
|
+
const collectSnapshot = () => {
|
|
1016
|
+
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
1017
|
+
const isVisible = (el) => {
|
|
1018
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
1019
|
+
const style = getComputedStyle(el);
|
|
1020
|
+
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
const rect = el.getBoundingClientRect();
|
|
1024
|
+
return rect.width > 2 && rect.height > 2;
|
|
1025
|
+
};
|
|
1026
|
+
const rectToJson = (rect) => ({
|
|
1027
|
+
left: rect.left,
|
|
1028
|
+
top: rect.top,
|
|
1029
|
+
width: rect.width,
|
|
1030
|
+
height: rect.height,
|
|
1031
|
+
right: rect.right,
|
|
1032
|
+
bottom: rect.bottom,
|
|
1033
|
+
});
|
|
1034
|
+
const panelSelectors = [
|
|
1035
|
+
'.base-info-single-top-detail',
|
|
1036
|
+
'.resume-detail-wrap',
|
|
1037
|
+
'.geek-card-detail',
|
|
1038
|
+
'.candidate-detail-wrap',
|
|
1039
|
+
'.chat-detail-wrap',
|
|
1040
|
+
];
|
|
1041
|
+
const closeButtons = Array.from(document.querySelectorAll('.close-btn')).filter(isVisible);
|
|
1042
|
+
const panelEntries = [];
|
|
1043
|
+
const seen = new Set();
|
|
1044
|
+
const pushPanel = (node, source) => {
|
|
1045
|
+
if (!(node instanceof HTMLElement) || !isVisible(node)) return;
|
|
1046
|
+
const rect = node.getBoundingClientRect();
|
|
1047
|
+
if (rect.width < 240 || rect.height < 160) return;
|
|
1048
|
+
const key = `${Math.round(rect.left)}:${Math.round(rect.top)}:${Math.round(rect.width)}:${Math.round(rect.height)}:${normalize(node.className || '')}`;
|
|
1049
|
+
if (seen.has(key)) return;
|
|
1050
|
+
seen.add(key);
|
|
1051
|
+
panelEntries.push({ node, rect, source });
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
for (const selector of panelSelectors) {
|
|
1055
|
+
for (const node of Array.from(document.querySelectorAll(selector))) {
|
|
1056
|
+
pushPanel(node, `selector:${selector}`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
for (const closeButton of closeButtons) {
|
|
1061
|
+
let current = closeButton.parentElement;
|
|
1062
|
+
let depth = 0;
|
|
1063
|
+
while (current instanceof HTMLElement && depth < 10) {
|
|
1064
|
+
pushPanel(current, 'close-ancestor');
|
|
1065
|
+
current = current.parentElement;
|
|
1066
|
+
depth += 1;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const scoredPanels = panelEntries
|
|
1071
|
+
.map((entry) => {
|
|
1072
|
+
const classText = normalize(entry.node.className || '').toLowerCase();
|
|
1073
|
+
const text = normalize(entry.node.textContent || '').slice(0, 240).toLowerCase();
|
|
1074
|
+
const containsClose = closeButtons.some((button) => entry.node.contains(button));
|
|
1075
|
+
const anchoredRight =
|
|
1076
|
+
entry.rect.left >= window.innerWidth * 0.4 ||
|
|
1077
|
+
entry.rect.right >= window.innerWidth * 0.72;
|
|
1078
|
+
const hasKnownDetailClass =
|
|
1079
|
+
classText.includes('base-info-single-top-detail') ||
|
|
1080
|
+
classText.includes('resume-detail-wrap') ||
|
|
1081
|
+
classText.includes('candidate-detail') ||
|
|
1082
|
+
classText.includes('chat-detail') ||
|
|
1083
|
+
classText.includes('geek-card-detail');
|
|
1084
|
+
const hasDetailHint =
|
|
1085
|
+
text.includes('在线简历') ||
|
|
1086
|
+
text.includes('附件简历') ||
|
|
1087
|
+
text.includes('牛人分析器') ||
|
|
1088
|
+
text.includes('活跃');
|
|
1089
|
+
|
|
1090
|
+
let score = 0;
|
|
1091
|
+
if (containsClose) score += 220;
|
|
1092
|
+
if (anchoredRight) score += 140;
|
|
1093
|
+
if (hasKnownDetailClass) score += 160;
|
|
1094
|
+
if (hasDetailHint) score += 80;
|
|
1095
|
+
if (entry.source === 'close-ancestor') score += 40;
|
|
1096
|
+
score += Math.min(180, Math.floor((entry.rect.width * entry.rect.height) / 12000));
|
|
1097
|
+
|
|
1098
|
+
return {
|
|
1099
|
+
...entry,
|
|
1100
|
+
score,
|
|
1101
|
+
};
|
|
1102
|
+
})
|
|
1103
|
+
.sort((a, b) => b.score - a.score);
|
|
1104
|
+
|
|
1105
|
+
const topPanel = scoredPanels[0] || null;
|
|
1106
|
+
const topPanelNode = topPanel?.node || null;
|
|
1107
|
+
const closeButton =
|
|
1108
|
+
closeButtons.find((button) => topPanelNode instanceof HTMLElement && topPanelNode.contains(button)) ||
|
|
1109
|
+
closeButtons[0] ||
|
|
1110
|
+
null;
|
|
1111
|
+
|
|
1112
|
+
return {
|
|
1113
|
+
open: Boolean(topPanel || closeButton),
|
|
1114
|
+
panelCount: scoredPanels.length,
|
|
1115
|
+
closeCount: closeButtons.length,
|
|
1116
|
+
topPanelClass: normalize(topPanelNode?.className || ''),
|
|
1117
|
+
topPanelScore: Number(topPanel?.score || 0),
|
|
1118
|
+
panelRect: topPanel ? rectToJson(topPanel.rect) : null,
|
|
1119
|
+
closeRect: closeButton ? rectToJson(closeButton.getBoundingClientRect()) : null,
|
|
1120
|
+
};
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
return collectSnapshot();
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function browserCloseCandidateDetailDomOnce() {
|
|
1127
|
+
const collectSnapshot = () => {
|
|
1128
|
+
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
1129
|
+
const isVisible = (el) => {
|
|
1130
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
1131
|
+
const style = getComputedStyle(el);
|
|
1132
|
+
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
const rect = el.getBoundingClientRect();
|
|
1136
|
+
return rect.width > 2 && rect.height > 2;
|
|
1137
|
+
};
|
|
1138
|
+
const rectToJson = (rect) => ({
|
|
1139
|
+
left: rect.left,
|
|
1140
|
+
top: rect.top,
|
|
1141
|
+
width: rect.width,
|
|
1142
|
+
height: rect.height,
|
|
1143
|
+
right: rect.right,
|
|
1144
|
+
bottom: rect.bottom,
|
|
1145
|
+
});
|
|
1146
|
+
const panelSelectors = [
|
|
1147
|
+
'.base-info-single-top-detail',
|
|
1148
|
+
'.resume-detail-wrap',
|
|
1149
|
+
'.geek-card-detail',
|
|
1150
|
+
'.candidate-detail-wrap',
|
|
1151
|
+
'.chat-detail-wrap',
|
|
1152
|
+
];
|
|
1153
|
+
const closeButtons = Array.from(document.querySelectorAll('.close-btn')).filter(isVisible);
|
|
1154
|
+
const panelEntries = [];
|
|
1155
|
+
const seen = new Set();
|
|
1156
|
+
const pushPanel = (node, source) => {
|
|
1157
|
+
if (!(node instanceof HTMLElement) || !isVisible(node)) return;
|
|
1158
|
+
const rect = node.getBoundingClientRect();
|
|
1159
|
+
if (rect.width < 240 || rect.height < 160) return;
|
|
1160
|
+
const key = `${Math.round(rect.left)}:${Math.round(rect.top)}:${Math.round(rect.width)}:${Math.round(rect.height)}:${normalize(node.className || '')}`;
|
|
1161
|
+
if (seen.has(key)) return;
|
|
1162
|
+
seen.add(key);
|
|
1163
|
+
panelEntries.push({ node, rect, source });
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
for (const selector of panelSelectors) {
|
|
1167
|
+
for (const node of Array.from(document.querySelectorAll(selector))) {
|
|
1168
|
+
pushPanel(node, `selector:${selector}`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
for (const closeButton of closeButtons) {
|
|
1173
|
+
let current = closeButton.parentElement;
|
|
1174
|
+
let depth = 0;
|
|
1175
|
+
while (current instanceof HTMLElement && depth < 10) {
|
|
1176
|
+
pushPanel(current, 'close-ancestor');
|
|
1177
|
+
current = current.parentElement;
|
|
1178
|
+
depth += 1;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const scoredPanels = panelEntries
|
|
1183
|
+
.map((entry) => {
|
|
1184
|
+
const classText = normalize(entry.node.className || '').toLowerCase();
|
|
1185
|
+
const text = normalize(entry.node.textContent || '').slice(0, 240).toLowerCase();
|
|
1186
|
+
const containsClose = closeButtons.some((button) => entry.node.contains(button));
|
|
1187
|
+
const anchoredRight =
|
|
1188
|
+
entry.rect.left >= window.innerWidth * 0.4 ||
|
|
1189
|
+
entry.rect.right >= window.innerWidth * 0.72;
|
|
1190
|
+
const hasKnownDetailClass =
|
|
1191
|
+
classText.includes('base-info-single-top-detail') ||
|
|
1192
|
+
classText.includes('resume-detail-wrap') ||
|
|
1193
|
+
classText.includes('candidate-detail') ||
|
|
1194
|
+
classText.includes('chat-detail') ||
|
|
1195
|
+
classText.includes('geek-card-detail');
|
|
1196
|
+
const hasDetailHint =
|
|
1197
|
+
text.includes('在线简历') ||
|
|
1198
|
+
text.includes('附件简历') ||
|
|
1199
|
+
text.includes('牛人分析器') ||
|
|
1200
|
+
text.includes('活跃');
|
|
1201
|
+
|
|
1202
|
+
let score = 0;
|
|
1203
|
+
if (containsClose) score += 220;
|
|
1204
|
+
if (anchoredRight) score += 140;
|
|
1205
|
+
if (hasKnownDetailClass) score += 160;
|
|
1206
|
+
if (hasDetailHint) score += 80;
|
|
1207
|
+
if (entry.source === 'close-ancestor') score += 40;
|
|
1208
|
+
score += Math.min(180, Math.floor((entry.rect.width * entry.rect.height) / 12000));
|
|
1209
|
+
|
|
1210
|
+
return {
|
|
1211
|
+
...entry,
|
|
1212
|
+
score,
|
|
1213
|
+
};
|
|
1214
|
+
})
|
|
1215
|
+
.sort((a, b) => b.score - a.score);
|
|
1216
|
+
|
|
1217
|
+
const topPanel = scoredPanels[0] || null;
|
|
1218
|
+
const topPanelNode = topPanel?.node || null;
|
|
1219
|
+
const closeButton =
|
|
1220
|
+
closeButtons.find((button) => topPanelNode instanceof HTMLElement && topPanelNode.contains(button)) ||
|
|
1221
|
+
closeButtons[0] ||
|
|
1222
|
+
null;
|
|
1223
|
+
|
|
1224
|
+
return {
|
|
1225
|
+
open: Boolean(topPanel || closeButton),
|
|
1226
|
+
panelCount: scoredPanels.length,
|
|
1227
|
+
closeCount: closeButtons.length,
|
|
1228
|
+
topPanelClass: normalize(topPanelNode?.className || ''),
|
|
1229
|
+
topPanelScore: Number(topPanel?.score || 0),
|
|
1230
|
+
panelRect: topPanel ? rectToJson(topPanel.rect) : null,
|
|
1231
|
+
closeRect: closeButton ? rectToJson(closeButton.getBoundingClientRect()) : null,
|
|
1232
|
+
closeButton,
|
|
1233
|
+
};
|
|
1234
|
+
};
|
|
1235
|
+
const serializeSnapshot = (snapshot = {}) => ({
|
|
1236
|
+
open: Boolean(snapshot?.open),
|
|
1237
|
+
panelCount: Number(snapshot?.panelCount || 0),
|
|
1238
|
+
closeCount: Number(snapshot?.closeCount || 0),
|
|
1239
|
+
topPanelClass: String(snapshot?.topPanelClass || ''),
|
|
1240
|
+
topPanelScore: Number(snapshot?.topPanelScore || 0),
|
|
1241
|
+
panelRect: snapshot?.panelRect || null,
|
|
1242
|
+
closeRect: snapshot?.closeRect || null,
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
const snapshot = collectSnapshot();
|
|
1246
|
+
if (!snapshot?.open || !(snapshot.closeButton instanceof HTMLElement)) {
|
|
1247
|
+
return {
|
|
1248
|
+
ok: false,
|
|
1249
|
+
error: 'CANDIDATE_DETAIL_CLOSE_BUTTON_NOT_FOUND',
|
|
1250
|
+
state: serializeSnapshot(snapshot),
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
try {
|
|
1255
|
+
snapshot.closeButton.click();
|
|
1256
|
+
const rect = snapshot.closeButton.getBoundingClientRect();
|
|
1257
|
+
return {
|
|
1258
|
+
ok: true,
|
|
1259
|
+
selector: '.close-btn',
|
|
1260
|
+
method: 'dom-click-once',
|
|
1261
|
+
rect: {
|
|
1262
|
+
left: rect.left,
|
|
1263
|
+
top: rect.top,
|
|
1264
|
+
width: rect.width,
|
|
1265
|
+
height: rect.height,
|
|
1266
|
+
right: rect.right,
|
|
1267
|
+
bottom: rect.bottom,
|
|
1268
|
+
},
|
|
1269
|
+
state: serializeSnapshot(snapshot),
|
|
1270
|
+
};
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
return {
|
|
1273
|
+
ok: false,
|
|
1274
|
+
error: `CANDIDATE_DETAIL_DOM_CLOSE_FAILED:${error?.message || error}`,
|
|
1275
|
+
state: serializeSnapshot(snapshot),
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1014
1280
|
function browserCloseResumeModalDomOnce() {
|
|
1015
1281
|
const isVisible = (el) => {
|
|
1016
1282
|
if (!(el instanceof HTMLElement)) return false;
|
|
@@ -2596,6 +2862,160 @@ export class BossChatPage {
|
|
|
2596
2862
|
};
|
|
2597
2863
|
}
|
|
2598
2864
|
|
|
2865
|
+
async isCandidateDetailOpen() {
|
|
2866
|
+
const result = await this.chromeClient.callFunction(browserIsCandidateDetailOpen);
|
|
2867
|
+
return Boolean(result?.open);
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
async getCandidateDetailState() {
|
|
2871
|
+
const result = await this.chromeClient.callFunction(browserIsCandidateDetailOpen);
|
|
2872
|
+
return {
|
|
2873
|
+
open: Boolean(result?.open),
|
|
2874
|
+
panelCount: Number(result?.panelCount || 0),
|
|
2875
|
+
closeCount: Number(result?.closeCount || 0),
|
|
2876
|
+
topPanelClass: String(result?.topPanelClass || ''),
|
|
2877
|
+
topPanelScore: Number(result?.topPanelScore || 0),
|
|
2878
|
+
panelRect: result?.panelRect || null,
|
|
2879
|
+
closeRect: result?.closeRect || null,
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
async closeCandidateDetailDomOnce() {
|
|
2884
|
+
const stateBefore = await this.getCandidateDetailState();
|
|
2885
|
+
const drawerOpen = (state) =>
|
|
2886
|
+
Boolean(state?.open) ||
|
|
2887
|
+
Number(state?.panelCount || 0) > 0 ||
|
|
2888
|
+
Number(state?.closeCount || 0) > 0;
|
|
2889
|
+
const openBefore = drawerOpen(stateBefore);
|
|
2890
|
+
if (!openBefore) {
|
|
2891
|
+
return {
|
|
2892
|
+
closed: true,
|
|
2893
|
+
method: 'already-closed',
|
|
2894
|
+
finalState: stateBefore,
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
const result = await this.chromeClient.callFunction(browserCloseCandidateDetailDomOnce);
|
|
2899
|
+
if (!result?.ok) {
|
|
2900
|
+
const finalState = await this.getCandidateDetailState();
|
|
2901
|
+
return {
|
|
2902
|
+
closed: false,
|
|
2903
|
+
method: `dom-close-miss:${result?.error || 'unknown'}`,
|
|
2904
|
+
finalState,
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
let finalState = await this.getCandidateDetailState();
|
|
2909
|
+
let openAfter = drawerOpen(finalState);
|
|
2910
|
+
for (let attempt = 0; openAfter && attempt < 8; attempt += 1) {
|
|
2911
|
+
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
2912
|
+
finalState = await this.getCandidateDetailState();
|
|
2913
|
+
openAfter = drawerOpen(finalState);
|
|
2914
|
+
}
|
|
2915
|
+
return {
|
|
2916
|
+
closed: !openAfter,
|
|
2917
|
+
method: `dom-close-once:${result.selector || '.close-btn'}`,
|
|
2918
|
+
finalState,
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
async closeCandidateDetail({ maxAttempts = 4, ensureDismiss = false } = {}) {
|
|
2923
|
+
const drawerOpen = (state) =>
|
|
2924
|
+
Boolean(state?.open) ||
|
|
2925
|
+
Number(state?.panelCount || 0) > 0 ||
|
|
2926
|
+
Number(state?.closeCount || 0) > 0;
|
|
2927
|
+
const methods = [];
|
|
2928
|
+
for (let index = 0; index < maxAttempts; index += 1) {
|
|
2929
|
+
const state = await this.getCandidateDetailState();
|
|
2930
|
+
if (!drawerOpen(state) && !ensureDismiss) {
|
|
2931
|
+
return {
|
|
2932
|
+
closed: true,
|
|
2933
|
+
method: methods.join('+') || 'already-closed',
|
|
2934
|
+
finalState: state,
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
const selectorResult = await this.chromeClient.callFunction(browserCloseCandidateDetailDomOnce);
|
|
2939
|
+
if (selectorResult?.ok) {
|
|
2940
|
+
methods.push(`selector:${selectorResult.selector || '.close-btn'}`);
|
|
2941
|
+
} else {
|
|
2942
|
+
methods.push(`selector-miss:${selectorResult?.error || 'unknown'}`);
|
|
2943
|
+
}
|
|
2944
|
+
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
2945
|
+
|
|
2946
|
+
let midState = await this.getCandidateDetailState();
|
|
2947
|
+
if (!drawerOpen(midState)) {
|
|
2948
|
+
return {
|
|
2949
|
+
closed: true,
|
|
2950
|
+
method: methods.join('+'),
|
|
2951
|
+
finalState: midState,
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
if (midState?.panelRect) {
|
|
2956
|
+
await this.clickRect(midState.panelRect);
|
|
2957
|
+
methods.push('focus-panel');
|
|
2958
|
+
await new Promise((resolve) => setTimeout(resolve, 160));
|
|
2959
|
+
} else if (midState?.closeRect) {
|
|
2960
|
+
await this.clickRect(midState.closeRect);
|
|
2961
|
+
methods.push('focus-close');
|
|
2962
|
+
await new Promise((resolve) => setTimeout(resolve, 160));
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
await this.chromeClient.pressEscape();
|
|
2966
|
+
methods.push('escape');
|
|
2967
|
+
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
2968
|
+
|
|
2969
|
+
midState = await this.getCandidateDetailState();
|
|
2970
|
+
if (!drawerOpen(midState)) {
|
|
2971
|
+
return {
|
|
2972
|
+
closed: true,
|
|
2973
|
+
method: methods.join('+'),
|
|
2974
|
+
finalState: midState,
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
if (midState?.closeRect) {
|
|
2979
|
+
await this.clickRect(midState.closeRect);
|
|
2980
|
+
methods.push('rect-close');
|
|
2981
|
+
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
2982
|
+
midState = await this.getCandidateDetailState();
|
|
2983
|
+
if (!drawerOpen(midState)) {
|
|
2984
|
+
return {
|
|
2985
|
+
closed: true,
|
|
2986
|
+
method: methods.join('+'),
|
|
2987
|
+
finalState: midState,
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
if (ensureDismiss && index >= 1) {
|
|
2993
|
+
const finalSweep = await this.getCandidateDetailState();
|
|
2994
|
+
if (!drawerOpen(finalSweep)) {
|
|
2995
|
+
return {
|
|
2996
|
+
closed: true,
|
|
2997
|
+
method: methods.join('+'),
|
|
2998
|
+
finalState: finalSweep,
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
const finalState = await this.getCandidateDetailState();
|
|
3005
|
+
if (!drawerOpen(finalState)) {
|
|
3006
|
+
return {
|
|
3007
|
+
closed: true,
|
|
3008
|
+
method: methods.join('+') || 'fallback',
|
|
3009
|
+
finalState,
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
3012
|
+
return {
|
|
3013
|
+
closed: false,
|
|
3014
|
+
method: methods.join('+') || 'failed',
|
|
3015
|
+
finalState,
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
|
|
2599
3019
|
async waitForResumeModalOpen(options = {}) {
|
|
2600
3020
|
const maxAttempts = options.maxAttempts || 30;
|
|
2601
3021
|
const delayMs = options.delayMs || 300;
|