@reconcrap/boss-recommend-mcp 1.1.7 → 1.1.8

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.
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
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,
@@ -33,18 +34,39 @@ const TOOL_CANCEL_RUN = "cancel_recommend_pipeline_run";
33
34
  const TOOL_PAUSE_RUN = "pause_recommend_pipeline_run";
34
35
  const TOOL_RESUME_RUN = "resume_recommend_pipeline_run";
35
36
 
36
- const SERVER_NAME = "boss-recommend-mcp";
37
- const FRAMING_UNKNOWN = "unknown";
38
- const FRAMING_HEADER = "header";
39
- const FRAMING_LINE = "line";
37
+ const SERVER_NAME = "boss-recommend-mcp";
38
+ const FRAMING_UNKNOWN = "unknown";
39
+ const FRAMING_HEADER = "header";
40
+ const FRAMING_LINE = "line";
41
+ const ASYNC_WORKER_FLAG = "--async-worker";
42
+ const thisFilePath = fileURLToPath(import.meta.url);
40
43
 
41
44
  const activeAsyncRuns = new Map();
42
45
  let runPipelineImpl = runRecommendPipeline;
43
46
  const TERMINAL_RUN_STATES = new Set([RUN_STATE_COMPLETED, RUN_STATE_FAILED, RUN_STATE_CANCELED]);
44
47
 
45
- function normalizeText(value) {
46
- return String(value || "").replace(/\s+/g, " ").trim();
47
- }
48
+ function normalizeText(value) {
49
+ return String(value || "").replace(/\s+/g, " ").trim();
50
+ }
51
+
52
+ function encodeWorkerPayload(payload) {
53
+ return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
54
+ }
55
+
56
+ function decodeWorkerPayload(encoded) {
57
+ try {
58
+ const raw = Buffer.from(String(encoded || ""), "base64").toString("utf8");
59
+ const parsed = JSON.parse(raw);
60
+ if (!parsed || typeof parsed !== "object") return null;
61
+ return parsed;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function shouldUseInProcessAsyncRunner() {
68
+ return normalizeText(process.env.BOSS_RECOMMEND_ASYNC_INPROC || "") === "1";
69
+ }
48
70
 
49
71
  function parsePositiveInteger(raw, fallback) {
50
72
  const value = Number.parseInt(String(raw || ""), 10);
@@ -726,14 +748,14 @@ async function executeTrackedPipeline({
726
748
  };
727
749
  }
728
750
 
729
- function initializeRunStateOrThrow(runId, mode, workspaceRoot, args) {
730
- const artifacts = getRunArtifacts(runId);
731
- const snapshot = createRunStateSnapshot({
732
- runId,
733
- mode,
734
- state: "queued",
735
- stage: RUN_STAGE_PREFLIGHT,
736
- pid: process.pid,
751
+ function initializeRunStateOrThrow(runId, mode, workspaceRoot, args, pid = process.pid) {
752
+ const artifacts = getRunArtifacts(runId);
753
+ const snapshot = createRunStateSnapshot({
754
+ runId,
755
+ mode,
756
+ state: "queued",
757
+ stage: RUN_STAGE_PREFLIGHT,
758
+ pid,
737
759
  lastMessage: "流水线任务已创建,等待执行。",
738
760
  context: buildRunContext(workspaceRoot, args),
739
761
  control: {
@@ -754,8 +776,8 @@ function initializeRunStateOrThrow(runId, mode, workspaceRoot, args) {
754
776
  return writeRunState(snapshot);
755
777
  }
756
778
 
757
- function launchAsyncRun({ runId, mode, workspaceRoot, args, resumeRun = false }) {
758
- const abortController = new AbortController();
779
+ function launchAsyncRun({ runId, mode, workspaceRoot, args, resumeRun = false }) {
780
+ const abortController = new AbortController();
759
781
  const promise = executeTrackedPipeline({
760
782
  runId,
761
783
  mode,
@@ -769,9 +791,80 @@ function launchAsyncRun({ runId, mode, workspaceRoot, args, resumeRun = false })
769
791
  activeAsyncRuns.set(runId, {
770
792
  abortController,
771
793
  promise
772
- });
773
- return { abortController, promise };
774
- }
794
+ });
795
+ return { abortController, promise };
796
+ }
797
+
798
+ function launchDetachedAsyncRun({ runId, mode, workspaceRoot, args, resumeRun = false }) {
799
+ const payload = encodeWorkerPayload({
800
+ runId,
801
+ mode,
802
+ workspaceRoot,
803
+ args,
804
+ resumeRun
805
+ });
806
+ const child = spawn(
807
+ process.execPath,
808
+ [thisFilePath, ASYNC_WORKER_FLAG, payload],
809
+ {
810
+ detached: true,
811
+ stdio: "ignore",
812
+ cwd: path.resolve(workspaceRoot),
813
+ env: process.env
814
+ }
815
+ );
816
+ child.unref();
817
+ return child;
818
+ }
819
+
820
+ async function runAsyncWorkerMain(encodedPayload) {
821
+ const payload = decodeWorkerPayload(encodedPayload);
822
+ if (!payload) return;
823
+ const runId = normalizeText(payload.runId);
824
+ const mode = normalizeText(payload.mode) || RUN_MODE_ASYNC;
825
+ const workspaceRoot = normalizeText(payload.workspaceRoot);
826
+ const args = payload.args && typeof payload.args === "object" ? payload.args : {};
827
+ const resumeRun = payload.resumeRun === true;
828
+ if (!runId || !workspaceRoot || typeof args.instruction !== "string") return;
829
+
830
+ const existing = readRunState(runId);
831
+ if (!existing) {
832
+ try {
833
+ initializeRunStateOrThrow(runId, mode, workspaceRoot, args, process.pid);
834
+ } catch {
835
+ return;
836
+ }
837
+ } else {
838
+ safeUpdateRunState(runId, { mode, pid: process.pid });
839
+ }
840
+
841
+ try {
842
+ await executeTrackedPipeline({
843
+ runId,
844
+ mode,
845
+ workspaceRoot,
846
+ args,
847
+ signal: null,
848
+ resumeRun
849
+ });
850
+ } catch (error) {
851
+ const failedResult = {
852
+ status: "FAILED",
853
+ error: {
854
+ code: "UNEXPECTED_ERROR",
855
+ message: error?.message || "Unexpected worker error",
856
+ retryable: true
857
+ }
858
+ };
859
+ safeUpdateRunState(runId, {
860
+ state: RUN_STATE_FAILED,
861
+ stage: RUN_STAGE_PREFLIGHT,
862
+ last_message: failedResult.error.message,
863
+ error: failedResult.error,
864
+ result: failedResult
865
+ });
866
+ }
867
+ }
775
868
 
776
869
  async function handleStartRunTool({ workspaceRoot, args }) {
777
870
  const precheckArgs = buildAsyncPrecheckArgs(args);
@@ -805,29 +898,58 @@ async function handleStartRunTool({ workspaceRoot, args }) {
805
898
  return precheckResult;
806
899
  }
807
900
 
808
- cleanupExpiredRuns();
809
- const runId = createRunId();
810
- try {
811
- initializeRunStateOrThrow(runId, RUN_MODE_ASYNC, workspaceRoot, args);
812
- } catch (error) {
813
- return {
814
- status: "FAILED",
901
+ cleanupExpiredRuns();
902
+ const runId = createRunId();
903
+ try {
904
+ initializeRunStateOrThrow(runId, RUN_MODE_ASYNC, workspaceRoot, args, process.pid);
905
+ } catch (error) {
906
+ return {
907
+ status: "FAILED",
815
908
  error: {
816
909
  code: "RUN_STATE_IO_ERROR",
817
910
  message: `无法写入运行状态目录:${error.message || "unknown"}`,
818
911
  retryable: false
819
- }
820
- };
821
- }
822
-
823
- launchAsyncRun({
824
- runId,
825
- mode: RUN_MODE_ASYNC,
826
- workspaceRoot,
827
- args
828
- });
829
-
830
- return {
912
+ }
913
+ };
914
+ }
915
+
916
+ try {
917
+ if (shouldUseInProcessAsyncRunner()) {
918
+ launchAsyncRun({
919
+ runId,
920
+ mode: RUN_MODE_ASYNC,
921
+ workspaceRoot,
922
+ args
923
+ });
924
+ } else {
925
+ const worker = launchDetachedAsyncRun({
926
+ runId,
927
+ mode: RUN_MODE_ASYNC,
928
+ workspaceRoot,
929
+ args
930
+ });
931
+ safeUpdateRunState(runId, { pid: worker.pid || process.pid });
932
+ }
933
+ } catch (error) {
934
+ const failed = {
935
+ status: "FAILED",
936
+ error: {
937
+ code: "RUN_START_FAILED",
938
+ message: error?.message || "无法启动异步 worker 进程。",
939
+ retryable: true
940
+ }
941
+ };
942
+ safeUpdateRunState(runId, {
943
+ state: RUN_STATE_FAILED,
944
+ stage: RUN_STAGE_PREFLIGHT,
945
+ last_message: failed.error.message,
946
+ error: failed.error,
947
+ result: failed
948
+ });
949
+ return failed;
950
+ }
951
+
952
+ return {
831
953
  status: "ACCEPTED",
832
954
  run_id: runId,
833
955
  state: "queued",
@@ -928,8 +1050,9 @@ function handleCancelRunTool(args) {
928
1050
  };
929
1051
  }
930
1052
 
931
- const activeRun = activeAsyncRuns.get(runId);
932
- if (!activeRun) {
1053
+ const activeRun = activeAsyncRuns.get(runId);
1054
+ const hasLiveDetachedWorker = isProcessAlive(snapshot.pid);
1055
+ if (!activeRun && !hasLiveDetachedWorker) {
933
1056
  const canceledResult = {
934
1057
  status: "FAILED",
935
1058
  error: {
@@ -1112,15 +1235,45 @@ function handleResumeRunTool(args) {
1112
1235
  }
1113
1236
  })) || readRunState(runId) || snapshot;
1114
1237
 
1115
- launchAsyncRun({
1116
- runId,
1117
- mode: RUN_MODE_ASYNC,
1118
- workspaceRoot: executionContext.workspaceRoot,
1119
- args: executionContext.args,
1120
- resumeRun: true
1121
- });
1122
-
1123
- return {
1238
+ try {
1239
+ if (shouldUseInProcessAsyncRunner()) {
1240
+ launchAsyncRun({
1241
+ runId,
1242
+ mode: RUN_MODE_ASYNC,
1243
+ workspaceRoot: executionContext.workspaceRoot,
1244
+ args: executionContext.args,
1245
+ resumeRun: true
1246
+ });
1247
+ } else {
1248
+ const worker = launchDetachedAsyncRun({
1249
+ runId,
1250
+ mode: RUN_MODE_ASYNC,
1251
+ workspaceRoot: executionContext.workspaceRoot,
1252
+ args: executionContext.args,
1253
+ resumeRun: true
1254
+ });
1255
+ safeUpdateRunState(runId, { pid: worker.pid || updated.pid || process.pid });
1256
+ }
1257
+ } catch (error) {
1258
+ const failed = {
1259
+ status: "FAILED",
1260
+ error: {
1261
+ code: "RUN_RESUME_FAILED",
1262
+ message: error?.message || "无法启动继续执行 worker 进程。",
1263
+ retryable: true
1264
+ }
1265
+ };
1266
+ safeUpdateRunState(runId, {
1267
+ state: RUN_STATE_FAILED,
1268
+ stage: updated.stage || RUN_STAGE_PREFLIGHT,
1269
+ last_message: failed.error.message,
1270
+ error: failed.error,
1271
+ result: failed
1272
+ });
1273
+ return failed;
1274
+ }
1275
+
1276
+ return {
1124
1277
  status: "RESUME_REQUESTED",
1125
1278
  run: updated,
1126
1279
  poll_after_sec: getDefaultPollAfterSec(),
@@ -1320,15 +1473,21 @@ export function startServer() {
1320
1473
  });
1321
1474
  }
1322
1475
 
1323
- export const __testables = {
1324
- handleRequest,
1325
- activeAsyncRuns,
1326
- setRunPipelineImplForTests(nextImpl) {
1327
- runPipelineImpl = typeof nextImpl === "function" ? nextImpl : runRecommendPipeline;
1328
- }
1329
- };
1330
-
1331
- const thisFilePath = fileURLToPath(import.meta.url);
1332
- if (process.argv[1] && path.resolve(process.argv[1]) === thisFilePath) {
1333
- startServer();
1334
- }
1476
+ export const __testables = {
1477
+ handleRequest,
1478
+ activeAsyncRuns,
1479
+ runAsyncWorkerMain,
1480
+ setRunPipelineImplForTests(nextImpl) {
1481
+ runPipelineImpl = typeof nextImpl === "function" ? nextImpl : runRecommendPipeline;
1482
+ }
1483
+ };
1484
+
1485
+ if (process.argv[1] && path.resolve(process.argv[1]) === thisFilePath) {
1486
+ const workerFlagIndex = process.argv.indexOf(ASYNC_WORKER_FLAG);
1487
+ if (workerFlagIndex !== -1) {
1488
+ const payloadArg = process.argv[workerFlagIndex + 1] || "";
1489
+ runAsyncWorkerMain(payloadArg).catch(() => {});
1490
+ } else {
1491
+ startServer();
1492
+ }
1493
+ }