@reconcrap/boss-recommend-mcp 1.3.25 → 1.3.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
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,7 @@ 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";
|
|
19
20
|
import { BossChatPage } from "../vendor/boss-chat-cli/src/browser/chat-page.js";
|
|
20
21
|
import { LlmClient, parseLlmJson } from "../vendor/boss-chat-cli/src/services/llm.js";
|
|
21
22
|
|
|
@@ -684,6 +685,118 @@ async function testBossChatCliShouldSupportRunAndFollowUpParsing() {
|
|
|
684
685
|
});
|
|
685
686
|
}
|
|
686
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
|
+
|
|
687
800
|
function testBossChatLlmEvidenceGateShouldDemoteMissingEvidence() {
|
|
688
801
|
const parsed = parseLlmJson(
|
|
689
802
|
JSON.stringify({
|
|
@@ -902,6 +1015,233 @@ async function testBossChatLlmShouldApplyThinkingDefaultsAndOverrides() {
|
|
|
902
1015
|
assert.deepEqual(responsesPayload.reasoning, { effort: "low" });
|
|
903
1016
|
}
|
|
904
1017
|
|
|
1018
|
+
async function testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime() {
|
|
1019
|
+
const calls = [];
|
|
1020
|
+
const page = {
|
|
1021
|
+
async ensureReady() {
|
|
1022
|
+
calls.push("ensureReady");
|
|
1023
|
+
return { hasListContainer: true, listItemCount: 1 };
|
|
1024
|
+
},
|
|
1025
|
+
async activatePrimaryChatLabel(label) {
|
|
1026
|
+
calls.push(`activatePrimaryChatLabel:${label}`);
|
|
1027
|
+
return { changed: false, verified: true, activeLabel: label };
|
|
1028
|
+
},
|
|
1029
|
+
async selectJob(jobSelection) {
|
|
1030
|
+
calls.push(`selectJob:${jobSelection.label}`);
|
|
1031
|
+
return jobSelection;
|
|
1032
|
+
},
|
|
1033
|
+
async activateUnreadFilter() {
|
|
1034
|
+
calls.push("activateUnreadFilter");
|
|
1035
|
+
return { changed: false, verified: true, activeLabel: "未读" };
|
|
1036
|
+
},
|
|
1037
|
+
async primeConversationByFirstCandidate() {
|
|
1038
|
+
calls.push("primeConversationByFirstCandidate:1");
|
|
1039
|
+
return {
|
|
1040
|
+
candidate: {
|
|
1041
|
+
customerId: "1001",
|
|
1042
|
+
name: "候选人A",
|
|
1043
|
+
sourceJob: "算法工程师",
|
|
1044
|
+
domIndex: 0,
|
|
1045
|
+
},
|
|
1046
|
+
totalVisibleCandidates: 1,
|
|
1047
|
+
readyState: {
|
|
1048
|
+
hasOnlineResume: true,
|
|
1049
|
+
hasAskResume: true,
|
|
1050
|
+
hasAttachmentResume: false,
|
|
1051
|
+
},
|
|
1052
|
+
};
|
|
1053
|
+
},
|
|
1054
|
+
async getLoadedCustomers() {
|
|
1055
|
+
calls.push("getLoadedCustomers:1");
|
|
1056
|
+
return [];
|
|
1057
|
+
},
|
|
1058
|
+
async closeResumeModalDomOnce() {
|
|
1059
|
+
return {
|
|
1060
|
+
closed: true,
|
|
1061
|
+
method: "already-closed",
|
|
1062
|
+
finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
|
|
1063
|
+
};
|
|
1064
|
+
},
|
|
1065
|
+
};
|
|
1066
|
+
const stateStore = {
|
|
1067
|
+
async load() {},
|
|
1068
|
+
hasAny() {
|
|
1069
|
+
return false;
|
|
1070
|
+
},
|
|
1071
|
+
async record() {},
|
|
1072
|
+
};
|
|
1073
|
+
const app = new BossChatApp({
|
|
1074
|
+
page,
|
|
1075
|
+
llmClient: {},
|
|
1076
|
+
interaction: {
|
|
1077
|
+
async sleepRange() {},
|
|
1078
|
+
async maybeRest() {},
|
|
1079
|
+
},
|
|
1080
|
+
resumeCaptureService: {},
|
|
1081
|
+
stateStore,
|
|
1082
|
+
reportStore: {
|
|
1083
|
+
async write() {
|
|
1084
|
+
return "report.json";
|
|
1085
|
+
},
|
|
1086
|
+
},
|
|
1087
|
+
logger: { log() {} },
|
|
1088
|
+
dryRun: true,
|
|
1089
|
+
artifactRootDir: os.tmpdir(),
|
|
1090
|
+
resumeOpenCooldownMs: 0,
|
|
1091
|
+
});
|
|
1092
|
+
app.processCustomer = async (_customer, _profile, _runId, options = {}) => {
|
|
1093
|
+
calls.push(`processCustomer:${options.skipCardClick === true ? "skip" : "click"}`);
|
|
1094
|
+
return {
|
|
1095
|
+
name: "候选人A",
|
|
1096
|
+
passed: false,
|
|
1097
|
+
requested: false,
|
|
1098
|
+
reason: "skip",
|
|
1099
|
+
error: "",
|
|
1100
|
+
artifacts: {},
|
|
1101
|
+
};
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
const summary = await app.run({
|
|
1105
|
+
screeningCriteria: "有 AI 项目经验",
|
|
1106
|
+
targetCount: 1,
|
|
1107
|
+
startFrom: "unread",
|
|
1108
|
+
jobSelection: { label: "算法工程师", value: "job-1" },
|
|
1109
|
+
chrome: { port: 9222 },
|
|
1110
|
+
llm: { model: "gpt-test" },
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
assert.deepEqual(calls.slice(0, 5), [
|
|
1114
|
+
"ensureReady",
|
|
1115
|
+
"activatePrimaryChatLabel:全部",
|
|
1116
|
+
"selectJob:算法工程师",
|
|
1117
|
+
"activateUnreadFilter",
|
|
1118
|
+
"primeConversationByFirstCandidate:1",
|
|
1119
|
+
]);
|
|
1120
|
+
assert.equal(calls.includes("processCustomer:skip"), true);
|
|
1121
|
+
assert.equal(summary.inspected, 1);
|
|
1122
|
+
assert.equal(summary.skipped, 1);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
async function testBossChatAppShouldRestoreListContextAfterRecovery() {
|
|
1126
|
+
const calls = [];
|
|
1127
|
+
let primeCount = 0;
|
|
1128
|
+
let loadedCount = 0;
|
|
1129
|
+
const page = {
|
|
1130
|
+
async ensureReady() {
|
|
1131
|
+
return { hasListContainer: true, listItemCount: 1 };
|
|
1132
|
+
},
|
|
1133
|
+
async activatePrimaryChatLabel(label) {
|
|
1134
|
+
calls.push(`activatePrimaryChatLabel:${label}`);
|
|
1135
|
+
return { changed: false, verified: true, activeLabel: label };
|
|
1136
|
+
},
|
|
1137
|
+
async selectJob(jobSelection) {
|
|
1138
|
+
calls.push(`selectJob:${jobSelection.label}`);
|
|
1139
|
+
return jobSelection;
|
|
1140
|
+
},
|
|
1141
|
+
async activateUnreadFilter() {
|
|
1142
|
+
calls.push("activateUnreadFilter");
|
|
1143
|
+
return { changed: false, verified: true, activeLabel: "未读" };
|
|
1144
|
+
},
|
|
1145
|
+
async primeConversationByFirstCandidate() {
|
|
1146
|
+
primeCount += 1;
|
|
1147
|
+
calls.push(`primeConversationByFirstCandidate:${primeCount}`);
|
|
1148
|
+
if (primeCount === 1) {
|
|
1149
|
+
throw new Error("NO_FIRST_CANDIDATE");
|
|
1150
|
+
}
|
|
1151
|
+
return {
|
|
1152
|
+
candidate: {
|
|
1153
|
+
customerId: "1002",
|
|
1154
|
+
name: "候选人B",
|
|
1155
|
+
sourceJob: "算法工程师",
|
|
1156
|
+
domIndex: 0,
|
|
1157
|
+
},
|
|
1158
|
+
totalVisibleCandidates: 1,
|
|
1159
|
+
readyState: {
|
|
1160
|
+
hasOnlineResume: true,
|
|
1161
|
+
hasAskResume: true,
|
|
1162
|
+
hasAttachmentResume: false,
|
|
1163
|
+
},
|
|
1164
|
+
};
|
|
1165
|
+
},
|
|
1166
|
+
async getLoadedCustomers() {
|
|
1167
|
+
loadedCount += 1;
|
|
1168
|
+
calls.push(`getLoadedCustomers:${loadedCount}`);
|
|
1169
|
+
if (loadedCount === 1) {
|
|
1170
|
+
throw new Error("CHAT_CARD_LIST_NOT_FOUND");
|
|
1171
|
+
}
|
|
1172
|
+
return [];
|
|
1173
|
+
},
|
|
1174
|
+
async recoverToChatIndex() {
|
|
1175
|
+
calls.push("recoverToChatIndex");
|
|
1176
|
+
return { changed: true, href: "https://www.zhipin.com/web/chat/index" };
|
|
1177
|
+
},
|
|
1178
|
+
async closeResumeModalDomOnce() {
|
|
1179
|
+
return {
|
|
1180
|
+
closed: true,
|
|
1181
|
+
method: "already-closed",
|
|
1182
|
+
finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
|
|
1183
|
+
};
|
|
1184
|
+
},
|
|
1185
|
+
};
|
|
1186
|
+
const stateStore = {
|
|
1187
|
+
async load() {},
|
|
1188
|
+
hasAny() {
|
|
1189
|
+
return false;
|
|
1190
|
+
},
|
|
1191
|
+
async record() {},
|
|
1192
|
+
};
|
|
1193
|
+
const app = new BossChatApp({
|
|
1194
|
+
page,
|
|
1195
|
+
llmClient: {},
|
|
1196
|
+
interaction: {
|
|
1197
|
+
async sleepRange() {},
|
|
1198
|
+
async maybeRest() {},
|
|
1199
|
+
},
|
|
1200
|
+
resumeCaptureService: {},
|
|
1201
|
+
stateStore,
|
|
1202
|
+
reportStore: {
|
|
1203
|
+
async write() {
|
|
1204
|
+
return "report.json";
|
|
1205
|
+
},
|
|
1206
|
+
},
|
|
1207
|
+
logger: { log() {} },
|
|
1208
|
+
dryRun: true,
|
|
1209
|
+
artifactRootDir: os.tmpdir(),
|
|
1210
|
+
resumeOpenCooldownMs: 0,
|
|
1211
|
+
});
|
|
1212
|
+
app.processCustomer = async (_customer, _profile, _runId, options = {}) => {
|
|
1213
|
+
calls.push(`processCustomer:${options.skipCardClick === true ? "skip" : "click"}`);
|
|
1214
|
+
return {
|
|
1215
|
+
name: "候选人B",
|
|
1216
|
+
passed: false,
|
|
1217
|
+
requested: false,
|
|
1218
|
+
reason: "skip",
|
|
1219
|
+
error: "",
|
|
1220
|
+
artifacts: {},
|
|
1221
|
+
};
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
const summary = await app.run({
|
|
1225
|
+
screeningCriteria: "有 AI 项目经验",
|
|
1226
|
+
targetCount: 1,
|
|
1227
|
+
startFrom: "unread",
|
|
1228
|
+
jobSelection: { label: "算法工程师", value: "job-1" },
|
|
1229
|
+
chrome: { port: 9222 },
|
|
1230
|
+
llm: { model: "gpt-test" },
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
assert.equal(calls.filter((item) => item === "activatePrimaryChatLabel:全部").length, 2);
|
|
1234
|
+
const recoverIndex = calls.indexOf("recoverToChatIndex");
|
|
1235
|
+
assert.equal(recoverIndex >= 0, true);
|
|
1236
|
+
assert.equal(calls[recoverIndex + 1], "activatePrimaryChatLabel:全部");
|
|
1237
|
+
assert.equal(calls[recoverIndex + 2], "selectJob:算法工程师");
|
|
1238
|
+
assert.equal(calls[recoverIndex + 3], "activateUnreadFilter");
|
|
1239
|
+
assert.equal(calls[recoverIndex + 4], "primeConversationByFirstCandidate:2");
|
|
1240
|
+
assert.equal(calls.includes("processCustomer:skip"), true);
|
|
1241
|
+
assert.equal(summary.inspected, 1);
|
|
1242
|
+
assert.equal(summary.skipped, 1);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
905
1245
|
async function testBossChatAppShouldPersistEvidenceArtifacts() {
|
|
906
1246
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-artifacts-"));
|
|
907
1247
|
await mkdir(tempDir, { recursive: true });
|
|
@@ -1030,10 +1370,15 @@ async function main() {
|
|
|
1030
1370
|
await testBossChatRecoverToChatIndexShouldForceNavigateAndWaitForCompleteLoad();
|
|
1031
1371
|
await testBossChatMcpToolsShouldValidateAndRoute();
|
|
1032
1372
|
await testBossChatCliShouldSupportRunAndFollowUpParsing();
|
|
1373
|
+
await testVendorBossChatCliShouldWaitForHydratedChatShell();
|
|
1374
|
+
await testVendorBossChatCliShouldRetryJobListDuringPromptRunProfile();
|
|
1375
|
+
testCliShouldPinInstalledPackageVersionInGeneratedMcpConfig();
|
|
1033
1376
|
testBossChatLlmEvidenceGateShouldDemoteMissingEvidence();
|
|
1034
1377
|
testBossChatLlmEvidenceGateShouldDemoteUnmatchedEvidence();
|
|
1035
1378
|
await testBossChatLlmTextChunkFallbackShouldWork();
|
|
1036
1379
|
await testBossChatLlmShouldApplyThinkingDefaultsAndOverrides();
|
|
1380
|
+
await testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime();
|
|
1381
|
+
await testBossChatAppShouldRestoreListContextAfterRecovery();
|
|
1037
1382
|
await testBossChatAppShouldPersistEvidenceArtifacts();
|
|
1038
1383
|
console.log("boss-chat tests passed");
|
|
1039
1384
|
}
|
|
@@ -142,6 +142,16 @@ export class BossChatApp {
|
|
|
142
142
|
} catch {}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
async restoreListContext(profile) {
|
|
146
|
+
if (typeof this.page.activatePrimaryChatLabel === 'function') {
|
|
147
|
+
await this.page.activatePrimaryChatLabel('全部');
|
|
148
|
+
}
|
|
149
|
+
await this.page.selectJob(profile.jobSelection);
|
|
150
|
+
return profile.startFrom === 'all'
|
|
151
|
+
? this.page.activateAllFilter()
|
|
152
|
+
: this.page.activateUnreadFilter();
|
|
153
|
+
}
|
|
154
|
+
|
|
145
155
|
async run(profile) {
|
|
146
156
|
const startedAt = new Date().toISOString();
|
|
147
157
|
const runId = runToken(new Date());
|
|
@@ -158,12 +168,7 @@ export class BossChatApp {
|
|
|
158
168
|
} catch (error) {
|
|
159
169
|
this.logger.log(`页面就绪检查告警:${error?.message || error},将继续执行预热恢复流程。`);
|
|
160
170
|
}
|
|
161
|
-
await this.
|
|
162
|
-
|
|
163
|
-
const filterResult =
|
|
164
|
-
startFrom === 'all'
|
|
165
|
-
? await this.page.activateAllFilter()
|
|
166
|
-
: await this.page.activateUnreadFilter();
|
|
171
|
+
const filterResult = await this.restoreListContext(profile);
|
|
167
172
|
await this.interaction.sleepRange(420, 160);
|
|
168
173
|
this.logger.log('预热步骤:准备点击首位人选初始化聊天容器...');
|
|
169
174
|
let primedCustomer = null;
|
|
@@ -270,6 +275,16 @@ export class BossChatApp {
|
|
|
270
275
|
`页面恢复:changed=${recover.changed} | href=${recover.href || 'unknown'},准备重新预热并继续。`,
|
|
271
276
|
);
|
|
272
277
|
await this.interaction.sleepRange(900, 220);
|
|
278
|
+
const recoveredFilterResult = await this.restoreListContext(profile);
|
|
279
|
+
this.logger.log(
|
|
280
|
+
`恢复后列表上下文:岗位=${profile.jobSelection?.label || profile.jobSelection?.value || '未知'};列表范围: ${filterLabel}${
|
|
281
|
+
recoveredFilterResult.changed
|
|
282
|
+
? recoveredFilterResult.verified === false
|
|
283
|
+
? '(已尝试切换,未验证 active)'
|
|
284
|
+
: '(已切换)'
|
|
285
|
+
: '(已在目标筛选)'
|
|
286
|
+
}${recoveredFilterResult?.activeLabel ? ` | active=${recoveredFilterResult.activeLabel}` : ''}`,
|
|
287
|
+
);
|
|
273
288
|
const prime = await this.page.primeConversationByFirstCandidate();
|
|
274
289
|
const candidate = prime?.candidate || {};
|
|
275
290
|
const candidateBase = {
|
|
@@ -276,6 +276,63 @@ async function browserActivateFilterTab(label) {
|
|
|
276
276
|
};
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
async function browserActivatePrimaryChatLabel(label) {
|
|
280
|
+
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
281
|
+
const isVisible = (node) => {
|
|
282
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
283
|
+
const rect = node.getBoundingClientRect();
|
|
284
|
+
if (rect.width <= 2 || rect.height <= 2) return false;
|
|
285
|
+
const style = getComputedStyle(node);
|
|
286
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || '1') > 0.01;
|
|
287
|
+
};
|
|
288
|
+
const matchesLabel = (node) => {
|
|
289
|
+
const text = normalize(node?.textContent || '');
|
|
290
|
+
return text === label || text.startsWith(`${label}(`);
|
|
291
|
+
};
|
|
292
|
+
const hasActiveState = (node) =>
|
|
293
|
+
node instanceof HTMLElement &&
|
|
294
|
+
(
|
|
295
|
+
node.classList.contains('active') ||
|
|
296
|
+
node.classList.contains('selected') ||
|
|
297
|
+
node.getAttribute('aria-selected') === 'true' ||
|
|
298
|
+
node.getAttribute('data-active') === 'true'
|
|
299
|
+
);
|
|
300
|
+
const getLabels = () =>
|
|
301
|
+
Array.from(document.querySelectorAll('.label-list .chat-label-item, .chat-label-item'))
|
|
302
|
+
.filter((node) => node instanceof HTMLElement && isVisible(node));
|
|
303
|
+
const getActiveLabel = () => {
|
|
304
|
+
const activeNode = getLabels().find((node) => hasActiveState(node));
|
|
305
|
+
return normalize(activeNode?.textContent || '');
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const labels = getLabels();
|
|
309
|
+
const candidates = labels.filter((node) => matchesLabel(node));
|
|
310
|
+
if (candidates.length === 0) {
|
|
311
|
+
return { ok: false, error: `PRIMARY_CHAT_LABEL_NOT_FOUND:${label}` };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const activeLabelBefore = getActiveLabel();
|
|
315
|
+
if (candidates.some((node) => hasActiveState(node)) || matchesLabel({ textContent: activeLabelBefore })) {
|
|
316
|
+
return { ok: true, changed: false, verified: true, activeLabel: activeLabelBefore || label };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
candidates[0].click();
|
|
320
|
+
await new Promise((resolve) => window.setTimeout(resolve, 420));
|
|
321
|
+
|
|
322
|
+
const refreshedCandidates = getLabels().filter((node) => matchesLabel(node));
|
|
323
|
+
const activeLabelAfter = getActiveLabel();
|
|
324
|
+
const verified =
|
|
325
|
+
matchesLabel({ textContent: activeLabelAfter }) ||
|
|
326
|
+
refreshedCandidates.some((node) => hasActiveState(node));
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
ok: true,
|
|
330
|
+
changed: true,
|
|
331
|
+
verified,
|
|
332
|
+
activeLabel: activeLabelAfter || '',
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
279
336
|
function browserGetLoadedCustomers() {
|
|
280
337
|
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
281
338
|
const isScrollable = (el) =>
|
|
@@ -2317,6 +2374,14 @@ export class BossChatPage {
|
|
|
2317
2374
|
return result;
|
|
2318
2375
|
}
|
|
2319
2376
|
|
|
2377
|
+
async activatePrimaryChatLabel(label = '全部') {
|
|
2378
|
+
const result = await this.chromeClient.callFunction(browserActivatePrimaryChatLabel, label);
|
|
2379
|
+
if (!result?.ok) {
|
|
2380
|
+
throw new Error(result?.error || `ACTIVATE_PRIMARY_CHAT_LABEL_FAILED:${label}`);
|
|
2381
|
+
}
|
|
2382
|
+
return result;
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2320
2385
|
async getLoadedCustomers() {
|
|
2321
2386
|
let lastError = 'GET_LOADED_CUSTOMERS_FAILED';
|
|
2322
2387
|
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
@@ -45,6 +45,10 @@ 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
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;
|
|
48
52
|
|
|
49
53
|
function sanitizePathToken(value, fallback = 'run') {
|
|
50
54
|
const token = String(value || '')
|
|
@@ -71,6 +75,10 @@ function shouldPrintToMinimalTerminal(message) {
|
|
|
71
75
|
return MINIMAL_TERMINAL_PATTERNS.some((pattern) => pattern.test(message));
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
function sleep(ms) {
|
|
79
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
async function createRunLogger(dataDir, { runId = '', detachedWorker = false } = {}) {
|
|
75
83
|
const logsDir = path.join(dataDir, 'logs');
|
|
76
84
|
await mkdir(logsDir, { recursive: true });
|
|
@@ -498,7 +506,7 @@ function resolveJobSelection(jobs, input) {
|
|
|
498
506
|
}
|
|
499
507
|
|
|
500
508
|
async function promptRunProfile({ page, persistentProfile, overrides }) {
|
|
501
|
-
const jobs = await page
|
|
509
|
+
const jobs = await resolveJobsWithRetry({ page });
|
|
502
510
|
if (!Array.isArray(jobs) || jobs.length === 0) {
|
|
503
511
|
throw new Error('未解析到岗位列表,请确认岗位下拉可见。');
|
|
504
512
|
}
|
|
@@ -647,6 +655,102 @@ function buildPreparePendingQuestions(args, jobs = []) {
|
|
|
647
655
|
return pendingQuestions;
|
|
648
656
|
}
|
|
649
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
|
+
|
|
650
754
|
async function connectBossChatPage(chromeClient) {
|
|
651
755
|
const isBossDomainTarget = (target) =>
|
|
652
756
|
target?.type === 'page' && /zhipin\.com/i.test(String(target?.url || ''));
|
|
@@ -674,13 +778,25 @@ async function connectBossChatPage(chromeClient) {
|
|
|
674
778
|
};
|
|
675
779
|
} catch (error) {
|
|
676
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
|
+
}
|
|
677
794
|
const canRetry =
|
|
678
795
|
/ACTIVE_TAB_IS_NOT_BOSS_CHAT_PAGE|CHAT_LIST_CONTAINER_NOT_FOUND/.test(message)
|
|
679
796
|
&& attempt <= CHAT_PAGE_RENAVIGATE_MAX_ATTEMPTS;
|
|
680
797
|
|
|
681
798
|
if (!canRetry) {
|
|
682
799
|
if (/CHAT_LIST_CONTAINER_NOT_FOUND/.test(message)) {
|
|
683
|
-
blankChatPage = true;
|
|
684
800
|
await page.ensureOnChatPage();
|
|
685
801
|
break;
|
|
686
802
|
}
|
|
@@ -734,7 +850,7 @@ async function handlePrepareRunCommand(args, dataDir) {
|
|
|
734
850
|
try {
|
|
735
851
|
chromeClient = new ChromeClient(mergedProfile.chrome.port);
|
|
736
852
|
const { target, page, recoveredToChatIndex, blankChatPage, renavigateAttempts } = await connectBossChatPage(chromeClient);
|
|
737
|
-
const jobs = await page
|
|
853
|
+
const jobs = await resolveJobsWithRetry({ page });
|
|
738
854
|
if (!Array.isArray(jobs) || jobs.length === 0) {
|
|
739
855
|
return {
|
|
740
856
|
status: 'FAILED',
|
|
@@ -1423,12 +1539,22 @@ async function main() {
|
|
|
1423
1539
|
}
|
|
1424
1540
|
}
|
|
1425
1541
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
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
|
+
}
|