@reconcrap/boss-recommend-mcp 1.2.10 → 1.3.1

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 +783 -0
  19. package/vendor/boss-chat-cli/src/browser/chat-page.js +2698 -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/pipeline.js CHANGED
@@ -13,6 +13,13 @@ import {
13
13
  runRecommendScreenCli,
14
14
  switchRecommendTab
15
15
  } from "./adapters.js";
16
+ import {
17
+ cancelBossChatRun,
18
+ getBossChatRun,
19
+ pauseBossChatRun,
20
+ resumeBossChatRun,
21
+ startBossChatRun
22
+ } from "./boss-chat.js";
16
23
 
17
24
  const FORCED_RECENT_NOT_VIEW_ON_SCREEN_RECOVERY = "近14天没有";
18
25
  const MAX_SCREEN_AUTO_RECOVERY_ATTEMPTS = 5;
@@ -20,6 +27,7 @@ const MAX_SEARCH_NO_IFRAME_RETRY_ATTEMPTS = 1;
20
27
  const SEARCH_NO_IFRAME_RETRY_DELAY_MS = 1200;
21
28
  const MAX_SEARCH_FILTER_AUTO_RETRY_ATTEMPTS = 2;
22
29
  const SEARCH_FILTER_AUTO_RETRY_DELAY_MS = 1200;
30
+ const BOSS_CHAT_FOLLOW_UP_POLL_MS = 1500;
23
31
  const SEARCH_FILTER_RETRY_TOKENS = [
24
32
  "FILTER_CONFIRM_FAILED",
25
33
  "FILTER_DOM_CLASS_VERIFY_FAILED",
@@ -53,6 +61,11 @@ function normalizeText(value) {
53
61
  return String(value || "").replace(/\s+/g, " ").trim();
54
62
  }
55
63
 
64
+ function parsePositiveIntegerValue(value) {
65
+ const parsed = Number.parseInt(String(value ?? ""), 10);
66
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
67
+ }
68
+
56
69
  function sleep(ms) {
57
70
  return new Promise((resolve) => setTimeout(resolve, ms));
58
71
  }
@@ -331,11 +344,12 @@ function buildNeedInputResponse(parsedResult) {
331
344
  selected_page: parsedResult.proposed_page_scope || parsedResult.page_scope || "recommend",
332
345
  search_params: parsedResult.searchParams,
333
346
  screen_params: parsedResult.screenParams,
347
+ follow_up: parsedResult.follow_up || null,
334
348
  pending_questions: parsedResult.pending_questions,
335
349
  review: parsedResult.review,
336
350
  error: {
337
351
  code: "MISSING_REQUIRED_FIELDS",
338
- message: "缺少必要的筛选 criteria,请先补充或通过 overrides.criteria 明确传入。",
352
+ message: buildNeedInputMessage(parsedResult.missing_fields),
339
353
  retryable: true
340
354
  }
341
355
  };
@@ -353,20 +367,203 @@ function buildNeedConfirmationResponse(parsedResult) {
353
367
  post_action: parsedResult.proposed_post_action || parsedResult.screenParams.post_action,
354
368
  max_greet_count: parsedResult.proposed_max_greet_count || parsedResult.screenParams.max_greet_count
355
369
  },
370
+ follow_up: parsedResult.follow_up || null,
356
371
  pending_questions: parsedResult.pending_questions,
357
372
  review: parsedResult.review
358
373
  };
359
374
  }
360
375
 
361
- function buildFinalReviewQuestion({ searchParams, screenParams, selectedJob, selectedPage }) {
376
+ function normalizeFollowUpChatInput(followUp = null, defaults = null) {
377
+ const raw = followUp?.chat;
378
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
379
+ return {
380
+ requested: false,
381
+ missing_fields: [],
382
+ pending_questions: [],
383
+ summary: null,
384
+ input: null
385
+ };
386
+ }
387
+
388
+ const defaultCriteria = normalizeText(defaults?.criteria || "");
389
+ const defaultStartFromRaw = normalizeText(defaults?.start_from || "").toLowerCase();
390
+ const defaultStartFrom = defaultStartFromRaw === "all" ? "all" : "unread";
391
+ const defaultTargetCount = parsePositiveIntegerValue(defaults?.target_count);
392
+
393
+ const explicitCriteria = normalizeText(raw.criteria);
394
+ const explicitStartFromRaw = normalizeText(raw.start_from).toLowerCase();
395
+ const explicitStartFrom = explicitStartFromRaw === "all" ? "all" : explicitStartFromRaw === "unread" ? "unread" : "";
396
+ const explicitTargetCount = parsePositiveIntegerValue(raw.target_count);
397
+
398
+ const hasExplicitCriteria = Boolean(explicitCriteria);
399
+ const hasExplicitStartFrom = Boolean(explicitStartFrom);
400
+ const hasExplicitTargetCount = Number.isInteger(explicitTargetCount) && explicitTargetCount > 0;
401
+
402
+ const criteria = explicitCriteria || defaultCriteria;
403
+ const startFrom = explicitStartFrom || defaultStartFrom;
404
+ const targetCount = explicitTargetCount || defaultTargetCount;
405
+
406
+ const profile = normalizeText(raw.profile) || "default";
407
+ const summary = {
408
+ profile,
409
+ criteria: criteria || null,
410
+ start_from: startFrom || null,
411
+ target_count: targetCount,
412
+ dry_run: raw.dry_run === true,
413
+ no_state: raw.no_state === true,
414
+ safe_pacing: typeof raw.safe_pacing === "boolean" ? raw.safe_pacing : null,
415
+ batch_rest_enabled: typeof raw.batch_rest_enabled === "boolean" ? raw.batch_rest_enabled : null
416
+ };
417
+
418
+ const missing_fields = [];
419
+ const pending_questions = [];
420
+
421
+ if (!hasExplicitCriteria) {
422
+ missing_fields.push("follow_up.chat.criteria");
423
+ pending_questions.push({
424
+ field: "follow_up.chat.criteria",
425
+ question: "请填写 boss-chat follow-up 的筛选 criteria(自然语言,必填)。",
426
+ value: criteria || null
427
+ });
428
+ }
429
+ if (!hasExplicitStartFrom) {
430
+ missing_fields.push("follow_up.chat.start_from");
431
+ pending_questions.push({
432
+ field: "follow_up.chat.start_from",
433
+ question: "请确认 boss-chat follow-up 从未读还是全部聊天列表开始。",
434
+ value: summary.start_from,
435
+ options: [
436
+ { label: "未读", value: "unread" },
437
+ { label: "全部", value: "all" }
438
+ ]
439
+ });
440
+ }
441
+ if (!hasExplicitTargetCount) {
442
+ missing_fields.push("follow_up.chat.target_count");
443
+ pending_questions.push({
444
+ field: "follow_up.chat.target_count",
445
+ question: "请填写 boss-chat follow-up 本次处理人数上限(正整数,必填)。",
446
+ value: summary.target_count
447
+ });
448
+ }
449
+
450
+ return {
451
+ requested: true,
452
+ missing_fields,
453
+ pending_questions,
454
+ summary,
455
+ input: {
456
+ profile,
457
+ criteria: criteria || null,
458
+ start_from: startFrom || null,
459
+ target_count: targetCount,
460
+ dry_run: raw.dry_run === true,
461
+ no_state: raw.no_state === true,
462
+ safe_pacing: typeof raw.safe_pacing === "boolean" ? raw.safe_pacing : undefined,
463
+ batch_rest_enabled: typeof raw.batch_rest_enabled === "boolean" ? raw.batch_rest_enabled : undefined
464
+ }
465
+ };
466
+ }
467
+
468
+ function mergeParsedFollowUp(parsedResult, followUpChat) {
469
+ if (!followUpChat?.requested) {
470
+ return {
471
+ ...parsedResult,
472
+ follow_up: null,
473
+ follow_up_chat: followUpChat || null
474
+ };
475
+ }
476
+ const pending_questions = [
477
+ ...(Array.isArray(parsedResult?.pending_questions) ? parsedResult.pending_questions : []),
478
+ ...followUpChat.pending_questions
479
+ ];
480
+ return {
481
+ ...parsedResult,
482
+ missing_fields: dedupe([
483
+ ...(Array.isArray(parsedResult?.missing_fields) ? parsedResult.missing_fields : []),
484
+ ...followUpChat.missing_fields
485
+ ]),
486
+ pending_questions,
487
+ review: {
488
+ ...(parsedResult?.review || {}),
489
+ follow_up: {
490
+ chat: followUpChat.summary
491
+ }
492
+ },
493
+ follow_up: {
494
+ chat: followUpChat.summary
495
+ },
496
+ follow_up_chat: followUpChat
497
+ };
498
+ }
499
+
500
+ function buildResolvedFollowUpChatInput(followUpChat, { selectedJob, debugPort }) {
501
+ return {
502
+ profile: followUpChat?.input?.profile || "default",
503
+ job: selectedJob?.title || selectedJob?.label || selectedJob?.value || null,
504
+ start_from: followUpChat?.input?.start_from || null,
505
+ criteria: followUpChat?.input?.criteria || null,
506
+ target_count: followUpChat?.input?.target_count || null,
507
+ port: Number.isFinite(debugPort) ? debugPort : null,
508
+ dry_run: followUpChat?.input?.dry_run === true,
509
+ no_state: followUpChat?.input?.no_state === true,
510
+ safe_pacing: followUpChat?.input?.safe_pacing,
511
+ batch_rest_enabled: followUpChat?.input?.batch_rest_enabled
512
+ };
513
+ }
514
+
515
+ function buildBossChatFollowUpStatus({ payload, runId, fallbackInput = null, startMessage = null }) {
516
+ const run = payload?.run || null;
517
+ const progress = run?.progress && typeof run.progress === "object" ? run.progress : {};
518
+ return {
519
+ enabled: true,
520
+ run_id: normalizeText(payload?.run_id || run?.runId || runId) || null,
521
+ state: normalizeText(run?.state || payload?.state || payload?.status).toLowerCase() || null,
522
+ profile: normalizeText(fallbackInput?.profile) || "default",
523
+ job: normalizeText(fallbackInput?.job) || null,
524
+ start_from: normalizeText(fallbackInput?.start_from) || null,
525
+ criteria: normalizeText(fallbackInput?.criteria) || null,
526
+ target_count: parsePositiveIntegerValue(fallbackInput?.target_count),
527
+ port: parsePositiveIntegerValue(fallbackInput?.port),
528
+ progress: {
529
+ inspected: Number.isInteger(progress.inspected) ? progress.inspected : 0,
530
+ passed: Number.isInteger(progress.passed) ? progress.passed : 0,
531
+ requested: Number.isInteger(progress.requested) ? progress.requested : 0,
532
+ skipped: Number.isInteger(progress.skipped) ? progress.skipped : 0,
533
+ errors: Number.isInteger(progress.errors) ? progress.errors : 0
534
+ },
535
+ last_message: normalizeText(run?.lastMessage || payload?.message || startMessage) || null,
536
+ error: run?.error || payload?.error || null,
537
+ result: run?.result || null
538
+ };
539
+ }
540
+
541
+ function buildNeedInputMessage(missingFields = []) {
542
+ if (!Array.isArray(missingFields) || missingFields.length === 0) {
543
+ return "缺少必要字段,请先补充后再继续。";
544
+ }
545
+ if (missingFields.length === 1 && missingFields[0] === "criteria") {
546
+ return "缺少必要的筛选 criteria,请先补充或通过 overrides.criteria 明确传入。";
547
+ }
548
+ return `缺少必要字段:${missingFields.join(", ")}。请先补充后再继续。`;
549
+ }
550
+
551
+ function buildFinalReviewQuestion({ searchParams, screenParams, selectedJob, selectedPage, followUpChat }) {
362
552
  return {
363
553
  field: "final_review",
364
- question: "开始执行搜索和筛选前,请最后确认全部参数(岗位/页面/筛选条件/筛选 criteria/目标通过人数/post_action/max_greet_count)无误。",
554
+ question: followUpChat
555
+ ? "开始执行前,请最后确认全部参数(岗位/页面/筛选条件/筛选 criteria/目标通过人数/post_action/max_greet_count/boss-chat follow-up)无误。"
556
+ : "开始执行搜索和筛选前,请最后确认全部参数(岗位/页面/筛选条件/筛选 criteria/目标通过人数/post_action/max_greet_count)无误。",
365
557
  value: {
366
558
  job: selectedJob?.title || selectedJob?.label || selectedJob?.value || null,
367
559
  page_scope: selectedPage || "recommend",
368
560
  search_params: searchParams,
369
- screen_params: screenParams
561
+ screen_params: screenParams,
562
+ follow_up: followUpChat
563
+ ? {
564
+ chat: followUpChat
565
+ }
566
+ : null
370
567
  },
371
568
  options: [
372
569
  { label: "参数无误,开始执行", value: "confirm" },
@@ -375,6 +572,41 @@ function buildFinalReviewQuestion({ searchParams, screenParams, selectedJob, sel
375
572
  };
376
573
  }
377
574
 
575
+ function cloneJsonSafe(value) {
576
+ if (value === null || value === undefined) return null;
577
+ try {
578
+ return JSON.parse(JSON.stringify(value));
579
+ } catch {
580
+ return null;
581
+ }
582
+ }
583
+
584
+ function buildScreenInputSummary({
585
+ instruction,
586
+ selectedPage,
587
+ selectedJob,
588
+ userSearchParams,
589
+ effectiveSearchParams,
590
+ screenParams,
591
+ followUp
592
+ }) {
593
+ return {
594
+ instruction: normalizeText(instruction || "") || null,
595
+ selected_page: selectedPage || "recommend",
596
+ selected_job: selectedJob
597
+ ? {
598
+ value: normalizeText(selectedJob.value || "") || null,
599
+ title: normalizeText(selectedJob.title || "") || null,
600
+ label: normalizeText(selectedJob.label || "") || null
601
+ }
602
+ : null,
603
+ user_search_params: cloneJsonSafe(userSearchParams),
604
+ effective_search_params: cloneJsonSafe(effectiveSearchParams),
605
+ screen_params: cloneJsonSafe(screenParams),
606
+ follow_up: cloneJsonSafe(followUp)
607
+ };
608
+ }
609
+
378
610
  function buildFailedResponse(code, message, extra = {}) {
379
611
  return {
380
612
  status: "FAILED",
@@ -395,6 +627,40 @@ function buildPausedResponse(message, extra = {}) {
395
627
  };
396
628
  }
397
629
 
630
+ function buildRecommendFollowUpEnvelope(recommendResult, chatPayload = null) {
631
+ return {
632
+ search_params: recommendResult?.search_params || null,
633
+ screen_params: recommendResult?.screen_params || null,
634
+ result: recommendResult?.result || null,
635
+ partial_result: recommendResult?.result || null,
636
+ follow_up: chatPayload
637
+ ? {
638
+ chat: chatPayload
639
+ }
640
+ : null
641
+ };
642
+ }
643
+
644
+ function buildFollowUpFailedResponse(code, message, recommendResult, chatPayload) {
645
+ return {
646
+ status: "FAILED",
647
+ error: {
648
+ code,
649
+ message,
650
+ retryable: true
651
+ },
652
+ ...buildRecommendFollowUpEnvelope(recommendResult, chatPayload)
653
+ };
654
+ }
655
+
656
+ function buildFollowUpPausedResponse(message, recommendResult, chatPayload) {
657
+ return {
658
+ status: "PAUSED",
659
+ message: normalizeText(message || "") || "Recommend 流水线已暂停。",
660
+ ...buildRecommendFollowUpEnvelope(recommendResult, chatPayload)
661
+ };
662
+ }
663
+
398
664
  class PipelineAbortError extends Error {
399
665
  constructor(message = "Pipeline execution aborted") {
400
666
  super(message);
@@ -430,6 +696,9 @@ function createPipelineRuntime(runtime = null) {
430
696
  const isPauseRequested = typeof runtime?.isPauseRequested === "function"
431
697
  ? runtime.isPauseRequested
432
698
  : () => false;
699
+ const isCancelRequested = typeof runtime?.isCancelRequested === "function"
700
+ ? runtime.isCancelRequested
701
+ : () => false;
433
702
 
434
703
  function setStage(stage, message = null) {
435
704
  safeInvokeRuntimeCallback(runtime?.onStage, {
@@ -463,6 +732,13 @@ function createPipelineRuntime(runtime = null) {
463
732
  });
464
733
  }
465
734
 
735
+ function followUp(payload) {
736
+ safeInvokeRuntimeCallback(runtime?.onFollowUp, {
737
+ ...(payload || {}),
738
+ at: new Date().toISOString()
739
+ });
740
+ }
741
+
466
742
  function adapterRuntime(stage) {
467
743
  return {
468
744
  signal,
@@ -477,10 +753,12 @@ function createPipelineRuntime(runtime = null) {
477
753
  signal,
478
754
  heartbeatIntervalMs,
479
755
  isPauseRequested,
756
+ isCancelRequested,
480
757
  setStage,
481
758
  heartbeat,
482
759
  output,
483
760
  progress,
761
+ followUp,
484
762
  adapterRuntime
485
763
  };
486
764
  }
@@ -498,6 +776,14 @@ function isPauseRequested(runtimeHooks) {
498
776
  }
499
777
  }
500
778
 
779
+ function isCancelRequested(runtimeHooks) {
780
+ try {
781
+ return runtimeHooks?.isCancelRequested?.() === true;
782
+ } catch {
783
+ return false;
784
+ }
785
+ }
786
+
501
787
  function buildChromeSetupGuidance({ debugPort, pageState }) {
502
788
  const expectedUrl = pageState?.expected_url || "https://www.zhipin.com/web/chat/recommend";
503
789
  const loginUrl = pageState?.login_url || "https://www.zhipin.com/web/user/?ka=bticket";
@@ -536,6 +822,233 @@ function buildChromeSetupGuidance({ debugPort, pageState }) {
536
822
  };
537
823
  }
538
824
 
825
+ async function runBossChatFollowUpPhase({
826
+ workspaceRoot,
827
+ followUpChat,
828
+ selectedJob,
829
+ debugPort,
830
+ recommendResult,
831
+ resume,
832
+ runtimeHooks,
833
+ startChatRun,
834
+ getChatRun,
835
+ pauseChatRun,
836
+ resumeChatRun,
837
+ cancelChatRun
838
+ }) {
839
+ const recommendSummary = recommendResult?.result || null;
840
+ const resolvedChatInput = buildResolvedFollowUpChatInput(followUpChat, {
841
+ selectedJob,
842
+ debugPort
843
+ });
844
+ let chatRunId = normalizeText(resume?.chat_run_id || "");
845
+ const resumeFromChatPhase = resume?.resume === true && normalizeText(resume?.follow_up_phase) === "chat_followup";
846
+ let pauseRequested = false;
847
+ let cancelRequested = false;
848
+ let resumeIssued = false;
849
+
850
+ runtimeHooks.setStage(
851
+ "chat_followup",
852
+ chatRunId
853
+ ? "Recommend 流水线已完成,准备恢复 boss-chat follow-up。"
854
+ : "Recommend 流水线已完成,开始执行 boss-chat follow-up。"
855
+ );
856
+ runtimeHooks.heartbeat("chat_followup", {
857
+ profile: resolvedChatInput.profile,
858
+ job: resolvedChatInput.job,
859
+ start_from: resolvedChatInput.start_from,
860
+ target_count: resolvedChatInput.target_count
861
+ });
862
+
863
+ if (!chatRunId) {
864
+ const startResult = await startChatRun({
865
+ workspaceRoot,
866
+ input: {
867
+ profile: resolvedChatInput.profile,
868
+ job: resolvedChatInput.job,
869
+ start_from: resolvedChatInput.start_from,
870
+ criteria: resolvedChatInput.criteria,
871
+ target_count: resolvedChatInput.target_count,
872
+ port: resolvedChatInput.port,
873
+ dry_run: resolvedChatInput.dry_run,
874
+ no_state: resolvedChatInput.no_state,
875
+ safe_pacing: resolvedChatInput.safe_pacing,
876
+ batch_rest_enabled: resolvedChatInput.batch_rest_enabled
877
+ }
878
+ });
879
+ if (startResult?.status !== "ACCEPTED" || !normalizeText(startResult?.run_id)) {
880
+ return buildFollowUpFailedResponse(
881
+ startResult?.error?.code || "BOSS_CHAT_FOLLOW_UP_LAUNCH_FAILED",
882
+ startResult?.error?.message || "boss-chat follow-up 启动失败。",
883
+ recommendResult,
884
+ {
885
+ enabled: true,
886
+ input: resolvedChatInput,
887
+ launch_result: startResult || null
888
+ }
889
+ );
890
+ }
891
+ chatRunId = normalizeText(startResult.run_id);
892
+ runtimeHooks.followUp({
893
+ stage: "chat_followup",
894
+ last_message: startResult.message || "boss-chat follow-up 已启动。",
895
+ recommend_payload: recommendResult,
896
+ recommend_result: recommendSummary,
897
+ follow_up: {
898
+ chat: {
899
+ ...buildBossChatFollowUpStatus({
900
+ payload: startResult,
901
+ runId: chatRunId,
902
+ fallbackInput: resolvedChatInput,
903
+ startMessage: startResult.message || "boss-chat follow-up 已启动。"
904
+ }),
905
+ input: resolvedChatInput
906
+ }
907
+ }
908
+ });
909
+ }
910
+
911
+ while (true) {
912
+ ensurePipelineNotAborted(runtimeHooks.signal);
913
+
914
+ const chatStatusPayload = await getChatRun({
915
+ workspaceRoot,
916
+ input: {
917
+ profile: resolvedChatInput.profile,
918
+ runId: chatRunId
919
+ }
920
+ });
921
+ const chatStatus = buildBossChatFollowUpStatus({
922
+ payload: chatStatusPayload,
923
+ runId: chatRunId,
924
+ fallbackInput: resolvedChatInput
925
+ });
926
+ const chatState = normalizeText(chatStatus.state).toLowerCase();
927
+
928
+ runtimeHooks.followUp({
929
+ stage: "chat_followup",
930
+ last_message: chatStatus.last_message || "boss-chat follow-up 进行中。",
931
+ recommend_payload: recommendResult,
932
+ recommend_result: recommendSummary,
933
+ follow_up: {
934
+ chat: {
935
+ ...chatStatus,
936
+ input: resolvedChatInput
937
+ }
938
+ }
939
+ });
940
+
941
+ if (isCancelRequested(runtimeHooks) && !cancelRequested && !["completed", "failed", "canceled"].includes(chatState)) {
942
+ cancelRequested = true;
943
+ await cancelChatRun({
944
+ workspaceRoot,
945
+ input: {
946
+ profile: resolvedChatInput.profile,
947
+ runId: chatRunId
948
+ }
949
+ });
950
+ await sleep(500);
951
+ continue;
952
+ }
953
+
954
+ if (
955
+ isPauseRequested(runtimeHooks)
956
+ && !pauseRequested
957
+ && !cancelRequested
958
+ && !["paused", "completed", "failed", "canceled"].includes(chatState)
959
+ ) {
960
+ pauseRequested = true;
961
+ await pauseChatRun({
962
+ workspaceRoot,
963
+ input: {
964
+ profile: resolvedChatInput.profile,
965
+ runId: chatRunId
966
+ }
967
+ });
968
+ await sleep(500);
969
+ continue;
970
+ }
971
+
972
+ if (resumeFromChatPhase && !isPauseRequested(runtimeHooks) && !isCancelRequested(runtimeHooks) && !resumeIssued) {
973
+ if (chatState === "paused") {
974
+ resumeIssued = true;
975
+ await resumeChatRun({
976
+ workspaceRoot,
977
+ input: {
978
+ profile: resolvedChatInput.profile,
979
+ runId: chatRunId
980
+ }
981
+ });
982
+ await sleep(500);
983
+ continue;
984
+ }
985
+ if (chatState === "running" || chatState === "queued") {
986
+ resumeIssued = true;
987
+ }
988
+ }
989
+
990
+ if (chatState === "completed") {
991
+ return {
992
+ ...recommendResult,
993
+ follow_up: {
994
+ chat: {
995
+ ...chatStatus,
996
+ input: resolvedChatInput
997
+ }
998
+ },
999
+ message: "Recommend 流水线已完成,boss-chat follow-up 也已执行完成。"
1000
+ };
1001
+ }
1002
+
1003
+ if (chatState === "failed") {
1004
+ return buildFollowUpFailedResponse(
1005
+ chatStatus.error?.code || "BOSS_CHAT_FOLLOW_UP_FAILED",
1006
+ chatStatus.error?.message || "boss-chat follow-up 执行失败。",
1007
+ recommendResult,
1008
+ {
1009
+ ...chatStatus,
1010
+ input: resolvedChatInput
1011
+ }
1012
+ );
1013
+ }
1014
+
1015
+ if (chatState === "canceled") {
1016
+ if (isCancelRequested(runtimeHooks)) {
1017
+ return buildFollowUpPausedResponse(
1018
+ "Recommend 流水线已取消,boss-chat follow-up 已停止。",
1019
+ recommendResult,
1020
+ {
1021
+ ...chatStatus,
1022
+ input: resolvedChatInput
1023
+ }
1024
+ );
1025
+ }
1026
+ return buildFollowUpFailedResponse(
1027
+ "BOSS_CHAT_FOLLOW_UP_CANCELED",
1028
+ "boss-chat follow-up 已取消。",
1029
+ recommendResult,
1030
+ {
1031
+ ...chatStatus,
1032
+ input: resolvedChatInput
1033
+ }
1034
+ );
1035
+ }
1036
+
1037
+ if (chatState === "paused" && isPauseRequested(runtimeHooks)) {
1038
+ return buildFollowUpPausedResponse(
1039
+ "Recommend 流水线已暂停,可使用 resume 继续 boss-chat follow-up。",
1040
+ recommendResult,
1041
+ {
1042
+ ...chatStatus,
1043
+ input: resolvedChatInput
1044
+ }
1045
+ );
1046
+ }
1047
+
1048
+ await sleep(BOSS_CHAT_FOLLOW_UP_POLL_MS);
1049
+ }
1050
+ }
1051
+
539
1052
  const defaultDependencies = {
540
1053
  attemptPipelineAutoRepair,
541
1054
  parseRecommendInstruction,
@@ -548,11 +1061,16 @@ const defaultDependencies = {
548
1061
  runPipelinePreflight,
549
1062
  runRecommendSearchCli,
550
1063
  runRecommendScreenCli,
1064
+ startBossChatRun,
1065
+ getBossChatRun,
1066
+ pauseBossChatRun,
1067
+ resumeBossChatRun,
1068
+ cancelBossChatRun,
551
1069
  switchRecommendTab
552
1070
  };
553
1071
 
554
1072
  export async function runRecommendPipeline(
555
- { workspaceRoot, instruction, confirmation, overrides, resume = null },
1073
+ { workspaceRoot, instruction, confirmation, overrides, followUp = null, resume = null },
556
1074
  dependencies = defaultDependencies,
557
1075
  runtime = null
558
1076
  ) {
@@ -570,13 +1088,26 @@ export async function runRecommendPipeline(
570
1088
  runPipelinePreflight: runPreflight,
571
1089
  runRecommendSearchCli: searchCli,
572
1090
  runRecommendScreenCli: screenCli,
1091
+ startBossChatRun: startChatRun,
1092
+ getBossChatRun: getChatRun,
1093
+ pauseBossChatRun: pauseChatRun,
1094
+ resumeBossChatRun: resumeChatRun,
1095
+ cancelBossChatRun: cancelChatRun,
573
1096
  switchRecommendTab: switchTab
574
1097
  } = resolvedDependencies;
575
1098
  const runtimeHooks = createPipelineRuntime(runtime);
576
1099
  ensurePipelineNotAborted(runtimeHooks.signal);
577
1100
 
578
1101
  const startedAt = Date.now();
579
- const parsed = parseInstruction({ instruction, confirmation, overrides });
1102
+ const instructionParsed = parseInstruction({ instruction, confirmation, overrides });
1103
+ const parsed = mergeParsedFollowUp(
1104
+ instructionParsed,
1105
+ normalizeFollowUpChatInput(followUp, {
1106
+ criteria: instructionParsed?.screenParams?.criteria || null,
1107
+ target_count: instructionParsed?.screenParams?.target_count || null,
1108
+ start_from: "unread"
1109
+ })
1110
+ );
580
1111
  const selectedPage = resolvePipelinePageScope(parsed, confirmation, overrides);
581
1112
 
582
1113
  if (parsed.missing_fields.length > 0) {
@@ -598,6 +1129,41 @@ export async function runRecommendPipeline(
598
1129
  return buildNeedConfirmationResponse(parsed);
599
1130
  }
600
1131
 
1132
+ const resumeFromChatPhase = (
1133
+ resume?.resume === true
1134
+ && normalizeText(resume?.follow_up_phase) === "chat_followup"
1135
+ && normalizeText(resume?.chat_run_id)
1136
+ );
1137
+ if (resumeFromChatPhase) {
1138
+ if (!parsed.follow_up_chat?.requested || !resume?.recommend_result) {
1139
+ return buildFailedResponse(
1140
+ "BOSS_CHAT_FOLLOW_UP_RESUME_CONTEXT_MISSING",
1141
+ "缺少 boss-chat follow-up 恢复上下文,无法继续。"
1142
+ );
1143
+ }
1144
+ const preflight = runPreflight(workspaceRoot, { pageScope: selectedPage });
1145
+ return runBossChatFollowUpPhase({
1146
+ workspaceRoot,
1147
+ followUpChat: parsed.follow_up_chat,
1148
+ selectedJob: resume.recommend_result?.selected_job || null,
1149
+ debugPort: preflight.debug_port,
1150
+ recommendResult: {
1151
+ status: "COMPLETED",
1152
+ search_params: resume.recommend_search_params || parsed.searchParams,
1153
+ screen_params: resume.recommend_screen_params || parsed.screenParams,
1154
+ result: resume.recommend_result,
1155
+ message: "Recommend 流水线已完成,正在恢复 boss-chat follow-up。"
1156
+ },
1157
+ resume,
1158
+ runtimeHooks,
1159
+ startChatRun,
1160
+ getChatRun,
1161
+ pauseChatRun,
1162
+ resumeChatRun,
1163
+ cancelChatRun
1164
+ });
1165
+ }
1166
+
601
1167
  ensurePipelineNotAborted(runtimeHooks.signal);
602
1168
  runtimeHooks.setStage("preflight", "开始执行 preflight 检查。");
603
1169
  runtimeHooks.heartbeat("preflight");
@@ -874,6 +1440,7 @@ export async function runRecommendPipeline(
874
1440
  post_action: parsed.proposed_post_action || parsed.screenParams.post_action,
875
1441
  max_greet_count: parsed.proposed_max_greet_count || parsed.screenParams.max_greet_count
876
1442
  },
1443
+ follow_up: parsed.follow_up || null,
877
1444
  pending_questions: pendingQuestions,
878
1445
  review: parsed.review,
879
1446
  job_options: jobOptions
@@ -892,7 +1459,8 @@ export async function runRecommendPipeline(
892
1459
  max_greet_count: parsed.proposed_max_greet_count || parsed.screenParams.max_greet_count
893
1460
  },
894
1461
  selectedJob,
895
- selectedPage
1462
+ selectedPage,
1463
+ followUpChat: parsed.follow_up?.chat || null
896
1464
  }));
897
1465
  return {
898
1466
  status: "NEED_CONFIRMATION",
@@ -905,6 +1473,7 @@ export async function runRecommendPipeline(
905
1473
  post_action: parsed.proposed_post_action || parsed.screenParams.post_action,
906
1474
  max_greet_count: parsed.proposed_max_greet_count || parsed.screenParams.max_greet_count
907
1475
  },
1476
+ follow_up: parsed.follow_up || null,
908
1477
  selected_job: selectedJob,
909
1478
  pending_questions: pendingQuestions,
910
1479
  review: parsed.review,
@@ -1192,6 +1761,15 @@ export async function runRecommendPipeline(
1192
1761
  workspaceRoot,
1193
1762
  screenParams: parsed.screenParams,
1194
1763
  pageScope: selectedPage,
1764
+ inputSummary: buildScreenInputSummary({
1765
+ instruction,
1766
+ selectedPage,
1767
+ selectedJob,
1768
+ userSearchParams: parsed.searchParams,
1769
+ effectiveSearchParams,
1770
+ screenParams: parsed.screenParams,
1771
+ followUp: parsed.follow_up || null
1772
+ }),
1195
1773
  resume: currentResumeConfig,
1196
1774
  runtime: runtimeHooks.adapterRuntime("screen")
1197
1775
  });
@@ -1475,7 +2053,7 @@ export async function runRecommendPipeline(
1475
2053
  greet_count: screenSummary.greet_count ?? 0
1476
2054
  });
1477
2055
 
1478
- return {
2056
+ const recommendResult = {
1479
2057
  status: "COMPLETED",
1480
2058
  search_params: effectiveSearchParams,
1481
2059
  screen_params: parsed.screenParams,
@@ -1503,5 +2081,24 @@ export async function runRecommendPipeline(
1503
2081
  ? "Recommend 流水线已完成。本次 post_action=none:符合条件的人选仅记录到 CSV,不执行收藏或打招呼。"
1504
2082
  : "Recommend 流水线已完成。post_action 在运行开始时已一次性确认;若选择打招呼并设置上限,超出上限后会自动改为收藏。"
1505
2083
  };
2084
+
2085
+ if (!parsed.follow_up_chat?.requested) {
2086
+ return recommendResult;
2087
+ }
2088
+
2089
+ return runBossChatFollowUpPhase({
2090
+ workspaceRoot,
2091
+ followUpChat: parsed.follow_up_chat,
2092
+ selectedJob,
2093
+ debugPort: preflight.debug_port,
2094
+ recommendResult,
2095
+ resume,
2096
+ runtimeHooks,
2097
+ startChatRun,
2098
+ getChatRun,
2099
+ pauseChatRun,
2100
+ resumeChatRun,
2101
+ cancelChatRun
2102
+ });
1506
2103
  }
1507
2104
  }