@reconcrap/boss-recommend-mcp 1.3.24 → 1.3.26
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 +1 -1
- package/src/cli.js +28 -2
- package/src/test-boss-chat.js +203 -0
- package/vendor/boss-chat-cli/src/browser/chat-page.js +23 -8
- package/vendor/boss-chat-cli/src/cli.js +181 -21
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -39,7 +39,6 @@ const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|his
|
|
|
39
39
|
const supportedMcpClients = ["generic", "cursor", "trae", "claudecode", "openclaw"];
|
|
40
40
|
const defaultMcpServerName = "boss-recommend";
|
|
41
41
|
const defaultMcpCommand = "npx";
|
|
42
|
-
const defaultMcpArgs = ["-y", "@reconcrap/boss-recommend-mcp@latest", "start"];
|
|
43
42
|
const recommendMcpPackageName = "@reconcrap/boss-recommend-mcp";
|
|
44
43
|
const recommendMcpBinaryName = "boss-recommend-mcp";
|
|
45
44
|
const autoSyncSkipCommands = new Set(["install", "install-skill", "where", "help", "--help", "-h"]);
|
|
@@ -65,6 +64,29 @@ function getPackageVersion() {
|
|
|
65
64
|
|
|
66
65
|
const packageVersion = getPackageVersion();
|
|
67
66
|
|
|
67
|
+
function isInstalledPackageRoot(rootPath = packageRoot) {
|
|
68
|
+
const normalized = path.resolve(String(rootPath || ""))
|
|
69
|
+
.replace(/\\/g, "/")
|
|
70
|
+
.toLowerCase();
|
|
71
|
+
return (
|
|
72
|
+
normalized.includes("/appdata/local/npm-cache/_npx/")
|
|
73
|
+
|| normalized.includes("/node_modules/@reconcrap/boss-recommend-mcp")
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getDefaultMcpPackageSpecifier(options = {}) {
|
|
78
|
+
const version = String(options.packageVersion || packageVersion).trim();
|
|
79
|
+
const rootPath = options.packageRootPath || packageRoot;
|
|
80
|
+
if (version && version !== "0.0.0" && isInstalledPackageRoot(rootPath)) {
|
|
81
|
+
return `${recommendMcpPackageName}@${version}`;
|
|
82
|
+
}
|
|
83
|
+
return `${recommendMcpPackageName}@latest`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildDefaultMcpArgs(options = {}) {
|
|
87
|
+
return ["-y", getDefaultMcpPackageSpecifier(options), "start"];
|
|
88
|
+
}
|
|
89
|
+
|
|
68
90
|
function getCodexHome() {
|
|
69
91
|
return process.env.CODEX_HOME
|
|
70
92
|
? path.resolve(process.env.CODEX_HOME)
|
|
@@ -338,7 +360,7 @@ function buildMcpLaunchConfig(options = {}) {
|
|
|
338
360
|
? args
|
|
339
361
|
: command === "boss-recommend-mcp"
|
|
340
362
|
? ["start"]
|
|
341
|
-
:
|
|
363
|
+
: buildDefaultMcpArgs();
|
|
342
364
|
const launchConfig = { command, args: launchArgs };
|
|
343
365
|
if (env && typeof env === "object" && !Array.isArray(env) && Object.keys(env).length > 0) {
|
|
344
366
|
launchConfig.env = env;
|
|
@@ -1606,9 +1628,13 @@ export async function runCli(argv = process.argv) {
|
|
|
1606
1628
|
|
|
1607
1629
|
export const __testables = {
|
|
1608
1630
|
buildBossChatCliInput,
|
|
1631
|
+
buildDefaultMcpArgs,
|
|
1632
|
+
buildMcpLaunchConfig,
|
|
1609
1633
|
getBossChatCliRunTarget,
|
|
1634
|
+
getDefaultMcpPackageSpecifier,
|
|
1610
1635
|
getRunFollowUp,
|
|
1611
1636
|
installSkill,
|
|
1637
|
+
isInstalledPackageRoot,
|
|
1612
1638
|
runBossChatCliCommand,
|
|
1613
1639
|
runPipelineOnce
|
|
1614
1640
|
};
|
package/src/test-boss-chat.js
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
import { __testables as cliTestables } from "./cli.js";
|
|
17
17
|
import { __testables as indexTestables } from "./index.js";
|
|
18
18
|
import { BossChatApp } from "../vendor/boss-chat-cli/src/app.js";
|
|
19
|
+
import { __testables as vendorCliTestables } from "../vendor/boss-chat-cli/src/cli.js";
|
|
20
|
+
import { BossChatPage } from "../vendor/boss-chat-cli/src/browser/chat-page.js";
|
|
19
21
|
import { LlmClient, parseLlmJson } from "../vendor/boss-chat-cli/src/services/llm.js";
|
|
20
22
|
|
|
21
23
|
const { handleRequest } = indexTestables;
|
|
@@ -398,6 +400,90 @@ async function testBossChatPrepareShouldRetryWhenChatPageIsNotReady() {
|
|
|
398
400
|
});
|
|
399
401
|
}
|
|
400
402
|
|
|
403
|
+
async function testBossChatPageShouldTreatBlankChatShellAsOnChatPage() {
|
|
404
|
+
const fakeChromeClient = {
|
|
405
|
+
async callFunction() {
|
|
406
|
+
return {
|
|
407
|
+
href: "https://www.zhipin.com/web/chat/index",
|
|
408
|
+
readyState: "complete",
|
|
409
|
+
hasListContainer: false,
|
|
410
|
+
listItemCount: 0
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const page = new BossChatPage(fakeChromeClient);
|
|
416
|
+
const pageState = await page.ensureOnChatPage();
|
|
417
|
+
assert.equal(pageState.href, "https://www.zhipin.com/web/chat/index");
|
|
418
|
+
|
|
419
|
+
await assert.rejects(
|
|
420
|
+
() => page.ensureReady(),
|
|
421
|
+
/CHAT_LIST_CONTAINER_NOT_FOUND/
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function testBossChatRecoverToChatIndexShouldForceNavigateAndWaitForCompleteLoad() {
|
|
426
|
+
const calls = [];
|
|
427
|
+
let stateIndex = 0;
|
|
428
|
+
const states = [
|
|
429
|
+
{
|
|
430
|
+
href: "https://www.zhipin.com/web/chat/index",
|
|
431
|
+
readyState: "loading",
|
|
432
|
+
hasListContainer: false,
|
|
433
|
+
listItemCount: 0
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
href: "https://www.zhipin.com/web/chat/index",
|
|
437
|
+
readyState: "interactive",
|
|
438
|
+
hasListContainer: false,
|
|
439
|
+
listItemCount: 0
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
href: "https://www.zhipin.com/web/chat/index",
|
|
443
|
+
readyState: "complete",
|
|
444
|
+
hasListContainer: false,
|
|
445
|
+
listItemCount: 0
|
|
446
|
+
}
|
|
447
|
+
];
|
|
448
|
+
|
|
449
|
+
const fakeChromeClient = {
|
|
450
|
+
async callFunction(fn, arg) {
|
|
451
|
+
calls.push({ name: fn.name, arg });
|
|
452
|
+
if (fn.name === "browserGetCurrentHref") {
|
|
453
|
+
return { href: "https://www.zhipin.com/web/chat/index" };
|
|
454
|
+
}
|
|
455
|
+
if (fn.name === "browserNavigateToChatIndex") {
|
|
456
|
+
return { ok: true, changed: true, href: "https://www.zhipin.com/web/chat/index" };
|
|
457
|
+
}
|
|
458
|
+
if (fn.name === "browserGetPageState") {
|
|
459
|
+
const value = states[Math.min(stateIndex, states.length - 1)];
|
|
460
|
+
stateIndex += 1;
|
|
461
|
+
return value;
|
|
462
|
+
}
|
|
463
|
+
throw new Error(`unexpected function: ${fn.name}`);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const page = new BossChatPage(fakeChromeClient);
|
|
468
|
+
const result = await page.recoverToChatIndex({
|
|
469
|
+
forceNavigate: true,
|
|
470
|
+
waitForReadyState: "complete",
|
|
471
|
+
maxAttempts: 5,
|
|
472
|
+
delayMs: 0
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
assert.equal(result.changed, true);
|
|
476
|
+
assert.equal(result.href, "https://www.zhipin.com/web/chat/index");
|
|
477
|
+
assert.equal(
|
|
478
|
+
calls.some((entry) => entry.name === "browserNavigateToChatIndex" && entry.arg?.force === true),
|
|
479
|
+
true
|
|
480
|
+
);
|
|
481
|
+
assert.equal(
|
|
482
|
+
calls.filter((entry) => entry.name === "browserGetPageState").length >= 3,
|
|
483
|
+
true
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
401
487
|
async function testBossChatMcpToolsShouldValidateAndRoute() {
|
|
402
488
|
await withBossChatWorkspace(async (workspaceRoot) => {
|
|
403
489
|
const toolsResponse = await handleRequest({
|
|
@@ -599,6 +685,118 @@ async function testBossChatCliShouldSupportRunAndFollowUpParsing() {
|
|
|
599
685
|
});
|
|
600
686
|
}
|
|
601
687
|
|
|
688
|
+
async function testVendorBossChatCliShouldWaitForHydratedChatShell() {
|
|
689
|
+
const pageStates = [
|
|
690
|
+
{ href: "https://www.zhipin.com/web/chat/index", hasListContainer: false, listItemCount: 0 },
|
|
691
|
+
{ href: "https://www.zhipin.com/web/chat/index", hasListContainer: false, listItemCount: 0 },
|
|
692
|
+
{ href: "https://www.zhipin.com/web/chat/index", hasListContainer: true, listItemCount: 40 },
|
|
693
|
+
];
|
|
694
|
+
const jobsPerAttempt = [
|
|
695
|
+
[],
|
|
696
|
+
[],
|
|
697
|
+
[{ value: "job-1", label: "AI应用开发工程师(2026) _ 杭州", active: false }],
|
|
698
|
+
];
|
|
699
|
+
let ensureCallCount = 0;
|
|
700
|
+
let listJobsCallCount = 0;
|
|
701
|
+
const page = {
|
|
702
|
+
async ensureOnChatPage() {
|
|
703
|
+
const next = pageStates[Math.min(ensureCallCount, pageStates.length - 1)];
|
|
704
|
+
ensureCallCount += 1;
|
|
705
|
+
return next;
|
|
706
|
+
},
|
|
707
|
+
async listJobs() {
|
|
708
|
+
const next = jobsPerAttempt[Math.min(listJobsCallCount, jobsPerAttempt.length - 1)];
|
|
709
|
+
listJobsCallCount += 1;
|
|
710
|
+
return next;
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
const hydrated = await vendorCliTestables.waitForChatShellHydration({
|
|
715
|
+
page,
|
|
716
|
+
maxAttempts: 4,
|
|
717
|
+
delayMs: 0,
|
|
718
|
+
});
|
|
719
|
+
assert.equal(Array.isArray(hydrated.jobs), true);
|
|
720
|
+
assert.equal(hydrated.jobs.length, 1);
|
|
721
|
+
assert.equal(hydrated.pageState.listItemCount, 40);
|
|
722
|
+
assert.equal(ensureCallCount >= 3, true);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function testVendorBossChatCliShouldRetryJobListDuringPromptRunProfile() {
|
|
726
|
+
const page = {
|
|
727
|
+
_attempt: 0,
|
|
728
|
+
async ensureOnChatPage() {
|
|
729
|
+
return {
|
|
730
|
+
href: "https://www.zhipin.com/web/chat/index",
|
|
731
|
+
hasListContainer: this._attempt >= 1,
|
|
732
|
+
listItemCount: this._attempt >= 1 ? 10 : 0,
|
|
733
|
+
};
|
|
734
|
+
},
|
|
735
|
+
async listJobs() {
|
|
736
|
+
this._attempt += 1;
|
|
737
|
+
if (this._attempt < 2) {
|
|
738
|
+
return [];
|
|
739
|
+
}
|
|
740
|
+
return [
|
|
741
|
+
{
|
|
742
|
+
value: "job-1",
|
|
743
|
+
label: "AI应用开发工程师(2026) _ 杭州",
|
|
744
|
+
active: false,
|
|
745
|
+
},
|
|
746
|
+
];
|
|
747
|
+
},
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
const profile = await vendorCliTestables.promptRunProfile({
|
|
751
|
+
page,
|
|
752
|
+
persistentProfile: {
|
|
753
|
+
llm: {
|
|
754
|
+
baseUrl: "https://api.example.com/v1",
|
|
755
|
+
apiKey: "sk-test-key",
|
|
756
|
+
model: "gpt-4.1-mini",
|
|
757
|
+
},
|
|
758
|
+
chrome: {
|
|
759
|
+
port: 9222,
|
|
760
|
+
},
|
|
761
|
+
runtime: {},
|
|
762
|
+
},
|
|
763
|
+
overrides: {
|
|
764
|
+
jobSelection: "AI应用开发工程师(2026) _ 杭州",
|
|
765
|
+
startFrom: "unread",
|
|
766
|
+
screeningCriteria: "小样本联通性验证",
|
|
767
|
+
targetCount: 1,
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
assert.equal(profile.jobSelection.label, "AI应用开发工程师(2026) _ 杭州");
|
|
771
|
+
assert.equal(profile.startFrom, "unread");
|
|
772
|
+
assert.equal(profile.targetCount, 1);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function testCliShouldPinInstalledPackageVersionInGeneratedMcpConfig() {
|
|
776
|
+
const installedSpecifier = cliTestables.getDefaultMcpPackageSpecifier({
|
|
777
|
+
packageVersion: "1.3.25",
|
|
778
|
+
packageRootPath: "C:\\Users\\yaolin\\AppData\\Roaming\\npm\\node_modules\\@reconcrap\\boss-recommend-mcp",
|
|
779
|
+
});
|
|
780
|
+
assert.equal(installedSpecifier, "@reconcrap/boss-recommend-mcp@1.3.25");
|
|
781
|
+
|
|
782
|
+
const cachedSpecifier = cliTestables.getDefaultMcpPackageSpecifier({
|
|
783
|
+
packageVersion: "1.3.25",
|
|
784
|
+
packageRootPath: "C:\\Users\\yaolin\\AppData\\Local\\npm-cache\\_npx\\abcd1234\\node_modules\\@reconcrap\\boss-recommend-mcp",
|
|
785
|
+
});
|
|
786
|
+
assert.equal(cachedSpecifier, "@reconcrap/boss-recommend-mcp@1.3.25");
|
|
787
|
+
|
|
788
|
+
const sourceSpecifier = cliTestables.getDefaultMcpPackageSpecifier({
|
|
789
|
+
packageVersion: "1.3.25-dev",
|
|
790
|
+
packageRootPath: "C:\\Users\\yaolin\\Documents\\codex_projects\\boss recommend pipeline\\boss-recommend-mcp",
|
|
791
|
+
});
|
|
792
|
+
assert.equal(sourceSpecifier, "@reconcrap/boss-recommend-mcp@latest");
|
|
793
|
+
|
|
794
|
+
const launchConfig = cliTestables.buildMcpLaunchConfig({});
|
|
795
|
+
assert.equal(launchConfig.command, "npx");
|
|
796
|
+
assert.equal(Array.isArray(launchConfig.args), true);
|
|
797
|
+
assert.equal(launchConfig.args[0], "-y");
|
|
798
|
+
}
|
|
799
|
+
|
|
602
800
|
function testBossChatLlmEvidenceGateShouldDemoteMissingEvidence() {
|
|
603
801
|
const parsed = parseLlmJson(
|
|
604
802
|
JSON.stringify({
|
|
@@ -941,8 +1139,13 @@ async function testBossChatAppShouldPersistEvidenceArtifacts() {
|
|
|
941
1139
|
async function main() {
|
|
942
1140
|
await testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli();
|
|
943
1141
|
await testBossChatPrepareShouldRetryWhenChatPageIsNotReady();
|
|
1142
|
+
await testBossChatPageShouldTreatBlankChatShellAsOnChatPage();
|
|
1143
|
+
await testBossChatRecoverToChatIndexShouldForceNavigateAndWaitForCompleteLoad();
|
|
944
1144
|
await testBossChatMcpToolsShouldValidateAndRoute();
|
|
945
1145
|
await testBossChatCliShouldSupportRunAndFollowUpParsing();
|
|
1146
|
+
await testVendorBossChatCliShouldWaitForHydratedChatShell();
|
|
1147
|
+
await testVendorBossChatCliShouldRetryJobListDuringPromptRunProfile();
|
|
1148
|
+
testCliShouldPinInstalledPackageVersionInGeneratedMcpConfig();
|
|
946
1149
|
testBossChatLlmEvidenceGateShouldDemoteMissingEvidence();
|
|
947
1150
|
testBossChatLlmEvidenceGateShouldDemoteUnmatchedEvidence();
|
|
948
1151
|
await testBossChatLlmTextChunkFallbackShouldWork();
|
|
@@ -24,6 +24,7 @@ function browserGetPageState() {
|
|
|
24
24
|
|
|
25
25
|
return {
|
|
26
26
|
href: window.location.href,
|
|
27
|
+
readyState: document.readyState,
|
|
27
28
|
hasListContainer: Boolean(listContainer),
|
|
28
29
|
listItemCount: listItems.length,
|
|
29
30
|
};
|
|
@@ -33,9 +34,10 @@ function browserGetCurrentHref() {
|
|
|
33
34
|
return { href: window.location.href };
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
function browserNavigateToChatIndex() {
|
|
37
|
+
function browserNavigateToChatIndex(options = {}) {
|
|
37
38
|
const chatUrl = 'https://www.zhipin.com/web/chat/index';
|
|
38
|
-
|
|
39
|
+
const force = options?.force === true;
|
|
40
|
+
if (force || !String(window.location.href || '').includes('/web/chat/index')) {
|
|
39
41
|
window.location.assign(chatUrl);
|
|
40
42
|
return { ok: true, changed: true, href: chatUrl };
|
|
41
43
|
}
|
|
@@ -2236,11 +2238,20 @@ export class BossChatPage {
|
|
|
2236
2238
|
return target?.type === 'page' && String(target.url || '').includes(CHAT_URL_TOKEN);
|
|
2237
2239
|
}
|
|
2238
2240
|
|
|
2239
|
-
async
|
|
2240
|
-
|
|
2241
|
+
async getPageState() {
|
|
2242
|
+
return this.chromeClient.callFunction(browserGetPageState);
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
async ensureOnChatPage() {
|
|
2246
|
+
const pageState = await this.getPageState();
|
|
2241
2247
|
if (!pageState?.href?.includes(CHAT_URL_TOKEN)) {
|
|
2242
2248
|
throw new Error('ACTIVE_TAB_IS_NOT_BOSS_CHAT_PAGE');
|
|
2243
2249
|
}
|
|
2250
|
+
return pageState;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
async ensureReady() {
|
|
2254
|
+
const pageState = await this.ensureOnChatPage();
|
|
2244
2255
|
if (!pageState.hasListContainer && Number(pageState.listItemCount || 0) <= 0) {
|
|
2245
2256
|
throw new Error('CHAT_LIST_CONTAINER_NOT_FOUND');
|
|
2246
2257
|
}
|
|
@@ -2250,16 +2261,20 @@ export class BossChatPage {
|
|
|
2250
2261
|
async recoverToChatIndex(options = {}) {
|
|
2251
2262
|
const maxAttempts = options.maxAttempts || 20;
|
|
2252
2263
|
const delayMs = options.delayMs || 500;
|
|
2264
|
+
const forceNavigate = options.forceNavigate === true;
|
|
2265
|
+
const waitForReadyState = options.waitForReadyState || 'complete';
|
|
2253
2266
|
const hrefResult = await this.chromeClient.callFunction(browserGetCurrentHref);
|
|
2254
|
-
if (String(hrefResult?.href || '').includes(CHAT_URL_TOKEN)) {
|
|
2267
|
+
if (!forceNavigate && String(hrefResult?.href || '').includes(CHAT_URL_TOKEN)) {
|
|
2255
2268
|
return { changed: false, href: hrefResult?.href || '' };
|
|
2256
2269
|
}
|
|
2257
2270
|
|
|
2258
|
-
await this.chromeClient.callFunction(browserNavigateToChatIndex);
|
|
2271
|
+
await this.chromeClient.callFunction(browserNavigateToChatIndex, { force: forceNavigate });
|
|
2259
2272
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
2260
2273
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2261
|
-
const state = await this.
|
|
2262
|
-
|
|
2274
|
+
const state = await this.getPageState();
|
|
2275
|
+
const onChatPage = String(state?.href || '').includes(CHAT_URL_TOKEN);
|
|
2276
|
+
const ready = !waitForReadyState || String(state?.readyState || '').toLowerCase() === String(waitForReadyState).toLowerCase();
|
|
2277
|
+
if (onChatPage && ready) {
|
|
2263
2278
|
return { changed: true, href: state.href };
|
|
2264
2279
|
}
|
|
2265
2280
|
}
|
|
@@ -44,6 +44,11 @@ const CLI_FILE_PATH = fileURLToPath(import.meta.url);
|
|
|
44
44
|
const MINIMAL_TERMINAL_PATTERNS = [/^进度: /, /^候选人结果: /];
|
|
45
45
|
const CHAT_INDEX_URL = 'https://www.zhipin.com/web/chat/index';
|
|
46
46
|
const CHAT_START_REQUIRED_FIELDS = ['job', 'start_from', 'target_count', 'criteria'];
|
|
47
|
+
const CHAT_PAGE_RENAVIGATE_MAX_ATTEMPTS = 3;
|
|
48
|
+
const CHAT_PAGE_HYDRATION_MAX_ATTEMPTS = 12;
|
|
49
|
+
const CHAT_PAGE_HYDRATION_DELAY_MS = 250;
|
|
50
|
+
const CHAT_JOB_LIST_MAX_ATTEMPTS = 16;
|
|
51
|
+
const CHAT_JOB_LIST_DELAY_MS = 250;
|
|
47
52
|
|
|
48
53
|
function sanitizePathToken(value, fallback = 'run') {
|
|
49
54
|
const token = String(value || '')
|
|
@@ -70,6 +75,10 @@ function shouldPrintToMinimalTerminal(message) {
|
|
|
70
75
|
return MINIMAL_TERMINAL_PATTERNS.some((pattern) => pattern.test(message));
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
function sleep(ms) {
|
|
79
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
async function createRunLogger(dataDir, { runId = '', detachedWorker = false } = {}) {
|
|
74
83
|
const logsDir = path.join(dataDir, 'logs');
|
|
75
84
|
await mkdir(logsDir, { recursive: true });
|
|
@@ -497,7 +506,7 @@ function resolveJobSelection(jobs, input) {
|
|
|
497
506
|
}
|
|
498
507
|
|
|
499
508
|
async function promptRunProfile({ page, persistentProfile, overrides }) {
|
|
500
|
-
const jobs = await page
|
|
509
|
+
const jobs = await resolveJobsWithRetry({ page });
|
|
501
510
|
if (!Array.isArray(jobs) || jobs.length === 0) {
|
|
502
511
|
throw new Error('未解析到岗位列表,请确认岗位下拉可见。');
|
|
503
512
|
}
|
|
@@ -646,11 +655,109 @@ function buildPreparePendingQuestions(args, jobs = []) {
|
|
|
646
655
|
return pendingQuestions;
|
|
647
656
|
}
|
|
648
657
|
|
|
658
|
+
function hasHydratedChatShell(pageState, jobs = []) {
|
|
659
|
+
const hasChatList =
|
|
660
|
+
Boolean(pageState?.hasListContainer)
|
|
661
|
+
|| Number(pageState?.listItemCount || 0) > 0;
|
|
662
|
+
return hasChatList || (Array.isArray(jobs) && jobs.length > 0);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function waitForChatShellHydration({
|
|
666
|
+
page,
|
|
667
|
+
maxAttempts = CHAT_PAGE_HYDRATION_MAX_ATTEMPTS,
|
|
668
|
+
delayMs = CHAT_PAGE_HYDRATION_DELAY_MS,
|
|
669
|
+
} = {}) {
|
|
670
|
+
let lastPageState = null;
|
|
671
|
+
let lastJobs = [];
|
|
672
|
+
let lastError = null;
|
|
673
|
+
|
|
674
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
675
|
+
try {
|
|
676
|
+
lastPageState = await page.ensureOnChatPage();
|
|
677
|
+
} catch (error) {
|
|
678
|
+
lastError = error;
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
try {
|
|
683
|
+
lastJobs = await page.listJobs();
|
|
684
|
+
} catch (error) {
|
|
685
|
+
lastError = error;
|
|
686
|
+
lastJobs = [];
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (hasHydratedChatShell(lastPageState, lastJobs)) {
|
|
690
|
+
return {
|
|
691
|
+
pageState: lastPageState,
|
|
692
|
+
jobs: lastJobs,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (attempt < maxAttempts) {
|
|
697
|
+
await sleep(delayMs);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const lastErrorMessage = String(lastError?.message || lastError || '');
|
|
702
|
+
if (/ACTIVE_TAB_IS_NOT_BOSS_CHAT_PAGE/.test(lastErrorMessage)) {
|
|
703
|
+
throw lastError;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return {
|
|
707
|
+
pageState: lastPageState,
|
|
708
|
+
jobs: lastJobs,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function resolveJobsWithRetry({
|
|
713
|
+
page,
|
|
714
|
+
maxAttempts = CHAT_JOB_LIST_MAX_ATTEMPTS,
|
|
715
|
+
delayMs = CHAT_JOB_LIST_DELAY_MS,
|
|
716
|
+
} = {}) {
|
|
717
|
+
let lastError = null;
|
|
718
|
+
|
|
719
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
720
|
+
try {
|
|
721
|
+
const jobs = await page.listJobs();
|
|
722
|
+
if (Array.isArray(jobs) && jobs.length > 0) {
|
|
723
|
+
return jobs;
|
|
724
|
+
}
|
|
725
|
+
} catch (error) {
|
|
726
|
+
lastError = error;
|
|
727
|
+
const message = String(error?.message || error || '');
|
|
728
|
+
if (/ACTIVE_TAB_IS_NOT_BOSS_CHAT_PAGE/.test(message)) {
|
|
729
|
+
throw error;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const hydrated = await waitForChatShellHydration({
|
|
734
|
+
page,
|
|
735
|
+
maxAttempts: 1,
|
|
736
|
+
delayMs,
|
|
737
|
+
});
|
|
738
|
+
if (Array.isArray(hydrated?.jobs) && hydrated.jobs.length > 0) {
|
|
739
|
+
return hydrated.jobs;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (attempt < maxAttempts) {
|
|
743
|
+
await sleep(delayMs);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (lastError) {
|
|
748
|
+
throw lastError;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return [];
|
|
752
|
+
}
|
|
753
|
+
|
|
649
754
|
async function connectBossChatPage(chromeClient) {
|
|
650
755
|
const isBossDomainTarget = (target) =>
|
|
651
756
|
target?.type === 'page' && /zhipin\.com/i.test(String(target?.url || ''));
|
|
652
757
|
let target = null;
|
|
653
758
|
let recoveredToChatIndex = false;
|
|
759
|
+
let blankChatPage = false;
|
|
760
|
+
let renavigateAttempts = 0;
|
|
654
761
|
|
|
655
762
|
try {
|
|
656
763
|
target = await chromeClient.connect(BossChatPage.targetMatcher);
|
|
@@ -659,15 +766,53 @@ async function connectBossChatPage(chromeClient) {
|
|
|
659
766
|
}
|
|
660
767
|
|
|
661
768
|
const page = new BossChatPage(chromeClient);
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
769
|
+
for (let attempt = 1; attempt <= CHAT_PAGE_RENAVIGATE_MAX_ATTEMPTS + 1; attempt += 1) {
|
|
770
|
+
try {
|
|
771
|
+
await page.ensureReady();
|
|
772
|
+
return {
|
|
773
|
+
target,
|
|
774
|
+
page,
|
|
775
|
+
recoveredToChatIndex,
|
|
776
|
+
blankChatPage,
|
|
777
|
+
renavigateAttempts,
|
|
778
|
+
};
|
|
779
|
+
} catch (error) {
|
|
780
|
+
const message = String(error?.message || error || '');
|
|
781
|
+
if (/CHAT_LIST_CONTAINER_NOT_FOUND/.test(message)) {
|
|
782
|
+
blankChatPage = true;
|
|
783
|
+
const hydrated = await waitForChatShellHydration({ page });
|
|
784
|
+
if (hasHydratedChatShell(hydrated?.pageState, hydrated?.jobs)) {
|
|
785
|
+
return {
|
|
786
|
+
target,
|
|
787
|
+
page,
|
|
788
|
+
recoveredToChatIndex,
|
|
789
|
+
blankChatPage,
|
|
790
|
+
renavigateAttempts,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
const canRetry =
|
|
795
|
+
/ACTIVE_TAB_IS_NOT_BOSS_CHAT_PAGE|CHAT_LIST_CONTAINER_NOT_FOUND/.test(message)
|
|
796
|
+
&& attempt <= CHAT_PAGE_RENAVIGATE_MAX_ATTEMPTS;
|
|
797
|
+
|
|
798
|
+
if (!canRetry) {
|
|
799
|
+
if (/CHAT_LIST_CONTAINER_NOT_FOUND/.test(message)) {
|
|
800
|
+
await page.ensureOnChatPage();
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
throw error;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
await page.recoverToChatIndex({
|
|
807
|
+
forceNavigate: true,
|
|
808
|
+
waitForReadyState: 'complete',
|
|
809
|
+
});
|
|
810
|
+
recoveredToChatIndex = true;
|
|
811
|
+
renavigateAttempts += 1;
|
|
812
|
+
}
|
|
668
813
|
}
|
|
669
814
|
|
|
670
|
-
return { target, page, recoveredToChatIndex };
|
|
815
|
+
return { target, page, recoveredToChatIndex, blankChatPage, renavigateAttempts };
|
|
671
816
|
}
|
|
672
817
|
|
|
673
818
|
async function handlePrepareRunCommand(args, dataDir) {
|
|
@@ -704,8 +849,8 @@ async function handlePrepareRunCommand(args, dataDir) {
|
|
|
704
849
|
let chromeClient = null;
|
|
705
850
|
try {
|
|
706
851
|
chromeClient = new ChromeClient(mergedProfile.chrome.port);
|
|
707
|
-
const { target, page, recoveredToChatIndex } = await connectBossChatPage(chromeClient);
|
|
708
|
-
const jobs = await page
|
|
852
|
+
const { target, page, recoveredToChatIndex, blankChatPage, renavigateAttempts } = await connectBossChatPage(chromeClient);
|
|
853
|
+
const jobs = await resolveJobsWithRetry({ page });
|
|
709
854
|
if (!Array.isArray(jobs) || jobs.length === 0) {
|
|
710
855
|
return {
|
|
711
856
|
status: 'FAILED',
|
|
@@ -723,6 +868,8 @@ async function handlePrepareRunCommand(args, dataDir) {
|
|
|
723
868
|
page_url: CHAT_INDEX_URL,
|
|
724
869
|
connected_target: target?.url || '',
|
|
725
870
|
recovered_to_chat_index: recoveredToChatIndex,
|
|
871
|
+
blank_chat_page: blankChatPage,
|
|
872
|
+
renavigate_attempts: renavigateAttempts,
|
|
726
873
|
required_fields: CHAT_START_REQUIRED_FIELDS.slice(),
|
|
727
874
|
defaults: {
|
|
728
875
|
profile: String(args.profile || 'default').trim() || 'default',
|
|
@@ -1159,10 +1306,13 @@ async function executeRunCommand(args, dataDir) {
|
|
|
1159
1306
|
|
|
1160
1307
|
chromeClient = new ChromeClient(persistentProfile.chrome.port);
|
|
1161
1308
|
|
|
1162
|
-
const { target, page, recoveredToChatIndex } = await connectBossChatPage(chromeClient);
|
|
1309
|
+
const { target, page, recoveredToChatIndex, blankChatPage, renavigateAttempts } = await connectBossChatPage(chromeClient);
|
|
1163
1310
|
logger.log(`已连接 Chrome tab: ${target.title || target.url}`);
|
|
1164
1311
|
if (recoveredToChatIndex) {
|
|
1165
|
-
logger.log(
|
|
1312
|
+
logger.log(`检测到页面不符合预期,已重新跳转到 ${CHAT_INDEX_URL} 并等待加载完成。attempts=${renavigateAttempts}`);
|
|
1313
|
+
}
|
|
1314
|
+
if (blankChatPage) {
|
|
1315
|
+
logger.log('检测到聊天页处于空白未初始化状态,将继续通过岗位选择和首位候选人预热来恢复列表。');
|
|
1166
1316
|
}
|
|
1167
1317
|
|
|
1168
1318
|
const runProfile = await promptRunProfile({
|
|
@@ -1389,12 +1539,22 @@ async function main() {
|
|
|
1389
1539
|
}
|
|
1390
1540
|
}
|
|
1391
1541
|
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1542
|
+
export const __testables = {
|
|
1543
|
+
connectBossChatPage,
|
|
1544
|
+
hasHydratedChatShell,
|
|
1545
|
+
promptRunProfile,
|
|
1546
|
+
resolveJobsWithRetry,
|
|
1547
|
+
waitForChatShellHydration,
|
|
1548
|
+
};
|
|
1549
|
+
|
|
1550
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === CLI_FILE_PATH) {
|
|
1551
|
+
main().catch((error) => {
|
|
1552
|
+
const runLogPath = String(error?.runLogPath || '').trim();
|
|
1553
|
+
if (runLogPath) {
|
|
1554
|
+
console.error(`执行失败,详细日志见: ${runLogPath}`);
|
|
1555
|
+
} else {
|
|
1556
|
+
console.error(`执行失败: ${error.message}`);
|
|
1557
|
+
}
|
|
1558
|
+
process.exitCode = 1;
|
|
1559
|
+
});
|
|
1560
|
+
}
|