@reconcrap/boss-recommend-mcp 1.3.32 → 1.3.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/screening-config.example.json +11 -11
- package/package.json +64 -64
- package/src/boss-chat.js +769 -769
- package/src/test-adapters-runtime.js +628 -628
- package/src/test-boss-chat.js +2716 -2227
- package/vendor/boss-chat-cli/src/app.js +1435 -1268
- package/vendor/boss-chat-cli/src/browser/chat-page.js +412 -242
- package/vendor/boss-chat-cli/src/cli.js +1580 -1580
- package/vendor/boss-chat-cli/src/services/chrome-client.js +103 -103
- package/vendor/boss-chat-cli/src/services/llm.js +1146 -810
- package/vendor/boss-chat-cli/src/services/llm.test.js +326 -0
- package/vendor/boss-chat-cli/src/services/profile-store.js +168 -168
- package/vendor/boss-chat-cli/src/services/report-store.js +317 -317
- package/vendor/boss-chat-cli/src/services/resume-capture.js +469 -469
- package/vendor/boss-chat-cli/src/services/resume-network.js +727 -727
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +6660 -6272
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +429 -31
package/src/boss-chat.js
CHANGED
|
@@ -1,769 +1,769 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import process from "node:process";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
|
|
7
|
-
import { getScreenConfigResolution, resolveSharedLlmTransportConfig } from "./adapters.js";
|
|
8
|
-
|
|
9
|
-
const currentFilePath = fileURLToPath(import.meta.url);
|
|
10
|
-
const packageRoot = path.resolve(path.dirname(currentFilePath), "..");
|
|
11
|
-
const VENDORED_BOSS_CHAT_DIR = path.join(packageRoot, "vendor", "boss-chat-cli");
|
|
12
|
-
const DEFAULT_BOSS_CHAT_POLL_MS = 1500;
|
|
13
|
-
const PREPARE_BOSS_CHAT_MAX_ATTEMPTS = 3;
|
|
14
|
-
const PREPARE_BOSS_CHAT_RETRY_DELAY_MS = 1200;
|
|
15
|
-
const BOSS_CHAT_TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
|
|
16
|
-
const CHAT_REQUIRED_FIELDS = ["job", "start_from", "target_count", "criteria"];
|
|
17
|
-
export const TARGET_COUNT_CANONICAL_ALL = "all";
|
|
18
|
-
export const TARGET_COUNT_ACCEPTED_EXAMPLES = [TARGET_COUNT_CANONICAL_ALL, -1, 20, "全部候选人"];
|
|
19
|
-
const TARGET_COUNT_WRAPPER_KEYS = ["target_count", "targetCount", "value", "count", "limit"];
|
|
20
|
-
const LLM_THINKING_LEVEL_FIELDS = [
|
|
21
|
-
"llmThinkingLevel",
|
|
22
|
-
"thinkingLevel",
|
|
23
|
-
"reasoningEffort",
|
|
24
|
-
"reasoning_effort"
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
function normalizeText(value) {
|
|
28
|
-
return String(value || "").replace(/\s+/g, " ").trim();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function pathExists(targetPath) {
|
|
32
|
-
try {
|
|
33
|
-
return fs.existsSync(targetPath);
|
|
34
|
-
} catch {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function parsePositiveInteger(value, fallback = null) {
|
|
40
|
-
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
41
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function parseBooleanValue(value) {
|
|
45
|
-
if (typeof value === "boolean") return value;
|
|
46
|
-
const normalized = normalizeText(value).toLowerCase();
|
|
47
|
-
if (!normalized) return null;
|
|
48
|
-
if (["1", "true", "yes", "y", "on", "是"].includes(normalized)) return true;
|
|
49
|
-
if (["0", "false", "no", "n", "off", "否"].includes(normalized)) return false;
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function resolveHumanRestEnabled(config = {}) {
|
|
54
|
-
if (!config || typeof config !== "object" || Array.isArray(config)) return false;
|
|
55
|
-
const candidates = [
|
|
56
|
-
config.humanRestEnabled,
|
|
57
|
-
config.human_rest_enabled,
|
|
58
|
-
config.humanLikeRestEnabled,
|
|
59
|
-
config.human_like_rest_enabled
|
|
60
|
-
];
|
|
61
|
-
for (const candidate of candidates) {
|
|
62
|
-
const parsed = parseBooleanValue(candidate);
|
|
63
|
-
if (typeof parsed === "boolean") return parsed;
|
|
64
|
-
}
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function isUnlimitedTargetCountToken(value) {
|
|
69
|
-
const token = normalizeText(value).toLowerCase();
|
|
70
|
-
if (!token) return false;
|
|
71
|
-
const compact = token.replace(/\s+/g, "");
|
|
72
|
-
const withoutAnnotation = compact.replace(/[((【[].*?[))】\]]/gu, "");
|
|
73
|
-
const knownTokens = new Set([
|
|
74
|
-
"all",
|
|
75
|
-
"unlimited",
|
|
76
|
-
"infinity",
|
|
77
|
-
"inf",
|
|
78
|
-
"max",
|
|
79
|
-
"full",
|
|
80
|
-
"allcandidates",
|
|
81
|
-
"全部",
|
|
82
|
-
"全量",
|
|
83
|
-
"不限",
|
|
84
|
-
"扫到底",
|
|
85
|
-
"全部候选人",
|
|
86
|
-
"所有候选人",
|
|
87
|
-
"全部人选",
|
|
88
|
-
"所有人选",
|
|
89
|
-
"直到完成所有人选"
|
|
90
|
-
]);
|
|
91
|
-
if (knownTokens.has(token) || knownTokens.has(compact) || knownTokens.has(withoutAnnotation)) return true;
|
|
92
|
-
if (/^(?:all|unlimited|infinity|inf|max|full)(?:candidate|candidates)?$/i.test(compact)) return true;
|
|
93
|
-
if (/^(?:all|unlimited|infinity|inf|max|full)(?:候选人|人选|牛人|人才|人员)?$/iu.test(withoutAnnotation)) return true;
|
|
94
|
-
if (/^(?:全部|所有|全量|不限)(?:候选人|人选|牛人|人才|人员)?$/u.test(compact)) return true;
|
|
95
|
-
if (!/\d/.test(compact) && /(?:扫到底|全部候选人|所有候选人|全部人选|所有人选)/u.test(compact)) return true;
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function getWrappedTargetCountValue(value) {
|
|
100
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
101
|
-
for (const key of TARGET_COUNT_WRAPPER_KEYS) {
|
|
102
|
-
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
103
|
-
return value[key];
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return value;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function getBossChatTargetCountValue(input = {}) {
|
|
110
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) return undefined;
|
|
111
|
-
if (Object.prototype.hasOwnProperty.call(input, "target_count") && input.target_count !== undefined && input.target_count !== null) {
|
|
112
|
-
return input.target_count;
|
|
113
|
-
}
|
|
114
|
-
if (Object.prototype.hasOwnProperty.call(input, "targetCount") && input.targetCount !== undefined && input.targetCount !== null) {
|
|
115
|
-
return input.targetCount;
|
|
116
|
-
}
|
|
117
|
-
if (Object.prototype.hasOwnProperty.call(input, "target_count")) return input.target_count;
|
|
118
|
-
if (Object.prototype.hasOwnProperty.call(input, "targetCount")) return input.targetCount;
|
|
119
|
-
return undefined;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function cloneForDiagnostics(value) {
|
|
123
|
-
if (value === undefined) return undefined;
|
|
124
|
-
if (value === null || ["string", "number", "boolean"].includes(typeof value)) return value;
|
|
125
|
-
try {
|
|
126
|
-
return JSON.parse(JSON.stringify(value));
|
|
127
|
-
} catch {
|
|
128
|
-
return String(value);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function buildTargetCountCompatibilityHints({
|
|
133
|
-
argumentName = "target_count",
|
|
134
|
-
recommendedArgumentPatch = { target_count: TARGET_COUNT_CANONICAL_ALL },
|
|
135
|
-
includeOptions = true
|
|
136
|
-
} = {}) {
|
|
137
|
-
const normalizedArgumentName = normalizeText(argumentName) || "target_count";
|
|
138
|
-
const clonedRecommendedPatch = cloneForDiagnostics(recommendedArgumentPatch)
|
|
139
|
-
|| { target_count: TARGET_COUNT_CANONICAL_ALL };
|
|
140
|
-
const literal = `${normalizedArgumentName}="${TARGET_COUNT_CANONICAL_ALL}"`;
|
|
141
|
-
const base = {
|
|
142
|
-
argument_name: normalizedArgumentName,
|
|
143
|
-
answer_format: `${normalizedArgumentName} = 正整数 | "${TARGET_COUNT_CANONICAL_ALL}"`,
|
|
144
|
-
canonical_unlimited_value: TARGET_COUNT_CANONICAL_ALL,
|
|
145
|
-
recommended_value: TARGET_COUNT_CANONICAL_ALL,
|
|
146
|
-
recommended_argument_patch: clonedRecommendedPatch,
|
|
147
|
-
accepted_examples: TARGET_COUNT_ACCEPTED_EXAMPLES.slice()
|
|
148
|
-
};
|
|
149
|
-
if (!includeOptions) return base;
|
|
150
|
-
return {
|
|
151
|
-
...base,
|
|
152
|
-
options: [
|
|
153
|
-
{
|
|
154
|
-
label: `扫到底(必须传 ${literal},推荐)`,
|
|
155
|
-
value: TARGET_COUNT_CANONICAL_ALL,
|
|
156
|
-
canonical_value: TARGET_COUNT_CANONICAL_ALL,
|
|
157
|
-
argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
label: `不限(等价于 ${literal})`,
|
|
161
|
-
value: "unlimited",
|
|
162
|
-
canonical_value: TARGET_COUNT_CANONICAL_ALL,
|
|
163
|
-
argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
label: `全部候选人(等价于 ${literal})`,
|
|
167
|
-
value: "全部候选人",
|
|
168
|
-
canonical_value: TARGET_COUNT_CANONICAL_ALL,
|
|
169
|
-
argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
label: `所有候选人(等价于 ${literal})`,
|
|
173
|
-
value: "所有候选人",
|
|
174
|
-
canonical_value: TARGET_COUNT_CANONICAL_ALL,
|
|
175
|
-
argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
|
|
176
|
-
}
|
|
177
|
-
]
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function normalizeTargetCountInput(value) {
|
|
182
|
-
if (value === undefined || value === null) {
|
|
183
|
-
return {
|
|
184
|
-
provided: false,
|
|
185
|
-
targetCount: null,
|
|
186
|
-
cliArg: null,
|
|
187
|
-
publicValue: null,
|
|
188
|
-
rawValue: value,
|
|
189
|
-
parseError: null
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
const unwrapped = getWrappedTargetCountValue(value);
|
|
193
|
-
if (unwrapped !== value) {
|
|
194
|
-
return normalizeTargetCountInput(unwrapped);
|
|
195
|
-
}
|
|
196
|
-
const raw = normalizeText(unwrapped);
|
|
197
|
-
if (!raw) {
|
|
198
|
-
return {
|
|
199
|
-
provided: false,
|
|
200
|
-
targetCount: null,
|
|
201
|
-
cliArg: null,
|
|
202
|
-
publicValue: null,
|
|
203
|
-
rawValue: value,
|
|
204
|
-
parseError: null
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
if (isUnlimitedTargetCountToken(raw)) {
|
|
208
|
-
return {
|
|
209
|
-
provided: true,
|
|
210
|
-
targetCount: null,
|
|
211
|
-
cliArg: "-1",
|
|
212
|
-
publicValue: "all",
|
|
213
|
-
rawValue: cloneForDiagnostics(value),
|
|
214
|
-
parseError: null
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
const parsed = Number.parseInt(String(raw), 10);
|
|
218
|
-
if (Number.isFinite(parsed) && parsed === -1) {
|
|
219
|
-
return {
|
|
220
|
-
provided: true,
|
|
221
|
-
targetCount: null,
|
|
222
|
-
cliArg: "-1",
|
|
223
|
-
publicValue: "all",
|
|
224
|
-
rawValue: cloneForDiagnostics(value),
|
|
225
|
-
parseError: null
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
if (Number.isFinite(parsed) && parsed > 0) {
|
|
229
|
-
return {
|
|
230
|
-
provided: true,
|
|
231
|
-
targetCount: parsed,
|
|
232
|
-
cliArg: String(parsed),
|
|
233
|
-
publicValue: parsed,
|
|
234
|
-
rawValue: cloneForDiagnostics(value),
|
|
235
|
-
parseError: null
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
return {
|
|
239
|
-
provided: false,
|
|
240
|
-
targetCount: null,
|
|
241
|
-
cliArg: null,
|
|
242
|
-
publicValue: null,
|
|
243
|
-
rawValue: cloneForDiagnostics(value),
|
|
244
|
-
parseError: "target_count must be a positive integer, -1, or one of: all, unlimited, 全部, 不限, 扫到底, 全量, 全部候选人, 所有候选人"
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function parseJsonOutput(text) {
|
|
249
|
-
const trimmed = String(text || "").trim();
|
|
250
|
-
if (!trimmed) return null;
|
|
251
|
-
try {
|
|
252
|
-
return JSON.parse(trimmed);
|
|
253
|
-
} catch {}
|
|
254
|
-
const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
255
|
-
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
256
|
-
try {
|
|
257
|
-
return JSON.parse(lines[index]);
|
|
258
|
-
} catch {
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
return null;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function sleep(ms) {
|
|
266
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function resolveBossChatCliDir(workspaceRoot) {
|
|
270
|
-
const localDir = path.join(path.resolve(String(workspaceRoot || process.cwd())), "boss-chat-cli");
|
|
271
|
-
if (pathExists(localDir)) return localDir;
|
|
272
|
-
return pathExists(VENDORED_BOSS_CHAT_DIR) ? VENDORED_BOSS_CHAT_DIR : null;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function resolveBossChatCliPath(workspaceRoot) {
|
|
276
|
-
const cliDir = resolveBossChatCliDir(workspaceRoot);
|
|
277
|
-
if (!cliDir) return null;
|
|
278
|
-
const cliPath = path.join(cliDir, "src", "cli.js");
|
|
279
|
-
return pathExists(cliPath) ? cliPath : null;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function validateRecommendScreenConfig(config) {
|
|
283
|
-
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
284
|
-
return {
|
|
285
|
-
ok: false,
|
|
286
|
-
message: "screening-config.json 缺失或格式无效。请填写 baseUrl、apiKey、model。"
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
const baseUrl = normalizeText(config.baseUrl).replace(/\/+$/, "");
|
|
290
|
-
const apiKey = normalizeText(config.apiKey);
|
|
291
|
-
const model = normalizeText(config.model);
|
|
292
|
-
const missing = [];
|
|
293
|
-
if (!baseUrl) missing.push("baseUrl");
|
|
294
|
-
if (!apiKey) missing.push("apiKey");
|
|
295
|
-
if (!model) missing.push("model");
|
|
296
|
-
if (missing.length > 0) {
|
|
297
|
-
return {
|
|
298
|
-
ok: false,
|
|
299
|
-
message: `screening-config.json 缺少必填字段:${missing.join(", ")}。`
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
if (/^replace-with/i.test(apiKey)) {
|
|
303
|
-
return {
|
|
304
|
-
ok: false,
|
|
305
|
-
message: "screening-config.json 的 apiKey 仍是模板占位符,请填写真实 API Key。"
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
return { ok: true };
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function resolveLlmThinkingLevel(config = {}) {
|
|
312
|
-
if (!config || typeof config !== "object") return "";
|
|
313
|
-
for (const field of LLM_THINKING_LEVEL_FIELDS) {
|
|
314
|
-
const value = normalizeText(config[field]);
|
|
315
|
-
if (value) return value;
|
|
316
|
-
}
|
|
317
|
-
return "";
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function resolveBossChatScreenConfig(workspaceRoot) {
|
|
321
|
-
const resolution = getScreenConfigResolution(workspaceRoot);
|
|
322
|
-
const configPath = resolution.resolved_path || resolution.writable_path || resolution.legacy_path || null;
|
|
323
|
-
if (!configPath || !pathExists(configPath)) {
|
|
324
|
-
return {
|
|
325
|
-
ok: false,
|
|
326
|
-
error: {
|
|
327
|
-
code: "SCREEN_CONFIG_ERROR",
|
|
328
|
-
message: `screening-config.json 不存在。请先完成 recommend 配置。${configPath ? ` (path: ${configPath})` : ""}`
|
|
329
|
-
},
|
|
330
|
-
config_path: configPath,
|
|
331
|
-
config_dir: configPath ? path.dirname(configPath) : null
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
let parsed = null;
|
|
335
|
-
try {
|
|
336
|
-
parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
337
|
-
} catch (error) {
|
|
338
|
-
return {
|
|
339
|
-
ok: false,
|
|
340
|
-
error: {
|
|
341
|
-
code: "SCREEN_CONFIG_ERROR",
|
|
342
|
-
message: `screening-config.json 解析失败:${error.message || "unknown error"} (path: ${configPath})`
|
|
343
|
-
},
|
|
344
|
-
config_path: configPath,
|
|
345
|
-
config_dir: path.dirname(configPath)
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
const validation = validateRecommendScreenConfig(parsed);
|
|
349
|
-
if (!validation.ok) {
|
|
350
|
-
return {
|
|
351
|
-
ok: false,
|
|
352
|
-
error: {
|
|
353
|
-
code: "SCREEN_CONFIG_ERROR",
|
|
354
|
-
message: `${validation.message} (path: ${configPath})`
|
|
355
|
-
},
|
|
356
|
-
config_path: configPath,
|
|
357
|
-
config_dir: path.dirname(configPath)
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
return {
|
|
361
|
-
ok: true,
|
|
362
|
-
config: {
|
|
363
|
-
baseUrl: normalizeText(parsed.baseUrl).replace(/\/+$/, ""),
|
|
364
|
-
apiKey: normalizeText(parsed.apiKey),
|
|
365
|
-
model: normalizeText(parsed.model),
|
|
366
|
-
llmThinkingLevel: resolveLlmThinkingLevel(parsed),
|
|
367
|
-
...resolveSharedLlmTransportConfig(parsed),
|
|
368
|
-
debugPort: parsePositiveInteger(parsed.debugPort, 9222),
|
|
369
|
-
humanRestEnabled: resolveHumanRestEnabled(parsed)
|
|
370
|
-
},
|
|
371
|
-
config_path: configPath,
|
|
372
|
-
config_dir: path.dirname(configPath)
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function normalizeBossChatStartInput(input = {}) {
|
|
377
|
-
const profile = normalizeText(input.profile) || "default";
|
|
378
|
-
const job = normalizeText(input.job);
|
|
379
|
-
const startFromRaw = normalizeText(input.startFrom || input.start_from).toLowerCase();
|
|
380
|
-
const startFrom = startFromRaw === "all" ? "all" : startFromRaw === "unread" ? "unread" : "";
|
|
381
|
-
const criteria = normalizeText(input.criteria);
|
|
382
|
-
const parsedTarget = normalizeTargetCountInput(getBossChatTargetCountValue(input));
|
|
383
|
-
const port = parsePositiveInteger(input.port);
|
|
384
|
-
return {
|
|
385
|
-
profile,
|
|
386
|
-
job,
|
|
387
|
-
startFrom,
|
|
388
|
-
criteria,
|
|
389
|
-
targetCount: parsedTarget.targetCount,
|
|
390
|
-
targetCountArg: parsedTarget.cliArg,
|
|
391
|
-
targetCountProvided: parsedTarget.provided,
|
|
392
|
-
targetCountPublicValue: parsedTarget.publicValue,
|
|
393
|
-
targetCountRawValue: parsedTarget.rawValue,
|
|
394
|
-
targetCountParseError: parsedTarget.parseError,
|
|
395
|
-
port,
|
|
396
|
-
dryRun: input.dryRun === true || input.dry_run === true,
|
|
397
|
-
noState: input.noState === true || input.no_state === true,
|
|
398
|
-
safePacing: typeof input.safePacing === "boolean" ? input.safePacing : (
|
|
399
|
-
typeof input.safe_pacing === "boolean" ? input.safe_pacing : undefined
|
|
400
|
-
),
|
|
401
|
-
batchRestEnabled: typeof input.batchRestEnabled === "boolean" ? input.batchRestEnabled : (
|
|
402
|
-
typeof input.batch_rest_enabled === "boolean" ? input.batch_rest_enabled : undefined
|
|
403
|
-
)
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function normalizeBossChatRunId(input = {}) {
|
|
408
|
-
return normalizeText(input.runId || input.run_id);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function getMissingBossChatStartFields(input = {}) {
|
|
412
|
-
const normalized = normalizeBossChatStartInput(input);
|
|
413
|
-
const missing = [];
|
|
414
|
-
if (!normalized.job) missing.push("job");
|
|
415
|
-
if (!normalized.startFrom) missing.push("start_from");
|
|
416
|
-
if (!normalized.targetCountProvided) missing.push("target_count");
|
|
417
|
-
if (!normalized.criteria) missing.push("criteria");
|
|
418
|
-
return missing;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function buildTargetCountQuestionHint(item = {}) {
|
|
422
|
-
const next = { ...item };
|
|
423
|
-
const hints = buildTargetCountCompatibilityHints({
|
|
424
|
-
argumentName: "target_count",
|
|
425
|
-
recommendedArgumentPatch: { target_count: TARGET_COUNT_CANONICAL_ALL }
|
|
426
|
-
});
|
|
427
|
-
return {
|
|
428
|
-
...next,
|
|
429
|
-
...hints,
|
|
430
|
-
question: `请输入 target_count:正整数,或直接填写 "${TARGET_COUNT_CANONICAL_ALL}"(扫到底)。`,
|
|
431
|
-
examples: TARGET_COUNT_ACCEPTED_EXAMPLES.slice()
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function normalizePendingQuestions(pendingQuestions = []) {
|
|
436
|
-
return pendingQuestions.map((item) => {
|
|
437
|
-
if (String(item?.field || "") !== "target_count") return item;
|
|
438
|
-
return buildTargetCountQuestionHint(item);
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function buildNextCallExample(input = {}, missingFields = []) {
|
|
443
|
-
if (!Array.isArray(missingFields) || missingFields.length === 0) return null;
|
|
444
|
-
const normalized = normalizeBossChatStartInput(input);
|
|
445
|
-
const sample = {};
|
|
446
|
-
if (normalized.job) sample.job = normalized.job;
|
|
447
|
-
if (normalized.startFrom) sample.start_from = normalized.startFrom;
|
|
448
|
-
if (normalized.criteria) sample.criteria = normalized.criteria;
|
|
449
|
-
if (normalized.targetCountProvided) {
|
|
450
|
-
sample.target_count = normalized.targetCountPublicValue || (normalized.targetCountArg === "-1" ? "all" : normalized.targetCount);
|
|
451
|
-
} else if (missingFields.includes("target_count")) {
|
|
452
|
-
sample.target_count = "all";
|
|
453
|
-
}
|
|
454
|
-
return Object.keys(sample).length > 0 ? sample : null;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function buildTargetCountNeedInputDiagnostics(input = {}, missingFields = []) {
|
|
458
|
-
if (!Array.isArray(missingFields) || !missingFields.includes("target_count")) return {};
|
|
459
|
-
const normalized = normalizeBossChatStartInput(input);
|
|
460
|
-
const hints = buildTargetCountCompatibilityHints({
|
|
461
|
-
argumentName: "target_count",
|
|
462
|
-
recommendedArgumentPatch: { target_count: TARGET_COUNT_CANONICAL_ALL },
|
|
463
|
-
includeOptions: false
|
|
464
|
-
});
|
|
465
|
-
return {
|
|
466
|
-
...hints,
|
|
467
|
-
...(normalized.targetCountRawValue !== undefined ? { received_target_count: normalized.targetCountRawValue } : {}),
|
|
468
|
-
...(normalized.targetCountParseError ? { target_count_parse_error: normalized.targetCountParseError } : {})
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function buildBossChatCliArgs(command, input, resolvedConfig) {
|
|
473
|
-
const args = [command, "--json"];
|
|
474
|
-
if (command === "prepare-run") {
|
|
475
|
-
const normalized = normalizeBossChatStartInput(input);
|
|
476
|
-
args.push("--profile", normalized.profile);
|
|
477
|
-
if (normalized.job) args.push("--job", normalized.job);
|
|
478
|
-
if (normalized.startFrom) args.push("--start-from", normalized.startFrom);
|
|
479
|
-
if (normalized.criteria) args.push("--criteria", normalized.criteria);
|
|
480
|
-
if (normalized.targetCountArg) args.push("--targetCount", normalized.targetCountArg);
|
|
481
|
-
args.push("--port", String(normalized.port || resolvedConfig.debugPort || 9222));
|
|
482
|
-
args.push("--baseurl", resolvedConfig.baseUrl);
|
|
483
|
-
args.push("--apikey", resolvedConfig.apiKey);
|
|
484
|
-
args.push("--model", resolvedConfig.model);
|
|
485
|
-
if (resolvedConfig.llmThinkingLevel) {
|
|
486
|
-
args.push("--thinking-level", resolvedConfig.llmThinkingLevel);
|
|
487
|
-
}
|
|
488
|
-
if (resolvedConfig.llmTimeoutMs) {
|
|
489
|
-
args.push("--llm-timeout-ms", String(resolvedConfig.llmTimeoutMs));
|
|
490
|
-
}
|
|
491
|
-
if (resolvedConfig.llmMaxRetries) {
|
|
492
|
-
args.push("--llm-max-retries", String(resolvedConfig.llmMaxRetries));
|
|
493
|
-
}
|
|
494
|
-
return args;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
if (command === "start-run") {
|
|
498
|
-
const normalized = normalizeBossChatStartInput(input);
|
|
499
|
-
args.push("--profile", normalized.profile);
|
|
500
|
-
if (normalized.dryRun) args.push("--dry-run");
|
|
501
|
-
if (normalized.noState) args.push("--no-state");
|
|
502
|
-
args.push("--job", normalized.job);
|
|
503
|
-
args.push("--start-from", normalized.startFrom);
|
|
504
|
-
args.push("--criteria", normalized.criteria);
|
|
505
|
-
if (normalized.targetCountArg) {
|
|
506
|
-
args.push("--targetCount", normalized.targetCountArg);
|
|
507
|
-
}
|
|
508
|
-
args.push("--baseurl", resolvedConfig.baseUrl);
|
|
509
|
-
args.push("--apikey", resolvedConfig.apiKey);
|
|
510
|
-
args.push("--model", resolvedConfig.model);
|
|
511
|
-
if (resolvedConfig.llmThinkingLevel) {
|
|
512
|
-
args.push("--thinking-level", resolvedConfig.llmThinkingLevel);
|
|
513
|
-
}
|
|
514
|
-
if (resolvedConfig.llmTimeoutMs) {
|
|
515
|
-
args.push("--llm-timeout-ms", String(resolvedConfig.llmTimeoutMs));
|
|
516
|
-
}
|
|
517
|
-
if (resolvedConfig.llmMaxRetries) {
|
|
518
|
-
args.push("--llm-max-retries", String(resolvedConfig.llmMaxRetries));
|
|
519
|
-
}
|
|
520
|
-
args.push("--port", String(normalized.port || resolvedConfig.debugPort || 9222));
|
|
521
|
-
if (typeof normalized.safePacing === "boolean") {
|
|
522
|
-
args.push("--safe-pacing", String(normalized.safePacing));
|
|
523
|
-
}
|
|
524
|
-
if (typeof normalized.batchRestEnabled === "boolean") {
|
|
525
|
-
args.push("--batch-rest", String(normalized.batchRestEnabled));
|
|
526
|
-
} else if (typeof resolvedConfig?.humanRestEnabled === "boolean") {
|
|
527
|
-
args.push("--batch-rest", String(resolvedConfig.humanRestEnabled));
|
|
528
|
-
}
|
|
529
|
-
return args;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const runId = normalizeBossChatRunId(input);
|
|
533
|
-
args.push("--profile", normalizeText(input.profile) || "default");
|
|
534
|
-
args.push("--run-id", runId);
|
|
535
|
-
return args;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
|
|
539
|
-
const cliPath = resolveBossChatCliPath(workspaceRoot);
|
|
540
|
-
if (!cliPath) {
|
|
541
|
-
return {
|
|
542
|
-
ok: false,
|
|
543
|
-
exitCode: -1,
|
|
544
|
-
stdout: "",
|
|
545
|
-
stderr: "",
|
|
546
|
-
payload: {
|
|
547
|
-
status: "FAILED",
|
|
548
|
-
error: {
|
|
549
|
-
code: "BOSS_CHAT_CLI_MISSING",
|
|
550
|
-
message: "未找到 vendored boss-chat CLI。"
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
let configResolution = null;
|
|
557
|
-
if (command === "start-run" || command === "prepare-run") {
|
|
558
|
-
configResolution = resolveBossChatScreenConfig(workspaceRoot);
|
|
559
|
-
if (!configResolution.ok) {
|
|
560
|
-
return {
|
|
561
|
-
ok: false,
|
|
562
|
-
exitCode: 1,
|
|
563
|
-
stdout: "",
|
|
564
|
-
stderr: "",
|
|
565
|
-
payload: {
|
|
566
|
-
status: "FAILED",
|
|
567
|
-
error: configResolution.error,
|
|
568
|
-
config_path: configResolution.config_path,
|
|
569
|
-
config_dir: configResolution.config_dir
|
|
570
|
-
}
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
const args = [cliPath, ...buildBossChatCliArgs(command, input, configResolution?.config || {})];
|
|
576
|
-
const cwd = path.resolve(String(workspaceRoot || process.cwd()));
|
|
577
|
-
return new Promise((resolve) => {
|
|
578
|
-
const child = spawn(process.execPath, args, {
|
|
579
|
-
cwd,
|
|
580
|
-
env: process.env,
|
|
581
|
-
windowsHide: true,
|
|
582
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
let stdout = "";
|
|
586
|
-
let stderr = "";
|
|
587
|
-
child.stdout.on("data", (chunk) => {
|
|
588
|
-
stdout += String(chunk);
|
|
589
|
-
});
|
|
590
|
-
child.stderr.on("data", (chunk) => {
|
|
591
|
-
stderr += String(chunk);
|
|
592
|
-
});
|
|
593
|
-
child.on("error", (error) => {
|
|
594
|
-
resolve({
|
|
595
|
-
ok: false,
|
|
596
|
-
exitCode: -1,
|
|
597
|
-
stdout,
|
|
598
|
-
stderr,
|
|
599
|
-
payload: {
|
|
600
|
-
status: "FAILED",
|
|
601
|
-
error: {
|
|
602
|
-
code: "BOSS_CHAT_CLI_SPAWN_FAILED",
|
|
603
|
-
message: error?.message || "无法启动 vendored boss-chat CLI。"
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
});
|
|
607
|
-
});
|
|
608
|
-
child.on("close", (code) => {
|
|
609
|
-
const parsed = parseJsonOutput(stdout) || parseJsonOutput(stderr);
|
|
610
|
-
if (parsed && typeof parsed === "object") {
|
|
611
|
-
resolve({
|
|
612
|
-
ok: Number(code) === 0 && String(parsed.status || "").toUpperCase() !== "FAILED",
|
|
613
|
-
exitCode: Number.isInteger(code) ? code : 1,
|
|
614
|
-
stdout,
|
|
615
|
-
stderr,
|
|
616
|
-
payload: parsed
|
|
617
|
-
});
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
resolve({
|
|
621
|
-
ok: Number(code) === 0,
|
|
622
|
-
exitCode: Number.isInteger(code) ? code : 1,
|
|
623
|
-
stdout,
|
|
624
|
-
stderr,
|
|
625
|
-
payload: Number(code) === 0
|
|
626
|
-
? {
|
|
627
|
-
status: "OK",
|
|
628
|
-
message: normalizeText(stdout) || `${command} 执行成功。`
|
|
629
|
-
}
|
|
630
|
-
: {
|
|
631
|
-
status: "FAILED",
|
|
632
|
-
error: {
|
|
633
|
-
code: "BOSS_CHAT_CLI_EXECUTION_FAILED",
|
|
634
|
-
message: normalizeText(stderr || stdout) || `${command} 执行失败。`
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
});
|
|
638
|
-
});
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
export function getBossChatHealthCheck(workspaceRoot, input = {}) {
|
|
643
|
-
const cliDir = resolveBossChatCliDir(workspaceRoot);
|
|
644
|
-
const cliPath = resolveBossChatCliPath(workspaceRoot);
|
|
645
|
-
const configResolution = resolveBossChatScreenConfig(workspaceRoot);
|
|
646
|
-
const resolvedPort = parsePositiveInteger(input.port)
|
|
647
|
-
|| (configResolution.ok ? configResolution.config.debugPort : 9222);
|
|
648
|
-
if (!cliDir || !cliPath) {
|
|
649
|
-
return {
|
|
650
|
-
status: "FAILED",
|
|
651
|
-
error: {
|
|
652
|
-
code: "BOSS_CHAT_CLI_MISSING",
|
|
653
|
-
message: "未找到 vendored boss-chat CLI。"
|
|
654
|
-
}
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
if (!configResolution.ok) {
|
|
658
|
-
return {
|
|
659
|
-
status: "FAILED",
|
|
660
|
-
error: configResolution.error,
|
|
661
|
-
config_path: configResolution.config_path,
|
|
662
|
-
config_dir: configResolution.config_dir,
|
|
663
|
-
cli_dir: cliDir,
|
|
664
|
-
cli_path: cliPath
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
return {
|
|
668
|
-
status: "OK",
|
|
669
|
-
server: "boss-chat",
|
|
670
|
-
cli_dir: cliDir,
|
|
671
|
-
cli_path: cliPath,
|
|
672
|
-
config_path: configResolution.config_path,
|
|
673
|
-
debug_port: resolvedPort,
|
|
674
|
-
shared_llm_config: true
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
export async function startBossChatRun({ workspaceRoot, input = {} }) {
|
|
679
|
-
const missingFields = getMissingBossChatStartFields(input);
|
|
680
|
-
if (missingFields.length > 0) {
|
|
681
|
-
const prepared = await prepareBossChatRun({ workspaceRoot, input });
|
|
682
|
-
if (prepared?.status === "FAILED") return prepared;
|
|
683
|
-
const pendingQuestions = Array.isArray(prepared?.pending_questions)
|
|
684
|
-
? prepared.pending_questions.filter((item) => missingFields.includes(String(item?.field || "")))
|
|
685
|
-
: [];
|
|
686
|
-
const normalizedPendingQuestions = normalizePendingQuestions(pendingQuestions);
|
|
687
|
-
const nextCallExample = buildNextCallExample(input, missingFields);
|
|
688
|
-
const targetCountDiagnostics = buildTargetCountNeedInputDiagnostics(input, missingFields);
|
|
689
|
-
return {
|
|
690
|
-
...prepared,
|
|
691
|
-
status: "NEED_INPUT",
|
|
692
|
-
required_fields: CHAT_REQUIRED_FIELDS.slice(),
|
|
693
|
-
missing_fields: missingFields,
|
|
694
|
-
pending_questions: normalizedPendingQuestions,
|
|
695
|
-
...targetCountDiagnostics,
|
|
696
|
-
...(nextCallExample ? { next_call_example: nextCallExample } : {}),
|
|
697
|
-
message: prepared?.message
|
|
698
|
-
|| "已获取 Boss 聊天页岗位列表,请先补齐 job / start_from / target_count / criteria。"
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
return (await spawnBossChatCli({ workspaceRoot, command: "start-run", input })).payload;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
export async function prepareBossChatRun({ workspaceRoot, input = {} }) {
|
|
705
|
-
let payload = null;
|
|
706
|
-
for (let attempt = 1; attempt <= PREPARE_BOSS_CHAT_MAX_ATTEMPTS; attempt += 1) {
|
|
707
|
-
payload = (await spawnBossChatCli({ workspaceRoot, command: "prepare-run", input })).payload;
|
|
708
|
-
if (payload?.status !== "FAILED") break;
|
|
709
|
-
if (attempt >= PREPARE_BOSS_CHAT_MAX_ATTEMPTS) break;
|
|
710
|
-
await sleep(PREPARE_BOSS_CHAT_RETRY_DELAY_MS);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
if (payload?.status !== "NEED_INPUT") return payload;
|
|
714
|
-
|
|
715
|
-
const missingFields = getMissingBossChatStartFields(input);
|
|
716
|
-
const pendingQuestions = Array.isArray(payload?.pending_questions)
|
|
717
|
-
? payload.pending_questions.filter((item) => (
|
|
718
|
-
missingFields.length === 0 || missingFields.includes(String(item?.field || ""))
|
|
719
|
-
))
|
|
720
|
-
: [];
|
|
721
|
-
const nextCallExample = buildNextCallExample(input, missingFields);
|
|
722
|
-
const targetCountDiagnostics = buildTargetCountNeedInputDiagnostics(input, missingFields);
|
|
723
|
-
return {
|
|
724
|
-
...payload,
|
|
725
|
-
required_fields: CHAT_REQUIRED_FIELDS.slice(),
|
|
726
|
-
missing_fields: missingFields,
|
|
727
|
-
pending_questions: normalizePendingQuestions(pendingQuestions),
|
|
728
|
-
...targetCountDiagnostics,
|
|
729
|
-
...(nextCallExample ? { next_call_example: nextCallExample } : {})
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
export async function getBossChatRun({ workspaceRoot, input = {} }) {
|
|
734
|
-
return (await spawnBossChatCli({ workspaceRoot, command: "get-run", input })).payload;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
export async function pauseBossChatRun({ workspaceRoot, input = {} }) {
|
|
738
|
-
return (await spawnBossChatCli({ workspaceRoot, command: "pause-run", input })).payload;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
export async function resumeBossChatRun({ workspaceRoot, input = {} }) {
|
|
742
|
-
return (await spawnBossChatCli({ workspaceRoot, command: "resume-run", input })).payload;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
export async function cancelBossChatRun({ workspaceRoot, input = {} }) {
|
|
746
|
-
return (await spawnBossChatCli({ workspaceRoot, command: "cancel-run", input })).payload;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
export async function runBossChatSync({ workspaceRoot, input = {}, pollMs = DEFAULT_BOSS_CHAT_POLL_MS }) {
|
|
750
|
-
const accepted = await startBossChatRun({ workspaceRoot, input });
|
|
751
|
-
if (accepted?.status !== "ACCEPTED" || !normalizeText(accepted.run_id)) {
|
|
752
|
-
return accepted;
|
|
753
|
-
}
|
|
754
|
-
const runId = normalizeText(accepted.run_id);
|
|
755
|
-
while (true) {
|
|
756
|
-
await sleep(pollMs);
|
|
757
|
-
const statusPayload = await getBossChatRun({
|
|
758
|
-
workspaceRoot,
|
|
759
|
-
input: {
|
|
760
|
-
profile: input.profile,
|
|
761
|
-
runId
|
|
762
|
-
}
|
|
763
|
-
});
|
|
764
|
-
const runState = normalizeText(statusPayload?.run?.state).toLowerCase();
|
|
765
|
-
if (BOSS_CHAT_TERMINAL_STATES.has(runState)) {
|
|
766
|
-
return statusPayload;
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
}
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import { getScreenConfigResolution, resolveSharedLlmTransportConfig } from "./adapters.js";
|
|
8
|
+
|
|
9
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
10
|
+
const packageRoot = path.resolve(path.dirname(currentFilePath), "..");
|
|
11
|
+
const VENDORED_BOSS_CHAT_DIR = path.join(packageRoot, "vendor", "boss-chat-cli");
|
|
12
|
+
const DEFAULT_BOSS_CHAT_POLL_MS = 1500;
|
|
13
|
+
const PREPARE_BOSS_CHAT_MAX_ATTEMPTS = 3;
|
|
14
|
+
const PREPARE_BOSS_CHAT_RETRY_DELAY_MS = 1200;
|
|
15
|
+
const BOSS_CHAT_TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
|
|
16
|
+
const CHAT_REQUIRED_FIELDS = ["job", "start_from", "target_count", "criteria"];
|
|
17
|
+
export const TARGET_COUNT_CANONICAL_ALL = "all";
|
|
18
|
+
export const TARGET_COUNT_ACCEPTED_EXAMPLES = [TARGET_COUNT_CANONICAL_ALL, -1, 20, "全部候选人"];
|
|
19
|
+
const TARGET_COUNT_WRAPPER_KEYS = ["target_count", "targetCount", "value", "count", "limit"];
|
|
20
|
+
const LLM_THINKING_LEVEL_FIELDS = [
|
|
21
|
+
"llmThinkingLevel",
|
|
22
|
+
"thinkingLevel",
|
|
23
|
+
"reasoningEffort",
|
|
24
|
+
"reasoning_effort"
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function normalizeText(value) {
|
|
28
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function pathExists(targetPath) {
|
|
32
|
+
try {
|
|
33
|
+
return fs.existsSync(targetPath);
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parsePositiveInteger(value, fallback = null) {
|
|
40
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
41
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseBooleanValue(value) {
|
|
45
|
+
if (typeof value === "boolean") return value;
|
|
46
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
47
|
+
if (!normalized) return null;
|
|
48
|
+
if (["1", "true", "yes", "y", "on", "是"].includes(normalized)) return true;
|
|
49
|
+
if (["0", "false", "no", "n", "off", "否"].includes(normalized)) return false;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveHumanRestEnabled(config = {}) {
|
|
54
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return false;
|
|
55
|
+
const candidates = [
|
|
56
|
+
config.humanRestEnabled,
|
|
57
|
+
config.human_rest_enabled,
|
|
58
|
+
config.humanLikeRestEnabled,
|
|
59
|
+
config.human_like_rest_enabled
|
|
60
|
+
];
|
|
61
|
+
for (const candidate of candidates) {
|
|
62
|
+
const parsed = parseBooleanValue(candidate);
|
|
63
|
+
if (typeof parsed === "boolean") return parsed;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isUnlimitedTargetCountToken(value) {
|
|
69
|
+
const token = normalizeText(value).toLowerCase();
|
|
70
|
+
if (!token) return false;
|
|
71
|
+
const compact = token.replace(/\s+/g, "");
|
|
72
|
+
const withoutAnnotation = compact.replace(/[((【[].*?[))】\]]/gu, "");
|
|
73
|
+
const knownTokens = new Set([
|
|
74
|
+
"all",
|
|
75
|
+
"unlimited",
|
|
76
|
+
"infinity",
|
|
77
|
+
"inf",
|
|
78
|
+
"max",
|
|
79
|
+
"full",
|
|
80
|
+
"allcandidates",
|
|
81
|
+
"全部",
|
|
82
|
+
"全量",
|
|
83
|
+
"不限",
|
|
84
|
+
"扫到底",
|
|
85
|
+
"全部候选人",
|
|
86
|
+
"所有候选人",
|
|
87
|
+
"全部人选",
|
|
88
|
+
"所有人选",
|
|
89
|
+
"直到完成所有人选"
|
|
90
|
+
]);
|
|
91
|
+
if (knownTokens.has(token) || knownTokens.has(compact) || knownTokens.has(withoutAnnotation)) return true;
|
|
92
|
+
if (/^(?:all|unlimited|infinity|inf|max|full)(?:candidate|candidates)?$/i.test(compact)) return true;
|
|
93
|
+
if (/^(?:all|unlimited|infinity|inf|max|full)(?:候选人|人选|牛人|人才|人员)?$/iu.test(withoutAnnotation)) return true;
|
|
94
|
+
if (/^(?:全部|所有|全量|不限)(?:候选人|人选|牛人|人才|人员)?$/u.test(compact)) return true;
|
|
95
|
+
if (!/\d/.test(compact) && /(?:扫到底|全部候选人|所有候选人|全部人选|所有人选)/u.test(compact)) return true;
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getWrappedTargetCountValue(value) {
|
|
100
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
101
|
+
for (const key of TARGET_COUNT_WRAPPER_KEYS) {
|
|
102
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
103
|
+
return value[key];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getBossChatTargetCountValue(input = {}) {
|
|
110
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return undefined;
|
|
111
|
+
if (Object.prototype.hasOwnProperty.call(input, "target_count") && input.target_count !== undefined && input.target_count !== null) {
|
|
112
|
+
return input.target_count;
|
|
113
|
+
}
|
|
114
|
+
if (Object.prototype.hasOwnProperty.call(input, "targetCount") && input.targetCount !== undefined && input.targetCount !== null) {
|
|
115
|
+
return input.targetCount;
|
|
116
|
+
}
|
|
117
|
+
if (Object.prototype.hasOwnProperty.call(input, "target_count")) return input.target_count;
|
|
118
|
+
if (Object.prototype.hasOwnProperty.call(input, "targetCount")) return input.targetCount;
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cloneForDiagnostics(value) {
|
|
123
|
+
if (value === undefined) return undefined;
|
|
124
|
+
if (value === null || ["string", "number", "boolean"].includes(typeof value)) return value;
|
|
125
|
+
try {
|
|
126
|
+
return JSON.parse(JSON.stringify(value));
|
|
127
|
+
} catch {
|
|
128
|
+
return String(value);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function buildTargetCountCompatibilityHints({
|
|
133
|
+
argumentName = "target_count",
|
|
134
|
+
recommendedArgumentPatch = { target_count: TARGET_COUNT_CANONICAL_ALL },
|
|
135
|
+
includeOptions = true
|
|
136
|
+
} = {}) {
|
|
137
|
+
const normalizedArgumentName = normalizeText(argumentName) || "target_count";
|
|
138
|
+
const clonedRecommendedPatch = cloneForDiagnostics(recommendedArgumentPatch)
|
|
139
|
+
|| { target_count: TARGET_COUNT_CANONICAL_ALL };
|
|
140
|
+
const literal = `${normalizedArgumentName}="${TARGET_COUNT_CANONICAL_ALL}"`;
|
|
141
|
+
const base = {
|
|
142
|
+
argument_name: normalizedArgumentName,
|
|
143
|
+
answer_format: `${normalizedArgumentName} = 正整数 | "${TARGET_COUNT_CANONICAL_ALL}"`,
|
|
144
|
+
canonical_unlimited_value: TARGET_COUNT_CANONICAL_ALL,
|
|
145
|
+
recommended_value: TARGET_COUNT_CANONICAL_ALL,
|
|
146
|
+
recommended_argument_patch: clonedRecommendedPatch,
|
|
147
|
+
accepted_examples: TARGET_COUNT_ACCEPTED_EXAMPLES.slice()
|
|
148
|
+
};
|
|
149
|
+
if (!includeOptions) return base;
|
|
150
|
+
return {
|
|
151
|
+
...base,
|
|
152
|
+
options: [
|
|
153
|
+
{
|
|
154
|
+
label: `扫到底(必须传 ${literal},推荐)`,
|
|
155
|
+
value: TARGET_COUNT_CANONICAL_ALL,
|
|
156
|
+
canonical_value: TARGET_COUNT_CANONICAL_ALL,
|
|
157
|
+
argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
label: `不限(等价于 ${literal})`,
|
|
161
|
+
value: "unlimited",
|
|
162
|
+
canonical_value: TARGET_COUNT_CANONICAL_ALL,
|
|
163
|
+
argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
label: `全部候选人(等价于 ${literal})`,
|
|
167
|
+
value: "全部候选人",
|
|
168
|
+
canonical_value: TARGET_COUNT_CANONICAL_ALL,
|
|
169
|
+
argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
label: `所有候选人(等价于 ${literal})`,
|
|
173
|
+
value: "所有候选人",
|
|
174
|
+
canonical_value: TARGET_COUNT_CANONICAL_ALL,
|
|
175
|
+
argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function normalizeTargetCountInput(value) {
|
|
182
|
+
if (value === undefined || value === null) {
|
|
183
|
+
return {
|
|
184
|
+
provided: false,
|
|
185
|
+
targetCount: null,
|
|
186
|
+
cliArg: null,
|
|
187
|
+
publicValue: null,
|
|
188
|
+
rawValue: value,
|
|
189
|
+
parseError: null
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const unwrapped = getWrappedTargetCountValue(value);
|
|
193
|
+
if (unwrapped !== value) {
|
|
194
|
+
return normalizeTargetCountInput(unwrapped);
|
|
195
|
+
}
|
|
196
|
+
const raw = normalizeText(unwrapped);
|
|
197
|
+
if (!raw) {
|
|
198
|
+
return {
|
|
199
|
+
provided: false,
|
|
200
|
+
targetCount: null,
|
|
201
|
+
cliArg: null,
|
|
202
|
+
publicValue: null,
|
|
203
|
+
rawValue: value,
|
|
204
|
+
parseError: null
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (isUnlimitedTargetCountToken(raw)) {
|
|
208
|
+
return {
|
|
209
|
+
provided: true,
|
|
210
|
+
targetCount: null,
|
|
211
|
+
cliArg: "-1",
|
|
212
|
+
publicValue: "all",
|
|
213
|
+
rawValue: cloneForDiagnostics(value),
|
|
214
|
+
parseError: null
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const parsed = Number.parseInt(String(raw), 10);
|
|
218
|
+
if (Number.isFinite(parsed) && parsed === -1) {
|
|
219
|
+
return {
|
|
220
|
+
provided: true,
|
|
221
|
+
targetCount: null,
|
|
222
|
+
cliArg: "-1",
|
|
223
|
+
publicValue: "all",
|
|
224
|
+
rawValue: cloneForDiagnostics(value),
|
|
225
|
+
parseError: null
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
229
|
+
return {
|
|
230
|
+
provided: true,
|
|
231
|
+
targetCount: parsed,
|
|
232
|
+
cliArg: String(parsed),
|
|
233
|
+
publicValue: parsed,
|
|
234
|
+
rawValue: cloneForDiagnostics(value),
|
|
235
|
+
parseError: null
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
provided: false,
|
|
240
|
+
targetCount: null,
|
|
241
|
+
cliArg: null,
|
|
242
|
+
publicValue: null,
|
|
243
|
+
rawValue: cloneForDiagnostics(value),
|
|
244
|
+
parseError: "target_count must be a positive integer, -1, or one of: all, unlimited, 全部, 不限, 扫到底, 全量, 全部候选人, 所有候选人"
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function parseJsonOutput(text) {
|
|
249
|
+
const trimmed = String(text || "").trim();
|
|
250
|
+
if (!trimmed) return null;
|
|
251
|
+
try {
|
|
252
|
+
return JSON.parse(trimmed);
|
|
253
|
+
} catch {}
|
|
254
|
+
const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
255
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
256
|
+
try {
|
|
257
|
+
return JSON.parse(lines[index]);
|
|
258
|
+
} catch {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function sleep(ms) {
|
|
266
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function resolveBossChatCliDir(workspaceRoot) {
|
|
270
|
+
const localDir = path.join(path.resolve(String(workspaceRoot || process.cwd())), "boss-chat-cli");
|
|
271
|
+
if (pathExists(localDir)) return localDir;
|
|
272
|
+
return pathExists(VENDORED_BOSS_CHAT_DIR) ? VENDORED_BOSS_CHAT_DIR : null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function resolveBossChatCliPath(workspaceRoot) {
|
|
276
|
+
const cliDir = resolveBossChatCliDir(workspaceRoot);
|
|
277
|
+
if (!cliDir) return null;
|
|
278
|
+
const cliPath = path.join(cliDir, "src", "cli.js");
|
|
279
|
+
return pathExists(cliPath) ? cliPath : null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function validateRecommendScreenConfig(config) {
|
|
283
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
284
|
+
return {
|
|
285
|
+
ok: false,
|
|
286
|
+
message: "screening-config.json 缺失或格式无效。请填写 baseUrl、apiKey、model。"
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const baseUrl = normalizeText(config.baseUrl).replace(/\/+$/, "");
|
|
290
|
+
const apiKey = normalizeText(config.apiKey);
|
|
291
|
+
const model = normalizeText(config.model);
|
|
292
|
+
const missing = [];
|
|
293
|
+
if (!baseUrl) missing.push("baseUrl");
|
|
294
|
+
if (!apiKey) missing.push("apiKey");
|
|
295
|
+
if (!model) missing.push("model");
|
|
296
|
+
if (missing.length > 0) {
|
|
297
|
+
return {
|
|
298
|
+
ok: false,
|
|
299
|
+
message: `screening-config.json 缺少必填字段:${missing.join(", ")}。`
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (/^replace-with/i.test(apiKey)) {
|
|
303
|
+
return {
|
|
304
|
+
ok: false,
|
|
305
|
+
message: "screening-config.json 的 apiKey 仍是模板占位符,请填写真实 API Key。"
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
return { ok: true };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function resolveLlmThinkingLevel(config = {}) {
|
|
312
|
+
if (!config || typeof config !== "object") return "";
|
|
313
|
+
for (const field of LLM_THINKING_LEVEL_FIELDS) {
|
|
314
|
+
const value = normalizeText(config[field]);
|
|
315
|
+
if (value) return value;
|
|
316
|
+
}
|
|
317
|
+
return "";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function resolveBossChatScreenConfig(workspaceRoot) {
|
|
321
|
+
const resolution = getScreenConfigResolution(workspaceRoot);
|
|
322
|
+
const configPath = resolution.resolved_path || resolution.writable_path || resolution.legacy_path || null;
|
|
323
|
+
if (!configPath || !pathExists(configPath)) {
|
|
324
|
+
return {
|
|
325
|
+
ok: false,
|
|
326
|
+
error: {
|
|
327
|
+
code: "SCREEN_CONFIG_ERROR",
|
|
328
|
+
message: `screening-config.json 不存在。请先完成 recommend 配置。${configPath ? ` (path: ${configPath})` : ""}`
|
|
329
|
+
},
|
|
330
|
+
config_path: configPath,
|
|
331
|
+
config_dir: configPath ? path.dirname(configPath) : null
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
let parsed = null;
|
|
335
|
+
try {
|
|
336
|
+
parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
337
|
+
} catch (error) {
|
|
338
|
+
return {
|
|
339
|
+
ok: false,
|
|
340
|
+
error: {
|
|
341
|
+
code: "SCREEN_CONFIG_ERROR",
|
|
342
|
+
message: `screening-config.json 解析失败:${error.message || "unknown error"} (path: ${configPath})`
|
|
343
|
+
},
|
|
344
|
+
config_path: configPath,
|
|
345
|
+
config_dir: path.dirname(configPath)
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const validation = validateRecommendScreenConfig(parsed);
|
|
349
|
+
if (!validation.ok) {
|
|
350
|
+
return {
|
|
351
|
+
ok: false,
|
|
352
|
+
error: {
|
|
353
|
+
code: "SCREEN_CONFIG_ERROR",
|
|
354
|
+
message: `${validation.message} (path: ${configPath})`
|
|
355
|
+
},
|
|
356
|
+
config_path: configPath,
|
|
357
|
+
config_dir: path.dirname(configPath)
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
ok: true,
|
|
362
|
+
config: {
|
|
363
|
+
baseUrl: normalizeText(parsed.baseUrl).replace(/\/+$/, ""),
|
|
364
|
+
apiKey: normalizeText(parsed.apiKey),
|
|
365
|
+
model: normalizeText(parsed.model),
|
|
366
|
+
llmThinkingLevel: resolveLlmThinkingLevel(parsed),
|
|
367
|
+
...resolveSharedLlmTransportConfig(parsed),
|
|
368
|
+
debugPort: parsePositiveInteger(parsed.debugPort, 9222),
|
|
369
|
+
humanRestEnabled: resolveHumanRestEnabled(parsed)
|
|
370
|
+
},
|
|
371
|
+
config_path: configPath,
|
|
372
|
+
config_dir: path.dirname(configPath)
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function normalizeBossChatStartInput(input = {}) {
|
|
377
|
+
const profile = normalizeText(input.profile) || "default";
|
|
378
|
+
const job = normalizeText(input.job);
|
|
379
|
+
const startFromRaw = normalizeText(input.startFrom || input.start_from).toLowerCase();
|
|
380
|
+
const startFrom = startFromRaw === "all" ? "all" : startFromRaw === "unread" ? "unread" : "";
|
|
381
|
+
const criteria = normalizeText(input.criteria);
|
|
382
|
+
const parsedTarget = normalizeTargetCountInput(getBossChatTargetCountValue(input));
|
|
383
|
+
const port = parsePositiveInteger(input.port);
|
|
384
|
+
return {
|
|
385
|
+
profile,
|
|
386
|
+
job,
|
|
387
|
+
startFrom,
|
|
388
|
+
criteria,
|
|
389
|
+
targetCount: parsedTarget.targetCount,
|
|
390
|
+
targetCountArg: parsedTarget.cliArg,
|
|
391
|
+
targetCountProvided: parsedTarget.provided,
|
|
392
|
+
targetCountPublicValue: parsedTarget.publicValue,
|
|
393
|
+
targetCountRawValue: parsedTarget.rawValue,
|
|
394
|
+
targetCountParseError: parsedTarget.parseError,
|
|
395
|
+
port,
|
|
396
|
+
dryRun: input.dryRun === true || input.dry_run === true,
|
|
397
|
+
noState: input.noState === true || input.no_state === true,
|
|
398
|
+
safePacing: typeof input.safePacing === "boolean" ? input.safePacing : (
|
|
399
|
+
typeof input.safe_pacing === "boolean" ? input.safe_pacing : undefined
|
|
400
|
+
),
|
|
401
|
+
batchRestEnabled: typeof input.batchRestEnabled === "boolean" ? input.batchRestEnabled : (
|
|
402
|
+
typeof input.batch_rest_enabled === "boolean" ? input.batch_rest_enabled : undefined
|
|
403
|
+
)
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function normalizeBossChatRunId(input = {}) {
|
|
408
|
+
return normalizeText(input.runId || input.run_id);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function getMissingBossChatStartFields(input = {}) {
|
|
412
|
+
const normalized = normalizeBossChatStartInput(input);
|
|
413
|
+
const missing = [];
|
|
414
|
+
if (!normalized.job) missing.push("job");
|
|
415
|
+
if (!normalized.startFrom) missing.push("start_from");
|
|
416
|
+
if (!normalized.targetCountProvided) missing.push("target_count");
|
|
417
|
+
if (!normalized.criteria) missing.push("criteria");
|
|
418
|
+
return missing;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function buildTargetCountQuestionHint(item = {}) {
|
|
422
|
+
const next = { ...item };
|
|
423
|
+
const hints = buildTargetCountCompatibilityHints({
|
|
424
|
+
argumentName: "target_count",
|
|
425
|
+
recommendedArgumentPatch: { target_count: TARGET_COUNT_CANONICAL_ALL }
|
|
426
|
+
});
|
|
427
|
+
return {
|
|
428
|
+
...next,
|
|
429
|
+
...hints,
|
|
430
|
+
question: `请输入 target_count:正整数,或直接填写 "${TARGET_COUNT_CANONICAL_ALL}"(扫到底)。`,
|
|
431
|
+
examples: TARGET_COUNT_ACCEPTED_EXAMPLES.slice()
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function normalizePendingQuestions(pendingQuestions = []) {
|
|
436
|
+
return pendingQuestions.map((item) => {
|
|
437
|
+
if (String(item?.field || "") !== "target_count") return item;
|
|
438
|
+
return buildTargetCountQuestionHint(item);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function buildNextCallExample(input = {}, missingFields = []) {
|
|
443
|
+
if (!Array.isArray(missingFields) || missingFields.length === 0) return null;
|
|
444
|
+
const normalized = normalizeBossChatStartInput(input);
|
|
445
|
+
const sample = {};
|
|
446
|
+
if (normalized.job) sample.job = normalized.job;
|
|
447
|
+
if (normalized.startFrom) sample.start_from = normalized.startFrom;
|
|
448
|
+
if (normalized.criteria) sample.criteria = normalized.criteria;
|
|
449
|
+
if (normalized.targetCountProvided) {
|
|
450
|
+
sample.target_count = normalized.targetCountPublicValue || (normalized.targetCountArg === "-1" ? "all" : normalized.targetCount);
|
|
451
|
+
} else if (missingFields.includes("target_count")) {
|
|
452
|
+
sample.target_count = "all";
|
|
453
|
+
}
|
|
454
|
+
return Object.keys(sample).length > 0 ? sample : null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function buildTargetCountNeedInputDiagnostics(input = {}, missingFields = []) {
|
|
458
|
+
if (!Array.isArray(missingFields) || !missingFields.includes("target_count")) return {};
|
|
459
|
+
const normalized = normalizeBossChatStartInput(input);
|
|
460
|
+
const hints = buildTargetCountCompatibilityHints({
|
|
461
|
+
argumentName: "target_count",
|
|
462
|
+
recommendedArgumentPatch: { target_count: TARGET_COUNT_CANONICAL_ALL },
|
|
463
|
+
includeOptions: false
|
|
464
|
+
});
|
|
465
|
+
return {
|
|
466
|
+
...hints,
|
|
467
|
+
...(normalized.targetCountRawValue !== undefined ? { received_target_count: normalized.targetCountRawValue } : {}),
|
|
468
|
+
...(normalized.targetCountParseError ? { target_count_parse_error: normalized.targetCountParseError } : {})
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function buildBossChatCliArgs(command, input, resolvedConfig) {
|
|
473
|
+
const args = [command, "--json"];
|
|
474
|
+
if (command === "prepare-run") {
|
|
475
|
+
const normalized = normalizeBossChatStartInput(input);
|
|
476
|
+
args.push("--profile", normalized.profile);
|
|
477
|
+
if (normalized.job) args.push("--job", normalized.job);
|
|
478
|
+
if (normalized.startFrom) args.push("--start-from", normalized.startFrom);
|
|
479
|
+
if (normalized.criteria) args.push("--criteria", normalized.criteria);
|
|
480
|
+
if (normalized.targetCountArg) args.push("--targetCount", normalized.targetCountArg);
|
|
481
|
+
args.push("--port", String(normalized.port || resolvedConfig.debugPort || 9222));
|
|
482
|
+
args.push("--baseurl", resolvedConfig.baseUrl);
|
|
483
|
+
args.push("--apikey", resolvedConfig.apiKey);
|
|
484
|
+
args.push("--model", resolvedConfig.model);
|
|
485
|
+
if (resolvedConfig.llmThinkingLevel) {
|
|
486
|
+
args.push("--thinking-level", resolvedConfig.llmThinkingLevel);
|
|
487
|
+
}
|
|
488
|
+
if (resolvedConfig.llmTimeoutMs) {
|
|
489
|
+
args.push("--llm-timeout-ms", String(resolvedConfig.llmTimeoutMs));
|
|
490
|
+
}
|
|
491
|
+
if (resolvedConfig.llmMaxRetries) {
|
|
492
|
+
args.push("--llm-max-retries", String(resolvedConfig.llmMaxRetries));
|
|
493
|
+
}
|
|
494
|
+
return args;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (command === "start-run") {
|
|
498
|
+
const normalized = normalizeBossChatStartInput(input);
|
|
499
|
+
args.push("--profile", normalized.profile);
|
|
500
|
+
if (normalized.dryRun) args.push("--dry-run");
|
|
501
|
+
if (normalized.noState) args.push("--no-state");
|
|
502
|
+
args.push("--job", normalized.job);
|
|
503
|
+
args.push("--start-from", normalized.startFrom);
|
|
504
|
+
args.push("--criteria", normalized.criteria);
|
|
505
|
+
if (normalized.targetCountArg) {
|
|
506
|
+
args.push("--targetCount", normalized.targetCountArg);
|
|
507
|
+
}
|
|
508
|
+
args.push("--baseurl", resolvedConfig.baseUrl);
|
|
509
|
+
args.push("--apikey", resolvedConfig.apiKey);
|
|
510
|
+
args.push("--model", resolvedConfig.model);
|
|
511
|
+
if (resolvedConfig.llmThinkingLevel) {
|
|
512
|
+
args.push("--thinking-level", resolvedConfig.llmThinkingLevel);
|
|
513
|
+
}
|
|
514
|
+
if (resolvedConfig.llmTimeoutMs) {
|
|
515
|
+
args.push("--llm-timeout-ms", String(resolvedConfig.llmTimeoutMs));
|
|
516
|
+
}
|
|
517
|
+
if (resolvedConfig.llmMaxRetries) {
|
|
518
|
+
args.push("--llm-max-retries", String(resolvedConfig.llmMaxRetries));
|
|
519
|
+
}
|
|
520
|
+
args.push("--port", String(normalized.port || resolvedConfig.debugPort || 9222));
|
|
521
|
+
if (typeof normalized.safePacing === "boolean") {
|
|
522
|
+
args.push("--safe-pacing", String(normalized.safePacing));
|
|
523
|
+
}
|
|
524
|
+
if (typeof normalized.batchRestEnabled === "boolean") {
|
|
525
|
+
args.push("--batch-rest", String(normalized.batchRestEnabled));
|
|
526
|
+
} else if (typeof resolvedConfig?.humanRestEnabled === "boolean") {
|
|
527
|
+
args.push("--batch-rest", String(resolvedConfig.humanRestEnabled));
|
|
528
|
+
}
|
|
529
|
+
return args;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const runId = normalizeBossChatRunId(input);
|
|
533
|
+
args.push("--profile", normalizeText(input.profile) || "default");
|
|
534
|
+
args.push("--run-id", runId);
|
|
535
|
+
return args;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
|
|
539
|
+
const cliPath = resolveBossChatCliPath(workspaceRoot);
|
|
540
|
+
if (!cliPath) {
|
|
541
|
+
return {
|
|
542
|
+
ok: false,
|
|
543
|
+
exitCode: -1,
|
|
544
|
+
stdout: "",
|
|
545
|
+
stderr: "",
|
|
546
|
+
payload: {
|
|
547
|
+
status: "FAILED",
|
|
548
|
+
error: {
|
|
549
|
+
code: "BOSS_CHAT_CLI_MISSING",
|
|
550
|
+
message: "未找到 vendored boss-chat CLI。"
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
let configResolution = null;
|
|
557
|
+
if (command === "start-run" || command === "prepare-run") {
|
|
558
|
+
configResolution = resolveBossChatScreenConfig(workspaceRoot);
|
|
559
|
+
if (!configResolution.ok) {
|
|
560
|
+
return {
|
|
561
|
+
ok: false,
|
|
562
|
+
exitCode: 1,
|
|
563
|
+
stdout: "",
|
|
564
|
+
stderr: "",
|
|
565
|
+
payload: {
|
|
566
|
+
status: "FAILED",
|
|
567
|
+
error: configResolution.error,
|
|
568
|
+
config_path: configResolution.config_path,
|
|
569
|
+
config_dir: configResolution.config_dir
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const args = [cliPath, ...buildBossChatCliArgs(command, input, configResolution?.config || {})];
|
|
576
|
+
const cwd = path.resolve(String(workspaceRoot || process.cwd()));
|
|
577
|
+
return new Promise((resolve) => {
|
|
578
|
+
const child = spawn(process.execPath, args, {
|
|
579
|
+
cwd,
|
|
580
|
+
env: process.env,
|
|
581
|
+
windowsHide: true,
|
|
582
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
let stdout = "";
|
|
586
|
+
let stderr = "";
|
|
587
|
+
child.stdout.on("data", (chunk) => {
|
|
588
|
+
stdout += String(chunk);
|
|
589
|
+
});
|
|
590
|
+
child.stderr.on("data", (chunk) => {
|
|
591
|
+
stderr += String(chunk);
|
|
592
|
+
});
|
|
593
|
+
child.on("error", (error) => {
|
|
594
|
+
resolve({
|
|
595
|
+
ok: false,
|
|
596
|
+
exitCode: -1,
|
|
597
|
+
stdout,
|
|
598
|
+
stderr,
|
|
599
|
+
payload: {
|
|
600
|
+
status: "FAILED",
|
|
601
|
+
error: {
|
|
602
|
+
code: "BOSS_CHAT_CLI_SPAWN_FAILED",
|
|
603
|
+
message: error?.message || "无法启动 vendored boss-chat CLI。"
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
child.on("close", (code) => {
|
|
609
|
+
const parsed = parseJsonOutput(stdout) || parseJsonOutput(stderr);
|
|
610
|
+
if (parsed && typeof parsed === "object") {
|
|
611
|
+
resolve({
|
|
612
|
+
ok: Number(code) === 0 && String(parsed.status || "").toUpperCase() !== "FAILED",
|
|
613
|
+
exitCode: Number.isInteger(code) ? code : 1,
|
|
614
|
+
stdout,
|
|
615
|
+
stderr,
|
|
616
|
+
payload: parsed
|
|
617
|
+
});
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
resolve({
|
|
621
|
+
ok: Number(code) === 0,
|
|
622
|
+
exitCode: Number.isInteger(code) ? code : 1,
|
|
623
|
+
stdout,
|
|
624
|
+
stderr,
|
|
625
|
+
payload: Number(code) === 0
|
|
626
|
+
? {
|
|
627
|
+
status: "OK",
|
|
628
|
+
message: normalizeText(stdout) || `${command} 执行成功。`
|
|
629
|
+
}
|
|
630
|
+
: {
|
|
631
|
+
status: "FAILED",
|
|
632
|
+
error: {
|
|
633
|
+
code: "BOSS_CHAT_CLI_EXECUTION_FAILED",
|
|
634
|
+
message: normalizeText(stderr || stdout) || `${command} 执行失败。`
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export function getBossChatHealthCheck(workspaceRoot, input = {}) {
|
|
643
|
+
const cliDir = resolveBossChatCliDir(workspaceRoot);
|
|
644
|
+
const cliPath = resolveBossChatCliPath(workspaceRoot);
|
|
645
|
+
const configResolution = resolveBossChatScreenConfig(workspaceRoot);
|
|
646
|
+
const resolvedPort = parsePositiveInteger(input.port)
|
|
647
|
+
|| (configResolution.ok ? configResolution.config.debugPort : 9222);
|
|
648
|
+
if (!cliDir || !cliPath) {
|
|
649
|
+
return {
|
|
650
|
+
status: "FAILED",
|
|
651
|
+
error: {
|
|
652
|
+
code: "BOSS_CHAT_CLI_MISSING",
|
|
653
|
+
message: "未找到 vendored boss-chat CLI。"
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
if (!configResolution.ok) {
|
|
658
|
+
return {
|
|
659
|
+
status: "FAILED",
|
|
660
|
+
error: configResolution.error,
|
|
661
|
+
config_path: configResolution.config_path,
|
|
662
|
+
config_dir: configResolution.config_dir,
|
|
663
|
+
cli_dir: cliDir,
|
|
664
|
+
cli_path: cliPath
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
status: "OK",
|
|
669
|
+
server: "boss-chat",
|
|
670
|
+
cli_dir: cliDir,
|
|
671
|
+
cli_path: cliPath,
|
|
672
|
+
config_path: configResolution.config_path,
|
|
673
|
+
debug_port: resolvedPort,
|
|
674
|
+
shared_llm_config: true
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export async function startBossChatRun({ workspaceRoot, input = {} }) {
|
|
679
|
+
const missingFields = getMissingBossChatStartFields(input);
|
|
680
|
+
if (missingFields.length > 0) {
|
|
681
|
+
const prepared = await prepareBossChatRun({ workspaceRoot, input });
|
|
682
|
+
if (prepared?.status === "FAILED") return prepared;
|
|
683
|
+
const pendingQuestions = Array.isArray(prepared?.pending_questions)
|
|
684
|
+
? prepared.pending_questions.filter((item) => missingFields.includes(String(item?.field || "")))
|
|
685
|
+
: [];
|
|
686
|
+
const normalizedPendingQuestions = normalizePendingQuestions(pendingQuestions);
|
|
687
|
+
const nextCallExample = buildNextCallExample(input, missingFields);
|
|
688
|
+
const targetCountDiagnostics = buildTargetCountNeedInputDiagnostics(input, missingFields);
|
|
689
|
+
return {
|
|
690
|
+
...prepared,
|
|
691
|
+
status: "NEED_INPUT",
|
|
692
|
+
required_fields: CHAT_REQUIRED_FIELDS.slice(),
|
|
693
|
+
missing_fields: missingFields,
|
|
694
|
+
pending_questions: normalizedPendingQuestions,
|
|
695
|
+
...targetCountDiagnostics,
|
|
696
|
+
...(nextCallExample ? { next_call_example: nextCallExample } : {}),
|
|
697
|
+
message: prepared?.message
|
|
698
|
+
|| "已获取 Boss 聊天页岗位列表,请先补齐 job / start_from / target_count / criteria。"
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
return (await spawnBossChatCli({ workspaceRoot, command: "start-run", input })).payload;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export async function prepareBossChatRun({ workspaceRoot, input = {} }) {
|
|
705
|
+
let payload = null;
|
|
706
|
+
for (let attempt = 1; attempt <= PREPARE_BOSS_CHAT_MAX_ATTEMPTS; attempt += 1) {
|
|
707
|
+
payload = (await spawnBossChatCli({ workspaceRoot, command: "prepare-run", input })).payload;
|
|
708
|
+
if (payload?.status !== "FAILED") break;
|
|
709
|
+
if (attempt >= PREPARE_BOSS_CHAT_MAX_ATTEMPTS) break;
|
|
710
|
+
await sleep(PREPARE_BOSS_CHAT_RETRY_DELAY_MS);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (payload?.status !== "NEED_INPUT") return payload;
|
|
714
|
+
|
|
715
|
+
const missingFields = getMissingBossChatStartFields(input);
|
|
716
|
+
const pendingQuestions = Array.isArray(payload?.pending_questions)
|
|
717
|
+
? payload.pending_questions.filter((item) => (
|
|
718
|
+
missingFields.length === 0 || missingFields.includes(String(item?.field || ""))
|
|
719
|
+
))
|
|
720
|
+
: [];
|
|
721
|
+
const nextCallExample = buildNextCallExample(input, missingFields);
|
|
722
|
+
const targetCountDiagnostics = buildTargetCountNeedInputDiagnostics(input, missingFields);
|
|
723
|
+
return {
|
|
724
|
+
...payload,
|
|
725
|
+
required_fields: CHAT_REQUIRED_FIELDS.slice(),
|
|
726
|
+
missing_fields: missingFields,
|
|
727
|
+
pending_questions: normalizePendingQuestions(pendingQuestions),
|
|
728
|
+
...targetCountDiagnostics,
|
|
729
|
+
...(nextCallExample ? { next_call_example: nextCallExample } : {})
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export async function getBossChatRun({ workspaceRoot, input = {} }) {
|
|
734
|
+
return (await spawnBossChatCli({ workspaceRoot, command: "get-run", input })).payload;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export async function pauseBossChatRun({ workspaceRoot, input = {} }) {
|
|
738
|
+
return (await spawnBossChatCli({ workspaceRoot, command: "pause-run", input })).payload;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export async function resumeBossChatRun({ workspaceRoot, input = {} }) {
|
|
742
|
+
return (await spawnBossChatCli({ workspaceRoot, command: "resume-run", input })).payload;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
export async function cancelBossChatRun({ workspaceRoot, input = {} }) {
|
|
746
|
+
return (await spawnBossChatCli({ workspaceRoot, command: "cancel-run", input })).payload;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export async function runBossChatSync({ workspaceRoot, input = {}, pollMs = DEFAULT_BOSS_CHAT_POLL_MS }) {
|
|
750
|
+
const accepted = await startBossChatRun({ workspaceRoot, input });
|
|
751
|
+
if (accepted?.status !== "ACCEPTED" || !normalizeText(accepted.run_id)) {
|
|
752
|
+
return accepted;
|
|
753
|
+
}
|
|
754
|
+
const runId = normalizeText(accepted.run_id);
|
|
755
|
+
while (true) {
|
|
756
|
+
await sleep(pollMs);
|
|
757
|
+
const statusPayload = await getBossChatRun({
|
|
758
|
+
workspaceRoot,
|
|
759
|
+
input: {
|
|
760
|
+
profile: input.profile,
|
|
761
|
+
runId
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
const runState = normalizeText(statusPayload?.run?.state).toLowerCase();
|
|
765
|
+
if (BOSS_CHAT_TERMINAL_STATES.has(runState)) {
|
|
766
|
+
return statusPayload;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|