@reconcrap/boss-recommend-mcp 1.3.28 → 1.3.30
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/README.md +1 -0
- package/package.json +1 -1
- package/src/cli.js +87 -0
- package/src/test-boss-chat.js +218 -0
- package/vendor/boss-chat-cli/src/app.js +85 -19
- package/vendor/boss-chat-cli/src/browser/chat-page.js +420 -0
package/README.md
CHANGED
|
@@ -68,6 +68,7 @@ MCP 工具:
|
|
|
68
68
|
- 在真正开始 search/screen 前,会进行最后一轮全参数总确认(岗位 + 全部筛选参数 + criteria + target_count + post_action + max_greet_count)
|
|
69
69
|
- npm 全局安装后会自动执行 install:生成 skill、导出 MCP 模板,并自动尝试写入已检测到的外部 agent MCP 配置(含 Trae / trae-cn / Cursor / Claude / OpenClaw)
|
|
70
70
|
- npm / npx 安装后会自动初始化 `screening-config.json` 模板(优先写入 workspace 的 `config/`,不可写时回退到用户目录)
|
|
71
|
+
- npm 安装流程会预创建运行目录(跨平台):`~/.boss-recommend-mcp`、`~/.boss-recommend-mcp/runs`、`<workspace>/.boss-chat` 及其 `logs/runs/profiles/reports/artifacts`
|
|
71
72
|
- `post_action` 必须在每次完整运行开始时确认一次
|
|
72
73
|
- `target_count` 会在每次运行开始时询问一次(可留空,不设上限)
|
|
73
74
|
- 当 `post_action=greet` 时,必须在运行开始时确认 `max_greet_count`
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -742,6 +742,70 @@ function ensureUserConfig(options = {}) {
|
|
|
742
742
|
throw lastError || new Error("No writable target for screening-config.json");
|
|
743
743
|
}
|
|
744
744
|
|
|
745
|
+
function getBossChatDataDir(workspaceRoot) {
|
|
746
|
+
return path.join(path.resolve(String(workspaceRoot || process.cwd())), ".boss-chat");
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function collectRuntimeDirectories(options = {}) {
|
|
750
|
+
const workspaceRoot = getWorkspaceRoot(options);
|
|
751
|
+
const stateHome = getStateHome();
|
|
752
|
+
const bossChatRoot = getBossChatDataDir(workspaceRoot);
|
|
753
|
+
const recommendRuntimeDirs = [
|
|
754
|
+
stateHome,
|
|
755
|
+
path.join(stateHome, "runs")
|
|
756
|
+
];
|
|
757
|
+
const bossChatRuntimeDirs = [
|
|
758
|
+
bossChatRoot,
|
|
759
|
+
path.join(bossChatRoot, "logs"),
|
|
760
|
+
path.join(bossChatRoot, "runs"),
|
|
761
|
+
path.join(bossChatRoot, "profiles"),
|
|
762
|
+
path.join(bossChatRoot, "reports"),
|
|
763
|
+
path.join(bossChatRoot, "artifacts")
|
|
764
|
+
];
|
|
765
|
+
return {
|
|
766
|
+
workspaceRoot,
|
|
767
|
+
stateHome,
|
|
768
|
+
bossChatRoot,
|
|
769
|
+
directories: dedupePaths([
|
|
770
|
+
...recommendRuntimeDirs,
|
|
771
|
+
...bossChatRuntimeDirs
|
|
772
|
+
]).filter(Boolean)
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function ensureRuntimeDirectories(options = {}) {
|
|
777
|
+
const { workspaceRoot, stateHome, bossChatRoot, directories } = collectRuntimeDirectories(options);
|
|
778
|
+
const created = [];
|
|
779
|
+
const existed = [];
|
|
780
|
+
const failed = [];
|
|
781
|
+
|
|
782
|
+
for (const directory of directories) {
|
|
783
|
+
try {
|
|
784
|
+
const existedBefore = fs.existsSync(directory);
|
|
785
|
+
ensureDir(directory);
|
|
786
|
+
if (existedBefore) {
|
|
787
|
+
existed.push(directory);
|
|
788
|
+
} else {
|
|
789
|
+
created.push(directory);
|
|
790
|
+
}
|
|
791
|
+
} catch (error) {
|
|
792
|
+
failed.push({
|
|
793
|
+
path: directory,
|
|
794
|
+
message: error?.message || String(error)
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return {
|
|
800
|
+
workspaceRoot,
|
|
801
|
+
stateHome,
|
|
802
|
+
bossChatRoot,
|
|
803
|
+
created,
|
|
804
|
+
existed,
|
|
805
|
+
failed
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
745
809
|
function readJsonObjectFile(filePath) {
|
|
746
810
|
const raw = fs.readFileSync(filePath, "utf8");
|
|
747
811
|
const parsed = JSON.parse(raw);
|
|
@@ -1333,11 +1397,22 @@ function printMcpConfig(options = {}) {
|
|
|
1333
1397
|
}
|
|
1334
1398
|
|
|
1335
1399
|
function installAll(options = {}) {
|
|
1400
|
+
const runtimeDirsResult = ensureRuntimeDirectories(options);
|
|
1336
1401
|
const skillResults = installSkill();
|
|
1337
1402
|
const configResult = ensureUserConfig(options);
|
|
1338
1403
|
const mcpTemplateResult = writeMcpConfigFiles({ client: "all" });
|
|
1339
1404
|
const externalMcpResult = installExternalMcpConfigs(options);
|
|
1340
1405
|
const externalSkillResult = mirrorSkillToExternalDirs(options);
|
|
1406
|
+
console.log(
|
|
1407
|
+
`Runtime directories prepared: created=${runtimeDirsResult.created.length}, existing=${runtimeDirsResult.existed.length}, failed=${runtimeDirsResult.failed.length}`
|
|
1408
|
+
);
|
|
1409
|
+
console.log(`- recommend runtime: ${runtimeDirsResult.stateHome}`);
|
|
1410
|
+
console.log(`- boss-chat runtime: ${runtimeDirsResult.bossChatRoot}`);
|
|
1411
|
+
if (runtimeDirsResult.failed.length > 0) {
|
|
1412
|
+
for (const item of runtimeDirsResult.failed) {
|
|
1413
|
+
console.warn(`Runtime dir warning: ${item.path} -> ${item.message}`);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1341
1416
|
console.log(`Bundled skills installed: ${skillResults.length}`);
|
|
1342
1417
|
for (const item of skillResults) {
|
|
1343
1418
|
console.log(`- ${item.skill}: ${item.targetDir}`);
|
|
@@ -1546,7 +1621,18 @@ export async function runCli(argv = process.argv) {
|
|
|
1546
1621
|
}
|
|
1547
1622
|
break;
|
|
1548
1623
|
case "init-config": {
|
|
1624
|
+
const runtimeDirsResult = ensureRuntimeDirectories(options);
|
|
1549
1625
|
const result = ensureUserConfig(options);
|
|
1626
|
+
console.log(
|
|
1627
|
+
`Runtime directories prepared: created=${runtimeDirsResult.created.length}, existing=${runtimeDirsResult.existed.length}, failed=${runtimeDirsResult.failed.length}`
|
|
1628
|
+
);
|
|
1629
|
+
console.log(`- recommend runtime: ${runtimeDirsResult.stateHome}`);
|
|
1630
|
+
console.log(`- boss-chat runtime: ${runtimeDirsResult.bossChatRoot}`);
|
|
1631
|
+
if (runtimeDirsResult.failed.length > 0) {
|
|
1632
|
+
for (const item of runtimeDirsResult.failed) {
|
|
1633
|
+
console.warn(`Runtime dir warning: ${item.path} -> ${item.message}`);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1550
1636
|
console.log(result.created ? `Config template created at: ${result.path}` : `Config already exists at: ${result.path}`);
|
|
1551
1637
|
if (Array.isArray(result.patched_keys) && result.patched_keys.length > 0) {
|
|
1552
1638
|
console.log(`Config patched missing defaults: ${result.patched_keys.join(", ")}`);
|
|
@@ -1635,6 +1721,7 @@ export const __testables = {
|
|
|
1635
1721
|
getRunFollowUp,
|
|
1636
1722
|
installSkill,
|
|
1637
1723
|
isInstalledPackageRoot,
|
|
1724
|
+
ensureRuntimeDirectories,
|
|
1638
1725
|
runBossChatCliCommand,
|
|
1639
1726
|
runPipelineOnce
|
|
1640
1727
|
};
|
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;
|