@reconcrap/boss-recommend-mcp 1.3.38 → 2.0.0

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 (85) hide show
  1. package/README.md +53 -33
  2. package/package.json +61 -9
  3. package/skills/boss-recommend-pipeline/SKILL.md +4 -0
  4. package/src/chat-mcp.js +1333 -0
  5. package/src/chat-runtime-config.js +559 -0
  6. package/src/cli.js +1095 -196
  7. package/src/core/browser/index.js +378 -0
  8. package/src/core/capture/index.js +298 -0
  9. package/src/core/cv-acquisition/index.js +219 -0
  10. package/src/core/greet-quota/index.js +54 -0
  11. package/src/core/infinite-list/index.js +459 -0
  12. package/src/core/reporting/legacy-csv.js +332 -0
  13. package/src/core/run/index.js +286 -0
  14. package/src/core/screening/index.js +1166 -0
  15. package/src/core/self-heal/index.js +848 -0
  16. package/src/domains/chat/cards.js +129 -0
  17. package/src/domains/chat/constants.js +183 -0
  18. package/src/domains/chat/detail.js +1369 -0
  19. package/src/domains/chat/index.js +7 -0
  20. package/src/domains/chat/jobs.js +334 -0
  21. package/src/domains/chat/page-guard.js +88 -0
  22. package/src/domains/chat/roots.js +56 -0
  23. package/src/domains/chat/run-service.js +1101 -0
  24. package/src/domains/recommend/actions.js +457 -0
  25. package/src/domains/recommend/cards.js +228 -0
  26. package/src/domains/recommend/constants.js +141 -0
  27. package/src/domains/recommend/detail.js +341 -0
  28. package/src/domains/recommend/filters.js +581 -0
  29. package/src/domains/recommend/index.js +10 -0
  30. package/src/domains/recommend/jobs.js +232 -0
  31. package/src/domains/recommend/refresh.js +204 -0
  32. package/src/domains/recommend/roots.js +78 -0
  33. package/src/domains/recommend/run-service.js +903 -0
  34. package/src/domains/recommend/scopes.js +245 -0
  35. package/src/domains/recruit/actions.js +277 -0
  36. package/src/domains/recruit/cards.js +67 -0
  37. package/src/domains/recruit/constants.js +130 -0
  38. package/src/domains/recruit/detail.js +414 -0
  39. package/src/domains/recruit/index.js +9 -0
  40. package/src/domains/recruit/instruction-parser.js +451 -0
  41. package/src/domains/recruit/refresh.js +40 -0
  42. package/src/domains/recruit/roots.js +68 -0
  43. package/src/domains/recruit/run-service.js +580 -0
  44. package/src/domains/recruit/search.js +1149 -0
  45. package/src/index.js +578 -419
  46. package/src/recommend-mcp.js +1257 -0
  47. package/src/recruit-mcp.js +1035 -0
  48. package/src/adapters.js +0 -3079
  49. package/src/boss-chat.js +0 -1037
  50. package/src/pipeline.js +0 -2249
  51. package/src/recommend-healing-config.js +0 -131
  52. package/src/recommend-healing-rules.json +0 -261
  53. package/src/self-heal.js +0 -2237
  54. package/src/test-adapters-runtime.js +0 -628
  55. package/src/test-boss-chat.js +0 -3196
  56. package/src/test-index-async.js +0 -498
  57. package/src/test-parser.js +0 -742
  58. package/src/test-pipeline.js +0 -2703
  59. package/src/test-run-state.js +0 -152
  60. package/src/test-self-heal.js +0 -224
  61. package/vendor/boss-chat-cli/README.md +0 -134
  62. package/vendor/boss-chat-cli/package.json +0 -53
  63. package/vendor/boss-chat-cli/src/app.js +0 -1501
  64. package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
  65. package/vendor/boss-chat-cli/src/cli.js +0 -1713
  66. package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
  67. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
  68. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
  69. package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
  70. package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
  71. package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
  72. package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
  73. package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
  74. package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
  75. package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
  76. package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
  77. package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
  78. package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
  79. package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
  80. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -6927
  81. package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
  82. package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
  83. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2294
  84. package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
  85. package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
package/src/boss-chat.js DELETED
@@ -1,1037 +0,0 @@
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 { spawn } from "node:child_process";
6
- import { fileURLToPath } from "node:url";
7
-
8
- import { getScreenConfigResolution, resolveSharedLlmTransportConfig } from "./adapters.js";
9
-
10
- const currentFilePath = fileURLToPath(import.meta.url);
11
- const packageRoot = path.resolve(path.dirname(currentFilePath), "..");
12
- const VENDORED_BOSS_CHAT_DIR = path.join(packageRoot, "vendor", "boss-chat-cli");
13
- const DEFAULT_BOSS_CHAT_POLL_MS = 1500;
14
- const PREPARE_BOSS_CHAT_MAX_ATTEMPTS = 3;
15
- const PREPARE_BOSS_CHAT_RETRY_DELAY_MS = 1200;
16
- const BOSS_CHAT_TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
17
- const CHAT_REQUIRED_FIELDS = ["job", "start_from", "target_count", "criteria"];
18
- const BOSS_CHAT_RUNTIME_SUBDIR = "boss-chat";
19
- const BOSS_CHAT_RUNTIME_CHILD_DIRS = ["logs", "runs", "profiles", "reports", "artifacts", "state"];
20
- export const TARGET_COUNT_CANONICAL_ALL = "all";
21
- export const TARGET_COUNT_ACCEPTED_EXAMPLES = [TARGET_COUNT_CANONICAL_ALL, -1, 20, "全部候选人"];
22
- const TARGET_COUNT_WRAPPER_KEYS = ["target_count", "targetCount", "value", "count", "limit"];
23
- const LLM_THINKING_LEVEL_FIELDS = [
24
- "llmThinkingLevel",
25
- "thinkingLevel",
26
- "reasoningEffort",
27
- "reasoning_effort"
28
- ];
29
-
30
- function normalizeText(value) {
31
- return String(value || "").replace(/\s+/g, " ").trim();
32
- }
33
-
34
- function pathExists(targetPath) {
35
- try {
36
- return fs.existsSync(targetPath);
37
- } catch {
38
- return false;
39
- }
40
- }
41
-
42
- function getStateHome() {
43
- return process.env.BOSS_RECOMMEND_HOME
44
- ? path.resolve(process.env.BOSS_RECOMMEND_HOME)
45
- : path.join(os.homedir(), ".boss-recommend-mcp");
46
- }
47
-
48
- function isRootDirectory(targetPath) {
49
- const resolved = path.resolve(String(targetPath || ""));
50
- const parsed = path.parse(resolved);
51
- return resolved.toLowerCase() === String(parsed.root || "").toLowerCase();
52
- }
53
-
54
- function isSystemDirectoryWorkspaceRoot(workspaceRoot) {
55
- const root = path.resolve(String(workspaceRoot || ""));
56
- const normalized = root.replace(/\\/g, "/").toLowerCase();
57
- if (process.platform === "win32") {
58
- return (
59
- normalized.endsWith("/windows")
60
- || normalized.endsWith("/windows/system32")
61
- || normalized.endsWith("/windows/syswow64")
62
- || normalized.endsWith("/program files")
63
- || normalized.endsWith("/program files (x86)")
64
- );
65
- }
66
- return (
67
- normalized === "/system"
68
- || normalized.startsWith("/system/")
69
- || normalized === "/usr"
70
- || normalized.startsWith("/usr/")
71
- || normalized === "/bin"
72
- || normalized.startsWith("/bin/")
73
- || normalized === "/sbin"
74
- || normalized.startsWith("/sbin/")
75
- );
76
- }
77
-
78
- function isEphemeralWorkspaceRoot(workspaceRoot) {
79
- const normalized = path.resolve(String(workspaceRoot || ""))
80
- .replace(/\\/g, "/")
81
- .toLowerCase();
82
- return (
83
- normalized.includes("/appdata/local/npm-cache/_npx/")
84
- || normalized.includes("/node_modules/@reconcrap/boss-recommend-mcp")
85
- );
86
- }
87
-
88
- function isSafeBossChatLegacyWorkspaceRoot(workspaceRoot) {
89
- const root = path.resolve(String(workspaceRoot || ""));
90
- if (!root) return false;
91
- const home = path.resolve(os.homedir());
92
- return !(
93
- isEphemeralWorkspaceRoot(root)
94
- || isRootDirectory(root)
95
- || root.toLowerCase() === home.toLowerCase()
96
- || isSystemDirectoryWorkspaceRoot(root)
97
- );
98
- }
99
-
100
- function isUnsafeBossChatDataDir(targetPath) {
101
- const resolved = path.resolve(String(targetPath || ""));
102
- return isRootDirectory(resolved) || isSystemDirectoryWorkspaceRoot(resolved);
103
- }
104
-
105
- function resolveBossChatDataDir() {
106
- if (process.env.BOSS_CHAT_HOME) {
107
- return {
108
- data_dir: path.resolve(process.env.BOSS_CHAT_HOME),
109
- data_dir_source: "env:BOSS_CHAT_HOME"
110
- };
111
- }
112
- const stateHome = getStateHome();
113
- const source = process.env.BOSS_RECOMMEND_HOME
114
- ? "default:env:BOSS_RECOMMEND_HOME"
115
- : "default:user_home";
116
- return {
117
- data_dir: path.join(stateHome, BOSS_CHAT_RUNTIME_SUBDIR),
118
- data_dir_source: source
119
- };
120
- }
121
-
122
- export function getBossChatDataDir() {
123
- return resolveBossChatDataDir().data_dir;
124
- }
125
-
126
- export function getLegacyBossChatWorkspaceDataDir(workspaceRoot) {
127
- if (!isSafeBossChatLegacyWorkspaceRoot(workspaceRoot)) return null;
128
- return path.join(path.resolve(String(workspaceRoot)), ".boss-chat");
129
- }
130
-
131
- export function resolveBossChatRuntimeLayout(workspaceRoot) {
132
- const resolvedDataDir = resolveBossChatDataDir();
133
- const dataDir = resolvedDataDir.data_dir;
134
- const legacyWorkspaceDir = getLegacyBossChatWorkspaceDataDir(workspaceRoot);
135
- const migrationSourceDir =
136
- legacyWorkspaceDir && pathExists(legacyWorkspaceDir) && !pathExists(dataDir)
137
- ? legacyWorkspaceDir
138
- : null;
139
- return {
140
- workspace_root: workspaceRoot ? path.resolve(String(workspaceRoot)) : null,
141
- data_dir: dataDir,
142
- data_dir_source: resolvedDataDir.data_dir_source,
143
- legacy_workspace_dir: legacyWorkspaceDir,
144
- migration_source_dir: migrationSourceDir,
145
- migration_pending: Boolean(migrationSourceDir),
146
- directories: [
147
- dataDir,
148
- ...BOSS_CHAT_RUNTIME_CHILD_DIRS.map((name) => path.join(dataDir, name))
149
- ]
150
- };
151
- }
152
-
153
- export function ensureBossChatRuntimeReady(workspaceRoot) {
154
- const runtime = resolveBossChatRuntimeLayout(workspaceRoot);
155
- const created = [];
156
- const existed = [];
157
- const failed = [];
158
- let migration = {
159
- attempted: false,
160
- performed: false,
161
- source: runtime.migration_source_dir,
162
- target: runtime.data_dir,
163
- message: runtime.migration_source_dir
164
- ? `Pending legacy boss-chat migration from ${runtime.migration_source_dir}`
165
- : ""
166
- };
167
-
168
- if (isUnsafeBossChatDataDir(runtime.data_dir)) {
169
- return {
170
- ...runtime,
171
- created,
172
- existed,
173
- failed: [
174
- {
175
- path: runtime.data_dir,
176
- message: `Refusing unsafe boss-chat runtime path: ${runtime.data_dir}. Please use BOSS_CHAT_HOME in a writable user directory.`
177
- }
178
- ],
179
- migration,
180
- blocked_reason: "UNSAFE_DATA_DIR"
181
- };
182
- }
183
-
184
- if (runtime.migration_source_dir) {
185
- try {
186
- fs.cpSync(runtime.migration_source_dir, runtime.data_dir, {
187
- recursive: true,
188
- force: false,
189
- errorOnExist: false
190
- });
191
- migration = {
192
- attempted: true,
193
- performed: true,
194
- source: runtime.migration_source_dir,
195
- target: runtime.data_dir,
196
- message: `Migrated legacy boss-chat runtime from ${runtime.migration_source_dir} to ${runtime.data_dir}. Legacy source was preserved.`
197
- };
198
- } catch (error) {
199
- migration = {
200
- attempted: true,
201
- performed: false,
202
- source: runtime.migration_source_dir,
203
- target: runtime.data_dir,
204
- message: error?.message || "Legacy boss-chat migration failed."
205
- };
206
- failed.push({
207
- path: runtime.data_dir,
208
- message: `Legacy migration failed: ${migration.message}`
209
- });
210
- }
211
- }
212
-
213
- for (const directory of runtime.directories) {
214
- try {
215
- const existedBefore = pathExists(directory);
216
- fs.mkdirSync(directory, { recursive: true });
217
- if (existedBefore) {
218
- existed.push(directory);
219
- } else {
220
- created.push(directory);
221
- }
222
- } catch (error) {
223
- failed.push({
224
- path: directory,
225
- message: error?.message || String(error)
226
- });
227
- }
228
- }
229
-
230
- return {
231
- ...runtime,
232
- created,
233
- existed,
234
- failed,
235
- migration
236
- };
237
- }
238
-
239
- function parsePositiveInteger(value, fallback = null) {
240
- const parsed = Number.parseInt(String(value ?? ""), 10);
241
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
242
- }
243
-
244
- function parseBooleanValue(value) {
245
- if (typeof value === "boolean") return value;
246
- const normalized = normalizeText(value).toLowerCase();
247
- if (!normalized) return null;
248
- if (["1", "true", "yes", "y", "on", "是"].includes(normalized)) return true;
249
- if (["0", "false", "no", "n", "off", "否"].includes(normalized)) return false;
250
- return null;
251
- }
252
-
253
- function resolveHumanRestEnabled(config = {}) {
254
- if (!config || typeof config !== "object" || Array.isArray(config)) return false;
255
- const candidates = [
256
- config.humanRestEnabled,
257
- config.human_rest_enabled,
258
- config.humanLikeRestEnabled,
259
- config.human_like_rest_enabled
260
- ];
261
- for (const candidate of candidates) {
262
- const parsed = parseBooleanValue(candidate);
263
- if (typeof parsed === "boolean") return parsed;
264
- }
265
- return false;
266
- }
267
-
268
- function isUnlimitedTargetCountToken(value) {
269
- const token = normalizeText(value).toLowerCase();
270
- if (!token) return false;
271
- const compact = token.replace(/\s+/g, "");
272
- const withoutAnnotation = compact.replace(/[((【[].*?[))】\]]/gu, "");
273
- const knownTokens = new Set([
274
- "all",
275
- "unlimited",
276
- "infinity",
277
- "inf",
278
- "max",
279
- "full",
280
- "allcandidates",
281
- "全部",
282
- "全量",
283
- "不限",
284
- "扫到底",
285
- "全部候选人",
286
- "所有候选人",
287
- "全部人选",
288
- "所有人选",
289
- "直到完成所有人选"
290
- ]);
291
- if (knownTokens.has(token) || knownTokens.has(compact) || knownTokens.has(withoutAnnotation)) return true;
292
- if (/^(?:all|unlimited|infinity|inf|max|full)(?:candidate|candidates)?$/i.test(compact)) return true;
293
- if (/^(?:all|unlimited|infinity|inf|max|full)(?:候选人|人选|牛人|人才|人员)?$/iu.test(withoutAnnotation)) return true;
294
- if (/^(?:全部|所有|全量|不限)(?:候选人|人选|牛人|人才|人员)?$/u.test(compact)) return true;
295
- if (!/\d/.test(compact) && /(?:扫到底|全部候选人|所有候选人|全部人选|所有人选)/u.test(compact)) return true;
296
- return false;
297
- }
298
-
299
- function getWrappedTargetCountValue(value) {
300
- if (!value || typeof value !== "object" || Array.isArray(value)) return value;
301
- for (const key of TARGET_COUNT_WRAPPER_KEYS) {
302
- if (Object.prototype.hasOwnProperty.call(value, key)) {
303
- return value[key];
304
- }
305
- }
306
- return value;
307
- }
308
-
309
- export function getBossChatTargetCountValue(input = {}) {
310
- if (!input || typeof input !== "object" || Array.isArray(input)) return undefined;
311
- if (Object.prototype.hasOwnProperty.call(input, "target_count") && input.target_count !== undefined && input.target_count !== null) {
312
- return input.target_count;
313
- }
314
- if (Object.prototype.hasOwnProperty.call(input, "targetCount") && input.targetCount !== undefined && input.targetCount !== null) {
315
- return input.targetCount;
316
- }
317
- if (Object.prototype.hasOwnProperty.call(input, "target_count")) return input.target_count;
318
- if (Object.prototype.hasOwnProperty.call(input, "targetCount")) return input.targetCount;
319
- return undefined;
320
- }
321
-
322
- function cloneForDiagnostics(value) {
323
- if (value === undefined) return undefined;
324
- if (value === null || ["string", "number", "boolean"].includes(typeof value)) return value;
325
- try {
326
- return JSON.parse(JSON.stringify(value));
327
- } catch {
328
- return String(value);
329
- }
330
- }
331
-
332
- export function buildTargetCountCompatibilityHints({
333
- argumentName = "target_count",
334
- recommendedArgumentPatch = { target_count: TARGET_COUNT_CANONICAL_ALL },
335
- includeOptions = true
336
- } = {}) {
337
- const normalizedArgumentName = normalizeText(argumentName) || "target_count";
338
- const clonedRecommendedPatch = cloneForDiagnostics(recommendedArgumentPatch)
339
- || { target_count: TARGET_COUNT_CANONICAL_ALL };
340
- const literal = `${normalizedArgumentName}="${TARGET_COUNT_CANONICAL_ALL}"`;
341
- const base = {
342
- argument_name: normalizedArgumentName,
343
- answer_format: `${normalizedArgumentName} = 正整数 | "${TARGET_COUNT_CANONICAL_ALL}"`,
344
- canonical_unlimited_value: TARGET_COUNT_CANONICAL_ALL,
345
- recommended_value: TARGET_COUNT_CANONICAL_ALL,
346
- recommended_argument_patch: clonedRecommendedPatch,
347
- accepted_examples: TARGET_COUNT_ACCEPTED_EXAMPLES.slice()
348
- };
349
- if (!includeOptions) return base;
350
- return {
351
- ...base,
352
- options: [
353
- {
354
- label: `扫到底(必须传 ${literal},推荐)`,
355
- value: TARGET_COUNT_CANONICAL_ALL,
356
- canonical_value: TARGET_COUNT_CANONICAL_ALL,
357
- argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
358
- },
359
- {
360
- label: `不限(等价于 ${literal})`,
361
- value: "unlimited",
362
- canonical_value: TARGET_COUNT_CANONICAL_ALL,
363
- argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
364
- },
365
- {
366
- label: `全部候选人(等价于 ${literal})`,
367
- value: "全部候选人",
368
- canonical_value: TARGET_COUNT_CANONICAL_ALL,
369
- argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
370
- },
371
- {
372
- label: `所有候选人(等价于 ${literal})`,
373
- value: "所有候选人",
374
- canonical_value: TARGET_COUNT_CANONICAL_ALL,
375
- argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
376
- }
377
- ]
378
- };
379
- }
380
-
381
- export function normalizeTargetCountInput(value) {
382
- if (value === undefined || value === null) {
383
- return {
384
- provided: false,
385
- targetCount: null,
386
- cliArg: null,
387
- publicValue: null,
388
- rawValue: value,
389
- parseError: null
390
- };
391
- }
392
- const unwrapped = getWrappedTargetCountValue(value);
393
- if (unwrapped !== value) {
394
- return normalizeTargetCountInput(unwrapped);
395
- }
396
- const raw = normalizeText(unwrapped);
397
- if (!raw) {
398
- return {
399
- provided: false,
400
- targetCount: null,
401
- cliArg: null,
402
- publicValue: null,
403
- rawValue: value,
404
- parseError: null
405
- };
406
- }
407
- if (isUnlimitedTargetCountToken(raw)) {
408
- return {
409
- provided: true,
410
- targetCount: null,
411
- cliArg: "-1",
412
- publicValue: "all",
413
- rawValue: cloneForDiagnostics(value),
414
- parseError: null
415
- };
416
- }
417
- const parsed = Number.parseInt(String(raw), 10);
418
- if (Number.isFinite(parsed) && parsed === -1) {
419
- return {
420
- provided: true,
421
- targetCount: null,
422
- cliArg: "-1",
423
- publicValue: "all",
424
- rawValue: cloneForDiagnostics(value),
425
- parseError: null
426
- };
427
- }
428
- if (Number.isFinite(parsed) && parsed > 0) {
429
- return {
430
- provided: true,
431
- targetCount: parsed,
432
- cliArg: String(parsed),
433
- publicValue: parsed,
434
- rawValue: cloneForDiagnostics(value),
435
- parseError: null
436
- };
437
- }
438
- return {
439
- provided: false,
440
- targetCount: null,
441
- cliArg: null,
442
- publicValue: null,
443
- rawValue: cloneForDiagnostics(value),
444
- parseError: "target_count must be a positive integer, -1, or one of: all, unlimited, 全部, 不限, 扫到底, 全量, 全部候选人, 所有候选人"
445
- };
446
- }
447
-
448
- function parseJsonOutput(text) {
449
- const trimmed = String(text || "").trim();
450
- if (!trimmed) return null;
451
- try {
452
- return JSON.parse(trimmed);
453
- } catch {}
454
- const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
455
- for (let index = lines.length - 1; index >= 0; index -= 1) {
456
- try {
457
- return JSON.parse(lines[index]);
458
- } catch {
459
- continue;
460
- }
461
- }
462
- return null;
463
- }
464
-
465
- function sleep(ms) {
466
- return new Promise((resolve) => setTimeout(resolve, ms));
467
- }
468
-
469
- function resolveBossChatCliDir(workspaceRoot) {
470
- const localDir = path.join(path.resolve(String(workspaceRoot || process.cwd())), "boss-chat-cli");
471
- if (pathExists(localDir)) return localDir;
472
- return pathExists(VENDORED_BOSS_CHAT_DIR) ? VENDORED_BOSS_CHAT_DIR : null;
473
- }
474
-
475
- function resolveBossChatCliPath(workspaceRoot) {
476
- const cliDir = resolveBossChatCliDir(workspaceRoot);
477
- if (!cliDir) return null;
478
- const cliPath = path.join(cliDir, "src", "cli.js");
479
- return pathExists(cliPath) ? cliPath : null;
480
- }
481
-
482
- function validateRecommendScreenConfig(config) {
483
- if (!config || typeof config !== "object" || Array.isArray(config)) {
484
- return {
485
- ok: false,
486
- message: "screening-config.json 缺失或格式无效。请填写 baseUrl、apiKey、model。"
487
- };
488
- }
489
- const baseUrl = normalizeText(config.baseUrl).replace(/\/+$/, "");
490
- const apiKey = normalizeText(config.apiKey);
491
- const model = normalizeText(config.model);
492
- const missing = [];
493
- if (!baseUrl) missing.push("baseUrl");
494
- if (!apiKey) missing.push("apiKey");
495
- if (!model) missing.push("model");
496
- if (missing.length > 0) {
497
- return {
498
- ok: false,
499
- message: `screening-config.json 缺少必填字段:${missing.join(", ")}。`
500
- };
501
- }
502
- if (/^replace-with/i.test(apiKey)) {
503
- return {
504
- ok: false,
505
- message: "screening-config.json 的 apiKey 仍是模板占位符,请填写真实 API Key。"
506
- };
507
- }
508
- return { ok: true };
509
- }
510
-
511
- function resolveLlmThinkingLevel(config = {}) {
512
- if (!config || typeof config !== "object") return "";
513
- for (const field of LLM_THINKING_LEVEL_FIELDS) {
514
- const value = normalizeText(config[field]);
515
- if (value) return value;
516
- }
517
- return "";
518
- }
519
-
520
- function resolveBossChatScreenConfig(workspaceRoot) {
521
- const resolution = getScreenConfigResolution(workspaceRoot);
522
- const configPath = resolution.resolved_path || resolution.writable_path || resolution.legacy_path || null;
523
- if (!configPath || !pathExists(configPath)) {
524
- return {
525
- ok: false,
526
- error: {
527
- code: "SCREEN_CONFIG_ERROR",
528
- message: `screening-config.json 不存在。请先完成 recommend 配置。${configPath ? ` (path: ${configPath})` : ""}`
529
- },
530
- config_path: configPath,
531
- config_dir: configPath ? path.dirname(configPath) : null
532
- };
533
- }
534
- let parsed = null;
535
- try {
536
- parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
537
- } catch (error) {
538
- return {
539
- ok: false,
540
- error: {
541
- code: "SCREEN_CONFIG_ERROR",
542
- message: `screening-config.json 解析失败:${error.message || "unknown error"} (path: ${configPath})`
543
- },
544
- config_path: configPath,
545
- config_dir: path.dirname(configPath)
546
- };
547
- }
548
- const validation = validateRecommendScreenConfig(parsed);
549
- if (!validation.ok) {
550
- return {
551
- ok: false,
552
- error: {
553
- code: "SCREEN_CONFIG_ERROR",
554
- message: `${validation.message} (path: ${configPath})`
555
- },
556
- config_path: configPath,
557
- config_dir: path.dirname(configPath)
558
- };
559
- }
560
- return {
561
- ok: true,
562
- config: {
563
- baseUrl: normalizeText(parsed.baseUrl).replace(/\/+$/, ""),
564
- apiKey: normalizeText(parsed.apiKey),
565
- model: normalizeText(parsed.model),
566
- llmThinkingLevel: resolveLlmThinkingLevel(parsed),
567
- ...resolveSharedLlmTransportConfig(parsed),
568
- debugPort: parsePositiveInteger(parsed.debugPort, 9222),
569
- humanRestEnabled: resolveHumanRestEnabled(parsed)
570
- },
571
- config_path: configPath,
572
- config_dir: path.dirname(configPath)
573
- };
574
- }
575
-
576
- function normalizeBossChatStartInput(input = {}) {
577
- const profile = normalizeText(input.profile) || "default";
578
- const job = normalizeText(input.job);
579
- const startFromRaw = normalizeText(input.startFrom || input.start_from).toLowerCase();
580
- const startFrom = startFromRaw === "all" ? "all" : startFromRaw === "unread" ? "unread" : "";
581
- const criteria = normalizeText(input.criteria);
582
- const greetingText = normalizeText(input.greeting_text || input.greetingText || input.greeting);
583
- const parsedTarget = normalizeTargetCountInput(getBossChatTargetCountValue(input));
584
- const port = parsePositiveInteger(input.port);
585
- return {
586
- profile,
587
- job,
588
- startFrom,
589
- criteria,
590
- greetingText,
591
- targetCount: parsedTarget.targetCount,
592
- targetCountArg: parsedTarget.cliArg,
593
- targetCountProvided: parsedTarget.provided,
594
- targetCountPublicValue: parsedTarget.publicValue,
595
- targetCountRawValue: parsedTarget.rawValue,
596
- targetCountParseError: parsedTarget.parseError,
597
- port,
598
- dryRun: input.dryRun === true || input.dry_run === true,
599
- noState: input.noState === true || input.no_state === true,
600
- safePacing: typeof input.safePacing === "boolean" ? input.safePacing : (
601
- typeof input.safe_pacing === "boolean" ? input.safe_pacing : undefined
602
- ),
603
- batchRestEnabled: typeof input.batchRestEnabled === "boolean" ? input.batchRestEnabled : (
604
- typeof input.batch_rest_enabled === "boolean" ? input.batch_rest_enabled : undefined
605
- )
606
- };
607
- }
608
-
609
- function normalizeBossChatRunId(input = {}) {
610
- return normalizeText(input.runId || input.run_id);
611
- }
612
-
613
- function getMissingBossChatStartFields(input = {}) {
614
- const normalized = normalizeBossChatStartInput(input);
615
- const missing = [];
616
- if (!normalized.job) missing.push("job");
617
- if (!normalized.startFrom) missing.push("start_from");
618
- if (!normalized.targetCountProvided) missing.push("target_count");
619
- if (!normalized.criteria) missing.push("criteria");
620
- return missing;
621
- }
622
-
623
- function buildTargetCountQuestionHint(item = {}) {
624
- const next = { ...item };
625
- const hints = buildTargetCountCompatibilityHints({
626
- argumentName: "target_count",
627
- recommendedArgumentPatch: { target_count: TARGET_COUNT_CANONICAL_ALL }
628
- });
629
- return {
630
- ...next,
631
- ...hints,
632
- question: `请输入 target_count:正整数,或直接填写 "${TARGET_COUNT_CANONICAL_ALL}"(扫到底)。`,
633
- examples: TARGET_COUNT_ACCEPTED_EXAMPLES.slice()
634
- };
635
- }
636
-
637
- function normalizePendingQuestions(pendingQuestions = []) {
638
- return pendingQuestions.map((item) => {
639
- if (String(item?.field || "") !== "target_count") return item;
640
- return buildTargetCountQuestionHint(item);
641
- });
642
- }
643
-
644
- function buildNextCallExample(input = {}, missingFields = []) {
645
- if (!Array.isArray(missingFields) || missingFields.length === 0) return null;
646
- const normalized = normalizeBossChatStartInput(input);
647
- const sample = {};
648
- if (normalized.job) sample.job = normalized.job;
649
- if (normalized.startFrom) sample.start_from = normalized.startFrom;
650
- if (normalized.criteria) sample.criteria = normalized.criteria;
651
- if (normalized.greetingText) sample.greeting_text = normalized.greetingText;
652
- if (normalized.targetCountProvided) {
653
- sample.target_count = normalized.targetCountPublicValue || (normalized.targetCountArg === "-1" ? "all" : normalized.targetCount);
654
- } else if (missingFields.includes("target_count")) {
655
- sample.target_count = "all";
656
- }
657
- return Object.keys(sample).length > 0 ? sample : null;
658
- }
659
-
660
- function buildTargetCountNeedInputDiagnostics(input = {}, missingFields = []) {
661
- if (!Array.isArray(missingFields) || !missingFields.includes("target_count")) return {};
662
- const normalized = normalizeBossChatStartInput(input);
663
- const hints = buildTargetCountCompatibilityHints({
664
- argumentName: "target_count",
665
- recommendedArgumentPatch: { target_count: TARGET_COUNT_CANONICAL_ALL },
666
- includeOptions: false
667
- });
668
- return {
669
- ...hints,
670
- ...(normalized.targetCountRawValue !== undefined ? { received_target_count: normalized.targetCountRawValue } : {}),
671
- ...(normalized.targetCountParseError ? { target_count_parse_error: normalized.targetCountParseError } : {})
672
- };
673
- }
674
-
675
- function buildBossChatCliArgs(command, input, resolvedConfig, runtimeLayout = null) {
676
- const args = [command, "--json"];
677
- if (runtimeLayout?.data_dir) {
678
- args.push("--data-dir", runtimeLayout.data_dir);
679
- }
680
- if (command === "prepare-run") {
681
- const normalized = normalizeBossChatStartInput(input);
682
- args.push("--profile", normalized.profile);
683
- if (normalized.job) args.push("--job", normalized.job);
684
- if (normalized.startFrom) args.push("--start-from", normalized.startFrom);
685
- if (normalized.criteria) args.push("--criteria", normalized.criteria);
686
- if (normalized.greetingText) args.push("--greeting", normalized.greetingText);
687
- if (normalized.targetCountArg) args.push("--targetCount", normalized.targetCountArg);
688
- args.push("--port", String(normalized.port || resolvedConfig.debugPort || 9222));
689
- args.push("--baseurl", resolvedConfig.baseUrl);
690
- args.push("--apikey", resolvedConfig.apiKey);
691
- args.push("--model", resolvedConfig.model);
692
- if (resolvedConfig.llmThinkingLevel) {
693
- args.push("--thinking-level", resolvedConfig.llmThinkingLevel);
694
- }
695
- if (resolvedConfig.llmTimeoutMs) {
696
- args.push("--llm-timeout-ms", String(resolvedConfig.llmTimeoutMs));
697
- }
698
- if (resolvedConfig.llmMaxRetries) {
699
- args.push("--llm-max-retries", String(resolvedConfig.llmMaxRetries));
700
- }
701
- return args;
702
- }
703
-
704
- if (command === "start-run") {
705
- const normalized = normalizeBossChatStartInput(input);
706
- args.push("--profile", normalized.profile);
707
- if (normalized.dryRun) args.push("--dry-run");
708
- if (normalized.noState) args.push("--no-state");
709
- args.push("--job", normalized.job);
710
- args.push("--start-from", normalized.startFrom);
711
- args.push("--criteria", normalized.criteria);
712
- if (normalized.greetingText) args.push("--greeting", normalized.greetingText);
713
- if (normalized.targetCountArg) {
714
- args.push("--targetCount", normalized.targetCountArg);
715
- }
716
- args.push("--baseurl", resolvedConfig.baseUrl);
717
- args.push("--apikey", resolvedConfig.apiKey);
718
- args.push("--model", resolvedConfig.model);
719
- if (resolvedConfig.llmThinkingLevel) {
720
- args.push("--thinking-level", resolvedConfig.llmThinkingLevel);
721
- }
722
- if (resolvedConfig.llmTimeoutMs) {
723
- args.push("--llm-timeout-ms", String(resolvedConfig.llmTimeoutMs));
724
- }
725
- if (resolvedConfig.llmMaxRetries) {
726
- args.push("--llm-max-retries", String(resolvedConfig.llmMaxRetries));
727
- }
728
- args.push("--port", String(normalized.port || resolvedConfig.debugPort || 9222));
729
- if (typeof normalized.safePacing === "boolean") {
730
- args.push("--safe-pacing", String(normalized.safePacing));
731
- }
732
- if (typeof normalized.batchRestEnabled === "boolean") {
733
- args.push("--batch-rest", String(normalized.batchRestEnabled));
734
- } else if (typeof resolvedConfig?.humanRestEnabled === "boolean") {
735
- args.push("--batch-rest", String(resolvedConfig.humanRestEnabled));
736
- }
737
- return args;
738
- }
739
-
740
- const runId = normalizeBossChatRunId(input);
741
- args.push("--profile", normalizeText(input.profile) || "default");
742
- args.push("--run-id", runId);
743
- return args;
744
- }
745
-
746
- function withRuntimeDiagnostics(payload, runtimeLayout) {
747
- if (!payload || typeof payload !== "object") return payload;
748
- return {
749
- ...payload,
750
- data_dir: runtimeLayout?.data_dir || null,
751
- data_dir_source: runtimeLayout?.data_dir_source || null
752
- };
753
- }
754
-
755
- async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
756
- const runtimeLayout = ensureBossChatRuntimeReady(workspaceRoot);
757
- const cliPath = resolveBossChatCliPath(workspaceRoot);
758
- if (!cliPath) {
759
- return {
760
- ok: false,
761
- exitCode: -1,
762
- stdout: "",
763
- stderr: "",
764
- payload: {
765
- status: "FAILED",
766
- error: {
767
- code: "BOSS_CHAT_CLI_MISSING",
768
- message: "未找到 vendored boss-chat CLI。"
769
- },
770
- data_dir: runtimeLayout?.data_dir || null,
771
- data_dir_source: runtimeLayout?.data_dir_source || null
772
- }
773
- };
774
- }
775
-
776
- const runtimeInitFailed = runtimeLayout.failed.some((item) => item.path === runtimeLayout.data_dir)
777
- && !pathExists(runtimeLayout.data_dir);
778
- if (runtimeInitFailed) {
779
- return {
780
- ok: false,
781
- exitCode: 1,
782
- stdout: "",
783
- stderr: "",
784
- payload: {
785
- status: "FAILED",
786
- error: {
787
- code: "BOSS_CHAT_RUNTIME_INIT_FAILED",
788
- message: runtimeLayout.failed
789
- .filter((item) => item.path === runtimeLayout.data_dir)
790
- .map((item) => item.message)
791
- .join("; ") || "无法初始化 boss-chat runtime 目录。"
792
- },
793
- data_dir: runtimeLayout.data_dir,
794
- data_dir_source: runtimeLayout.data_dir_source,
795
- migration: runtimeLayout.migration
796
- }
797
- };
798
- }
799
-
800
- let configResolution = null;
801
- if (command === "start-run" || command === "prepare-run") {
802
- configResolution = resolveBossChatScreenConfig(workspaceRoot);
803
- if (!configResolution.ok) {
804
- return {
805
- ok: false,
806
- exitCode: 1,
807
- stdout: "",
808
- stderr: "",
809
- payload: {
810
- status: "FAILED",
811
- error: configResolution.error,
812
- config_path: configResolution.config_path,
813
- config_dir: configResolution.config_dir,
814
- data_dir: runtimeLayout.data_dir,
815
- data_dir_source: runtimeLayout.data_dir_source
816
- }
817
- };
818
- }
819
- }
820
-
821
- const args = [cliPath, ...buildBossChatCliArgs(command, input, configResolution?.config || {}, runtimeLayout)];
822
- const cwd = path.resolve(String(workspaceRoot || process.cwd()));
823
- return new Promise((resolve) => {
824
- const child = spawn(process.execPath, args, {
825
- cwd,
826
- env: {
827
- ...process.env,
828
- BOSS_CHAT_HOME: runtimeLayout.data_dir
829
- },
830
- windowsHide: true,
831
- stdio: ["ignore", "pipe", "pipe"]
832
- });
833
-
834
- let stdout = "";
835
- let stderr = "";
836
- child.stdout.on("data", (chunk) => {
837
- stdout += String(chunk);
838
- });
839
- child.stderr.on("data", (chunk) => {
840
- stderr += String(chunk);
841
- });
842
- child.on("error", (error) => {
843
- resolve({
844
- ok: false,
845
- exitCode: -1,
846
- stdout,
847
- stderr,
848
- payload: {
849
- status: "FAILED",
850
- error: {
851
- code: "BOSS_CHAT_CLI_SPAWN_FAILED",
852
- message: error?.message || "无法启动 vendored boss-chat CLI。"
853
- },
854
- data_dir: runtimeLayout.data_dir,
855
- data_dir_source: runtimeLayout.data_dir_source
856
- }
857
- });
858
- });
859
- child.on("close", (code) => {
860
- const parsed = parseJsonOutput(stdout) || parseJsonOutput(stderr);
861
- if (parsed && typeof parsed === "object") {
862
- resolve({
863
- ok: Number(code) === 0 && String(parsed.status || "").toUpperCase() !== "FAILED",
864
- exitCode: Number.isInteger(code) ? code : 1,
865
- stdout,
866
- stderr,
867
- payload: withRuntimeDiagnostics(parsed, runtimeLayout)
868
- });
869
- return;
870
- }
871
- resolve({
872
- ok: Number(code) === 0,
873
- exitCode: Number.isInteger(code) ? code : 1,
874
- stdout,
875
- stderr,
876
- payload: withRuntimeDiagnostics(
877
- Number(code) === 0
878
- ? {
879
- status: "OK",
880
- message: normalizeText(stdout) || `${command} 执行成功。`
881
- }
882
- : {
883
- status: "FAILED",
884
- error: {
885
- code: "BOSS_CHAT_CLI_EXECUTION_FAILED",
886
- message: normalizeText(stderr || stdout) || `${command} 执行失败。`
887
- }
888
- },
889
- runtimeLayout
890
- )
891
- });
892
- });
893
- });
894
- }
895
-
896
- export function getBossChatHealthCheck(workspaceRoot, input = {}) {
897
- const cliDir = resolveBossChatCliDir(workspaceRoot);
898
- const cliPath = resolveBossChatCliPath(workspaceRoot);
899
- const configResolution = resolveBossChatScreenConfig(workspaceRoot);
900
- const runtimeLayout = resolveBossChatRuntimeLayout(workspaceRoot);
901
- const resolvedPort = parsePositiveInteger(input.port)
902
- || (configResolution.ok ? configResolution.config.debugPort : 9222);
903
- if (!cliDir || !cliPath) {
904
- return {
905
- status: "FAILED",
906
- error: {
907
- code: "BOSS_CHAT_CLI_MISSING",
908
- message: "未找到 vendored boss-chat CLI。"
909
- },
910
- data_dir: runtimeLayout.data_dir,
911
- data_dir_source: runtimeLayout.data_dir_source,
912
- legacy_workspace_dir: runtimeLayout.legacy_workspace_dir,
913
- migration_pending: runtimeLayout.migration_pending
914
- };
915
- }
916
- if (!configResolution.ok) {
917
- return {
918
- status: "FAILED",
919
- error: configResolution.error,
920
- config_path: configResolution.config_path,
921
- config_dir: configResolution.config_dir,
922
- cli_dir: cliDir,
923
- cli_path: cliPath,
924
- data_dir: runtimeLayout.data_dir,
925
- data_dir_source: runtimeLayout.data_dir_source,
926
- legacy_workspace_dir: runtimeLayout.legacy_workspace_dir,
927
- migration_pending: runtimeLayout.migration_pending
928
- };
929
- }
930
- return {
931
- status: "OK",
932
- server: "boss-chat",
933
- cli_dir: cliDir,
934
- cli_path: cliPath,
935
- config_path: configResolution.config_path,
936
- debug_port: resolvedPort,
937
- shared_llm_config: true,
938
- data_dir: runtimeLayout.data_dir,
939
- data_dir_source: runtimeLayout.data_dir_source,
940
- legacy_workspace_dir: runtimeLayout.legacy_workspace_dir,
941
- migration_source_dir: runtimeLayout.migration_source_dir,
942
- migration_pending: runtimeLayout.migration_pending
943
- };
944
- }
945
-
946
- export async function startBossChatRun({ workspaceRoot, input = {} }) {
947
- const missingFields = getMissingBossChatStartFields(input);
948
- if (missingFields.length > 0) {
949
- const prepared = await prepareBossChatRun({ workspaceRoot, input });
950
- if (prepared?.status === "FAILED") return prepared;
951
- const pendingQuestions = Array.isArray(prepared?.pending_questions)
952
- ? prepared.pending_questions.filter((item) => missingFields.includes(String(item?.field || "")))
953
- : [];
954
- const normalizedPendingQuestions = normalizePendingQuestions(pendingQuestions);
955
- const nextCallExample = buildNextCallExample(input, missingFields);
956
- const targetCountDiagnostics = buildTargetCountNeedInputDiagnostics(input, missingFields);
957
- return {
958
- ...prepared,
959
- status: "NEED_INPUT",
960
- required_fields: CHAT_REQUIRED_FIELDS.slice(),
961
- missing_fields: missingFields,
962
- pending_questions: normalizedPendingQuestions,
963
- ...targetCountDiagnostics,
964
- ...(nextCallExample ? { next_call_example: nextCallExample } : {}),
965
- message: prepared?.message
966
- || "已获取 Boss 聊天页岗位列表,请先补齐 job / start_from / target_count / criteria。"
967
- };
968
- }
969
- return (await spawnBossChatCli({ workspaceRoot, command: "start-run", input })).payload;
970
- }
971
-
972
- export async function prepareBossChatRun({ workspaceRoot, input = {} }) {
973
- let payload = null;
974
- for (let attempt = 1; attempt <= PREPARE_BOSS_CHAT_MAX_ATTEMPTS; attempt += 1) {
975
- payload = (await spawnBossChatCli({ workspaceRoot, command: "prepare-run", input })).payload;
976
- if (payload?.status !== "FAILED") break;
977
- if (attempt >= PREPARE_BOSS_CHAT_MAX_ATTEMPTS) break;
978
- await sleep(PREPARE_BOSS_CHAT_RETRY_DELAY_MS);
979
- }
980
-
981
- if (payload?.status !== "NEED_INPUT") return payload;
982
-
983
- const missingFields = getMissingBossChatStartFields(input);
984
- const pendingQuestions = Array.isArray(payload?.pending_questions)
985
- ? payload.pending_questions.filter((item) => (
986
- missingFields.length === 0 || missingFields.includes(String(item?.field || ""))
987
- ))
988
- : [];
989
- const nextCallExample = buildNextCallExample(input, missingFields);
990
- const targetCountDiagnostics = buildTargetCountNeedInputDiagnostics(input, missingFields);
991
- return {
992
- ...payload,
993
- required_fields: CHAT_REQUIRED_FIELDS.slice(),
994
- missing_fields: missingFields,
995
- pending_questions: normalizePendingQuestions(pendingQuestions),
996
- ...targetCountDiagnostics,
997
- ...(nextCallExample ? { next_call_example: nextCallExample } : {})
998
- };
999
- }
1000
-
1001
- export async function getBossChatRun({ workspaceRoot, input = {} }) {
1002
- return (await spawnBossChatCli({ workspaceRoot, command: "get-run", input })).payload;
1003
- }
1004
-
1005
- export async function pauseBossChatRun({ workspaceRoot, input = {} }) {
1006
- return (await spawnBossChatCli({ workspaceRoot, command: "pause-run", input })).payload;
1007
- }
1008
-
1009
- export async function resumeBossChatRun({ workspaceRoot, input = {} }) {
1010
- return (await spawnBossChatCli({ workspaceRoot, command: "resume-run", input })).payload;
1011
- }
1012
-
1013
- export async function cancelBossChatRun({ workspaceRoot, input = {} }) {
1014
- return (await spawnBossChatCli({ workspaceRoot, command: "cancel-run", input })).payload;
1015
- }
1016
-
1017
- export async function runBossChatSync({ workspaceRoot, input = {}, pollMs = DEFAULT_BOSS_CHAT_POLL_MS }) {
1018
- const accepted = await startBossChatRun({ workspaceRoot, input });
1019
- if (accepted?.status !== "ACCEPTED" || !normalizeText(accepted.run_id)) {
1020
- return accepted;
1021
- }
1022
- const runId = normalizeText(accepted.run_id);
1023
- while (true) {
1024
- await sleep(pollMs);
1025
- const statusPayload = await getBossChatRun({
1026
- workspaceRoot,
1027
- input: {
1028
- profile: input.profile,
1029
- runId
1030
- }
1031
- });
1032
- const runState = normalizeText(statusPayload?.run?.state).toLowerCase();
1033
- if (BOSS_CHAT_TERMINAL_STATES.has(runState)) {
1034
- return statusPayload;
1035
- }
1036
- }
1037
- }