@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.23

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.
Files changed (67) hide show
  1. package/README.md +5 -2
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/config/screening-config.example.json +33 -33
  4. package/package.json +2 -1
  5. package/scripts/install-macos.sh +280 -280
  6. package/scripts/postinstall.cjs +44 -44
  7. package/skills/boss-chat/README.md +42 -42
  8. package/skills/boss-chat/SKILL.md +106 -106
  9. package/skills/boss-recommend-pipeline/README.md +13 -13
  10. package/skills/boss-recommend-pipeline/SKILL.md +219 -214
  11. package/skills/boss-recruit-pipeline/README.md +19 -19
  12. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  13. package/src/chat-mcp.js +127 -127
  14. package/src/chat-runtime-config.js +775 -775
  15. package/src/cli.js +573 -573
  16. package/src/core/boss-cards/index.js +199 -199
  17. package/src/core/browser/index.js +2416 -2385
  18. package/src/core/capture/index.js +1201 -1201
  19. package/src/core/cv-acquisition/index.js +238 -238
  20. package/src/core/cv-capture-target/index.js +299 -299
  21. package/src/core/greet-quota/index.js +71 -71
  22. package/src/core/infinite-list/index.js +1315 -1306
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +44 -27
  25. package/src/core/run/timing.js +33 -33
  26. package/src/core/screening/index.js +2135 -2135
  27. package/src/core/self-heal/index.js +973 -973
  28. package/src/core/self-heal/viewport.js +564 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/cards.js +137 -137
  31. package/src/domains/chat/constants.js +9 -9
  32. package/src/domains/chat/detail.js +113 -113
  33. package/src/domains/chat/index.js +7 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +122 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +571 -571
  38. package/src/domains/common/account-rights-panel.js +314 -314
  39. package/src/domains/common/recovery-settle.js +159 -159
  40. package/src/domains/recommend/actions.js +472 -472
  41. package/src/domains/recommend/cards.js +243 -243
  42. package/src/domains/recommend/colleague-contact.js +333 -333
  43. package/src/domains/recommend/constants.js +228 -159
  44. package/src/domains/recommend/detail.js +655 -646
  45. package/src/domains/recommend/filters.js +748 -377
  46. package/src/domains/recommend/index.js +4 -3
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -0
  49. package/src/domains/recommend/refresh.js +575 -351
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +1616 -878
  52. package/src/domains/recommend/scopes.js +246 -246
  53. package/src/domains/recruit/actions.js +277 -277
  54. package/src/domains/recruit/cards.js +74 -74
  55. package/src/domains/recruit/constants.js +236 -236
  56. package/src/domains/recruit/detail.js +588 -588
  57. package/src/domains/recruit/index.js +9 -9
  58. package/src/domains/recruit/instruction-parser.js +866 -866
  59. package/src/domains/recruit/refresh.js +45 -45
  60. package/src/domains/recruit/roots.js +68 -68
  61. package/src/domains/recruit/run-service.js +1620 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +121 -4
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +1174 -914
  66. package/src/recommend-scheduler.js +535 -469
  67. package/src/recruit-mcp.js +2121 -2121
@@ -1,775 +1,775 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import process from "node:process";
5
- import { normalizeHumanBehaviorOptions } from "./core/browser/index.js";
6
-
7
- const BOSS_CHAT_RUNTIME_SUBDIR = "boss-chat";
8
- const TARGET_COUNT_WRAPPER_KEYS = ["target_count", "targetCount", "value", "count", "limit"];
9
- const SCREEN_CONFIG_TEMPLATE_DEFAULTS = Object.freeze({
10
- baseUrl: "https://api.openai.com/v1",
11
- apiKey: "replace-with-your-api-key",
12
- model: "gpt-4.1-mini"
13
- });
14
- const LLM_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "auto", "current"]);
15
- const LLM_SCREENING_STRATEGIES = new Set(["single_pass", "fast_first_verified"]);
16
-
17
- export const TARGET_COUNT_CANONICAL_ALL = "all";
18
- export const TARGET_COUNT_ACCEPTED_EXAMPLES = [TARGET_COUNT_CANONICAL_ALL, -1, 20, "全部候选人"];
19
-
20
- function normalizeText(value) {
21
- return String(value || "").replace(/\s+/g, " ").trim();
22
- }
23
-
24
- function getStateHome() {
25
- return process.env.BOSS_RECOMMEND_HOME
26
- ? path.resolve(process.env.BOSS_RECOMMEND_HOME)
27
- : path.join(os.homedir(), ".boss-recommend-mcp");
28
- }
29
-
30
- function getCodexHome() {
31
- return process.env.CODEX_HOME
32
- ? path.resolve(process.env.CODEX_HOME)
33
- : path.join(os.homedir(), ".codex");
34
- }
35
-
36
- function pathExists(targetPath) {
37
- try {
38
- return Boolean(targetPath) && fs.existsSync(targetPath);
39
- } catch {
40
- return false;
41
- }
42
- }
43
-
44
- function isRootDirectory(workspaceRoot) {
45
- const root = path.resolve(String(workspaceRoot || ""));
46
- return path.parse(root).root.toLowerCase() === root.toLowerCase();
47
- }
48
-
49
- function isEphemeralNpxWorkspaceRoot(workspaceRoot) {
50
- const root = path.resolve(String(workspaceRoot || ""));
51
- const normalized = root.replace(/\\/g, "/").toLowerCase();
52
- return (
53
- normalized.includes("/appdata/local/npm-cache/_npx/")
54
- || normalized.includes("/node_modules/@reconcrap/boss-recommend-mcp")
55
- );
56
- }
57
-
58
- function isSystemDirectoryWorkspaceRoot(workspaceRoot) {
59
- const root = path.resolve(String(workspaceRoot || ""));
60
- const normalized = root.replace(/\\/g, "/").toLowerCase();
61
- if (process.platform === "win32") {
62
- return (
63
- normalized.endsWith("/windows")
64
- || normalized.endsWith("/windows/system32")
65
- || normalized.endsWith("/windows/syswow64")
66
- || normalized.endsWith("/program files")
67
- || normalized.endsWith("/program files (x86)")
68
- );
69
- }
70
- return (
71
- normalized === "/system"
72
- || normalized.startsWith("/system/")
73
- || normalized === "/usr"
74
- || normalized.startsWith("/usr/")
75
- || normalized === "/bin"
76
- || normalized.startsWith("/bin/")
77
- || normalized === "/sbin"
78
- || normalized.startsWith("/sbin/")
79
- );
80
- }
81
-
82
- function shouldIgnoreWorkspaceConfigRoot(workspaceRoot) {
83
- const root = path.resolve(String(workspaceRoot || process.cwd()));
84
- const home = path.resolve(os.homedir());
85
- return (
86
- isEphemeralNpxWorkspaceRoot(root)
87
- || isRootDirectory(root)
88
- || root.toLowerCase() === home.toLowerCase()
89
- || isSystemDirectoryWorkspaceRoot(root)
90
- );
91
- }
92
-
93
- function resolveWorkspaceConfigCandidates(workspaceRoot) {
94
- const root = path.resolve(String(workspaceRoot || process.cwd()));
95
- if (shouldIgnoreWorkspaceConfigRoot(root)) return [];
96
- const directPath = path.join(root, "config", "screening-config.json");
97
- const nestedPath = path.join(root, "boss-recommend-mcp", "config", "screening-config.json");
98
- const candidates = [directPath];
99
- if (path.basename(root).toLowerCase() !== "boss-recommend-mcp") {
100
- candidates.push(nestedPath);
101
- }
102
- return Array.from(new Set(candidates));
103
- }
104
-
105
- function getUserConfigPath() {
106
- return path.join(getStateHome(), "screening-config.json");
107
- }
108
-
109
- function getLegacyUserConfigPath() {
110
- return path.join(getCodexHome(), "boss-recommend-mcp", "screening-config.json");
111
- }
112
-
113
- function getUserCalibrationPath() {
114
- return path.join(getCodexHome(), "boss-recommend-mcp", "favorite-calibration.json");
115
- }
116
-
117
- function buildScreenConfigCandidateMap(workspaceRoot) {
118
- return {
119
- env_path: process.env.BOSS_RECOMMEND_SCREEN_CONFIG
120
- ? path.resolve(process.env.BOSS_RECOMMEND_SCREEN_CONFIG)
121
- : null,
122
- workspace_paths: resolveWorkspaceConfigCandidates(workspaceRoot),
123
- user_path: getUserConfigPath(),
124
- legacy_path: getLegacyUserConfigPath()
125
- };
126
- }
127
-
128
- function resolveScreenConfigCandidates(workspaceRoot) {
129
- const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
130
- return [
131
- candidateMap.env_path,
132
- candidateMap.user_path,
133
- ...candidateMap.workspace_paths,
134
- candidateMap.legacy_path
135
- ].filter(Boolean);
136
- }
137
-
138
- function canWriteDirectory(targetDir) {
139
- try {
140
- fs.mkdirSync(targetDir, { recursive: true });
141
- fs.accessSync(targetDir, fs.constants.W_OK);
142
- return true;
143
- } catch {
144
- return false;
145
- }
146
- }
147
-
148
- function resolveWritableScreenConfigPath(workspaceRoot) {
149
- const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
150
- const workspacePreferred = candidateMap.workspace_paths?.[0] || null;
151
- if (candidateMap.env_path) return candidateMap.env_path;
152
- if (candidateMap.user_path && canWriteDirectory(path.dirname(candidateMap.user_path))) {
153
- return candidateMap.user_path;
154
- }
155
- if (workspacePreferred && canWriteDirectory(path.dirname(workspacePreferred))) {
156
- return workspacePreferred;
157
- }
158
- if (workspacePreferred) return workspacePreferred;
159
- return candidateMap.user_path || candidateMap.legacy_path;
160
- }
161
-
162
- function resolveScreenConfigPath(workspaceRoot) {
163
- const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
164
- if (candidateMap.env_path) return candidateMap.env_path;
165
- if (candidateMap.user_path && pathExists(candidateMap.user_path)) return candidateMap.user_path;
166
- const existingWorkspacePath = candidateMap.workspace_paths.find((item) => pathExists(item));
167
- if (existingWorkspacePath) return existingWorkspacePath;
168
- return resolveWritableScreenConfigPath(workspaceRoot) || candidateMap.legacy_path;
169
- }
170
-
171
- function readJsonFile(filePath) {
172
- if (!filePath || !pathExists(filePath)) return null;
173
- try {
174
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
175
- } catch {
176
- return null;
177
- }
178
- }
179
-
180
- function isUsableFeaturedCalibrationFile(filePath) {
181
- const parsed = readJsonFile(filePath);
182
- return Boolean(
183
- parsed
184
- && typeof parsed === "object"
185
- && !Array.isArray(parsed)
186
- && parsed.favoritePosition
187
- && Number.isFinite(parsed.favoritePosition.pageX)
188
- && Number.isFinite(parsed.favoritePosition.pageY)
189
- );
190
- }
191
-
192
- function resolveFeaturedCalibrationPath(workspaceRoot) {
193
- const fromEnv = normalizeText(process.env.BOSS_RECOMMEND_CALIBRATION_FILE || "");
194
- if (fromEnv) return path.resolve(fromEnv);
195
-
196
- const configResolution = resolveBossScreeningConfig(workspaceRoot);
197
- const configPath = configResolution.config_path || resolveScreenConfigPath(workspaceRoot) || getUserConfigPath();
198
- const config = readJsonFile(configPath);
199
- const calibrationFile = normalizeText(config?.calibrationFile || "");
200
- if (calibrationFile && configPath) {
201
- return path.resolve(path.dirname(configPath), calibrationFile);
202
- }
203
-
204
- return getUserCalibrationPath();
205
- }
206
-
207
- function resolveRecruitCalibrationScriptPath(workspaceRoot) {
208
- const fromEnv = normalizeText(process.env.BOSS_RECOMMEND_RECRUIT_CALIBRATION_SCRIPT || "");
209
- const workspaceResolved = path.resolve(String(workspaceRoot || process.cwd()));
210
- const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
211
- const candidates = [
212
- fromEnv,
213
- path.join(workspaceResolved, "..", "..", "boss recruit pipeline", "boss-recruit-mcp", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs"),
214
- path.join(workspaceResolved, "..", "boss recruit pipeline", "boss-recruit-mcp", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs"),
215
- path.join(appData, "npm", "node_modules", "@reconcrap", "boss-recruit-mcp", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs")
216
- ].filter(Boolean).map((item) => path.resolve(item));
217
-
218
- for (const candidate of new Set(candidates)) {
219
- if (pathExists(candidate)) return candidate;
220
- }
221
- return null;
222
- }
223
-
224
- function parsePositiveInteger(raw, fallback = null) {
225
- const parsed = Number.parseInt(String(raw || ""), 10);
226
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
227
- }
228
-
229
- function parseConfigNumber(raw, fallback = null) {
230
- if (raw === undefined || raw === null || raw === "") return fallback;
231
- const parsed = Number(raw);
232
- return Number.isFinite(parsed) ? parsed : fallback;
233
- }
234
-
235
- function parseConfigBoolean(raw, fallback = false) {
236
- if (typeof raw === "boolean") return raw;
237
- const normalized = normalizeText(raw).toLowerCase();
238
- if (["true", "1", "yes", "y", "on", "enabled"].includes(normalized)) return true;
239
- if (["false", "0", "no", "n", "off", "disabled"].includes(normalized)) return false;
240
- return fallback;
241
- }
242
-
243
- function readFirstOwn(source, keys = []) {
244
- if (!source || typeof source !== "object") return undefined;
245
- for (const key of keys) {
246
- if (Object.prototype.hasOwnProperty.call(source, key)) return source[key];
247
- }
248
- return undefined;
249
- }
250
-
251
- function parseOptionalConfigBoolean(raw) {
252
- if (raw === undefined || raw === null || raw === "") return null;
253
- return parseConfigBoolean(raw, null);
254
- }
255
-
256
- function applyHumanBehaviorProfileDefaults(target, profileRaw) {
257
- const defaults = normalizeHumanBehaviorOptions(String(profileRaw || ""));
258
- Object.assign(target, {
259
- enabled: defaults.enabled,
260
- profile: defaults.profile,
261
- clickMovement: defaults.clickMovement,
262
- textEntry: defaults.textEntry,
263
- listScrollJitter: defaults.listScrollJitter,
264
- shortRest: defaults.shortRest,
265
- batchRest: defaults.batchRest,
266
- actionCooldown: defaults.actionCooldown
267
- });
268
- }
269
-
270
- export function resolveHumanBehaviorForRun(args = {}, config = {}) {
271
- const base = normalizeHumanBehaviorOptions(config.humanBehavior || config.human_behavior || null, {
272
- legacyEnabled: config.humanRestEnabled === true
273
- });
274
- const override = {};
275
- const rawBehavior = readFirstOwn(args, ["human_behavior", "humanBehavior"]);
276
- if (typeof rawBehavior === "boolean") {
277
- override.enabled = rawBehavior;
278
- } else if (typeof rawBehavior === "string") {
279
- applyHumanBehaviorProfileDefaults(override, rawBehavior);
280
- } else if (rawBehavior && typeof rawBehavior === "object" && !Array.isArray(rawBehavior)) {
281
- const rawProfile = readFirstOwn(rawBehavior, ["profile", "mode", "behaviorProfile", "behavior_profile"]);
282
- if (rawProfile !== undefined) applyHumanBehaviorProfileDefaults(override, rawProfile);
283
- Object.assign(override, rawBehavior);
284
- }
285
-
286
- const profile = readFirstOwn(args, ["human_behavior_profile", "humanBehaviorProfile"]);
287
- if (profile !== undefined) applyHumanBehaviorProfileDefaults(override, profile);
288
- const enabled = parseOptionalConfigBoolean(readFirstOwn(args, [
289
- "human_behavior_enabled",
290
- "humanBehaviorEnabled"
291
- ]));
292
- if (enabled !== null) {
293
- override.enabled = enabled;
294
- if (enabled === true && !override.profile) applyHumanBehaviorProfileDefaults(override, "paced");
295
- }
296
-
297
- const safePacing = parseOptionalConfigBoolean(readFirstOwn(args, ["safe_pacing", "safePacing"]));
298
- if (safePacing === true) {
299
- applyHumanBehaviorProfileDefaults(override, "paced");
300
- } else if (safePacing === false) {
301
- override.enabled = false;
302
- }
303
-
304
- const batchRest = parseOptionalConfigBoolean(readFirstOwn(args, [
305
- "batch_rest_enabled",
306
- "batchRestEnabled",
307
- "batch_rest"
308
- ]));
309
- if (batchRest === true) {
310
- applyHumanBehaviorProfileDefaults(override, "paced_with_rests");
311
- } else if (batchRest === false) {
312
- override.batchRest = false;
313
- }
314
-
315
- return normalizeHumanBehaviorOptions({
316
- ...base,
317
- ...override
318
- });
319
- }
320
-
321
- function normalizeLlmThinkingLevel(raw, fallback = "low") {
322
- const normalized = normalizeText(raw).toLowerCase();
323
- return LLM_THINKING_LEVELS.has(normalized) ? normalized : fallback;
324
- }
325
-
326
- function normalizeLlmScreeningStrategy(raw, fallback = "single_pass") {
327
- const normalized = normalizeText(raw).toLowerCase();
328
- return LLM_SCREENING_STRATEGIES.has(normalized) ? normalized : fallback;
329
- }
330
-
331
- function firstConfiguredValue(...values) {
332
- for (const value of values) {
333
- if (value === undefined || value === null) continue;
334
- if (typeof value === "string" && !value.trim()) continue;
335
- return value;
336
- }
337
- return "";
338
- }
339
-
340
- function resolveRawLlmModelEntries(config = {}) {
341
- if (Array.isArray(config.llmModels) && config.llmModels.length > 0) return config.llmModels;
342
- if (Array.isArray(config.models) && config.models.length > 0) return config.models;
343
- return [config];
344
- }
345
-
346
- function normalizeScreeningLlmModel(config = {}, rawEntry = {}, index = 0) {
347
- const entry = typeof rawEntry === "string"
348
- ? { model: rawEntry }
349
- : (rawEntry && typeof rawEntry === "object" && !Array.isArray(rawEntry) ? rawEntry : {});
350
- return {
351
- name: normalizeText(firstConfiguredValue(entry.name, entry.label, entry.id, entry.providerName, entry.provider)),
352
- baseUrl: normalizeText(firstConfiguredValue(entry.baseUrl, entry.base_url, config.baseUrl, config.base_url)).replace(/\/+$/, ""),
353
- apiKey: normalizeText(firstConfiguredValue(entry.apiKey, entry.api_key, config.apiKey, config.api_key)),
354
- model: normalizeText(firstConfiguredValue(entry.model, entry.modelName, entry.model_name, typeof rawEntry === "string" ? rawEntry : "", config.model)),
355
- openaiOrganization: normalizeText(firstConfiguredValue(entry.openaiOrganization, entry.organization, config.openaiOrganization, config.organization)),
356
- openaiProject: normalizeText(firstConfiguredValue(entry.openaiProject, entry.project, config.openaiProject, config.project)),
357
- llmThinkingLevel: normalizeLlmThinkingLevel(
358
- firstConfiguredValue(entry.llmThinkingLevel, entry.thinkingLevel, entry.reasoningEffort, config.llmThinkingLevel, config.thinkingLevel, config.reasoningEffort),
359
- "low"
360
- ),
361
- llmScreeningStrategy: normalizeLlmScreeningStrategy(
362
- firstConfiguredValue(entry.llmScreeningStrategy, entry.screeningStrategy, entry.screening_strategy, config.llmScreeningStrategy, config.screeningStrategy, config.screening_strategy),
363
- "single_pass"
364
- ),
365
- llmFastThinkingLevel: normalizeLlmThinkingLevel(
366
- firstConfiguredValue(entry.llmFastThinkingLevel, entry.fastThinkingLevel, entry.fast_thinking_level, config.llmFastThinkingLevel, config.fastThinkingLevel, config.fast_thinking_level),
367
- "current"
368
- ),
369
- llmVerifyThinkingLevel: normalizeLlmThinkingLevel(
370
- firstConfiguredValue(entry.llmVerifyThinkingLevel, entry.verifyThinkingLevel, entry.verify_thinking_level, config.llmVerifyThinkingLevel, config.verifyThinkingLevel, config.verify_thinking_level),
371
- "low"
372
- ),
373
- llmFastMaxTokens: parsePositiveInteger(
374
- firstConfiguredValue(entry.llmFastMaxTokens, entry.fastMaxTokens, entry.fast_max_tokens, config.llmFastMaxTokens, config.fastMaxTokens, config.fast_max_tokens),
375
- null
376
- ),
377
- llmVerifyMaxTokens: parsePositiveInteger(
378
- firstConfiguredValue(entry.llmVerifyMaxTokens, entry.verifyMaxTokens, entry.verify_max_tokens, config.llmVerifyMaxTokens, config.verifyMaxTokens, config.verify_max_tokens),
379
- null
380
- ),
381
- llmTimeoutMs: parsePositiveInteger(firstConfiguredValue(entry.llmTimeoutMs, entry.timeoutMs, config.llmTimeoutMs, config.timeoutMs), null),
382
- llmMaxRetries: parsePositiveInteger(firstConfiguredValue(entry.llmMaxRetries, entry.maxRetries, config.llmMaxRetries, config.maxRetries), null),
383
- llmMaxTokens: parsePositiveInteger(firstConfiguredValue(entry.llmMaxTokens, entry.maxTokens, config.llmMaxTokens, config.maxTokens), null),
384
- llmMaxCompletionTokens: parsePositiveInteger(
385
- firstConfiguredValue(entry.llmMaxCompletionTokens, entry.maxCompletionTokens, config.llmMaxCompletionTokens, config.maxCompletionTokens),
386
- null
387
- ),
388
- llmImageLimit: parsePositiveInteger(firstConfiguredValue(entry.llmImageLimit, entry.imageLimit, config.llmImageLimit, config.imageLimit), null),
389
- llmImageDetail: normalizeText(firstConfiguredValue(entry.llmImageDetail, entry.imageDetail, config.llmImageDetail, config.imageDetail)),
390
- temperature: parseConfigNumber(firstConfiguredValue(entry.temperature, config.temperature), null),
391
- topP: parseConfigNumber(firstConfiguredValue(entry.topP, entry.top_p, config.topP, config.top_p), null),
392
- llmProviderIndex: index
393
- };
394
- }
395
-
396
- function normalizeScreeningLlmModels(config = {}) {
397
- return resolveRawLlmModelEntries(config).map((entry, index) => normalizeScreeningLlmModel(config, entry, index));
398
- }
399
-
400
- function resolveConfigPathValue(raw, configDir) {
401
- const normalized = normalizeText(raw);
402
- if (!normalized) return "";
403
- return path.isAbsolute(normalized)
404
- ? path.resolve(normalized)
405
- : path.resolve(configDir || process.cwd(), normalized);
406
- }
407
-
408
- function validateScreeningConfig(config) {
409
- if (!config || typeof config !== "object" || Array.isArray(config)) {
410
- return {
411
- ok: false,
412
- reason: "INVALID_OR_MISSING_CONFIG",
413
- message: "screening-config.json 缺失或格式无效。请填写 baseUrl、apiKey、model。"
414
- };
415
- }
416
- const llmModels = normalizeScreeningLlmModels(config);
417
- const missing = [];
418
- for (const [index, llmModel] of llmModels.entries()) {
419
- const prefix = llmModels.length > 1 ? `llmModels[${index}]` : "";
420
- if (!llmModel.baseUrl) missing.push(prefix ? `${prefix}.baseUrl` : "baseUrl");
421
- if (!llmModel.apiKey) missing.push(prefix ? `${prefix}.apiKey` : "apiKey");
422
- if (!llmModel.model) missing.push(prefix ? `${prefix}.model` : "model");
423
- }
424
- if (missing.length > 0) {
425
- return {
426
- ok: false,
427
- reason: "MISSING_REQUIRED_FIELDS",
428
- message: `screening-config.json 缺少必填字段:${missing.join(", ")}。`
429
- };
430
- }
431
- const placeholderModel = llmModels.find((item) => /^replace-with/i.test(item.apiKey) || item.apiKey === SCREEN_CONFIG_TEMPLATE_DEFAULTS.apiKey);
432
- if (placeholderModel) {
433
- return {
434
- ok: false,
435
- reason: "PLACEHOLDER_API_KEY",
436
- message: "screening-config.json 的 apiKey 仍是模板占位符,请填写真实 API Key。"
437
- };
438
- }
439
- const firstModel = llmModels[0] || {};
440
- if (
441
- llmModels.length === 1
442
- && firstModel.baseUrl === SCREEN_CONFIG_TEMPLATE_DEFAULTS.baseUrl
443
- && firstModel.apiKey === SCREEN_CONFIG_TEMPLATE_DEFAULTS.apiKey
444
- && firstModel.model === SCREEN_CONFIG_TEMPLATE_DEFAULTS.model
445
- ) {
446
- return {
447
- ok: false,
448
- reason: "PLACEHOLDER_TEMPLATE_VALUES",
449
- message: "screening-config.json 仍是默认模板值,请填写 baseUrl、apiKey、model。"
450
- };
451
- }
452
- return { ok: true, reason: "OK", message: "screening-config.json 校验通过。" };
453
- }
454
-
455
- export function resolveBossChatDataDir() {
456
- if (process.env.BOSS_CHAT_HOME) {
457
- return {
458
- data_dir: path.resolve(process.env.BOSS_CHAT_HOME),
459
- data_dir_source: "env:BOSS_CHAT_HOME"
460
- };
461
- }
462
- const stateHome = getStateHome();
463
- const source = process.env.BOSS_RECOMMEND_HOME
464
- ? "default:env:BOSS_RECOMMEND_HOME"
465
- : "default:user_home";
466
- return {
467
- data_dir: path.join(stateHome, BOSS_CHAT_RUNTIME_SUBDIR),
468
- data_dir_source: source
469
- };
470
- }
471
-
472
- export function getBossChatDataDir() {
473
- return resolveBossChatDataDir().data_dir;
474
- }
475
-
476
- export function getLegacyBossChatWorkspaceDataDir(workspaceRoot) {
477
- const root = path.resolve(String(workspaceRoot || ""));
478
- if (!root || isRootDirectory(root) || isSystemDirectoryWorkspaceRoot(root)) return null;
479
- return path.join(root, ".boss-chat");
480
- }
481
-
482
- export function resolveBossChatRuntimeLayout(workspaceRoot) {
483
- const resolvedDataDir = resolveBossChatDataDir();
484
- const legacyWorkspaceDir = getLegacyBossChatWorkspaceDataDir(workspaceRoot);
485
- const migrationSourceDir = legacyWorkspaceDir && pathExists(legacyWorkspaceDir) && !pathExists(resolvedDataDir.data_dir)
486
- ? legacyWorkspaceDir
487
- : null;
488
- return {
489
- workspace_root: workspaceRoot ? path.resolve(String(workspaceRoot)) : null,
490
- data_dir: resolvedDataDir.data_dir,
491
- data_dir_source: resolvedDataDir.data_dir_source,
492
- legacy_workspace_dir: legacyWorkspaceDir,
493
- migration_source_dir: migrationSourceDir,
494
- migration_pending: Boolean(migrationSourceDir)
495
- };
496
- }
497
-
498
- export function getBossScreenConfigResolution(workspaceRoot) {
499
- const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
500
- const workspaceRootResolved = path.resolve(String(workspaceRoot || process.cwd()));
501
- return {
502
- resolved_path: resolveScreenConfigPath(workspaceRoot) || null,
503
- candidate_paths: resolveScreenConfigCandidates(workspaceRoot),
504
- workspace_root: workspaceRootResolved,
505
- workspace_ephemeral: isEphemeralNpxWorkspaceRoot(workspaceRootResolved),
506
- workspace_ignored_for_config: shouldIgnoreWorkspaceConfigRoot(workspaceRootResolved),
507
- writable_path: resolveWritableScreenConfigPath(workspaceRoot),
508
- legacy_path: candidateMap.legacy_path
509
- };
510
- }
511
-
512
- export function getFeaturedCalibrationResolution(workspaceRoot) {
513
- const calibrationPath = resolveFeaturedCalibrationPath(workspaceRoot);
514
- return {
515
- calibration_path: calibrationPath,
516
- calibration_exists: pathExists(calibrationPath),
517
- calibration_usable: isUsableFeaturedCalibrationFile(calibrationPath),
518
- calibration_script_path: resolveRecruitCalibrationScriptPath(workspaceRoot)
519
- };
520
- }
521
-
522
- export function resolveBossScreeningConfig(workspaceRoot) {
523
- const candidatePaths = resolveScreenConfigCandidates(workspaceRoot);
524
- const configPath = resolveScreenConfigPath(workspaceRoot) || null;
525
- const configDir = configPath ? path.dirname(configPath) : null;
526
- if (!configPath || !pathExists(configPath)) {
527
- return {
528
- ok: false,
529
- error: {
530
- code: "SCREEN_CONFIG_ERROR",
531
- message: `screening-config.json 不存在。请先完成 recommend 配置。${configPath ? ` (path: ${configPath})` : ""}`,
532
- retryable: true
533
- },
534
- config_path: configPath,
535
- config_dir: configDir,
536
- candidate_paths: candidatePaths
537
- };
538
- }
539
- const parsed = readJsonFile(configPath);
540
- const validation = validateScreeningConfig(parsed);
541
- if (!validation.ok) {
542
- return {
543
- ok: false,
544
- error: {
545
- code: "SCREEN_CONFIG_ERROR",
546
- message: `${validation.message} (path: ${configPath})`,
547
- retryable: true
548
- },
549
- config_path: configPath,
550
- config_dir: configDir,
551
- candidate_paths: candidatePaths
552
- };
553
- }
554
- const llmModels = normalizeScreeningLlmModels(parsed);
555
- const primaryLlmModel = llmModels[0] || {};
556
- const greetingText = normalizeText(
557
- parsed.greetingMessage
558
- || parsed.greeting_message
559
- || parsed.greetingText
560
- || parsed.greeting_text
561
- || parsed.greeting
562
- );
563
- const humanRestEnabled = parseConfigBoolean(parsed.humanRestEnabled, false);
564
- const humanBehavior = normalizeHumanBehaviorOptions(parsed.humanBehavior || parsed.human_behavior || null, {
565
- legacyEnabled: humanRestEnabled
566
- });
567
- return {
568
- ok: true,
569
- config: {
570
- ...primaryLlmModel,
571
- baseUrl: primaryLlmModel.baseUrl,
572
- apiKey: primaryLlmModel.apiKey,
573
- model: primaryLlmModel.model,
574
- openaiOrganization: primaryLlmModel.openaiOrganization,
575
- openaiProject: primaryLlmModel.openaiProject,
576
- llmModels,
577
- greetingMessage: greetingText,
578
- greetingText,
579
- debugPort: parsePositiveInteger(parsed.debugPort, 9222),
580
- outputDir: resolveConfigPathValue(parsed.outputDir, configDir),
581
- humanRestEnabled,
582
- humanBehavior
583
- },
584
- config_path: configPath,
585
- config_dir: configDir,
586
- candidate_paths: candidatePaths
587
- };
588
- }
589
-
590
- export function resolveBossConfiguredOutputDir(workspaceRoot, fallbackDir = "") {
591
- const configResolution = resolveBossScreeningConfig(workspaceRoot);
592
- const configuredDir = configResolution.ok ? normalizeText(configResolution.config.outputDir) : "";
593
- if (configuredDir) return configuredDir;
594
- return fallbackDir ? path.resolve(fallbackDir) : "";
595
- }
596
-
597
- function isUnlimitedTargetCountToken(value) {
598
- const token = normalizeText(value).toLowerCase();
599
- if (!token) return false;
600
- const compact = token.replace(/\s+/g, "");
601
- const withoutAnnotation = compact.replace(/[((【[].*?[))】\]]/gu, "");
602
- const knownTokens = new Set([
603
- "all",
604
- "unlimited",
605
- "infinity",
606
- "inf",
607
- "max",
608
- "full",
609
- "allcandidates",
610
- "全部",
611
- "全量",
612
- "不限",
613
- "扫到底",
614
- "全部候选人",
615
- "所有候选人",
616
- "全部人选",
617
- "所有人选",
618
- "直到完成所有人选"
619
- ]);
620
- if (knownTokens.has(token) || knownTokens.has(compact) || knownTokens.has(withoutAnnotation)) return true;
621
- if (/^(?:all|unlimited|infinity|inf|max|full)(?:candidate|candidates)?$/i.test(compact)) return true;
622
- if (/^(?:all|unlimited|infinity|inf|max|full)(?:候选人|人选|牛人|人才|人员)?$/iu.test(withoutAnnotation)) return true;
623
- if (/^(?:全部|所有|全量|不限)(?:候选人|人选|牛人|人才|人员)?$/u.test(compact)) return true;
624
- if (!/\d/.test(compact) && /(?:扫到底|全部候选人|所有候选人|全部人选|所有人选)/u.test(compact)) return true;
625
- return false;
626
- }
627
-
628
- function getWrappedTargetCountValue(value) {
629
- if (!value || typeof value !== "object" || Array.isArray(value)) return value;
630
- for (const key of TARGET_COUNT_WRAPPER_KEYS) {
631
- if (Object.prototype.hasOwnProperty.call(value, key)) {
632
- return value[key];
633
- }
634
- }
635
- return value;
636
- }
637
-
638
- export function getBossChatTargetCountValue(input = {}) {
639
- if (!input || typeof input !== "object" || Array.isArray(input)) return undefined;
640
- if (Object.prototype.hasOwnProperty.call(input, "target_count") && input.target_count !== undefined && input.target_count !== null) {
641
- return input.target_count;
642
- }
643
- if (Object.prototype.hasOwnProperty.call(input, "targetCount") && input.targetCount !== undefined && input.targetCount !== null) {
644
- return input.targetCount;
645
- }
646
- if (Object.prototype.hasOwnProperty.call(input, "target_count")) return input.target_count;
647
- if (Object.prototype.hasOwnProperty.call(input, "targetCount")) return input.targetCount;
648
- return undefined;
649
- }
650
-
651
- function cloneForDiagnostics(value) {
652
- if (value === undefined) return undefined;
653
- if (value === null || ["string", "number", "boolean"].includes(typeof value)) return value;
654
- try {
655
- return JSON.parse(JSON.stringify(value));
656
- } catch {
657
- return String(value);
658
- }
659
- }
660
-
661
- export function buildTargetCountCompatibilityHints({
662
- argumentName = "target_count",
663
- recommendedArgumentPatch = { target_count: TARGET_COUNT_CANONICAL_ALL },
664
- includeOptions = true
665
- } = {}) {
666
- const normalizedArgumentName = normalizeText(argumentName) || "target_count";
667
- const clonedRecommendedPatch = cloneForDiagnostics(recommendedArgumentPatch)
668
- || { target_count: TARGET_COUNT_CANONICAL_ALL };
669
- const literal = `${normalizedArgumentName}="${TARGET_COUNT_CANONICAL_ALL}"`;
670
- const base = {
671
- argument_name: normalizedArgumentName,
672
- answer_format: `${normalizedArgumentName} = 正整数 | "${TARGET_COUNT_CANONICAL_ALL}"`,
673
- canonical_unlimited_value: TARGET_COUNT_CANONICAL_ALL,
674
- recommended_value: TARGET_COUNT_CANONICAL_ALL,
675
- recommended_argument_patch: clonedRecommendedPatch,
676
- accepted_examples: TARGET_COUNT_ACCEPTED_EXAMPLES.slice()
677
- };
678
- if (!includeOptions) return base;
679
- return {
680
- ...base,
681
- options: [
682
- {
683
- label: `扫到底(必须传 ${literal},推荐)`,
684
- value: TARGET_COUNT_CANONICAL_ALL,
685
- canonical_value: TARGET_COUNT_CANONICAL_ALL,
686
- argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
687
- },
688
- {
689
- label: `不限(等价于 ${literal})`,
690
- value: "unlimited",
691
- canonical_value: TARGET_COUNT_CANONICAL_ALL,
692
- argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
693
- },
694
- {
695
- label: `全部候选人(等价于 ${literal})`,
696
- value: "全部候选人",
697
- canonical_value: TARGET_COUNT_CANONICAL_ALL,
698
- argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
699
- },
700
- {
701
- label: `所有候选人(等价于 ${literal})`,
702
- value: "所有候选人",
703
- canonical_value: TARGET_COUNT_CANONICAL_ALL,
704
- argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
705
- }
706
- ]
707
- };
708
- }
709
-
710
- export function normalizeTargetCountInput(value) {
711
- if (value === undefined || value === null) {
712
- return {
713
- provided: false,
714
- targetCount: null,
715
- cliArg: null,
716
- publicValue: null,
717
- rawValue: value,
718
- parseError: null
719
- };
720
- }
721
- const unwrapped = getWrappedTargetCountValue(value);
722
- if (unwrapped !== value) {
723
- return normalizeTargetCountInput(unwrapped);
724
- }
725
- const raw = normalizeText(unwrapped);
726
- if (!raw) {
727
- return {
728
- provided: false,
729
- targetCount: null,
730
- cliArg: null,
731
- publicValue: null,
732
- rawValue: value,
733
- parseError: null
734
- };
735
- }
736
- if (isUnlimitedTargetCountToken(raw)) {
737
- return {
738
- provided: true,
739
- targetCount: null,
740
- cliArg: "-1",
741
- publicValue: TARGET_COUNT_CANONICAL_ALL,
742
- rawValue: cloneForDiagnostics(value),
743
- parseError: null
744
- };
745
- }
746
- const parsed = Number.parseInt(String(raw), 10);
747
- if (Number.isFinite(parsed) && parsed === -1) {
748
- return {
749
- provided: true,
750
- targetCount: null,
751
- cliArg: "-1",
752
- publicValue: TARGET_COUNT_CANONICAL_ALL,
753
- rawValue: cloneForDiagnostics(value),
754
- parseError: null
755
- };
756
- }
757
- if (Number.isFinite(parsed) && parsed > 0) {
758
- return {
759
- provided: true,
760
- targetCount: parsed,
761
- cliArg: String(parsed),
762
- publicValue: parsed,
763
- rawValue: cloneForDiagnostics(value),
764
- parseError: null
765
- };
766
- }
767
- return {
768
- provided: false,
769
- targetCount: null,
770
- cliArg: null,
771
- publicValue: null,
772
- rawValue: cloneForDiagnostics(value),
773
- parseError: "target_count must be a positive integer, -1, or one of: all, unlimited, 全部, 不限, 扫到底, 全量, 全部候选人, 所有候选人"
774
- };
775
- }
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { normalizeHumanBehaviorOptions } from "./core/browser/index.js";
6
+
7
+ const BOSS_CHAT_RUNTIME_SUBDIR = "boss-chat";
8
+ const TARGET_COUNT_WRAPPER_KEYS = ["target_count", "targetCount", "value", "count", "limit"];
9
+ const SCREEN_CONFIG_TEMPLATE_DEFAULTS = Object.freeze({
10
+ baseUrl: "https://api.openai.com/v1",
11
+ apiKey: "replace-with-your-api-key",
12
+ model: "gpt-4.1-mini"
13
+ });
14
+ const LLM_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "auto", "current"]);
15
+ const LLM_SCREENING_STRATEGIES = new Set(["single_pass", "fast_first_verified"]);
16
+
17
+ export const TARGET_COUNT_CANONICAL_ALL = "all";
18
+ export const TARGET_COUNT_ACCEPTED_EXAMPLES = [TARGET_COUNT_CANONICAL_ALL, -1, 20, "全部候选人"];
19
+
20
+ function normalizeText(value) {
21
+ return String(value || "").replace(/\s+/g, " ").trim();
22
+ }
23
+
24
+ function getStateHome() {
25
+ return process.env.BOSS_RECOMMEND_HOME
26
+ ? path.resolve(process.env.BOSS_RECOMMEND_HOME)
27
+ : path.join(os.homedir(), ".boss-recommend-mcp");
28
+ }
29
+
30
+ function getCodexHome() {
31
+ return process.env.CODEX_HOME
32
+ ? path.resolve(process.env.CODEX_HOME)
33
+ : path.join(os.homedir(), ".codex");
34
+ }
35
+
36
+ function pathExists(targetPath) {
37
+ try {
38
+ return Boolean(targetPath) && fs.existsSync(targetPath);
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ function isRootDirectory(workspaceRoot) {
45
+ const root = path.resolve(String(workspaceRoot || ""));
46
+ return path.parse(root).root.toLowerCase() === root.toLowerCase();
47
+ }
48
+
49
+ function isEphemeralNpxWorkspaceRoot(workspaceRoot) {
50
+ const root = path.resolve(String(workspaceRoot || ""));
51
+ const normalized = root.replace(/\\/g, "/").toLowerCase();
52
+ return (
53
+ normalized.includes("/appdata/local/npm-cache/_npx/")
54
+ || normalized.includes("/node_modules/@reconcrap/boss-recommend-mcp")
55
+ );
56
+ }
57
+
58
+ function isSystemDirectoryWorkspaceRoot(workspaceRoot) {
59
+ const root = path.resolve(String(workspaceRoot || ""));
60
+ const normalized = root.replace(/\\/g, "/").toLowerCase();
61
+ if (process.platform === "win32") {
62
+ return (
63
+ normalized.endsWith("/windows")
64
+ || normalized.endsWith("/windows/system32")
65
+ || normalized.endsWith("/windows/syswow64")
66
+ || normalized.endsWith("/program files")
67
+ || normalized.endsWith("/program files (x86)")
68
+ );
69
+ }
70
+ return (
71
+ normalized === "/system"
72
+ || normalized.startsWith("/system/")
73
+ || normalized === "/usr"
74
+ || normalized.startsWith("/usr/")
75
+ || normalized === "/bin"
76
+ || normalized.startsWith("/bin/")
77
+ || normalized === "/sbin"
78
+ || normalized.startsWith("/sbin/")
79
+ );
80
+ }
81
+
82
+ function shouldIgnoreWorkspaceConfigRoot(workspaceRoot) {
83
+ const root = path.resolve(String(workspaceRoot || process.cwd()));
84
+ const home = path.resolve(os.homedir());
85
+ return (
86
+ isEphemeralNpxWorkspaceRoot(root)
87
+ || isRootDirectory(root)
88
+ || root.toLowerCase() === home.toLowerCase()
89
+ || isSystemDirectoryWorkspaceRoot(root)
90
+ );
91
+ }
92
+
93
+ function resolveWorkspaceConfigCandidates(workspaceRoot) {
94
+ const root = path.resolve(String(workspaceRoot || process.cwd()));
95
+ if (shouldIgnoreWorkspaceConfigRoot(root)) return [];
96
+ const directPath = path.join(root, "config", "screening-config.json");
97
+ const nestedPath = path.join(root, "boss-recommend-mcp", "config", "screening-config.json");
98
+ const candidates = [directPath];
99
+ if (path.basename(root).toLowerCase() !== "boss-recommend-mcp") {
100
+ candidates.push(nestedPath);
101
+ }
102
+ return Array.from(new Set(candidates));
103
+ }
104
+
105
+ function getUserConfigPath() {
106
+ return path.join(getStateHome(), "screening-config.json");
107
+ }
108
+
109
+ function getLegacyUserConfigPath() {
110
+ return path.join(getCodexHome(), "boss-recommend-mcp", "screening-config.json");
111
+ }
112
+
113
+ function getUserCalibrationPath() {
114
+ return path.join(getCodexHome(), "boss-recommend-mcp", "favorite-calibration.json");
115
+ }
116
+
117
+ function buildScreenConfigCandidateMap(workspaceRoot) {
118
+ return {
119
+ env_path: process.env.BOSS_RECOMMEND_SCREEN_CONFIG
120
+ ? path.resolve(process.env.BOSS_RECOMMEND_SCREEN_CONFIG)
121
+ : null,
122
+ workspace_paths: resolveWorkspaceConfigCandidates(workspaceRoot),
123
+ user_path: getUserConfigPath(),
124
+ legacy_path: getLegacyUserConfigPath()
125
+ };
126
+ }
127
+
128
+ function resolveScreenConfigCandidates(workspaceRoot) {
129
+ const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
130
+ return [
131
+ candidateMap.env_path,
132
+ candidateMap.user_path,
133
+ ...candidateMap.workspace_paths,
134
+ candidateMap.legacy_path
135
+ ].filter(Boolean);
136
+ }
137
+
138
+ function canWriteDirectory(targetDir) {
139
+ try {
140
+ fs.mkdirSync(targetDir, { recursive: true });
141
+ fs.accessSync(targetDir, fs.constants.W_OK);
142
+ return true;
143
+ } catch {
144
+ return false;
145
+ }
146
+ }
147
+
148
+ function resolveWritableScreenConfigPath(workspaceRoot) {
149
+ const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
150
+ const workspacePreferred = candidateMap.workspace_paths?.[0] || null;
151
+ if (candidateMap.env_path) return candidateMap.env_path;
152
+ if (candidateMap.user_path && canWriteDirectory(path.dirname(candidateMap.user_path))) {
153
+ return candidateMap.user_path;
154
+ }
155
+ if (workspacePreferred && canWriteDirectory(path.dirname(workspacePreferred))) {
156
+ return workspacePreferred;
157
+ }
158
+ if (workspacePreferred) return workspacePreferred;
159
+ return candidateMap.user_path || candidateMap.legacy_path;
160
+ }
161
+
162
+ function resolveScreenConfigPath(workspaceRoot) {
163
+ const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
164
+ if (candidateMap.env_path) return candidateMap.env_path;
165
+ if (candidateMap.user_path && pathExists(candidateMap.user_path)) return candidateMap.user_path;
166
+ const existingWorkspacePath = candidateMap.workspace_paths.find((item) => pathExists(item));
167
+ if (existingWorkspacePath) return existingWorkspacePath;
168
+ return resolveWritableScreenConfigPath(workspaceRoot) || candidateMap.legacy_path;
169
+ }
170
+
171
+ function readJsonFile(filePath) {
172
+ if (!filePath || !pathExists(filePath)) return null;
173
+ try {
174
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+
180
+ function isUsableFeaturedCalibrationFile(filePath) {
181
+ const parsed = readJsonFile(filePath);
182
+ return Boolean(
183
+ parsed
184
+ && typeof parsed === "object"
185
+ && !Array.isArray(parsed)
186
+ && parsed.favoritePosition
187
+ && Number.isFinite(parsed.favoritePosition.pageX)
188
+ && Number.isFinite(parsed.favoritePosition.pageY)
189
+ );
190
+ }
191
+
192
+ function resolveFeaturedCalibrationPath(workspaceRoot) {
193
+ const fromEnv = normalizeText(process.env.BOSS_RECOMMEND_CALIBRATION_FILE || "");
194
+ if (fromEnv) return path.resolve(fromEnv);
195
+
196
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
197
+ const configPath = configResolution.config_path || resolveScreenConfigPath(workspaceRoot) || getUserConfigPath();
198
+ const config = readJsonFile(configPath);
199
+ const calibrationFile = normalizeText(config?.calibrationFile || "");
200
+ if (calibrationFile && configPath) {
201
+ return path.resolve(path.dirname(configPath), calibrationFile);
202
+ }
203
+
204
+ return getUserCalibrationPath();
205
+ }
206
+
207
+ function resolveRecruitCalibrationScriptPath(workspaceRoot) {
208
+ const fromEnv = normalizeText(process.env.BOSS_RECOMMEND_RECRUIT_CALIBRATION_SCRIPT || "");
209
+ const workspaceResolved = path.resolve(String(workspaceRoot || process.cwd()));
210
+ const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
211
+ const candidates = [
212
+ fromEnv,
213
+ path.join(workspaceResolved, "..", "..", "boss recruit pipeline", "boss-recruit-mcp", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs"),
214
+ path.join(workspaceResolved, "..", "boss recruit pipeline", "boss-recruit-mcp", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs"),
215
+ path.join(appData, "npm", "node_modules", "@reconcrap", "boss-recruit-mcp", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs")
216
+ ].filter(Boolean).map((item) => path.resolve(item));
217
+
218
+ for (const candidate of new Set(candidates)) {
219
+ if (pathExists(candidate)) return candidate;
220
+ }
221
+ return null;
222
+ }
223
+
224
+ function parsePositiveInteger(raw, fallback = null) {
225
+ const parsed = Number.parseInt(String(raw || ""), 10);
226
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
227
+ }
228
+
229
+ function parseConfigNumber(raw, fallback = null) {
230
+ if (raw === undefined || raw === null || raw === "") return fallback;
231
+ const parsed = Number(raw);
232
+ return Number.isFinite(parsed) ? parsed : fallback;
233
+ }
234
+
235
+ function parseConfigBoolean(raw, fallback = false) {
236
+ if (typeof raw === "boolean") return raw;
237
+ const normalized = normalizeText(raw).toLowerCase();
238
+ if (["true", "1", "yes", "y", "on", "enabled"].includes(normalized)) return true;
239
+ if (["false", "0", "no", "n", "off", "disabled"].includes(normalized)) return false;
240
+ return fallback;
241
+ }
242
+
243
+ function readFirstOwn(source, keys = []) {
244
+ if (!source || typeof source !== "object") return undefined;
245
+ for (const key of keys) {
246
+ if (Object.prototype.hasOwnProperty.call(source, key)) return source[key];
247
+ }
248
+ return undefined;
249
+ }
250
+
251
+ function parseOptionalConfigBoolean(raw) {
252
+ if (raw === undefined || raw === null || raw === "") return null;
253
+ return parseConfigBoolean(raw, null);
254
+ }
255
+
256
+ function applyHumanBehaviorProfileDefaults(target, profileRaw) {
257
+ const defaults = normalizeHumanBehaviorOptions(String(profileRaw || ""));
258
+ Object.assign(target, {
259
+ enabled: defaults.enabled,
260
+ profile: defaults.profile,
261
+ clickMovement: defaults.clickMovement,
262
+ textEntry: defaults.textEntry,
263
+ listScrollJitter: defaults.listScrollJitter,
264
+ shortRest: defaults.shortRest,
265
+ batchRest: defaults.batchRest,
266
+ actionCooldown: defaults.actionCooldown
267
+ });
268
+ }
269
+
270
+ export function resolveHumanBehaviorForRun(args = {}, config = {}) {
271
+ const base = normalizeHumanBehaviorOptions(config.humanBehavior || config.human_behavior || null, {
272
+ legacyEnabled: config.humanRestEnabled === true
273
+ });
274
+ const override = {};
275
+ const rawBehavior = readFirstOwn(args, ["human_behavior", "humanBehavior"]);
276
+ if (typeof rawBehavior === "boolean") {
277
+ override.enabled = rawBehavior;
278
+ } else if (typeof rawBehavior === "string") {
279
+ applyHumanBehaviorProfileDefaults(override, rawBehavior);
280
+ } else if (rawBehavior && typeof rawBehavior === "object" && !Array.isArray(rawBehavior)) {
281
+ const rawProfile = readFirstOwn(rawBehavior, ["profile", "mode", "behaviorProfile", "behavior_profile"]);
282
+ if (rawProfile !== undefined) applyHumanBehaviorProfileDefaults(override, rawProfile);
283
+ Object.assign(override, rawBehavior);
284
+ }
285
+
286
+ const profile = readFirstOwn(args, ["human_behavior_profile", "humanBehaviorProfile"]);
287
+ if (profile !== undefined) applyHumanBehaviorProfileDefaults(override, profile);
288
+ const enabled = parseOptionalConfigBoolean(readFirstOwn(args, [
289
+ "human_behavior_enabled",
290
+ "humanBehaviorEnabled"
291
+ ]));
292
+ if (enabled !== null) {
293
+ override.enabled = enabled;
294
+ if (enabled === true && !override.profile) applyHumanBehaviorProfileDefaults(override, "paced");
295
+ }
296
+
297
+ const safePacing = parseOptionalConfigBoolean(readFirstOwn(args, ["safe_pacing", "safePacing"]));
298
+ if (safePacing === true) {
299
+ applyHumanBehaviorProfileDefaults(override, "paced");
300
+ } else if (safePacing === false) {
301
+ override.enabled = false;
302
+ }
303
+
304
+ const batchRest = parseOptionalConfigBoolean(readFirstOwn(args, [
305
+ "batch_rest_enabled",
306
+ "batchRestEnabled",
307
+ "batch_rest"
308
+ ]));
309
+ if (batchRest === true) {
310
+ applyHumanBehaviorProfileDefaults(override, "paced_with_rests");
311
+ } else if (batchRest === false) {
312
+ override.batchRest = false;
313
+ }
314
+
315
+ return normalizeHumanBehaviorOptions({
316
+ ...base,
317
+ ...override
318
+ });
319
+ }
320
+
321
+ function normalizeLlmThinkingLevel(raw, fallback = "low") {
322
+ const normalized = normalizeText(raw).toLowerCase();
323
+ return LLM_THINKING_LEVELS.has(normalized) ? normalized : fallback;
324
+ }
325
+
326
+ function normalizeLlmScreeningStrategy(raw, fallback = "single_pass") {
327
+ const normalized = normalizeText(raw).toLowerCase();
328
+ return LLM_SCREENING_STRATEGIES.has(normalized) ? normalized : fallback;
329
+ }
330
+
331
+ function firstConfiguredValue(...values) {
332
+ for (const value of values) {
333
+ if (value === undefined || value === null) continue;
334
+ if (typeof value === "string" && !value.trim()) continue;
335
+ return value;
336
+ }
337
+ return "";
338
+ }
339
+
340
+ function resolveRawLlmModelEntries(config = {}) {
341
+ if (Array.isArray(config.llmModels) && config.llmModels.length > 0) return config.llmModels;
342
+ if (Array.isArray(config.models) && config.models.length > 0) return config.models;
343
+ return [config];
344
+ }
345
+
346
+ function normalizeScreeningLlmModel(config = {}, rawEntry = {}, index = 0) {
347
+ const entry = typeof rawEntry === "string"
348
+ ? { model: rawEntry }
349
+ : (rawEntry && typeof rawEntry === "object" && !Array.isArray(rawEntry) ? rawEntry : {});
350
+ return {
351
+ name: normalizeText(firstConfiguredValue(entry.name, entry.label, entry.id, entry.providerName, entry.provider)),
352
+ baseUrl: normalizeText(firstConfiguredValue(entry.baseUrl, entry.base_url, config.baseUrl, config.base_url)).replace(/\/+$/, ""),
353
+ apiKey: normalizeText(firstConfiguredValue(entry.apiKey, entry.api_key, config.apiKey, config.api_key)),
354
+ model: normalizeText(firstConfiguredValue(entry.model, entry.modelName, entry.model_name, typeof rawEntry === "string" ? rawEntry : "", config.model)),
355
+ openaiOrganization: normalizeText(firstConfiguredValue(entry.openaiOrganization, entry.organization, config.openaiOrganization, config.organization)),
356
+ openaiProject: normalizeText(firstConfiguredValue(entry.openaiProject, entry.project, config.openaiProject, config.project)),
357
+ llmThinkingLevel: normalizeLlmThinkingLevel(
358
+ firstConfiguredValue(entry.llmThinkingLevel, entry.thinkingLevel, entry.reasoningEffort, config.llmThinkingLevel, config.thinkingLevel, config.reasoningEffort),
359
+ "low"
360
+ ),
361
+ llmScreeningStrategy: normalizeLlmScreeningStrategy(
362
+ firstConfiguredValue(entry.llmScreeningStrategy, entry.screeningStrategy, entry.screening_strategy, config.llmScreeningStrategy, config.screeningStrategy, config.screening_strategy),
363
+ "single_pass"
364
+ ),
365
+ llmFastThinkingLevel: normalizeLlmThinkingLevel(
366
+ firstConfiguredValue(entry.llmFastThinkingLevel, entry.fastThinkingLevel, entry.fast_thinking_level, config.llmFastThinkingLevel, config.fastThinkingLevel, config.fast_thinking_level),
367
+ "current"
368
+ ),
369
+ llmVerifyThinkingLevel: normalizeLlmThinkingLevel(
370
+ firstConfiguredValue(entry.llmVerifyThinkingLevel, entry.verifyThinkingLevel, entry.verify_thinking_level, config.llmVerifyThinkingLevel, config.verifyThinkingLevel, config.verify_thinking_level),
371
+ "low"
372
+ ),
373
+ llmFastMaxTokens: parsePositiveInteger(
374
+ firstConfiguredValue(entry.llmFastMaxTokens, entry.fastMaxTokens, entry.fast_max_tokens, config.llmFastMaxTokens, config.fastMaxTokens, config.fast_max_tokens),
375
+ null
376
+ ),
377
+ llmVerifyMaxTokens: parsePositiveInteger(
378
+ firstConfiguredValue(entry.llmVerifyMaxTokens, entry.verifyMaxTokens, entry.verify_max_tokens, config.llmVerifyMaxTokens, config.verifyMaxTokens, config.verify_max_tokens),
379
+ null
380
+ ),
381
+ llmTimeoutMs: parsePositiveInteger(firstConfiguredValue(entry.llmTimeoutMs, entry.timeoutMs, config.llmTimeoutMs, config.timeoutMs), null),
382
+ llmMaxRetries: parsePositiveInteger(firstConfiguredValue(entry.llmMaxRetries, entry.maxRetries, config.llmMaxRetries, config.maxRetries), null),
383
+ llmMaxTokens: parsePositiveInteger(firstConfiguredValue(entry.llmMaxTokens, entry.maxTokens, config.llmMaxTokens, config.maxTokens), null),
384
+ llmMaxCompletionTokens: parsePositiveInteger(
385
+ firstConfiguredValue(entry.llmMaxCompletionTokens, entry.maxCompletionTokens, config.llmMaxCompletionTokens, config.maxCompletionTokens),
386
+ null
387
+ ),
388
+ llmImageLimit: parsePositiveInteger(firstConfiguredValue(entry.llmImageLimit, entry.imageLimit, config.llmImageLimit, config.imageLimit), null),
389
+ llmImageDetail: normalizeText(firstConfiguredValue(entry.llmImageDetail, entry.imageDetail, config.llmImageDetail, config.imageDetail)),
390
+ temperature: parseConfigNumber(firstConfiguredValue(entry.temperature, config.temperature), null),
391
+ topP: parseConfigNumber(firstConfiguredValue(entry.topP, entry.top_p, config.topP, config.top_p), null),
392
+ llmProviderIndex: index
393
+ };
394
+ }
395
+
396
+ function normalizeScreeningLlmModels(config = {}) {
397
+ return resolveRawLlmModelEntries(config).map((entry, index) => normalizeScreeningLlmModel(config, entry, index));
398
+ }
399
+
400
+ function resolveConfigPathValue(raw, configDir) {
401
+ const normalized = normalizeText(raw);
402
+ if (!normalized) return "";
403
+ return path.isAbsolute(normalized)
404
+ ? path.resolve(normalized)
405
+ : path.resolve(configDir || process.cwd(), normalized);
406
+ }
407
+
408
+ function validateScreeningConfig(config) {
409
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
410
+ return {
411
+ ok: false,
412
+ reason: "INVALID_OR_MISSING_CONFIG",
413
+ message: "screening-config.json 缺失或格式无效。请填写 baseUrl、apiKey、model。"
414
+ };
415
+ }
416
+ const llmModels = normalizeScreeningLlmModels(config);
417
+ const missing = [];
418
+ for (const [index, llmModel] of llmModels.entries()) {
419
+ const prefix = llmModels.length > 1 ? `llmModels[${index}]` : "";
420
+ if (!llmModel.baseUrl) missing.push(prefix ? `${prefix}.baseUrl` : "baseUrl");
421
+ if (!llmModel.apiKey) missing.push(prefix ? `${prefix}.apiKey` : "apiKey");
422
+ if (!llmModel.model) missing.push(prefix ? `${prefix}.model` : "model");
423
+ }
424
+ if (missing.length > 0) {
425
+ return {
426
+ ok: false,
427
+ reason: "MISSING_REQUIRED_FIELDS",
428
+ message: `screening-config.json 缺少必填字段:${missing.join(", ")}。`
429
+ };
430
+ }
431
+ const placeholderModel = llmModels.find((item) => /^replace-with/i.test(item.apiKey) || item.apiKey === SCREEN_CONFIG_TEMPLATE_DEFAULTS.apiKey);
432
+ if (placeholderModel) {
433
+ return {
434
+ ok: false,
435
+ reason: "PLACEHOLDER_API_KEY",
436
+ message: "screening-config.json 的 apiKey 仍是模板占位符,请填写真实 API Key。"
437
+ };
438
+ }
439
+ const firstModel = llmModels[0] || {};
440
+ if (
441
+ llmModels.length === 1
442
+ && firstModel.baseUrl === SCREEN_CONFIG_TEMPLATE_DEFAULTS.baseUrl
443
+ && firstModel.apiKey === SCREEN_CONFIG_TEMPLATE_DEFAULTS.apiKey
444
+ && firstModel.model === SCREEN_CONFIG_TEMPLATE_DEFAULTS.model
445
+ ) {
446
+ return {
447
+ ok: false,
448
+ reason: "PLACEHOLDER_TEMPLATE_VALUES",
449
+ message: "screening-config.json 仍是默认模板值,请填写 baseUrl、apiKey、model。"
450
+ };
451
+ }
452
+ return { ok: true, reason: "OK", message: "screening-config.json 校验通过。" };
453
+ }
454
+
455
+ export function resolveBossChatDataDir() {
456
+ if (process.env.BOSS_CHAT_HOME) {
457
+ return {
458
+ data_dir: path.resolve(process.env.BOSS_CHAT_HOME),
459
+ data_dir_source: "env:BOSS_CHAT_HOME"
460
+ };
461
+ }
462
+ const stateHome = getStateHome();
463
+ const source = process.env.BOSS_RECOMMEND_HOME
464
+ ? "default:env:BOSS_RECOMMEND_HOME"
465
+ : "default:user_home";
466
+ return {
467
+ data_dir: path.join(stateHome, BOSS_CHAT_RUNTIME_SUBDIR),
468
+ data_dir_source: source
469
+ };
470
+ }
471
+
472
+ export function getBossChatDataDir() {
473
+ return resolveBossChatDataDir().data_dir;
474
+ }
475
+
476
+ export function getLegacyBossChatWorkspaceDataDir(workspaceRoot) {
477
+ const root = path.resolve(String(workspaceRoot || ""));
478
+ if (!root || isRootDirectory(root) || isSystemDirectoryWorkspaceRoot(root)) return null;
479
+ return path.join(root, ".boss-chat");
480
+ }
481
+
482
+ export function resolveBossChatRuntimeLayout(workspaceRoot) {
483
+ const resolvedDataDir = resolveBossChatDataDir();
484
+ const legacyWorkspaceDir = getLegacyBossChatWorkspaceDataDir(workspaceRoot);
485
+ const migrationSourceDir = legacyWorkspaceDir && pathExists(legacyWorkspaceDir) && !pathExists(resolvedDataDir.data_dir)
486
+ ? legacyWorkspaceDir
487
+ : null;
488
+ return {
489
+ workspace_root: workspaceRoot ? path.resolve(String(workspaceRoot)) : null,
490
+ data_dir: resolvedDataDir.data_dir,
491
+ data_dir_source: resolvedDataDir.data_dir_source,
492
+ legacy_workspace_dir: legacyWorkspaceDir,
493
+ migration_source_dir: migrationSourceDir,
494
+ migration_pending: Boolean(migrationSourceDir)
495
+ };
496
+ }
497
+
498
+ export function getBossScreenConfigResolution(workspaceRoot) {
499
+ const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
500
+ const workspaceRootResolved = path.resolve(String(workspaceRoot || process.cwd()));
501
+ return {
502
+ resolved_path: resolveScreenConfigPath(workspaceRoot) || null,
503
+ candidate_paths: resolveScreenConfigCandidates(workspaceRoot),
504
+ workspace_root: workspaceRootResolved,
505
+ workspace_ephemeral: isEphemeralNpxWorkspaceRoot(workspaceRootResolved),
506
+ workspace_ignored_for_config: shouldIgnoreWorkspaceConfigRoot(workspaceRootResolved),
507
+ writable_path: resolveWritableScreenConfigPath(workspaceRoot),
508
+ legacy_path: candidateMap.legacy_path
509
+ };
510
+ }
511
+
512
+ export function getFeaturedCalibrationResolution(workspaceRoot) {
513
+ const calibrationPath = resolveFeaturedCalibrationPath(workspaceRoot);
514
+ return {
515
+ calibration_path: calibrationPath,
516
+ calibration_exists: pathExists(calibrationPath),
517
+ calibration_usable: isUsableFeaturedCalibrationFile(calibrationPath),
518
+ calibration_script_path: resolveRecruitCalibrationScriptPath(workspaceRoot)
519
+ };
520
+ }
521
+
522
+ export function resolveBossScreeningConfig(workspaceRoot) {
523
+ const candidatePaths = resolveScreenConfigCandidates(workspaceRoot);
524
+ const configPath = resolveScreenConfigPath(workspaceRoot) || null;
525
+ const configDir = configPath ? path.dirname(configPath) : null;
526
+ if (!configPath || !pathExists(configPath)) {
527
+ return {
528
+ ok: false,
529
+ error: {
530
+ code: "SCREEN_CONFIG_ERROR",
531
+ message: `screening-config.json 不存在。请先完成 recommend 配置。${configPath ? ` (path: ${configPath})` : ""}`,
532
+ retryable: true
533
+ },
534
+ config_path: configPath,
535
+ config_dir: configDir,
536
+ candidate_paths: candidatePaths
537
+ };
538
+ }
539
+ const parsed = readJsonFile(configPath);
540
+ const validation = validateScreeningConfig(parsed);
541
+ if (!validation.ok) {
542
+ return {
543
+ ok: false,
544
+ error: {
545
+ code: "SCREEN_CONFIG_ERROR",
546
+ message: `${validation.message} (path: ${configPath})`,
547
+ retryable: true
548
+ },
549
+ config_path: configPath,
550
+ config_dir: configDir,
551
+ candidate_paths: candidatePaths
552
+ };
553
+ }
554
+ const llmModels = normalizeScreeningLlmModels(parsed);
555
+ const primaryLlmModel = llmModels[0] || {};
556
+ const greetingText = normalizeText(
557
+ parsed.greetingMessage
558
+ || parsed.greeting_message
559
+ || parsed.greetingText
560
+ || parsed.greeting_text
561
+ || parsed.greeting
562
+ );
563
+ const humanRestEnabled = parseConfigBoolean(parsed.humanRestEnabled, false);
564
+ const humanBehavior = normalizeHumanBehaviorOptions(parsed.humanBehavior || parsed.human_behavior || null, {
565
+ legacyEnabled: humanRestEnabled
566
+ });
567
+ return {
568
+ ok: true,
569
+ config: {
570
+ ...primaryLlmModel,
571
+ baseUrl: primaryLlmModel.baseUrl,
572
+ apiKey: primaryLlmModel.apiKey,
573
+ model: primaryLlmModel.model,
574
+ openaiOrganization: primaryLlmModel.openaiOrganization,
575
+ openaiProject: primaryLlmModel.openaiProject,
576
+ llmModels,
577
+ greetingMessage: greetingText,
578
+ greetingText,
579
+ debugPort: parsePositiveInteger(parsed.debugPort, 9222),
580
+ outputDir: resolveConfigPathValue(parsed.outputDir, configDir),
581
+ humanRestEnabled,
582
+ humanBehavior
583
+ },
584
+ config_path: configPath,
585
+ config_dir: configDir,
586
+ candidate_paths: candidatePaths
587
+ };
588
+ }
589
+
590
+ export function resolveBossConfiguredOutputDir(workspaceRoot, fallbackDir = "") {
591
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
592
+ const configuredDir = configResolution.ok ? normalizeText(configResolution.config.outputDir) : "";
593
+ if (configuredDir) return configuredDir;
594
+ return fallbackDir ? path.resolve(fallbackDir) : "";
595
+ }
596
+
597
+ function isUnlimitedTargetCountToken(value) {
598
+ const token = normalizeText(value).toLowerCase();
599
+ if (!token) return false;
600
+ const compact = token.replace(/\s+/g, "");
601
+ const withoutAnnotation = compact.replace(/[((【[].*?[))】\]]/gu, "");
602
+ const knownTokens = new Set([
603
+ "all",
604
+ "unlimited",
605
+ "infinity",
606
+ "inf",
607
+ "max",
608
+ "full",
609
+ "allcandidates",
610
+ "全部",
611
+ "全量",
612
+ "不限",
613
+ "扫到底",
614
+ "全部候选人",
615
+ "所有候选人",
616
+ "全部人选",
617
+ "所有人选",
618
+ "直到完成所有人选"
619
+ ]);
620
+ if (knownTokens.has(token) || knownTokens.has(compact) || knownTokens.has(withoutAnnotation)) return true;
621
+ if (/^(?:all|unlimited|infinity|inf|max|full)(?:candidate|candidates)?$/i.test(compact)) return true;
622
+ if (/^(?:all|unlimited|infinity|inf|max|full)(?:候选人|人选|牛人|人才|人员)?$/iu.test(withoutAnnotation)) return true;
623
+ if (/^(?:全部|所有|全量|不限)(?:候选人|人选|牛人|人才|人员)?$/u.test(compact)) return true;
624
+ if (!/\d/.test(compact) && /(?:扫到底|全部候选人|所有候选人|全部人选|所有人选)/u.test(compact)) return true;
625
+ return false;
626
+ }
627
+
628
+ function getWrappedTargetCountValue(value) {
629
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
630
+ for (const key of TARGET_COUNT_WRAPPER_KEYS) {
631
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
632
+ return value[key];
633
+ }
634
+ }
635
+ return value;
636
+ }
637
+
638
+ export function getBossChatTargetCountValue(input = {}) {
639
+ if (!input || typeof input !== "object" || Array.isArray(input)) return undefined;
640
+ if (Object.prototype.hasOwnProperty.call(input, "target_count") && input.target_count !== undefined && input.target_count !== null) {
641
+ return input.target_count;
642
+ }
643
+ if (Object.prototype.hasOwnProperty.call(input, "targetCount") && input.targetCount !== undefined && input.targetCount !== null) {
644
+ return input.targetCount;
645
+ }
646
+ if (Object.prototype.hasOwnProperty.call(input, "target_count")) return input.target_count;
647
+ if (Object.prototype.hasOwnProperty.call(input, "targetCount")) return input.targetCount;
648
+ return undefined;
649
+ }
650
+
651
+ function cloneForDiagnostics(value) {
652
+ if (value === undefined) return undefined;
653
+ if (value === null || ["string", "number", "boolean"].includes(typeof value)) return value;
654
+ try {
655
+ return JSON.parse(JSON.stringify(value));
656
+ } catch {
657
+ return String(value);
658
+ }
659
+ }
660
+
661
+ export function buildTargetCountCompatibilityHints({
662
+ argumentName = "target_count",
663
+ recommendedArgumentPatch = { target_count: TARGET_COUNT_CANONICAL_ALL },
664
+ includeOptions = true
665
+ } = {}) {
666
+ const normalizedArgumentName = normalizeText(argumentName) || "target_count";
667
+ const clonedRecommendedPatch = cloneForDiagnostics(recommendedArgumentPatch)
668
+ || { target_count: TARGET_COUNT_CANONICAL_ALL };
669
+ const literal = `${normalizedArgumentName}="${TARGET_COUNT_CANONICAL_ALL}"`;
670
+ const base = {
671
+ argument_name: normalizedArgumentName,
672
+ answer_format: `${normalizedArgumentName} = 正整数 | "${TARGET_COUNT_CANONICAL_ALL}"`,
673
+ canonical_unlimited_value: TARGET_COUNT_CANONICAL_ALL,
674
+ recommended_value: TARGET_COUNT_CANONICAL_ALL,
675
+ recommended_argument_patch: clonedRecommendedPatch,
676
+ accepted_examples: TARGET_COUNT_ACCEPTED_EXAMPLES.slice()
677
+ };
678
+ if (!includeOptions) return base;
679
+ return {
680
+ ...base,
681
+ options: [
682
+ {
683
+ label: `扫到底(必须传 ${literal},推荐)`,
684
+ value: TARGET_COUNT_CANONICAL_ALL,
685
+ canonical_value: TARGET_COUNT_CANONICAL_ALL,
686
+ argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
687
+ },
688
+ {
689
+ label: `不限(等价于 ${literal})`,
690
+ value: "unlimited",
691
+ canonical_value: TARGET_COUNT_CANONICAL_ALL,
692
+ argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
693
+ },
694
+ {
695
+ label: `全部候选人(等价于 ${literal})`,
696
+ value: "全部候选人",
697
+ canonical_value: TARGET_COUNT_CANONICAL_ALL,
698
+ argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
699
+ },
700
+ {
701
+ label: `所有候选人(等价于 ${literal})`,
702
+ value: "所有候选人",
703
+ canonical_value: TARGET_COUNT_CANONICAL_ALL,
704
+ argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
705
+ }
706
+ ]
707
+ };
708
+ }
709
+
710
+ export function normalizeTargetCountInput(value) {
711
+ if (value === undefined || value === null) {
712
+ return {
713
+ provided: false,
714
+ targetCount: null,
715
+ cliArg: null,
716
+ publicValue: null,
717
+ rawValue: value,
718
+ parseError: null
719
+ };
720
+ }
721
+ const unwrapped = getWrappedTargetCountValue(value);
722
+ if (unwrapped !== value) {
723
+ return normalizeTargetCountInput(unwrapped);
724
+ }
725
+ const raw = normalizeText(unwrapped);
726
+ if (!raw) {
727
+ return {
728
+ provided: false,
729
+ targetCount: null,
730
+ cliArg: null,
731
+ publicValue: null,
732
+ rawValue: value,
733
+ parseError: null
734
+ };
735
+ }
736
+ if (isUnlimitedTargetCountToken(raw)) {
737
+ return {
738
+ provided: true,
739
+ targetCount: null,
740
+ cliArg: "-1",
741
+ publicValue: TARGET_COUNT_CANONICAL_ALL,
742
+ rawValue: cloneForDiagnostics(value),
743
+ parseError: null
744
+ };
745
+ }
746
+ const parsed = Number.parseInt(String(raw), 10);
747
+ if (Number.isFinite(parsed) && parsed === -1) {
748
+ return {
749
+ provided: true,
750
+ targetCount: null,
751
+ cliArg: "-1",
752
+ publicValue: TARGET_COUNT_CANONICAL_ALL,
753
+ rawValue: cloneForDiagnostics(value),
754
+ parseError: null
755
+ };
756
+ }
757
+ if (Number.isFinite(parsed) && parsed > 0) {
758
+ return {
759
+ provided: true,
760
+ targetCount: parsed,
761
+ cliArg: String(parsed),
762
+ publicValue: parsed,
763
+ rawValue: cloneForDiagnostics(value),
764
+ parseError: null
765
+ };
766
+ }
767
+ return {
768
+ provided: false,
769
+ targetCount: null,
770
+ cliArg: null,
771
+ publicValue: null,
772
+ rawValue: cloneForDiagnostics(value),
773
+ parseError: "target_count must be a positive integer, -1, or one of: all, unlimited, 全部, 不限, 扫到底, 全量, 全部候选人, 所有候选人"
774
+ };
775
+ }