@reconcrap/boss-recommend-mcp 1.1.2 → 1.1.4

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,1254 +1,1254 @@
1
- import path from "node:path";
2
- import { createRequire } from "node:module";
3
- import process from "node:process";
4
- import { fileURLToPath } from "node:url";
5
- import { runRecommendPipeline } from "./pipeline.js";
6
- import {
7
- RUN_MODE_ASYNC,
8
- RUN_STAGE_PREFLIGHT,
9
- RUN_STATE_CANCELED,
10
- RUN_STATE_COMPLETED,
11
- RUN_STATE_FAILED,
12
- RUN_STATE_PAUSED,
13
- RUN_STATE_RUNNING,
14
- cleanupExpiredRuns,
15
- createRunId,
16
- createRunStateSnapshot,
17
- getRunHeartbeatIntervalMs,
18
- getRunsDir,
19
- readRunState,
20
- touchRunHeartbeat,
21
- updateRunProgress,
22
- updateRunState,
23
- writeRunState
24
- } from "./run-state.js";
25
-
26
- const require = createRequire(import.meta.url);
27
- const { version: SERVER_VERSION } = require("../package.json");
28
-
29
- const TOOL_START_RUN = "start_recommend_pipeline_run";
30
- const TOOL_GET_RUN = "get_recommend_pipeline_run";
31
- const TOOL_CANCEL_RUN = "cancel_recommend_pipeline_run";
32
- const TOOL_PAUSE_RUN = "pause_recommend_pipeline_run";
33
- const TOOL_RESUME_RUN = "resume_recommend_pipeline_run";
34
-
35
- const SERVER_NAME = "boss-recommend-mcp";
36
- const FRAMING_UNKNOWN = "unknown";
37
- const FRAMING_HEADER = "header";
38
- const FRAMING_LINE = "line";
39
-
40
- const activeAsyncRuns = new Map();
41
- let runPipelineImpl = runRecommendPipeline;
42
- const TERMINAL_RUN_STATES = new Set([RUN_STATE_COMPLETED, RUN_STATE_FAILED, RUN_STATE_CANCELED]);
43
-
44
- function normalizeText(value) {
45
- return String(value || "").replace(/\s+/g, " ").trim();
46
- }
47
-
48
- function parsePositiveInteger(raw, fallback) {
49
- const value = Number.parseInt(String(raw || ""), 10);
50
- return Number.isFinite(value) && value > 0 ? value : fallback;
51
- }
52
-
53
- function getDefaultPollAfterSec() {
54
- const fromEnv = parsePositiveInteger(process.env.BOSS_RECOMMEND_POLL_AFTER_SEC, 10);
55
- return Math.max(5, Math.min(15, fromEnv));
56
- }
57
-
58
- function getRunArtifacts(runId) {
59
- const normalizedRunId = normalizeText(runId);
60
- return {
61
- run_state_path: path.join(getRunsDir(), `${normalizedRunId}.json`),
62
- checkpoint_path: path.join(getRunsDir(), `${normalizedRunId}.checkpoint.json`)
63
- };
64
- }
65
-
66
- function buildRunContext(workspaceRoot, args = {}) {
67
- return {
68
- workspace_root: path.resolve(workspaceRoot),
69
- instruction: String(args?.instruction || ""),
70
- confirmation: args?.confirmation && typeof args.confirmation === "object" ? args.confirmation : {},
71
- overrides: args?.overrides && typeof args.overrides === "object" ? args.overrides : {}
72
- };
73
- }
74
-
75
- function resolveRunContext(snapshot) {
76
- const workspaceRoot = normalizeText(snapshot?.context?.workspace_root || "");
77
- const instruction = typeof snapshot?.context?.instruction === "string"
78
- ? snapshot.context.instruction
79
- : "";
80
- if (!workspaceRoot || !instruction.trim()) return null;
81
- return {
82
- workspaceRoot,
83
- args: {
84
- instruction,
85
- confirmation: snapshot?.context?.confirmation && typeof snapshot.context.confirmation === "object"
86
- ? snapshot.context.confirmation
87
- : {},
88
- overrides: snapshot?.context?.overrides && typeof snapshot.context.overrides === "object"
89
- ? snapshot.context.overrides
90
- : {}
91
- }
92
- };
93
- }
94
-
95
- function isRunPauseRequested(runId) {
96
- const snapshot = readRunState(runId);
97
- return snapshot?.control?.pause_requested === true;
98
- }
99
-
100
- function isRunCancelRequested(runId) {
101
- const snapshot = readRunState(runId);
102
- return snapshot?.control?.cancel_requested === true;
103
- }
104
-
105
- function getOutputCsvFromResult(result) {
106
- const direct = normalizeText(result?.result?.output_csv || "");
107
- if (direct) return direct;
108
- const partial = normalizeText(result?.partial_result?.output_csv || "");
109
- if (partial) return partial;
110
- return null;
111
- }
112
-
113
- function getCompletionReasonFromResult(result) {
114
- const direct = normalizeText(result?.result?.completion_reason || "");
115
- if (direct) return direct;
116
- const partial = normalizeText(result?.partial_result?.completion_reason || "");
117
- if (partial) return partial;
118
- return null;
119
- }
120
-
121
- function writeMessage(message, framing = FRAMING_LINE) {
122
- const body = JSON.stringify(message);
123
- if (framing === FRAMING_HEADER) {
124
- const header = `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n`;
125
- process.stdout.write(header + body);
126
- return;
127
- }
128
- process.stdout.write(`${body}\n`);
129
- }
130
-
131
- function createJsonRpcError(id, code, message) {
132
- return {
133
- jsonrpc: "2.0",
134
- id: id ?? null,
135
- error: { code, message }
136
- };
137
- }
138
-
139
- function createRunInputSchema() {
140
- return {
141
- type: "object",
142
- properties: {
143
- instruction: {
144
- type: "string",
145
- description: "用户自然语言推荐筛选指令"
146
- },
147
- confirmation: {
148
- type: "object",
149
- properties: {
150
- filters_confirmed: { type: "boolean" },
151
- school_tag_confirmed: { type: "boolean" },
152
- school_tag_value: {
153
- oneOf: [
154
- {
155
- type: "string",
156
- enum: ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"]
157
- },
158
- {
159
- type: "array",
160
- items: {
161
- type: "string",
162
- enum: ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"]
163
- },
164
- minItems: 1,
165
- uniqueItems: true
166
- }
167
- ]
168
- },
169
- degree_confirmed: { type: "boolean" },
170
- degree_value: {
171
- oneOf: [
172
- {
173
- type: "string",
174
- enum: ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"]
175
- },
176
- {
177
- type: "array",
178
- items: {
179
- type: "string",
180
- enum: ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"]
181
- },
182
- minItems: 1,
183
- uniqueItems: true
184
- }
185
- ]
186
- },
187
- gender_confirmed: { type: "boolean" },
188
- gender_value: {
189
- type: "string",
190
- enum: ["不限", "男", "女"]
191
- },
192
- recent_not_view_confirmed: { type: "boolean" },
193
- recent_not_view_value: {
194
- type: "string",
195
- enum: ["不限", "近14天没有"]
196
- },
197
- criteria_confirmed: { type: "boolean" },
198
- target_count_confirmed: { type: "boolean" },
199
- target_count_value: {
200
- type: "integer",
201
- minimum: 1
202
- },
203
- post_action_confirmed: { type: "boolean" },
204
- post_action_value: {
205
- type: "string",
206
- enum: ["favorite", "greet"]
207
- },
208
- final_confirmed: { type: "boolean" },
209
- job_confirmed: { type: "boolean" },
210
- job_value: { type: "string" },
211
- max_greet_count_confirmed: { type: "boolean" },
212
- max_greet_count_value: {
213
- type: "integer",
214
- minimum: 1
215
- }
216
- },
217
- additionalProperties: false
218
- },
219
- overrides: {
220
- type: "object",
221
- properties: {
222
- school_tag: {
223
- oneOf: [
224
- {
225
- type: "string",
226
- enum: ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"]
227
- },
228
- {
229
- type: "array",
230
- items: {
231
- type: "string",
232
- enum: ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"]
233
- },
234
- minItems: 1,
235
- uniqueItems: true
236
- }
237
- ]
238
- },
239
- degree: {
240
- oneOf: [
241
- {
242
- type: "string",
243
- enum: ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"]
244
- },
245
- {
246
- type: "array",
247
- items: {
248
- type: "string",
249
- enum: ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"]
250
- },
251
- minItems: 1,
252
- uniqueItems: true
253
- }
254
- ]
255
- },
256
- gender: {
257
- type: "string",
258
- enum: ["不限", "男", "女"]
259
- },
260
- recent_not_view: {
261
- type: "string",
262
- enum: ["不限", "近14天没有"]
263
- },
264
- criteria: { type: "string" },
265
- job: { type: "string" },
266
- target_count: { type: "integer", minimum: 1 },
267
- max_greet_count: { type: "integer", minimum: 1 },
268
- post_action: {
269
- type: "string",
270
- enum: ["favorite", "greet"]
271
- }
272
- },
273
- additionalProperties: false
274
- }
275
- },
276
- required: ["instruction"],
277
- additionalProperties: false
278
- };
279
- }
280
-
281
- function createToolsSchema() {
282
- return [
283
- {
284
- name: TOOL_START_RUN,
285
- description: "异步启动 Boss 推荐页流水线(含同步门禁预检);只有在前置确认与页面就绪通过后才返回 run_id。",
286
- inputSchema: createRunInputSchema()
287
- },
288
- {
289
- name: TOOL_GET_RUN,
290
- description: "按 run_id 查询异步/同步流水线运行状态快照。",
291
- inputSchema: {
292
- type: "object",
293
- properties: {
294
- run_id: { type: "string" }
295
- },
296
- required: ["run_id"],
297
- additionalProperties: false
298
- }
299
- },
300
- {
301
- name: TOOL_CANCEL_RUN,
302
- description: "取消指定 run_id 的运行中流水线。",
303
- inputSchema: {
304
- type: "object",
305
- properties: {
306
- run_id: { type: "string" }
307
- },
308
- required: ["run_id"],
309
- additionalProperties: false
310
- }
311
- },
312
- {
313
- name: TOOL_PAUSE_RUN,
314
- description: "请求暂停指定 run_id 的流水线;会在当前候选人处理完成后进入 paused。",
315
- inputSchema: {
316
- type: "object",
317
- properties: {
318
- run_id: { type: "string" }
319
- },
320
- required: ["run_id"],
321
- additionalProperties: false
322
- }
323
- },
324
- {
325
- name: TOOL_RESUME_RUN,
326
- description: "继续指定 run_id 的 paused 流水线;沿用原 CSV 与 checkpoint 续跑。",
327
- inputSchema: {
328
- type: "object",
329
- properties: {
330
- run_id: { type: "string" }
331
- },
332
- required: ["run_id"],
333
- additionalProperties: false
334
- }
335
- }
336
- ];
337
- }
338
-
339
- function createToolResultResponse(id, payload, isError = false) {
340
- return {
341
- jsonrpc: "2.0",
342
- id,
343
- result: {
344
- content: [
345
- {
346
- type: "text",
347
- text: JSON.stringify(payload, null, 2)
348
- }
349
- ],
350
- structuredContent: payload,
351
- ...(isError ? { isError: true } : {})
352
- }
353
- };
354
- }
355
-
356
- function validateRunArgs(args) {
357
- if (!args || typeof args !== "object") {
358
- return "arguments must be an object";
359
- }
360
- if (!args.instruction || typeof args.instruction !== "string") {
361
- return "instruction is required and must be a string";
362
- }
363
- return null;
364
- }
365
-
366
- function getLastOutputLine(text) {
367
- const lines = String(text || "")
368
- .split(/\r?\n/)
369
- .map((line) => normalizeText(line))
370
- .filter(Boolean);
371
- return lines.length > 0 ? lines[lines.length - 1] : null;
372
- }
373
-
374
- function normalizeRequiredConfirmations(value) {
375
- if (!Array.isArray(value)) return [];
376
- return value
377
- .map((item) => normalizeText(item))
378
- .filter(Boolean);
379
- }
380
-
381
- function hasExplicitFinalConfirmation(args) {
382
- return args?.confirmation?.final_confirmed === true;
383
- }
384
-
385
- function buildAsyncPrecheckConfirmation(confirmation) {
386
- if (!confirmation || typeof confirmation !== "object") {
387
- return {
388
- final_confirmed: false
389
- };
390
- }
391
- return {
392
- ...confirmation,
393
- final_confirmed: false
394
- };
395
- }
396
-
397
- function buildAsyncPrecheckArgs(args) {
398
- return {
399
- instruction: args.instruction,
400
- confirmation: buildAsyncPrecheckConfirmation(args.confirmation),
401
- overrides: args.overrides
402
- };
403
- }
404
-
405
- function isFinalReviewOnlyConfirmation(result) {
406
- if (result?.status !== "NEED_CONFIRMATION") return false;
407
- const required = normalizeRequiredConfirmations(result.required_confirmations);
408
- return required.length > 0 && required.every((item) => item === "final_review");
409
- }
410
-
411
- function safeUpdateRunState(runId, updater) {
412
- try {
413
- return updateRunState(runId, updater);
414
- } catch {
415
- return null;
416
- }
417
- }
418
-
419
- function safeUpdateRunProgress(runId, patch, message = null) {
420
- try {
421
- return updateRunProgress(runId, patch, message);
422
- } catch {
423
- return null;
424
- }
425
- }
426
-
427
- function createRuntimeCallbacks(runId, heartbeatIntervalMs) {
428
- let lastStage = RUN_STAGE_PREFLIGHT;
429
- let lastOutputPersistAt = 0;
430
- return {
431
- heartbeatIntervalMs,
432
- onStage(event) {
433
- const stage = normalizeText(event?.stage) || RUN_STAGE_PREFLIGHT;
434
- lastStage = stage;
435
- safeUpdateRunState(runId, {
436
- state: RUN_STATE_RUNNING,
437
- stage,
438
- last_message: normalizeText(event?.message || "")
439
- });
440
- },
441
- onHeartbeat(event) {
442
- const stage = normalizeText(event?.stage) || lastStage;
443
- lastStage = stage || lastStage;
444
- const detailsMessage = normalizeText(event?.details?.message || "");
445
- const patch = { stage: lastStage };
446
- if (detailsMessage) {
447
- patch.last_message = detailsMessage;
448
- }
449
- safeUpdateRunState(runId, patch);
450
- try {
451
- touchRunHeartbeat(runId, detailsMessage || undefined);
452
- } catch {
453
- // Ignore heartbeat persistence failures here; state updates above already best-effort.
454
- }
455
- },
456
- onOutput(event) {
457
- const stage = normalizeText(event?.stage) || lastStage;
458
- lastStage = stage || lastStage;
459
- const now = Date.now();
460
- if (now - lastOutputPersistAt < 1000) return;
461
- lastOutputPersistAt = now;
462
- const message = getLastOutputLine(event?.text);
463
- if (!message) return;
464
- safeUpdateRunState(runId, {
465
- stage: lastStage,
466
- last_message: message
467
- });
468
- },
469
- onProgress(event) {
470
- const stage = normalizeText(event?.stage) || lastStage;
471
- lastStage = stage || lastStage;
472
- safeUpdateRunState(runId, { stage: lastStage });
473
- safeUpdateRunProgress(
474
- runId,
475
- {
476
- processed: Number.isInteger(event?.processed) ? event.processed : undefined,
477
- passed: Number.isInteger(event?.passed) ? event.passed : undefined,
478
- skipped: Number.isInteger(event?.skipped) ? event.skipped : undefined,
479
- greet_count: Number.isInteger(event?.greet_count) ? event.greet_count : undefined
480
- },
481
- normalizeText(event?.line || "")
482
- );
483
- },
484
- getLastStage() {
485
- return lastStage;
486
- }
487
- };
488
- }
489
-
490
-
491
- async function executeTrackedPipeline({
492
- runId,
493
- mode,
494
- workspaceRoot,
495
- args,
496
- signal,
497
- resumeRun = false
498
- }) {
499
- const heartbeatIntervalMs = getRunHeartbeatIntervalMs();
500
- const runtimeCallbacks = createRuntimeCallbacks(runId, heartbeatIntervalMs);
501
- const artifacts = getRunArtifacts(runId);
502
- const existingSnapshot = readRunState(runId);
503
- const resumeConfig = {
504
- resume: resumeRun === true,
505
- checkpoint_path: normalizeText(existingSnapshot?.resume?.checkpoint_path || artifacts.checkpoint_path),
506
- pause_control_path: normalizeText(existingSnapshot?.resume?.pause_control_path || artifacts.run_state_path),
507
- output_csv: normalizeText(existingSnapshot?.resume?.output_csv || "") || null,
508
- previous_completion_reason: getCompletionReasonFromResult(existingSnapshot?.result || null)
509
- };
510
- safeUpdateRunState(runId, {
511
- state: RUN_STATE_RUNNING,
512
- stage: RUN_STAGE_PREFLIGHT,
513
- last_message: resumeRun
514
- ? "流水线继续执行中,等待 preflight。"
515
- : "流水线已启动,等待 preflight。",
516
- resume: resumeConfig
517
- });
518
-
519
- let result;
520
- try {
521
- result = await runPipelineImpl(
522
- {
523
- workspaceRoot,
524
- instruction: args.instruction,
525
- confirmation: args.confirmation,
526
- overrides: args.overrides,
527
- resume: resumeConfig
528
- },
529
- undefined,
530
- {
531
- signal,
532
- heartbeatIntervalMs,
533
- isPauseRequested: () => isRunPauseRequested(runId),
534
- onStage: runtimeCallbacks.onStage,
535
- onHeartbeat: runtimeCallbacks.onHeartbeat,
536
- onOutput: runtimeCallbacks.onOutput,
537
- onProgress: runtimeCallbacks.onProgress
538
- }
539
- );
540
- } catch (error) {
541
- const canceled = Boolean(signal?.aborted) || error?.code === "PIPELINE_ABORTED";
542
- if (canceled) {
543
- const canceledResult = {
544
- status: "FAILED",
545
- error: {
546
- code: "PIPELINE_CANCELED",
547
- message: "流水线已取消。",
548
- retryable: true
549
- }
550
- };
551
- safeUpdateRunState(runId, {
552
- mode,
553
- state: RUN_STATE_CANCELED,
554
- stage: runtimeCallbacks.getLastStage(),
555
- last_message: "流水线已取消。",
556
- control: {
557
- pause_requested: false,
558
- pause_requested_at: null,
559
- pause_requested_by: null,
560
- cancel_requested: false
561
- },
562
- resume: {
563
- ...resumeConfig,
564
- output_csv: getOutputCsvFromResult(canceledResult) || resumeConfig.output_csv
565
- },
566
- error: canceledResult.error,
567
- result: canceledResult
568
- });
569
- return {
570
- result: canceledResult,
571
- lastStage: runtimeCallbacks.getLastStage(),
572
- state: RUN_STATE_CANCELED
573
- };
574
- }
575
-
576
- const failedResult = {
577
- status: "FAILED",
578
- error: {
579
- code: "UNEXPECTED_ERROR",
580
- message: error?.message || "Unexpected error",
581
- retryable: true
582
- }
583
- };
584
- safeUpdateRunState(runId, {
585
- mode,
586
- state: RUN_STATE_FAILED,
587
- stage: runtimeCallbacks.getLastStage(),
588
- last_message: failedResult.error.message,
589
- error: failedResult.error,
590
- result: failedResult
591
- });
592
- return {
593
- result: failedResult,
594
- lastStage: runtimeCallbacks.getLastStage(),
595
- state: RUN_STATE_FAILED
596
- };
597
- }
598
-
599
- const terminalState = result?.status === "FAILED"
600
- ? RUN_STATE_FAILED
601
- : result?.status === "PAUSED"
602
- ? (isRunCancelRequested(runId) ? RUN_STATE_CANCELED : RUN_STATE_PAUSED)
603
- : RUN_STATE_COMPLETED;
604
- const outputCsv = getOutputCsvFromResult(result) || resumeConfig.output_csv;
605
- const checkpointPath = normalizeText(result?.result?.checkpoint_path || resumeConfig.checkpoint_path);
606
- const canceledError = terminalState === RUN_STATE_CANCELED
607
- ? {
608
- code: "PIPELINE_CANCELED",
609
- message: "流水线已取消。",
610
- retryable: true
611
- }
612
- : null;
613
- safeUpdateRunState(runId, {
614
- mode,
615
- state: terminalState,
616
- stage: runtimeCallbacks.getLastStage(),
617
- last_message: terminalState === RUN_STATE_COMPLETED
618
- ? "流水线执行完成。"
619
- : terminalState === RUN_STATE_CANCELED
620
- ? "流水线已取消(已在边界安全停靠)。"
621
- : terminalState === RUN_STATE_PAUSED
622
- ? "流水线已暂停。"
623
- : (result?.error?.message || "流水线执行失败。"),
624
- control: {
625
- pause_requested: false,
626
- pause_requested_at: null,
627
- pause_requested_by: null,
628
- cancel_requested: false
629
- },
630
- resume: {
631
- checkpoint_path: checkpointPath,
632
- pause_control_path: resumeConfig.pause_control_path,
633
- output_csv: outputCsv,
634
- last_paused_at: terminalState === RUN_STATE_PAUSED ? new Date().toISOString() : null
635
- },
636
- error: terminalState === RUN_STATE_FAILED
637
- ? (result?.error || null)
638
- : terminalState === RUN_STATE_CANCELED
639
- ? canceledError
640
- : null,
641
- result: result || null
642
- });
643
- return {
644
- result,
645
- lastStage: runtimeCallbacks.getLastStage(),
646
- state: terminalState
647
- };
648
- }
649
-
650
- function initializeRunStateOrThrow(runId, mode, workspaceRoot, args) {
651
- const artifacts = getRunArtifacts(runId);
652
- const snapshot = createRunStateSnapshot({
653
- runId,
654
- mode,
655
- state: "queued",
656
- stage: RUN_STAGE_PREFLIGHT,
657
- pid: process.pid,
658
- lastMessage: "流水线任务已创建,等待执行。",
659
- context: buildRunContext(workspaceRoot, args),
660
- control: {
661
- pause_requested: false,
662
- pause_requested_at: null,
663
- pause_requested_by: null,
664
- cancel_requested: false
665
- },
666
- resume: {
667
- checkpoint_path: artifacts.checkpoint_path,
668
- pause_control_path: artifacts.run_state_path,
669
- output_csv: null,
670
- resume_count: 0,
671
- last_resumed_at: null,
672
- last_paused_at: null
673
- }
674
- });
675
- return writeRunState(snapshot);
676
- }
677
-
678
- function launchAsyncRun({ runId, mode, workspaceRoot, args, resumeRun = false }) {
679
- const abortController = new AbortController();
680
- const promise = executeTrackedPipeline({
681
- runId,
682
- mode,
683
- workspaceRoot,
684
- args,
685
- signal: abortController.signal,
686
- resumeRun
687
- }).finally(() => {
688
- activeAsyncRuns.delete(runId);
689
- });
690
- activeAsyncRuns.set(runId, {
691
- abortController,
692
- promise
693
- });
694
- return { abortController, promise };
695
- }
696
-
697
- async function handleStartRunTool({ workspaceRoot, args }) {
698
- const precheckArgs = buildAsyncPrecheckArgs(args);
699
- let precheckResult;
700
- try {
701
- precheckResult = await runPipelineImpl(
702
- {
703
- workspaceRoot,
704
- instruction: precheckArgs.instruction,
705
- confirmation: precheckArgs.confirmation,
706
- overrides: precheckArgs.overrides
707
- },
708
- undefined,
709
- null
710
- );
711
- } catch (error) {
712
- precheckResult = {
713
- status: "FAILED",
714
- error: {
715
- code: "UNEXPECTED_ERROR",
716
- message: error?.message || "Unexpected error",
717
- retryable: true
718
- }
719
- };
720
- }
721
-
722
- if (precheckResult?.status !== "NEED_CONFIRMATION") {
723
- return precheckResult;
724
- }
725
- if (!hasExplicitFinalConfirmation(args) || !isFinalReviewOnlyConfirmation(precheckResult)) {
726
- return precheckResult;
727
- }
728
-
729
- cleanupExpiredRuns();
730
- const runId = createRunId();
731
- try {
732
- initializeRunStateOrThrow(runId, RUN_MODE_ASYNC, workspaceRoot, args);
733
- } catch (error) {
734
- return {
735
- status: "FAILED",
736
- error: {
737
- code: "RUN_STATE_IO_ERROR",
738
- message: `无法写入运行状态目录:${error.message || "unknown"}`,
739
- retryable: false
740
- }
741
- };
742
- }
743
-
744
- launchAsyncRun({
745
- runId,
746
- mode: RUN_MODE_ASYNC,
747
- workspaceRoot,
748
- args
749
- });
750
-
751
- return {
752
- status: "ACCEPTED",
753
- run_id: runId,
754
- state: "queued",
755
- poll_after_sec: getDefaultPollAfterSec(),
756
- message: "异步流水线已启动。默认不自动轮询;如需进度请按需调用 get_recommend_pipeline_run。"
757
- };
758
- }
759
-
760
- function handleGetRunTool(args) {
761
- cleanupExpiredRuns();
762
- const runId = normalizeText(args?.run_id);
763
- if (!runId) {
764
- return {
765
- status: "FAILED",
766
- error: {
767
- code: "INVALID_RUN_ID",
768
- message: "run_id is required",
769
- retryable: false
770
- }
771
- };
772
- }
773
- const snapshot = readRunState(runId);
774
- if (!snapshot) {
775
- return {
776
- status: "FAILED",
777
- error: {
778
- code: "RUN_NOT_FOUND",
779
- message: `未找到 run_id=${runId} 的运行记录。`,
780
- retryable: false
781
- }
782
- };
783
- }
784
- return {
785
- status: "RUN_STATUS",
786
- run: snapshot
787
- };
788
- }
789
-
790
- function handleCancelRunTool(args) {
791
- const runId = normalizeText(args?.run_id);
792
- if (!runId) {
793
- return {
794
- status: "FAILED",
795
- error: {
796
- code: "INVALID_RUN_ID",
797
- message: "run_id is required",
798
- retryable: false
799
- }
800
- };
801
- }
802
- const snapshot = readRunState(runId);
803
- if (!snapshot) {
804
- return {
805
- status: "FAILED",
806
- error: {
807
- code: "RUN_NOT_FOUND",
808
- message: `未找到 run_id=${runId} 的运行记录。`,
809
- retryable: false
810
- }
811
- };
812
- }
813
-
814
- if (TERMINAL_RUN_STATES.has(snapshot.state)) {
815
- return {
816
- status: "CANCEL_IGNORED",
817
- run: snapshot,
818
- message: "目标任务已结束,无需取消。"
819
- };
820
- }
821
-
822
- if (snapshot.state === RUN_STATE_PAUSED) {
823
- const canceledResult = {
824
- status: "FAILED",
825
- error: {
826
- code: "PIPELINE_CANCELED",
827
- message: "流水线已取消。",
828
- retryable: true
829
- },
830
- partial_result: snapshot.result?.partial_result || snapshot.result?.result || null
831
- };
832
- const canceledRun = safeUpdateRunState(runId, {
833
- state: RUN_STATE_CANCELED,
834
- stage: snapshot.stage || RUN_STAGE_PREFLIGHT,
835
- last_message: "流水线已取消。",
836
- control: {
837
- pause_requested: false,
838
- pause_requested_at: null,
839
- pause_requested_by: null,
840
- cancel_requested: false
841
- },
842
- error: canceledResult.error,
843
- result: canceledResult
844
- }) || readRunState(runId) || snapshot;
845
- return {
846
- status: "CANCEL_REQUESTED",
847
- run: canceledRun
848
- };
849
- }
850
-
851
- const activeRun = activeAsyncRuns.get(runId);
852
- if (!activeRun) {
853
- const canceledResult = {
854
- status: "FAILED",
855
- error: {
856
- code: "PIPELINE_CANCELED",
857
- message: "流水线已取消。",
858
- retryable: true
859
- },
860
- partial_result: snapshot.result?.partial_result || snapshot.result?.result || null
861
- };
862
- const canceledRun = safeUpdateRunState(runId, {
863
- state: RUN_STATE_CANCELED,
864
- stage: snapshot.stage || RUN_STAGE_PREFLIGHT,
865
- last_message: "流水线已取消。",
866
- control: {
867
- pause_requested: false,
868
- pause_requested_at: null,
869
- pause_requested_by: null,
870
- cancel_requested: false
871
- },
872
- error: canceledResult.error,
873
- result: canceledResult
874
- }) || readRunState(runId) || snapshot;
875
- return {
876
- status: "CANCEL_REQUESTED",
877
- run: canceledRun
878
- };
879
- }
880
- safeUpdateRunState(runId, {
881
- stage: snapshot.stage || RUN_STAGE_PREFLIGHT,
882
- last_message: "已收到取消请求,将在当前候选人处理完成后安全停止并落盘 CSV。",
883
- control: {
884
- pause_requested: true,
885
- pause_requested_at: new Date().toISOString(),
886
- pause_requested_by: TOOL_CANCEL_RUN,
887
- cancel_requested: true
888
- }
889
- });
890
-
891
- const latest = readRunState(runId) || snapshot;
892
- return {
893
- status: "CANCEL_REQUESTED",
894
- run: latest
895
- };
896
- }
897
-
898
- function handlePauseRunTool(args) {
899
- const runId = normalizeText(args?.run_id);
900
- if (!runId) {
901
- return {
902
- status: "FAILED",
903
- error: {
904
- code: "INVALID_RUN_ID",
905
- message: "run_id is required",
906
- retryable: false
907
- }
908
- };
909
- }
910
- const snapshot = readRunState(runId);
911
- if (!snapshot) {
912
- return {
913
- status: "FAILED",
914
- error: {
915
- code: "RUN_NOT_FOUND",
916
- message: `未找到 run_id=${runId} 的运行记录。`,
917
- retryable: false
918
- }
919
- };
920
- }
921
-
922
- if (TERMINAL_RUN_STATES.has(snapshot.state)) {
923
- return {
924
- status: "PAUSE_IGNORED",
925
- run: snapshot,
926
- message: "目标任务已结束,无需暂停。"
927
- };
928
- }
929
- if (snapshot.state === RUN_STATE_PAUSED) {
930
- return {
931
- status: "PAUSE_IGNORED",
932
- run: snapshot,
933
- message: "目标任务已经处于 paused 状态。"
934
- };
935
- }
936
-
937
- const requestedRun = safeUpdateRunState(runId, {
938
- control: {
939
- pause_requested: true,
940
- pause_requested_at: new Date().toISOString(),
941
- pause_requested_by: TOOL_PAUSE_RUN,
942
- cancel_requested: false
943
- },
944
- last_message: "已收到暂停请求,将在当前候选人处理完成后暂停。"
945
- }) || readRunState(runId) || snapshot;
946
- return {
947
- status: "PAUSE_REQUESTED",
948
- run: requestedRun,
949
- message: "暂停请求已接收,将在当前候选人处理完成后进入 paused。"
950
- };
951
- }
952
-
953
- function handleResumeRunTool(args) {
954
- const runId = normalizeText(args?.run_id);
955
- if (!runId) {
956
- return {
957
- status: "FAILED",
958
- error: {
959
- code: "INVALID_RUN_ID",
960
- message: "run_id is required",
961
- retryable: false
962
- }
963
- };
964
- }
965
- const snapshot = readRunState(runId);
966
- if (!snapshot) {
967
- return {
968
- status: "FAILED",
969
- error: {
970
- code: "RUN_NOT_FOUND",
971
- message: `未找到 run_id=${runId} 的运行记录。`,
972
- retryable: false
973
- }
974
- };
975
- }
976
- if (TERMINAL_RUN_STATES.has(snapshot.state)) {
977
- return {
978
- status: "FAILED",
979
- error: {
980
- code: "RUN_ALREADY_TERMINATED",
981
- message: "目标任务已结束,无法继续。",
982
- retryable: false
983
- }
984
- };
985
- }
986
- if (snapshot.state !== RUN_STATE_PAUSED) {
987
- return {
988
- status: "FAILED",
989
- error: {
990
- code: "RUN_NOT_PAUSED",
991
- message: "仅 paused 状态的 run 才能继续。",
992
- retryable: true
993
- },
994
- run: snapshot
995
- };
996
- }
997
- if (activeAsyncRuns.has(runId)) {
998
- return {
999
- status: "RESUME_IGNORED",
1000
- run: snapshot,
1001
- message: "该 run 当前已在执行,无需继续。"
1002
- };
1003
- }
1004
-
1005
- const executionContext = resolveRunContext(snapshot);
1006
- if (!executionContext) {
1007
- return {
1008
- status: "FAILED",
1009
- error: {
1010
- code: "RUN_CONTEXT_MISSING",
1011
- message: "run 缺少可恢复的执行上下文,无法继续。",
1012
- retryable: false
1013
- }
1014
- };
1015
- }
1016
-
1017
- const updated = safeUpdateRunState(runId, (current) => ({
1018
- state: "queued",
1019
- last_message: "已收到继续请求,准备恢复执行。",
1020
- control: {
1021
- pause_requested: false,
1022
- pause_requested_at: null,
1023
- pause_requested_by: null,
1024
- cancel_requested: false
1025
- },
1026
- resume: {
1027
- checkpoint_path: current?.resume?.checkpoint_path || getRunArtifacts(runId).checkpoint_path,
1028
- pause_control_path: current?.resume?.pause_control_path || getRunArtifacts(runId).run_state_path,
1029
- output_csv: current?.resume?.output_csv || null,
1030
- resume_count: Number.isInteger(current?.resume?.resume_count) ? current.resume.resume_count + 1 : 1,
1031
- last_resumed_at: new Date().toISOString()
1032
- }
1033
- })) || readRunState(runId) || snapshot;
1034
-
1035
- launchAsyncRun({
1036
- runId,
1037
- mode: RUN_MODE_ASYNC,
1038
- workspaceRoot: executionContext.workspaceRoot,
1039
- args: executionContext.args,
1040
- resumeRun: true
1041
- });
1042
-
1043
- return {
1044
- status: "RESUME_REQUESTED",
1045
- run: updated,
1046
- poll_after_sec: getDefaultPollAfterSec(),
1047
- message: "已恢复 Recommend 流水线。默认不自动轮询;如需进度请按需调用 get_recommend_pipeline_run。"
1048
- };
1049
- }
1050
-
1051
- async function handleRequest(message, workspaceRoot) {
1052
- if (!message || message.jsonrpc !== "2.0") {
1053
- return createJsonRpcError(null, -32600, "Invalid JSON-RPC request");
1054
- }
1055
-
1056
- const { id, method, params } = message;
1057
-
1058
- if (method === "initialize") {
1059
- return {
1060
- jsonrpc: "2.0",
1061
- id,
1062
- result: {
1063
- protocolVersion: "2024-11-05",
1064
- capabilities: {
1065
- tools: {}
1066
- },
1067
- serverInfo: {
1068
- name: SERVER_NAME,
1069
- version: SERVER_VERSION
1070
- }
1071
- }
1072
- };
1073
- }
1074
-
1075
- if (method === "notifications/initialized") {
1076
- return null;
1077
- }
1078
-
1079
- if (method === "tools/list") {
1080
- return {
1081
- jsonrpc: "2.0",
1082
- id,
1083
- result: {
1084
- tools: createToolsSchema()
1085
- }
1086
- };
1087
- }
1088
-
1089
- if (method === "tools/call") {
1090
- const toolName = params?.name;
1091
- const args = params?.arguments || {};
1092
-
1093
- if (toolName === TOOL_START_RUN) {
1094
- const inputError = validateRunArgs(args);
1095
- if (inputError) {
1096
- return createJsonRpcError(id, -32602, inputError);
1097
- }
1098
- }
1099
-
1100
- if ([TOOL_GET_RUN, TOOL_CANCEL_RUN, TOOL_PAUSE_RUN, TOOL_RESUME_RUN].includes(toolName)) {
1101
- if (!args || typeof args.run_id !== "string" || !normalizeText(args.run_id)) {
1102
- return createJsonRpcError(id, -32602, "run_id is required and must be a string");
1103
- }
1104
- }
1105
-
1106
- try {
1107
- let payload;
1108
- if (toolName === TOOL_START_RUN) {
1109
- payload = await handleStartRunTool({ workspaceRoot, args });
1110
- } else if (toolName === TOOL_GET_RUN) {
1111
- payload = handleGetRunTool(args);
1112
- } else if (toolName === TOOL_CANCEL_RUN) {
1113
- payload = handleCancelRunTool(args);
1114
- } else if (toolName === TOOL_PAUSE_RUN) {
1115
- payload = handlePauseRunTool(args);
1116
- } else if (toolName === TOOL_RESUME_RUN) {
1117
- payload = handleResumeRunTool(args);
1118
- } else {
1119
- return createJsonRpcError(id, -32602, `Unknown tool: ${toolName || ""}`);
1120
- }
1121
- const isError = payload?.status === "FAILED";
1122
- return createToolResultResponse(id, payload, isError);
1123
- } catch (error) {
1124
- const failed = {
1125
- status: "FAILED",
1126
- error: {
1127
- code: "UNEXPECTED_ERROR",
1128
- message: error?.message || "Unexpected error",
1129
- retryable: true
1130
- }
1131
- };
1132
- return createToolResultResponse(id, failed, true);
1133
- }
1134
- }
1135
-
1136
- if (method === "ping") {
1137
- return { jsonrpc: "2.0", id, result: {} };
1138
- }
1139
-
1140
- if (id === undefined || id === null) {
1141
- return null;
1142
- }
1143
- return createJsonRpcError(id, -32601, `Method not found: ${method}`);
1144
- }
1145
-
1146
- export function startServer() {
1147
- const envRoot = process.env.BOSS_WORKSPACE_ROOT;
1148
- const workspaceRoot = envRoot
1149
- ? path.resolve(envRoot)
1150
- : process.env.INIT_CWD
1151
- ? path.resolve(process.env.INIT_CWD)
1152
- : path.resolve(process.cwd());
1153
- let buffer = Buffer.alloc(0);
1154
- let framing = FRAMING_UNKNOWN;
1155
-
1156
- process.stdin.on("data", async (chunk) => {
1157
- buffer = Buffer.concat([buffer, chunk]);
1158
- if (buffer.length >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) {
1159
- buffer = buffer.slice(3);
1160
- }
1161
-
1162
- while (true) {
1163
- const crlfHeaderEnd = buffer.indexOf("\r\n\r\n");
1164
- const lfHeaderEnd = buffer.indexOf("\n\n");
1165
- const crHeaderEnd = buffer.indexOf("\r\r");
1166
- let headerEnd = -1;
1167
- let headerSeparatorLength = 0;
1168
- if (
1169
- crlfHeaderEnd !== -1
1170
- && (lfHeaderEnd === -1 || crlfHeaderEnd < lfHeaderEnd)
1171
- && (crHeaderEnd === -1 || crlfHeaderEnd < crHeaderEnd)
1172
- ) {
1173
- headerEnd = crlfHeaderEnd;
1174
- headerSeparatorLength = 4;
1175
- } else if (lfHeaderEnd !== -1 && (crHeaderEnd === -1 || lfHeaderEnd < crHeaderEnd)) {
1176
- headerEnd = lfHeaderEnd;
1177
- headerSeparatorLength = 2;
1178
- } else if (crHeaderEnd !== -1) {
1179
- headerEnd = crHeaderEnd;
1180
- headerSeparatorLength = 2;
1181
- }
1182
- if (headerEnd !== -1) {
1183
- const headerText = buffer.slice(0, headerEnd).toString("utf8");
1184
- const contentLengthLine = headerText
1185
- .split(/\r\n|\n|\r/)
1186
- .find((line) => line.toLowerCase().startsWith("content-length:"));
1187
-
1188
- if (!contentLengthLine) {
1189
- buffer = buffer.slice(headerEnd + headerSeparatorLength);
1190
- continue;
1191
- }
1192
-
1193
- const contentLength = Number.parseInt(contentLengthLine.split(":")[1].trim(), 10);
1194
- if (!Number.isFinite(contentLength) || contentLength < 0) {
1195
- buffer = buffer.slice(headerEnd + headerSeparatorLength);
1196
- continue;
1197
- }
1198
-
1199
- const bodyStart = headerEnd + headerSeparatorLength;
1200
- const bodyEnd = bodyStart + contentLength;
1201
- if (buffer.length < bodyEnd) break;
1202
-
1203
- const body = buffer.slice(bodyStart, bodyEnd).toString("utf8");
1204
- buffer = buffer.slice(bodyEnd);
1205
- framing = FRAMING_HEADER;
1206
-
1207
- let message;
1208
- try {
1209
- message = JSON.parse(body);
1210
- } catch {
1211
- writeMessage(createJsonRpcError(null, -32700, "Parse error"), FRAMING_HEADER);
1212
- continue;
1213
- }
1214
-
1215
- const response = await handleRequest(message, workspaceRoot);
1216
- if (response) writeMessage(response, framing);
1217
- continue;
1218
- }
1219
-
1220
- const newlineIndex = buffer.indexOf("\n");
1221
- if (newlineIndex === -1) break;
1222
- const rawLine = buffer.slice(0, newlineIndex).toString("utf8").replace(/\r$/, "");
1223
- if (/^\s*content-length:/i.test(rawLine)) break;
1224
- buffer = buffer.slice(newlineIndex + 1);
1225
- const line = rawLine.trim();
1226
- if (!line) continue;
1227
- framing = FRAMING_LINE;
1228
-
1229
- let message;
1230
- try {
1231
- message = JSON.parse(line);
1232
- } catch {
1233
- writeMessage(createJsonRpcError(null, -32700, "Parse error"), FRAMING_LINE);
1234
- continue;
1235
- }
1236
-
1237
- const response = await handleRequest(message, workspaceRoot);
1238
- if (response) writeMessage(response, framing);
1239
- }
1240
- });
1241
- }
1242
-
1243
- export const __testables = {
1244
- handleRequest,
1245
- activeAsyncRuns,
1246
- setRunPipelineImplForTests(nextImpl) {
1247
- runPipelineImpl = typeof nextImpl === "function" ? nextImpl : runRecommendPipeline;
1248
- }
1249
- };
1250
-
1251
- const thisFilePath = fileURLToPath(import.meta.url);
1252
- if (process.argv[1] && path.resolve(process.argv[1]) === thisFilePath) {
1253
- startServer();
1254
- }
1
+ import path from "node:path";
2
+ import { createRequire } from "node:module";
3
+ import process from "node:process";
4
+ import { fileURLToPath } from "node:url";
5
+ import { runRecommendPipeline } from "./pipeline.js";
6
+ import {
7
+ RUN_MODE_ASYNC,
8
+ RUN_STAGE_PREFLIGHT,
9
+ RUN_STATE_CANCELED,
10
+ RUN_STATE_COMPLETED,
11
+ RUN_STATE_FAILED,
12
+ RUN_STATE_PAUSED,
13
+ RUN_STATE_RUNNING,
14
+ cleanupExpiredRuns,
15
+ createRunId,
16
+ createRunStateSnapshot,
17
+ getRunHeartbeatIntervalMs,
18
+ getRunsDir,
19
+ readRunState,
20
+ touchRunHeartbeat,
21
+ updateRunProgress,
22
+ updateRunState,
23
+ writeRunState
24
+ } from "./run-state.js";
25
+
26
+ const require = createRequire(import.meta.url);
27
+ const { version: SERVER_VERSION } = require("../package.json");
28
+
29
+ const TOOL_START_RUN = "start_recommend_pipeline_run";
30
+ const TOOL_GET_RUN = "get_recommend_pipeline_run";
31
+ const TOOL_CANCEL_RUN = "cancel_recommend_pipeline_run";
32
+ const TOOL_PAUSE_RUN = "pause_recommend_pipeline_run";
33
+ const TOOL_RESUME_RUN = "resume_recommend_pipeline_run";
34
+
35
+ const SERVER_NAME = "boss-recommend-mcp";
36
+ const FRAMING_UNKNOWN = "unknown";
37
+ const FRAMING_HEADER = "header";
38
+ const FRAMING_LINE = "line";
39
+
40
+ const activeAsyncRuns = new Map();
41
+ let runPipelineImpl = runRecommendPipeline;
42
+ const TERMINAL_RUN_STATES = new Set([RUN_STATE_COMPLETED, RUN_STATE_FAILED, RUN_STATE_CANCELED]);
43
+
44
+ function normalizeText(value) {
45
+ return String(value || "").replace(/\s+/g, " ").trim();
46
+ }
47
+
48
+ function parsePositiveInteger(raw, fallback) {
49
+ const value = Number.parseInt(String(raw || ""), 10);
50
+ return Number.isFinite(value) && value > 0 ? value : fallback;
51
+ }
52
+
53
+ function getDefaultPollAfterSec() {
54
+ const fromEnv = parsePositiveInteger(process.env.BOSS_RECOMMEND_POLL_AFTER_SEC, 10);
55
+ return Math.max(5, Math.min(15, fromEnv));
56
+ }
57
+
58
+ function getRunArtifacts(runId) {
59
+ const normalizedRunId = normalizeText(runId);
60
+ return {
61
+ run_state_path: path.join(getRunsDir(), `${normalizedRunId}.json`),
62
+ checkpoint_path: path.join(getRunsDir(), `${normalizedRunId}.checkpoint.json`)
63
+ };
64
+ }
65
+
66
+ function buildRunContext(workspaceRoot, args = {}) {
67
+ return {
68
+ workspace_root: path.resolve(workspaceRoot),
69
+ instruction: String(args?.instruction || ""),
70
+ confirmation: args?.confirmation && typeof args.confirmation === "object" ? args.confirmation : {},
71
+ overrides: args?.overrides && typeof args.overrides === "object" ? args.overrides : {}
72
+ };
73
+ }
74
+
75
+ function resolveRunContext(snapshot) {
76
+ const workspaceRoot = normalizeText(snapshot?.context?.workspace_root || "");
77
+ const instruction = typeof snapshot?.context?.instruction === "string"
78
+ ? snapshot.context.instruction
79
+ : "";
80
+ if (!workspaceRoot || !instruction.trim()) return null;
81
+ return {
82
+ workspaceRoot,
83
+ args: {
84
+ instruction,
85
+ confirmation: snapshot?.context?.confirmation && typeof snapshot.context.confirmation === "object"
86
+ ? snapshot.context.confirmation
87
+ : {},
88
+ overrides: snapshot?.context?.overrides && typeof snapshot.context.overrides === "object"
89
+ ? snapshot.context.overrides
90
+ : {}
91
+ }
92
+ };
93
+ }
94
+
95
+ function isRunPauseRequested(runId) {
96
+ const snapshot = readRunState(runId);
97
+ return snapshot?.control?.pause_requested === true;
98
+ }
99
+
100
+ function isRunCancelRequested(runId) {
101
+ const snapshot = readRunState(runId);
102
+ return snapshot?.control?.cancel_requested === true;
103
+ }
104
+
105
+ function getOutputCsvFromResult(result) {
106
+ const direct = normalizeText(result?.result?.output_csv || "");
107
+ if (direct) return direct;
108
+ const partial = normalizeText(result?.partial_result?.output_csv || "");
109
+ if (partial) return partial;
110
+ return null;
111
+ }
112
+
113
+ function getCompletionReasonFromResult(result) {
114
+ const direct = normalizeText(result?.result?.completion_reason || "");
115
+ if (direct) return direct;
116
+ const partial = normalizeText(result?.partial_result?.completion_reason || "");
117
+ if (partial) return partial;
118
+ return null;
119
+ }
120
+
121
+ function writeMessage(message, framing = FRAMING_LINE) {
122
+ const body = JSON.stringify(message);
123
+ if (framing === FRAMING_HEADER) {
124
+ const header = `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n`;
125
+ process.stdout.write(header + body);
126
+ return;
127
+ }
128
+ process.stdout.write(`${body}\n`);
129
+ }
130
+
131
+ function createJsonRpcError(id, code, message) {
132
+ return {
133
+ jsonrpc: "2.0",
134
+ id: id ?? null,
135
+ error: { code, message }
136
+ };
137
+ }
138
+
139
+ function createRunInputSchema() {
140
+ return {
141
+ type: "object",
142
+ properties: {
143
+ instruction: {
144
+ type: "string",
145
+ description: "用户自然语言推荐筛选指令"
146
+ },
147
+ confirmation: {
148
+ type: "object",
149
+ properties: {
150
+ filters_confirmed: { type: "boolean" },
151
+ school_tag_confirmed: { type: "boolean" },
152
+ school_tag_value: {
153
+ oneOf: [
154
+ {
155
+ type: "string",
156
+ enum: ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"]
157
+ },
158
+ {
159
+ type: "array",
160
+ items: {
161
+ type: "string",
162
+ enum: ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"]
163
+ },
164
+ minItems: 1,
165
+ uniqueItems: true
166
+ }
167
+ ]
168
+ },
169
+ degree_confirmed: { type: "boolean" },
170
+ degree_value: {
171
+ oneOf: [
172
+ {
173
+ type: "string",
174
+ enum: ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"]
175
+ },
176
+ {
177
+ type: "array",
178
+ items: {
179
+ type: "string",
180
+ enum: ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"]
181
+ },
182
+ minItems: 1,
183
+ uniqueItems: true
184
+ }
185
+ ]
186
+ },
187
+ gender_confirmed: { type: "boolean" },
188
+ gender_value: {
189
+ type: "string",
190
+ enum: ["不限", "男", "女"]
191
+ },
192
+ recent_not_view_confirmed: { type: "boolean" },
193
+ recent_not_view_value: {
194
+ type: "string",
195
+ enum: ["不限", "近14天没有"]
196
+ },
197
+ criteria_confirmed: { type: "boolean" },
198
+ target_count_confirmed: { type: "boolean" },
199
+ target_count_value: {
200
+ type: "integer",
201
+ minimum: 1
202
+ },
203
+ post_action_confirmed: { type: "boolean" },
204
+ post_action_value: {
205
+ type: "string",
206
+ enum: ["favorite", "greet", "none"]
207
+ },
208
+ final_confirmed: { type: "boolean" },
209
+ job_confirmed: { type: "boolean" },
210
+ job_value: { type: "string" },
211
+ max_greet_count_confirmed: { type: "boolean" },
212
+ max_greet_count_value: {
213
+ type: "integer",
214
+ minimum: 1
215
+ }
216
+ },
217
+ additionalProperties: false
218
+ },
219
+ overrides: {
220
+ type: "object",
221
+ properties: {
222
+ school_tag: {
223
+ oneOf: [
224
+ {
225
+ type: "string",
226
+ enum: ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"]
227
+ },
228
+ {
229
+ type: "array",
230
+ items: {
231
+ type: "string",
232
+ enum: ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"]
233
+ },
234
+ minItems: 1,
235
+ uniqueItems: true
236
+ }
237
+ ]
238
+ },
239
+ degree: {
240
+ oneOf: [
241
+ {
242
+ type: "string",
243
+ enum: ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"]
244
+ },
245
+ {
246
+ type: "array",
247
+ items: {
248
+ type: "string",
249
+ enum: ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"]
250
+ },
251
+ minItems: 1,
252
+ uniqueItems: true
253
+ }
254
+ ]
255
+ },
256
+ gender: {
257
+ type: "string",
258
+ enum: ["不限", "男", "女"]
259
+ },
260
+ recent_not_view: {
261
+ type: "string",
262
+ enum: ["不限", "近14天没有"]
263
+ },
264
+ criteria: { type: "string" },
265
+ job: { type: "string" },
266
+ target_count: { type: "integer", minimum: 1 },
267
+ max_greet_count: { type: "integer", minimum: 1 },
268
+ post_action: {
269
+ type: "string",
270
+ enum: ["favorite", "greet", "none"]
271
+ }
272
+ },
273
+ additionalProperties: false
274
+ }
275
+ },
276
+ required: ["instruction"],
277
+ additionalProperties: false
278
+ };
279
+ }
280
+
281
+ function createToolsSchema() {
282
+ return [
283
+ {
284
+ name: TOOL_START_RUN,
285
+ description: "异步启动 Boss 推荐页流水线(含同步门禁预检);只有在前置确认与页面就绪通过后才返回 run_id。",
286
+ inputSchema: createRunInputSchema()
287
+ },
288
+ {
289
+ name: TOOL_GET_RUN,
290
+ description: "按 run_id 查询异步/同步流水线运行状态快照。",
291
+ inputSchema: {
292
+ type: "object",
293
+ properties: {
294
+ run_id: { type: "string" }
295
+ },
296
+ required: ["run_id"],
297
+ additionalProperties: false
298
+ }
299
+ },
300
+ {
301
+ name: TOOL_CANCEL_RUN,
302
+ description: "取消指定 run_id 的运行中流水线。",
303
+ inputSchema: {
304
+ type: "object",
305
+ properties: {
306
+ run_id: { type: "string" }
307
+ },
308
+ required: ["run_id"],
309
+ additionalProperties: false
310
+ }
311
+ },
312
+ {
313
+ name: TOOL_PAUSE_RUN,
314
+ description: "请求暂停指定 run_id 的流水线;会在当前候选人处理完成后进入 paused。",
315
+ inputSchema: {
316
+ type: "object",
317
+ properties: {
318
+ run_id: { type: "string" }
319
+ },
320
+ required: ["run_id"],
321
+ additionalProperties: false
322
+ }
323
+ },
324
+ {
325
+ name: TOOL_RESUME_RUN,
326
+ description: "继续指定 run_id 的 paused 流水线;沿用原 CSV 与 checkpoint 续跑。",
327
+ inputSchema: {
328
+ type: "object",
329
+ properties: {
330
+ run_id: { type: "string" }
331
+ },
332
+ required: ["run_id"],
333
+ additionalProperties: false
334
+ }
335
+ }
336
+ ];
337
+ }
338
+
339
+ function createToolResultResponse(id, payload, isError = false) {
340
+ return {
341
+ jsonrpc: "2.0",
342
+ id,
343
+ result: {
344
+ content: [
345
+ {
346
+ type: "text",
347
+ text: JSON.stringify(payload, null, 2)
348
+ }
349
+ ],
350
+ structuredContent: payload,
351
+ ...(isError ? { isError: true } : {})
352
+ }
353
+ };
354
+ }
355
+
356
+ function validateRunArgs(args) {
357
+ if (!args || typeof args !== "object") {
358
+ return "arguments must be an object";
359
+ }
360
+ if (!args.instruction || typeof args.instruction !== "string") {
361
+ return "instruction is required and must be a string";
362
+ }
363
+ return null;
364
+ }
365
+
366
+ function getLastOutputLine(text) {
367
+ const lines = String(text || "")
368
+ .split(/\r?\n/)
369
+ .map((line) => normalizeText(line))
370
+ .filter(Boolean);
371
+ return lines.length > 0 ? lines[lines.length - 1] : null;
372
+ }
373
+
374
+ function normalizeRequiredConfirmations(value) {
375
+ if (!Array.isArray(value)) return [];
376
+ return value
377
+ .map((item) => normalizeText(item))
378
+ .filter(Boolean);
379
+ }
380
+
381
+ function hasExplicitFinalConfirmation(args) {
382
+ return args?.confirmation?.final_confirmed === true;
383
+ }
384
+
385
+ function buildAsyncPrecheckConfirmation(confirmation) {
386
+ if (!confirmation || typeof confirmation !== "object") {
387
+ return {
388
+ final_confirmed: false
389
+ };
390
+ }
391
+ return {
392
+ ...confirmation,
393
+ final_confirmed: false
394
+ };
395
+ }
396
+
397
+ function buildAsyncPrecheckArgs(args) {
398
+ return {
399
+ instruction: args.instruction,
400
+ confirmation: buildAsyncPrecheckConfirmation(args.confirmation),
401
+ overrides: args.overrides
402
+ };
403
+ }
404
+
405
+ function isFinalReviewOnlyConfirmation(result) {
406
+ if (result?.status !== "NEED_CONFIRMATION") return false;
407
+ const required = normalizeRequiredConfirmations(result.required_confirmations);
408
+ return required.length > 0 && required.every((item) => item === "final_review");
409
+ }
410
+
411
+ function safeUpdateRunState(runId, updater) {
412
+ try {
413
+ return updateRunState(runId, updater);
414
+ } catch {
415
+ return null;
416
+ }
417
+ }
418
+
419
+ function safeUpdateRunProgress(runId, patch, message = null) {
420
+ try {
421
+ return updateRunProgress(runId, patch, message);
422
+ } catch {
423
+ return null;
424
+ }
425
+ }
426
+
427
+ function createRuntimeCallbacks(runId, heartbeatIntervalMs) {
428
+ let lastStage = RUN_STAGE_PREFLIGHT;
429
+ let lastOutputPersistAt = 0;
430
+ return {
431
+ heartbeatIntervalMs,
432
+ onStage(event) {
433
+ const stage = normalizeText(event?.stage) || RUN_STAGE_PREFLIGHT;
434
+ lastStage = stage;
435
+ safeUpdateRunState(runId, {
436
+ state: RUN_STATE_RUNNING,
437
+ stage,
438
+ last_message: normalizeText(event?.message || "")
439
+ });
440
+ },
441
+ onHeartbeat(event) {
442
+ const stage = normalizeText(event?.stage) || lastStage;
443
+ lastStage = stage || lastStage;
444
+ const detailsMessage = normalizeText(event?.details?.message || "");
445
+ const patch = { stage: lastStage };
446
+ if (detailsMessage) {
447
+ patch.last_message = detailsMessage;
448
+ }
449
+ safeUpdateRunState(runId, patch);
450
+ try {
451
+ touchRunHeartbeat(runId, detailsMessage || undefined);
452
+ } catch {
453
+ // Ignore heartbeat persistence failures here; state updates above already best-effort.
454
+ }
455
+ },
456
+ onOutput(event) {
457
+ const stage = normalizeText(event?.stage) || lastStage;
458
+ lastStage = stage || lastStage;
459
+ const now = Date.now();
460
+ if (now - lastOutputPersistAt < 1000) return;
461
+ lastOutputPersistAt = now;
462
+ const message = getLastOutputLine(event?.text);
463
+ if (!message) return;
464
+ safeUpdateRunState(runId, {
465
+ stage: lastStage,
466
+ last_message: message
467
+ });
468
+ },
469
+ onProgress(event) {
470
+ const stage = normalizeText(event?.stage) || lastStage;
471
+ lastStage = stage || lastStage;
472
+ safeUpdateRunState(runId, { stage: lastStage });
473
+ safeUpdateRunProgress(
474
+ runId,
475
+ {
476
+ processed: Number.isInteger(event?.processed) ? event.processed : undefined,
477
+ passed: Number.isInteger(event?.passed) ? event.passed : undefined,
478
+ skipped: Number.isInteger(event?.skipped) ? event.skipped : undefined,
479
+ greet_count: Number.isInteger(event?.greet_count) ? event.greet_count : undefined
480
+ },
481
+ normalizeText(event?.line || "")
482
+ );
483
+ },
484
+ getLastStage() {
485
+ return lastStage;
486
+ }
487
+ };
488
+ }
489
+
490
+
491
+ async function executeTrackedPipeline({
492
+ runId,
493
+ mode,
494
+ workspaceRoot,
495
+ args,
496
+ signal,
497
+ resumeRun = false
498
+ }) {
499
+ const heartbeatIntervalMs = getRunHeartbeatIntervalMs();
500
+ const runtimeCallbacks = createRuntimeCallbacks(runId, heartbeatIntervalMs);
501
+ const artifacts = getRunArtifacts(runId);
502
+ const existingSnapshot = readRunState(runId);
503
+ const resumeConfig = {
504
+ resume: resumeRun === true,
505
+ checkpoint_path: normalizeText(existingSnapshot?.resume?.checkpoint_path || artifacts.checkpoint_path),
506
+ pause_control_path: normalizeText(existingSnapshot?.resume?.pause_control_path || artifacts.run_state_path),
507
+ output_csv: normalizeText(existingSnapshot?.resume?.output_csv || "") || null,
508
+ previous_completion_reason: getCompletionReasonFromResult(existingSnapshot?.result || null)
509
+ };
510
+ safeUpdateRunState(runId, {
511
+ state: RUN_STATE_RUNNING,
512
+ stage: RUN_STAGE_PREFLIGHT,
513
+ last_message: resumeRun
514
+ ? "流水线继续执行中,等待 preflight。"
515
+ : "流水线已启动,等待 preflight。",
516
+ resume: resumeConfig
517
+ });
518
+
519
+ let result;
520
+ try {
521
+ result = await runPipelineImpl(
522
+ {
523
+ workspaceRoot,
524
+ instruction: args.instruction,
525
+ confirmation: args.confirmation,
526
+ overrides: args.overrides,
527
+ resume: resumeConfig
528
+ },
529
+ undefined,
530
+ {
531
+ signal,
532
+ heartbeatIntervalMs,
533
+ isPauseRequested: () => isRunPauseRequested(runId),
534
+ onStage: runtimeCallbacks.onStage,
535
+ onHeartbeat: runtimeCallbacks.onHeartbeat,
536
+ onOutput: runtimeCallbacks.onOutput,
537
+ onProgress: runtimeCallbacks.onProgress
538
+ }
539
+ );
540
+ } catch (error) {
541
+ const canceled = Boolean(signal?.aborted) || error?.code === "PIPELINE_ABORTED";
542
+ if (canceled) {
543
+ const canceledResult = {
544
+ status: "FAILED",
545
+ error: {
546
+ code: "PIPELINE_CANCELED",
547
+ message: "流水线已取消。",
548
+ retryable: true
549
+ }
550
+ };
551
+ safeUpdateRunState(runId, {
552
+ mode,
553
+ state: RUN_STATE_CANCELED,
554
+ stage: runtimeCallbacks.getLastStage(),
555
+ last_message: "流水线已取消。",
556
+ control: {
557
+ pause_requested: false,
558
+ pause_requested_at: null,
559
+ pause_requested_by: null,
560
+ cancel_requested: false
561
+ },
562
+ resume: {
563
+ ...resumeConfig,
564
+ output_csv: getOutputCsvFromResult(canceledResult) || resumeConfig.output_csv
565
+ },
566
+ error: canceledResult.error,
567
+ result: canceledResult
568
+ });
569
+ return {
570
+ result: canceledResult,
571
+ lastStage: runtimeCallbacks.getLastStage(),
572
+ state: RUN_STATE_CANCELED
573
+ };
574
+ }
575
+
576
+ const failedResult = {
577
+ status: "FAILED",
578
+ error: {
579
+ code: "UNEXPECTED_ERROR",
580
+ message: error?.message || "Unexpected error",
581
+ retryable: true
582
+ }
583
+ };
584
+ safeUpdateRunState(runId, {
585
+ mode,
586
+ state: RUN_STATE_FAILED,
587
+ stage: runtimeCallbacks.getLastStage(),
588
+ last_message: failedResult.error.message,
589
+ error: failedResult.error,
590
+ result: failedResult
591
+ });
592
+ return {
593
+ result: failedResult,
594
+ lastStage: runtimeCallbacks.getLastStage(),
595
+ state: RUN_STATE_FAILED
596
+ };
597
+ }
598
+
599
+ const terminalState = result?.status === "FAILED"
600
+ ? RUN_STATE_FAILED
601
+ : result?.status === "PAUSED"
602
+ ? (isRunCancelRequested(runId) ? RUN_STATE_CANCELED : RUN_STATE_PAUSED)
603
+ : RUN_STATE_COMPLETED;
604
+ const outputCsv = getOutputCsvFromResult(result) || resumeConfig.output_csv;
605
+ const checkpointPath = normalizeText(result?.result?.checkpoint_path || resumeConfig.checkpoint_path);
606
+ const canceledError = terminalState === RUN_STATE_CANCELED
607
+ ? {
608
+ code: "PIPELINE_CANCELED",
609
+ message: "流水线已取消。",
610
+ retryable: true
611
+ }
612
+ : null;
613
+ safeUpdateRunState(runId, {
614
+ mode,
615
+ state: terminalState,
616
+ stage: runtimeCallbacks.getLastStage(),
617
+ last_message: terminalState === RUN_STATE_COMPLETED
618
+ ? "流水线执行完成。"
619
+ : terminalState === RUN_STATE_CANCELED
620
+ ? "流水线已取消(已在边界安全停靠)。"
621
+ : terminalState === RUN_STATE_PAUSED
622
+ ? "流水线已暂停。"
623
+ : (result?.error?.message || "流水线执行失败。"),
624
+ control: {
625
+ pause_requested: false,
626
+ pause_requested_at: null,
627
+ pause_requested_by: null,
628
+ cancel_requested: false
629
+ },
630
+ resume: {
631
+ checkpoint_path: checkpointPath,
632
+ pause_control_path: resumeConfig.pause_control_path,
633
+ output_csv: outputCsv,
634
+ last_paused_at: terminalState === RUN_STATE_PAUSED ? new Date().toISOString() : null
635
+ },
636
+ error: terminalState === RUN_STATE_FAILED
637
+ ? (result?.error || null)
638
+ : terminalState === RUN_STATE_CANCELED
639
+ ? canceledError
640
+ : null,
641
+ result: result || null
642
+ });
643
+ return {
644
+ result,
645
+ lastStage: runtimeCallbacks.getLastStage(),
646
+ state: terminalState
647
+ };
648
+ }
649
+
650
+ function initializeRunStateOrThrow(runId, mode, workspaceRoot, args) {
651
+ const artifacts = getRunArtifacts(runId);
652
+ const snapshot = createRunStateSnapshot({
653
+ runId,
654
+ mode,
655
+ state: "queued",
656
+ stage: RUN_STAGE_PREFLIGHT,
657
+ pid: process.pid,
658
+ lastMessage: "流水线任务已创建,等待执行。",
659
+ context: buildRunContext(workspaceRoot, args),
660
+ control: {
661
+ pause_requested: false,
662
+ pause_requested_at: null,
663
+ pause_requested_by: null,
664
+ cancel_requested: false
665
+ },
666
+ resume: {
667
+ checkpoint_path: artifacts.checkpoint_path,
668
+ pause_control_path: artifacts.run_state_path,
669
+ output_csv: null,
670
+ resume_count: 0,
671
+ last_resumed_at: null,
672
+ last_paused_at: null
673
+ }
674
+ });
675
+ return writeRunState(snapshot);
676
+ }
677
+
678
+ function launchAsyncRun({ runId, mode, workspaceRoot, args, resumeRun = false }) {
679
+ const abortController = new AbortController();
680
+ const promise = executeTrackedPipeline({
681
+ runId,
682
+ mode,
683
+ workspaceRoot,
684
+ args,
685
+ signal: abortController.signal,
686
+ resumeRun
687
+ }).finally(() => {
688
+ activeAsyncRuns.delete(runId);
689
+ });
690
+ activeAsyncRuns.set(runId, {
691
+ abortController,
692
+ promise
693
+ });
694
+ return { abortController, promise };
695
+ }
696
+
697
+ async function handleStartRunTool({ workspaceRoot, args }) {
698
+ const precheckArgs = buildAsyncPrecheckArgs(args);
699
+ let precheckResult;
700
+ try {
701
+ precheckResult = await runPipelineImpl(
702
+ {
703
+ workspaceRoot,
704
+ instruction: precheckArgs.instruction,
705
+ confirmation: precheckArgs.confirmation,
706
+ overrides: precheckArgs.overrides
707
+ },
708
+ undefined,
709
+ null
710
+ );
711
+ } catch (error) {
712
+ precheckResult = {
713
+ status: "FAILED",
714
+ error: {
715
+ code: "UNEXPECTED_ERROR",
716
+ message: error?.message || "Unexpected error",
717
+ retryable: true
718
+ }
719
+ };
720
+ }
721
+
722
+ if (precheckResult?.status !== "NEED_CONFIRMATION") {
723
+ return precheckResult;
724
+ }
725
+ if (!hasExplicitFinalConfirmation(args) || !isFinalReviewOnlyConfirmation(precheckResult)) {
726
+ return precheckResult;
727
+ }
728
+
729
+ cleanupExpiredRuns();
730
+ const runId = createRunId();
731
+ try {
732
+ initializeRunStateOrThrow(runId, RUN_MODE_ASYNC, workspaceRoot, args);
733
+ } catch (error) {
734
+ return {
735
+ status: "FAILED",
736
+ error: {
737
+ code: "RUN_STATE_IO_ERROR",
738
+ message: `无法写入运行状态目录:${error.message || "unknown"}`,
739
+ retryable: false
740
+ }
741
+ };
742
+ }
743
+
744
+ launchAsyncRun({
745
+ runId,
746
+ mode: RUN_MODE_ASYNC,
747
+ workspaceRoot,
748
+ args
749
+ });
750
+
751
+ return {
752
+ status: "ACCEPTED",
753
+ run_id: runId,
754
+ state: "queued",
755
+ poll_after_sec: getDefaultPollAfterSec(),
756
+ message: "异步流水线已启动。默认不自动轮询;如需进度请按需调用 get_recommend_pipeline_run。"
757
+ };
758
+ }
759
+
760
+ function handleGetRunTool(args) {
761
+ cleanupExpiredRuns();
762
+ const runId = normalizeText(args?.run_id);
763
+ if (!runId) {
764
+ return {
765
+ status: "FAILED",
766
+ error: {
767
+ code: "INVALID_RUN_ID",
768
+ message: "run_id is required",
769
+ retryable: false
770
+ }
771
+ };
772
+ }
773
+ const snapshot = readRunState(runId);
774
+ if (!snapshot) {
775
+ return {
776
+ status: "FAILED",
777
+ error: {
778
+ code: "RUN_NOT_FOUND",
779
+ message: `未找到 run_id=${runId} 的运行记录。`,
780
+ retryable: false
781
+ }
782
+ };
783
+ }
784
+ return {
785
+ status: "RUN_STATUS",
786
+ run: snapshot
787
+ };
788
+ }
789
+
790
+ function handleCancelRunTool(args) {
791
+ const runId = normalizeText(args?.run_id);
792
+ if (!runId) {
793
+ return {
794
+ status: "FAILED",
795
+ error: {
796
+ code: "INVALID_RUN_ID",
797
+ message: "run_id is required",
798
+ retryable: false
799
+ }
800
+ };
801
+ }
802
+ const snapshot = readRunState(runId);
803
+ if (!snapshot) {
804
+ return {
805
+ status: "FAILED",
806
+ error: {
807
+ code: "RUN_NOT_FOUND",
808
+ message: `未找到 run_id=${runId} 的运行记录。`,
809
+ retryable: false
810
+ }
811
+ };
812
+ }
813
+
814
+ if (TERMINAL_RUN_STATES.has(snapshot.state)) {
815
+ return {
816
+ status: "CANCEL_IGNORED",
817
+ run: snapshot,
818
+ message: "目标任务已结束,无需取消。"
819
+ };
820
+ }
821
+
822
+ if (snapshot.state === RUN_STATE_PAUSED) {
823
+ const canceledResult = {
824
+ status: "FAILED",
825
+ error: {
826
+ code: "PIPELINE_CANCELED",
827
+ message: "流水线已取消。",
828
+ retryable: true
829
+ },
830
+ partial_result: snapshot.result?.partial_result || snapshot.result?.result || null
831
+ };
832
+ const canceledRun = safeUpdateRunState(runId, {
833
+ state: RUN_STATE_CANCELED,
834
+ stage: snapshot.stage || RUN_STAGE_PREFLIGHT,
835
+ last_message: "流水线已取消。",
836
+ control: {
837
+ pause_requested: false,
838
+ pause_requested_at: null,
839
+ pause_requested_by: null,
840
+ cancel_requested: false
841
+ },
842
+ error: canceledResult.error,
843
+ result: canceledResult
844
+ }) || readRunState(runId) || snapshot;
845
+ return {
846
+ status: "CANCEL_REQUESTED",
847
+ run: canceledRun
848
+ };
849
+ }
850
+
851
+ const activeRun = activeAsyncRuns.get(runId);
852
+ if (!activeRun) {
853
+ const canceledResult = {
854
+ status: "FAILED",
855
+ error: {
856
+ code: "PIPELINE_CANCELED",
857
+ message: "流水线已取消。",
858
+ retryable: true
859
+ },
860
+ partial_result: snapshot.result?.partial_result || snapshot.result?.result || null
861
+ };
862
+ const canceledRun = safeUpdateRunState(runId, {
863
+ state: RUN_STATE_CANCELED,
864
+ stage: snapshot.stage || RUN_STAGE_PREFLIGHT,
865
+ last_message: "流水线已取消。",
866
+ control: {
867
+ pause_requested: false,
868
+ pause_requested_at: null,
869
+ pause_requested_by: null,
870
+ cancel_requested: false
871
+ },
872
+ error: canceledResult.error,
873
+ result: canceledResult
874
+ }) || readRunState(runId) || snapshot;
875
+ return {
876
+ status: "CANCEL_REQUESTED",
877
+ run: canceledRun
878
+ };
879
+ }
880
+ safeUpdateRunState(runId, {
881
+ stage: snapshot.stage || RUN_STAGE_PREFLIGHT,
882
+ last_message: "已收到取消请求,将在当前候选人处理完成后安全停止并落盘 CSV。",
883
+ control: {
884
+ pause_requested: true,
885
+ pause_requested_at: new Date().toISOString(),
886
+ pause_requested_by: TOOL_CANCEL_RUN,
887
+ cancel_requested: true
888
+ }
889
+ });
890
+
891
+ const latest = readRunState(runId) || snapshot;
892
+ return {
893
+ status: "CANCEL_REQUESTED",
894
+ run: latest
895
+ };
896
+ }
897
+
898
+ function handlePauseRunTool(args) {
899
+ const runId = normalizeText(args?.run_id);
900
+ if (!runId) {
901
+ return {
902
+ status: "FAILED",
903
+ error: {
904
+ code: "INVALID_RUN_ID",
905
+ message: "run_id is required",
906
+ retryable: false
907
+ }
908
+ };
909
+ }
910
+ const snapshot = readRunState(runId);
911
+ if (!snapshot) {
912
+ return {
913
+ status: "FAILED",
914
+ error: {
915
+ code: "RUN_NOT_FOUND",
916
+ message: `未找到 run_id=${runId} 的运行记录。`,
917
+ retryable: false
918
+ }
919
+ };
920
+ }
921
+
922
+ if (TERMINAL_RUN_STATES.has(snapshot.state)) {
923
+ return {
924
+ status: "PAUSE_IGNORED",
925
+ run: snapshot,
926
+ message: "目标任务已结束,无需暂停。"
927
+ };
928
+ }
929
+ if (snapshot.state === RUN_STATE_PAUSED) {
930
+ return {
931
+ status: "PAUSE_IGNORED",
932
+ run: snapshot,
933
+ message: "目标任务已经处于 paused 状态。"
934
+ };
935
+ }
936
+
937
+ const requestedRun = safeUpdateRunState(runId, {
938
+ control: {
939
+ pause_requested: true,
940
+ pause_requested_at: new Date().toISOString(),
941
+ pause_requested_by: TOOL_PAUSE_RUN,
942
+ cancel_requested: false
943
+ },
944
+ last_message: "已收到暂停请求,将在当前候选人处理完成后暂停。"
945
+ }) || readRunState(runId) || snapshot;
946
+ return {
947
+ status: "PAUSE_REQUESTED",
948
+ run: requestedRun,
949
+ message: "暂停请求已接收,将在当前候选人处理完成后进入 paused。"
950
+ };
951
+ }
952
+
953
+ function handleResumeRunTool(args) {
954
+ const runId = normalizeText(args?.run_id);
955
+ if (!runId) {
956
+ return {
957
+ status: "FAILED",
958
+ error: {
959
+ code: "INVALID_RUN_ID",
960
+ message: "run_id is required",
961
+ retryable: false
962
+ }
963
+ };
964
+ }
965
+ const snapshot = readRunState(runId);
966
+ if (!snapshot) {
967
+ return {
968
+ status: "FAILED",
969
+ error: {
970
+ code: "RUN_NOT_FOUND",
971
+ message: `未找到 run_id=${runId} 的运行记录。`,
972
+ retryable: false
973
+ }
974
+ };
975
+ }
976
+ if (TERMINAL_RUN_STATES.has(snapshot.state)) {
977
+ return {
978
+ status: "FAILED",
979
+ error: {
980
+ code: "RUN_ALREADY_TERMINATED",
981
+ message: "目标任务已结束,无法继续。",
982
+ retryable: false
983
+ }
984
+ };
985
+ }
986
+ if (snapshot.state !== RUN_STATE_PAUSED) {
987
+ return {
988
+ status: "FAILED",
989
+ error: {
990
+ code: "RUN_NOT_PAUSED",
991
+ message: "仅 paused 状态的 run 才能继续。",
992
+ retryable: true
993
+ },
994
+ run: snapshot
995
+ };
996
+ }
997
+ if (activeAsyncRuns.has(runId)) {
998
+ return {
999
+ status: "RESUME_IGNORED",
1000
+ run: snapshot,
1001
+ message: "该 run 当前已在执行,无需继续。"
1002
+ };
1003
+ }
1004
+
1005
+ const executionContext = resolveRunContext(snapshot);
1006
+ if (!executionContext) {
1007
+ return {
1008
+ status: "FAILED",
1009
+ error: {
1010
+ code: "RUN_CONTEXT_MISSING",
1011
+ message: "run 缺少可恢复的执行上下文,无法继续。",
1012
+ retryable: false
1013
+ }
1014
+ };
1015
+ }
1016
+
1017
+ const updated = safeUpdateRunState(runId, (current) => ({
1018
+ state: "queued",
1019
+ last_message: "已收到继续请求,准备恢复执行。",
1020
+ control: {
1021
+ pause_requested: false,
1022
+ pause_requested_at: null,
1023
+ pause_requested_by: null,
1024
+ cancel_requested: false
1025
+ },
1026
+ resume: {
1027
+ checkpoint_path: current?.resume?.checkpoint_path || getRunArtifacts(runId).checkpoint_path,
1028
+ pause_control_path: current?.resume?.pause_control_path || getRunArtifacts(runId).run_state_path,
1029
+ output_csv: current?.resume?.output_csv || null,
1030
+ resume_count: Number.isInteger(current?.resume?.resume_count) ? current.resume.resume_count + 1 : 1,
1031
+ last_resumed_at: new Date().toISOString()
1032
+ }
1033
+ })) || readRunState(runId) || snapshot;
1034
+
1035
+ launchAsyncRun({
1036
+ runId,
1037
+ mode: RUN_MODE_ASYNC,
1038
+ workspaceRoot: executionContext.workspaceRoot,
1039
+ args: executionContext.args,
1040
+ resumeRun: true
1041
+ });
1042
+
1043
+ return {
1044
+ status: "RESUME_REQUESTED",
1045
+ run: updated,
1046
+ poll_after_sec: getDefaultPollAfterSec(),
1047
+ message: "已恢复 Recommend 流水线。默认不自动轮询;如需进度请按需调用 get_recommend_pipeline_run。"
1048
+ };
1049
+ }
1050
+
1051
+ async function handleRequest(message, workspaceRoot) {
1052
+ if (!message || message.jsonrpc !== "2.0") {
1053
+ return createJsonRpcError(null, -32600, "Invalid JSON-RPC request");
1054
+ }
1055
+
1056
+ const { id, method, params } = message;
1057
+
1058
+ if (method === "initialize") {
1059
+ return {
1060
+ jsonrpc: "2.0",
1061
+ id,
1062
+ result: {
1063
+ protocolVersion: "2024-11-05",
1064
+ capabilities: {
1065
+ tools: {}
1066
+ },
1067
+ serverInfo: {
1068
+ name: SERVER_NAME,
1069
+ version: SERVER_VERSION
1070
+ }
1071
+ }
1072
+ };
1073
+ }
1074
+
1075
+ if (method === "notifications/initialized") {
1076
+ return null;
1077
+ }
1078
+
1079
+ if (method === "tools/list") {
1080
+ return {
1081
+ jsonrpc: "2.0",
1082
+ id,
1083
+ result: {
1084
+ tools: createToolsSchema()
1085
+ }
1086
+ };
1087
+ }
1088
+
1089
+ if (method === "tools/call") {
1090
+ const toolName = params?.name;
1091
+ const args = params?.arguments || {};
1092
+
1093
+ if (toolName === TOOL_START_RUN) {
1094
+ const inputError = validateRunArgs(args);
1095
+ if (inputError) {
1096
+ return createJsonRpcError(id, -32602, inputError);
1097
+ }
1098
+ }
1099
+
1100
+ if ([TOOL_GET_RUN, TOOL_CANCEL_RUN, TOOL_PAUSE_RUN, TOOL_RESUME_RUN].includes(toolName)) {
1101
+ if (!args || typeof args.run_id !== "string" || !normalizeText(args.run_id)) {
1102
+ return createJsonRpcError(id, -32602, "run_id is required and must be a string");
1103
+ }
1104
+ }
1105
+
1106
+ try {
1107
+ let payload;
1108
+ if (toolName === TOOL_START_RUN) {
1109
+ payload = await handleStartRunTool({ workspaceRoot, args });
1110
+ } else if (toolName === TOOL_GET_RUN) {
1111
+ payload = handleGetRunTool(args);
1112
+ } else if (toolName === TOOL_CANCEL_RUN) {
1113
+ payload = handleCancelRunTool(args);
1114
+ } else if (toolName === TOOL_PAUSE_RUN) {
1115
+ payload = handlePauseRunTool(args);
1116
+ } else if (toolName === TOOL_RESUME_RUN) {
1117
+ payload = handleResumeRunTool(args);
1118
+ } else {
1119
+ return createJsonRpcError(id, -32602, `Unknown tool: ${toolName || ""}`);
1120
+ }
1121
+ const isError = payload?.status === "FAILED";
1122
+ return createToolResultResponse(id, payload, isError);
1123
+ } catch (error) {
1124
+ const failed = {
1125
+ status: "FAILED",
1126
+ error: {
1127
+ code: "UNEXPECTED_ERROR",
1128
+ message: error?.message || "Unexpected error",
1129
+ retryable: true
1130
+ }
1131
+ };
1132
+ return createToolResultResponse(id, failed, true);
1133
+ }
1134
+ }
1135
+
1136
+ if (method === "ping") {
1137
+ return { jsonrpc: "2.0", id, result: {} };
1138
+ }
1139
+
1140
+ if (id === undefined || id === null) {
1141
+ return null;
1142
+ }
1143
+ return createJsonRpcError(id, -32601, `Method not found: ${method}`);
1144
+ }
1145
+
1146
+ export function startServer() {
1147
+ const envRoot = process.env.BOSS_WORKSPACE_ROOT;
1148
+ const workspaceRoot = envRoot
1149
+ ? path.resolve(envRoot)
1150
+ : process.env.INIT_CWD
1151
+ ? path.resolve(process.env.INIT_CWD)
1152
+ : path.resolve(process.cwd());
1153
+ let buffer = Buffer.alloc(0);
1154
+ let framing = FRAMING_UNKNOWN;
1155
+
1156
+ process.stdin.on("data", async (chunk) => {
1157
+ buffer = Buffer.concat([buffer, chunk]);
1158
+ if (buffer.length >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) {
1159
+ buffer = buffer.slice(3);
1160
+ }
1161
+
1162
+ while (true) {
1163
+ const crlfHeaderEnd = buffer.indexOf("\r\n\r\n");
1164
+ const lfHeaderEnd = buffer.indexOf("\n\n");
1165
+ const crHeaderEnd = buffer.indexOf("\r\r");
1166
+ let headerEnd = -1;
1167
+ let headerSeparatorLength = 0;
1168
+ if (
1169
+ crlfHeaderEnd !== -1
1170
+ && (lfHeaderEnd === -1 || crlfHeaderEnd < lfHeaderEnd)
1171
+ && (crHeaderEnd === -1 || crlfHeaderEnd < crHeaderEnd)
1172
+ ) {
1173
+ headerEnd = crlfHeaderEnd;
1174
+ headerSeparatorLength = 4;
1175
+ } else if (lfHeaderEnd !== -1 && (crHeaderEnd === -1 || lfHeaderEnd < crHeaderEnd)) {
1176
+ headerEnd = lfHeaderEnd;
1177
+ headerSeparatorLength = 2;
1178
+ } else if (crHeaderEnd !== -1) {
1179
+ headerEnd = crHeaderEnd;
1180
+ headerSeparatorLength = 2;
1181
+ }
1182
+ if (headerEnd !== -1) {
1183
+ const headerText = buffer.slice(0, headerEnd).toString("utf8");
1184
+ const contentLengthLine = headerText
1185
+ .split(/\r\n|\n|\r/)
1186
+ .find((line) => line.toLowerCase().startsWith("content-length:"));
1187
+
1188
+ if (!contentLengthLine) {
1189
+ buffer = buffer.slice(headerEnd + headerSeparatorLength);
1190
+ continue;
1191
+ }
1192
+
1193
+ const contentLength = Number.parseInt(contentLengthLine.split(":")[1].trim(), 10);
1194
+ if (!Number.isFinite(contentLength) || contentLength < 0) {
1195
+ buffer = buffer.slice(headerEnd + headerSeparatorLength);
1196
+ continue;
1197
+ }
1198
+
1199
+ const bodyStart = headerEnd + headerSeparatorLength;
1200
+ const bodyEnd = bodyStart + contentLength;
1201
+ if (buffer.length < bodyEnd) break;
1202
+
1203
+ const body = buffer.slice(bodyStart, bodyEnd).toString("utf8");
1204
+ buffer = buffer.slice(bodyEnd);
1205
+ framing = FRAMING_HEADER;
1206
+
1207
+ let message;
1208
+ try {
1209
+ message = JSON.parse(body);
1210
+ } catch {
1211
+ writeMessage(createJsonRpcError(null, -32700, "Parse error"), FRAMING_HEADER);
1212
+ continue;
1213
+ }
1214
+
1215
+ const response = await handleRequest(message, workspaceRoot);
1216
+ if (response) writeMessage(response, framing);
1217
+ continue;
1218
+ }
1219
+
1220
+ const newlineIndex = buffer.indexOf("\n");
1221
+ if (newlineIndex === -1) break;
1222
+ const rawLine = buffer.slice(0, newlineIndex).toString("utf8").replace(/\r$/, "");
1223
+ if (/^\s*content-length:/i.test(rawLine)) break;
1224
+ buffer = buffer.slice(newlineIndex + 1);
1225
+ const line = rawLine.trim();
1226
+ if (!line) continue;
1227
+ framing = FRAMING_LINE;
1228
+
1229
+ let message;
1230
+ try {
1231
+ message = JSON.parse(line);
1232
+ } catch {
1233
+ writeMessage(createJsonRpcError(null, -32700, "Parse error"), FRAMING_LINE);
1234
+ continue;
1235
+ }
1236
+
1237
+ const response = await handleRequest(message, workspaceRoot);
1238
+ if (response) writeMessage(response, framing);
1239
+ }
1240
+ });
1241
+ }
1242
+
1243
+ export const __testables = {
1244
+ handleRequest,
1245
+ activeAsyncRuns,
1246
+ setRunPipelineImplForTests(nextImpl) {
1247
+ runPipelineImpl = typeof nextImpl === "function" ? nextImpl : runRecommendPipeline;
1248
+ }
1249
+ };
1250
+
1251
+ const thisFilePath = fileURLToPath(import.meta.url);
1252
+ if (process.argv[1] && path.resolve(process.argv[1]) === thisFilePath) {
1253
+ startServer();
1254
+ }