@reconcrap/boss-recommend-mcp 1.2.10 → 1.3.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 (34) hide show
  1. package/README.md +82 -1
  2. package/package.json +2 -1
  3. package/skills/boss-chat/README.md +5 -0
  4. package/skills/boss-chat/SKILL.md +69 -0
  5. package/skills/boss-recommend-pipeline/SKILL.md +40 -4
  6. package/src/adapters.js +19 -5
  7. package/src/boss-chat.js +436 -0
  8. package/src/cli.js +294 -129
  9. package/src/index.js +459 -108
  10. package/src/pipeline.js +605 -8
  11. package/src/run-state.js +5 -0
  12. package/src/test-adapters-runtime.js +69 -0
  13. package/src/test-boss-chat.js +399 -0
  14. package/src/test-index-async.js +238 -4
  15. package/src/test-pipeline.js +408 -1
  16. package/vendor/boss-chat-cli/README.md +134 -0
  17. package/vendor/boss-chat-cli/package.json +53 -0
  18. package/vendor/boss-chat-cli/src/app.js +769 -0
  19. package/vendor/boss-chat-cli/src/browser/chat-page.js +2681 -0
  20. package/vendor/boss-chat-cli/src/cli.js +1350 -0
  21. package/vendor/boss-chat-cli/src/mcp/server.js +149 -0
  22. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +193 -0
  23. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +260 -0
  24. package/vendor/boss-chat-cli/src/runtime/interaction.js +102 -0
  25. package/vendor/boss-chat-cli/src/runtime/run-control.js +102 -0
  26. package/vendor/boss-chat-cli/src/services/chrome-client.js +97 -0
  27. package/vendor/boss-chat-cli/src/services/llm.js +352 -0
  28. package/vendor/boss-chat-cli/src/services/profile-store.js +157 -0
  29. package/vendor/boss-chat-cli/src/services/report-store.js +19 -0
  30. package/vendor/boss-chat-cli/src/services/resume-capture.js +554 -0
  31. package/vendor/boss-chat-cli/src/services/state-store.js +217 -0
  32. package/vendor/boss-chat-cli/src/utils/customer-key.js +82 -0
  33. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +902 -56
  34. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +387 -1
@@ -0,0 +1,436 @@
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 } 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 BOSS_CHAT_TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
14
+ const CHAT_REQUIRED_FIELDS = ["job", "start_from", "target_count", "criteria"];
15
+
16
+ function normalizeText(value) {
17
+ return String(value || "").replace(/\s+/g, " ").trim();
18
+ }
19
+
20
+ function pathExists(targetPath) {
21
+ try {
22
+ return fs.existsSync(targetPath);
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ function parsePositiveInteger(value, fallback = null) {
29
+ const parsed = Number.parseInt(String(value ?? ""), 10);
30
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
31
+ }
32
+
33
+ function parseJsonOutput(text) {
34
+ const trimmed = String(text || "").trim();
35
+ if (!trimmed) return null;
36
+ try {
37
+ return JSON.parse(trimmed);
38
+ } catch {}
39
+ const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
40
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
41
+ try {
42
+ return JSON.parse(lines[index]);
43
+ } catch {
44
+ continue;
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+
50
+ function sleep(ms) {
51
+ return new Promise((resolve) => setTimeout(resolve, ms));
52
+ }
53
+
54
+ function resolveBossChatCliDir(workspaceRoot) {
55
+ const localDir = path.join(path.resolve(String(workspaceRoot || process.cwd())), "boss-chat-cli");
56
+ if (pathExists(localDir)) return localDir;
57
+ return pathExists(VENDORED_BOSS_CHAT_DIR) ? VENDORED_BOSS_CHAT_DIR : null;
58
+ }
59
+
60
+ function resolveBossChatCliPath(workspaceRoot) {
61
+ const cliDir = resolveBossChatCliDir(workspaceRoot);
62
+ if (!cliDir) return null;
63
+ const cliPath = path.join(cliDir, "src", "cli.js");
64
+ return pathExists(cliPath) ? cliPath : null;
65
+ }
66
+
67
+ function validateRecommendScreenConfig(config) {
68
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
69
+ return {
70
+ ok: false,
71
+ message: "screening-config.json 缺失或格式无效。请填写 baseUrl、apiKey、model。"
72
+ };
73
+ }
74
+ const baseUrl = normalizeText(config.baseUrl).replace(/\/+$/, "");
75
+ const apiKey = normalizeText(config.apiKey);
76
+ const model = normalizeText(config.model);
77
+ const missing = [];
78
+ if (!baseUrl) missing.push("baseUrl");
79
+ if (!apiKey) missing.push("apiKey");
80
+ if (!model) missing.push("model");
81
+ if (missing.length > 0) {
82
+ return {
83
+ ok: false,
84
+ message: `screening-config.json 缺少必填字段:${missing.join(", ")}。`
85
+ };
86
+ }
87
+ if (/^replace-with/i.test(apiKey)) {
88
+ return {
89
+ ok: false,
90
+ message: "screening-config.json 的 apiKey 仍是模板占位符,请填写真实 API Key。"
91
+ };
92
+ }
93
+ return { ok: true };
94
+ }
95
+
96
+ function resolveBossChatScreenConfig(workspaceRoot) {
97
+ const resolution = getScreenConfigResolution(workspaceRoot);
98
+ const configPath = resolution.resolved_path || resolution.writable_path || resolution.legacy_path || null;
99
+ if (!configPath || !pathExists(configPath)) {
100
+ return {
101
+ ok: false,
102
+ error: {
103
+ code: "SCREEN_CONFIG_ERROR",
104
+ message: `screening-config.json 不存在。请先完成 recommend 配置。${configPath ? ` (path: ${configPath})` : ""}`
105
+ },
106
+ config_path: configPath,
107
+ config_dir: configPath ? path.dirname(configPath) : null
108
+ };
109
+ }
110
+ let parsed = null;
111
+ try {
112
+ parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
113
+ } catch (error) {
114
+ return {
115
+ ok: false,
116
+ error: {
117
+ code: "SCREEN_CONFIG_ERROR",
118
+ message: `screening-config.json 解析失败:${error.message || "unknown error"} (path: ${configPath})`
119
+ },
120
+ config_path: configPath,
121
+ config_dir: path.dirname(configPath)
122
+ };
123
+ }
124
+ const validation = validateRecommendScreenConfig(parsed);
125
+ if (!validation.ok) {
126
+ return {
127
+ ok: false,
128
+ error: {
129
+ code: "SCREEN_CONFIG_ERROR",
130
+ message: `${validation.message} (path: ${configPath})`
131
+ },
132
+ config_path: configPath,
133
+ config_dir: path.dirname(configPath)
134
+ };
135
+ }
136
+ return {
137
+ ok: true,
138
+ config: {
139
+ baseUrl: normalizeText(parsed.baseUrl).replace(/\/+$/, ""),
140
+ apiKey: normalizeText(parsed.apiKey),
141
+ model: normalizeText(parsed.model),
142
+ debugPort: parsePositiveInteger(parsed.debugPort, 9222)
143
+ },
144
+ config_path: configPath,
145
+ config_dir: path.dirname(configPath)
146
+ };
147
+ }
148
+
149
+ function normalizeBossChatStartInput(input = {}) {
150
+ const profile = normalizeText(input.profile) || "default";
151
+ const job = normalizeText(input.job);
152
+ const startFromRaw = normalizeText(input.startFrom || input.start_from).toLowerCase();
153
+ const startFrom = startFromRaw === "all" ? "all" : startFromRaw === "unread" ? "unread" : "";
154
+ const criteria = normalizeText(input.criteria);
155
+ const targetCount = parsePositiveInteger(input.targetCount ?? input.target_count);
156
+ const port = parsePositiveInteger(input.port);
157
+ return {
158
+ profile,
159
+ job,
160
+ startFrom,
161
+ criteria,
162
+ targetCount,
163
+ port,
164
+ dryRun: input.dryRun === true || input.dry_run === true,
165
+ noState: input.noState === true || input.no_state === true,
166
+ safePacing: typeof input.safePacing === "boolean" ? input.safePacing : (
167
+ typeof input.safe_pacing === "boolean" ? input.safe_pacing : undefined
168
+ ),
169
+ batchRestEnabled: typeof input.batchRestEnabled === "boolean" ? input.batchRestEnabled : (
170
+ typeof input.batch_rest_enabled === "boolean" ? input.batch_rest_enabled : undefined
171
+ )
172
+ };
173
+ }
174
+
175
+ function normalizeBossChatRunId(input = {}) {
176
+ return normalizeText(input.runId || input.run_id);
177
+ }
178
+
179
+ function getMissingBossChatStartFields(input = {}) {
180
+ const normalized = normalizeBossChatStartInput(input);
181
+ const missing = [];
182
+ if (!normalized.job) missing.push("job");
183
+ if (!normalized.startFrom) missing.push("start_from");
184
+ if (!normalized.targetCount) missing.push("target_count");
185
+ if (!normalized.criteria) missing.push("criteria");
186
+ return missing;
187
+ }
188
+
189
+ function buildBossChatCliArgs(command, input, resolvedConfig) {
190
+ const args = [command, "--json"];
191
+ if (command === "prepare-run") {
192
+ const normalized = normalizeBossChatStartInput(input);
193
+ args.push("--profile", normalized.profile);
194
+ if (normalized.job) args.push("--job", normalized.job);
195
+ if (normalized.startFrom) args.push("--start-from", normalized.startFrom);
196
+ if (normalized.criteria) args.push("--criteria", normalized.criteria);
197
+ if (normalized.targetCount) args.push("--targetCount", String(normalized.targetCount));
198
+ args.push("--port", String(normalized.port || resolvedConfig.debugPort || 9222));
199
+ args.push("--baseurl", resolvedConfig.baseUrl);
200
+ args.push("--apikey", resolvedConfig.apiKey);
201
+ args.push("--model", resolvedConfig.model);
202
+ return args;
203
+ }
204
+
205
+ if (command === "start-run") {
206
+ const normalized = normalizeBossChatStartInput(input);
207
+ args.push("--profile", normalized.profile);
208
+ if (normalized.dryRun) args.push("--dry-run");
209
+ if (normalized.noState) args.push("--no-state");
210
+ args.push("--job", normalized.job);
211
+ args.push("--start-from", normalized.startFrom);
212
+ args.push("--criteria", normalized.criteria);
213
+ if (normalized.targetCount) {
214
+ args.push("--targetCount", String(normalized.targetCount));
215
+ }
216
+ args.push("--baseurl", resolvedConfig.baseUrl);
217
+ args.push("--apikey", resolvedConfig.apiKey);
218
+ args.push("--model", resolvedConfig.model);
219
+ args.push("--port", String(normalized.port || resolvedConfig.debugPort || 9222));
220
+ if (typeof normalized.safePacing === "boolean") {
221
+ args.push("--safe-pacing", String(normalized.safePacing));
222
+ }
223
+ if (typeof normalized.batchRestEnabled === "boolean") {
224
+ args.push("--batch-rest", String(normalized.batchRestEnabled));
225
+ }
226
+ return args;
227
+ }
228
+
229
+ const runId = normalizeBossChatRunId(input);
230
+ args.push("--profile", normalizeText(input.profile) || "default");
231
+ args.push("--run-id", runId);
232
+ return args;
233
+ }
234
+
235
+ async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
236
+ const cliPath = resolveBossChatCliPath(workspaceRoot);
237
+ if (!cliPath) {
238
+ return {
239
+ ok: false,
240
+ exitCode: -1,
241
+ stdout: "",
242
+ stderr: "",
243
+ payload: {
244
+ status: "FAILED",
245
+ error: {
246
+ code: "BOSS_CHAT_CLI_MISSING",
247
+ message: "未找到 vendored boss-chat CLI。"
248
+ }
249
+ }
250
+ };
251
+ }
252
+
253
+ let configResolution = null;
254
+ if (command === "start-run" || command === "prepare-run") {
255
+ configResolution = resolveBossChatScreenConfig(workspaceRoot);
256
+ if (!configResolution.ok) {
257
+ return {
258
+ ok: false,
259
+ exitCode: 1,
260
+ stdout: "",
261
+ stderr: "",
262
+ payload: {
263
+ status: "FAILED",
264
+ error: configResolution.error,
265
+ config_path: configResolution.config_path,
266
+ config_dir: configResolution.config_dir
267
+ }
268
+ };
269
+ }
270
+ }
271
+
272
+ const args = [cliPath, ...buildBossChatCliArgs(command, input, configResolution?.config || {})];
273
+ const cwd = path.resolve(String(workspaceRoot || process.cwd()));
274
+ return new Promise((resolve) => {
275
+ const child = spawn(process.execPath, args, {
276
+ cwd,
277
+ env: process.env,
278
+ windowsHide: true,
279
+ stdio: ["ignore", "pipe", "pipe"]
280
+ });
281
+
282
+ let stdout = "";
283
+ let stderr = "";
284
+ child.stdout.on("data", (chunk) => {
285
+ stdout += String(chunk);
286
+ });
287
+ child.stderr.on("data", (chunk) => {
288
+ stderr += String(chunk);
289
+ });
290
+ child.on("error", (error) => {
291
+ resolve({
292
+ ok: false,
293
+ exitCode: -1,
294
+ stdout,
295
+ stderr,
296
+ payload: {
297
+ status: "FAILED",
298
+ error: {
299
+ code: "BOSS_CHAT_CLI_SPAWN_FAILED",
300
+ message: error?.message || "无法启动 vendored boss-chat CLI。"
301
+ }
302
+ }
303
+ });
304
+ });
305
+ child.on("close", (code) => {
306
+ const parsed = parseJsonOutput(stdout) || parseJsonOutput(stderr);
307
+ if (parsed && typeof parsed === "object") {
308
+ resolve({
309
+ ok: Number(code) === 0 && String(parsed.status || "").toUpperCase() !== "FAILED",
310
+ exitCode: Number.isInteger(code) ? code : 1,
311
+ stdout,
312
+ stderr,
313
+ payload: parsed
314
+ });
315
+ return;
316
+ }
317
+ resolve({
318
+ ok: Number(code) === 0,
319
+ exitCode: Number.isInteger(code) ? code : 1,
320
+ stdout,
321
+ stderr,
322
+ payload: Number(code) === 0
323
+ ? {
324
+ status: "OK",
325
+ message: normalizeText(stdout) || `${command} 执行成功。`
326
+ }
327
+ : {
328
+ status: "FAILED",
329
+ error: {
330
+ code: "BOSS_CHAT_CLI_EXECUTION_FAILED",
331
+ message: normalizeText(stderr || stdout) || `${command} 执行失败。`
332
+ }
333
+ }
334
+ });
335
+ });
336
+ });
337
+ }
338
+
339
+ export function getBossChatHealthCheck(workspaceRoot, input = {}) {
340
+ const cliDir = resolveBossChatCliDir(workspaceRoot);
341
+ const cliPath = resolveBossChatCliPath(workspaceRoot);
342
+ const configResolution = resolveBossChatScreenConfig(workspaceRoot);
343
+ const resolvedPort = parsePositiveInteger(input.port)
344
+ || (configResolution.ok ? configResolution.config.debugPort : 9222);
345
+ if (!cliDir || !cliPath) {
346
+ return {
347
+ status: "FAILED",
348
+ error: {
349
+ code: "BOSS_CHAT_CLI_MISSING",
350
+ message: "未找到 vendored boss-chat CLI。"
351
+ }
352
+ };
353
+ }
354
+ if (!configResolution.ok) {
355
+ return {
356
+ status: "FAILED",
357
+ error: configResolution.error,
358
+ config_path: configResolution.config_path,
359
+ config_dir: configResolution.config_dir,
360
+ cli_dir: cliDir,
361
+ cli_path: cliPath
362
+ };
363
+ }
364
+ return {
365
+ status: "OK",
366
+ server: "boss-chat",
367
+ cli_dir: cliDir,
368
+ cli_path: cliPath,
369
+ config_path: configResolution.config_path,
370
+ debug_port: resolvedPort,
371
+ shared_llm_config: true
372
+ };
373
+ }
374
+
375
+ export async function startBossChatRun({ workspaceRoot, input = {} }) {
376
+ const missingFields = getMissingBossChatStartFields(input);
377
+ if (missingFields.length > 0) {
378
+ const prepared = await prepareBossChatRun({ workspaceRoot, input });
379
+ if (prepared?.status === "FAILED") return prepared;
380
+ const pendingQuestions = Array.isArray(prepared?.pending_questions)
381
+ ? prepared.pending_questions.filter((item) => missingFields.includes(String(item?.field || "")))
382
+ : [];
383
+ return {
384
+ ...prepared,
385
+ status: "NEED_INPUT",
386
+ required_fields: CHAT_REQUIRED_FIELDS.slice(),
387
+ missing_fields: missingFields,
388
+ pending_questions: pendingQuestions,
389
+ message: prepared?.message
390
+ || "已获取 Boss 聊天页岗位列表,请先补齐 job / start_from / target_count / criteria。"
391
+ };
392
+ }
393
+ return (await spawnBossChatCli({ workspaceRoot, command: "start-run", input })).payload;
394
+ }
395
+
396
+ export async function prepareBossChatRun({ workspaceRoot, input = {} }) {
397
+ return (await spawnBossChatCli({ workspaceRoot, command: "prepare-run", input })).payload;
398
+ }
399
+
400
+ export async function getBossChatRun({ workspaceRoot, input = {} }) {
401
+ return (await spawnBossChatCli({ workspaceRoot, command: "get-run", input })).payload;
402
+ }
403
+
404
+ export async function pauseBossChatRun({ workspaceRoot, input = {} }) {
405
+ return (await spawnBossChatCli({ workspaceRoot, command: "pause-run", input })).payload;
406
+ }
407
+
408
+ export async function resumeBossChatRun({ workspaceRoot, input = {} }) {
409
+ return (await spawnBossChatCli({ workspaceRoot, command: "resume-run", input })).payload;
410
+ }
411
+
412
+ export async function cancelBossChatRun({ workspaceRoot, input = {} }) {
413
+ return (await spawnBossChatCli({ workspaceRoot, command: "cancel-run", input })).payload;
414
+ }
415
+
416
+ export async function runBossChatSync({ workspaceRoot, input = {}, pollMs = DEFAULT_BOSS_CHAT_POLL_MS }) {
417
+ const accepted = await startBossChatRun({ workspaceRoot, input });
418
+ if (accepted?.status !== "ACCEPTED" || !normalizeText(accepted.run_id)) {
419
+ return accepted;
420
+ }
421
+ const runId = normalizeText(accepted.run_id);
422
+ while (true) {
423
+ await sleep(pollMs);
424
+ const statusPayload = await getBossChatRun({
425
+ workspaceRoot,
426
+ input: {
427
+ profile: input.profile,
428
+ runId
429
+ }
430
+ });
431
+ const runState = normalizeText(statusPayload?.run?.state).toLowerCase();
432
+ if (BOSS_CHAT_TERMINAL_STATES.has(runState)) {
433
+ return statusPayload;
434
+ }
435
+ }
436
+ }