@reconcrap/boss-recommend-mcp 2.1.11 → 2.1.13
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 +6 -2
- package/package.json +1 -1
- package/skills/boss-chat/SKILL.md +2 -0
- package/skills/boss-recommend-pipeline/SKILL.md +2 -0
- package/skills/boss-recruit-pipeline/SKILL.md +2 -0
- package/src/chat-mcp.js +2174 -2174
- package/src/cli.js +129 -85
- package/src/core/reporting/legacy-csv.js +4 -15
- package/src/core/screening/index.js +33 -13
- package/src/domains/chat/run-service.js +83 -83
- package/src/domains/recommend/run-service.js +15 -15
- package/src/index.js +238 -18
package/src/cli.js
CHANGED
|
@@ -57,12 +57,15 @@ const bossLoginUrl = "https://www.zhipin.com/web/user/?ka=bticket";
|
|
|
57
57
|
const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|history-sync|settings\/syncSetup)/i;
|
|
58
58
|
const bossLoginUrlPattern = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
|
|
59
59
|
const bossLoginTitlePattern = /登录|signin|扫码登录|BOSS直聘登录/i;
|
|
60
|
-
const supportedMcpClients = ["generic", "cursor", "trae", "claudecode", "openclaw", "qclaw"];
|
|
60
|
+
const supportedMcpClients = ["generic", "cursor", "trae", "claudecode", "openclaw", "qclaw"];
|
|
61
61
|
const defaultMcpServerName = "boss-recommend";
|
|
62
|
+
const bossChatMcpServerName = "boss-chat";
|
|
63
|
+
const bossRecruitMcpServerName = "boss-recruit";
|
|
62
64
|
const defaultMcpCommand = "npx";
|
|
63
65
|
const recommendMcpPackageName = "@reconcrap/boss-recommend-mcp";
|
|
64
66
|
const recommendMcpBinaryName = "boss-recommend-mcp";
|
|
65
67
|
const globalMcpWrapperFileName = "boss-recommend-mcp-mcp-server";
|
|
68
|
+
const mcpToolsetEnv = "BOSS_RECOMMEND_MCP_TOOLSET";
|
|
66
69
|
const supportedMcpLaunchModes = ["npx", "global-wrapper"];
|
|
67
70
|
const autoSyncSkipCommands = new Set(["install", "install-skill", "where", "help", "--help", "-h", "list-jobs", "jobs", "recommend-jobs"]);
|
|
68
71
|
const externalMcpTargetsEnv = "BOSS_RECOMMEND_MCP_CONFIG_TARGETS";
|
|
@@ -807,12 +810,35 @@ function shouldDefaultRecommendDetachedMcpEnv(options = {}) {
|
|
|
807
810
|
return client === "openclaw"
|
|
808
811
|
|| client === "qclaw"
|
|
809
812
|
|| agent === "openclaw"
|
|
810
|
-
|| agent === "qclaw";
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
function
|
|
814
|
-
|
|
815
|
-
|
|
813
|
+
|| agent === "qclaw";
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function normalizeMcpToolsetOption(value) {
|
|
817
|
+
const raw = String(value || "").trim().toLowerCase().replace(/[\s_]+/g, "-");
|
|
818
|
+
if (!raw || raw === "all") return "";
|
|
819
|
+
if (raw === "boss-recommend" || raw === "recommend-page") return "recommend";
|
|
820
|
+
if (raw === "boss-chat" || raw === "chat-only" || raw === "chat-page") return "chat";
|
|
821
|
+
if (raw === "boss-recruit" || raw === "search" || raw === "search-page" || raw === "recruit-page") return "recruit";
|
|
822
|
+
if (["recommend", "chat", "recruit"].includes(raw)) return raw;
|
|
823
|
+
throw new Error(`Unsupported --toolset value: ${raw}. Supported: recommend, chat, recruit, all`);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function getMcpToolsetEnv(options = {}) {
|
|
827
|
+
const toolset = normalizeMcpToolsetOption(options.toolset || options["toolset"]);
|
|
828
|
+
return toolset ? { [mcpToolsetEnv]: toolset } : {};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function shouldUseSplitBossMcpServers(options = {}) {
|
|
832
|
+
if (options["server-name"] || options.serverName || options.toolset || options["toolset"]) return false;
|
|
833
|
+
if (options.split === true || options["split-tools"] === true || options.splitTools === true) return true;
|
|
834
|
+
const client = normalizeMcpClientName(options.client);
|
|
835
|
+
const agent = normalizeAgentName(options.agent);
|
|
836
|
+
return client === "trae" || agent === "trae" || agent === "trae-cn";
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function getDefaultMcpEnv(options = {}) {
|
|
840
|
+
return shouldDefaultRecommendDetachedMcpEnv(options)
|
|
841
|
+
? { ...detachedRecommendMcpEnv }
|
|
816
842
|
: {};
|
|
817
843
|
}
|
|
818
844
|
|
|
@@ -834,6 +860,7 @@ function buildMcpLaunchConfig(options = {}) {
|
|
|
834
860
|
};
|
|
835
861
|
const mergedEnv = {
|
|
836
862
|
...getDefaultMcpEnv(options),
|
|
863
|
+
...getMcpToolsetEnv(options),
|
|
837
864
|
...(isPlainObject(env) ? env : {})
|
|
838
865
|
};
|
|
839
866
|
if (Object.keys(mergedEnv).length > 0) {
|
|
@@ -853,19 +880,36 @@ function buildMcpLaunchConfig(options = {}) {
|
|
|
853
880
|
? ["start"]
|
|
854
881
|
: buildDefaultMcpArgs(options);
|
|
855
882
|
const launchConfig = { command, args: launchArgs };
|
|
856
|
-
const mergedEnv = {
|
|
857
|
-
...getDefaultMcpEnv(options),
|
|
858
|
-
...(
|
|
859
|
-
|
|
883
|
+
const mergedEnv = {
|
|
884
|
+
...getDefaultMcpEnv(options),
|
|
885
|
+
...getMcpToolsetEnv(options),
|
|
886
|
+
...(isPlainObject(env) ? env : {})
|
|
887
|
+
};
|
|
860
888
|
if (Object.keys(mergedEnv).length > 0) {
|
|
861
889
|
launchConfig.env = mergedEnv;
|
|
862
890
|
}
|
|
863
891
|
return launchConfig;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
function
|
|
867
|
-
if (
|
|
868
|
-
return
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function buildBossMcpServerEntries(options = {}) {
|
|
895
|
+
if (shouldUseSplitBossMcpServers(options)) {
|
|
896
|
+
return {
|
|
897
|
+
[defaultMcpServerName]: buildMcpLaunchConfig({ ...options, toolset: "recommend" }),
|
|
898
|
+
[bossChatMcpServerName]: buildMcpLaunchConfig({ ...options, toolset: "chat" }),
|
|
899
|
+
[bossRecruitMcpServerName]: buildMcpLaunchConfig({ ...options, toolset: "recruit" })
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
const serverName = typeof options["server-name"] === "string" && options["server-name"].trim()
|
|
903
|
+
? options["server-name"].trim()
|
|
904
|
+
: defaultMcpServerName;
|
|
905
|
+
return {
|
|
906
|
+
[serverName]: buildMcpLaunchConfig(options)
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function mergeExistingMcpEntryEnv(existingEntry, launchConfig) {
|
|
911
|
+
if (!isPlainObject(existingEntry?.env) || !isPlainObject(launchConfig)) {
|
|
912
|
+
return launchConfig;
|
|
869
913
|
}
|
|
870
914
|
return {
|
|
871
915
|
...launchConfig,
|
|
@@ -874,28 +918,21 @@ function mergeExistingMcpEntryEnv(existingEntry, launchConfig) {
|
|
|
874
918
|
...(isPlainObject(launchConfig.env) ? launchConfig.env : {})
|
|
875
919
|
}
|
|
876
920
|
};
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
function buildMcpConfigFileContent(options = {}) {
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
}
|
|
893
|
-
return {
|
|
894
|
-
mcpServers: {
|
|
895
|
-
[serverName]: launchConfig
|
|
896
|
-
}
|
|
897
|
-
};
|
|
898
|
-
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function buildMcpConfigFileContent(options = {}) {
|
|
924
|
+
const servers = buildBossMcpServerEntries(options);
|
|
925
|
+
if (normalizeMcpClientName(options.client) === "qclaw") {
|
|
926
|
+
return {
|
|
927
|
+
mcp: {
|
|
928
|
+
servers
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
return {
|
|
933
|
+
mcpServers: servers
|
|
934
|
+
};
|
|
935
|
+
}
|
|
899
936
|
|
|
900
937
|
function writeMcpConfigFiles(options = {}) {
|
|
901
938
|
const clients = parseMcpClientTargets(options.client);
|
|
@@ -1039,25 +1076,28 @@ function getMcpServersFromConfig(config = {}, useQClawShape = false) {
|
|
|
1039
1076
|
return {};
|
|
1040
1077
|
}
|
|
1041
1078
|
|
|
1042
|
-
function mergeMcpServerConfigFile(filePath, options = {}) {
|
|
1043
|
-
const current = readJsonObjectFileSafe(filePath);
|
|
1044
|
-
const useQClawShape = isQClawMcpConfigTarget(filePath, options, current);
|
|
1045
|
-
const nextConfig = buildMcpConfigFileContent({ ...options, client: useQClawShape ? "qclaw" : options.client });
|
|
1046
|
-
const nextServers = useQClawShape ? nextConfig.mcp?.servers : nextConfig.mcpServers;
|
|
1047
|
-
const
|
|
1048
|
-
const
|
|
1049
|
-
const
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1079
|
+
function mergeMcpServerConfigFile(filePath, options = {}) {
|
|
1080
|
+
const current = readJsonObjectFileSafe(filePath);
|
|
1081
|
+
const useQClawShape = isQClawMcpConfigTarget(filePath, options, current);
|
|
1082
|
+
const nextConfig = buildMcpConfigFileContent({ ...options, client: useQClawShape ? "qclaw" : options.client });
|
|
1083
|
+
const nextServers = useQClawShape ? nextConfig.mcp?.servers : nextConfig.mcpServers;
|
|
1084
|
+
const serverNames = Object.keys(nextServers || {});
|
|
1085
|
+
const nextBossServerNames = new Set(serverNames);
|
|
1086
|
+
const existingServers = getMcpServersFromConfig(current, useQClawShape);
|
|
1087
|
+
const mergedBossServers = {};
|
|
1088
|
+
for (const serverName of serverNames) {
|
|
1089
|
+
mergedBossServers[serverName] = mergeExistingMcpEntryEnv(
|
|
1090
|
+
existingServers[serverName],
|
|
1091
|
+
nextServers?.[serverName] || buildMcpLaunchConfig(options)
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
const retainedServers = {};
|
|
1095
|
+
const migratedLegacyServers = [];
|
|
1096
|
+
for (const [name, config] of Object.entries(existingServers)) {
|
|
1097
|
+
if (nextBossServerNames.has(name)) continue;
|
|
1098
|
+
if (isBossMcpServerEntry(name, config)) {
|
|
1099
|
+
migratedLegacyServers.push(name);
|
|
1100
|
+
continue;
|
|
1061
1101
|
}
|
|
1062
1102
|
retainedServers[name] = config;
|
|
1063
1103
|
}
|
|
@@ -1065,20 +1105,20 @@ function mergeMcpServerConfigFile(filePath, options = {}) {
|
|
|
1065
1105
|
? {
|
|
1066
1106
|
...current,
|
|
1067
1107
|
mcp: {
|
|
1068
|
-
...(current?.mcp && typeof current.mcp === "object" && !Array.isArray(current.mcp) ? current.mcp : {}),
|
|
1069
|
-
servers: {
|
|
1070
|
-
...retainedServers,
|
|
1071
|
-
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
: {
|
|
1076
|
-
...current,
|
|
1077
|
-
mcpServers: {
|
|
1078
|
-
...retainedServers,
|
|
1079
|
-
|
|
1080
|
-
}
|
|
1081
|
-
};
|
|
1108
|
+
...(current?.mcp && typeof current.mcp === "object" && !Array.isArray(current.mcp) ? current.mcp : {}),
|
|
1109
|
+
servers: {
|
|
1110
|
+
...retainedServers,
|
|
1111
|
+
...mergedBossServers
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
: {
|
|
1116
|
+
...current,
|
|
1117
|
+
mcpServers: {
|
|
1118
|
+
...retainedServers,
|
|
1119
|
+
...mergedBossServers
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1082
1122
|
|
|
1083
1123
|
ensureDir(path.dirname(filePath));
|
|
1084
1124
|
const before = pathExists(filePath) ? fs.readFileSync(filePath, "utf8") : "";
|
|
@@ -1088,14 +1128,15 @@ function mergeMcpServerConfigFile(filePath, options = {}) {
|
|
|
1088
1128
|
backupFile = `${filePath}.boss-mcp-migration-${new Date().toISOString().replace(/[:.]/g, "-")}.bak`;
|
|
1089
1129
|
fs.writeFileSync(backupFile, before, "utf8");
|
|
1090
1130
|
}
|
|
1091
|
-
fs.writeFileSync(filePath, JSON.stringify(merged, null, 2), "utf8");
|
|
1092
|
-
const updated = before.trim() !== next.trim()
|
|
1093
|
-
return {
|
|
1094
|
-
file: filePath,
|
|
1095
|
-
server:
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1131
|
+
fs.writeFileSync(filePath, JSON.stringify(merged, null, 2), "utf8");
|
|
1132
|
+
const updated = before.trim() !== next.trim();
|
|
1133
|
+
return {
|
|
1134
|
+
file: filePath,
|
|
1135
|
+
server: serverNames[0] || defaultMcpServerName,
|
|
1136
|
+
servers: serverNames,
|
|
1137
|
+
config_shape: useQClawShape ? "qclaw" : "mcpServers",
|
|
1138
|
+
updated,
|
|
1139
|
+
migrated_legacy_servers: migratedLegacyServers,
|
|
1099
1140
|
backup_file: backupFile
|
|
1100
1141
|
};
|
|
1101
1142
|
}
|
|
@@ -1108,12 +1149,13 @@ function installExternalMcpConfigs(options = {}) {
|
|
|
1108
1149
|
try {
|
|
1109
1150
|
const existed = pathExists(target);
|
|
1110
1151
|
const merged = mergeMcpServerConfigFile(target, options);
|
|
1111
|
-
applied.push({
|
|
1112
|
-
file: target,
|
|
1113
|
-
server: merged.server,
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1152
|
+
applied.push({
|
|
1153
|
+
file: target,
|
|
1154
|
+
server: merged.server,
|
|
1155
|
+
servers: merged.servers,
|
|
1156
|
+
created: !existed,
|
|
1157
|
+
updated: merged.updated,
|
|
1158
|
+
migrated_legacy_servers: merged.migrated_legacy_servers,
|
|
1117
1159
|
backup_file: merged.backup_file
|
|
1118
1160
|
});
|
|
1119
1161
|
} catch (error) {
|
|
@@ -2718,6 +2760,7 @@ function printHelp() {
|
|
|
2718
2760
|
console.log(" boss-recommend-mcp launch-chrome Launch or reuse Chrome debug instance and open Boss recommend page");
|
|
2719
2761
|
console.log(" boss-recommend-mcp where Print installed package, skill, and config paths");
|
|
2720
2762
|
console.log(" boss-recommend-mcp install --mcp-launch global-wrapper Use ~/.boss-recommend-mcp/bin wrapper so npm global upgrades affect MCP hosts");
|
|
2763
|
+
console.log(" boss-recommend-mcp start --toolset recommend Start a narrowed MCP server (all|recommend|chat|recruit)");
|
|
2721
2764
|
console.log("");
|
|
2722
2765
|
console.log("Run command:");
|
|
2723
2766
|
console.log(" boss-recommend-mcp prepare-run --instruction \"...\" --overrides-file overrides.json --confirmation-file confirmation.json --rest-level medium");
|
|
@@ -3397,6 +3440,7 @@ export const __testables = {
|
|
|
3397
3440
|
buildBossChatCliInput,
|
|
3398
3441
|
buildDefaultMcpArgs,
|
|
3399
3442
|
buildMcpLaunchConfig,
|
|
3443
|
+
buildMcpConfigFileContent,
|
|
3400
3444
|
ensureGlobalMcpWrapper,
|
|
3401
3445
|
getGlobalMcpWrapperPath,
|
|
3402
3446
|
collectRuntimeDirectories,
|
|
@@ -16,9 +16,7 @@ export const LEGACY_RESULT_HEADER = [
|
|
|
16
16
|
"简历来源",
|
|
17
17
|
"原始判定通过",
|
|
18
18
|
"最终判定通过",
|
|
19
|
-
"
|
|
20
|
-
"证据命中数",
|
|
21
|
-
"证据门控降级",
|
|
19
|
+
"LLM thinking_level",
|
|
22
20
|
"错误码",
|
|
23
21
|
"错误信息",
|
|
24
22
|
"候选人ID",
|
|
@@ -182,12 +180,6 @@ function firstBoolean(...values) {
|
|
|
182
180
|
return "";
|
|
183
181
|
}
|
|
184
182
|
|
|
185
|
-
function evidenceCount(llm = {}) {
|
|
186
|
-
if (Number.isFinite(llm.evidence_count)) return llm.evidence_count;
|
|
187
|
-
if (Array.isArray(llm.evidence)) return llm.evidence.length;
|
|
188
|
-
return "";
|
|
189
|
-
}
|
|
190
|
-
|
|
191
183
|
function actionResultText(row = {}) {
|
|
192
184
|
const action = row.post_action || row.action || {};
|
|
193
185
|
if (action.requested === true && !action.skipped) {
|
|
@@ -258,10 +250,10 @@ export function legacyScreenResultRow(row = {}) {
|
|
|
258
250
|
? "passed"
|
|
259
251
|
: "skipped";
|
|
260
252
|
const cot = firstText(
|
|
261
|
-
llm.reasoning_content,
|
|
262
|
-
llm.raw_reasoning_content,
|
|
263
253
|
llm.decision_cot,
|
|
264
254
|
llm.cot,
|
|
255
|
+
llm.reasoning_content,
|
|
256
|
+
llm.raw_reasoning_content,
|
|
265
257
|
llm.raw_model_output,
|
|
266
258
|
llm.raw_content,
|
|
267
259
|
row.decision_cot,
|
|
@@ -276,7 +268,6 @@ export function legacyScreenResultRow(row = {}) {
|
|
|
276
268
|
candidate.source,
|
|
277
269
|
screening.candidate?.source
|
|
278
270
|
);
|
|
279
|
-
const totalEvidence = evidenceCount(llm);
|
|
280
271
|
return [
|
|
281
272
|
identity.name,
|
|
282
273
|
identity.school,
|
|
@@ -290,9 +281,7 @@ export function legacyScreenResultRow(row = {}) {
|
|
|
290
281
|
cvSource,
|
|
291
282
|
rawPassed,
|
|
292
283
|
finalPassed,
|
|
293
|
-
|
|
294
|
-
totalEvidence,
|
|
295
|
-
"",
|
|
284
|
+
firstText(llm.provider?.thinking_level),
|
|
296
285
|
row.error_code || error.code || error.name || (llm.error ? "LLM_SCREENING_ERROR" : ""),
|
|
297
286
|
row.error_message || error.message || llm.error || "",
|
|
298
287
|
candidate.id || row.candidate_id || "",
|
|
@@ -1477,7 +1477,7 @@ export function llmResultToScreening(llmResult, candidate) {
|
|
|
1477
1477
|
}
|
|
1478
1478
|
|
|
1479
1479
|
export function isRecoverableLlmScreeningError(error) {
|
|
1480
|
-
return /(?:LLM response missing boolean passed decision|LLM response was not valid JSON)/i
|
|
1480
|
+
return /(?:LLM response missing boolean passed decision|LLM response missing brief summary|LLM response was not valid JSON)/i
|
|
1481
1481
|
.test(String(error?.message || error || ""));
|
|
1482
1482
|
}
|
|
1483
1483
|
|
|
@@ -1504,6 +1504,7 @@ export function createFailedLlmScreeningResult(error) {
|
|
|
1504
1504
|
export function buildScreeningLlmMessages({
|
|
1505
1505
|
candidate,
|
|
1506
1506
|
criteria,
|
|
1507
|
+
thinkingLevel = "low",
|
|
1507
1508
|
imageEvidence = null,
|
|
1508
1509
|
imagePaths = [],
|
|
1509
1510
|
imageInputs = null,
|
|
@@ -1512,6 +1513,8 @@ export function buildScreeningLlmMessages({
|
|
|
1512
1513
|
}) {
|
|
1513
1514
|
const safeCriteria = normalizeText(criteria || "判断候选人是否符合本次招聘筛选标准");
|
|
1514
1515
|
const safeText = String(candidate?.text?.raw || candidate?.text || "");
|
|
1516
|
+
const normalizedThinkingLevel = normalizeLlmThinkingLevel(thinkingLevel) || "low";
|
|
1517
|
+
const requestSummary = normalizedThinkingLevel === "current";
|
|
1515
1518
|
const images = Array.isArray(imageInputs)
|
|
1516
1519
|
? imageInputs
|
|
1517
1520
|
: buildScreeningLlmImageInputs({
|
|
@@ -1520,6 +1523,11 @@ export function buildScreeningLlmMessages({
|
|
|
1520
1523
|
maxImages,
|
|
1521
1524
|
detail: imageDetail
|
|
1522
1525
|
});
|
|
1526
|
+
const outputShape = requestSummary
|
|
1527
|
+
? "4) 只返回 JSON,格式为:"
|
|
1528
|
+
+ "{\"passed\": true/false, \"summary\": \"少于100个中文词的筛选总结\"}"
|
|
1529
|
+
: "4) 只返回 JSON,格式为:"
|
|
1530
|
+
+ "{\"passed\": true/false}";
|
|
1523
1531
|
const prompt =
|
|
1524
1532
|
`请根据以下标准判断候选人是否通过筛选。\n\n筛选标准:\n${safeCriteria}\n\n`
|
|
1525
1533
|
+ `候选人信息:\n${safeText || "候选人的完整简历信息在后续截图中,请按截图顺序阅读。"}\n\n`
|
|
@@ -1529,9 +1537,10 @@ export function buildScreeningLlmMessages({
|
|
|
1529
1537
|
+ "要求:\n"
|
|
1530
1538
|
+ "1) 只能依据候选人信息或截图中真实出现的内容判断。\n"
|
|
1531
1539
|
+ "2) 若证据不足或截图无法确认,必须返回 passed=false。\n"
|
|
1532
|
-
+
|
|
1533
|
-
|
|
1534
|
-
|
|
1540
|
+
+ (requestSummary
|
|
1541
|
+
? "3) summary 必须为少于100个中文词的简短筛选总结,可包含核心依据和主要风险;不要输出推理过程。\n"
|
|
1542
|
+
: "3) 不要输出评估原因、证据列表、解释或额外文字。\n")
|
|
1543
|
+
+ outputShape;
|
|
1535
1544
|
const userContent = images.length
|
|
1536
1545
|
? [
|
|
1537
1546
|
{ type: "text", text: prompt },
|
|
@@ -1546,7 +1555,9 @@ export function buildScreeningLlmMessages({
|
|
|
1546
1555
|
role: "system",
|
|
1547
1556
|
content:
|
|
1548
1557
|
"你是一位严谨的招聘筛选助手。必须完整阅读输入内容,严禁编造不存在的候选人经历。"
|
|
1549
|
-
+
|
|
1558
|
+
+ (requestSummary
|
|
1559
|
+
? "只能返回严格 JSON。必须包含 passed 和 summary;summary 用中文,少于100个词,只概括筛选结论、核心依据和主要风险,不要输出推理过程。"
|
|
1560
|
+
: "只能返回严格 JSON,不要输出原因、证据或额外文字。")
|
|
1550
1561
|
},
|
|
1551
1562
|
{
|
|
1552
1563
|
role: "user",
|
|
@@ -1608,6 +1619,7 @@ async function callScreeningLlmWithProvider({
|
|
|
1608
1619
|
messages: buildScreeningLlmMessages({
|
|
1609
1620
|
candidate,
|
|
1610
1621
|
criteria,
|
|
1622
|
+
thinkingLevel,
|
|
1611
1623
|
imageInputs
|
|
1612
1624
|
})
|
|
1613
1625
|
};
|
|
@@ -1665,20 +1677,27 @@ async function callScreeningLlmWithProvider({
|
|
|
1665
1677
|
if (passed === null) {
|
|
1666
1678
|
throw new Error(`LLM response missing boolean passed decision: ${content.slice(0, 240)}`);
|
|
1667
1679
|
}
|
|
1680
|
+
const normalizedThinkingLevel = normalizeLlmThinkingLevel(thinkingLevel) || "low";
|
|
1681
|
+
const summary = normalizeBlockText(parsed?.summary || parsed?.screen_summary || parsed?.brief_summary);
|
|
1682
|
+
if (normalizedThinkingLevel === "current" && !summary) {
|
|
1683
|
+
throw new Error(`LLM response missing brief summary for current thinking level: ${content.slice(0, 240)}`);
|
|
1684
|
+
}
|
|
1668
1685
|
const evidence = Array.isArray(parsed?.evidence)
|
|
1669
1686
|
? parsed.evidence.map(normalizeText).filter(Boolean)
|
|
1670
1687
|
: [];
|
|
1671
|
-
const decisionCot =
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1688
|
+
const decisionCot = normalizedThinkingLevel === "current"
|
|
1689
|
+
? summary
|
|
1690
|
+
: (firstReasoningText([
|
|
1691
|
+
parsed?.cot,
|
|
1692
|
+
parsed?.decision_cot,
|
|
1693
|
+
parsed?.reasoning,
|
|
1694
|
+
parsed?.chain_of_thought,
|
|
1695
|
+
reasoningContent
|
|
1696
|
+
].map(normalizeBlockText).filter(Boolean)) || reasoningContent);
|
|
1678
1697
|
const providerName = normalizeText(config.llmProviderName || config.name || config.label || config.id);
|
|
1679
1698
|
const providerIndex = Number.isFinite(Number(config.llmProviderIndex)) ? Number(config.llmProviderIndex) : 0;
|
|
1680
1699
|
const providerCount = Number.isFinite(Number(config.llmProviderCount)) ? Number(config.llmProviderCount) : 1;
|
|
1681
|
-
|
|
1700
|
+
const result = {
|
|
1682
1701
|
ok: true,
|
|
1683
1702
|
provider: {
|
|
1684
1703
|
baseUrl: redactBaseUrl(baseUrl),
|
|
@@ -1708,6 +1727,7 @@ async function callScreeningLlmWithProvider({
|
|
|
1708
1727
|
provider_attempt_count: attempt,
|
|
1709
1728
|
screened_at: nowIso()
|
|
1710
1729
|
};
|
|
1730
|
+
return result;
|
|
1711
1731
|
} catch (error) {
|
|
1712
1732
|
lastError = error;
|
|
1713
1733
|
if (attempt >= maxAttempts || !isRetryableLlmRequestError(error)) {
|