@reconcrap/boss-recruit-mcp 1.0.19 → 1.0.21
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/README.md +24 -3
- package/package.json +3 -1
- package/skills/boss-recruit-pipeline/SKILL.md +11 -2
- package/src/adapters.js +204 -7
- package/src/index.js +608 -80
- package/src/pipeline.js +230 -11
- package/src/run-state.js +294 -0
- package/src/test-index-async.js +231 -0
- package/src/test-run-state.js +115 -0
package/src/index.js
CHANGED
|
@@ -2,16 +2,59 @@ import path from "node:path";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
PIPELINE_STATUS_READY_TO_START_ASYNC,
|
|
7
|
+
runRecruitPipeline
|
|
8
|
+
} from "./pipeline.js";
|
|
9
|
+
import {
|
|
10
|
+
RUN_MODE_ASYNC,
|
|
11
|
+
RUN_MODE_SYNC,
|
|
12
|
+
RUN_STAGE_PREFLIGHT,
|
|
13
|
+
RUN_STATE_CANCELED,
|
|
14
|
+
RUN_STATE_COMPLETED,
|
|
15
|
+
RUN_STATE_FAILED,
|
|
16
|
+
RUN_STATE_RUNNING,
|
|
17
|
+
cleanupExpiredRuns,
|
|
18
|
+
createRunId,
|
|
19
|
+
createRunStateSnapshot,
|
|
20
|
+
getRunHeartbeatIntervalMs,
|
|
21
|
+
readRunState,
|
|
22
|
+
touchRunHeartbeat,
|
|
23
|
+
updateRunProgress,
|
|
24
|
+
updateRunState,
|
|
25
|
+
writeRunState
|
|
26
|
+
} from "./run-state.js";
|
|
6
27
|
|
|
7
28
|
const require = createRequire(import.meta.url);
|
|
8
29
|
const { version: SERVER_VERSION } = require("../package.json");
|
|
9
|
-
|
|
30
|
+
|
|
31
|
+
const TOOL_RUN_PIPELINE = "run_recruit_pipeline";
|
|
32
|
+
const TOOL_START_RUN = "start_recruit_pipeline_run";
|
|
33
|
+
const TOOL_GET_RUN = "get_recruit_pipeline_run";
|
|
34
|
+
const TOOL_CANCEL_RUN = "cancel_recruit_pipeline_run";
|
|
35
|
+
|
|
10
36
|
const SERVER_NAME = "boss-recruit-mcp";
|
|
11
37
|
const FRAMING_UNKNOWN = "unknown";
|
|
12
38
|
const FRAMING_HEADER = "header";
|
|
13
39
|
const FRAMING_LINE = "line";
|
|
14
40
|
|
|
41
|
+
const activeAsyncRuns = new Map();
|
|
42
|
+
let runPipelineImpl = runRecruitPipeline;
|
|
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_RECRUIT_POLL_AFTER_SEC, 10);
|
|
55
|
+
return Math.max(5, Math.min(15, fromEnv));
|
|
56
|
+
}
|
|
57
|
+
|
|
15
58
|
function writeMessage(message, framing = FRAMING_LINE) {
|
|
16
59
|
const body = JSON.stringify(message);
|
|
17
60
|
if (framing === FRAMING_HEADER) {
|
|
@@ -30,48 +73,530 @@ function createJsonRpcError(id, code, message) {
|
|
|
30
73
|
};
|
|
31
74
|
}
|
|
32
75
|
|
|
33
|
-
function
|
|
76
|
+
function createRunInputSchema() {
|
|
34
77
|
return {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
instruction: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "用户自然语言招聘指令"
|
|
83
|
+
},
|
|
84
|
+
execution_mode: {
|
|
85
|
+
type: "string",
|
|
86
|
+
enum: [RUN_MODE_ASYNC, RUN_MODE_SYNC],
|
|
87
|
+
description: "执行模式;默认 async。"
|
|
88
|
+
},
|
|
89
|
+
confirmation: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
keyword_confirmed: { type: "boolean" },
|
|
93
|
+
keyword_value: { type: "string" },
|
|
94
|
+
search_params_confirmed: { type: "boolean" },
|
|
95
|
+
criteria_confirmed: { type: "boolean" },
|
|
96
|
+
use_default_for_missing: { type: "boolean" }
|
|
43
97
|
},
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
98
|
+
additionalProperties: false
|
|
99
|
+
},
|
|
100
|
+
overrides: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
city: { type: "string" },
|
|
104
|
+
degree: { type: "string" },
|
|
105
|
+
filter_recent_viewed: { type: "boolean" },
|
|
106
|
+
schools: {
|
|
107
|
+
anyOf: [
|
|
108
|
+
{ type: "array", items: { type: "string" } },
|
|
109
|
+
{ type: "string" }
|
|
110
|
+
]
|
|
51
111
|
},
|
|
52
|
-
|
|
112
|
+
keyword: { type: "string" },
|
|
113
|
+
target_count: { type: "integer", minimum: 1 },
|
|
114
|
+
criteria: { type: "string" }
|
|
53
115
|
},
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
116
|
+
additionalProperties: false
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
required: ["instruction"],
|
|
120
|
+
additionalProperties: false
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function createToolsSchema() {
|
|
125
|
+
return [
|
|
126
|
+
{
|
|
127
|
+
name: TOOL_RUN_PIPELINE,
|
|
128
|
+
description: "Boss 招聘流水线:默认异步,但会先走与同步一致的前置确认/页面就绪门禁;仅在门禁通过后返回 run_id。传 execution_mode=sync 可改为全程同步执行。",
|
|
129
|
+
inputSchema: createRunInputSchema()
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: TOOL_START_RUN,
|
|
133
|
+
description: "异步启动 Boss 招聘流水线(含同步门禁预检);只有在前置确认与页面就绪通过后才返回 run_id。",
|
|
134
|
+
inputSchema: createRunInputSchema()
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: TOOL_GET_RUN,
|
|
138
|
+
description: "按 run_id 查询异步/同步流水线运行状态快照。",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: "object",
|
|
141
|
+
properties: {
|
|
142
|
+
run_id: { type: "string" }
|
|
143
|
+
},
|
|
144
|
+
required: ["run_id"],
|
|
145
|
+
additionalProperties: false
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: TOOL_CANCEL_RUN,
|
|
150
|
+
description: "取消指定 run_id 的运行中流水线。",
|
|
151
|
+
inputSchema: {
|
|
152
|
+
type: "object",
|
|
153
|
+
properties: {
|
|
154
|
+
run_id: { type: "string" }
|
|
155
|
+
},
|
|
156
|
+
required: ["run_id"],
|
|
157
|
+
additionalProperties: false
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function createToolResultResponse(id, payload, isError = false) {
|
|
164
|
+
return {
|
|
165
|
+
jsonrpc: "2.0",
|
|
166
|
+
id,
|
|
167
|
+
result: {
|
|
168
|
+
content: [
|
|
169
|
+
{
|
|
170
|
+
type: "text",
|
|
171
|
+
text: JSON.stringify(payload, null, 2)
|
|
70
172
|
}
|
|
173
|
+
],
|
|
174
|
+
structuredContent: payload,
|
|
175
|
+
...(isError ? { isError: true } : {})
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function validateRunArgs(args) {
|
|
181
|
+
if (!args || typeof args !== "object") {
|
|
182
|
+
return "arguments must be an object";
|
|
183
|
+
}
|
|
184
|
+
if (!args.instruction || typeof args.instruction !== "string") {
|
|
185
|
+
return "instruction is required and must be a string";
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getLastOutputLine(text) {
|
|
191
|
+
const lines = String(text || "")
|
|
192
|
+
.split(/\r?\n/)
|
|
193
|
+
.map((line) => normalizeText(line))
|
|
194
|
+
.filter(Boolean);
|
|
195
|
+
return lines.length > 0 ? lines[lines.length - 1] : null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function normalizeExecutionMode(value) {
|
|
199
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
200
|
+
if (normalized === RUN_MODE_SYNC) return RUN_MODE_SYNC;
|
|
201
|
+
return RUN_MODE_ASYNC;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function buildAsyncPrecheckArgs(args) {
|
|
205
|
+
return {
|
|
206
|
+
instruction: args.instruction,
|
|
207
|
+
confirmation: args.confirmation,
|
|
208
|
+
overrides: args.overrides
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function safeUpdateRunState(runId, updater) {
|
|
213
|
+
try {
|
|
214
|
+
return updateRunState(runId, updater);
|
|
215
|
+
} catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function safeUpdateRunProgress(runId, patch, message = null) {
|
|
221
|
+
try {
|
|
222
|
+
return updateRunProgress(runId, patch, message);
|
|
223
|
+
} catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function createRuntimeCallbacks(runId, heartbeatIntervalMs) {
|
|
229
|
+
let lastStage = RUN_STAGE_PREFLIGHT;
|
|
230
|
+
let lastOutputPersistAt = 0;
|
|
231
|
+
return {
|
|
232
|
+
heartbeatIntervalMs,
|
|
233
|
+
onStage(event) {
|
|
234
|
+
const stage = normalizeText(event?.stage) || RUN_STAGE_PREFLIGHT;
|
|
235
|
+
lastStage = stage;
|
|
236
|
+
safeUpdateRunState(runId, {
|
|
237
|
+
state: RUN_STATE_RUNNING,
|
|
238
|
+
stage,
|
|
239
|
+
last_message: normalizeText(event?.message || "")
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
onHeartbeat(event) {
|
|
243
|
+
const stage = normalizeText(event?.stage) || lastStage;
|
|
244
|
+
lastStage = stage || lastStage;
|
|
245
|
+
const detailsMessage = normalizeText(event?.details?.message || "");
|
|
246
|
+
const patch = { stage: lastStage };
|
|
247
|
+
if (detailsMessage) {
|
|
248
|
+
patch.last_message = detailsMessage;
|
|
249
|
+
}
|
|
250
|
+
safeUpdateRunState(runId, patch);
|
|
251
|
+
try {
|
|
252
|
+
touchRunHeartbeat(runId, detailsMessage || undefined);
|
|
253
|
+
} catch {
|
|
254
|
+
// Ignore heartbeat persistence failures here; state updates above already best-effort.
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
onOutput(event) {
|
|
258
|
+
const stage = normalizeText(event?.stage) || lastStage;
|
|
259
|
+
lastStage = stage || lastStage;
|
|
260
|
+
const now = Date.now();
|
|
261
|
+
if (now - lastOutputPersistAt < 1000) return;
|
|
262
|
+
lastOutputPersistAt = now;
|
|
263
|
+
const message = getLastOutputLine(event?.text);
|
|
264
|
+
if (!message) return;
|
|
265
|
+
safeUpdateRunState(runId, {
|
|
266
|
+
stage: lastStage,
|
|
267
|
+
last_message: message
|
|
268
|
+
});
|
|
269
|
+
},
|
|
270
|
+
onProgress(event) {
|
|
271
|
+
const stage = normalizeText(event?.stage) || lastStage;
|
|
272
|
+
lastStage = stage || lastStage;
|
|
273
|
+
safeUpdateRunState(runId, { stage: lastStage });
|
|
274
|
+
safeUpdateRunProgress(
|
|
275
|
+
runId,
|
|
276
|
+
{
|
|
277
|
+
processed: Number.isInteger(event?.processed) ? event.processed : undefined,
|
|
278
|
+
passed: Number.isInteger(event?.passed) ? event.passed : undefined,
|
|
279
|
+
skipped: Number.isInteger(event?.skipped) ? event.skipped : undefined,
|
|
280
|
+
greet_count: Number.isInteger(event?.greet_count) ? event.greet_count : undefined
|
|
281
|
+
},
|
|
282
|
+
normalizeText(event?.line || "")
|
|
283
|
+
);
|
|
284
|
+
},
|
|
285
|
+
getLastStage() {
|
|
286
|
+
return lastStage;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function attachSyncRunMetadata(result, runId, lastStage) {
|
|
292
|
+
if (!result || typeof result !== "object") return result;
|
|
293
|
+
if (result.status === "COMPLETED") {
|
|
294
|
+
const nextResult = result.result && typeof result.result === "object" ? result.result : {};
|
|
295
|
+
return {
|
|
296
|
+
...result,
|
|
297
|
+
result: {
|
|
298
|
+
...nextResult,
|
|
299
|
+
run_id: runId
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (result.status === "FAILED") {
|
|
304
|
+
const diagnostics = result.diagnostics && typeof result.diagnostics === "object" ? result.diagnostics : {};
|
|
305
|
+
return {
|
|
306
|
+
...result,
|
|
307
|
+
diagnostics: {
|
|
308
|
+
...diagnostics,
|
|
309
|
+
run_id: runId,
|
|
310
|
+
last_stage: lastStage || RUN_STAGE_PREFLIGHT
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function executeTrackedPipeline({
|
|
318
|
+
runId,
|
|
319
|
+
mode,
|
|
320
|
+
workspaceRoot,
|
|
321
|
+
args,
|
|
322
|
+
signal
|
|
323
|
+
}) {
|
|
324
|
+
const heartbeatIntervalMs = getRunHeartbeatIntervalMs();
|
|
325
|
+
const runtimeCallbacks = createRuntimeCallbacks(runId, heartbeatIntervalMs);
|
|
326
|
+
safeUpdateRunState(runId, {
|
|
327
|
+
state: RUN_STATE_RUNNING,
|
|
328
|
+
stage: RUN_STAGE_PREFLIGHT,
|
|
329
|
+
last_message: "流水线已启动,等待 preflight。"
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
let result;
|
|
333
|
+
try {
|
|
334
|
+
result = await runPipelineImpl(
|
|
335
|
+
{
|
|
336
|
+
workspaceRoot,
|
|
337
|
+
instruction: args.instruction,
|
|
338
|
+
confirmation: args.confirmation,
|
|
339
|
+
overrides: args.overrides
|
|
71
340
|
},
|
|
72
|
-
|
|
73
|
-
|
|
341
|
+
undefined,
|
|
342
|
+
{
|
|
343
|
+
signal,
|
|
344
|
+
heartbeatIntervalMs,
|
|
345
|
+
onStage: runtimeCallbacks.onStage,
|
|
346
|
+
onHeartbeat: runtimeCallbacks.onHeartbeat,
|
|
347
|
+
onOutput: runtimeCallbacks.onOutput,
|
|
348
|
+
onProgress: runtimeCallbacks.onProgress
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
const canceled = Boolean(signal?.aborted) || error?.code === "PIPELINE_ABORTED";
|
|
353
|
+
if (canceled) {
|
|
354
|
+
const canceledResult = {
|
|
355
|
+
status: "FAILED",
|
|
356
|
+
error: {
|
|
357
|
+
code: "PIPELINE_CANCELED",
|
|
358
|
+
message: "流水线已取消。",
|
|
359
|
+
retryable: true
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
safeUpdateRunState(runId, {
|
|
363
|
+
mode,
|
|
364
|
+
state: RUN_STATE_CANCELED,
|
|
365
|
+
stage: runtimeCallbacks.getLastStage(),
|
|
366
|
+
last_message: "流水线已取消。",
|
|
367
|
+
error: canceledResult.error,
|
|
368
|
+
result: canceledResult
|
|
369
|
+
});
|
|
370
|
+
return {
|
|
371
|
+
result: canceledResult,
|
|
372
|
+
lastStage: runtimeCallbacks.getLastStage(),
|
|
373
|
+
state: RUN_STATE_CANCELED
|
|
374
|
+
};
|
|
74
375
|
}
|
|
376
|
+
|
|
377
|
+
const failedResult = {
|
|
378
|
+
status: "FAILED",
|
|
379
|
+
error: {
|
|
380
|
+
code: "UNEXPECTED_ERROR",
|
|
381
|
+
message: error?.message || "Unexpected error",
|
|
382
|
+
retryable: true
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
safeUpdateRunState(runId, {
|
|
386
|
+
mode,
|
|
387
|
+
state: RUN_STATE_FAILED,
|
|
388
|
+
stage: runtimeCallbacks.getLastStage(),
|
|
389
|
+
last_message: failedResult.error.message,
|
|
390
|
+
error: failedResult.error,
|
|
391
|
+
result: failedResult
|
|
392
|
+
});
|
|
393
|
+
return {
|
|
394
|
+
result: failedResult,
|
|
395
|
+
lastStage: runtimeCallbacks.getLastStage(),
|
|
396
|
+
state: RUN_STATE_FAILED
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const terminalState = result?.status === "FAILED"
|
|
401
|
+
? RUN_STATE_FAILED
|
|
402
|
+
: RUN_STATE_COMPLETED;
|
|
403
|
+
safeUpdateRunState(runId, {
|
|
404
|
+
mode,
|
|
405
|
+
state: terminalState,
|
|
406
|
+
stage: runtimeCallbacks.getLastStage(),
|
|
407
|
+
last_message: terminalState === RUN_STATE_COMPLETED ? "流水线执行完成。" : (result?.error?.message || "流水线执行失败。"),
|
|
408
|
+
error: terminalState === RUN_STATE_FAILED ? (result?.error || null) : null,
|
|
409
|
+
result: result || null
|
|
410
|
+
});
|
|
411
|
+
return {
|
|
412
|
+
result,
|
|
413
|
+
lastStage: runtimeCallbacks.getLastStage(),
|
|
414
|
+
state: terminalState
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function initializeRunStateOrThrow(runId, mode) {
|
|
419
|
+
const snapshot = createRunStateSnapshot({
|
|
420
|
+
runId,
|
|
421
|
+
mode,
|
|
422
|
+
state: "queued",
|
|
423
|
+
stage: RUN_STAGE_PREFLIGHT,
|
|
424
|
+
pid: process.pid,
|
|
425
|
+
lastMessage: "流水线任务已创建,等待执行。"
|
|
426
|
+
});
|
|
427
|
+
return writeRunState(snapshot);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function handleSyncRunTool({ workspaceRoot, args }) {
|
|
431
|
+
cleanupExpiredRuns();
|
|
432
|
+
const runId = createRunId();
|
|
433
|
+
try {
|
|
434
|
+
initializeRunStateOrThrow(runId, RUN_MODE_SYNC);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
return {
|
|
437
|
+
status: "FAILED",
|
|
438
|
+
error: {
|
|
439
|
+
code: "RUN_STATE_IO_ERROR",
|
|
440
|
+
message: `无法写入运行状态目录:${error.message || "unknown"}`,
|
|
441
|
+
retryable: false
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
const tracked = await executeTrackedPipeline({
|
|
446
|
+
runId,
|
|
447
|
+
mode: RUN_MODE_SYNC,
|
|
448
|
+
workspaceRoot,
|
|
449
|
+
args,
|
|
450
|
+
signal: null
|
|
451
|
+
});
|
|
452
|
+
return attachSyncRunMetadata(tracked.result, runId, tracked.lastStage);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function handleStartRunTool({ workspaceRoot, args }) {
|
|
456
|
+
const precheckArgs = buildAsyncPrecheckArgs(args);
|
|
457
|
+
let precheckResult;
|
|
458
|
+
try {
|
|
459
|
+
precheckResult = await runPipelineImpl(
|
|
460
|
+
{
|
|
461
|
+
workspaceRoot,
|
|
462
|
+
instruction: precheckArgs.instruction,
|
|
463
|
+
confirmation: precheckArgs.confirmation,
|
|
464
|
+
overrides: precheckArgs.overrides
|
|
465
|
+
},
|
|
466
|
+
undefined,
|
|
467
|
+
{
|
|
468
|
+
precheckOnly: true
|
|
469
|
+
}
|
|
470
|
+
);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
precheckResult = {
|
|
473
|
+
status: "FAILED",
|
|
474
|
+
error: {
|
|
475
|
+
code: "UNEXPECTED_ERROR",
|
|
476
|
+
message: error?.message || "Unexpected error",
|
|
477
|
+
retryable: true
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (precheckResult?.status !== PIPELINE_STATUS_READY_TO_START_ASYNC) {
|
|
483
|
+
return precheckResult;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
cleanupExpiredRuns();
|
|
487
|
+
const runId = createRunId();
|
|
488
|
+
try {
|
|
489
|
+
initializeRunStateOrThrow(runId, RUN_MODE_ASYNC);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
return {
|
|
492
|
+
status: "FAILED",
|
|
493
|
+
error: {
|
|
494
|
+
code: "RUN_STATE_IO_ERROR",
|
|
495
|
+
message: `无法写入运行状态目录:${error.message || "unknown"}`,
|
|
496
|
+
retryable: false
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const abortController = new AbortController();
|
|
502
|
+
const promise = executeTrackedPipeline({
|
|
503
|
+
runId,
|
|
504
|
+
mode: RUN_MODE_ASYNC,
|
|
505
|
+
workspaceRoot,
|
|
506
|
+
args,
|
|
507
|
+
signal: abortController.signal
|
|
508
|
+
}).finally(() => {
|
|
509
|
+
activeAsyncRuns.delete(runId);
|
|
510
|
+
});
|
|
511
|
+
activeAsyncRuns.set(runId, {
|
|
512
|
+
abortController,
|
|
513
|
+
promise
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
status: "ACCEPTED",
|
|
518
|
+
run_id: runId,
|
|
519
|
+
state: "queued",
|
|
520
|
+
poll_after_sec: getDefaultPollAfterSec(),
|
|
521
|
+
message: "异步流水线已启动,请使用 get_recruit_pipeline_run 轮询状态。"
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function handleGetRunTool(args) {
|
|
526
|
+
cleanupExpiredRuns();
|
|
527
|
+
const runId = normalizeText(args?.run_id);
|
|
528
|
+
if (!runId) {
|
|
529
|
+
return {
|
|
530
|
+
status: "FAILED",
|
|
531
|
+
error: {
|
|
532
|
+
code: "INVALID_RUN_ID",
|
|
533
|
+
message: "run_id is required",
|
|
534
|
+
retryable: false
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
const snapshot = readRunState(runId);
|
|
539
|
+
if (!snapshot) {
|
|
540
|
+
return {
|
|
541
|
+
status: "FAILED",
|
|
542
|
+
error: {
|
|
543
|
+
code: "RUN_NOT_FOUND",
|
|
544
|
+
message: `未找到 run_id=${runId} 的运行记录。`,
|
|
545
|
+
retryable: false
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
status: "RUN_STATUS",
|
|
551
|
+
run: snapshot
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function handleCancelRunTool(args) {
|
|
556
|
+
const runId = normalizeText(args?.run_id);
|
|
557
|
+
if (!runId) {
|
|
558
|
+
return {
|
|
559
|
+
status: "FAILED",
|
|
560
|
+
error: {
|
|
561
|
+
code: "INVALID_RUN_ID",
|
|
562
|
+
message: "run_id is required",
|
|
563
|
+
retryable: false
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
const snapshot = readRunState(runId);
|
|
568
|
+
if (!snapshot) {
|
|
569
|
+
return {
|
|
570
|
+
status: "FAILED",
|
|
571
|
+
error: {
|
|
572
|
+
code: "RUN_NOT_FOUND",
|
|
573
|
+
message: `未找到 run_id=${runId} 的运行记录。`,
|
|
574
|
+
retryable: false
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if ([RUN_STATE_COMPLETED, RUN_STATE_FAILED, RUN_STATE_CANCELED].includes(snapshot.state)) {
|
|
580
|
+
return {
|
|
581
|
+
status: "CANCEL_IGNORED",
|
|
582
|
+
run: snapshot,
|
|
583
|
+
message: "目标任务已结束,无需取消。"
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const activeRun = activeAsyncRuns.get(runId);
|
|
588
|
+
if (activeRun?.abortController) {
|
|
589
|
+
activeRun.abortController.abort();
|
|
590
|
+
}
|
|
591
|
+
safeUpdateRunState(runId, {
|
|
592
|
+
stage: snapshot.stage || RUN_STAGE_PREFLIGHT,
|
|
593
|
+
last_message: "已收到取消请求,正在停止任务。"
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const latest = readRunState(runId) || snapshot;
|
|
597
|
+
return {
|
|
598
|
+
status: "CANCEL_REQUESTED",
|
|
599
|
+
run: latest
|
|
75
600
|
};
|
|
76
601
|
}
|
|
77
602
|
|
|
@@ -108,64 +633,56 @@ async function handleRequest(message, workspaceRoot) {
|
|
|
108
633
|
jsonrpc: "2.0",
|
|
109
634
|
id,
|
|
110
635
|
result: {
|
|
111
|
-
tools:
|
|
636
|
+
tools: createToolsSchema()
|
|
112
637
|
}
|
|
113
638
|
};
|
|
114
639
|
}
|
|
115
640
|
|
|
116
641
|
if (method === "tools/call") {
|
|
117
|
-
|
|
118
|
-
|
|
642
|
+
const toolName = params?.name;
|
|
643
|
+
const args = params?.arguments || {};
|
|
644
|
+
|
|
645
|
+
if ([TOOL_RUN_PIPELINE, TOOL_START_RUN].includes(toolName)) {
|
|
646
|
+
const inputError = validateRunArgs(args);
|
|
647
|
+
if (inputError) {
|
|
648
|
+
return createJsonRpcError(id, -32602, inputError);
|
|
649
|
+
}
|
|
119
650
|
}
|
|
120
651
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
652
|
+
if ([TOOL_GET_RUN, TOOL_CANCEL_RUN].includes(toolName)) {
|
|
653
|
+
if (!args || typeof args.run_id !== "string" || !normalizeText(args.run_id)) {
|
|
654
|
+
return createJsonRpcError(id, -32602, "run_id is required and must be a string");
|
|
655
|
+
}
|
|
124
656
|
}
|
|
125
657
|
|
|
126
658
|
try {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
};
|
|
659
|
+
let payload;
|
|
660
|
+
if (toolName === TOOL_RUN_PIPELINE) {
|
|
661
|
+
const executionMode = normalizeExecutionMode(args.execution_mode);
|
|
662
|
+
payload = executionMode === RUN_MODE_SYNC
|
|
663
|
+
? await handleSyncRunTool({ workspaceRoot, args })
|
|
664
|
+
: await handleStartRunTool({ workspaceRoot, args });
|
|
665
|
+
} else if (toolName === TOOL_START_RUN) {
|
|
666
|
+
payload = await handleStartRunTool({ workspaceRoot, args });
|
|
667
|
+
} else if (toolName === TOOL_GET_RUN) {
|
|
668
|
+
payload = handleGetRunTool(args);
|
|
669
|
+
} else if (toolName === TOOL_CANCEL_RUN) {
|
|
670
|
+
payload = handleCancelRunTool(args);
|
|
671
|
+
} else {
|
|
672
|
+
return createJsonRpcError(id, -32602, `Unknown tool: ${toolName || ""}`);
|
|
673
|
+
}
|
|
674
|
+
const isError = payload?.status === "FAILED";
|
|
675
|
+
return createToolResultResponse(id, payload, isError);
|
|
146
676
|
} catch (error) {
|
|
147
677
|
const failed = {
|
|
148
678
|
status: "FAILED",
|
|
149
679
|
error: {
|
|
150
680
|
code: "UNEXPECTED_ERROR",
|
|
151
|
-
message: error
|
|
681
|
+
message: error?.message || "Unexpected error",
|
|
152
682
|
retryable: true
|
|
153
683
|
}
|
|
154
684
|
};
|
|
155
|
-
return
|
|
156
|
-
jsonrpc: "2.0",
|
|
157
|
-
id,
|
|
158
|
-
result: {
|
|
159
|
-
content: [
|
|
160
|
-
{
|
|
161
|
-
type: "text",
|
|
162
|
-
text: JSON.stringify(failed, null, 2)
|
|
163
|
-
}
|
|
164
|
-
],
|
|
165
|
-
structuredContent: failed,
|
|
166
|
-
isError: true
|
|
167
|
-
}
|
|
168
|
-
};
|
|
685
|
+
return createToolResultResponse(id, failed, true);
|
|
169
686
|
}
|
|
170
687
|
}
|
|
171
688
|
|
|
@@ -181,9 +698,11 @@ async function handleRequest(message, workspaceRoot) {
|
|
|
181
698
|
|
|
182
699
|
export function startServer() {
|
|
183
700
|
const envRoot = process.env.BOSS_WORKSPACE_ROOT;
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
701
|
+
const workspaceRoot = envRoot
|
|
702
|
+
? path.resolve(envRoot)
|
|
703
|
+
: process.env.INIT_CWD
|
|
704
|
+
? path.resolve(process.env.INIT_CWD)
|
|
705
|
+
: path.resolve(process.cwd());
|
|
187
706
|
let buffer = Buffer.alloc(0);
|
|
188
707
|
let framing = FRAMING_UNKNOWN;
|
|
189
708
|
|
|
@@ -274,7 +793,16 @@ export function startServer() {
|
|
|
274
793
|
});
|
|
275
794
|
}
|
|
276
795
|
|
|
796
|
+
export const __testables = {
|
|
797
|
+
handleRequest,
|
|
798
|
+
activeAsyncRuns,
|
|
799
|
+
setRunPipelineImplForTests(nextImpl) {
|
|
800
|
+
runPipelineImpl = typeof nextImpl === "function" ? nextImpl : runRecruitPipeline;
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
|
|
277
804
|
const thisFilePath = fileURLToPath(import.meta.url);
|
|
278
805
|
if (process.argv[1] && path.resolve(process.argv[1]) === thisFilePath) {
|
|
279
806
|
startServer();
|
|
280
807
|
}
|
|
808
|
+
|