@reconcrap/boss-recommend-mcp 1.2.3 → 1.2.5
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
CHANGED
package/src/index.js
CHANGED
|
@@ -4,7 +4,11 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
getFeaturedCalibrationResolution,
|
|
9
|
+
runRecommendCalibration
|
|
10
|
+
} from "./adapters.js";
|
|
11
|
+
import { runRecommendPipeline } from "./pipeline.js";
|
|
8
12
|
import {
|
|
9
13
|
RUN_MODE_ASYNC,
|
|
10
14
|
RUN_STAGE_PREFLIGHT,
|
|
@@ -28,11 +32,13 @@ import {
|
|
|
28
32
|
const require = createRequire(import.meta.url);
|
|
29
33
|
const { version: SERVER_VERSION } = require("../package.json");
|
|
30
34
|
|
|
31
|
-
const TOOL_START_RUN = "start_recommend_pipeline_run";
|
|
32
|
-
const TOOL_GET_RUN = "get_recommend_pipeline_run";
|
|
33
|
-
const TOOL_CANCEL_RUN = "cancel_recommend_pipeline_run";
|
|
34
|
-
const TOOL_PAUSE_RUN = "pause_recommend_pipeline_run";
|
|
35
|
-
const TOOL_RESUME_RUN = "resume_recommend_pipeline_run";
|
|
35
|
+
const TOOL_START_RUN = "start_recommend_pipeline_run";
|
|
36
|
+
const TOOL_GET_RUN = "get_recommend_pipeline_run";
|
|
37
|
+
const TOOL_CANCEL_RUN = "cancel_recommend_pipeline_run";
|
|
38
|
+
const TOOL_PAUSE_RUN = "pause_recommend_pipeline_run";
|
|
39
|
+
const TOOL_RESUME_RUN = "resume_recommend_pipeline_run";
|
|
40
|
+
const TOOL_RUN_FEATURED_CALIBRATION = "run_featured_calibration";
|
|
41
|
+
const TOOL_GET_FEATURED_CALIBRATION_STATUS = "get_featured_calibration_status";
|
|
36
42
|
|
|
37
43
|
const SERVER_NAME = "boss-recommend-mcp";
|
|
38
44
|
const FRAMING_UNKNOWN = "unknown";
|
|
@@ -141,7 +147,7 @@ function createJsonRpcError(id, code, message) {
|
|
|
141
147
|
};
|
|
142
148
|
}
|
|
143
149
|
|
|
144
|
-
function createRunInputSchema() {
|
|
150
|
+
function createRunInputSchema() {
|
|
145
151
|
return {
|
|
146
152
|
type: "object",
|
|
147
153
|
properties: {
|
|
@@ -290,10 +296,33 @@ function createRunInputSchema() {
|
|
|
290
296
|
required: ["instruction"],
|
|
291
297
|
additionalProperties: false
|
|
292
298
|
};
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function
|
|
296
|
-
return
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function createRunFeaturedCalibrationInputSchema() {
|
|
302
|
+
return {
|
|
303
|
+
type: "object",
|
|
304
|
+
properties: {
|
|
305
|
+
port: {
|
|
306
|
+
type: "integer",
|
|
307
|
+
minimum: 1,
|
|
308
|
+
description: "可选,Boss Chrome 远程调试端口(默认读取配置或 9222)"
|
|
309
|
+
},
|
|
310
|
+
timeout_ms: {
|
|
311
|
+
type: "integer",
|
|
312
|
+
minimum: 1000,
|
|
313
|
+
description: "可选,等待收藏点击的超时时间(毫秒)"
|
|
314
|
+
},
|
|
315
|
+
output: {
|
|
316
|
+
type: "string",
|
|
317
|
+
description: "可选,校准文件输出路径(默认 favorite-calibration.json)"
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
additionalProperties: false
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function createToolsSchema() {
|
|
325
|
+
return [
|
|
297
326
|
{
|
|
298
327
|
name: TOOL_START_RUN,
|
|
299
328
|
description: "异步启动 Boss 推荐页流水线(含同步门禁预检);只有在前置确认与页面就绪通过后才返回 run_id。",
|
|
@@ -335,20 +364,34 @@ function createToolsSchema() {
|
|
|
335
364
|
additionalProperties: false
|
|
336
365
|
}
|
|
337
366
|
},
|
|
338
|
-
{
|
|
339
|
-
name: TOOL_RESUME_RUN,
|
|
340
|
-
description: "继续指定 run_id 的 paused 流水线;沿用原 CSV 与 checkpoint 续跑。",
|
|
341
|
-
inputSchema: {
|
|
367
|
+
{
|
|
368
|
+
name: TOOL_RESUME_RUN,
|
|
369
|
+
description: "继续指定 run_id 的 paused 流水线;沿用原 CSV 与 checkpoint 续跑。",
|
|
370
|
+
inputSchema: {
|
|
342
371
|
type: "object",
|
|
343
372
|
properties: {
|
|
344
373
|
run_id: { type: "string" }
|
|
345
374
|
},
|
|
346
|
-
required: ["run_id"],
|
|
347
|
-
additionalProperties: false
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
375
|
+
required: ["run_id"],
|
|
376
|
+
additionalProperties: false
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
name: TOOL_RUN_FEATURED_CALIBRATION,
|
|
381
|
+
description: "手动执行精选页收藏按钮校准。执行前请先在 Boss 推荐页切换到精选 tab 并打开任意候选人详情页。",
|
|
382
|
+
inputSchema: createRunFeaturedCalibrationInputSchema()
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: TOOL_GET_FEATURED_CALIBRATION_STATUS,
|
|
386
|
+
description: "查询精选页收藏校准文件与校准脚本可用性。",
|
|
387
|
+
inputSchema: {
|
|
388
|
+
type: "object",
|
|
389
|
+
properties: {},
|
|
390
|
+
additionalProperties: false
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
];
|
|
394
|
+
}
|
|
352
395
|
|
|
353
396
|
function createToolResultResponse(id, payload, isError = false) {
|
|
354
397
|
return {
|
|
@@ -367,7 +410,7 @@ function createToolResultResponse(id, payload, isError = false) {
|
|
|
367
410
|
};
|
|
368
411
|
}
|
|
369
412
|
|
|
370
|
-
function validateRunArgs(args) {
|
|
413
|
+
function validateRunArgs(args) {
|
|
371
414
|
if (!args || typeof args !== "object") {
|
|
372
415
|
return "arguments must be an object";
|
|
373
416
|
}
|
|
@@ -375,9 +418,37 @@ function validateRunArgs(args) {
|
|
|
375
418
|
return "instruction is required and must be a string";
|
|
376
419
|
}
|
|
377
420
|
return null;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function validateRunFeaturedCalibrationArgs(args) {
|
|
424
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) {
|
|
425
|
+
return "arguments must be an object";
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (Object.prototype.hasOwnProperty.call(args, "port")) {
|
|
429
|
+
const port = Number.parseInt(String(args.port), 10);
|
|
430
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
431
|
+
return "port must be a positive integer";
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (Object.prototype.hasOwnProperty.call(args, "timeout_ms")) {
|
|
436
|
+
const timeoutMs = Number.parseInt(String(args.timeout_ms), 10);
|
|
437
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 1000) {
|
|
438
|
+
return "timeout_ms must be an integer >= 1000";
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (Object.prototype.hasOwnProperty.call(args, "output")) {
|
|
443
|
+
if (typeof args.output !== "string" || !normalizeText(args.output)) {
|
|
444
|
+
return "output must be a non-empty string when provided";
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function getLastOutputLine(text) {
|
|
381
452
|
const lines = String(text || "")
|
|
382
453
|
.split(/\r?\n/)
|
|
383
454
|
.map((line) => normalizeText(line))
|
|
@@ -1219,8 +1290,57 @@ function handleResumeRunTool(args) {
|
|
|
1219
1290
|
message: "已恢复 Recommend 流水线(detached)。默认不自动轮询;如需进度请按需调用 get_recommend_pipeline_run。"
|
|
1220
1291
|
};
|
|
1221
1292
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1293
|
+
|
|
1294
|
+
function handleGetFeaturedCalibrationStatusTool(workspaceRoot) {
|
|
1295
|
+
const resolution = getFeaturedCalibrationResolution(workspaceRoot);
|
|
1296
|
+
return {
|
|
1297
|
+
status: "CALIBRATION_STATUS",
|
|
1298
|
+
ready: resolution.calibration_usable === true,
|
|
1299
|
+
calibration_path: resolution.calibration_path,
|
|
1300
|
+
calibration_exists: resolution.calibration_exists,
|
|
1301
|
+
calibration_usable: resolution.calibration_usable,
|
|
1302
|
+
calibration_script_path: resolution.calibration_script_path,
|
|
1303
|
+
message: resolution.calibration_usable
|
|
1304
|
+
? "精选页收藏校准文件可用。"
|
|
1305
|
+
: "精选页收藏校准文件不存在或无效。"
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
async function handleRunFeaturedCalibrationTool({ workspaceRoot, args }) {
|
|
1310
|
+
const result = await runRecommendCalibration(workspaceRoot, {
|
|
1311
|
+
port: args.port,
|
|
1312
|
+
timeoutMs: args.timeout_ms,
|
|
1313
|
+
output: args.output
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
if (!result?.ok) {
|
|
1317
|
+
return {
|
|
1318
|
+
status: "FAILED",
|
|
1319
|
+
error: {
|
|
1320
|
+
code: result?.error?.code || "CALIBRATION_REQUIRED",
|
|
1321
|
+
message: result?.error?.message || "精选页收藏校准失败,请在推荐页精选 tab 打开候选人详情后点击收藏按钮再重试。",
|
|
1322
|
+
retryable: true
|
|
1323
|
+
},
|
|
1324
|
+
calibration_path: result?.calibration_path || null,
|
|
1325
|
+
calibration_script_path: result?.calibration_script_path || null,
|
|
1326
|
+
debug_port: result?.debug_port || null,
|
|
1327
|
+
diagnostics: {
|
|
1328
|
+
stdout_last_line: getLastOutputLine(result?.stdout),
|
|
1329
|
+
stderr_last_line: getLastOutputLine(result?.stderr)
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
return {
|
|
1335
|
+
status: "CALIBRATED",
|
|
1336
|
+
message: "精选页收藏按钮校准完成,可重新执行 start_recommend_pipeline_run。",
|
|
1337
|
+
calibration_path: result.calibration_path,
|
|
1338
|
+
calibration_script_path: result.calibration_script_path,
|
|
1339
|
+
debug_port: result.debug_port
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
async function handleRequest(message, workspaceRoot) {
|
|
1224
1344
|
if (!message || message.jsonrpc !== "2.0") {
|
|
1225
1345
|
return createJsonRpcError(null, -32600, "Invalid JSON-RPC request");
|
|
1226
1346
|
}
|
|
@@ -1262,34 +1382,45 @@ async function handleRequest(message, workspaceRoot) {
|
|
|
1262
1382
|
const toolName = params?.name;
|
|
1263
1383
|
const args = params?.arguments || {};
|
|
1264
1384
|
|
|
1265
|
-
if (toolName === TOOL_START_RUN) {
|
|
1266
|
-
const inputError = validateRunArgs(args);
|
|
1267
|
-
if (inputError) {
|
|
1268
|
-
return createJsonRpcError(id, -32602, inputError);
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
if (
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1385
|
+
if (toolName === TOOL_START_RUN) {
|
|
1386
|
+
const inputError = validateRunArgs(args);
|
|
1387
|
+
if (inputError) {
|
|
1388
|
+
return createJsonRpcError(id, -32602, inputError);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
if (toolName === TOOL_RUN_FEATURED_CALIBRATION) {
|
|
1393
|
+
const inputError = validateRunFeaturedCalibrationArgs(args);
|
|
1394
|
+
if (inputError) {
|
|
1395
|
+
return createJsonRpcError(id, -32602, inputError);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
if ([TOOL_GET_RUN, TOOL_CANCEL_RUN, TOOL_PAUSE_RUN, TOOL_RESUME_RUN].includes(toolName)) {
|
|
1400
|
+
if (!args || typeof args.run_id !== "string" || !normalizeText(args.run_id)) {
|
|
1401
|
+
return createJsonRpcError(id, -32602, "run_id is required and must be a string");
|
|
1402
|
+
}
|
|
1276
1403
|
}
|
|
1277
1404
|
|
|
1278
1405
|
try {
|
|
1279
1406
|
let payload;
|
|
1280
|
-
if (toolName === TOOL_START_RUN) {
|
|
1281
|
-
payload = await handleStartRunTool({ workspaceRoot, args });
|
|
1282
|
-
} else if (toolName === TOOL_GET_RUN) {
|
|
1283
|
-
payload = handleGetRunTool(args);
|
|
1284
|
-
} else if (toolName === TOOL_CANCEL_RUN) {
|
|
1285
|
-
payload = handleCancelRunTool(args);
|
|
1286
|
-
} else if (toolName === TOOL_PAUSE_RUN) {
|
|
1287
|
-
payload = handlePauseRunTool(args);
|
|
1288
|
-
} else if (toolName === TOOL_RESUME_RUN) {
|
|
1289
|
-
payload = handleResumeRunTool(args);
|
|
1290
|
-
} else {
|
|
1291
|
-
|
|
1292
|
-
}
|
|
1407
|
+
if (toolName === TOOL_START_RUN) {
|
|
1408
|
+
payload = await handleStartRunTool({ workspaceRoot, args });
|
|
1409
|
+
} else if (toolName === TOOL_GET_RUN) {
|
|
1410
|
+
payload = handleGetRunTool(args);
|
|
1411
|
+
} else if (toolName === TOOL_CANCEL_RUN) {
|
|
1412
|
+
payload = handleCancelRunTool(args);
|
|
1413
|
+
} else if (toolName === TOOL_PAUSE_RUN) {
|
|
1414
|
+
payload = handlePauseRunTool(args);
|
|
1415
|
+
} else if (toolName === TOOL_RESUME_RUN) {
|
|
1416
|
+
payload = handleResumeRunTool(args);
|
|
1417
|
+
} else if (toolName === TOOL_GET_FEATURED_CALIBRATION_STATUS) {
|
|
1418
|
+
payload = handleGetFeaturedCalibrationStatusTool(workspaceRoot);
|
|
1419
|
+
} else if (toolName === TOOL_RUN_FEATURED_CALIBRATION) {
|
|
1420
|
+
payload = await handleRunFeaturedCalibrationTool({ workspaceRoot, args });
|
|
1421
|
+
} else {
|
|
1422
|
+
return createJsonRpcError(id, -32602, `Unknown tool: ${toolName || ""}`);
|
|
1423
|
+
}
|
|
1293
1424
|
const isError = payload?.status === "FAILED";
|
|
1294
1425
|
return createToolResultResponse(id, payload, isError);
|
|
1295
1426
|
} catch (error) {
|
package/src/pipeline.js
CHANGED
|
@@ -896,15 +896,6 @@ export async function runRecommendPipeline(
|
|
|
896
896
|
};
|
|
897
897
|
|
|
898
898
|
const ensureSelectedPageTab = async () => {
|
|
899
|
-
if (selectedPage === "recommend") {
|
|
900
|
-
activeTabStatus = selectedTabStatus;
|
|
901
|
-
return {
|
|
902
|
-
ok: true,
|
|
903
|
-
switched: false,
|
|
904
|
-
before_state: null,
|
|
905
|
-
after_state: null
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
899
|
const expectedStatus = selectedTabStatus;
|
|
909
900
|
let beforeState = null;
|
|
910
901
|
if (typeof readTabState === "function") {
|
|
@@ -1267,11 +1258,11 @@ export async function runRecommendPipeline(
|
|
|
1267
1258
|
);
|
|
1268
1259
|
runtimeHooks.heartbeat("screen_recovery", lastAutoRecovery);
|
|
1269
1260
|
} else {
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1261
|
+
const recoveryFailureText = "简历获取失败(network + 截图)";
|
|
1262
|
+
runtimeHooks.setStage(
|
|
1263
|
+
"screen_recovery",
|
|
1264
|
+
`screen 连续${recoveryFailureText},开始自动恢复(第 ${screenAutoRecoveryCount} 次):刷新 recommend 页面并重跑 search。`
|
|
1265
|
+
);
|
|
1275
1266
|
runtimeHooks.heartbeat("screen_recovery", lastAutoRecovery);
|
|
1276
1267
|
}
|
|
1277
1268
|
|
|
@@ -1388,11 +1379,9 @@ export async function runRecommendPipeline(
|
|
|
1388
1379
|
|| tabStatusToPageScope(resolvedActiveTabStatus)
|
|
1389
1380
|
) || selectedPage;
|
|
1390
1381
|
const resolvedResumeSourceRaw = normalizeText(screenSummary.resume_source || "").toLowerCase();
|
|
1391
|
-
const resolvedResumeSource =
|
|
1392
|
-
?
|
|
1393
|
-
:
|
|
1394
|
-
? "network"
|
|
1395
|
-
: "image_fallback";
|
|
1382
|
+
const resolvedResumeSource = ["network", "image_fallback"].includes(resolvedResumeSourceRaw)
|
|
1383
|
+
? resolvedResumeSourceRaw
|
|
1384
|
+
: "network";
|
|
1396
1385
|
runtimeHooks.progress("finalize", {
|
|
1397
1386
|
processed: screenSummary.processed_count ?? 0,
|
|
1398
1387
|
passed: screenSummary.passed_count ?? 0,
|
|
@@ -679,13 +679,44 @@ function parseGeekIdFromUrl(url) {
|
|
|
679
679
|
if (!raw) return null;
|
|
680
680
|
try {
|
|
681
681
|
const parsed = new URL(raw);
|
|
682
|
-
const keys = ["geekId", "geek_id", "gid", "encryptGeekId", "securityId"];
|
|
682
|
+
const keys = ["geekId", "geek_id", "gid", "encryptGeekId", "encryptJid", "jid", "securityId"];
|
|
683
683
|
for (const key of keys) {
|
|
684
684
|
const value = normalizeText(parsed.searchParams.get(key) || "");
|
|
685
685
|
if (value) return value;
|
|
686
686
|
}
|
|
687
687
|
} catch {}
|
|
688
|
-
const matched = raw.match(/[?&](?:geekId|geek_id|gid|encryptGeekId|securityId)=([^&]+)/i);
|
|
688
|
+
const matched = raw.match(/[?&](?:geekId|geek_id|gid|encryptGeekId|encryptJid|jid|securityId)=([^&]+)/i);
|
|
689
|
+
if (matched?.[1]) return decodeURIComponent(matched[1]);
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function parseGeekIdFromPostData(postData) {
|
|
694
|
+
const raw = normalizeText(postData);
|
|
695
|
+
if (!raw) return null;
|
|
696
|
+
const keys = ["geekId", "geek_id", "gid", "encryptGeekId", "encryptJid", "jid", "securityId"];
|
|
697
|
+
try {
|
|
698
|
+
const parsed = JSON.parse(raw);
|
|
699
|
+
if (parsed && typeof parsed === "object") {
|
|
700
|
+
const queue = [parsed];
|
|
701
|
+
let depth = 0;
|
|
702
|
+
while (queue.length > 0 && depth < 5) {
|
|
703
|
+
const current = queue.shift();
|
|
704
|
+
depth += 1;
|
|
705
|
+
if (!current || typeof current !== "object") continue;
|
|
706
|
+
for (const key of keys) {
|
|
707
|
+
const value = normalizeText(current[key] || "");
|
|
708
|
+
if (value) return value;
|
|
709
|
+
}
|
|
710
|
+
for (const value of Object.values(current)) {
|
|
711
|
+
if (value && typeof value === "object") {
|
|
712
|
+
queue.push(value);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
} catch {}
|
|
718
|
+
|
|
719
|
+
const matched = raw.match(/(?:^|[?&,\s"'])?(?:geekId|geek_id|gid|encryptGeekId|encryptJid|jid|securityId)(?:["']?\s*[:=]\s*["']?)([^&,"'\s}]+)/i);
|
|
689
720
|
if (matched?.[1]) return decodeURIComponent(matched[1]);
|
|
690
721
|
return null;
|
|
691
722
|
}
|
|
@@ -800,9 +831,22 @@ function isResumeInfoRequestUrl(url) {
|
|
|
800
831
|
const normalizedUrl = normalizeText(url).toLowerCase();
|
|
801
832
|
if (!normalizedUrl || !normalizedUrl.includes("/wapi/")) return false;
|
|
802
833
|
if (!normalizedUrl.includes("geek") || !normalizedUrl.includes("info")) return false;
|
|
834
|
+
if (/\/wapi\/zpjob\/view\/geek\/info\b/.test(normalizedUrl)) return true;
|
|
835
|
+
if (/\/wapi\/zpitem\/web\/boss\/[^?#]*\/geek\/info\b/.test(normalizedUrl)) return true;
|
|
803
836
|
if (/\/boss\/[^?#]*\/geek\/info\b/.test(normalizedUrl)) return true;
|
|
804
837
|
if (/\/geek\/info\b/.test(normalizedUrl)) return true;
|
|
805
|
-
return /[?&](?:geekid|geek_id|encryptgeekid|securityid)=/.test(normalizedUrl);
|
|
838
|
+
return /[?&](?:geekid|geek_id|encryptgeekid|encryptjid|jid|securityid)=/.test(normalizedUrl);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function isResumeRelatedWapiUrl(url) {
|
|
842
|
+
const normalizedUrl = normalizeText(url).toLowerCase();
|
|
843
|
+
if (!normalizedUrl || !normalizedUrl.includes("/wapi/")) return false;
|
|
844
|
+
return (
|
|
845
|
+
normalizedUrl.includes("geek")
|
|
846
|
+
|| normalizedUrl.includes("resume")
|
|
847
|
+
|| normalizedUrl.includes("candidate")
|
|
848
|
+
|| normalizedUrl.includes("friend")
|
|
849
|
+
);
|
|
806
850
|
}
|
|
807
851
|
|
|
808
852
|
function formatResumeApiData(data) {
|
|
@@ -2044,6 +2088,8 @@ class RecommendScreenCli {
|
|
|
2044
2088
|
this.resumeCaptureFailureStreakKeys = [];
|
|
2045
2089
|
this.currentCandidateKey = null;
|
|
2046
2090
|
this.resumeNetworkRequests = new Map();
|
|
2091
|
+
this.resumeNetworkRelatedRequests = new Map();
|
|
2092
|
+
this.resumeNetworkDiagnostics = [];
|
|
2047
2093
|
this.resumeNetworkByGeekId = new Map();
|
|
2048
2094
|
this.latestResumeNetworkPayload = null;
|
|
2049
2095
|
this.favoriteActionEvents = [];
|
|
@@ -2117,7 +2163,7 @@ class RecommendScreenCli {
|
|
|
2117
2163
|
}
|
|
2118
2164
|
|
|
2119
2165
|
buildProgressSnapshot(completionReason = null) {
|
|
2120
|
-
const defaultResumeSource =
|
|
2166
|
+
const defaultResumeSource = "network";
|
|
2121
2167
|
const snapshot = {
|
|
2122
2168
|
processed_count: this.processedCount,
|
|
2123
2169
|
passed_count: this.passedCandidates.length,
|
|
@@ -2185,6 +2231,89 @@ class RecommendScreenCli {
|
|
|
2185
2231
|
});
|
|
2186
2232
|
}
|
|
2187
2233
|
|
|
2234
|
+
recordResumeNetworkDiagnostic(entry) {
|
|
2235
|
+
const normalized = {
|
|
2236
|
+
ts: Number.isFinite(Number(entry?.ts)) ? Number(entry.ts) : Date.now(),
|
|
2237
|
+
kind: normalizeText(entry?.kind || "unknown") || "unknown",
|
|
2238
|
+
request_id: normalizeText(entry?.request_id || "") || null,
|
|
2239
|
+
method: normalizeText(entry?.method || "").toUpperCase() || null,
|
|
2240
|
+
url: normalizeText(entry?.url || "") || null,
|
|
2241
|
+
geek_id: normalizeText(entry?.geek_id || "") || null,
|
|
2242
|
+
match: normalizeText(entry?.match || "") || null,
|
|
2243
|
+
reason: normalizeText(entry?.reason || "") || null,
|
|
2244
|
+
error: normalizeText(entry?.error || "") || null,
|
|
2245
|
+
resume_text_len: Number.isFinite(Number(entry?.resume_text_len)) ? Number(entry.resume_text_len) : null,
|
|
2246
|
+
candidate_key: normalizeText(entry?.candidate_key || "") || null,
|
|
2247
|
+
source: normalizeText(entry?.source || "") || null,
|
|
2248
|
+
waited_ms: Number.isFinite(Number(entry?.waited_ms)) ? Number(entry.waited_ms) : null
|
|
2249
|
+
};
|
|
2250
|
+
this.resumeNetworkDiagnostics.push(normalized);
|
|
2251
|
+
if (this.resumeNetworkDiagnostics.length > 240) {
|
|
2252
|
+
this.resumeNetworkDiagnostics = this.resumeNetworkDiagnostics.slice(-240);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
summarizeResumeNetworkDiagnostics(since = 0) {
|
|
2257
|
+
const timestamp = Number.isFinite(since) ? since : 0;
|
|
2258
|
+
return this.resumeNetworkDiagnostics
|
|
2259
|
+
.filter((item) => Number(item?.ts || 0) >= timestamp)
|
|
2260
|
+
.slice(-20)
|
|
2261
|
+
.map((item) => {
|
|
2262
|
+
const prefix = `[${item.kind}]`;
|
|
2263
|
+
if (item.kind === "request") {
|
|
2264
|
+
return `${prefix} ${item.method || "GET"} ${item.url || ""} match=${item.match || "none"} geek=${item.geek_id || "-"}`;
|
|
2265
|
+
}
|
|
2266
|
+
if (item.kind === "response_hit") {
|
|
2267
|
+
return `${prefix} ${item.url || ""} geek=${item.geek_id || "-"} resume_len=${item.resume_text_len ?? "?"}`;
|
|
2268
|
+
}
|
|
2269
|
+
if (item.kind === "response_miss") {
|
|
2270
|
+
return `${prefix} ${item.url || ""} reason=${item.reason || "payload_not_found"}`;
|
|
2271
|
+
}
|
|
2272
|
+
if (item.kind === "response_error") {
|
|
2273
|
+
return `${prefix} ${item.url || ""} error=${item.error || "unknown"}`;
|
|
2274
|
+
}
|
|
2275
|
+
if (item.kind === "wait_hit") {
|
|
2276
|
+
return `${prefix} candidate=${item.candidate_key || "-"} source=${item.source || "-"} waited_ms=${item.waited_ms ?? "?"} resume_len=${item.resume_text_len ?? "?"}`;
|
|
2277
|
+
}
|
|
2278
|
+
if (item.kind === "wait_timeout") {
|
|
2279
|
+
return `${prefix} candidate=${item.candidate_key || "-"} waited_ms=${item.waited_ms ?? "?"} reason=${item.reason || "timeout"}`;
|
|
2280
|
+
}
|
|
2281
|
+
return `${prefix} ${item.url || item.reason || "n/a"}`;
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
logResumeNetworkMissDiagnostics(candidate, options = {}) {
|
|
2286
|
+
const candidateKey = normalizeText(candidate?.key || candidate?.geek_id || "");
|
|
2287
|
+
const candidateName = normalizeText(candidate?.name || "");
|
|
2288
|
+
const waitStartedAt = Number.isFinite(options.waitStartedAt) ? options.waitStartedAt : 0;
|
|
2289
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 0;
|
|
2290
|
+
const now = Date.now();
|
|
2291
|
+
const latestPayloadAgeMs = this.latestResumeNetworkPayload
|
|
2292
|
+
? Math.max(0, now - Number(this.latestResumeNetworkPayload.ts || 0))
|
|
2293
|
+
: null;
|
|
2294
|
+
const latestPayloadGeekIds = Array.isArray(this.latestResumeNetworkPayload?.geekIds)
|
|
2295
|
+
? this.latestResumeNetworkPayload.geekIds.slice(0, 4)
|
|
2296
|
+
: [];
|
|
2297
|
+
const recentLines = this.summarizeResumeNetworkDiagnostics(waitStartedAt);
|
|
2298
|
+
const trackedResumeRequestCount = this.resumeNetworkRequests.size;
|
|
2299
|
+
const trackedRelatedRequestCount = this.resumeNetworkRelatedRequests.size;
|
|
2300
|
+
|
|
2301
|
+
log(
|
|
2302
|
+
`[network简历未命中] candidate=${candidateKey || candidateName || "unknown"} `
|
|
2303
|
+
+ `wait_ms=${timeoutMs || "n/a"} `
|
|
2304
|
+
+ `tracked_resume_requests=${trackedResumeRequestCount} `
|
|
2305
|
+
+ `tracked_related_requests=${trackedRelatedRequestCount} `
|
|
2306
|
+
+ `cached_by_geek=${this.resumeNetworkByGeekId.size} `
|
|
2307
|
+
+ `latest_payload_age_ms=${latestPayloadAgeMs ?? "none"} `
|
|
2308
|
+
+ `latest_payload_geek_ids=${latestPayloadGeekIds.length ? latestPayloadGeekIds.join("|") : "none"}`
|
|
2309
|
+
);
|
|
2310
|
+
if (recentLines.length > 0) {
|
|
2311
|
+
log(`[network简历未命中][最近网络事件] ${recentLines.join(" || ")}`);
|
|
2312
|
+
} else {
|
|
2313
|
+
log("[network简历未命中][最近网络事件] none");
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2188
2317
|
cacheResumeNetworkPayload(payload, fallbackGeekId = null) {
|
|
2189
2318
|
if (!payload || typeof payload !== "object") return;
|
|
2190
2319
|
const geekDetail = payload.geekDetail || payload;
|
|
@@ -2232,35 +2361,81 @@ class RecommendScreenCli {
|
|
|
2232
2361
|
}
|
|
2233
2362
|
|
|
2234
2363
|
async waitForNetworkResumeCandidateInfo(candidate, timeoutMs = 2200) {
|
|
2364
|
+
const waitStartedAt = Date.now();
|
|
2365
|
+
const candidateKey = normalizeText(candidate?.key || candidate?.geek_id || "");
|
|
2235
2366
|
const deadline = Date.now() + timeoutMs;
|
|
2236
2367
|
while (Date.now() < deadline) {
|
|
2237
2368
|
const info = this.tryExtractNetworkResumeForCandidate(candidate);
|
|
2238
2369
|
if (info && normalizeText(info.resumeText)) {
|
|
2370
|
+
const source = candidateKey && this.resumeNetworkByGeekId.has(candidateKey)
|
|
2371
|
+
? "geek_id_map"
|
|
2372
|
+
: "latest_payload";
|
|
2373
|
+
this.recordResumeNetworkDiagnostic({
|
|
2374
|
+
kind: "wait_hit",
|
|
2375
|
+
candidate_key: candidateKey,
|
|
2376
|
+
source,
|
|
2377
|
+
waited_ms: Date.now() - waitStartedAt,
|
|
2378
|
+
resume_text_len: normalizeText(info.resumeText).length
|
|
2379
|
+
});
|
|
2239
2380
|
return info;
|
|
2240
2381
|
}
|
|
2241
2382
|
await sleep(120);
|
|
2242
2383
|
}
|
|
2384
|
+
this.recordResumeNetworkDiagnostic({
|
|
2385
|
+
kind: "wait_timeout",
|
|
2386
|
+
candidate_key: candidateKey,
|
|
2387
|
+
waited_ms: Date.now() - waitStartedAt,
|
|
2388
|
+
reason: "resume_text_not_ready"
|
|
2389
|
+
});
|
|
2243
2390
|
return null;
|
|
2244
2391
|
}
|
|
2245
2392
|
|
|
2246
2393
|
handleNetworkRequestWillBeSent(params) {
|
|
2247
2394
|
const url = normalizeText(params?.request?.url || "");
|
|
2395
|
+
const postData = params?.request?.postData || "";
|
|
2248
2396
|
if (!url) return;
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2397
|
+
const requestTs = Date.now();
|
|
2398
|
+
const method = normalizeText(params?.request?.method || "").toUpperCase() || "GET";
|
|
2399
|
+
const isResumeInfo = isResumeInfoRequestUrl(url);
|
|
2400
|
+
const isResumeRelated = isResumeInfo || isResumeRelatedWapiUrl(url);
|
|
2401
|
+
if (isResumeRelated) {
|
|
2402
|
+
const geekId = parseGeekIdFromUrl(url) || parseGeekIdFromPostData(postData);
|
|
2403
|
+
const meta = {
|
|
2404
|
+
ts: requestTs,
|
|
2253
2405
|
url,
|
|
2254
|
-
geekId
|
|
2406
|
+
geekId,
|
|
2407
|
+
method,
|
|
2408
|
+
isResumeInfo
|
|
2409
|
+
};
|
|
2410
|
+
this.resumeNetworkRelatedRequests.set(params.requestId, meta);
|
|
2411
|
+
this.recordResumeNetworkDiagnostic({
|
|
2412
|
+
kind: "request",
|
|
2413
|
+
request_id: params.requestId,
|
|
2414
|
+
method,
|
|
2415
|
+
url: url.slice(0, 280),
|
|
2416
|
+
geek_id: geekId,
|
|
2417
|
+
match: isResumeInfo ? "resume_info_url" : "wapi_related_non_resume_info"
|
|
2255
2418
|
});
|
|
2256
|
-
|
|
2419
|
+
if (this.resumeNetworkRelatedRequests.size > 400) {
|
|
2420
|
+
const oldest = [...this.resumeNetworkRelatedRequests.entries()]
|
|
2421
|
+
.sort((a, b) => Number(a[1]?.ts || 0) - Number(b[1]?.ts || 0))
|
|
2422
|
+
.slice(0, this.resumeNetworkRelatedRequests.size - 320);
|
|
2423
|
+
for (const [requestId] of oldest) {
|
|
2424
|
+
this.resumeNetworkRelatedRequests.delete(requestId);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
if (isResumeInfo) {
|
|
2428
|
+
this.resumeNetworkRequests.set(params.requestId, {
|
|
2429
|
+
ts: requestTs,
|
|
2430
|
+
url,
|
|
2431
|
+
geekId
|
|
2432
|
+
});
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2257
2435
|
}
|
|
2258
2436
|
|
|
2259
2437
|
if (this.favoriteClickPendingSince <= 0) return;
|
|
2260
|
-
const requestTs = Date.now();
|
|
2261
2438
|
if (requestTs < this.favoriteClickPendingSince) return;
|
|
2262
|
-
const method = normalizeText(params?.request?.method || "").toUpperCase() || "GET";
|
|
2263
|
-
const postData = params?.request?.postData || "";
|
|
2264
2439
|
this.recordFavoriteNetworkTrace({
|
|
2265
2440
|
ts: requestTs,
|
|
2266
2441
|
kind: "http",
|
|
@@ -2310,21 +2485,60 @@ class RecommendScreenCli {
|
|
|
2310
2485
|
}
|
|
2311
2486
|
|
|
2312
2487
|
async handleNetworkLoadingFinished(params) {
|
|
2313
|
-
const
|
|
2314
|
-
|
|
2315
|
-
this.
|
|
2488
|
+
const requestId = params?.requestId;
|
|
2489
|
+
const requestMeta = this.resumeNetworkRequests.get(requestId);
|
|
2490
|
+
const relatedMeta = this.resumeNetworkRelatedRequests.get(requestId);
|
|
2491
|
+
if (!requestMeta && !relatedMeta) return;
|
|
2492
|
+
this.resumeNetworkRequests.delete(requestId);
|
|
2493
|
+
this.resumeNetworkRelatedRequests.delete(requestId);
|
|
2494
|
+
const effectiveMeta = requestMeta || relatedMeta || {};
|
|
2495
|
+
const effectiveUrl = normalizeText(effectiveMeta.url || "");
|
|
2496
|
+
const effectiveGeekId = normalizeText(effectiveMeta.geekId || "");
|
|
2316
2497
|
try {
|
|
2317
|
-
const responseBody = await this.Network.getResponseBody({ requestId
|
|
2318
|
-
if (!responseBody?.body)
|
|
2498
|
+
const responseBody = await this.Network.getResponseBody({ requestId });
|
|
2499
|
+
if (!responseBody?.body) {
|
|
2500
|
+
this.recordResumeNetworkDiagnostic({
|
|
2501
|
+
kind: "response_miss",
|
|
2502
|
+
request_id: requestId,
|
|
2503
|
+
url: effectiveUrl.slice(0, 280),
|
|
2504
|
+
geek_id: effectiveGeekId,
|
|
2505
|
+
reason: "empty_body"
|
|
2506
|
+
});
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2319
2509
|
const rawBody = responseBody.base64Encoded
|
|
2320
2510
|
? Buffer.from(responseBody.body, "base64").toString("utf8")
|
|
2321
2511
|
: responseBody.body;
|
|
2322
2512
|
const parsed = JSON.parse(rawBody);
|
|
2323
2513
|
const resumePayload = extractResumePayloadFromResponseBody(parsed);
|
|
2324
2514
|
if (resumePayload) {
|
|
2325
|
-
this.cacheResumeNetworkPayload(resumePayload,
|
|
2515
|
+
this.cacheResumeNetworkPayload(resumePayload, effectiveGeekId);
|
|
2516
|
+
const formattedText = normalizeText(formatResumeApiData(resumePayload));
|
|
2517
|
+
this.recordResumeNetworkDiagnostic({
|
|
2518
|
+
kind: "response_hit",
|
|
2519
|
+
request_id: requestId,
|
|
2520
|
+
url: effectiveUrl.slice(0, 280),
|
|
2521
|
+
geek_id: effectiveGeekId,
|
|
2522
|
+
resume_text_len: formattedText.length
|
|
2523
|
+
});
|
|
2524
|
+
} else {
|
|
2525
|
+
this.recordResumeNetworkDiagnostic({
|
|
2526
|
+
kind: "response_miss",
|
|
2527
|
+
request_id: requestId,
|
|
2528
|
+
url: effectiveUrl.slice(0, 280),
|
|
2529
|
+
geek_id: effectiveGeekId,
|
|
2530
|
+
reason: "payload_not_found"
|
|
2531
|
+
});
|
|
2326
2532
|
}
|
|
2327
|
-
} catch {
|
|
2533
|
+
} catch (error) {
|
|
2534
|
+
this.recordResumeNetworkDiagnostic({
|
|
2535
|
+
kind: "response_error",
|
|
2536
|
+
request_id: requestId,
|
|
2537
|
+
url: effectiveUrl.slice(0, 280),
|
|
2538
|
+
geek_id: effectiveGeekId,
|
|
2539
|
+
error: normalizeText(error?.message || String(error)).slice(0, 240)
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2328
2542
|
}
|
|
2329
2543
|
|
|
2330
2544
|
resetResumeCaptureFailureStreak() {
|
|
@@ -2433,13 +2647,13 @@ class RecommendScreenCli {
|
|
|
2433
2647
|
const imageFallbackCount = this.passedCandidates.filter((item) => item?.resumeSource === "image_fallback").length;
|
|
2434
2648
|
this.resumeSourceStats = {
|
|
2435
2649
|
network: networkCount,
|
|
2436
|
-
image_fallback:
|
|
2650
|
+
image_fallback: imageFallbackCount
|
|
2437
2651
|
};
|
|
2438
2652
|
if (this.resumeSourceStats.network <= 0 && this.resumeSourceStats.image_fallback <= 0) {
|
|
2439
2653
|
const snapshotSource = normalizeText(parsed.resume_source || "").toLowerCase();
|
|
2440
2654
|
if (snapshotSource === "network") {
|
|
2441
2655
|
this.resumeSourceStats.network = 1;
|
|
2442
|
-
} else if (snapshotSource === "image_fallback"
|
|
2656
|
+
} else if (snapshotSource === "image_fallback") {
|
|
2443
2657
|
this.resumeSourceStats.image_fallback = 1;
|
|
2444
2658
|
}
|
|
2445
2659
|
}
|
|
@@ -3570,12 +3784,18 @@ class RecommendScreenCli {
|
|
|
3570
3784
|
throw this.buildError("DETAIL_OPEN_FAILED", "详情页打开超时");
|
|
3571
3785
|
}
|
|
3572
3786
|
|
|
3573
|
-
const isFeaturedScope = this.args.pageScope === "featured";
|
|
3574
|
-
const allowImageFallback = !isFeaturedScope;
|
|
3575
3787
|
let capture = null;
|
|
3576
3788
|
let screening = null;
|
|
3577
|
-
let resumeSource =
|
|
3578
|
-
const
|
|
3789
|
+
let resumeSource = "image_fallback";
|
|
3790
|
+
const networkWaitMs = 4200;
|
|
3791
|
+
const networkWaitStartedAt = Date.now();
|
|
3792
|
+
const networkCandidateInfo = await this.waitForNetworkResumeCandidateInfo(nextCandidate, networkWaitMs);
|
|
3793
|
+
if (!normalizeText(networkCandidateInfo?.resumeText) && typeof this.logResumeNetworkMissDiagnostics === "function") {
|
|
3794
|
+
this.logResumeNetworkMissDiagnostics(nextCandidate, {
|
|
3795
|
+
timeoutMs: networkWaitMs,
|
|
3796
|
+
waitStartedAt: networkWaitStartedAt
|
|
3797
|
+
});
|
|
3798
|
+
}
|
|
3579
3799
|
const candidateProfile = {
|
|
3580
3800
|
name: networkCandidateInfo?.name || nextCandidate.name || "",
|
|
3581
3801
|
school: networkCandidateInfo?.school || nextCandidate.school || "",
|
|
@@ -3584,17 +3804,7 @@ class RecommendScreenCli {
|
|
|
3584
3804
|
position: networkCandidateInfo?.position || nextCandidate.last_position || ""
|
|
3585
3805
|
};
|
|
3586
3806
|
|
|
3587
|
-
if (
|
|
3588
|
-
if (!networkCandidateInfo?.resumeText) {
|
|
3589
|
-
throw this.buildError(
|
|
3590
|
-
"RESUME_NETWORK_UNAVAILABLE",
|
|
3591
|
-
"精选页未获取到 network 简历数据,已跳过当前候选人。"
|
|
3592
|
-
);
|
|
3593
|
-
}
|
|
3594
|
-
screening = await this.callTextModel(networkCandidateInfo.resumeText);
|
|
3595
|
-
resumeSource = "network";
|
|
3596
|
-
this.resumeSourceStats.network += 1;
|
|
3597
|
-
} else if (networkCandidateInfo?.resumeText) {
|
|
3807
|
+
if (networkCandidateInfo?.resumeText) {
|
|
3598
3808
|
screening = await this.callTextModel(networkCandidateInfo.resumeText);
|
|
3599
3809
|
resumeSource = "network";
|
|
3600
3810
|
this.resumeSourceStats.network += 1;
|
|
@@ -3667,7 +3877,9 @@ class RecommendScreenCli {
|
|
|
3667
3877
|
log(`候选人处理失败: ${error.code || error.message}`);
|
|
3668
3878
|
if (["RESUME_CAPTURE_FAILED", "RESUME_NETWORK_UNAVAILABLE"].includes(error.code)) {
|
|
3669
3879
|
this.recordResumeCaptureFailure(nextCandidate.key);
|
|
3670
|
-
const failureLabel = error.code === "RESUME_NETWORK_UNAVAILABLE"
|
|
3880
|
+
const failureLabel = error.code === "RESUME_NETWORK_UNAVAILABLE"
|
|
3881
|
+
? "简历 network 获取失败且截图回退未完成"
|
|
3882
|
+
: "简历截图失败";
|
|
3671
3883
|
log(
|
|
3672
3884
|
`[候选人跳过] ${nextCandidate.name || nextCandidate.geek_id || "unknown"} ${failureLabel},` +
|
|
3673
3885
|
`已跳过当前候选人;连续失败 ${this.consecutiveResumeCaptureFailures}/${MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES}`
|
|
@@ -3675,7 +3887,7 @@ class RecommendScreenCli {
|
|
|
3675
3887
|
if (this.consecutiveResumeCaptureFailures >= MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES) {
|
|
3676
3888
|
shouldMarkProcessed = false;
|
|
3677
3889
|
const rollback = this.rollbackResumeCaptureFailureStreak(nextCandidate.key);
|
|
3678
|
-
const failureTypeText =
|
|
3890
|
+
const failureTypeText = "简历获取失败(network + 截图)";
|
|
3679
3891
|
throw this.buildError(
|
|
3680
3892
|
"RESUME_CAPTURE_FAILED_CONSECUTIVE_LIMIT",
|
|
3681
3893
|
`连续 ${MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES} 位候选人${failureTypeText},已停止运行以避免错误跳过。` +
|
|
@@ -225,7 +225,7 @@ async function testConsecutiveResumeCaptureFailuresStillAbort() {
|
|
|
225
225
|
() => cli.run(),
|
|
226
226
|
(error) => {
|
|
227
227
|
assert.equal(error.code, "RESUME_CAPTURE_FAILED_CONSECUTIVE_LIMIT");
|
|
228
|
-
assert.match(error.message, /连续 .*
|
|
228
|
+
assert.match(error.message, /连续 .* 位候选人简历(?:捕获失败|获取失败(network \+ 截图))/);
|
|
229
229
|
assert.equal(error.rollback?.rollback_count, maxFailures);
|
|
230
230
|
assert.equal(error.partial_result?.processed_count, 0);
|
|
231
231
|
assert.equal(error.partial_result?.skipped_count, 0);
|