@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
package/src/run-state.js CHANGED
@@ -18,6 +18,7 @@ export const RUN_STAGE_PAGE_READY = "page_ready";
18
18
  export const RUN_STAGE_JOB_LIST = "job_list";
19
19
  export const RUN_STAGE_SEARCH = "search";
20
20
  export const RUN_STAGE_SCREEN = "screen";
21
+ export const RUN_STAGE_CHAT_FOLLOWUP = "chat_followup";
21
22
  export const RUN_STAGE_FINALIZE = "finalize";
22
23
 
23
24
  const DEFAULT_HEARTBEAT_INTERVAL_MS = 120_000;
@@ -38,6 +39,7 @@ const VALID_RUN_STAGES = new Set([
38
39
  RUN_STAGE_JOB_LIST,
39
40
  RUN_STAGE_SEARCH,
40
41
  RUN_STAGE_SCREEN,
42
+ RUN_STAGE_CHAT_FOLLOWUP,
41
43
  RUN_STAGE_FINALIZE
42
44
  ]);
43
45
 
@@ -158,6 +160,9 @@ function defaultResume(resume = {}) {
158
160
  checkpoint_path: normalizeMessage(resume?.checkpoint_path || ""),
159
161
  pause_control_path: normalizeMessage(resume?.pause_control_path || ""),
160
162
  output_csv: normalizeMessage(resume?.output_csv || ""),
163
+ follow_up_phase: normalizeMessage(resume?.follow_up_phase || ""),
164
+ chat_run_id: normalizeMessage(resume?.chat_run_id || ""),
165
+ chat_state: normalizeMessage(resume?.chat_state || ""),
161
166
  resume_count: Number.isInteger(resume?.resume_count) && resume.resume_count >= 0 ? resume.resume_count : 0,
162
167
  last_resumed_at: normalizeMessage(resume?.last_resumed_at || ""),
163
168
  last_paused_at: normalizeMessage(resume?.last_paused_at || "")
@@ -446,6 +446,74 @@ async function testScreenCliShouldPassLatestPageScopeArgument() {
446
446
  }
447
447
  }
448
448
 
449
+ async function testScreenCliShouldPassInputSummaryArgument() {
450
+ const previousHome = process.env.BOSS_RECOMMEND_HOME;
451
+ const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-input-summary-home-"));
452
+ const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-input-summary-workspace-"));
453
+ const cliDir = path.join(workspaceRoot, "boss-recommend-screen-cli");
454
+ fs.mkdirSync(cliDir, { recursive: true });
455
+ const cliPath = path.join(cliDir, "boss-recommend-screen-cli.cjs");
456
+ fs.writeFileSync(
457
+ cliPath,
458
+ [
459
+ "#!/usr/bin/env node",
460
+ "console.log(JSON.stringify({",
461
+ " status: 'COMPLETED',",
462
+ " result: {",
463
+ " processed_count: 0,",
464
+ " passed_count: 0,",
465
+ " skipped_count: 0,",
466
+ " argv: process.argv.slice(2),",
467
+ " resume_source: 'network',",
468
+ " active_tab_status: '0'",
469
+ " }",
470
+ "}));"
471
+ ].join("\n"),
472
+ "utf8"
473
+ );
474
+
475
+ process.env.BOSS_RECOMMEND_HOME = tempHome;
476
+ fs.writeFileSync(path.join(tempHome, "screening-config.json"), JSON.stringify({
477
+ baseUrl: "https://api.openai.com/v1",
478
+ apiKey: "sk-valid-test",
479
+ model: "gpt-4.1-mini"
480
+ }, null, 2));
481
+
482
+ try {
483
+ const inputSummary = {
484
+ instruction: "测试输入摘要",
485
+ search_params: { school_tag: ["985"], gender: "男" },
486
+ screen_params: { criteria: "有 MCP 经验" }
487
+ };
488
+ const result = await runRecommendScreenCli({
489
+ workspaceRoot,
490
+ screenParams: {
491
+ criteria: "有 MCP 经验",
492
+ target_count: null,
493
+ post_action: "none",
494
+ max_greet_count: null
495
+ },
496
+ inputSummary
497
+ });
498
+ assert.equal(result.ok, true);
499
+ const argv = result.summary?.argv || [];
500
+ const summaryIndex = argv.indexOf("--input-summary-json");
501
+ assert.equal(summaryIndex >= 0, true);
502
+ const parsedSummary = JSON.parse(String(argv[summaryIndex + 1] || "{}"));
503
+ assert.equal(parsedSummary.instruction, "测试输入摘要");
504
+ assert.equal(parsedSummary.search_params?.gender, "男");
505
+ assert.equal(parsedSummary.screen_params?.criteria, "有 MCP 经验");
506
+ } finally {
507
+ if (previousHome === undefined) {
508
+ delete process.env.BOSS_RECOMMEND_HOME;
509
+ } else {
510
+ process.env.BOSS_RECOMMEND_HOME = previousHome;
511
+ }
512
+ fs.rmSync(tempHome, { recursive: true, force: true });
513
+ fs.rmSync(workspaceRoot, { recursive: true, force: true });
514
+ }
515
+ }
516
+
449
517
  async function main() {
450
518
  await testRunProcessHeartbeatAndOutput();
451
519
  await testRunProcessAbortSignal();
@@ -463,6 +531,7 @@ async function main() {
463
531
  await testSearchCliShouldPassLatestPageScopeWithoutCalibration();
464
532
  await testScreenCliShouldPassPageScopeArgument();
465
533
  await testScreenCliShouldPassLatestPageScopeArgument();
534
+ await testScreenCliShouldPassInputSummaryArgument();
466
535
  console.log("adapters runtime tests passed");
467
536
  }
468
537
 
@@ -0,0 +1,399 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ import {
7
+ cancelBossChatRun,
8
+ getBossChatHealthCheck,
9
+ getBossChatRun,
10
+ pauseBossChatRun,
11
+ resumeBossChatRun,
12
+ startBossChatRun
13
+ } from "./boss-chat.js";
14
+ import { __testables as cliTestables } from "./cli.js";
15
+ import { __testables as indexTestables } from "./index.js";
16
+
17
+ const { handleRequest } = indexTestables;
18
+
19
+ const TOOL_BOSS_CHAT_HEALTH_CHECK = "boss_chat_health_check";
20
+ const TOOL_BOSS_CHAT_START_RUN = "start_boss_chat_run";
21
+ const TOOL_BOSS_CHAT_GET_RUN = "get_boss_chat_run";
22
+ const TOOL_BOSS_CHAT_PAUSE_RUN = "pause_boss_chat_run";
23
+ const TOOL_BOSS_CHAT_RESUME_RUN = "resume_boss_chat_run";
24
+ const TOOL_BOSS_CHAT_CANCEL_RUN = "cancel_boss_chat_run";
25
+
26
+ function makeToolCall(id, name, args = {}) {
27
+ return {
28
+ jsonrpc: "2.0",
29
+ id,
30
+ method: "tools/call",
31
+ params: {
32
+ name,
33
+ arguments: args
34
+ }
35
+ };
36
+ }
37
+
38
+ async function callTool(workspaceRoot, name, args = {}, id = 1) {
39
+ const response = await handleRequest(makeToolCall(id, name, args), workspaceRoot);
40
+ return response?.result?.structuredContent;
41
+ }
42
+
43
+ function createBossChatTestWorkspace() {
44
+ const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-boss-chat-"));
45
+ const configDir = path.join(workspaceRoot, "config");
46
+ const cliDir = path.join(workspaceRoot, "boss-chat-cli", "src");
47
+ fs.mkdirSync(configDir, { recursive: true });
48
+ fs.mkdirSync(cliDir, { recursive: true });
49
+
50
+ fs.writeFileSync(path.join(configDir, "screening-config.json"), JSON.stringify({
51
+ baseUrl: "https://api.example.com/v1",
52
+ apiKey: "sk-test-key",
53
+ model: "gpt-4.1-mini",
54
+ debugPort: 9666
55
+ }, null, 2));
56
+
57
+ fs.writeFileSync(path.join(cliDir, "cli.js"), [
58
+ "#!/usr/bin/env node",
59
+ "const fs = require('node:fs');",
60
+ "const path = require('node:path');",
61
+ "const cwd = process.cwd();",
62
+ "const statePath = path.join(cwd, '.boss-chat', 'stub-state.json');",
63
+ "fs.mkdirSync(path.dirname(statePath), { recursive: true });",
64
+ "const raw = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf8') : '{}';",
65
+ "const state = JSON.parse(raw || '{}');",
66
+ "state.counter = Number.isInteger(state.counter) ? state.counter : 0;",
67
+ "state.runs = state.runs && typeof state.runs === 'object' ? state.runs : {};",
68
+ "state.get_calls = state.get_calls && typeof state.get_calls === 'object' ? state.get_calls : {};",
69
+ "const argv = process.argv.slice(2);",
70
+ "const command = String(argv[0] || '').trim();",
71
+ "const options = {};",
72
+ "for (let index = 1; index < argv.length; index += 1) {",
73
+ " const token = String(argv[index] || '');",
74
+ " if (!token.startsWith('--')) continue;",
75
+ " const key = token.slice(2);",
76
+ " const next = argv[index + 1];",
77
+ " if (next && !String(next).startsWith('--')) {",
78
+ " options[key] = String(next);",
79
+ " index += 1;",
80
+ " } else {",
81
+ " options[key] = true;",
82
+ " }",
83
+ "}",
84
+ "function saveAndPrint(payload) {",
85
+ " fs.writeFileSync(statePath, JSON.stringify(state, null, 2));",
86
+ " process.stdout.write(`${JSON.stringify(payload)}\\n`);",
87
+ "}",
88
+ "if (command === 'prepare-run') {",
89
+ " state.last_prepare_args = options;",
90
+ " saveAndPrint({",
91
+ " status: 'NEED_INPUT',",
92
+ " stage: 'chat_run_setup',",
93
+ " page_url: 'https://www.zhipin.com/web/chat/index',",
94
+ " required_fields: ['job', 'start_from', 'target_count', 'criteria'],",
95
+ " job_options: [",
96
+ " { index: 1, label: '算法工程师', value: '算法工程师', active: true },",
97
+ " { index: 2, label: '大模型算法', value: '大模型算法', active: false }",
98
+ " ],",
99
+ " pending_questions: [",
100
+ " { field: 'job', question: '请选择岗位(必须从岗位列表中选择)', required: true },",
101
+ " { field: 'start_from', question: '请选择起始范围', required: true },",
102
+ " { field: 'target_count', question: '请输入目标数量(正整数)', required: true },",
103
+ " { field: 'criteria', question: '请输入筛选标准(自然语言)', required: true }",
104
+ " ],",
105
+ " message: 'prepared'",
106
+ " });",
107
+ " process.exit(0);",
108
+ "}",
109
+ "if (command === 'start-run') {",
110
+ " state.counter += 1;",
111
+ " const runId = `chat-${state.counter}`;",
112
+ " state.last_start_args = options;",
113
+ " state.runs[runId] = { state: 'queued' };",
114
+ " state.get_calls[runId] = 0;",
115
+ " saveAndPrint({ status: 'ACCEPTED', run_id: runId, message: 'chat started' });",
116
+ " process.exit(0);",
117
+ "}",
118
+ "const runId = String(options['run-id'] || '');",
119
+ "const current = state.runs[runId] || { state: 'queued' };",
120
+ "if (command === 'get-run') {",
121
+ " state.get_calls[runId] = (state.get_calls[runId] || 0) + 1;",
122
+ " if (!['paused', 'canceled'].includes(current.state)) {",
123
+ " current.state = state.get_calls[runId] >= 2 ? 'completed' : 'running';",
124
+ " }",
125
+ " state.runs[runId] = current;",
126
+ " saveAndPrint({",
127
+ " status: 'RUN_STATUS',",
128
+ " run: {",
129
+ " runId,",
130
+ " state: current.state,",
131
+ " lastMessage: `state=${current.state}`,",
132
+ " progress: { inspected: state.get_calls[runId], passed: current.state === 'completed' ? 1 : 0, requested: current.state === 'completed' ? 1 : 0, skipped: 0, errors: 0 },",
133
+ " result: current.state === 'completed' ? { requested_count: 1 } : null",
134
+ " }",
135
+ " });",
136
+ " process.exit(0);",
137
+ "}",
138
+ "if (command === 'pause-run') {",
139
+ " current.state = 'paused';",
140
+ " state.runs[runId] = current;",
141
+ " saveAndPrint({ status: 'PAUSE_REQUESTED', run: { runId, state: 'paused' } });",
142
+ " process.exit(0);",
143
+ "}",
144
+ "if (command === 'resume-run') {",
145
+ " current.state = 'running';",
146
+ " state.runs[runId] = current;",
147
+ " saveAndPrint({ status: 'RESUME_REQUESTED', run: { runId, state: 'running' } });",
148
+ " process.exit(0);",
149
+ "}",
150
+ "if (command === 'cancel-run') {",
151
+ " current.state = 'canceled';",
152
+ " state.runs[runId] = current;",
153
+ " saveAndPrint({ status: 'CANCEL_REQUESTED', run: { runId, state: 'canceled' } });",
154
+ " process.exit(0);",
155
+ "}",
156
+ "saveAndPrint({ status: 'FAILED', error: { code: 'UNKNOWN_COMMAND', message: command || 'missing command' } });",
157
+ "process.exit(1);"
158
+ ].join("\n"), "utf8");
159
+
160
+ return workspaceRoot;
161
+ }
162
+
163
+ function readStubState(workspaceRoot) {
164
+ const statePath = path.join(workspaceRoot, ".boss-chat", "stub-state.json");
165
+ return JSON.parse(fs.readFileSync(statePath, "utf8"));
166
+ }
167
+
168
+ async function withBossChatWorkspace(testFn) {
169
+ const workspaceRoot = createBossChatTestWorkspace();
170
+ const previousScreenConfig = process.env.BOSS_RECOMMEND_SCREEN_CONFIG;
171
+ process.env.BOSS_RECOMMEND_SCREEN_CONFIG = path.join(workspaceRoot, "config", "screening-config.json");
172
+ try {
173
+ await testFn(workspaceRoot);
174
+ } finally {
175
+ if (previousScreenConfig === undefined) {
176
+ delete process.env.BOSS_RECOMMEND_SCREEN_CONFIG;
177
+ } else {
178
+ process.env.BOSS_RECOMMEND_SCREEN_CONFIG = previousScreenConfig;
179
+ }
180
+ fs.rmSync(workspaceRoot, { recursive: true, force: true });
181
+ }
182
+ }
183
+
184
+ async function captureConsoleLogs(fn) {
185
+ const messages = [];
186
+ const originalLog = console.log;
187
+ console.log = (...args) => {
188
+ messages.push(args.join(" "));
189
+ };
190
+ try {
191
+ await fn();
192
+ } finally {
193
+ console.log = originalLog;
194
+ }
195
+ return messages;
196
+ }
197
+
198
+ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
199
+ await withBossChatWorkspace(async (workspaceRoot) => {
200
+ const health = getBossChatHealthCheck(workspaceRoot);
201
+ assert.equal(health.status, "OK");
202
+ assert.equal(health.shared_llm_config, true);
203
+ assert.equal(health.debug_port, 9666);
204
+
205
+ const preflight = await startBossChatRun({
206
+ workspaceRoot,
207
+ input: {}
208
+ });
209
+ assert.equal(preflight.status, "NEED_INPUT");
210
+ assert.deepEqual(preflight.required_fields, ["job", "start_from", "target_count", "criteria"]);
211
+ assert.equal(Array.isArray(preflight.job_options), true);
212
+ assert.equal(preflight.job_options.length, 2);
213
+
214
+ const stateAfterPrepare = readStubState(workspaceRoot);
215
+ assert.equal(stateAfterPrepare.last_prepare_args.profile, "default");
216
+ assert.equal(stateAfterPrepare.last_prepare_args.port, "9666");
217
+ assert.equal(stateAfterPrepare.last_prepare_args.baseurl, "https://api.example.com/v1");
218
+ assert.equal(stateAfterPrepare.last_prepare_args.apikey, "sk-test-key");
219
+ assert.equal(stateAfterPrepare.last_prepare_args.model, "gpt-4.1-mini");
220
+
221
+ const started = await startBossChatRun({
222
+ workspaceRoot,
223
+ input: {
224
+ profile: "default",
225
+ job: "算法工程师",
226
+ start_from: "unread",
227
+ criteria: "有 AI Agent 经验",
228
+ target_count: 2
229
+ }
230
+ });
231
+ assert.equal(started.status, "ACCEPTED");
232
+ assert.equal(Boolean(started.run_id), true);
233
+
234
+ const stateAfterStart = readStubState(workspaceRoot);
235
+ assert.equal(stateAfterStart.last_start_args.profile, "default");
236
+ assert.equal(stateAfterStart.last_start_args.job, "算法工程师");
237
+ assert.equal(stateAfterStart.last_start_args["start-from"], "unread");
238
+ assert.equal(stateAfterStart.last_start_args.criteria, "有 AI Agent 经验");
239
+ assert.equal(stateAfterStart.last_start_args.targetCount, "2");
240
+ assert.equal(stateAfterStart.last_start_args.baseurl, "https://api.example.com/v1");
241
+ assert.equal(stateAfterStart.last_start_args.apikey, "sk-test-key");
242
+ assert.equal(stateAfterStart.last_start_args.model, "gpt-4.1-mini");
243
+ assert.equal(stateAfterStart.last_start_args.port, "9666");
244
+
245
+ const running = await getBossChatRun({
246
+ workspaceRoot,
247
+ input: {
248
+ profile: "default",
249
+ run_id: started.run_id
250
+ }
251
+ });
252
+ assert.equal(running.run.state, "running");
253
+
254
+ const paused = await pauseBossChatRun({
255
+ workspaceRoot,
256
+ input: {
257
+ profile: "default",
258
+ run_id: started.run_id
259
+ }
260
+ });
261
+ assert.equal(paused.run.state, "paused");
262
+
263
+ const resumed = await resumeBossChatRun({
264
+ workspaceRoot,
265
+ input: {
266
+ profile: "default",
267
+ run_id: started.run_id
268
+ }
269
+ });
270
+ assert.equal(resumed.run.state, "running");
271
+
272
+ const canceled = await cancelBossChatRun({
273
+ workspaceRoot,
274
+ input: {
275
+ profile: "default",
276
+ run_id: started.run_id
277
+ }
278
+ });
279
+ assert.equal(canceled.run.state, "canceled");
280
+ });
281
+ }
282
+
283
+ async function testBossChatMcpToolsShouldValidateAndRoute() {
284
+ await withBossChatWorkspace(async (workspaceRoot) => {
285
+ const needInput = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {}, 11);
286
+ assert.equal(needInput.status, "NEED_INPUT");
287
+ assert.deepEqual(needInput.required_fields, ["job", "start_from", "target_count", "criteria"]);
288
+ assert.equal(Array.isArray(needInput.job_options), true);
289
+ assert.equal(needInput.job_options.length, 2);
290
+
291
+ const invalidStartResponse = await handleRequest(
292
+ makeToolCall(11, TOOL_BOSS_CHAT_START_RUN, {
293
+ start_from: "invalid-value"
294
+ }),
295
+ workspaceRoot
296
+ );
297
+ assert.equal(invalidStartResponse.error.code, -32602);
298
+
299
+ const invalidGetResponse = await handleRequest(
300
+ makeToolCall(12, TOOL_BOSS_CHAT_GET_RUN, {}),
301
+ workspaceRoot
302
+ );
303
+ assert.equal(invalidGetResponse.error.code, -32602);
304
+
305
+ const health = await callTool(workspaceRoot, TOOL_BOSS_CHAT_HEALTH_CHECK, {}, 13);
306
+ assert.equal(health.status, "OK");
307
+
308
+ const started = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {
309
+ job: "算法工程师",
310
+ start_from: "unread",
311
+ criteria: "有 AI Agent 经验",
312
+ target_count: 2
313
+ }, 14);
314
+ assert.equal(started.status, "ACCEPTED");
315
+
316
+ const running = await callTool(workspaceRoot, TOOL_BOSS_CHAT_GET_RUN, {
317
+ run_id: started.run_id,
318
+ profile: "default"
319
+ }, 15);
320
+ assert.equal(running.run.state, "running");
321
+
322
+ const paused = await callTool(workspaceRoot, TOOL_BOSS_CHAT_PAUSE_RUN, {
323
+ run_id: started.run_id,
324
+ profile: "default"
325
+ }, 16);
326
+ assert.equal(paused.run.state, "paused");
327
+
328
+ const resumed = await callTool(workspaceRoot, TOOL_BOSS_CHAT_RESUME_RUN, {
329
+ run_id: started.run_id,
330
+ profile: "default"
331
+ }, 17);
332
+ assert.equal(resumed.run.state, "running");
333
+
334
+ const canceled = await callTool(workspaceRoot, TOOL_BOSS_CHAT_CANCEL_RUN, {
335
+ run_id: started.run_id,
336
+ profile: "default"
337
+ }, 18);
338
+ assert.equal(canceled.run.state, "canceled");
339
+ });
340
+ }
341
+
342
+ async function testBossChatCliShouldSupportRunAndFollowUpParsing() {
343
+ const followUpJson = cliTestables.getRunFollowUp({
344
+ "follow-up-json": JSON.stringify({
345
+ chat: {
346
+ criteria: "有 AI Agent 经验",
347
+ start_from: "unread",
348
+ target_count: 2
349
+ }
350
+ })
351
+ });
352
+ assert.equal(followUpJson.chat.criteria, "有 AI Agent 经验");
353
+ assert.equal(followUpJson.chat.target_count, 2);
354
+
355
+ const tempFile = path.join(os.tmpdir(), `boss-recommend-follow-up-${Date.now()}.json`);
356
+ fs.writeFileSync(tempFile, JSON.stringify({
357
+ chat: {
358
+ criteria: "熟悉 MCP",
359
+ start_from: "all",
360
+ target_count: 3
361
+ }
362
+ }, null, 2));
363
+ try {
364
+ const followUpFile = cliTestables.getRunFollowUp({
365
+ "follow-up-file": tempFile
366
+ });
367
+ assert.equal(followUpFile.chat.criteria, "熟悉 MCP");
368
+ assert.equal(followUpFile.chat.start_from, "all");
369
+ } finally {
370
+ fs.rmSync(tempFile, { force: true });
371
+ }
372
+
373
+ await withBossChatWorkspace(async (workspaceRoot) => {
374
+ const logs = await captureConsoleLogs(async () => {
375
+ await cliTestables.runBossChatCliCommand("run", {
376
+ "workspace-root": workspaceRoot,
377
+ job: "算法工程师",
378
+ "start-from": "unread",
379
+ criteria: "有 AI Agent 经验",
380
+ targetCount: "2"
381
+ });
382
+ });
383
+ assert.equal(logs.length > 0, true);
384
+ const payload = JSON.parse(logs[0]);
385
+ assert.equal(payload.status, "ACCEPTED");
386
+ assert.equal(typeof payload.run_id, "string");
387
+ const state = readStubState(workspaceRoot);
388
+ assert.equal(state.get_calls[payload.run_id] || 0, 0);
389
+ });
390
+ }
391
+
392
+ async function main() {
393
+ await testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli();
394
+ await testBossChatMcpToolsShouldValidateAndRoute();
395
+ await testBossChatCliShouldSupportRunAndFollowUpParsing();
396
+ console.log("boss-chat tests passed");
397
+ }
398
+
399
+ await main();