@reconcrap/boss-recommend-mcp 1.1.12 → 1.2.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.
package/src/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { createRequire } from "node:module";
4
- import process from "node:process";
5
- import { fileURLToPath } from "node:url";
3
+ import { spawn } from "node:child_process";
4
+ import { createRequire } from "node:module";
5
+ import process from "node:process";
6
+ import { fileURLToPath } from "node:url";
6
7
  import { runRecommendPipeline } from "./pipeline.js";
7
8
  import {
8
9
  RUN_MODE_ASYNC,
@@ -34,13 +35,16 @@ const TOOL_PAUSE_RUN = "pause_recommend_pipeline_run";
34
35
  const TOOL_RESUME_RUN = "resume_recommend_pipeline_run";
35
36
 
36
37
  const SERVER_NAME = "boss-recommend-mcp";
37
- const FRAMING_UNKNOWN = "unknown";
38
- const FRAMING_HEADER = "header";
39
- const FRAMING_LINE = "line";
40
-
41
- const activeAsyncRuns = new Map();
42
- let runPipelineImpl = runRecommendPipeline;
43
- const TERMINAL_RUN_STATES = new Set([RUN_STATE_COMPLETED, RUN_STATE_FAILED, RUN_STATE_CANCELED]);
38
+ const FRAMING_UNKNOWN = "unknown";
39
+ const FRAMING_HEADER = "header";
40
+ const FRAMING_LINE = "line";
41
+ const DETACHED_WORKER_FLAG = "--detached-worker";
42
+ const DETACHED_WORKER_RUN_ID_FLAG = "--run-id";
43
+ const DETACHED_WORKER_RESUME_FLAG = "--resume";
44
+
45
+ let runPipelineImpl = runRecommendPipeline;
46
+ let spawnProcessImpl = spawn;
47
+ const TERMINAL_RUN_STATES = new Set([RUN_STATE_COMPLETED, RUN_STATE_FAILED, RUN_STATE_CANCELED]);
44
48
 
45
49
  function normalizeText(value) {
46
50
  return String(value || "").replace(/\s+/g, " ").trim();
@@ -151,7 +155,7 @@ function createRunInputSchema() {
151
155
  page_confirmed: { type: "boolean" },
152
156
  page_value: {
153
157
  type: "string",
154
- enum: ["recommend", "featured"]
158
+ enum: ["recommend", "featured", "latest"]
155
159
  },
156
160
  filters_confirmed: { type: "boolean" },
157
161
  school_tag_confirmed: { type: "boolean" },
@@ -227,7 +231,7 @@ function createRunInputSchema() {
227
231
  properties: {
228
232
  page_scope: {
229
233
  type: "string",
230
- enum: ["recommend", "featured"]
234
+ enum: ["recommend", "featured", "latest"]
231
235
  },
232
236
  school_tag: {
233
237
  oneOf: [
@@ -470,9 +474,6 @@ function reconcileOrphanRunIfNeeded(runId, snapshot) {
470
474
  if (snapshot.state === RUN_STATE_PAUSED) {
471
475
  return snapshot;
472
476
  }
473
- if (activeAsyncRuns.has(runId)) {
474
- return snapshot;
475
- }
476
477
  if (isProcessAlive(snapshot.pid)) {
477
478
  return snapshot;
478
479
  }
@@ -511,6 +512,71 @@ function reconcileOrphanRunIfNeeded(runId, snapshot) {
511
512
  });
512
513
  return recovered || readRunState(runId) || snapshot;
513
514
  }
515
+
516
+ function parseDetachedWorkerOptions(argv = process.argv.slice(2)) {
517
+ if (!Array.isArray(argv) || !argv.includes(DETACHED_WORKER_FLAG)) {
518
+ return null;
519
+ }
520
+ const runIdFlagIndex = argv.indexOf(DETACHED_WORKER_RUN_ID_FLAG);
521
+ const runId = runIdFlagIndex >= 0 ? normalizeText(argv[runIdFlagIndex + 1]) : "";
522
+ return {
523
+ runId,
524
+ resumeRun: argv.includes(DETACHED_WORKER_RESUME_FLAG)
525
+ };
526
+ }
527
+
528
+ function launchDetachedRunWorker({ runId, resumeRun = false }) {
529
+ const childArgs = [thisFilePath, DETACHED_WORKER_FLAG, DETACHED_WORKER_RUN_ID_FLAG, String(runId)];
530
+ if (resumeRun) {
531
+ childArgs.push(DETACHED_WORKER_RESUME_FLAG);
532
+ }
533
+ const child = spawnProcessImpl(process.execPath, childArgs, {
534
+ detached: true,
535
+ stdio: "ignore",
536
+ windowsHide: true,
537
+ env: process.env
538
+ });
539
+ if (typeof child?.unref === "function") {
540
+ child.unref();
541
+ }
542
+ return child;
543
+ }
544
+
545
+ function buildWorkerLaunchFailedPayload(message) {
546
+ return {
547
+ status: "FAILED",
548
+ error: {
549
+ code: "RUN_WORKER_LAUNCH_FAILED",
550
+ message,
551
+ retryable: true
552
+ }
553
+ };
554
+ }
555
+
556
+ function finalizeCanceledRun(runId, snapshot) {
557
+ const canceledResult = {
558
+ status: "FAILED",
559
+ error: {
560
+ code: "PIPELINE_CANCELED",
561
+ message: "流水线已取消。",
562
+ retryable: true
563
+ },
564
+ partial_result: snapshot?.result?.partial_result || snapshot?.result?.result || null
565
+ };
566
+ return safeUpdateRunState(runId, {
567
+ state: RUN_STATE_CANCELED,
568
+ stage: snapshot?.stage || RUN_STAGE_PREFLIGHT,
569
+ last_message: "流水线已取消。",
570
+ control: {
571
+ pause_requested: false,
572
+ pause_requested_at: null,
573
+ pause_requested_by: null,
574
+ cancel_requested: false
575
+ },
576
+ error: canceledResult.error,
577
+ result: canceledResult
578
+ }) || readRunState(runId) || snapshot;
579
+ }
514
580
 
515
581
  function createRuntimeCallbacks(runId, heartbeatIntervalMs) {
516
582
  let lastStage = RUN_STAGE_PREFLIGHT;
@@ -735,14 +801,14 @@ async function executeTrackedPipeline({
735
801
  };
736
802
  }
737
803
 
738
- function initializeRunStateOrThrow(runId, mode, workspaceRoot, args) {
739
- const artifacts = getRunArtifacts(runId);
740
- const snapshot = createRunStateSnapshot({
741
- runId,
742
- mode,
743
- state: "queued",
744
- stage: RUN_STAGE_PREFLIGHT,
745
- pid: process.pid,
804
+ function initializeRunStateOrThrow(runId, mode, workspaceRoot, args, pid = process.pid) {
805
+ const artifacts = getRunArtifacts(runId);
806
+ const snapshot = createRunStateSnapshot({
807
+ runId,
808
+ mode,
809
+ state: "queued",
810
+ stage: RUN_STAGE_PREFLIGHT,
811
+ pid,
746
812
  lastMessage: "流水线任务已创建,等待执行。",
747
813
  context: buildRunContext(workspaceRoot, args),
748
814
  control: {
@@ -759,28 +825,59 @@ function initializeRunStateOrThrow(runId, mode, workspaceRoot, args) {
759
825
  last_resumed_at: null,
760
826
  last_paused_at: null
761
827
  }
762
- });
763
- return writeRunState(snapshot);
764
- }
765
-
766
- function launchAsyncRun({ runId, mode, workspaceRoot, args, resumeRun = false }) {
767
- const abortController = new AbortController();
768
- const promise = executeTrackedPipeline({
769
- runId,
770
- mode,
771
- workspaceRoot,
772
- args,
773
- signal: abortController.signal,
774
- resumeRun
775
- }).finally(() => {
776
- activeAsyncRuns.delete(runId);
777
- });
778
- activeAsyncRuns.set(runId, {
779
- abortController,
780
- promise
781
- });
782
- return { abortController, promise };
783
- }
828
+ });
829
+ return writeRunState(snapshot);
830
+ }
831
+
832
+ async function runDetachedWorker({ runId, resumeRun = false, workerPid = process.pid }) {
833
+ const normalizedRunId = normalizeText(runId);
834
+ if (!normalizedRunId) {
835
+ return { ok: false, error: "run_id is required" };
836
+ }
837
+ const snapshot = readRunState(normalizedRunId);
838
+ if (!snapshot) {
839
+ return { ok: false, error: `run_id=${normalizedRunId} not found` };
840
+ }
841
+
842
+ const executionContext = resolveRunContext(snapshot);
843
+ if (!executionContext) {
844
+ const failedPayload = {
845
+ code: "RUN_CONTEXT_MISSING",
846
+ message: "run 缺少可恢复的执行上下文,无法继续。",
847
+ retryable: false
848
+ };
849
+ safeUpdateRunState(normalizedRunId, {
850
+ state: RUN_STATE_FAILED,
851
+ stage: snapshot.stage || RUN_STAGE_PREFLIGHT,
852
+ last_message: failedPayload.message,
853
+ error: failedPayload,
854
+ result: {
855
+ status: "FAILED",
856
+ error: failedPayload
857
+ }
858
+ });
859
+ return { ok: false, error: failedPayload.message };
860
+ }
861
+
862
+ safeUpdateRunState(normalizedRunId, {
863
+ pid: Number.isInteger(workerPid) && workerPid > 0 ? workerPid : process.pid,
864
+ mode: RUN_MODE_ASYNC,
865
+ state: "queued",
866
+ last_message: resumeRun
867
+ ? "detached worker 已启动,准备恢复执行。"
868
+ : "detached worker 已启动,准备执行。"
869
+ });
870
+
871
+ await executeTrackedPipeline({
872
+ runId: normalizedRunId,
873
+ mode: RUN_MODE_ASYNC,
874
+ workspaceRoot: executionContext.workspaceRoot,
875
+ args: executionContext.args,
876
+ signal: new AbortController().signal,
877
+ resumeRun
878
+ });
879
+ return { ok: true };
880
+ }
784
881
 
785
882
  async function handleStartRunTool({ workspaceRoot, args }) {
786
883
  const precheckArgs = buildAsyncPrecheckArgs(args);
@@ -814,36 +911,57 @@ async function handleStartRunTool({ workspaceRoot, args }) {
814
911
  return precheckResult;
815
912
  }
816
913
 
817
- cleanupExpiredRuns();
818
- const runId = createRunId();
819
- try {
820
- initializeRunStateOrThrow(runId, RUN_MODE_ASYNC, workspaceRoot, args);
821
- } catch (error) {
822
- return {
823
- status: "FAILED",
914
+ cleanupExpiredRuns();
915
+ const runId = createRunId();
916
+ try {
917
+ initializeRunStateOrThrow(runId, RUN_MODE_ASYNC, workspaceRoot, args, process.pid);
918
+ } catch (error) {
919
+ return {
920
+ status: "FAILED",
824
921
  error: {
825
922
  code: "RUN_STATE_IO_ERROR",
826
923
  message: `无法写入运行状态目录:${error.message || "unknown"}`,
827
924
  retryable: false
828
- }
829
- };
830
- }
831
-
832
- launchAsyncRun({
833
- runId,
834
- mode: RUN_MODE_ASYNC,
835
- workspaceRoot,
836
- args
837
- });
838
-
839
- return {
840
- status: "ACCEPTED",
841
- run_id: runId,
842
- state: "queued",
843
- poll_after_sec: getDefaultPollAfterSec(),
844
- message: "异步流水线已启动。默认不自动轮询;如需进度请按需调用 get_recommend_pipeline_run。"
845
- };
846
- }
925
+ }
926
+ };
927
+ }
928
+
929
+ let worker;
930
+ try {
931
+ worker = launchDetachedRunWorker({
932
+ runId,
933
+ resumeRun: false
934
+ });
935
+ } catch (error) {
936
+ const failedMessage = `无法启动 detached 运行进程:${error?.message || "unknown"}`;
937
+ safeUpdateRunState(runId, {
938
+ state: RUN_STATE_FAILED,
939
+ stage: RUN_STAGE_PREFLIGHT,
940
+ last_message: failedMessage,
941
+ error: {
942
+ code: "RUN_WORKER_LAUNCH_FAILED",
943
+ message: failedMessage,
944
+ retryable: true
945
+ },
946
+ result: buildWorkerLaunchFailedPayload(failedMessage)
947
+ });
948
+ return buildWorkerLaunchFailedPayload(failedMessage);
949
+ }
950
+
951
+ safeUpdateRunState(runId, {
952
+ pid: worker?.pid,
953
+ state: "queued",
954
+ last_message: "异步流水线已启动(detached)。"
955
+ });
956
+
957
+ return {
958
+ status: "ACCEPTED",
959
+ run_id: runId,
960
+ state: "queued",
961
+ poll_after_sec: getDefaultPollAfterSec(),
962
+ message: "异步流水线已启动(detached)。默认不自动轮询;如需进度请按需调用 get_recommend_pipeline_run。"
963
+ };
964
+ }
847
965
 
848
966
  function handleGetRunTool(args) {
849
967
  cleanupExpiredRuns();
@@ -876,7 +994,7 @@ function handleGetRunTool(args) {
876
994
  };
877
995
  }
878
996
 
879
- function handleCancelRunTool(args) {
997
+ function handleCancelRunTool(args) {
880
998
  const runId = normalizeText(args?.run_id);
881
999
  if (!runId) {
882
1000
  return {
@@ -888,8 +1006,8 @@ function handleCancelRunTool(args) {
888
1006
  }
889
1007
  };
890
1008
  }
891
- const snapshot = readRunState(runId);
892
- if (!snapshot) {
1009
+ const snapshot = readRunState(runId);
1010
+ if (!snapshot) {
893
1011
  return {
894
1012
  status: "FAILED",
895
1013
  error: {
@@ -898,93 +1016,43 @@ function handleCancelRunTool(args) {
898
1016
  retryable: false
899
1017
  }
900
1018
  };
901
- }
902
-
903
- if (TERMINAL_RUN_STATES.has(snapshot.state)) {
904
- return {
905
- status: "CANCEL_IGNORED",
906
- run: snapshot,
907
- message: "目标任务已结束,无需取消。"
908
- };
909
- }
910
-
911
- if (snapshot.state === RUN_STATE_PAUSED) {
912
- const canceledResult = {
913
- status: "FAILED",
914
- error: {
915
- code: "PIPELINE_CANCELED",
916
- message: "流水线已取消。",
917
- retryable: true
918
- },
919
- partial_result: snapshot.result?.partial_result || snapshot.result?.result || null
920
- };
921
- const canceledRun = safeUpdateRunState(runId, {
922
- state: RUN_STATE_CANCELED,
923
- stage: snapshot.stage || RUN_STAGE_PREFLIGHT,
924
- last_message: "流水线已取消。",
925
- control: {
926
- pause_requested: false,
927
- pause_requested_at: null,
928
- pause_requested_by: null,
929
- cancel_requested: false
930
- },
931
- error: canceledResult.error,
932
- result: canceledResult
933
- }) || readRunState(runId) || snapshot;
934
- return {
935
- status: "CANCEL_REQUESTED",
936
- run: canceledRun
937
- };
938
- }
939
-
940
- const activeRun = activeAsyncRuns.get(runId);
941
- if (!activeRun) {
942
- const canceledResult = {
943
- status: "FAILED",
944
- error: {
945
- code: "PIPELINE_CANCELED",
946
- message: "流水线已取消。",
947
- retryable: true
948
- },
949
- partial_result: snapshot.result?.partial_result || snapshot.result?.result || null
950
- };
951
- const canceledRun = safeUpdateRunState(runId, {
952
- state: RUN_STATE_CANCELED,
953
- stage: snapshot.stage || RUN_STAGE_PREFLIGHT,
954
- last_message: "流水线已取消。",
955
- control: {
956
- pause_requested: false,
957
- pause_requested_at: null,
958
- pause_requested_by: null,
959
- cancel_requested: false
960
- },
961
- error: canceledResult.error,
962
- result: canceledResult
963
- }) || readRunState(runId) || snapshot;
964
- return {
965
- status: "CANCEL_REQUESTED",
966
- run: canceledRun
967
- };
968
- }
969
- safeUpdateRunState(runId, {
970
- stage: snapshot.stage || RUN_STAGE_PREFLIGHT,
971
- last_message: "已收到取消请求,将在当前候选人处理完成后安全停止并落盘 CSV。",
972
- control: {
1019
+ }
1020
+ const reconciled = reconcileOrphanRunIfNeeded(runId, snapshot) || snapshot;
1021
+
1022
+ if (TERMINAL_RUN_STATES.has(reconciled.state)) {
1023
+ return {
1024
+ status: "CANCEL_IGNORED",
1025
+ run: reconciled,
1026
+ message: "目标任务已结束,无需取消。"
1027
+ };
1028
+ }
1029
+
1030
+ if (reconciled.state === RUN_STATE_PAUSED || !isProcessAlive(reconciled.pid)) {
1031
+ const canceledRun = finalizeCanceledRun(runId, reconciled);
1032
+ return {
1033
+ status: "CANCEL_REQUESTED",
1034
+ run: canceledRun
1035
+ };
1036
+ }
1037
+ safeUpdateRunState(runId, {
1038
+ stage: reconciled.stage || RUN_STAGE_PREFLIGHT,
1039
+ last_message: "已收到取消请求,将在当前候选人处理完成后安全停止并落盘 CSV。",
1040
+ control: {
973
1041
  pause_requested: true,
974
1042
  pause_requested_at: new Date().toISOString(),
975
1043
  pause_requested_by: TOOL_CANCEL_RUN,
976
1044
  cancel_requested: true
977
1045
  }
978
- });
979
-
980
- const latest = readRunState(runId) || snapshot;
981
- return {
982
- status: "CANCEL_REQUESTED",
1046
+ });
1047
+
1048
+ const latest = readRunState(runId) || reconciled;
1049
+ return {
1050
+ status: "CANCEL_REQUESTED",
983
1051
  run: latest
984
1052
  };
985
1053
  }
986
1054
 
987
- function handlePauseRunTool(args) {
1055
+ function handlePauseRunTool(args) {
988
1056
  const runId = normalizeText(args?.run_id);
989
1057
  if (!runId) {
990
1058
  return {
@@ -996,8 +1064,8 @@ function handlePauseRunTool(args) {
996
1064
  }
997
1065
  };
998
1066
  }
999
- const snapshot = readRunState(runId);
1000
- if (!snapshot) {
1067
+ const snapshot = readRunState(runId);
1068
+ if (!snapshot) {
1001
1069
  return {
1002
1070
  status: "FAILED",
1003
1071
  error: {
@@ -1006,22 +1074,23 @@ function handlePauseRunTool(args) {
1006
1074
  retryable: false
1007
1075
  }
1008
1076
  };
1009
- }
1010
-
1011
- if (TERMINAL_RUN_STATES.has(snapshot.state)) {
1012
- return {
1013
- status: "PAUSE_IGNORED",
1014
- run: snapshot,
1015
- message: "目标任务已结束,无需暂停。"
1016
- };
1017
- }
1018
- if (snapshot.state === RUN_STATE_PAUSED) {
1019
- return {
1020
- status: "PAUSE_IGNORED",
1021
- run: snapshot,
1022
- message: "目标任务已经处于 paused 状态。"
1023
- };
1024
- }
1077
+ }
1078
+ const reconciled = reconcileOrphanRunIfNeeded(runId, snapshot) || snapshot;
1079
+
1080
+ if (TERMINAL_RUN_STATES.has(reconciled.state)) {
1081
+ return {
1082
+ status: "PAUSE_IGNORED",
1083
+ run: reconciled,
1084
+ message: "目标任务已结束,无需暂停。"
1085
+ };
1086
+ }
1087
+ if (reconciled.state === RUN_STATE_PAUSED) {
1088
+ return {
1089
+ status: "PAUSE_IGNORED",
1090
+ run: reconciled,
1091
+ message: "目标任务已经处于 paused 状态。"
1092
+ };
1093
+ }
1025
1094
 
1026
1095
  const requestedRun = safeUpdateRunState(runId, {
1027
1096
  control: {
@@ -1031,15 +1100,15 @@ function handlePauseRunTool(args) {
1031
1100
  cancel_requested: false
1032
1101
  },
1033
1102
  last_message: "已收到暂停请求,将在当前候选人处理完成后暂停。"
1034
- }) || readRunState(runId) || snapshot;
1035
- return {
1103
+ }) || readRunState(runId) || reconciled;
1104
+ return {
1036
1105
  status: "PAUSE_REQUESTED",
1037
1106
  run: requestedRun,
1038
1107
  message: "暂停请求已接收,将在当前候选人处理完成后进入 paused。"
1039
1108
  };
1040
1109
  }
1041
1110
 
1042
- function handleResumeRunTool(args) {
1111
+ function handleResumeRunTool(args) {
1043
1112
  const runId = normalizeText(args?.run_id);
1044
1113
  if (!runId) {
1045
1114
  return {
@@ -1051,8 +1120,8 @@ function handleResumeRunTool(args) {
1051
1120
  }
1052
1121
  };
1053
1122
  }
1054
- const snapshot = readRunState(runId);
1055
- if (!snapshot) {
1123
+ const snapshot = readRunState(runId);
1124
+ if (!snapshot) {
1056
1125
  return {
1057
1126
  status: "FAILED",
1058
1127
  error: {
@@ -1061,38 +1130,32 @@ function handleResumeRunTool(args) {
1061
1130
  retryable: false
1062
1131
  }
1063
1132
  };
1064
- }
1065
- if (TERMINAL_RUN_STATES.has(snapshot.state)) {
1066
- return {
1067
- status: "FAILED",
1133
+ }
1134
+ const reconciled = reconcileOrphanRunIfNeeded(runId, snapshot) || snapshot;
1135
+ if (TERMINAL_RUN_STATES.has(reconciled.state)) {
1136
+ return {
1137
+ status: "FAILED",
1068
1138
  error: {
1069
1139
  code: "RUN_ALREADY_TERMINATED",
1070
1140
  message: "目标任务已结束,无法继续。",
1071
1141
  retryable: false
1072
1142
  }
1073
1143
  };
1074
- }
1075
- if (snapshot.state !== RUN_STATE_PAUSED) {
1076
- return {
1077
- status: "FAILED",
1144
+ }
1145
+ if (reconciled.state !== RUN_STATE_PAUSED) {
1146
+ return {
1147
+ status: "FAILED",
1078
1148
  error: {
1079
1149
  code: "RUN_NOT_PAUSED",
1080
1150
  message: "仅 paused 状态的 run 才能继续。",
1081
1151
  retryable: true
1082
1152
  },
1083
- run: snapshot
1084
- };
1085
- }
1086
- if (activeAsyncRuns.has(runId)) {
1087
- return {
1088
- status: "RESUME_IGNORED",
1089
- run: snapshot,
1090
- message: "该 run 当前已在执行,无需继续。"
1091
- };
1092
- }
1093
-
1094
- const executionContext = resolveRunContext(snapshot);
1095
- if (!executionContext) {
1153
+ run: reconciled
1154
+ };
1155
+ }
1156
+
1157
+ const executionContext = resolveRunContext(reconciled);
1158
+ if (!executionContext) {
1096
1159
  return {
1097
1160
  status: "FAILED",
1098
1161
  error: {
@@ -1119,23 +1182,43 @@ function handleResumeRunTool(args) {
1119
1182
  resume_count: Number.isInteger(current?.resume?.resume_count) ? current.resume.resume_count + 1 : 1,
1120
1183
  last_resumed_at: new Date().toISOString()
1121
1184
  }
1122
- })) || readRunState(runId) || snapshot;
1123
-
1124
- launchAsyncRun({
1125
- runId,
1126
- mode: RUN_MODE_ASYNC,
1127
- workspaceRoot: executionContext.workspaceRoot,
1128
- args: executionContext.args,
1129
- resumeRun: true
1130
- });
1131
-
1132
- return {
1133
- status: "RESUME_REQUESTED",
1134
- run: updated,
1135
- poll_after_sec: getDefaultPollAfterSec(),
1136
- message: "已恢复 Recommend 流水线。默认不自动轮询;如需进度请按需调用 get_recommend_pipeline_run。"
1137
- };
1138
- }
1185
+ })) || readRunState(runId) || reconciled;
1186
+
1187
+ let worker;
1188
+ try {
1189
+ worker = launchDetachedRunWorker({
1190
+ runId,
1191
+ resumeRun: true
1192
+ });
1193
+ } catch (error) {
1194
+ const failedMessage = `无法启动 detached 恢复进程:${error?.message || "unknown"}`;
1195
+ safeUpdateRunState(runId, {
1196
+ state: RUN_STATE_FAILED,
1197
+ stage: reconciled.stage || RUN_STAGE_PREFLIGHT,
1198
+ last_message: failedMessage,
1199
+ error: {
1200
+ code: "RUN_WORKER_LAUNCH_FAILED",
1201
+ message: failedMessage,
1202
+ retryable: true
1203
+ },
1204
+ result: buildWorkerLaunchFailedPayload(failedMessage)
1205
+ });
1206
+ return buildWorkerLaunchFailedPayload(failedMessage);
1207
+ }
1208
+
1209
+ const started = safeUpdateRunState(runId, {
1210
+ pid: worker?.pid,
1211
+ state: "queued",
1212
+ last_message: "已恢复 Recommend 流水线(detached)。"
1213
+ }) || readRunState(runId) || updated;
1214
+
1215
+ return {
1216
+ status: "RESUME_REQUESTED",
1217
+ run: started,
1218
+ poll_after_sec: getDefaultPollAfterSec(),
1219
+ message: "已恢复 Recommend 流水线(detached)。默认不自动轮询;如需进度请按需调用 get_recommend_pipeline_run。"
1220
+ };
1221
+ }
1139
1222
 
1140
1223
  async function handleRequest(message, workspaceRoot) {
1141
1224
  if (!message || message.jsonrpc !== "2.0") {
@@ -1329,15 +1412,34 @@ export function startServer() {
1329
1412
  });
1330
1413
  }
1331
1414
 
1332
- export const __testables = {
1333
- handleRequest,
1334
- activeAsyncRuns,
1335
- setRunPipelineImplForTests(nextImpl) {
1336
- runPipelineImpl = typeof nextImpl === "function" ? nextImpl : runRecommendPipeline;
1337
- }
1338
- };
1339
-
1340
- const thisFilePath = fileURLToPath(import.meta.url);
1341
- if (process.argv[1] && path.resolve(process.argv[1]) === thisFilePath) {
1342
- startServer();
1343
- }
1415
+ export const __testables = {
1416
+ handleRequest,
1417
+ runDetachedWorkerForTests(options = {}) {
1418
+ return runDetachedWorker(options);
1419
+ },
1420
+ setSpawnProcessImplForTests(nextImpl) {
1421
+ spawnProcessImpl = typeof nextImpl === "function" ? nextImpl : spawn;
1422
+ },
1423
+ setRunPipelineImplForTests(nextImpl) {
1424
+ runPipelineImpl = typeof nextImpl === "function" ? nextImpl : runRecommendPipeline;
1425
+ }
1426
+ };
1427
+
1428
+ const thisFilePath = fileURLToPath(import.meta.url);
1429
+ if (process.argv[1] && path.resolve(process.argv[1]) === thisFilePath) {
1430
+ const detachedWorkerOptions = parseDetachedWorkerOptions(process.argv.slice(2));
1431
+ if (detachedWorkerOptions) {
1432
+ runDetachedWorker({
1433
+ runId: detachedWorkerOptions.runId,
1434
+ resumeRun: detachedWorkerOptions.resumeRun
1435
+ }).then((result) => {
1436
+ if (!result?.ok) {
1437
+ process.exitCode = 1;
1438
+ }
1439
+ }).catch(() => {
1440
+ process.exitCode = 1;
1441
+ });
1442
+ } else {
1443
+ startServer();
1444
+ }
1445
+ }