@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/package.json +3 -2
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-recommend-pipeline/README.md +12 -12
- package/skills/boss-recommend-pipeline/SKILL.md +195 -195
- package/src/adapters.js +1876 -1806
- package/src/index.js +1254 -1254
- package/src/parser.js +19 -28
- package/src/pipeline.js +919 -792
- package/src/run-state.js +351 -351
- package/src/test-adapters-runtime.js +163 -163
- package/src/test-index-async.js +236 -236
- package/src/test-parser.js +55 -0
- package/src/test-pipeline.js +103 -0
- package/src/test-run-state.js +152 -152
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +111 -18
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +508 -452
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +245 -0
- package/vendor/boss-recommend-search-cli/src/cli.js +811 -811
- package/vendor/boss-recommend-search-cli/src/test-job-selection.js +201 -201
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
|
+
}
|