@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/test-run-state.js
CHANGED
|
@@ -1,152 +1,152 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
RUN_MODE_ASYNC,
|
|
7
|
-
RUN_STATE_PAUSED,
|
|
8
|
-
RUN_STAGE_SCREEN,
|
|
9
|
-
RUN_STATE_COMPLETED,
|
|
10
|
-
RUN_STATE_QUEUED,
|
|
11
|
-
RUN_STATE_RUNNING,
|
|
12
|
-
cleanupExpiredRuns,
|
|
13
|
-
createRunId,
|
|
14
|
-
createRunStateSnapshot,
|
|
15
|
-
getRunsDir,
|
|
16
|
-
readRunState,
|
|
17
|
-
touchRunHeartbeat,
|
|
18
|
-
updateRunProgress,
|
|
19
|
-
updateRunState,
|
|
20
|
-
writeRunState
|
|
21
|
-
} from "./run-state.js";
|
|
22
|
-
|
|
23
|
-
function withTempHome(testFn) {
|
|
24
|
-
const previous = process.env.BOSS_RECOMMEND_HOME;
|
|
25
|
-
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-run-state-"));
|
|
26
|
-
process.env.BOSS_RECOMMEND_HOME = tempHome;
|
|
27
|
-
try {
|
|
28
|
-
testFn(tempHome);
|
|
29
|
-
} finally {
|
|
30
|
-
if (previous === undefined) {
|
|
31
|
-
delete process.env.BOSS_RECOMMEND_HOME;
|
|
32
|
-
} else {
|
|
33
|
-
process.env.BOSS_RECOMMEND_HOME = previous;
|
|
34
|
-
}
|
|
35
|
-
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function testRunStateLifecycle() {
|
|
40
|
-
withTempHome(() => {
|
|
41
|
-
const runId = createRunId();
|
|
42
|
-
const queued = writeRunState(createRunStateSnapshot({
|
|
43
|
-
runId,
|
|
44
|
-
mode: RUN_MODE_ASYNC,
|
|
45
|
-
state: RUN_STATE_QUEUED,
|
|
46
|
-
stage: "preflight",
|
|
47
|
-
context: {
|
|
48
|
-
workspace_root: "C:/workspace",
|
|
49
|
-
instruction: "筛选有 MCP 经验候选人",
|
|
50
|
-
confirmation: {
|
|
51
|
-
final_confirmed: true
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
control: {
|
|
55
|
-
pause_requested: false
|
|
56
|
-
},
|
|
57
|
-
resume: {
|
|
58
|
-
checkpoint_path: `C:/workspace/.state/${runId}.checkpoint.json`,
|
|
59
|
-
pause_control_path: `C:/workspace/.state/${runId}.json`,
|
|
60
|
-
output_csv: "C:/workspace/result.csv"
|
|
61
|
-
}
|
|
62
|
-
}));
|
|
63
|
-
assert.equal(queued.run_id, runId);
|
|
64
|
-
assert.equal(queued.state, RUN_STATE_QUEUED);
|
|
65
|
-
assert.equal(queued.context.workspace_root, "C:/workspace");
|
|
66
|
-
assert.equal(queued.resume.output_csv, "C:/workspace/result.csv");
|
|
67
|
-
assert.equal(queued.control.cancel_requested, false);
|
|
68
|
-
|
|
69
|
-
const running = updateRunState(runId, {
|
|
70
|
-
state: RUN_STATE_RUNNING,
|
|
71
|
-
stage: RUN_STAGE_SCREEN,
|
|
72
|
-
last_message: "screening in progress"
|
|
73
|
-
});
|
|
74
|
-
assert.equal(running.state, RUN_STATE_RUNNING);
|
|
75
|
-
assert.equal(running.stage, RUN_STAGE_SCREEN);
|
|
76
|
-
const heartbeatBeforeProgress = running.heartbeat_at;
|
|
77
|
-
|
|
78
|
-
const progressed = updateRunProgress(runId, {
|
|
79
|
-
processed: 7,
|
|
80
|
-
passed: 2,
|
|
81
|
-
skipped: 5,
|
|
82
|
-
greet_count: 1
|
|
83
|
-
});
|
|
84
|
-
assert.equal(progressed.progress.processed, 7);
|
|
85
|
-
assert.equal(progressed.progress.passed, 2);
|
|
86
|
-
assert.equal(progressed.progress.skipped, 5);
|
|
87
|
-
assert.equal(progressed.progress.greet_count, 1);
|
|
88
|
-
assert.equal(progressed.heartbeat_at, heartbeatBeforeProgress);
|
|
89
|
-
|
|
90
|
-
const paused = updateRunState(runId, {
|
|
91
|
-
state: RUN_STATE_PAUSED,
|
|
92
|
-
control: {
|
|
93
|
-
pause_requested: true,
|
|
94
|
-
pause_requested_at: "2026-01-01T00:00:00.000Z",
|
|
95
|
-
pause_requested_by: "pause_recommend_pipeline_run",
|
|
96
|
-
cancel_requested: true
|
|
97
|
-
},
|
|
98
|
-
resume: {
|
|
99
|
-
output_csv: "C:/workspace/result-partial.csv",
|
|
100
|
-
last_paused_at: "2026-01-01T00:00:01.000Z"
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
assert.equal(paused.state, RUN_STATE_PAUSED);
|
|
104
|
-
assert.equal(paused.control.pause_requested, true);
|
|
105
|
-
assert.equal(paused.control.pause_requested_by, "pause_recommend_pipeline_run");
|
|
106
|
-
assert.equal(paused.control.cancel_requested, true);
|
|
107
|
-
assert.equal(paused.resume.output_csv, "C:/workspace/result-partial.csv");
|
|
108
|
-
|
|
109
|
-
const heartbeated = touchRunHeartbeat(runId, "still running");
|
|
110
|
-
assert.equal(heartbeated.last_message, "still running");
|
|
111
|
-
assert.equal(Date.parse(heartbeated.heartbeat_at) >= Date.parse(heartbeatBeforeProgress), true);
|
|
112
|
-
|
|
113
|
-
const completed = updateRunState(runId, {
|
|
114
|
-
state: RUN_STATE_COMPLETED,
|
|
115
|
-
stage: "finalize",
|
|
116
|
-
result: {
|
|
117
|
-
status: "COMPLETED",
|
|
118
|
-
result: {
|
|
119
|
-
processed_count: 7
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
assert.equal(completed.state, RUN_STATE_COMPLETED);
|
|
124
|
-
assert.equal(completed.result.status, "COMPLETED");
|
|
125
|
-
|
|
126
|
-
const reloaded = readRunState(runId);
|
|
127
|
-
assert.equal(reloaded.state, RUN_STATE_COMPLETED);
|
|
128
|
-
assert.equal(reloaded.progress.processed, 7);
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function testRunStateCleanup() {
|
|
133
|
-
withTempHome(() => {
|
|
134
|
-
const runId = createRunId();
|
|
135
|
-
writeRunState(createRunStateSnapshot({ runId, mode: RUN_MODE_ASYNC }));
|
|
136
|
-
const runFile = path.join(getRunsDir(), `${runId}.json`);
|
|
137
|
-
const oldSeconds = Math.floor((Date.now() - 3 * 24 * 60 * 60 * 1000) / 1000);
|
|
138
|
-
fs.utimesSync(runFile, oldSeconds, oldSeconds);
|
|
139
|
-
|
|
140
|
-
const cleaned = cleanupExpiredRuns(1000);
|
|
141
|
-
assert.equal(cleaned.removed.some((item) => item.endsWith(`${runId}.json`)), true);
|
|
142
|
-
assert.equal(fs.existsSync(runFile), false);
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function main() {
|
|
147
|
-
testRunStateLifecycle();
|
|
148
|
-
testRunStateCleanup();
|
|
149
|
-
console.log("run-state tests passed");
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
main();
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
RUN_MODE_ASYNC,
|
|
7
|
+
RUN_STATE_PAUSED,
|
|
8
|
+
RUN_STAGE_SCREEN,
|
|
9
|
+
RUN_STATE_COMPLETED,
|
|
10
|
+
RUN_STATE_QUEUED,
|
|
11
|
+
RUN_STATE_RUNNING,
|
|
12
|
+
cleanupExpiredRuns,
|
|
13
|
+
createRunId,
|
|
14
|
+
createRunStateSnapshot,
|
|
15
|
+
getRunsDir,
|
|
16
|
+
readRunState,
|
|
17
|
+
touchRunHeartbeat,
|
|
18
|
+
updateRunProgress,
|
|
19
|
+
updateRunState,
|
|
20
|
+
writeRunState
|
|
21
|
+
} from "./run-state.js";
|
|
22
|
+
|
|
23
|
+
function withTempHome(testFn) {
|
|
24
|
+
const previous = process.env.BOSS_RECOMMEND_HOME;
|
|
25
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-run-state-"));
|
|
26
|
+
process.env.BOSS_RECOMMEND_HOME = tempHome;
|
|
27
|
+
try {
|
|
28
|
+
testFn(tempHome);
|
|
29
|
+
} finally {
|
|
30
|
+
if (previous === undefined) {
|
|
31
|
+
delete process.env.BOSS_RECOMMEND_HOME;
|
|
32
|
+
} else {
|
|
33
|
+
process.env.BOSS_RECOMMEND_HOME = previous;
|
|
34
|
+
}
|
|
35
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function testRunStateLifecycle() {
|
|
40
|
+
withTempHome(() => {
|
|
41
|
+
const runId = createRunId();
|
|
42
|
+
const queued = writeRunState(createRunStateSnapshot({
|
|
43
|
+
runId,
|
|
44
|
+
mode: RUN_MODE_ASYNC,
|
|
45
|
+
state: RUN_STATE_QUEUED,
|
|
46
|
+
stage: "preflight",
|
|
47
|
+
context: {
|
|
48
|
+
workspace_root: "C:/workspace",
|
|
49
|
+
instruction: "筛选有 MCP 经验候选人",
|
|
50
|
+
confirmation: {
|
|
51
|
+
final_confirmed: true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
control: {
|
|
55
|
+
pause_requested: false
|
|
56
|
+
},
|
|
57
|
+
resume: {
|
|
58
|
+
checkpoint_path: `C:/workspace/.state/${runId}.checkpoint.json`,
|
|
59
|
+
pause_control_path: `C:/workspace/.state/${runId}.json`,
|
|
60
|
+
output_csv: "C:/workspace/result.csv"
|
|
61
|
+
}
|
|
62
|
+
}));
|
|
63
|
+
assert.equal(queued.run_id, runId);
|
|
64
|
+
assert.equal(queued.state, RUN_STATE_QUEUED);
|
|
65
|
+
assert.equal(queued.context.workspace_root, "C:/workspace");
|
|
66
|
+
assert.equal(queued.resume.output_csv, "C:/workspace/result.csv");
|
|
67
|
+
assert.equal(queued.control.cancel_requested, false);
|
|
68
|
+
|
|
69
|
+
const running = updateRunState(runId, {
|
|
70
|
+
state: RUN_STATE_RUNNING,
|
|
71
|
+
stage: RUN_STAGE_SCREEN,
|
|
72
|
+
last_message: "screening in progress"
|
|
73
|
+
});
|
|
74
|
+
assert.equal(running.state, RUN_STATE_RUNNING);
|
|
75
|
+
assert.equal(running.stage, RUN_STAGE_SCREEN);
|
|
76
|
+
const heartbeatBeforeProgress = running.heartbeat_at;
|
|
77
|
+
|
|
78
|
+
const progressed = updateRunProgress(runId, {
|
|
79
|
+
processed: 7,
|
|
80
|
+
passed: 2,
|
|
81
|
+
skipped: 5,
|
|
82
|
+
greet_count: 1
|
|
83
|
+
});
|
|
84
|
+
assert.equal(progressed.progress.processed, 7);
|
|
85
|
+
assert.equal(progressed.progress.passed, 2);
|
|
86
|
+
assert.equal(progressed.progress.skipped, 5);
|
|
87
|
+
assert.equal(progressed.progress.greet_count, 1);
|
|
88
|
+
assert.equal(progressed.heartbeat_at, heartbeatBeforeProgress);
|
|
89
|
+
|
|
90
|
+
const paused = updateRunState(runId, {
|
|
91
|
+
state: RUN_STATE_PAUSED,
|
|
92
|
+
control: {
|
|
93
|
+
pause_requested: true,
|
|
94
|
+
pause_requested_at: "2026-01-01T00:00:00.000Z",
|
|
95
|
+
pause_requested_by: "pause_recommend_pipeline_run",
|
|
96
|
+
cancel_requested: true
|
|
97
|
+
},
|
|
98
|
+
resume: {
|
|
99
|
+
output_csv: "C:/workspace/result-partial.csv",
|
|
100
|
+
last_paused_at: "2026-01-01T00:00:01.000Z"
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
assert.equal(paused.state, RUN_STATE_PAUSED);
|
|
104
|
+
assert.equal(paused.control.pause_requested, true);
|
|
105
|
+
assert.equal(paused.control.pause_requested_by, "pause_recommend_pipeline_run");
|
|
106
|
+
assert.equal(paused.control.cancel_requested, true);
|
|
107
|
+
assert.equal(paused.resume.output_csv, "C:/workspace/result-partial.csv");
|
|
108
|
+
|
|
109
|
+
const heartbeated = touchRunHeartbeat(runId, "still running");
|
|
110
|
+
assert.equal(heartbeated.last_message, "still running");
|
|
111
|
+
assert.equal(Date.parse(heartbeated.heartbeat_at) >= Date.parse(heartbeatBeforeProgress), true);
|
|
112
|
+
|
|
113
|
+
const completed = updateRunState(runId, {
|
|
114
|
+
state: RUN_STATE_COMPLETED,
|
|
115
|
+
stage: "finalize",
|
|
116
|
+
result: {
|
|
117
|
+
status: "COMPLETED",
|
|
118
|
+
result: {
|
|
119
|
+
processed_count: 7
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
assert.equal(completed.state, RUN_STATE_COMPLETED);
|
|
124
|
+
assert.equal(completed.result.status, "COMPLETED");
|
|
125
|
+
|
|
126
|
+
const reloaded = readRunState(runId);
|
|
127
|
+
assert.equal(reloaded.state, RUN_STATE_COMPLETED);
|
|
128
|
+
assert.equal(reloaded.progress.processed, 7);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function testRunStateCleanup() {
|
|
133
|
+
withTempHome(() => {
|
|
134
|
+
const runId = createRunId();
|
|
135
|
+
writeRunState(createRunStateSnapshot({ runId, mode: RUN_MODE_ASYNC }));
|
|
136
|
+
const runFile = path.join(getRunsDir(), `${runId}.json`);
|
|
137
|
+
const oldSeconds = Math.floor((Date.now() - 3 * 24 * 60 * 60 * 1000) / 1000);
|
|
138
|
+
fs.utimesSync(runFile, oldSeconds, oldSeconds);
|
|
139
|
+
|
|
140
|
+
const cleaned = cleanupExpiredRuns(1000);
|
|
141
|
+
assert.equal(cleaned.removed.some((item) => item.endsWith(`${runId}.json`)), true);
|
|
142
|
+
assert.equal(fs.existsSync(runFile), false);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function main() {
|
|
147
|
+
testRunStateLifecycle();
|
|
148
|
+
testRunStateCleanup();
|
|
149
|
+
console.log("run-state tests passed");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
main();
|
|
@@ -12,6 +12,7 @@ const CSV_HEADER = ["姓名", "最高学历学校", "最高学历专业", "最
|
|
|
12
12
|
const RESUME_CAPTURE_WAIT_MS = 60000;
|
|
13
13
|
const RESUME_CAPTURE_MAX_ATTEMPTS = 4;
|
|
14
14
|
const RESUME_CAPTURE_RETRY_DELAY_MS = 1200;
|
|
15
|
+
const MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES = 10;
|
|
15
16
|
|
|
16
17
|
function log(...args) {
|
|
17
18
|
console.error(...args);
|
|
@@ -31,6 +32,9 @@ function normalizePostAction(value) {
|
|
|
31
32
|
if (!normalized) return null;
|
|
32
33
|
if (["favorite", "fav", "收藏"].includes(normalized)) return "favorite";
|
|
33
34
|
if (["greet", "chat", "打招呼", "直接沟通", "沟通"].includes(normalized)) return "greet";
|
|
35
|
+
if (["none", "noop", "no-op", "什么也不做", "不做任何操作", "不操作", "仅筛选", "只筛选"].includes(normalized)) {
|
|
36
|
+
return "none";
|
|
37
|
+
}
|
|
34
38
|
return null;
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -210,10 +214,11 @@ async function promptMissingInputs(args) {
|
|
|
210
214
|
if (!(args.postActionConfirmed === true && args.postAction)) {
|
|
211
215
|
args.postAction = await askWithValidation(
|
|
212
216
|
ask,
|
|
213
|
-
"本次通过人选统一执行什么动作?请输入 1(收藏)
|
|
217
|
+
"本次通过人选统一执行什么动作?请输入 1(收藏) / 2(直接沟通) / 3(什么也不做): ",
|
|
214
218
|
(value) => {
|
|
215
219
|
if (value === "1") return "favorite";
|
|
216
220
|
if (value === "2") return "greet";
|
|
221
|
+
if (value === "3") return "none";
|
|
217
222
|
return null;
|
|
218
223
|
}
|
|
219
224
|
);
|
|
@@ -289,9 +294,10 @@ async function promptPostAction() {
|
|
|
289
294
|
});
|
|
290
295
|
const ask = (question) => new Promise((resolve) => rl.question(question, resolve));
|
|
291
296
|
try {
|
|
292
|
-
const answer = normalizeText(await ask("本次通过人选统一执行什么动作?请输入 1(收藏)
|
|
297
|
+
const answer = normalizeText(await ask("本次通过人选统一执行什么动作?请输入 1(收藏) / 2(直接沟通) / 3(什么也不做): "));
|
|
293
298
|
if (answer === "1") return "favorite";
|
|
294
299
|
if (answer === "2") return "greet";
|
|
300
|
+
if (answer === "3") return "none";
|
|
295
301
|
throw new Error("INVALID_POST_ACTION_CONFIRMATION");
|
|
296
302
|
} finally {
|
|
297
303
|
rl.close();
|
|
@@ -1048,6 +1054,8 @@ class RecommendScreenCli {
|
|
|
1048
1054
|
this.skippedCount = 0;
|
|
1049
1055
|
this.greetCount = 0;
|
|
1050
1056
|
this.greetLimitFallbackCount = 0;
|
|
1057
|
+
this.consecutiveResumeCaptureFailures = 0;
|
|
1058
|
+
this.resumeCaptureFailureStreakKeys = [];
|
|
1051
1059
|
this.restCounter = 0;
|
|
1052
1060
|
this.restThreshold = 25 + Math.floor(Math.random() * 8);
|
|
1053
1061
|
this.checkpointPath = this.args.checkpointPath ? path.resolve(this.args.checkpointPath) : null;
|
|
@@ -1105,6 +1113,49 @@ class RecommendScreenCli {
|
|
|
1105
1113
|
};
|
|
1106
1114
|
}
|
|
1107
1115
|
|
|
1116
|
+
resetResumeCaptureFailureStreak() {
|
|
1117
|
+
this.consecutiveResumeCaptureFailures = 0;
|
|
1118
|
+
this.resumeCaptureFailureStreakKeys = [];
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
recordResumeCaptureFailure(candidateKey) {
|
|
1122
|
+
this.consecutiveResumeCaptureFailures += 1;
|
|
1123
|
+
if (candidateKey) {
|
|
1124
|
+
this.resumeCaptureFailureStreakKeys.push(candidateKey);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
rollbackResumeCaptureFailureStreak(currentCandidateKey = null) {
|
|
1129
|
+
const streakKeys = Array.from(new Set([
|
|
1130
|
+
...this.resumeCaptureFailureStreakKeys,
|
|
1131
|
+
...(currentCandidateKey ? [currentCandidateKey] : [])
|
|
1132
|
+
].filter(Boolean)));
|
|
1133
|
+
const rollbackCount = streakKeys.length;
|
|
1134
|
+
if (rollbackCount <= 0) {
|
|
1135
|
+
this.resetResumeCaptureFailureStreak();
|
|
1136
|
+
return {
|
|
1137
|
+
rollback_count: 0,
|
|
1138
|
+
processed_count: this.processedCount,
|
|
1139
|
+
skipped_count: this.skippedCount,
|
|
1140
|
+
rolled_back_keys: []
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
this.processedCount = Math.max(0, this.processedCount - rollbackCount);
|
|
1145
|
+
this.skippedCount = Math.max(0, this.skippedCount - rollbackCount);
|
|
1146
|
+
for (const key of streakKeys) {
|
|
1147
|
+
this.processedKeys.delete(key);
|
|
1148
|
+
this.discoveredKeys.delete(key);
|
|
1149
|
+
}
|
|
1150
|
+
this.resetResumeCaptureFailureStreak();
|
|
1151
|
+
return {
|
|
1152
|
+
rollback_count: rollbackCount,
|
|
1153
|
+
processed_count: this.processedCount,
|
|
1154
|
+
skipped_count: this.skippedCount,
|
|
1155
|
+
rolled_back_keys: streakKeys
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1108
1159
|
saveCheckpoint() {
|
|
1109
1160
|
if (!this.checkpointPath) return;
|
|
1110
1161
|
const payload = this.buildCheckpointPayload();
|
|
@@ -1930,6 +1981,7 @@ class RecommendScreenCli {
|
|
|
1930
1981
|
this.scrollRetryCount = 0;
|
|
1931
1982
|
this.processedCount += 1;
|
|
1932
1983
|
log(`处理第 ${this.processedCount} 位候选人: ${nextCandidate.name || nextCandidate.geek_id}`);
|
|
1984
|
+
let shouldMarkProcessed = true;
|
|
1933
1985
|
|
|
1934
1986
|
try {
|
|
1935
1987
|
await this.clickCandidate(nextCandidate);
|
|
@@ -1939,6 +1991,7 @@ class RecommendScreenCli {
|
|
|
1939
1991
|
}
|
|
1940
1992
|
|
|
1941
1993
|
const capture = await this.captureResumeImage(nextCandidate);
|
|
1994
|
+
this.resetResumeCaptureFailureStreak();
|
|
1942
1995
|
const screening = await this.callVisionModel(capture.stitchedImage);
|
|
1943
1996
|
log(`筛选结果: ${screening.passed ? "通过" : "不通过"}`);
|
|
1944
1997
|
|
|
@@ -1955,7 +2008,9 @@ class RecommendScreenCli {
|
|
|
1955
2008
|
}
|
|
1956
2009
|
const actionResult = effectiveAction === "favorite"
|
|
1957
2010
|
? await this.favoriteCandidate()
|
|
1958
|
-
:
|
|
2011
|
+
: effectiveAction === "greet"
|
|
2012
|
+
? await this.greetCandidate()
|
|
2013
|
+
: { actionTaken: "none" };
|
|
1959
2014
|
if (actionResult.actionTaken === "greet") {
|
|
1960
2015
|
this.greetCount += 1;
|
|
1961
2016
|
}
|
|
@@ -1977,7 +2032,30 @@ class RecommendScreenCli {
|
|
|
1977
2032
|
} catch (error) {
|
|
1978
2033
|
this.skippedCount += 1;
|
|
1979
2034
|
log(`候选人处理失败: ${error.code || error.message}`);
|
|
1980
|
-
if (
|
|
2035
|
+
if (error.code === "RESUME_CAPTURE_FAILED") {
|
|
2036
|
+
this.recordResumeCaptureFailure(nextCandidate.key);
|
|
2037
|
+
log(
|
|
2038
|
+
`[候选人跳过] ${nextCandidate.name || nextCandidate.geek_id || "unknown"} 简历截图失败,` +
|
|
2039
|
+
`已跳过当前候选人;连续失败 ${this.consecutiveResumeCaptureFailures}/${MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES}`
|
|
2040
|
+
);
|
|
2041
|
+
if (this.consecutiveResumeCaptureFailures >= MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES) {
|
|
2042
|
+
shouldMarkProcessed = false;
|
|
2043
|
+
const rollback = this.rollbackResumeCaptureFailureStreak(nextCandidate.key);
|
|
2044
|
+
throw this.buildError(
|
|
2045
|
+
"RESUME_CAPTURE_FAILED_CONSECUTIVE_LIMIT",
|
|
2046
|
+
`连续 ${MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES} 位候选人简历捕获失败,已停止运行以避免错误跳过。` +
|
|
2047
|
+
`已回滚这 ${rollback.rollback_count} 个失败样本的计数;最后错误: ${error.message || error}`,
|
|
2048
|
+
true,
|
|
2049
|
+
{
|
|
2050
|
+
cause_code: error.code,
|
|
2051
|
+
rollback
|
|
2052
|
+
}
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
} else {
|
|
2056
|
+
this.resetResumeCaptureFailureStreak();
|
|
2057
|
+
}
|
|
2058
|
+
if (["VISION_MODEL_FAILED"].includes(error.code)) {
|
|
1981
2059
|
throw error;
|
|
1982
2060
|
}
|
|
1983
2061
|
} finally {
|
|
@@ -1985,7 +2063,9 @@ class RecommendScreenCli {
|
|
|
1985
2063
|
if (!closed) {
|
|
1986
2064
|
throw this.buildError("DETAIL_CLOSE_FAILED", "详情页未能正确关闭");
|
|
1987
2065
|
}
|
|
1988
|
-
|
|
2066
|
+
if (shouldMarkProcessed) {
|
|
2067
|
+
this.processedKeys.add(nextCandidate.key);
|
|
2068
|
+
}
|
|
1989
2069
|
}
|
|
1990
2070
|
|
|
1991
2071
|
await this.takeBreakIfNeeded();
|
|
@@ -2050,7 +2130,7 @@ async function main() {
|
|
|
2050
2130
|
console.log(JSON.stringify({
|
|
2051
2131
|
status: "COMPLETED",
|
|
2052
2132
|
result: {
|
|
2053
|
-
usage: "node boss-recommend-screen-cli.cjs --criteria \"有 MCP 开发经验\" --post-action greet --max-greet-count 10 --post-action-confirmed true --baseurl <url> --apikey <key> --model <model> --port 9222 --output <csv-path> --checkpoint-path <checkpoint.json> --pause-control-path <pause-control.json> [--resume]"
|
|
2133
|
+
usage: "node boss-recommend-screen-cli.cjs --criteria \"有 MCP 开发经验\" --post-action <favorite|greet|none> --max-greet-count 10 --post-action-confirmed true --baseurl <url> --apikey <key> --model <model> --port 9222 --output <csv-path> --checkpoint-path <checkpoint.json> --pause-control-path <pause-control.json> [--resume]"
|
|
2054
2134
|
}
|
|
2055
2135
|
}));
|
|
2056
2136
|
return;
|
|
@@ -2062,17 +2142,30 @@ async function main() {
|
|
|
2062
2142
|
console.log(JSON.stringify(result));
|
|
2063
2143
|
}
|
|
2064
2144
|
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2145
|
+
if (require.main === module) {
|
|
2146
|
+
main().catch((error) => {
|
|
2147
|
+
const payload = {
|
|
2148
|
+
status: "FAILED",
|
|
2149
|
+
error: {
|
|
2150
|
+
code: error.code || "RECOMMEND_SCREEN_FAILED",
|
|
2151
|
+
message: error.message || "推荐页筛选执行失败。",
|
|
2152
|
+
retryable: error.retryable !== false
|
|
2153
|
+
},
|
|
2154
|
+
result: error.partial_result || null
|
|
2155
|
+
};
|
|
2156
|
+
console.log(JSON.stringify(payload));
|
|
2157
|
+
process.exitCode = 1;
|
|
2158
|
+
});
|
|
2159
|
+
} else {
|
|
2160
|
+
module.exports = {
|
|
2161
|
+
RecommendScreenCli,
|
|
2162
|
+
parseArgs,
|
|
2163
|
+
promptMissingInputs,
|
|
2164
|
+
__testables: {
|
|
2165
|
+
MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES,
|
|
2166
|
+
RESUME_CAPTURE_MAX_ATTEMPTS,
|
|
2167
|
+
RESUME_CAPTURE_WAIT_MS
|
|
2168
|
+
}
|
|
2074
2169
|
};
|
|
2075
|
-
|
|
2076
|
-
process.exitCode = 1;
|
|
2077
|
-
});
|
|
2170
|
+
}
|
|
2078
2171
|
|