@reconcrap/boss-recruit-mcp 1.0.20 → 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 -5
- package/src/adapters.js +204 -7
- package/src/cli.js +33 -8
- package/src/index.js +608 -80
- package/src/parser.js +4 -40
- package/src/pipeline.js +230 -11
- package/src/run-state.js +294 -0
- package/src/test-index-async.js +231 -0
- package/src/test-parser.js +0 -35
- package/src/test-run-state.js +115 -0
package/src/parser.js
CHANGED
|
@@ -31,8 +31,6 @@ const DEFAULT_PARAM_LABELS = {
|
|
|
31
31
|
};
|
|
32
32
|
const DEGREE_VALUES = new Set(["不限", "本科", "本科及以上", "硕士及以上", "博士"]);
|
|
33
33
|
const CITY_STOP_PATTERN = /(?:筛选|搜索|查找|找|做过|从事过|有过|相关|的人选|的人|并且|且|学历|学校|目标|必须|优先|,|。|;|;|,)/;
|
|
34
|
-
const SEARCH_INTENT_LEADING_PATTERN = /^(?:帮我|请)?(?:在boss上|在boss直聘上)?(?:找|筛选|搜索|查找)/i;
|
|
35
|
-
const SEARCH_INTENT_META_PATTERN = /(?:城市|地点|学历|学校|目标|人数|候选人|人选|岗位|职位|做过|有过|从事过|本科|硕士|博士)/i;
|
|
36
34
|
|
|
37
35
|
function normalizeText(input) {
|
|
38
36
|
return String(input || "").replace(/\s+/g, " ").trim();
|
|
@@ -93,19 +91,6 @@ function extractCity(text) {
|
|
|
93
91
|
}
|
|
94
92
|
}
|
|
95
93
|
|
|
96
|
-
const implicitPatterns = [
|
|
97
|
-
/(?:^|[,,。;;\n])(?:帮我|请)?(?:在boss上|在boss直聘上)?(?:找|筛选|搜索|查找)\s*([^\s,。;;、]{2,12}?)(?=(?:本科|硕士|博士|做过|有过|从事过|的人选|的人|候选人|工程师|研究员|岗位|职位|,|。|;|;|,|$))/i,
|
|
98
|
-
/(?:^|[,,。;;\n])在\s*([^\s,。;;、]{2,12}?)(?=(?:招|招聘|找|筛选|搜索|查找|本科|硕士|博士|做过|有过|从事过|的人选|的人|候选人|工程师|研究员|岗位|职位|,|。|;|;|,|$))/i
|
|
99
|
-
];
|
|
100
|
-
|
|
101
|
-
for (const pattern of implicitPatterns) {
|
|
102
|
-
const m = text.match(pattern);
|
|
103
|
-
if (m && m[1]) {
|
|
104
|
-
const city = sanitizeCityCandidate(m[1]);
|
|
105
|
-
if (city) return city;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
94
|
return null;
|
|
110
95
|
}
|
|
111
96
|
|
|
@@ -198,9 +183,9 @@ function extractKeywordExplicit(text) {
|
|
|
198
183
|
|
|
199
184
|
function extractKeywordAuto(text) {
|
|
200
185
|
const patterns = [
|
|
201
|
-
|
|
202
|
-
/有过\s*([
|
|
203
|
-
/从事过\s*([
|
|
186
|
+
/做过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:的人选|的人|相关|并且|且|,|。|,|$)/i,
|
|
187
|
+
/有过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:经验|背景|的人选|并且|且|,|。|,|$)/i,
|
|
188
|
+
/从事过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:相关|的人选|并且|且|,|。|,|$)/i
|
|
204
189
|
];
|
|
205
190
|
|
|
206
191
|
for (const pattern of patterns) {
|
|
@@ -231,21 +216,6 @@ function extractTargetCount(text) {
|
|
|
231
216
|
return null;
|
|
232
217
|
}
|
|
233
218
|
|
|
234
|
-
function extractExplicitCriteria(text) {
|
|
235
|
-
const patterns = [
|
|
236
|
-
/(?:筛选条件|筛选要求|硬性条件|筛选标准|要求)(?:为|是|:|:)\s*([^\n]+)/i
|
|
237
|
-
];
|
|
238
|
-
|
|
239
|
-
for (const pattern of patterns) {
|
|
240
|
-
const m = text.match(pattern);
|
|
241
|
-
if (m && m[1]) {
|
|
242
|
-
const criteria = m[1].trim();
|
|
243
|
-
if (criteria) return criteria;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
219
|
function sanitizeClause(clause) {
|
|
250
220
|
return clause
|
|
251
221
|
.replace(/^使用boss-recruit-pipeline skills/i, "")
|
|
@@ -262,11 +232,6 @@ function isCountPlanningClause(clause) {
|
|
|
262
232
|
}
|
|
263
233
|
|
|
264
234
|
function buildScreenCriteria(text, searchParams) {
|
|
265
|
-
const explicitCriteria = extractExplicitCriteria(text);
|
|
266
|
-
if (explicitCriteria) {
|
|
267
|
-
return explicitCriteria;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
235
|
const clauses = text
|
|
271
236
|
.split(/[,,。;;\n]/)
|
|
272
237
|
.map((s) => sanitizeClause(s))
|
|
@@ -277,7 +242,6 @@ function buildScreenCriteria(text, searchParams) {
|
|
|
277
242
|
if (/地点|城市/.test(clause)) return false;
|
|
278
243
|
if (/近?14天(?:内)?查看(?:过)?|过滤近14天查看/.test(clause)) return false;
|
|
279
244
|
if (isCountPlanningClause(clause)) return false;
|
|
280
|
-
if (SEARCH_INTENT_LEADING_PATTERN.test(clause) && SEARCH_INTENT_META_PATTERN.test(clause)) return false;
|
|
281
245
|
return true;
|
|
282
246
|
});
|
|
283
247
|
|
|
@@ -479,7 +443,7 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
|
|
|
479
443
|
if (!baseSearchParams.city) missingBeforeDefaults.push("city");
|
|
480
444
|
if (!baseSearchParams.degree) missingBeforeDefaults.push("degree");
|
|
481
445
|
if (!baseSearchParams.schools || baseSearchParams.schools.length === 0) missingBeforeDefaults.push("schools");
|
|
482
|
-
if (!baseSearchParams.keyword
|
|
446
|
+
if (!baseSearchParams.keyword) missingBeforeDefaults.push("keyword");
|
|
483
447
|
if (!baseScreenParams.target_count) missingBeforeDefaults.push("target_count");
|
|
484
448
|
|
|
485
449
|
const useDefaultForMissing = confirmation?.use_default_for_missing === true;
|
package/src/pipeline.js
CHANGED
|
@@ -8,10 +8,16 @@ import {
|
|
|
8
8
|
runScreenCli
|
|
9
9
|
} from "./adapters.js";
|
|
10
10
|
|
|
11
|
+
export const PIPELINE_STATUS_READY_TO_START_ASYNC = "READY_TO_START_ASYNC";
|
|
12
|
+
|
|
11
13
|
function dedupe(values = []) {
|
|
12
14
|
return [...new Set(values.filter(Boolean))];
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
function normalizeText(value) {
|
|
18
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
function failedCheckSet(checks = []) {
|
|
16
22
|
const failed = checks
|
|
17
23
|
.filter((item) => item && item.ok === false && typeof item.key === "string")
|
|
@@ -195,6 +201,99 @@ function buildFailedResponse(code, message, extra = {}) {
|
|
|
195
201
|
};
|
|
196
202
|
}
|
|
197
203
|
|
|
204
|
+
class PipelineAbortError extends Error {
|
|
205
|
+
constructor(message = "Pipeline execution aborted") {
|
|
206
|
+
super(message);
|
|
207
|
+
this.name = "PipelineAbortError";
|
|
208
|
+
this.code = "PIPELINE_ABORTED";
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isAbortSignalTriggered(signal) {
|
|
213
|
+
return Boolean(signal && signal.aborted);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function ensurePipelineNotAborted(signal) {
|
|
217
|
+
if (isAbortSignalTriggered(signal)) {
|
|
218
|
+
throw new PipelineAbortError("Pipeline execution aborted by caller.");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function safeInvokeRuntimeCallback(callback, payload) {
|
|
223
|
+
if (typeof callback !== "function") return;
|
|
224
|
+
try {
|
|
225
|
+
callback(payload);
|
|
226
|
+
} catch {
|
|
227
|
+
// Keep pipeline stable even if runtime callback fails.
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function createPipelineRuntime(runtime = null) {
|
|
232
|
+
const signal = runtime?.signal;
|
|
233
|
+
const heartbeatIntervalMs = Number.isFinite(runtime?.heartbeatIntervalMs) && runtime.heartbeatIntervalMs > 0
|
|
234
|
+
? runtime.heartbeatIntervalMs
|
|
235
|
+
: 10_000;
|
|
236
|
+
const precheckOnly = runtime?.precheckOnly === true;
|
|
237
|
+
|
|
238
|
+
function setStage(stage, message = null) {
|
|
239
|
+
safeInvokeRuntimeCallback(runtime?.onStage, {
|
|
240
|
+
stage,
|
|
241
|
+
message: normalizeText(message || "") || null,
|
|
242
|
+
at: new Date().toISOString()
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function heartbeat(stage, details = null) {
|
|
247
|
+
safeInvokeRuntimeCallback(runtime?.onHeartbeat, {
|
|
248
|
+
stage,
|
|
249
|
+
details: details || null,
|
|
250
|
+
at: new Date().toISOString()
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function output(stage, event) {
|
|
255
|
+
safeInvokeRuntimeCallback(runtime?.onOutput, {
|
|
256
|
+
stage,
|
|
257
|
+
...(event || {}),
|
|
258
|
+
at: new Date().toISOString()
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function progress(stage, payload) {
|
|
263
|
+
safeInvokeRuntimeCallback(runtime?.onProgress, {
|
|
264
|
+
stage,
|
|
265
|
+
...(payload || {}),
|
|
266
|
+
at: new Date().toISOString()
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function adapterRuntime(stage) {
|
|
271
|
+
return {
|
|
272
|
+
signal,
|
|
273
|
+
heartbeatIntervalMs,
|
|
274
|
+
onOutput: (event) => output(stage, event),
|
|
275
|
+
onHeartbeat: (event) => heartbeat(stage, event),
|
|
276
|
+
onProgress: (payload) => progress(stage, payload)
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
signal,
|
|
282
|
+
heartbeatIntervalMs,
|
|
283
|
+
precheckOnly,
|
|
284
|
+
setStage,
|
|
285
|
+
heartbeat,
|
|
286
|
+
output,
|
|
287
|
+
progress,
|
|
288
|
+
adapterRuntime
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function isProcessAbortError(errorLike) {
|
|
293
|
+
const code = normalizeText(errorLike?.code || errorLike?.error_code || "").toUpperCase();
|
|
294
|
+
return code === "PROCESS_ABORTED" || code === "ABORTED";
|
|
295
|
+
}
|
|
296
|
+
|
|
198
297
|
function normalizeCsvPath(csvPath) {
|
|
199
298
|
if (typeof csvPath !== "string") return null;
|
|
200
299
|
const trimmed = csvPath.trim();
|
|
@@ -365,12 +464,16 @@ const defaultDependencies = {
|
|
|
365
464
|
runScreenCli
|
|
366
465
|
};
|
|
367
466
|
|
|
368
|
-
export async function runRecruitPipeline(
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
467
|
+
export async function runRecruitPipeline(
|
|
468
|
+
{
|
|
469
|
+
workspaceRoot,
|
|
470
|
+
instruction,
|
|
471
|
+
confirmation,
|
|
472
|
+
overrides
|
|
473
|
+
},
|
|
474
|
+
dependencies = defaultDependencies,
|
|
475
|
+
runtime = null
|
|
476
|
+
) {
|
|
374
477
|
const {
|
|
375
478
|
parseRecruitInstruction: parseInstruction,
|
|
376
479
|
ensureBossSearchPageReady: ensureSearchPageReady,
|
|
@@ -378,6 +481,8 @@ export async function runRecruitPipeline({
|
|
|
378
481
|
runSearchCli: searchCli,
|
|
379
482
|
runScreenCli: screenCli
|
|
380
483
|
} = dependencies;
|
|
484
|
+
const runtimeHooks = createPipelineRuntime(runtime);
|
|
485
|
+
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
381
486
|
const startedAt = Date.now();
|
|
382
487
|
const parsed = parseInstruction({
|
|
383
488
|
instruction,
|
|
@@ -398,8 +503,14 @@ export async function runRecruitPipeline({
|
|
|
398
503
|
return buildNeedConfirmationResponse(parsed);
|
|
399
504
|
}
|
|
400
505
|
|
|
506
|
+
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
507
|
+
runtimeHooks.setStage("preflight", "开始执行 preflight 检查。");
|
|
508
|
+
runtimeHooks.heartbeat("preflight");
|
|
401
509
|
const preflight = runPreflight(workspaceRoot);
|
|
402
510
|
if (!preflight.ok) {
|
|
511
|
+
runtimeHooks.heartbeat("preflight", {
|
|
512
|
+
status: "failed"
|
|
513
|
+
});
|
|
403
514
|
const recovery = buildPreflightRecovery(preflight.checks, workspaceRoot);
|
|
404
515
|
return buildFailedResponse(
|
|
405
516
|
"PIPELINE_PREFLIGHT_FAILED",
|
|
@@ -429,12 +540,70 @@ export async function runRecruitPipeline({
|
|
|
429
540
|
);
|
|
430
541
|
}
|
|
431
542
|
|
|
543
|
+
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
544
|
+
runtimeHooks.setStage("page_ready", "preflight 完成,开始检查 search 页面就绪状态。");
|
|
545
|
+
runtimeHooks.heartbeat("page_ready");
|
|
546
|
+
const initialPageCheck = await ensureSearchPageReady(workspaceRoot, {
|
|
547
|
+
port: preflight.debug_port
|
|
548
|
+
});
|
|
549
|
+
if (!initialPageCheck.ok) {
|
|
550
|
+
if (
|
|
551
|
+
initialPageCheck.state === "LOGIN_REQUIRED"
|
|
552
|
+
|| initialPageCheck.state === "LOGIN_REQUIRED_AFTER_REDIRECT"
|
|
553
|
+
) {
|
|
554
|
+
return buildFailedResponse(
|
|
555
|
+
"BOSS_LOGIN_REQUIRED",
|
|
556
|
+
"Boss 页面未稳定停留在 search 页面,疑似未登录或登录态失效。请先在当前 Chrome 窗口手动登录 Boss,登录完成后再继续搜索和筛选。",
|
|
557
|
+
{
|
|
558
|
+
search_params: parsed.searchParams,
|
|
559
|
+
screen_params: parsed.screenParams,
|
|
560
|
+
diagnostics: buildProgressDiagnostics({
|
|
561
|
+
preflight,
|
|
562
|
+
totalProcessedCount: 0,
|
|
563
|
+
totalPassedCount: 0,
|
|
564
|
+
roundCount: 0,
|
|
565
|
+
extra: {
|
|
566
|
+
page_state: initialPageCheck.page_state
|
|
567
|
+
}
|
|
568
|
+
})
|
|
569
|
+
}
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
return buildFailedResponse(
|
|
573
|
+
"BOSS_SEARCH_PAGE_NOT_READY",
|
|
574
|
+
"无法确认 Boss search 页面已就绪。请先确保 Chrome 调试端口可连,并且页面能稳定停留在 https://www.zhipin.com/web/chat/search。",
|
|
575
|
+
{
|
|
576
|
+
search_params: parsed.searchParams,
|
|
577
|
+
screen_params: parsed.screenParams,
|
|
578
|
+
diagnostics: buildProgressDiagnostics({
|
|
579
|
+
preflight,
|
|
580
|
+
totalProcessedCount: 0,
|
|
581
|
+
totalPassedCount: 0,
|
|
582
|
+
roundCount: 0,
|
|
583
|
+
extra: {
|
|
584
|
+
page_state: initialPageCheck.page_state
|
|
585
|
+
}
|
|
586
|
+
})
|
|
587
|
+
}
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (runtimeHooks.precheckOnly) {
|
|
592
|
+
return {
|
|
593
|
+
status: PIPELINE_STATUS_READY_TO_START_ASYNC,
|
|
594
|
+
search_params: parsed.searchParams,
|
|
595
|
+
screen_params: parsed.screenParams,
|
|
596
|
+
message: "前置门禁检查通过,可启动异步流水线。"
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
432
600
|
let totalProcessedCount = 0;
|
|
433
601
|
let totalPassedCount = 0;
|
|
434
602
|
let roundCount = 0;
|
|
435
603
|
const roundOutputCsvPaths = [];
|
|
436
604
|
|
|
437
605
|
while (totalProcessedCount < initialTargetCount) {
|
|
606
|
+
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
438
607
|
roundCount += 1;
|
|
439
608
|
|
|
440
609
|
const remainingTargetCount = Math.max(0, initialTargetCount - totalProcessedCount);
|
|
@@ -447,9 +616,17 @@ export async function runRecruitPipeline({
|
|
|
447
616
|
target_count: remainingTargetCount
|
|
448
617
|
};
|
|
449
618
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
619
|
+
if (roundCount > 1) {
|
|
620
|
+
runtimeHooks.setStage("page_ready", `第 ${roundCount} 轮:检查 search 页面就绪状态。`);
|
|
621
|
+
runtimeHooks.heartbeat("page_ready", {
|
|
622
|
+
round: roundCount
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
const pageCheck = roundCount === 1
|
|
626
|
+
? initialPageCheck
|
|
627
|
+
: await ensureSearchPageReady(workspaceRoot, {
|
|
628
|
+
port: preflight.debug_port
|
|
629
|
+
});
|
|
453
630
|
if (!pageCheck.ok) {
|
|
454
631
|
if (
|
|
455
632
|
pageCheck.state === "LOGIN_REQUIRED"
|
|
@@ -493,10 +670,20 @@ export async function runRecruitPipeline({
|
|
|
493
670
|
);
|
|
494
671
|
}
|
|
495
672
|
|
|
673
|
+
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
674
|
+
runtimeHooks.setStage("search", `第 ${roundCount} 轮:开始执行 search。`);
|
|
675
|
+
runtimeHooks.heartbeat("search", {
|
|
676
|
+
round: roundCount
|
|
677
|
+
});
|
|
496
678
|
const searchResult = await searchCli({
|
|
497
679
|
workspaceRoot,
|
|
498
|
-
searchParams: roundSearchParams
|
|
680
|
+
searchParams: roundSearchParams,
|
|
681
|
+
runtime: runtimeHooks.adapterRuntime("search")
|
|
499
682
|
});
|
|
683
|
+
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
684
|
+
if (isProcessAbortError(searchResult)) {
|
|
685
|
+
throw new PipelineAbortError("搜索流程已取消。");
|
|
686
|
+
}
|
|
500
687
|
|
|
501
688
|
if (!searchResult.ok) {
|
|
502
689
|
const failure = classifySearchFailure(searchResult);
|
|
@@ -545,6 +732,14 @@ export async function runRecruitPipeline({
|
|
|
545
732
|
|
|
546
733
|
const exhaustedByTipNoData = searchResult.no_data_tip_present === true;
|
|
547
734
|
if (exhaustedByTipNoData || searchResult.candidate_count === 0) {
|
|
735
|
+
runtimeHooks.setStage("finalize", "候选池已耗尽,正在汇总结果。");
|
|
736
|
+
runtimeHooks.heartbeat("finalize");
|
|
737
|
+
runtimeHooks.progress("finalize", {
|
|
738
|
+
processed: totalProcessedCount,
|
|
739
|
+
passed: totalPassedCount,
|
|
740
|
+
skipped: Math.max(totalProcessedCount - totalPassedCount, 0),
|
|
741
|
+
greet_count: 0
|
|
742
|
+
});
|
|
548
743
|
const durationSec = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
|
|
549
744
|
const mergedCsvPath = mergeRoundCsvFiles(roundOutputCsvPaths);
|
|
550
745
|
return {
|
|
@@ -568,10 +763,20 @@ export async function runRecruitPipeline({
|
|
|
568
763
|
};
|
|
569
764
|
}
|
|
570
765
|
|
|
766
|
+
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
767
|
+
runtimeHooks.setStage("screen", `第 ${roundCount} 轮:开始执行 screen。`);
|
|
768
|
+
runtimeHooks.heartbeat("screen", {
|
|
769
|
+
round: roundCount
|
|
770
|
+
});
|
|
571
771
|
const screenResult = await screenCli({
|
|
572
772
|
workspaceRoot,
|
|
573
|
-
screenParams: roundScreenParams
|
|
773
|
+
screenParams: roundScreenParams,
|
|
774
|
+
runtime: runtimeHooks.adapterRuntime("screen")
|
|
574
775
|
});
|
|
776
|
+
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
777
|
+
if (isProcessAbortError(screenResult)) {
|
|
778
|
+
throw new PipelineAbortError("筛选流程已取消。");
|
|
779
|
+
}
|
|
575
780
|
|
|
576
781
|
if (!screenResult.ok) {
|
|
577
782
|
const failure = classifyScreenFailure(screenResult);
|
|
@@ -634,10 +839,24 @@ export async function runRecruitPipeline({
|
|
|
634
839
|
: 0;
|
|
635
840
|
totalProcessedCount += roundProcessedCount;
|
|
636
841
|
totalPassedCount += roundPassedCount;
|
|
842
|
+
runtimeHooks.progress("screen", {
|
|
843
|
+
processed: totalProcessedCount,
|
|
844
|
+
passed: totalPassedCount,
|
|
845
|
+
skipped: Math.max(totalProcessedCount - totalPassedCount, 0),
|
|
846
|
+
greet_count: 0
|
|
847
|
+
});
|
|
637
848
|
}
|
|
638
849
|
|
|
850
|
+
runtimeHooks.setStage("finalize", "筛选完成,正在汇总结果。");
|
|
851
|
+
runtimeHooks.heartbeat("finalize");
|
|
639
852
|
const durationSec = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
|
|
640
853
|
const mergedCsvPath = mergeRoundCsvFiles(roundOutputCsvPaths);
|
|
854
|
+
runtimeHooks.progress("finalize", {
|
|
855
|
+
processed: totalProcessedCount,
|
|
856
|
+
passed: totalPassedCount,
|
|
857
|
+
skipped: Math.max(totalProcessedCount - totalPassedCount, 0),
|
|
858
|
+
greet_count: 0
|
|
859
|
+
});
|
|
641
860
|
return {
|
|
642
861
|
status: "COMPLETED",
|
|
643
862
|
search_params: parsed.searchParams,
|