@reconcrap/boss-recommend-mcp 0.1.0
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 +127 -0
- package/bin/boss-recommend-mcp.js +2 -0
- package/config/screening-config.example.json +7 -0
- package/package.json +46 -0
- package/scripts/postinstall.cjs +42 -0
- package/skills/boss-recommend-pipeline/README.md +10 -0
- package/skills/boss-recommend-pipeline/SKILL.md +94 -0
- package/src/adapters.js +637 -0
- package/src/cli.js +550 -0
- package/src/index.js +244 -0
- package/src/parser.js +423 -0
- package/src/pipeline.js +206 -0
- package/src/test-parser.js +203 -0
- package/src/test-pipeline.js +227 -0
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +1626 -0
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +407 -0
- package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +141 -0
- package/vendor/boss-recommend-search-cli/src/cli.js +526 -0
package/src/adapters.js
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
8
|
+
const packagedMcpDir = path.resolve(path.dirname(currentFilePath), "..");
|
|
9
|
+
const bossRecommendUrl = "https://www.zhipin.com/web/chat/recommend";
|
|
10
|
+
const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|history-sync|settings\/syncSetup)/i;
|
|
11
|
+
|
|
12
|
+
function getCodexHome() {
|
|
13
|
+
return process.env.CODEX_HOME
|
|
14
|
+
? path.resolve(process.env.CODEX_HOME)
|
|
15
|
+
: path.join(os.homedir(), ".codex");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getUserConfigPath() {
|
|
19
|
+
return path.join(getCodexHome(), "boss-recommend-mcp", "screening-config.json");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getDesktopDir() {
|
|
23
|
+
return path.join(os.homedir(), "Desktop");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pathExists(targetPath) {
|
|
27
|
+
try {
|
|
28
|
+
return fs.existsSync(targetPath);
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parsePositiveInteger(raw) {
|
|
35
|
+
const value = Number.parseInt(String(raw || ""), 10);
|
|
36
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveScreenConfigPath(workspaceRoot) {
|
|
40
|
+
const envConfigPath = process.env.BOSS_RECOMMEND_SCREEN_CONFIG
|
|
41
|
+
? path.resolve(process.env.BOSS_RECOMMEND_SCREEN_CONFIG)
|
|
42
|
+
: null;
|
|
43
|
+
const workspaceConfigPath = path.join(workspaceRoot, "boss-recommend-mcp", "config", "screening-config.json");
|
|
44
|
+
const userConfigPath = getUserConfigPath();
|
|
45
|
+
const packagedConfigPath = path.join(packagedMcpDir, "config", "screening-config.json");
|
|
46
|
+
const candidates = [
|
|
47
|
+
envConfigPath,
|
|
48
|
+
workspaceConfigPath,
|
|
49
|
+
userConfigPath,
|
|
50
|
+
packagedConfigPath
|
|
51
|
+
].filter(Boolean);
|
|
52
|
+
return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readJsonFile(filePath) {
|
|
56
|
+
if (!filePath || !pathExists(filePath)) return null;
|
|
57
|
+
try {
|
|
58
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
59
|
+
return JSON.parse(raw);
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveWorkspaceDebugPort(workspaceRoot) {
|
|
66
|
+
const fromEnv = parsePositiveInteger(process.env.BOSS_RECOMMEND_CHROME_PORT);
|
|
67
|
+
if (fromEnv) return fromEnv;
|
|
68
|
+
const config = readJsonFile(resolveScreenConfigPath(workspaceRoot));
|
|
69
|
+
return parsePositiveInteger(config?.debugPort) || 9222;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveRecommendSearchCliDir(workspaceRoot) {
|
|
73
|
+
const localDir = path.join(workspaceRoot, "boss-recommend-search-cli");
|
|
74
|
+
if (pathExists(localDir)) return localDir;
|
|
75
|
+
const vendoredDir = path.join(packagedMcpDir, "vendor", "boss-recommend-search-cli");
|
|
76
|
+
if (pathExists(vendoredDir)) return vendoredDir;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveRecommendScreenCliDir(workspaceRoot) {
|
|
81
|
+
const localDir = path.join(workspaceRoot, "boss-recommend-screen-cli");
|
|
82
|
+
if (pathExists(localDir)) return localDir;
|
|
83
|
+
const vendoredDir = path.join(packagedMcpDir, "vendor", "boss-recommend-screen-cli");
|
|
84
|
+
if (pathExists(vendoredDir)) return vendoredDir;
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveRecommendScreenCliEntry(screenDir) {
|
|
89
|
+
const candidates = [
|
|
90
|
+
path.join(screenDir, "boss-recommend-screen-cli.cjs"),
|
|
91
|
+
path.join(screenDir, "boss-recommend-screen-cli.js")
|
|
92
|
+
];
|
|
93
|
+
return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveRecommendSearchCliEntry(searchDir) {
|
|
97
|
+
const candidates = [
|
|
98
|
+
path.join(searchDir, "src", "cli.js"),
|
|
99
|
+
path.join(searchDir, "src", "cli.cjs")
|
|
100
|
+
];
|
|
101
|
+
return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function runProcess({ command, args, cwd, timeoutMs }) {
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
let stdout = "";
|
|
107
|
+
let stderr = "";
|
|
108
|
+
let settled = false;
|
|
109
|
+
let timer = null;
|
|
110
|
+
|
|
111
|
+
function finish(payload) {
|
|
112
|
+
if (settled) return;
|
|
113
|
+
settled = true;
|
|
114
|
+
if (timer) clearTimeout(timer);
|
|
115
|
+
resolve(payload);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let child;
|
|
119
|
+
try {
|
|
120
|
+
child = spawn(command, args, {
|
|
121
|
+
cwd,
|
|
122
|
+
windowsHide: true,
|
|
123
|
+
shell: false,
|
|
124
|
+
env: process.env
|
|
125
|
+
});
|
|
126
|
+
} catch (error) {
|
|
127
|
+
finish({
|
|
128
|
+
code: -1,
|
|
129
|
+
stdout,
|
|
130
|
+
stderr: error.message,
|
|
131
|
+
error_code: error.code || "SPAWN_FAILED"
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (timeoutMs && Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
137
|
+
timer = setTimeout(() => {
|
|
138
|
+
try {
|
|
139
|
+
child.kill();
|
|
140
|
+
} catch {}
|
|
141
|
+
finish({
|
|
142
|
+
code: -1,
|
|
143
|
+
stdout,
|
|
144
|
+
stderr: `${stderr}\nProcess timed out after ${timeoutMs}ms`.trim(),
|
|
145
|
+
error_code: "TIMEOUT"
|
|
146
|
+
});
|
|
147
|
+
}, timeoutMs);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
child.stdout.on("data", (chunk) => {
|
|
151
|
+
stdout += chunk.toString();
|
|
152
|
+
});
|
|
153
|
+
child.stderr.on("data", (chunk) => {
|
|
154
|
+
stderr += chunk.toString();
|
|
155
|
+
});
|
|
156
|
+
child.on("close", (code) => finish({ code, stdout, stderr }));
|
|
157
|
+
child.on("error", (error) => {
|
|
158
|
+
finish({
|
|
159
|
+
code: -1,
|
|
160
|
+
stdout,
|
|
161
|
+
stderr: `${stderr}\n${error.message}`.trim(),
|
|
162
|
+
error_code: error.code || "SPAWN_FAILED"
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function parseJsonOutput(text) {
|
|
169
|
+
const trimmed = String(text || "").trim();
|
|
170
|
+
if (!trimmed) return null;
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(trimmed);
|
|
173
|
+
} catch {}
|
|
174
|
+
const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
175
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(lines[index]);
|
|
178
|
+
} catch {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function loadScreenConfig(configPath) {
|
|
186
|
+
const parsed = readJsonFile(configPath);
|
|
187
|
+
if (!parsed) {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
error: `Screen config file not found or invalid: ${configPath}`
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (!parsed.baseUrl || !parsed.apiKey || !parsed.model) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
error: "Invalid screen config: baseUrl/apiKey/model are required"
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return { ok: true, config: parsed };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function localDirHint(workspaceRoot, dirName) {
|
|
203
|
+
return path.join(workspaceRoot, dirName);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function runPipelinePreflight(workspaceRoot) {
|
|
207
|
+
const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
|
|
208
|
+
const screenDir = resolveRecommendScreenCliDir(workspaceRoot);
|
|
209
|
+
const screenConfigPath = resolveScreenConfigPath(workspaceRoot);
|
|
210
|
+
const checks = [
|
|
211
|
+
{
|
|
212
|
+
key: "recommend_search_cli_dir",
|
|
213
|
+
ok: Boolean(searchDir && pathExists(searchDir)),
|
|
214
|
+
path: searchDir || localDirHint(workspaceRoot, "boss-recommend-search-cli"),
|
|
215
|
+
message: "boss-recommend-search-cli 目录不存在"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
key: "recommend_search_cli_entry",
|
|
219
|
+
ok: Boolean(searchDir && pathExists(resolveRecommendSearchCliEntry(searchDir))),
|
|
220
|
+
path: searchDir ? resolveRecommendSearchCliEntry(searchDir) : path.join(localDirHint(workspaceRoot, "boss-recommend-search-cli"), "src", "cli.js"),
|
|
221
|
+
message: "boss-recommend-search-cli 入口文件缺失"
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
key: "recommend_screen_cli_dir",
|
|
225
|
+
ok: Boolean(screenDir && pathExists(screenDir)),
|
|
226
|
+
path: screenDir || localDirHint(workspaceRoot, "boss-recommend-screen-cli"),
|
|
227
|
+
message: "boss-recommend-screen-cli 目录不存在"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
key: "recommend_screen_cli_entry",
|
|
231
|
+
ok: Boolean(screenDir && pathExists(resolveRecommendScreenCliEntry(screenDir))),
|
|
232
|
+
path: screenDir ? resolveRecommendScreenCliEntry(screenDir) : path.join(localDirHint(workspaceRoot, "boss-recommend-screen-cli"), "boss-recommend-screen-cli.cjs"),
|
|
233
|
+
message: "boss-recommend-screen-cli 入口文件缺失"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
key: "screen_config",
|
|
237
|
+
ok: pathExists(screenConfigPath),
|
|
238
|
+
path: screenConfigPath,
|
|
239
|
+
message: "screening-config.json 不存在"
|
|
240
|
+
}
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
ok: checks.every((item) => item.ok),
|
|
245
|
+
checks,
|
|
246
|
+
debug_port: resolveWorkspaceDebugPort(workspaceRoot)
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function sleep(ms) {
|
|
251
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function listChromeTabs(port) {
|
|
255
|
+
const response = await fetch(`http://127.0.0.1:${port}/json/list`);
|
|
256
|
+
if (!response.ok) {
|
|
257
|
+
throw new Error(`DevTools endpoint returned ${response.status}`);
|
|
258
|
+
}
|
|
259
|
+
const data = await response.json();
|
|
260
|
+
return Array.isArray(data) ? data : [];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function buildBossPageState(payload) {
|
|
264
|
+
return {
|
|
265
|
+
key: "boss_page_state",
|
|
266
|
+
...payload
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function extractSampleUrls(tabs, limit = 5) {
|
|
271
|
+
return tabs
|
|
272
|
+
.map((tab) => tab?.url)
|
|
273
|
+
.filter(Boolean)
|
|
274
|
+
.slice(0, limit);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function findChromeOnboardingUrl(tabs) {
|
|
278
|
+
for (const tab of tabs) {
|
|
279
|
+
if (typeof tab?.url === "string" && chromeOnboardingUrlPattern.test(tab.url)) {
|
|
280
|
+
return tab.url;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function inspectBossRecommendPageState(port, options = {}) {
|
|
287
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 6000;
|
|
288
|
+
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 1000;
|
|
289
|
+
const expectedUrl = options.expectedUrl || bossRecommendUrl;
|
|
290
|
+
const deadline = Date.now() + timeoutMs;
|
|
291
|
+
let lastError = null;
|
|
292
|
+
let lastTabs = [];
|
|
293
|
+
|
|
294
|
+
while (Date.now() < deadline) {
|
|
295
|
+
try {
|
|
296
|
+
const tabs = await listChromeTabs(port);
|
|
297
|
+
lastTabs = tabs;
|
|
298
|
+
const exactTab = tabs.find(
|
|
299
|
+
(tab) => typeof tab?.url === "string" && tab.url.includes("/web/chat/recommend")
|
|
300
|
+
);
|
|
301
|
+
if (exactTab) {
|
|
302
|
+
return buildBossPageState({
|
|
303
|
+
ok: true,
|
|
304
|
+
state: "RECOMMEND_READY",
|
|
305
|
+
path: exactTab.url,
|
|
306
|
+
current_url: exactTab.url,
|
|
307
|
+
title: exactTab.title || null,
|
|
308
|
+
requires_login: false,
|
|
309
|
+
message: "Boss 推荐页已打开,且当前仍停留在 recommend 页面。"
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const bossTab = tabs.find(
|
|
314
|
+
(tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")
|
|
315
|
+
);
|
|
316
|
+
if (bossTab) {
|
|
317
|
+
return buildBossPageState({
|
|
318
|
+
ok: false,
|
|
319
|
+
state: "LOGIN_REQUIRED",
|
|
320
|
+
path: bossTab.url,
|
|
321
|
+
current_url: bossTab.url,
|
|
322
|
+
title: bossTab.title || null,
|
|
323
|
+
requires_login: true,
|
|
324
|
+
expected_url: expectedUrl,
|
|
325
|
+
message: "Boss 页面没有停留在 recommend 页面,通常表示需要重新登录。"
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
} catch (error) {
|
|
329
|
+
lastError = error;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
await sleep(pollMs);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (lastError) {
|
|
336
|
+
return buildBossPageState({
|
|
337
|
+
ok: false,
|
|
338
|
+
state: "DEBUG_PORT_UNREACHABLE",
|
|
339
|
+
path: `http://127.0.0.1:${port}`,
|
|
340
|
+
current_url: null,
|
|
341
|
+
title: null,
|
|
342
|
+
requires_login: false,
|
|
343
|
+
expected_url,
|
|
344
|
+
message: `无法连接到 Chrome DevTools 端口 ${port}。请确认 Chrome 已以远程调试模式启动。`,
|
|
345
|
+
error: lastError.message
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const onboardingUrl = findChromeOnboardingUrl(lastTabs);
|
|
350
|
+
if (onboardingUrl) {
|
|
351
|
+
return buildBossPageState({
|
|
352
|
+
ok: false,
|
|
353
|
+
state: "CHROME_ONBOARDING_INTERCEPTED",
|
|
354
|
+
path: onboardingUrl,
|
|
355
|
+
current_url: onboardingUrl,
|
|
356
|
+
title: null,
|
|
357
|
+
requires_login: false,
|
|
358
|
+
expected_url,
|
|
359
|
+
message: "Chrome 当前停留在登录或引导页,尚未稳定到 Boss 推荐页。",
|
|
360
|
+
sample_urls: extractSampleUrls(lastTabs)
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return buildBossPageState({
|
|
365
|
+
ok: false,
|
|
366
|
+
state: "BOSS_TAB_NOT_FOUND",
|
|
367
|
+
path: expectedUrl,
|
|
368
|
+
current_url: null,
|
|
369
|
+
title: null,
|
|
370
|
+
requires_login: false,
|
|
371
|
+
expected_url,
|
|
372
|
+
message: "未检测到 Boss 推荐页标签页。",
|
|
373
|
+
sample_urls: extractSampleUrls(lastTabs)
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function openBossRecommendTab(port) {
|
|
378
|
+
const endpoint = `http://127.0.0.1:${port}/json/new?${encodeURIComponent(bossRecommendUrl)}`;
|
|
379
|
+
const attempts = ["PUT", "GET"];
|
|
380
|
+
let lastError = null;
|
|
381
|
+
|
|
382
|
+
for (const method of attempts) {
|
|
383
|
+
try {
|
|
384
|
+
const response = await fetch(endpoint, { method });
|
|
385
|
+
if (response.ok) {
|
|
386
|
+
return { ok: true, method };
|
|
387
|
+
}
|
|
388
|
+
lastError = new Error(`DevTools /json/new returned ${response.status}`);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
lastError = error;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
ok: false,
|
|
396
|
+
error: lastError?.message || "Failed to open Boss recommend tab via DevTools /json/new"
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function verifyRecommendPageStable(port, options = {}) {
|
|
401
|
+
const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 1500;
|
|
402
|
+
const recheckTimeoutMs = Number.isFinite(options.recheckTimeoutMs) ? options.recheckTimeoutMs : 2500;
|
|
403
|
+
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 600;
|
|
404
|
+
|
|
405
|
+
await sleep(settleMs);
|
|
406
|
+
const recheck = await inspectBossRecommendPageState(port, {
|
|
407
|
+
timeoutMs: recheckTimeoutMs,
|
|
408
|
+
pollMs
|
|
409
|
+
});
|
|
410
|
+
if (recheck.state === "RECOMMEND_READY") {
|
|
411
|
+
return recheck;
|
|
412
|
+
}
|
|
413
|
+
if (recheck.state === "LOGIN_REQUIRED") {
|
|
414
|
+
return buildBossPageState({
|
|
415
|
+
...recheck,
|
|
416
|
+
state: "LOGIN_REQUIRED_AFTER_REDIRECT",
|
|
417
|
+
message: "Boss 页面曾进入 recommend 但随后跳转到其他页面,通常表示登录态失效。"
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return recheck;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export async function ensureBossRecommendPageReady(workspaceRoot, options = {}) {
|
|
424
|
+
const debugPort = Number.isFinite(options.port)
|
|
425
|
+
? options.port
|
|
426
|
+
: resolveWorkspaceDebugPort(workspaceRoot);
|
|
427
|
+
const attempts = Number.isFinite(options.attempts) ? Math.max(0, options.attempts) : 3;
|
|
428
|
+
const inspectTimeoutMs = Number.isFinite(options.inspectTimeoutMs) ? options.inspectTimeoutMs : 6000;
|
|
429
|
+
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 800;
|
|
430
|
+
const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 800;
|
|
431
|
+
|
|
432
|
+
let pageState = await inspectBossRecommendPageState(debugPort, {
|
|
433
|
+
timeoutMs: inspectTimeoutMs,
|
|
434
|
+
pollMs
|
|
435
|
+
});
|
|
436
|
+
if (pageState.state === "RECOMMEND_READY") {
|
|
437
|
+
const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
|
|
438
|
+
return {
|
|
439
|
+
ok: stableState.state === "RECOMMEND_READY",
|
|
440
|
+
debug_port: debugPort,
|
|
441
|
+
state: stableState.state,
|
|
442
|
+
page_state: stableState
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
if (pageState.state === "LOGIN_REQUIRED") {
|
|
446
|
+
return {
|
|
447
|
+
ok: false,
|
|
448
|
+
debug_port: debugPort,
|
|
449
|
+
state: pageState.state,
|
|
450
|
+
page_state: pageState
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
455
|
+
if (pageState.state === "DEBUG_PORT_UNREACHABLE") {
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
await openBossRecommendTab(debugPort);
|
|
459
|
+
await sleep(settleMs);
|
|
460
|
+
pageState = await inspectBossRecommendPageState(debugPort, {
|
|
461
|
+
timeoutMs: inspectTimeoutMs,
|
|
462
|
+
pollMs
|
|
463
|
+
});
|
|
464
|
+
if (pageState.state === "RECOMMEND_READY") {
|
|
465
|
+
const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
|
|
466
|
+
return {
|
|
467
|
+
ok: stableState.state === "RECOMMEND_READY",
|
|
468
|
+
debug_port: debugPort,
|
|
469
|
+
state: stableState.state,
|
|
470
|
+
page_state: stableState
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
if (pageState.state === "LOGIN_REQUIRED") {
|
|
474
|
+
return {
|
|
475
|
+
ok: false,
|
|
476
|
+
debug_port: debugPort,
|
|
477
|
+
state: pageState.state,
|
|
478
|
+
page_state: pageState
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
ok: false,
|
|
485
|
+
debug_port: debugPort,
|
|
486
|
+
state: pageState.state || "UNKNOWN",
|
|
487
|
+
page_state: pageState
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export async function runRecommendSearchCli({ workspaceRoot, searchParams }) {
|
|
492
|
+
const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
|
|
493
|
+
if (!searchDir) {
|
|
494
|
+
return {
|
|
495
|
+
ok: false,
|
|
496
|
+
stdout: "",
|
|
497
|
+
stderr: "boss-recommend-search-cli package not found",
|
|
498
|
+
error: {
|
|
499
|
+
code: "RECOMMEND_SEARCH_CLI_MISSING",
|
|
500
|
+
message: "boss-recommend-search-cli 目录不存在。"
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const cliPath = resolveRecommendSearchCliEntry(searchDir);
|
|
505
|
+
const args = [
|
|
506
|
+
cliPath,
|
|
507
|
+
"--school-tag",
|
|
508
|
+
searchParams.school_tag,
|
|
509
|
+
"--gender",
|
|
510
|
+
searchParams.gender,
|
|
511
|
+
"--recent-not-view",
|
|
512
|
+
searchParams.recent_not_view,
|
|
513
|
+
"--port",
|
|
514
|
+
String(resolveWorkspaceDebugPort(workspaceRoot))
|
|
515
|
+
];
|
|
516
|
+
const result = await runProcess({
|
|
517
|
+
command: "node",
|
|
518
|
+
args,
|
|
519
|
+
cwd: searchDir,
|
|
520
|
+
timeoutMs: 180000
|
|
521
|
+
});
|
|
522
|
+
const structured = parseJsonOutput(result.stdout);
|
|
523
|
+
return {
|
|
524
|
+
ok: result.code === 0 && structured?.status === "COMPLETED",
|
|
525
|
+
stdout: result.stdout,
|
|
526
|
+
stderr: result.stderr,
|
|
527
|
+
structured,
|
|
528
|
+
summary: structured?.result || null,
|
|
529
|
+
error: structured?.error || (
|
|
530
|
+
result.code === 0
|
|
531
|
+
? null
|
|
532
|
+
: {
|
|
533
|
+
code: "RECOMMEND_SEARCH_FAILED",
|
|
534
|
+
message: "推荐页筛选命令执行失败。"
|
|
535
|
+
}
|
|
536
|
+
)
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export async function runRecommendScreenCli({ workspaceRoot, screenParams }) {
|
|
541
|
+
const screenDir = resolveRecommendScreenCliDir(workspaceRoot);
|
|
542
|
+
if (!screenDir) {
|
|
543
|
+
return {
|
|
544
|
+
ok: false,
|
|
545
|
+
stdout: "",
|
|
546
|
+
stderr: "boss-recommend-screen-cli package not found",
|
|
547
|
+
error: {
|
|
548
|
+
code: "RECOMMEND_SCREEN_CLI_MISSING",
|
|
549
|
+
message: "boss-recommend-screen-cli 目录不存在。"
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
const configPath = resolveScreenConfigPath(workspaceRoot);
|
|
554
|
+
const loaded = loadScreenConfig(configPath);
|
|
555
|
+
if (!loaded.ok) {
|
|
556
|
+
return {
|
|
557
|
+
ok: false,
|
|
558
|
+
stdout: "",
|
|
559
|
+
stderr: loaded.error,
|
|
560
|
+
error: {
|
|
561
|
+
code: "SCREEN_CONFIG_ERROR",
|
|
562
|
+
message: loaded.error
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const outputName = `recommend_screen_result_${Date.now()}.csv`;
|
|
568
|
+
let outputPath = outputName;
|
|
569
|
+
if (loaded.config.outputDir) {
|
|
570
|
+
const resolvedOutputDir = path.resolve(path.dirname(configPath), loaded.config.outputDir);
|
|
571
|
+
fs.mkdirSync(resolvedOutputDir, { recursive: true });
|
|
572
|
+
outputPath = path.join(resolvedOutputDir, outputName);
|
|
573
|
+
} else {
|
|
574
|
+
const desktopDir = getDesktopDir();
|
|
575
|
+
fs.mkdirSync(desktopDir, { recursive: true });
|
|
576
|
+
outputPath = path.join(desktopDir, outputName);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const cliPath = resolveRecommendScreenCliEntry(screenDir);
|
|
580
|
+
const args = [
|
|
581
|
+
cliPath,
|
|
582
|
+
"--baseurl",
|
|
583
|
+
loaded.config.baseUrl,
|
|
584
|
+
"--apikey",
|
|
585
|
+
loaded.config.apiKey,
|
|
586
|
+
"--model",
|
|
587
|
+
loaded.config.model,
|
|
588
|
+
"--port",
|
|
589
|
+
String(resolveWorkspaceDebugPort(workspaceRoot)),
|
|
590
|
+
"--criteria",
|
|
591
|
+
screenParams.criteria,
|
|
592
|
+
"--post-action",
|
|
593
|
+
screenParams.post_action,
|
|
594
|
+
"--post-action-confirmed",
|
|
595
|
+
"true",
|
|
596
|
+
"--output",
|
|
597
|
+
outputPath
|
|
598
|
+
];
|
|
599
|
+
|
|
600
|
+
if (loaded.config.openaiOrganization) {
|
|
601
|
+
args.push("--openai-organization", loaded.config.openaiOrganization);
|
|
602
|
+
}
|
|
603
|
+
if (loaded.config.openaiProject) {
|
|
604
|
+
args.push("--openai-project", loaded.config.openaiProject);
|
|
605
|
+
}
|
|
606
|
+
if (Number.isInteger(screenParams.target_count) && screenParams.target_count > 0) {
|
|
607
|
+
args.push("--targetCount", String(screenParams.target_count));
|
|
608
|
+
}
|
|
609
|
+
if (screenParams.post_action === "greet"
|
|
610
|
+
&& Number.isInteger(screenParams.max_greet_count)
|
|
611
|
+
&& screenParams.max_greet_count > 0) {
|
|
612
|
+
args.push("--max-greet-count", String(screenParams.max_greet_count));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const result = await runProcess({
|
|
616
|
+
command: "node",
|
|
617
|
+
args,
|
|
618
|
+
cwd: screenDir,
|
|
619
|
+
timeoutMs: 60 * 60 * 1000
|
|
620
|
+
});
|
|
621
|
+
const structured = parseJsonOutput(result.stdout);
|
|
622
|
+
return {
|
|
623
|
+
ok: result.code === 0 && structured?.status === "COMPLETED",
|
|
624
|
+
stdout: result.stdout,
|
|
625
|
+
stderr: result.stderr,
|
|
626
|
+
structured,
|
|
627
|
+
summary: structured?.result || null,
|
|
628
|
+
error: structured?.error || (
|
|
629
|
+
result.code === 0
|
|
630
|
+
? null
|
|
631
|
+
: {
|
|
632
|
+
code: "RECOMMEND_SCREEN_FAILED",
|
|
633
|
+
message: "推荐页筛选命令执行失败。"
|
|
634
|
+
}
|
|
635
|
+
)
|
|
636
|
+
};
|
|
637
|
+
}
|