@reconcrap/boss-recommend-mcp 1.2.9 → 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 (36) 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/parser.js +4 -5
  11. package/src/pipeline.js +605 -8
  12. package/src/run-state.js +5 -0
  13. package/src/test-adapters-runtime.js +69 -0
  14. package/src/test-boss-chat.js +399 -0
  15. package/src/test-index-async.js +238 -4
  16. package/src/test-parser.js +33 -6
  17. package/src/test-pipeline.js +408 -1
  18. package/vendor/boss-chat-cli/README.md +134 -0
  19. package/vendor/boss-chat-cli/package.json +53 -0
  20. package/vendor/boss-chat-cli/src/app.js +769 -0
  21. package/vendor/boss-chat-cli/src/browser/chat-page.js +2681 -0
  22. package/vendor/boss-chat-cli/src/cli.js +1350 -0
  23. package/vendor/boss-chat-cli/src/mcp/server.js +149 -0
  24. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +193 -0
  25. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +260 -0
  26. package/vendor/boss-chat-cli/src/runtime/interaction.js +102 -0
  27. package/vendor/boss-chat-cli/src/runtime/run-control.js +102 -0
  28. package/vendor/boss-chat-cli/src/services/chrome-client.js +97 -0
  29. package/vendor/boss-chat-cli/src/services/llm.js +352 -0
  30. package/vendor/boss-chat-cli/src/services/profile-store.js +157 -0
  31. package/vendor/boss-chat-cli/src/services/report-store.js +19 -0
  32. package/vendor/boss-chat-cli/src/services/resume-capture.js +554 -0
  33. package/vendor/boss-chat-cli/src/services/state-store.js +217 -0
  34. package/vendor/boss-chat-cli/src/utils/customer-key.js +82 -0
  35. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +902 -56
  36. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +387 -1
@@ -21,6 +21,17 @@ function sleep(ms) {
21
21
  return new Promise((resolve) => setTimeout(resolve, ms));
22
22
  }
23
23
 
24
+ function createFollowUpChat(overrides = {}) {
25
+ return {
26
+ chat: {
27
+ criteria: "有 AI Agent 经验",
28
+ start_from: "unread",
29
+ target_count: 3,
30
+ ...overrides
31
+ }
32
+ };
33
+ }
34
+
24
35
  function makeToolCall(id, name, args = {}) {
25
36
  return {
26
37
  jsonrpc: "2.0",
@@ -57,14 +68,25 @@ async function waitForRunState(runId, acceptedStates, timeoutMs = 6000) {
57
68
  throw new Error(`Timed out waiting run state (${Array.from(accepted).join(", ")}) for run_id=${runId}`);
58
69
  }
59
70
 
60
- async function startAcceptedRun(instruction, idSeed = 1) {
71
+ async function waitForRunSnapshot(runId, predicate, timeoutMs = 6000) {
72
+ const deadline = Date.now() + timeoutMs;
73
+ while (Date.now() < deadline) {
74
+ const payload = await callTool(TOOL_GET_RUN, { run_id: runId }, 1002);
75
+ if (predicate(payload?.run)) return payload.run;
76
+ await sleep(80);
77
+ }
78
+ throw new Error(`Timed out waiting run snapshot for run_id=${runId}`);
79
+ }
80
+
81
+ async function startAcceptedRun(instruction, idSeed = 1, extraArgs = {}) {
61
82
  const payload = await callTool(TOOL_START_RUN, {
62
83
  instruction,
63
84
  confirmation: {
64
85
  job_confirmed: true,
65
86
  job_value: "mock job",
66
87
  final_confirmed: true
67
- }
88
+ },
89
+ ...extraArgs
68
90
  }, idSeed);
69
91
  assert.equal(payload.status, "ACCEPTED");
70
92
  assert.equal(typeof payload.run_id, "string");
@@ -73,6 +95,7 @@ async function startAcceptedRun(instruction, idSeed = 1) {
73
95
 
74
96
  function setupPipelineMock() {
75
97
  const checkpointStore = new Map();
98
+ const chatStateStore = new Map();
76
99
  setRunPipelineImplForTests(async (input, _deps, runtime) => {
77
100
  if (input.confirmation?.job_confirmed !== true) {
78
101
  return {
@@ -128,15 +151,172 @@ function setupPipelineMock() {
128
151
  }
129
152
 
130
153
  checkpointStore.delete(checkpointPath);
131
- return {
154
+ const recommendResult = {
132
155
  status: "COMPLETED",
156
+ search_params: {
157
+ school_tag: ["985"]
158
+ },
159
+ screen_params: {
160
+ criteria: "mock criteria",
161
+ target_count: 12,
162
+ post_action: "favorite",
163
+ max_greet_count: null
164
+ },
133
165
  result: {
134
166
  processed_count: total,
135
167
  passed_count: Math.floor(total / 3),
136
168
  skipped_count: total - Math.floor(total / 3),
137
169
  greet_count: 0,
138
170
  output_csv: outputCsv,
139
- completion_reason: "screen_completed"
171
+ completion_reason: "screen_completed",
172
+ selected_job: {
173
+ title: "mock job"
174
+ }
175
+ }
176
+ };
177
+
178
+ if (!input.followUp?.chat) {
179
+ return recommendResult;
180
+ }
181
+
182
+ runtime?.onStage?.({ stage: "chat_followup", message: "chat follow-up running" });
183
+ const chatCheckpointKey = `${checkpointPath}:chat`;
184
+ let chatState = "running";
185
+ if (input.resume?.resume === true && input.resume?.follow_up_phase === "chat_followup") {
186
+ const storedState = String(chatStateStore.get(chatCheckpointKey) || "running").trim().toLowerCase();
187
+ chatState = storedState === "paused" ? "running" : storedState;
188
+ }
189
+
190
+ runtime?.onFollowUp?.({
191
+ stage: "chat_followup",
192
+ last_message: "chat follow-up running",
193
+ recommend_payload: recommendResult,
194
+ recommend_result: recommendResult.result,
195
+ follow_up: {
196
+ chat: {
197
+ run_id: "mock-chat-run",
198
+ state: chatState,
199
+ input: {
200
+ ...input.followUp.chat
201
+ },
202
+ progress: {
203
+ inspected: 1,
204
+ passed: 0,
205
+ requested: 0,
206
+ skipped: 1,
207
+ errors: 0
208
+ }
209
+ }
210
+ }
211
+ });
212
+
213
+ for (let chatTick = 0; chatTick < 20; chatTick += 1) {
214
+ await sleep(25);
215
+ if (runtime?.isCancelRequested?.() === true) {
216
+ chatStateStore.set(chatCheckpointKey, "canceled");
217
+ runtime?.onFollowUp?.({
218
+ stage: "chat_followup",
219
+ last_message: "chat follow-up canceled",
220
+ recommend_payload: recommendResult,
221
+ recommend_result: recommendResult.result,
222
+ follow_up: {
223
+ chat: {
224
+ run_id: "mock-chat-run",
225
+ state: "canceled",
226
+ input: {
227
+ ...input.followUp.chat
228
+ }
229
+ }
230
+ }
231
+ });
232
+ return {
233
+ ...recommendResult,
234
+ status: "PAUSED",
235
+ partial_result: recommendResult.result,
236
+ follow_up: {
237
+ chat: {
238
+ run_id: "mock-chat-run",
239
+ state: "canceled",
240
+ input: {
241
+ ...input.followUp.chat
242
+ }
243
+ }
244
+ }
245
+ };
246
+ }
247
+ if (runtime?.isPauseRequested?.() === true) {
248
+ chatStateStore.set(chatCheckpointKey, "paused");
249
+ runtime?.onFollowUp?.({
250
+ stage: "chat_followup",
251
+ last_message: "chat follow-up paused",
252
+ recommend_payload: recommendResult,
253
+ recommend_result: recommendResult.result,
254
+ follow_up: {
255
+ chat: {
256
+ run_id: "mock-chat-run",
257
+ state: "paused",
258
+ input: {
259
+ ...input.followUp.chat
260
+ }
261
+ }
262
+ }
263
+ });
264
+ return {
265
+ ...recommendResult,
266
+ status: "PAUSED",
267
+ partial_result: recommendResult.result,
268
+ follow_up: {
269
+ chat: {
270
+ run_id: "mock-chat-run",
271
+ state: "paused",
272
+ input: {
273
+ ...input.followUp.chat
274
+ }
275
+ }
276
+ }
277
+ };
278
+ }
279
+ }
280
+
281
+ chatStateStore.delete(chatCheckpointKey);
282
+ runtime?.onFollowUp?.({
283
+ stage: "chat_followup",
284
+ last_message: "chat follow-up completed",
285
+ recommend_payload: recommendResult,
286
+ recommend_result: recommendResult.result,
287
+ follow_up: {
288
+ chat: {
289
+ run_id: "mock-chat-run",
290
+ state: "completed",
291
+ input: {
292
+ ...input.followUp.chat
293
+ },
294
+ progress: {
295
+ inspected: 3,
296
+ passed: 1,
297
+ requested: 1,
298
+ skipped: 2,
299
+ errors: 0
300
+ },
301
+ result: {
302
+ requested_count: 1
303
+ }
304
+ }
305
+ }
306
+ });
307
+ return {
308
+ ...recommendResult,
309
+ follow_up: {
310
+ chat: {
311
+ run_id: "mock-chat-run",
312
+ state: "completed",
313
+ input: {
314
+ ...input.followUp.chat
315
+ },
316
+ result: {
317
+ requested_count: 1
318
+ }
319
+ }
140
320
  }
141
321
  };
142
322
  });
@@ -236,6 +416,58 @@ async function testCancelRunningRunKeepsCsv() {
236
416
  assert.equal(Boolean(canceledRun.resume?.output_csv), true);
237
417
  }
238
418
 
419
+ async function testPauseAndResumeDuringChatFollowUp() {
420
+ const runId = await startAcceptedRun("run with follow-up chat pause resume", 51, {
421
+ follow_up: createFollowUpChat()
422
+ });
423
+ const runningChatRun = await waitForRunSnapshot(
424
+ runId,
425
+ (run) => run?.stage === "chat_followup" && run?.state === "running" && run?.result?.follow_up?.chat?.state === "running"
426
+ );
427
+ assert.equal(runningChatRun.result?.result?.processed_count, 12);
428
+ assert.equal(runningChatRun.result?.follow_up?.chat?.run_id, "mock-chat-run");
429
+
430
+ const pausePayload = await callTool(TOOL_PAUSE_RUN, { run_id: runId }, 52);
431
+ assert.equal(pausePayload.status, "PAUSE_REQUESTED");
432
+
433
+ const pausedRun = await waitForRunSnapshot(
434
+ runId,
435
+ (run) => run?.state === "paused" && run?.stage === "chat_followup"
436
+ );
437
+ assert.equal(pausedRun.result?.follow_up?.chat?.state, "paused");
438
+ assert.equal(pausedRun.result?.result?.processed_count, 12);
439
+
440
+ const resumePayload = await callTool(TOOL_RESUME_RUN, { run_id: runId }, 53);
441
+ assert.equal(resumePayload.status, "RESUME_REQUESTED");
442
+
443
+ const completedRun = await waitForRunSnapshot(
444
+ runId,
445
+ (run) => run?.state === "completed" && run?.result?.follow_up?.chat?.state === "completed"
446
+ );
447
+ assert.equal(completedRun.result?.follow_up?.chat?.result?.requested_count, 1);
448
+ assert.equal(completedRun.result?.result?.processed_count, 12);
449
+ }
450
+
451
+ async function testCancelDuringChatFollowUp() {
452
+ const runId = await startAcceptedRun("run with follow-up chat cancel", 61, {
453
+ follow_up: createFollowUpChat()
454
+ });
455
+ await waitForRunSnapshot(
456
+ runId,
457
+ (run) => run?.stage === "chat_followup" && run?.state === "running" && run?.result?.follow_up?.chat?.state === "running"
458
+ );
459
+
460
+ const cancelPayload = await callTool(TOOL_CANCEL_RUN, { run_id: runId }, 62);
461
+ assert.equal(cancelPayload.status, "CANCEL_REQUESTED");
462
+
463
+ const canceledRun = await waitForRunSnapshot(
464
+ runId,
465
+ (run) => run?.state === "canceled" && run?.stage === "chat_followup"
466
+ );
467
+ assert.equal(canceledRun.result?.follow_up?.chat?.state, "canceled");
468
+ assert.equal(canceledRun.result?.result?.processed_count, 12);
469
+ }
470
+
239
471
  async function main() {
240
472
  const previousHome = process.env.BOSS_RECOMMEND_HOME;
241
473
  const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-index-async-"));
@@ -248,6 +480,8 @@ async function main() {
248
480
  await testResumeAfterProcessRestartSimulation();
249
481
  await testCancelPausedRun();
250
482
  await testCancelRunningRunKeepsCsv();
483
+ await testPauseAndResumeDuringChatFollowUp();
484
+ await testCancelDuringChatFollowUp();
251
485
  console.log("index async tests passed");
252
486
  } finally {
253
487
  setRunPipelineImplForTests(null);
@@ -385,7 +385,7 @@ function testGreetMaxGreetCountCanComeFromOverrides() {
385
385
  assert.equal(result.needs_max_greet_count_confirmation, true);
386
386
  }
387
387
 
388
- function testGreetAutoFilledMaxGreetCountShouldRequireReconfirmation() {
388
+ function testGreetAutoFilledMaxGreetCountShouldRequireReconfirmationWhenNotExplicitlyConfirmed() {
389
389
  const result = parseRecommendInstruction({
390
390
  instruction: "推荐页筛选985男生,有大模型工程经验,目标3人,符合标准直接沟通",
391
391
  confirmation: {
@@ -398,11 +398,11 @@ function testGreetAutoFilledMaxGreetCountShouldRequireReconfirmation() {
398
398
  target_count_confirmed: true,
399
399
  target_count_value: 3,
400
400
  post_action_confirmed: true,
401
- post_action_value: "greet",
402
- max_greet_count_confirmed: true,
403
- max_greet_count_value: 3
401
+ post_action_value: "greet"
404
402
  },
405
- overrides: null
403
+ overrides: {
404
+ max_greet_count: 3
405
+ }
406
406
  });
407
407
 
408
408
  assert.equal(result.screenParams.post_action, "greet");
@@ -412,6 +412,32 @@ function testGreetAutoFilledMaxGreetCountShouldRequireReconfirmation() {
412
412
  assert.equal(result.suspicious_fields.some((item) => item.field === "max_greet_count"), true);
413
413
  }
414
414
 
415
+ function testGreetMaxGreetCountEqualTargetShouldPassAfterExplicitConfirmation() {
416
+ const result = parseRecommendInstruction({
417
+ instruction: "推荐页筛选985男生,有大模型工程经验,目标3人,符合标准直接沟通",
418
+ confirmation: {
419
+ filters_confirmed: true,
420
+ school_tag_confirmed: true,
421
+ degree_confirmed: true,
422
+ gender_confirmed: true,
423
+ recent_not_view_confirmed: true,
424
+ criteria_confirmed: true,
425
+ target_count_confirmed: true,
426
+ target_count_value: 3,
427
+ post_action_confirmed: true,
428
+ post_action_value: "greet",
429
+ max_greet_count_confirmed: true,
430
+ max_greet_count_value: 3
431
+ },
432
+ overrides: null
433
+ });
434
+
435
+ assert.equal(result.screenParams.post_action, "greet");
436
+ assert.equal(result.screenParams.max_greet_count, 3);
437
+ assert.equal(result.needs_max_greet_count_confirmation, false);
438
+ assert.equal(result.pending_questions.some((q) => q.field === "max_greet_count"), false);
439
+ }
440
+
415
441
  function testTargetCountNeedsConfirmationEvenWhenOptional() {
416
442
  const result = parseRecommendInstruction({
417
443
  instruction: "推荐页筛选985男生,有大模型平台经验,符合标准收藏",
@@ -602,7 +628,8 @@ function main() {
602
628
  testMcpMentionShouldStayInCriteria();
603
629
  testGreetNeedsMaxGreetCountConfirmation();
604
630
  testGreetMaxGreetCountCanComeFromOverrides();
605
- testGreetAutoFilledMaxGreetCountShouldRequireReconfirmation();
631
+ testGreetAutoFilledMaxGreetCountShouldRequireReconfirmationWhenNotExplicitlyConfirmed();
632
+ testGreetMaxGreetCountEqualTargetShouldPassAfterExplicitConfirmation();
606
633
  testTargetCountNeedsConfirmationEvenWhenOptional();
607
634
  testTargetCountCanBeSkippedAfterConfirmation();
608
635
  testPostActionNoneCanBeConfirmed();