@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
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
const { RecommendScreenCli, __testables } = require("./boss-recommend-screen-cli.cjs");
|
|
7
|
+
const { __testables: captureTestables } = require("./scripts/capture-full-resume-canvas.cjs");
|
|
8
|
+
|
|
9
|
+
class FakeRecommendScreenCli extends RecommendScreenCli {
|
|
10
|
+
constructor(args, options = {}) {
|
|
11
|
+
super(args);
|
|
12
|
+
this.testCandidates = options.candidates || [];
|
|
13
|
+
this.captureOutcomes = options.captureOutcomes || new Map();
|
|
14
|
+
this.screeningByKey = options.screeningByKey || new Map();
|
|
15
|
+
this.discoveryCalls = 0;
|
|
16
|
+
this.lastCapturedCandidateKey = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async connect() {}
|
|
20
|
+
|
|
21
|
+
async disconnect() {}
|
|
22
|
+
|
|
23
|
+
async getDetailClosedState() {
|
|
24
|
+
return { closed: true, reason: "test" };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async closeDetailPage() {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async waitForListReady() {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async ensureHealthyListViewport() {
|
|
36
|
+
return {
|
|
37
|
+
ok: true,
|
|
38
|
+
state: { ok: true }
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async discoverCandidates() {
|
|
43
|
+
if (this.discoveryCalls === 0) {
|
|
44
|
+
for (const candidate of this.testCandidates) {
|
|
45
|
+
this.candidateByKey.set(candidate.key, candidate);
|
|
46
|
+
this.discoveredKeys.add(candidate.key);
|
|
47
|
+
this.candidateQueue.push(candidate.key);
|
|
48
|
+
this.insertCounter += 1;
|
|
49
|
+
this.insertedAt.set(candidate.key, this.insertCounter);
|
|
50
|
+
}
|
|
51
|
+
this.discoveryCalls += 1;
|
|
52
|
+
return {
|
|
53
|
+
ok: true,
|
|
54
|
+
added: this.testCandidates.length,
|
|
55
|
+
candidate_count: this.testCandidates.length,
|
|
56
|
+
total_cards: this.testCandidates.length
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
this.discoveryCalls += 1;
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
added: 0,
|
|
63
|
+
candidate_count: this.testCandidates.length,
|
|
64
|
+
total_cards: this.testCandidates.length
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async scrollAndLoadMore() {
|
|
69
|
+
return {
|
|
70
|
+
before: {
|
|
71
|
+
candidateCount: this.testCandidates.length,
|
|
72
|
+
scrollTop: 0,
|
|
73
|
+
scrollHeight: 100
|
|
74
|
+
},
|
|
75
|
+
after: {
|
|
76
|
+
candidateCount: this.testCandidates.length,
|
|
77
|
+
scrollTop: 0,
|
|
78
|
+
scrollHeight: 100
|
|
79
|
+
},
|
|
80
|
+
bottom: {
|
|
81
|
+
isBottom: true
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async clickCandidate() {}
|
|
87
|
+
|
|
88
|
+
async ensureDetailOpen() {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async captureResumeImage(candidate) {
|
|
93
|
+
const outcome = this.captureOutcomes.get(candidate.key);
|
|
94
|
+
if (outcome instanceof Error) {
|
|
95
|
+
throw outcome;
|
|
96
|
+
}
|
|
97
|
+
this.lastCapturedCandidateKey = candidate.key;
|
|
98
|
+
return outcome || {
|
|
99
|
+
stitchedImage: path.join(os.tmpdir(), `${candidate.key}.png`)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async callVisionModel() {
|
|
104
|
+
return this.screeningByKey.get(this.lastCapturedCandidateKey) || {
|
|
105
|
+
passed: false,
|
|
106
|
+
reason: "not matched",
|
|
107
|
+
summary: "not matched"
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async favoriteCandidate() {
|
|
112
|
+
return { actionTaken: "favorite" };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async greetCandidate() {
|
|
116
|
+
return { actionTaken: "greet" };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async takeBreakIfNeeded() {}
|
|
120
|
+
|
|
121
|
+
saveCsv() {}
|
|
122
|
+
|
|
123
|
+
saveCheckpoint() {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function createResumeCaptureError(message = "Resume canvas not found") {
|
|
127
|
+
const error = new Error(message);
|
|
128
|
+
error.code = "RESUME_CAPTURE_FAILED";
|
|
129
|
+
error.retryable = true;
|
|
130
|
+
return error;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function createArgs(tempDir) {
|
|
134
|
+
return {
|
|
135
|
+
baseUrl: "https://example.invalid/v1",
|
|
136
|
+
apiKey: "test-key",
|
|
137
|
+
model: "test-model",
|
|
138
|
+
criteria: "test criteria",
|
|
139
|
+
targetCount: null,
|
|
140
|
+
maxGreetCount: null,
|
|
141
|
+
port: 9222,
|
|
142
|
+
output: path.join(tempDir, "result.csv"),
|
|
143
|
+
checkpointPath: path.join(tempDir, "checkpoint.json"),
|
|
144
|
+
pauseControlPath: path.join(tempDir, "pause.json"),
|
|
145
|
+
resume: false,
|
|
146
|
+
postAction: "none",
|
|
147
|
+
postActionConfirmed: true,
|
|
148
|
+
help: false,
|
|
149
|
+
__provided: {
|
|
150
|
+
baseUrl: true,
|
|
151
|
+
apiKey: true,
|
|
152
|
+
model: true,
|
|
153
|
+
criteria: true,
|
|
154
|
+
targetCount: true,
|
|
155
|
+
maxGreetCount: false,
|
|
156
|
+
port: true,
|
|
157
|
+
postAction: true,
|
|
158
|
+
postActionConfirmed: true
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function testShouldAbortResumeProbeEarly() {
|
|
164
|
+
const probe = {
|
|
165
|
+
ok: false,
|
|
166
|
+
reason: "NO_CRESUME_IFRAME",
|
|
167
|
+
debug: {
|
|
168
|
+
activeScopeCount: 0,
|
|
169
|
+
totalResumeIframes: 0,
|
|
170
|
+
visibleResumeIframes: 0
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const shouldAbort = captureTestables.shouldAbortResumeProbeEarly({
|
|
174
|
+
probe,
|
|
175
|
+
stableNoResumeIframePolls: captureTestables.EARLY_FAIL_NO_RESUME_IFRAME_STABLE_POLLS,
|
|
176
|
+
elapsedMs: captureTestables.EARLY_FAIL_NO_RESUME_IFRAME_MIN_WAIT_MS,
|
|
177
|
+
waitResumeMs: 60000
|
|
178
|
+
});
|
|
179
|
+
assert.equal(shouldAbort, true);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function testSingleResumeCaptureFailureIsSkipped() {
|
|
183
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-skip-"));
|
|
184
|
+
const badCandidate = { key: "bad", geek_id: "bad", name: "bad candidate" };
|
|
185
|
+
const goodCandidate = { key: "good", geek_id: "good", name: "good candidate" };
|
|
186
|
+
const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
|
|
187
|
+
candidates: [badCandidate, goodCandidate],
|
|
188
|
+
captureOutcomes: new Map([
|
|
189
|
+
["bad", createResumeCaptureError()],
|
|
190
|
+
["good", { stitchedImage: path.join(tempDir, "good.png") }]
|
|
191
|
+
]),
|
|
192
|
+
screeningByKey: new Map([
|
|
193
|
+
["good", { passed: true, reason: "matched", summary: "matched" }]
|
|
194
|
+
])
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const result = await cli.run();
|
|
198
|
+
assert.equal(result.status, "COMPLETED");
|
|
199
|
+
assert.equal(result.result.processed_count, 2);
|
|
200
|
+
assert.equal(result.result.passed_count, 1);
|
|
201
|
+
assert.equal(result.result.skipped_count, 1);
|
|
202
|
+
assert.equal(cli.consecutiveResumeCaptureFailures, 0);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function testConsecutiveResumeCaptureFailuresStillAbort() {
|
|
206
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-abort-"));
|
|
207
|
+
const maxFailures = __testables.MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES;
|
|
208
|
+
const candidates = Array.from({ length: maxFailures }, (_, index) => ({
|
|
209
|
+
key: `fail-${index + 1}`,
|
|
210
|
+
geek_id: `fail-${index + 1}`,
|
|
211
|
+
name: `fail-${index + 1}`
|
|
212
|
+
}));
|
|
213
|
+
const captureOutcomes = new Map(
|
|
214
|
+
candidates.map((candidate) => [candidate.key, createResumeCaptureError(`Resume capture failed for ${candidate.key}`)])
|
|
215
|
+
);
|
|
216
|
+
const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
|
|
217
|
+
candidates,
|
|
218
|
+
captureOutcomes
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
await assert.rejects(
|
|
222
|
+
() => cli.run(),
|
|
223
|
+
(error) => {
|
|
224
|
+
assert.equal(error.code, "RESUME_CAPTURE_FAILED_CONSECUTIVE_LIMIT");
|
|
225
|
+
assert.match(error.message, /连续 .* 位候选人简历捕获失败/);
|
|
226
|
+
assert.equal(error.rollback?.rollback_count, maxFailures);
|
|
227
|
+
assert.equal(error.partial_result?.processed_count, 0);
|
|
228
|
+
assert.equal(error.partial_result?.skipped_count, 0);
|
|
229
|
+
assert.deepEqual(Array.from(cli.processedKeys), []);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function main() {
|
|
236
|
+
testShouldAbortResumeProbeEarly();
|
|
237
|
+
await testSingleResumeCaptureFailureIsSkipped();
|
|
238
|
+
await testConsecutiveResumeCaptureFailuresStillAbort();
|
|
239
|
+
console.log("recoverable resume failure tests passed");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
main().catch((error) => {
|
|
243
|
+
console.error(error);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
});
|