@reconcrap/boss-recommend-mcp 1.1.8 → 1.1.9
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 +1 -1
- package/skills/boss-recommend-pipeline/SKILL.md +8 -0
- package/src/adapters.js +756 -101
- package/src/cli.js +149 -5
- package/src/index.js +84 -234
- package/src/parser.js +53 -0
- package/src/pipeline.js +253 -10
- package/src/test-adapters-runtime.js +191 -1
- package/src/test-parser.js +44 -0
- package/src/test-pipeline.js +315 -0
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +766 -64
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +151 -0
- package/vendor/boss-recommend-search-cli/src/cli.js +42 -3
- package/vendor/boss-recommend-search-cli/src/test-job-selection.js +9 -1
package/src/adapters.js
CHANGED
|
@@ -12,12 +12,16 @@ const bossLoginUrl = "https://www.zhipin.com/web/user/?ka=bticket";
|
|
|
12
12
|
const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|history-sync|settings\/syncSetup)/i;
|
|
13
13
|
const bossLoginUrlPattern = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
|
|
14
14
|
const bossLoginTitlePattern = /登录|signin|扫码登录|BOSS直聘登录/i;
|
|
15
|
-
const screenConfigTemplateDefaults = {
|
|
16
|
-
baseUrl: "https://api.openai.com/v1",
|
|
17
|
-
apiKey: "replace-with-openai-api-key",
|
|
18
|
-
model: "gpt-4.1-mini"
|
|
19
|
-
};
|
|
20
|
-
const DEFAULT_RECOMMEND_SCREEN_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
|
15
|
+
const screenConfigTemplateDefaults = {
|
|
16
|
+
baseUrl: "https://api.openai.com/v1",
|
|
17
|
+
apiKey: "replace-with-openai-api-key",
|
|
18
|
+
model: "gpt-4.1-mini"
|
|
19
|
+
};
|
|
20
|
+
const DEFAULT_RECOMMEND_SCREEN_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
|
21
|
+
const PAGE_SCOPE_TO_TAB_STATUS = {
|
|
22
|
+
recommend: "0",
|
|
23
|
+
featured: "3"
|
|
24
|
+
};
|
|
21
25
|
|
|
22
26
|
function getCodexHome() {
|
|
23
27
|
return process.env.CODEX_HOME
|
|
@@ -35,13 +39,17 @@ function getUserConfigPath() {
|
|
|
35
39
|
return path.join(getStateHome(), "screening-config.json");
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
function getLegacyUserConfigPath() {
|
|
39
|
-
return path.join(getCodexHome(), "boss-recommend-mcp", "screening-config.json");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function
|
|
43
|
-
return path.join(
|
|
44
|
-
}
|
|
42
|
+
function getLegacyUserConfigPath() {
|
|
43
|
+
return path.join(getCodexHome(), "boss-recommend-mcp", "screening-config.json");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getUserCalibrationPath() {
|
|
47
|
+
return path.join(getCodexHome(), "boss-recommend-mcp", "favorite-calibration.json");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getDesktopDir() {
|
|
51
|
+
return path.join(os.homedir(), "Desktop");
|
|
52
|
+
}
|
|
45
53
|
|
|
46
54
|
function ensureDir(targetPath) {
|
|
47
55
|
fs.mkdirSync(targetPath, { recursive: true });
|
|
@@ -64,6 +72,14 @@ function normalizeText(value) {
|
|
|
64
72
|
return String(value || "").replace(/\s+/g, " ").trim();
|
|
65
73
|
}
|
|
66
74
|
|
|
75
|
+
function normalizePageScope(value) {
|
|
76
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
77
|
+
if (!normalized) return null;
|
|
78
|
+
if (["recommend", "推荐", "推荐页", "推荐页面"].includes(normalized)) return "recommend";
|
|
79
|
+
if (["featured", "精选", "精选页", "精选页面", "精选牛人"].includes(normalized)) return "featured";
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
function shouldBringChromeToFront() {
|
|
68
84
|
const envValue = normalizeText(process.env.BOSS_RECOMMEND_BRING_TO_FRONT || "").toLowerCase();
|
|
69
85
|
if (envValue) {
|
|
@@ -245,11 +261,11 @@ export function getScreenConfigResolution(workspaceRoot) {
|
|
|
245
261
|
};
|
|
246
262
|
}
|
|
247
263
|
|
|
248
|
-
function readJsonFile(filePath) {
|
|
249
|
-
if (!filePath || !pathExists(filePath)) return null;
|
|
250
|
-
try {
|
|
251
|
-
const raw = fs.readFileSync(filePath, "utf8");
|
|
252
|
-
return JSON.parse(raw);
|
|
264
|
+
function readJsonFile(filePath) {
|
|
265
|
+
if (!filePath || !pathExists(filePath)) return null;
|
|
266
|
+
try {
|
|
267
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
268
|
+
return JSON.parse(raw);
|
|
253
269
|
} catch {
|
|
254
270
|
return null;
|
|
255
271
|
}
|
|
@@ -432,19 +448,83 @@ function resolveRecommendScreenCliEntry(screenDir) {
|
|
|
432
448
|
return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
|
|
433
449
|
}
|
|
434
450
|
|
|
435
|
-
function resolveRecommendSearchCliEntry(searchDir) {
|
|
436
|
-
const candidates = [
|
|
437
|
-
path.join(searchDir, "src", "cli.js"),
|
|
438
|
-
path.join(searchDir, "src", "cli.cjs")
|
|
451
|
+
function resolveRecommendSearchCliEntry(searchDir) {
|
|
452
|
+
const candidates = [
|
|
453
|
+
path.join(searchDir, "src", "cli.js"),
|
|
454
|
+
path.join(searchDir, "src", "cli.cjs")
|
|
439
455
|
];
|
|
440
|
-
return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
456
|
+
return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function parseKeyValueOutput(text) {
|
|
460
|
+
const result = {};
|
|
461
|
+
for (const line of String(text || "").split(/\r?\n/)) {
|
|
462
|
+
const trimmed = line.trim();
|
|
463
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
464
|
+
const sep = trimmed.indexOf("=");
|
|
465
|
+
if (sep <= 0) continue;
|
|
466
|
+
const key = trimmed.slice(0, sep).trim();
|
|
467
|
+
const value = trimmed.slice(sep + 1).trim();
|
|
468
|
+
if (!key) continue;
|
|
469
|
+
result[key] = value;
|
|
470
|
+
}
|
|
471
|
+
return result;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function runBossRecruitWhere() {
|
|
475
|
+
const direct = runProcessSync({
|
|
476
|
+
command: "boss-recruit-mcp",
|
|
477
|
+
args: ["where"]
|
|
478
|
+
});
|
|
479
|
+
if (direct.ok) {
|
|
480
|
+
return parseKeyValueOutput(direct.stdout);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (process.platform !== "win32") {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const fallback = runProcessSync({
|
|
488
|
+
command: "cmd.exe",
|
|
489
|
+
args: ["/d", "/s", "/c", "boss-recruit-mcp where"]
|
|
490
|
+
});
|
|
491
|
+
if (!fallback.ok) return null;
|
|
492
|
+
return parseKeyValueOutput(fallback.stdout);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function resolveRecruitCalibrationScriptPath(workspaceRoot) {
|
|
496
|
+
const fromEnv = normalizeText(process.env.BOSS_RECOMMEND_RECRUIT_CALIBRATION_SCRIPT || "");
|
|
497
|
+
const fromWhere = runBossRecruitWhere();
|
|
498
|
+
const packageRootFromWhere = normalizeText(fromWhere?.package_root || "");
|
|
499
|
+
const workspaceResolved = path.resolve(String(workspaceRoot || process.cwd()));
|
|
500
|
+
const candidates = [
|
|
501
|
+
fromEnv,
|
|
502
|
+
packageRootFromWhere
|
|
503
|
+
? path.join(packageRootFromWhere, "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs")
|
|
504
|
+
: null,
|
|
505
|
+
path.join(workspaceResolved, "..", "boss-recruit-mcp-main", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs"),
|
|
506
|
+
path.join(packagedMcpDir, "..", "boss-recruit-mcp-main", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs")
|
|
507
|
+
].filter(Boolean).map((item) => path.resolve(item));
|
|
508
|
+
|
|
509
|
+
for (const candidate of new Set(candidates)) {
|
|
510
|
+
if (pathExists(candidate)) {
|
|
511
|
+
return candidate;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function getCalibrationTimeoutMs(raw) {
|
|
518
|
+
const parsed = parsePositiveInteger(raw);
|
|
519
|
+
if (!parsed) return 60000;
|
|
520
|
+
return Math.max(5000, parsed);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function safeInvokeCallback(callback, payload) {
|
|
524
|
+
if (typeof callback !== "function") return;
|
|
525
|
+
try {
|
|
526
|
+
callback(payload);
|
|
527
|
+
} catch {
|
|
448
528
|
// Ignore callback errors to keep pipeline runtime stable.
|
|
449
529
|
}
|
|
450
530
|
}
|
|
@@ -912,13 +992,139 @@ function loadScreenConfig(configPath) {
|
|
|
912
992
|
return { ok: true, config: parsed };
|
|
913
993
|
}
|
|
914
994
|
|
|
915
|
-
function localDirHint(workspaceRoot, dirName) {
|
|
916
|
-
return path.join(workspaceRoot, dirName);
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
export function
|
|
920
|
-
const
|
|
921
|
-
const
|
|
995
|
+
function localDirHint(workspaceRoot, dirName) {
|
|
996
|
+
return path.join(workspaceRoot, dirName);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
export function getFeaturedCalibrationResolution(workspaceRoot) {
|
|
1000
|
+
const calibration_path = resolveFavoriteCalibrationPath(workspaceRoot);
|
|
1001
|
+
const calibration_exists = pathExists(calibration_path);
|
|
1002
|
+
const calibration_usable = isUsableCalibrationFile(calibration_path);
|
|
1003
|
+
const calibration_script_path = resolveRecruitCalibrationScriptPath(workspaceRoot);
|
|
1004
|
+
return {
|
|
1005
|
+
calibration_path,
|
|
1006
|
+
calibration_exists,
|
|
1007
|
+
calibration_usable,
|
|
1008
|
+
calibration_script_path
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
export async function runRecommendCalibration(
|
|
1013
|
+
workspaceRoot,
|
|
1014
|
+
options = {}
|
|
1015
|
+
) {
|
|
1016
|
+
const debugPort = parsePositiveInteger(options.port) || resolveWorkspaceDebugPort(workspaceRoot);
|
|
1017
|
+
const calibrationPath = options.output
|
|
1018
|
+
? path.resolve(String(options.output))
|
|
1019
|
+
: resolveFavoriteCalibrationPath(workspaceRoot);
|
|
1020
|
+
const timeoutMs = getCalibrationTimeoutMs(options.timeoutMs);
|
|
1021
|
+
const calibrationScriptPath = resolveRecruitCalibrationScriptPath(workspaceRoot);
|
|
1022
|
+
|
|
1023
|
+
if (!calibrationScriptPath) {
|
|
1024
|
+
return {
|
|
1025
|
+
ok: false,
|
|
1026
|
+
stdout: "",
|
|
1027
|
+
stderr: "",
|
|
1028
|
+
calibration_path: calibrationPath,
|
|
1029
|
+
calibration_script_path: null,
|
|
1030
|
+
debug_port: debugPort,
|
|
1031
|
+
error: {
|
|
1032
|
+
code: "CALIBRATION_SCRIPT_MISSING",
|
|
1033
|
+
message: "未找到 boss-recruit-mcp 校准脚本 calibrate-favorite-position-v2.cjs。"
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
ensureDir(path.dirname(calibrationPath));
|
|
1039
|
+
const result = await runProcess({
|
|
1040
|
+
command: "node",
|
|
1041
|
+
args: [
|
|
1042
|
+
calibrationScriptPath,
|
|
1043
|
+
"--port",
|
|
1044
|
+
String(debugPort),
|
|
1045
|
+
"--output",
|
|
1046
|
+
calibrationPath,
|
|
1047
|
+
"--timeout-ms",
|
|
1048
|
+
String(timeoutMs)
|
|
1049
|
+
],
|
|
1050
|
+
cwd: path.dirname(calibrationScriptPath),
|
|
1051
|
+
timeoutMs: timeoutMs + 15_000,
|
|
1052
|
+
heartbeatIntervalMs: options.runtime?.heartbeatIntervalMs,
|
|
1053
|
+
signal: options.runtime?.signal,
|
|
1054
|
+
onOutput: (event) => {
|
|
1055
|
+
safeInvokeCallback(options.runtime?.onOutput, event);
|
|
1056
|
+
},
|
|
1057
|
+
onHeartbeat: (event) => {
|
|
1058
|
+
safeInvokeCallback(options.runtime?.onHeartbeat, event);
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
const usable = isUsableCalibrationFile(calibrationPath);
|
|
1063
|
+
const ok = result.code === 0 && usable;
|
|
1064
|
+
return {
|
|
1065
|
+
ok,
|
|
1066
|
+
stdout: result.stdout,
|
|
1067
|
+
stderr: result.stderr,
|
|
1068
|
+
calibration_path: calibrationPath,
|
|
1069
|
+
calibration_script_path: calibrationScriptPath,
|
|
1070
|
+
debug_port: debugPort,
|
|
1071
|
+
auto_started: true,
|
|
1072
|
+
error: ok
|
|
1073
|
+
? null
|
|
1074
|
+
: {
|
|
1075
|
+
code: result.error_code === "ABORTED"
|
|
1076
|
+
? "CALIBRATION_ABORTED"
|
|
1077
|
+
: result.error_code === "TIMEOUT"
|
|
1078
|
+
? "CALIBRATION_TIMEOUT"
|
|
1079
|
+
: "CALIBRATION_FAILED",
|
|
1080
|
+
message: usable
|
|
1081
|
+
? "校准脚本执行异常。"
|
|
1082
|
+
: "校准脚本未生成可用的 favorite-calibration.json。"
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
export async function ensureFeaturedCalibrationReady(
|
|
1088
|
+
workspaceRoot,
|
|
1089
|
+
options = {}
|
|
1090
|
+
) {
|
|
1091
|
+
const calibrationPath = resolveFavoriteCalibrationPath(workspaceRoot);
|
|
1092
|
+
if (isUsableCalibrationFile(calibrationPath)) {
|
|
1093
|
+
return {
|
|
1094
|
+
ok: true,
|
|
1095
|
+
calibration_path: calibrationPath,
|
|
1096
|
+
auto_started: false
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
if (options.autoCalibrate === false) {
|
|
1100
|
+
return {
|
|
1101
|
+
ok: false,
|
|
1102
|
+
calibration_path: calibrationPath,
|
|
1103
|
+
auto_started: false,
|
|
1104
|
+
error: {
|
|
1105
|
+
code: "CALIBRATION_REQUIRED",
|
|
1106
|
+
message: "精选页收藏缺少可用校准文件,需先在推荐页精选 tab 完成校准。"
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
const calibrationRun = await runRecommendCalibration(workspaceRoot, options);
|
|
1111
|
+
if (calibrationRun.ok) {
|
|
1112
|
+
return calibrationRun;
|
|
1113
|
+
}
|
|
1114
|
+
return {
|
|
1115
|
+
...calibrationRun,
|
|
1116
|
+
ok: false,
|
|
1117
|
+
error: {
|
|
1118
|
+
code: "CALIBRATION_REQUIRED",
|
|
1119
|
+
message: calibrationRun.error?.message || "精选页收藏校准失败,请在推荐页精选 tab 重试校准。"
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
export function runPipelinePreflight(workspaceRoot, options = {}) {
|
|
1125
|
+
const pageScope = normalizePageScope(options.pageScope) || "recommend";
|
|
1126
|
+
const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
|
|
1127
|
+
const screenDir = resolveRecommendScreenCliDir(workspaceRoot);
|
|
922
1128
|
const searchDirExists = Boolean(searchDir && pathExists(searchDir));
|
|
923
1129
|
const searchEntryPath = searchDir
|
|
924
1130
|
? resolveRecommendSearchCliEntry(searchDir)
|
|
@@ -929,11 +1135,13 @@ export function runPipelinePreflight(workspaceRoot) {
|
|
|
929
1135
|
? resolveRecommendScreenCliEntry(screenDir)
|
|
930
1136
|
: path.join(localDirHint(workspaceRoot, "boss-recommend-screen-cli"), "boss-recommend-screen-cli.cjs");
|
|
931
1137
|
const screenEntryExists = Boolean(screenDir && pathExists(screenEntryPath));
|
|
932
|
-
const configResolution = getScreenConfigResolution(workspaceRoot);
|
|
933
|
-
const screenConfigPath = configResolution.resolved_path;
|
|
934
|
-
const screenConfigParsed = readJsonFile(screenConfigPath);
|
|
935
|
-
const screenConfigValidation = validateScreenConfig(screenConfigParsed);
|
|
936
|
-
const
|
|
1138
|
+
const configResolution = getScreenConfigResolution(workspaceRoot);
|
|
1139
|
+
const screenConfigPath = configResolution.resolved_path;
|
|
1140
|
+
const screenConfigParsed = readJsonFile(screenConfigPath);
|
|
1141
|
+
const screenConfigValidation = validateScreenConfig(screenConfigParsed);
|
|
1142
|
+
const calibrationPath = resolveFavoriteCalibrationPath(workspaceRoot);
|
|
1143
|
+
const calibrationUsable = isUsableCalibrationFile(calibrationPath);
|
|
1144
|
+
const checks = [
|
|
937
1145
|
{
|
|
938
1146
|
key: "recommend_search_cli_dir",
|
|
939
1147
|
ok: searchDirExists,
|
|
@@ -966,23 +1174,50 @@ export function runPipelinePreflight(workspaceRoot) {
|
|
|
966
1174
|
? "boss-recommend-screen-cli 入口文件可用"
|
|
967
1175
|
: "boss-recommend-screen-cli 入口文件缺失"
|
|
968
1176
|
},
|
|
969
|
-
{
|
|
970
|
-
key: "screen_config",
|
|
971
|
-
ok: screenConfigValidation.ok,
|
|
972
|
-
path: screenConfigPath,
|
|
973
|
-
reason: screenConfigValidation.reason || null,
|
|
974
|
-
message: screenConfigValidation.ok ? "screening-config.json 可用" : screenConfigValidation.message
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1177
|
+
{
|
|
1178
|
+
key: "screen_config",
|
|
1179
|
+
ok: screenConfigValidation.ok,
|
|
1180
|
+
path: screenConfigPath,
|
|
1181
|
+
reason: screenConfigValidation.reason || null,
|
|
1182
|
+
message: screenConfigValidation.ok ? "screening-config.json 可用" : screenConfigValidation.message
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
key: "favorite_calibration",
|
|
1186
|
+
ok: calibrationUsable,
|
|
1187
|
+
path: calibrationPath,
|
|
1188
|
+
optional: pageScope !== "featured",
|
|
1189
|
+
message: calibrationUsable
|
|
1190
|
+
? "favorite-calibration.json 可用"
|
|
1191
|
+
: "favorite-calibration.json 不存在或无效(精选页收藏仅支持校准坐标点击)"
|
|
1192
|
+
}
|
|
1193
|
+
];
|
|
1194
|
+
checks.push(...buildRuntimeDependencyChecks({ searchDir, screenDir }));
|
|
1195
|
+
|
|
1196
|
+
const requiredCheckKeys = new Set([
|
|
1197
|
+
"recommend_search_cli_dir",
|
|
1198
|
+
"recommend_search_cli_entry",
|
|
1199
|
+
"recommend_screen_cli_dir",
|
|
1200
|
+
"recommend_screen_cli_entry",
|
|
1201
|
+
"screen_config",
|
|
1202
|
+
"node_cli",
|
|
1203
|
+
"npm_dep_chrome_remote_interface_search",
|
|
1204
|
+
"npm_dep_chrome_remote_interface_screen",
|
|
1205
|
+
"npm_dep_ws",
|
|
1206
|
+
"npm_dep_sharp"
|
|
1207
|
+
]);
|
|
1208
|
+
if (pageScope === "featured") {
|
|
1209
|
+
requiredCheckKeys.add("favorite_calibration");
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
return {
|
|
1213
|
+
ok: checks.every((item) => !requiredCheckKeys.has(item.key) || item.ok),
|
|
1214
|
+
checks,
|
|
1215
|
+
debug_port: resolveWorkspaceDebugPort(workspaceRoot),
|
|
1216
|
+
config_resolution: configResolution,
|
|
1217
|
+
calibration_path: calibrationPath,
|
|
1218
|
+
page_scope: pageScope
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
986
1221
|
|
|
987
1222
|
function collectFailedCheckKeys(checks = []) {
|
|
988
1223
|
return new Set(
|
|
@@ -1036,10 +1271,10 @@ function installNpmDependencies(checks, workspaceRoot) {
|
|
|
1036
1271
|
};
|
|
1037
1272
|
}
|
|
1038
1273
|
|
|
1039
|
-
export function attemptPipelineAutoRepair(workspaceRoot, preflight = {}) {
|
|
1040
|
-
const checks = Array.isArray(preflight.checks) ? preflight.checks : [];
|
|
1041
|
-
const failed = collectFailedCheckKeys(checks);
|
|
1042
|
-
const actions = [];
|
|
1274
|
+
export function attemptPipelineAutoRepair(workspaceRoot, preflight = {}) {
|
|
1275
|
+
const checks = Array.isArray(preflight.checks) ? preflight.checks : [];
|
|
1276
|
+
const failed = collectFailedCheckKeys(checks);
|
|
1277
|
+
const actions = [];
|
|
1043
1278
|
|
|
1044
1279
|
if (
|
|
1045
1280
|
failed.has("npm_dep_chrome_remote_interface_search")
|
|
@@ -1058,13 +1293,15 @@ export function attemptPipelineAutoRepair(workspaceRoot, preflight = {}) {
|
|
|
1058
1293
|
});
|
|
1059
1294
|
}
|
|
1060
1295
|
}
|
|
1061
|
-
|
|
1062
|
-
const attempted = actions.length > 0;
|
|
1063
|
-
const nextPreflight = runPipelinePreflight(workspaceRoot
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1296
|
+
|
|
1297
|
+
const attempted = actions.length > 0;
|
|
1298
|
+
const nextPreflight = runPipelinePreflight(workspaceRoot, {
|
|
1299
|
+
pageScope: preflight?.page_scope
|
|
1300
|
+
});
|
|
1301
|
+
return {
|
|
1302
|
+
attempted,
|
|
1303
|
+
actions,
|
|
1304
|
+
preflight: nextPreflight
|
|
1068
1305
|
};
|
|
1069
1306
|
}
|
|
1070
1307
|
|
|
@@ -1290,17 +1527,413 @@ function pickBossRecommendReloadTarget(tabs = []) {
|
|
|
1290
1527
|
) || null;
|
|
1291
1528
|
}
|
|
1292
1529
|
|
|
1293
|
-
async function evaluateCdpExpression(client, expression) {
|
|
1294
|
-
const result = await client.Runtime.evaluate({
|
|
1295
|
-
expression,
|
|
1296
|
-
returnByValue: true,
|
|
1297
|
-
awaitPromise: true
|
|
1530
|
+
async function evaluateCdpExpression(client, expression) {
|
|
1531
|
+
const result = await client.Runtime.evaluate({
|
|
1532
|
+
expression,
|
|
1533
|
+
returnByValue: true,
|
|
1534
|
+
awaitPromise: true
|
|
1298
1535
|
});
|
|
1299
1536
|
if (result.exceptionDetails) {
|
|
1300
1537
|
throw new Error(result.exceptionDetails.exception?.description || "Runtime.evaluate failed");
|
|
1301
|
-
}
|
|
1302
|
-
return result.result?.value;
|
|
1303
|
-
}
|
|
1538
|
+
}
|
|
1539
|
+
return result.result?.value;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function buildRecommendTabStateExpression() {
|
|
1543
|
+
return `(() => {
|
|
1544
|
+
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
1545
|
+
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1546
|
+
|| document.querySelector('iframe');
|
|
1547
|
+
if (!frame || !frame.contentDocument) {
|
|
1548
|
+
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1549
|
+
}
|
|
1550
|
+
const doc = frame.contentDocument;
|
|
1551
|
+
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
1552
|
+
const tabs = Array.from(doc.querySelectorAll('li.tab-item[data-status], li[data-status][class*="tab"]')).map((node) => {
|
|
1553
|
+
const status = normalize(node.getAttribute('data-status'));
|
|
1554
|
+
const className = normalize(node.className);
|
|
1555
|
+
const active = (
|
|
1556
|
+
/(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(className)
|
|
1557
|
+
|| normalize(node.getAttribute('aria-selected')) === 'true'
|
|
1558
|
+
|| normalize(node.getAttribute('data-selected')) === 'true'
|
|
1559
|
+
);
|
|
1560
|
+
return {
|
|
1561
|
+
status: status || null,
|
|
1562
|
+
title: normalize(node.getAttribute('title')) || null,
|
|
1563
|
+
label: normalize(node.textContent) || null,
|
|
1564
|
+
active,
|
|
1565
|
+
class_name: className || null
|
|
1566
|
+
};
|
|
1567
|
+
});
|
|
1568
|
+
const activeTab = tabs.find((item) => item.active && item.status) || null;
|
|
1569
|
+
const featuredCount = doc.querySelectorAll('li.geek-info-card').length;
|
|
1570
|
+
const recommendCount = doc.querySelectorAll('ul.card-list > li.card-item').length;
|
|
1571
|
+
let inferredStatus = activeTab?.status || null;
|
|
1572
|
+
if (!inferredStatus) {
|
|
1573
|
+
if (featuredCount > 0 && recommendCount === 0) inferredStatus = '3';
|
|
1574
|
+
else if (recommendCount > 0 && featuredCount === 0) inferredStatus = '0';
|
|
1575
|
+
}
|
|
1576
|
+
return {
|
|
1577
|
+
ok: true,
|
|
1578
|
+
active_status: inferredStatus,
|
|
1579
|
+
tabs,
|
|
1580
|
+
layout: {
|
|
1581
|
+
featured_count: featuredCount,
|
|
1582
|
+
recommend_count: recommendCount
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
})()`;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function buildRecommendTabSwitchExpression(targetStatus) {
|
|
1589
|
+
return `((targetStatus) => {
|
|
1590
|
+
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
1591
|
+
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1592
|
+
|| document.querySelector('iframe');
|
|
1593
|
+
if (!frame || !frame.contentDocument) {
|
|
1594
|
+
return { ok: false, state: 'NO_RECOMMEND_IFRAME' };
|
|
1595
|
+
}
|
|
1596
|
+
const doc = frame.contentDocument;
|
|
1597
|
+
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
1598
|
+
const tabs = Array.from(doc.querySelectorAll('li.tab-item[data-status], li[data-status][class*="tab"]'));
|
|
1599
|
+
const target = tabs.find((node) => normalize(node.getAttribute('data-status')) === String(targetStatus)) || null;
|
|
1600
|
+
if (!target) {
|
|
1601
|
+
return { ok: false, state: 'TAB_NOT_FOUND', target_status: String(targetStatus) };
|
|
1602
|
+
}
|
|
1603
|
+
try {
|
|
1604
|
+
target.scrollIntoView({ behavior: 'instant', block: 'center', inline: 'center' });
|
|
1605
|
+
} catch {}
|
|
1606
|
+
try {
|
|
1607
|
+
target.click();
|
|
1608
|
+
} catch (error) {
|
|
1609
|
+
return {
|
|
1610
|
+
ok: false,
|
|
1611
|
+
state: 'TAB_CLICK_FAILED',
|
|
1612
|
+
message: error?.message || String(error),
|
|
1613
|
+
target_status: String(targetStatus)
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
return {
|
|
1617
|
+
ok: true,
|
|
1618
|
+
state: 'TAB_CLICKED',
|
|
1619
|
+
target_status: String(targetStatus)
|
|
1620
|
+
};
|
|
1621
|
+
})(${JSON.stringify(String(targetStatus || ""))})`;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
function buildRecommendDetailStateExpression() {
|
|
1625
|
+
return `(() => {
|
|
1626
|
+
const selectors = [
|
|
1627
|
+
'.dialog-wrap.active',
|
|
1628
|
+
'.boss-popup__wrapper',
|
|
1629
|
+
'.boss-popup_wrapper',
|
|
1630
|
+
'.boss-dialog_wrapper',
|
|
1631
|
+
'.boss-dialog',
|
|
1632
|
+
'.resume-item-detail',
|
|
1633
|
+
'.geek-detail-modal',
|
|
1634
|
+
'iframe[src*="/web/frame/c-resume/"]',
|
|
1635
|
+
'iframe[name*="resume"]',
|
|
1636
|
+
'[class*="popup"][class*="wrapper"]',
|
|
1637
|
+
'[class*="dialog"][class*="wrapper"]'
|
|
1638
|
+
];
|
|
1639
|
+
const isVisible = (node) => {
|
|
1640
|
+
if (!node || !node.getBoundingClientRect) return false;
|
|
1641
|
+
const rect = node.getBoundingClientRect();
|
|
1642
|
+
if (!rect || rect.width < 4 || rect.height < 4) return false;
|
|
1643
|
+
const style = window.getComputedStyle ? window.getComputedStyle(node) : null;
|
|
1644
|
+
if (style) {
|
|
1645
|
+
if (style.display === 'none') return false;
|
|
1646
|
+
if (style.visibility === 'hidden') return false;
|
|
1647
|
+
if (Number(style.opacity || '1') <= 0) return false;
|
|
1648
|
+
}
|
|
1649
|
+
return true;
|
|
1650
|
+
};
|
|
1651
|
+
const findVisibleDetail = (doc, source) => {
|
|
1652
|
+
if (!doc) return null;
|
|
1653
|
+
for (const selector of selectors) {
|
|
1654
|
+
const nodes = doc.querySelectorAll(selector);
|
|
1655
|
+
for (const node of nodes) {
|
|
1656
|
+
if (isVisible(node)) {
|
|
1657
|
+
return {
|
|
1658
|
+
ok: true,
|
|
1659
|
+
open: true,
|
|
1660
|
+
source,
|
|
1661
|
+
selector
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
return null;
|
|
1667
|
+
};
|
|
1668
|
+
|
|
1669
|
+
const topDetail = findVisibleDetail(document, 'top');
|
|
1670
|
+
if (topDetail) return topDetail;
|
|
1671
|
+
|
|
1672
|
+
const frame = document.querySelector('iframe[name="recommendFrame"]')
|
|
1673
|
+
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1674
|
+
|| null;
|
|
1675
|
+
if (!frame || !frame.contentDocument) {
|
|
1676
|
+
return { ok: true, open: false, reason: 'NO_RECOMMEND_IFRAME' };
|
|
1677
|
+
}
|
|
1678
|
+
const frameDetail = findVisibleDetail(frame.contentDocument, 'recommendFrame');
|
|
1679
|
+
if (frameDetail) return frameDetail;
|
|
1680
|
+
|
|
1681
|
+
return { ok: true, open: false, reason: 'DETAIL_NOT_VISIBLE' };
|
|
1682
|
+
})()`;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
export async function waitRecommendFeaturedDetailReady(workspaceRoot, options = {}) {
|
|
1686
|
+
const debugPort = Number.isFinite(options.port)
|
|
1687
|
+
? options.port
|
|
1688
|
+
: resolveWorkspaceDebugPort(workspaceRoot);
|
|
1689
|
+
const timeoutMs = Number.isFinite(options.timeoutMs)
|
|
1690
|
+
? Math.max(5000, options.timeoutMs)
|
|
1691
|
+
: 120000;
|
|
1692
|
+
const pollMs = Number.isFinite(options.pollMs)
|
|
1693
|
+
? Math.max(150, options.pollMs)
|
|
1694
|
+
: 400;
|
|
1695
|
+
|
|
1696
|
+
let client = null;
|
|
1697
|
+
try {
|
|
1698
|
+
const tabs = await listChromeTabs(debugPort);
|
|
1699
|
+
const target = pickBossRecommendReloadTarget(tabs);
|
|
1700
|
+
if (!target) {
|
|
1701
|
+
return {
|
|
1702
|
+
ok: false,
|
|
1703
|
+
debug_port: debugPort,
|
|
1704
|
+
state: "BOSS_TAB_NOT_FOUND",
|
|
1705
|
+
message: "未找到可检测详情页状态的 Boss recommend 标签页。"
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
client = await CDP({ port: debugPort, target });
|
|
1709
|
+
const { Runtime, Page } = client;
|
|
1710
|
+
if (Runtime && typeof Runtime.enable === "function") {
|
|
1711
|
+
await Runtime.enable();
|
|
1712
|
+
}
|
|
1713
|
+
if (Page && typeof Page.enable === "function") {
|
|
1714
|
+
await Page.enable();
|
|
1715
|
+
}
|
|
1716
|
+
if (shouldBringChromeToFront() && Page && typeof Page.bringToFront === "function") {
|
|
1717
|
+
await Page.bringToFront();
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
const deadline = Date.now() + timeoutMs;
|
|
1721
|
+
let lastState = null;
|
|
1722
|
+
while (Date.now() < deadline) {
|
|
1723
|
+
lastState = await evaluateCdpExpression(client, buildRecommendDetailStateExpression());
|
|
1724
|
+
if (lastState?.ok && lastState.open) {
|
|
1725
|
+
return {
|
|
1726
|
+
ok: true,
|
|
1727
|
+
debug_port: debugPort,
|
|
1728
|
+
state: "DETAIL_READY",
|
|
1729
|
+
detail_state: lastState
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
await sleep(pollMs);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
return {
|
|
1736
|
+
ok: false,
|
|
1737
|
+
debug_port: debugPort,
|
|
1738
|
+
state: "DETAIL_NOT_READY_TIMEOUT",
|
|
1739
|
+
message: "未在超时内检测到候选人详情页,请先打开精选候选人详情后重试。",
|
|
1740
|
+
detail_state: lastState || null
|
|
1741
|
+
};
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
return {
|
|
1744
|
+
ok: false,
|
|
1745
|
+
debug_port: debugPort,
|
|
1746
|
+
state: "DETAIL_STATE_CHECK_FAILED",
|
|
1747
|
+
message: error?.message || "检测候选人详情页状态失败。"
|
|
1748
|
+
};
|
|
1749
|
+
} finally {
|
|
1750
|
+
if (client) {
|
|
1751
|
+
try {
|
|
1752
|
+
await client.close();
|
|
1753
|
+
} catch {}
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
export async function readRecommendTabState(workspaceRoot, options = {}) {
|
|
1759
|
+
const debugPort = Number.isFinite(options.port)
|
|
1760
|
+
? options.port
|
|
1761
|
+
: resolveWorkspaceDebugPort(workspaceRoot);
|
|
1762
|
+
|
|
1763
|
+
let client = null;
|
|
1764
|
+
try {
|
|
1765
|
+
const tabs = await listChromeTabs(debugPort);
|
|
1766
|
+
const target = pickBossRecommendReloadTarget(tabs);
|
|
1767
|
+
if (!target) {
|
|
1768
|
+
return {
|
|
1769
|
+
ok: false,
|
|
1770
|
+
debug_port: debugPort,
|
|
1771
|
+
state: "BOSS_TAB_NOT_FOUND",
|
|
1772
|
+
message: "未找到可读取 tab 状态的 Boss recommend 标签页。"
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
client = await CDP({ port: debugPort, target });
|
|
1776
|
+
const { Runtime } = client;
|
|
1777
|
+
if (Runtime && typeof Runtime.enable === "function") {
|
|
1778
|
+
await Runtime.enable();
|
|
1779
|
+
}
|
|
1780
|
+
const tabState = await evaluateCdpExpression(client, buildRecommendTabStateExpression());
|
|
1781
|
+
if (!tabState?.ok) {
|
|
1782
|
+
return {
|
|
1783
|
+
ok: false,
|
|
1784
|
+
debug_port: debugPort,
|
|
1785
|
+
state: tabState?.error || "RECOMMEND_TAB_STATE_FAILED",
|
|
1786
|
+
message: "读取 recommend tab 状态失败。",
|
|
1787
|
+
tab_state: tabState || null
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
return {
|
|
1791
|
+
ok: true,
|
|
1792
|
+
debug_port: debugPort,
|
|
1793
|
+
active_status: normalizeText(tabState.active_status),
|
|
1794
|
+
tab_state: tabState
|
|
1795
|
+
};
|
|
1796
|
+
} catch (error) {
|
|
1797
|
+
return {
|
|
1798
|
+
ok: false,
|
|
1799
|
+
debug_port: debugPort,
|
|
1800
|
+
state: "RECOMMEND_TAB_STATE_FAILED",
|
|
1801
|
+
message: error?.message || "读取 recommend tab 状态失败。"
|
|
1802
|
+
};
|
|
1803
|
+
} finally {
|
|
1804
|
+
if (client) {
|
|
1805
|
+
try {
|
|
1806
|
+
await client.close();
|
|
1807
|
+
} catch {}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function isUsableCalibrationFile(filePath) {
|
|
1813
|
+
if (!filePath || !pathExists(filePath)) return false;
|
|
1814
|
+
const parsed = readJsonFile(filePath);
|
|
1815
|
+
return Boolean(
|
|
1816
|
+
parsed
|
|
1817
|
+
&& parsed.favoritePosition
|
|
1818
|
+
&& Number.isFinite(parsed.favoritePosition.pageX)
|
|
1819
|
+
&& Number.isFinite(parsed.favoritePosition.pageY)
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
function resolveFavoriteCalibrationPath(workspaceRoot) {
|
|
1824
|
+
const fromEnv = normalizeText(process.env.BOSS_RECOMMEND_CALIBRATION_FILE || "");
|
|
1825
|
+
if (fromEnv) return path.resolve(fromEnv);
|
|
1826
|
+
|
|
1827
|
+
const screenConfigPath = resolveScreenConfigPath(workspaceRoot);
|
|
1828
|
+
const screenConfig = readJsonFile(screenConfigPath);
|
|
1829
|
+
const calibrationFile = normalizeText(screenConfig?.calibrationFile || "");
|
|
1830
|
+
if (calibrationFile) {
|
|
1831
|
+
return path.resolve(path.dirname(screenConfigPath), calibrationFile);
|
|
1832
|
+
}
|
|
1833
|
+
return getUserCalibrationPath();
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
export async function switchRecommendTab(workspaceRoot, options = {}) {
|
|
1837
|
+
const debugPort = Number.isFinite(options.port)
|
|
1838
|
+
? options.port
|
|
1839
|
+
: resolveWorkspaceDebugPort(workspaceRoot);
|
|
1840
|
+
const targetScope = normalizePageScope(options.page_scope);
|
|
1841
|
+
const targetStatus = normalizeText(options.target_status || PAGE_SCOPE_TO_TAB_STATUS[targetScope] || "");
|
|
1842
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 8000;
|
|
1843
|
+
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 350;
|
|
1844
|
+
if (!targetStatus) {
|
|
1845
|
+
return {
|
|
1846
|
+
ok: false,
|
|
1847
|
+
debug_port: debugPort,
|
|
1848
|
+
state: "TAB_STATUS_REQUIRED",
|
|
1849
|
+
message: "切换 recommend tab 失败:缺少 target_status。"
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
let client = null;
|
|
1854
|
+
try {
|
|
1855
|
+
const tabs = await listChromeTabs(debugPort);
|
|
1856
|
+
const target = pickBossRecommendReloadTarget(tabs);
|
|
1857
|
+
if (!target) {
|
|
1858
|
+
return {
|
|
1859
|
+
ok: false,
|
|
1860
|
+
debug_port: debugPort,
|
|
1861
|
+
state: "BOSS_TAB_NOT_FOUND",
|
|
1862
|
+
message: "未找到可操作的 Boss recommend 标签页。"
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
client = await CDP({ port: debugPort, target });
|
|
1866
|
+
const { Runtime, Page } = client;
|
|
1867
|
+
if (Runtime && typeof Runtime.enable === "function") {
|
|
1868
|
+
await Runtime.enable();
|
|
1869
|
+
}
|
|
1870
|
+
if (Page && typeof Page.enable === "function") {
|
|
1871
|
+
await Page.enable();
|
|
1872
|
+
}
|
|
1873
|
+
if (shouldBringChromeToFront() && Page && typeof Page.bringToFront === "function") {
|
|
1874
|
+
await Page.bringToFront();
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
const beforeState = await evaluateCdpExpression(client, buildRecommendTabStateExpression());
|
|
1878
|
+
if (beforeState?.ok && normalizeText(beforeState.active_status) === targetStatus) {
|
|
1879
|
+
return {
|
|
1880
|
+
ok: true,
|
|
1881
|
+
debug_port: debugPort,
|
|
1882
|
+
state: "TAB_ALREADY_ACTIVE",
|
|
1883
|
+
active_status: targetStatus,
|
|
1884
|
+
tab_state: beforeState
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
const clickResult = await evaluateCdpExpression(client, buildRecommendTabSwitchExpression(targetStatus));
|
|
1889
|
+
if (!clickResult?.ok) {
|
|
1890
|
+
return {
|
|
1891
|
+
ok: false,
|
|
1892
|
+
debug_port: debugPort,
|
|
1893
|
+
state: clickResult?.state || "TAB_CLICK_FAILED",
|
|
1894
|
+
message: clickResult?.message || "点击 tab 失败。",
|
|
1895
|
+
tab_state: beforeState || null
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
const deadline = Date.now() + timeoutMs;
|
|
1900
|
+
let lastState = beforeState || null;
|
|
1901
|
+
while (Date.now() < deadline) {
|
|
1902
|
+
await sleep(pollMs);
|
|
1903
|
+
lastState = await evaluateCdpExpression(client, buildRecommendTabStateExpression());
|
|
1904
|
+
if (lastState?.ok && normalizeText(lastState.active_status) === targetStatus) {
|
|
1905
|
+
return {
|
|
1906
|
+
ok: true,
|
|
1907
|
+
debug_port: debugPort,
|
|
1908
|
+
state: "TAB_SWITCHED",
|
|
1909
|
+
active_status: targetStatus,
|
|
1910
|
+
tab_state: lastState
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
return {
|
|
1916
|
+
ok: false,
|
|
1917
|
+
debug_port: debugPort,
|
|
1918
|
+
state: "TAB_SWITCH_NOT_APPLIED",
|
|
1919
|
+
message: "点击 tab 后未在超时内确认激活状态。",
|
|
1920
|
+
tab_state: lastState || null
|
|
1921
|
+
};
|
|
1922
|
+
} catch (error) {
|
|
1923
|
+
return {
|
|
1924
|
+
ok: false,
|
|
1925
|
+
debug_port: debugPort,
|
|
1926
|
+
state: "RECOMMEND_TAB_SWITCH_FAILED",
|
|
1927
|
+
message: error?.message || "切换 recommend tab 失败。"
|
|
1928
|
+
};
|
|
1929
|
+
} finally {
|
|
1930
|
+
if (client) {
|
|
1931
|
+
try {
|
|
1932
|
+
await client.close();
|
|
1933
|
+
} catch {}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1304
1937
|
|
|
1305
1938
|
function buildRecommendRefreshStateExpression() {
|
|
1306
1939
|
return `(() => {
|
|
@@ -1698,8 +2331,8 @@ export async function listRecommendJobs({ workspaceRoot, port, runtime = null })
|
|
|
1698
2331
|
};
|
|
1699
2332
|
}
|
|
1700
2333
|
const cliPath = resolveRecommendSearchCliEntry(searchDir);
|
|
1701
|
-
const args = [
|
|
1702
|
-
cliPath,
|
|
2334
|
+
const args = [
|
|
2335
|
+
cliPath,
|
|
1703
2336
|
"--list-jobs",
|
|
1704
2337
|
"--port",
|
|
1705
2338
|
String(parsePositiveInteger(port) || resolveWorkspaceDebugPort(workspaceRoot))
|
|
@@ -1751,7 +2384,13 @@ export async function listRecommendJobs({ workspaceRoot, port, runtime = null })
|
|
|
1751
2384
|
};
|
|
1752
2385
|
}
|
|
1753
2386
|
|
|
1754
|
-
export async function runRecommendSearchCli({
|
|
2387
|
+
export async function runRecommendSearchCli({
|
|
2388
|
+
workspaceRoot,
|
|
2389
|
+
searchParams,
|
|
2390
|
+
selectedJob,
|
|
2391
|
+
pageScope = "recommend",
|
|
2392
|
+
runtime = null
|
|
2393
|
+
}) {
|
|
1755
2394
|
const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
|
|
1756
2395
|
if (!searchDir) {
|
|
1757
2396
|
return {
|
|
@@ -1765,7 +2404,7 @@ export async function runRecommendSearchCli({ workspaceRoot, searchParams, selec
|
|
|
1765
2404
|
};
|
|
1766
2405
|
}
|
|
1767
2406
|
const cliPath = resolveRecommendSearchCliEntry(searchDir);
|
|
1768
|
-
const args = [
|
|
2407
|
+
const args = [
|
|
1769
2408
|
cliPath,
|
|
1770
2409
|
"--school-tag",
|
|
1771
2410
|
serializeSchoolTagSelection(searchParams.school_tag),
|
|
@@ -1775,13 +2414,18 @@ export async function runRecommendSearchCli({ workspaceRoot, searchParams, selec
|
|
|
1775
2414
|
searchParams.gender,
|
|
1776
2415
|
"--recent-not-view",
|
|
1777
2416
|
searchParams.recent_not_view,
|
|
1778
|
-
"--port",
|
|
1779
|
-
String(resolveWorkspaceDebugPort(workspaceRoot))
|
|
1780
|
-
];
|
|
1781
|
-
const
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
2417
|
+
"--port",
|
|
2418
|
+
String(resolveWorkspaceDebugPort(workspaceRoot))
|
|
2419
|
+
];
|
|
2420
|
+
const normalizedPageScope = normalizePageScope(pageScope) || "recommend";
|
|
2421
|
+
args.push("--page-scope", normalizedPageScope);
|
|
2422
|
+
if (normalizedPageScope === "featured") {
|
|
2423
|
+
args.push("--calibration", resolveFavoriteCalibrationPath(workspaceRoot));
|
|
2424
|
+
}
|
|
2425
|
+
const normalizedSelectedJob = String(selectedJob || "").trim();
|
|
2426
|
+
if (normalizedSelectedJob) {
|
|
2427
|
+
args.push("--job", normalizedSelectedJob);
|
|
2428
|
+
}
|
|
1785
2429
|
const result = await runProcess({
|
|
1786
2430
|
command: "node",
|
|
1787
2431
|
args,
|
|
@@ -1825,7 +2469,13 @@ export async function runRecommendSearchCli({ workspaceRoot, searchParams, selec
|
|
|
1825
2469
|
};
|
|
1826
2470
|
}
|
|
1827
2471
|
|
|
1828
|
-
export async function runRecommendScreenCli({
|
|
2472
|
+
export async function runRecommendScreenCli({
|
|
2473
|
+
workspaceRoot,
|
|
2474
|
+
screenParams,
|
|
2475
|
+
pageScope = "recommend",
|
|
2476
|
+
resume = null,
|
|
2477
|
+
runtime = null
|
|
2478
|
+
}) {
|
|
1829
2479
|
const screenDir = resolveRecommendScreenCliDir(workspaceRoot);
|
|
1830
2480
|
if (!screenDir) {
|
|
1831
2481
|
return {
|
|
@@ -1909,7 +2559,7 @@ export async function runRecommendScreenCli({ workspaceRoot, screenParams, resum
|
|
|
1909
2559
|
}
|
|
1910
2560
|
|
|
1911
2561
|
const cliPath = resolveRecommendScreenCliEntry(screenDir);
|
|
1912
|
-
const args = [
|
|
2562
|
+
const args = [
|
|
1913
2563
|
cliPath,
|
|
1914
2564
|
"--baseurl",
|
|
1915
2565
|
loaded.config.baseUrl,
|
|
@@ -1925,9 +2575,11 @@ export async function runRecommendScreenCli({ workspaceRoot, screenParams, resum
|
|
|
1925
2575
|
screenParams.post_action,
|
|
1926
2576
|
"--post-action-confirmed",
|
|
1927
2577
|
"true",
|
|
1928
|
-
"--output",
|
|
1929
|
-
outputPath
|
|
1930
|
-
];
|
|
2578
|
+
"--output",
|
|
2579
|
+
outputPath
|
|
2580
|
+
];
|
|
2581
|
+
const normalizedPageScope = normalizePageScope(pageScope) || "recommend";
|
|
2582
|
+
args.push("--page-scope", normalizedPageScope);
|
|
1931
2583
|
|
|
1932
2584
|
if (loaded.config.openaiOrganization) {
|
|
1933
2585
|
args.push("--openai-organization", loaded.config.openaiOrganization);
|
|
@@ -2014,10 +2666,13 @@ export async function runRecommendScreenCli({ workspaceRoot, screenParams, resum
|
|
|
2014
2666
|
};
|
|
2015
2667
|
}
|
|
2016
2668
|
|
|
2017
|
-
export const __testables = {
|
|
2018
|
-
runProcess,
|
|
2019
|
-
parseJsonOutput,
|
|
2020
|
-
parseScreenProgressLine,
|
|
2021
|
-
resolveRecommendScreenTimeoutMs,
|
|
2022
|
-
buildRecommendScreenProcessError
|
|
2023
|
-
|
|
2669
|
+
export const __testables = {
|
|
2670
|
+
runProcess,
|
|
2671
|
+
parseJsonOutput,
|
|
2672
|
+
parseScreenProgressLine,
|
|
2673
|
+
resolveRecommendScreenTimeoutMs,
|
|
2674
|
+
buildRecommendScreenProcessError,
|
|
2675
|
+
normalizePageScope,
|
|
2676
|
+
buildRecommendTabStateExpression,
|
|
2677
|
+
buildRecommendTabSwitchExpression
|
|
2678
|
+
};
|