@reconcrap/boss-recommend-mcp 0.1.7 → 0.1.8
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 +14 -9
- package/src/adapters.js +277 -30
- package/src/cli.js +2 -3
- package/src/pipeline.js +121 -64
- package/src/test-pipeline.js +159 -0
package/package.json
CHANGED
|
@@ -91,10 +91,10 @@ CLI fallback 的状态机与 MCP 保持一致:
|
|
|
91
91
|
## Setup Checklist
|
|
92
92
|
|
|
93
93
|
执行前先检查:
|
|
94
|
-
|
|
95
|
-
- `boss-recommend-mcp` 是否已安装
|
|
96
|
-
- `screening-config.json`
|
|
97
|
-
- Chrome 远程调试端口是否可连
|
|
94
|
+
|
|
95
|
+
- `boss-recommend-mcp` 是否已安装
|
|
96
|
+
- `screening-config.json` 是否存在且 `baseUrl/apiKey/model` 均已由用户填写为可用值(不能是模板占位符)
|
|
97
|
+
- Chrome 远程调试端口是否可连
|
|
98
98
|
- 当前 Chrome 是否停留在 `https://www.zhipin.com/web/chat/recommend`
|
|
99
99
|
|
|
100
100
|
在开始执行 recommend-search-cli / recommend-screen-cli 前,必须做页面就绪门禁:
|
|
@@ -102,14 +102,19 @@ CLI fallback 的状态机与 MCP 保持一致:
|
|
|
102
102
|
- 检查 Chrome DevTools 端口是否可连接
|
|
103
103
|
- 检查 Boss 是否已登录
|
|
104
104
|
- 检查当前页面是否已停留在 recommend 页面
|
|
105
|
-
-
|
|
105
|
+
- 若端口不可连接:先自动尝试启动 Chrome,并且必须使用 `--remote-debugging-port=<port>` + `--user-data-dir=<profile>`
|
|
106
|
+
- 若检测到 Boss 已登录但不在 recommend 页面:先自动 navigate 到 `https://www.zhipin.com/web/chat/recommend`
|
|
107
|
+
- 若检测到 Boss 未登录:提示用户先登录;用户登录后先 navigate 到 recommend 页面再继续
|
|
108
|
+
- 自动修复后仍失败时,才提示用户介入并等待“已就绪”后重试
|
|
106
109
|
|
|
107
110
|
## Preflight 失败自动修复
|
|
108
111
|
|
|
109
|
-
当工具返回 `status=FAILED` 且 `error.code=PIPELINE_PREFLIGHT_FAILED` 时:
|
|
110
|
-
|
|
111
|
-
1.
|
|
112
|
-
2.
|
|
112
|
+
当工具返回 `status=FAILED` 且 `error.code=PIPELINE_PREFLIGHT_FAILED` 时:
|
|
113
|
+
|
|
114
|
+
1. 若 `diagnostics.checks` 中 `screen_config` 失败,优先引导用户填写 `screening-config.json` 的 `baseUrl/apiKey/model`(必须让用户提供真实值,不可保留模板值)。
|
|
115
|
+
2. 优先查看 `diagnostics.auto_repair`,若有自动修复动作则先基于其结果继续执行或给出最小化补救提示。
|
|
116
|
+
3. 若自动修复后仍失败,再读取 `diagnostics.recovery.agent_prompt`,直接把这段提示词交给 AI agent 执行环境修复。
|
|
117
|
+
4. 若 `diagnostics.recovery.agent_prompt` 不存在,使用下面的兜底提示词(严格顺序,不可跳步):
|
|
113
118
|
|
|
114
119
|
```text
|
|
115
120
|
你是环境修复 agent。请根据 diagnostics.checks 修复依赖,必须串行执行:
|
package/src/adapters.js
CHANGED
|
@@ -8,6 +8,12 @@ const currentFilePath = fileURLToPath(import.meta.url);
|
|
|
8
8
|
const packagedMcpDir = path.resolve(path.dirname(currentFilePath), "..");
|
|
9
9
|
const bossRecommendUrl = "https://www.zhipin.com/web/chat/recommend";
|
|
10
10
|
const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|history-sync|settings\/syncSetup)/i;
|
|
11
|
+
const bossLoginUrlPattern = /zhipin\.com\/web\/user|passport\.zhipin\.com/i;
|
|
12
|
+
const screenConfigTemplateDefaults = {
|
|
13
|
+
baseUrl: "https://api.openai.com/v1",
|
|
14
|
+
apiKey: "replace-with-openai-api-key",
|
|
15
|
+
model: "gpt-4.1-mini"
|
|
16
|
+
};
|
|
11
17
|
|
|
12
18
|
function getCodexHome() {
|
|
13
19
|
return process.env.CODEX_HOME
|
|
@@ -23,6 +29,10 @@ function getDesktopDir() {
|
|
|
23
29
|
return path.join(os.homedir(), "Desktop");
|
|
24
30
|
}
|
|
25
31
|
|
|
32
|
+
function ensureDir(targetPath) {
|
|
33
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
function pathExists(targetPath) {
|
|
27
37
|
try {
|
|
28
38
|
return fs.existsSync(targetPath);
|
|
@@ -71,6 +81,45 @@ function readJsonFile(filePath) {
|
|
|
71
81
|
}
|
|
72
82
|
}
|
|
73
83
|
|
|
84
|
+
function validateScreenConfig(config) {
|
|
85
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
message: "screening-config.json 缺失或格式无效。请填写 baseUrl、apiKey、model。"
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const baseUrl = String(config.baseUrl || "").trim();
|
|
92
|
+
const apiKey = String(config.apiKey || "").trim();
|
|
93
|
+
const model = String(config.model || "").trim();
|
|
94
|
+
const missing = [];
|
|
95
|
+
if (!baseUrl) missing.push("baseUrl");
|
|
96
|
+
if (!apiKey) missing.push("apiKey");
|
|
97
|
+
if (!model) missing.push("model");
|
|
98
|
+
if (missing.length > 0) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
message: `screening-config.json 缺少必填字段:${missing.join(", ")}。`
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (/^replace-with/i.test(apiKey) || apiKey === screenConfigTemplateDefaults.apiKey) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
message: "screening-config.json 的 apiKey 仍是模板占位符,请填写真实 API Key。"
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (
|
|
111
|
+
baseUrl === screenConfigTemplateDefaults.baseUrl
|
|
112
|
+
&& apiKey === screenConfigTemplateDefaults.apiKey
|
|
113
|
+
&& model === screenConfigTemplateDefaults.model
|
|
114
|
+
) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
message: "screening-config.json 仍是默认模板值,请填写 baseUrl、apiKey、model。"
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return { ok: true, message: "screening-config.json 校验通过。" };
|
|
121
|
+
}
|
|
122
|
+
|
|
74
123
|
function resolveWorkspaceDebugPort(workspaceRoot) {
|
|
75
124
|
const fromEnv = parsePositiveInteger(process.env.BOSS_RECOMMEND_CHROME_PORT);
|
|
76
125
|
if (fromEnv) return fromEnv;
|
|
@@ -78,6 +127,63 @@ function resolveWorkspaceDebugPort(workspaceRoot) {
|
|
|
78
127
|
return parsePositiveInteger(config?.debugPort) || 9222;
|
|
79
128
|
}
|
|
80
129
|
|
|
130
|
+
function getChromeExecutable() {
|
|
131
|
+
const candidates = [
|
|
132
|
+
process.env.BOSS_RECOMMEND_CHROME_PATH,
|
|
133
|
+
path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
134
|
+
path.join(process.env.ProgramFiles || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
135
|
+
path.join(process.env["ProgramFiles(x86)"] || "", "Google", "Chrome", "Application", "chrome.exe")
|
|
136
|
+
].filter(Boolean);
|
|
137
|
+
return candidates.find((candidate) => pathExists(candidate)) || null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getChromeUserDataDir(port) {
|
|
141
|
+
const profileDir = path.join(getCodexHome(), "boss-recommend-mcp", `chrome-profile-${port}`);
|
|
142
|
+
ensureDir(profileDir);
|
|
143
|
+
return profileDir;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function launchChromeWithDebugPort(port) {
|
|
147
|
+
const chromePath = getChromeExecutable();
|
|
148
|
+
if (!chromePath) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
code: "CHROME_EXECUTABLE_NOT_FOUND",
|
|
152
|
+
message: "未找到 Chrome 可执行文件,请安装 Chrome 或设置 BOSS_RECOMMEND_CHROME_PATH。"
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const userDataDir = getChromeUserDataDir(port);
|
|
156
|
+
const args = [
|
|
157
|
+
`--remote-debugging-port=${port}`,
|
|
158
|
+
`--user-data-dir=${userDataDir}`,
|
|
159
|
+
"--no-first-run",
|
|
160
|
+
"--no-default-browser-check",
|
|
161
|
+
"--new-window",
|
|
162
|
+
bossRecommendUrl
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const child = spawn(chromePath, args, {
|
|
167
|
+
detached: true,
|
|
168
|
+
stdio: "ignore",
|
|
169
|
+
windowsHide: false
|
|
170
|
+
});
|
|
171
|
+
child.unref();
|
|
172
|
+
return {
|
|
173
|
+
ok: true,
|
|
174
|
+
code: "CHROME_LAUNCHED",
|
|
175
|
+
chrome_path: chromePath,
|
|
176
|
+
user_data_dir: userDataDir
|
|
177
|
+
};
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
code: "CHROME_LAUNCH_FAILED",
|
|
182
|
+
message: error.message || "Chrome 启动失败。"
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
81
187
|
function resolveRecommendSearchCliDir(workspaceRoot) {
|
|
82
188
|
const localDir = path.join(workspaceRoot, "boss-recommend-search-cli");
|
|
83
189
|
if (pathExists(localDir)) return localDir;
|
|
@@ -388,16 +494,11 @@ function parseJsonOutput(text) {
|
|
|
388
494
|
|
|
389
495
|
function loadScreenConfig(configPath) {
|
|
390
496
|
const parsed = readJsonFile(configPath);
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
ok: false,
|
|
394
|
-
error: `Screen config file not found or invalid: ${configPath}`
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
if (!parsed.baseUrl || !parsed.apiKey || !parsed.model) {
|
|
497
|
+
const validation = validateScreenConfig(parsed);
|
|
498
|
+
if (!validation.ok) {
|
|
398
499
|
return {
|
|
399
500
|
ok: false,
|
|
400
|
-
error:
|
|
501
|
+
error: `${validation.message} (path: ${configPath})`
|
|
401
502
|
};
|
|
402
503
|
}
|
|
403
504
|
return { ok: true, config: parsed };
|
|
@@ -411,6 +512,8 @@ export function runPipelinePreflight(workspaceRoot) {
|
|
|
411
512
|
const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
|
|
412
513
|
const screenDir = resolveRecommendScreenCliDir(workspaceRoot);
|
|
413
514
|
const screenConfigPath = resolveScreenConfigPath(workspaceRoot);
|
|
515
|
+
const screenConfigParsed = readJsonFile(screenConfigPath);
|
|
516
|
+
const screenConfigValidation = validateScreenConfig(screenConfigParsed);
|
|
414
517
|
const checks = [
|
|
415
518
|
{
|
|
416
519
|
key: "recommend_search_cli_dir",
|
|
@@ -438,9 +541,9 @@ export function runPipelinePreflight(workspaceRoot) {
|
|
|
438
541
|
},
|
|
439
542
|
{
|
|
440
543
|
key: "screen_config",
|
|
441
|
-
ok:
|
|
544
|
+
ok: screenConfigValidation.ok,
|
|
442
545
|
path: screenConfigPath,
|
|
443
|
-
message: "screening-config.json
|
|
546
|
+
message: screenConfigValidation.ok ? "screening-config.json 可用" : screenConfigValidation.message
|
|
444
547
|
}
|
|
445
548
|
];
|
|
446
549
|
checks.push(...buildRuntimeDependencyChecks({ searchDir, screenDir }));
|
|
@@ -452,6 +555,123 @@ export function runPipelinePreflight(workspaceRoot) {
|
|
|
452
555
|
};
|
|
453
556
|
}
|
|
454
557
|
|
|
558
|
+
function collectFailedCheckKeys(checks = []) {
|
|
559
|
+
return new Set(
|
|
560
|
+
checks
|
|
561
|
+
.filter((item) => item && item.ok === false && typeof item.key === "string")
|
|
562
|
+
.map((item) => item.key)
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function collectNpmInstallDirsFromChecks(checks = [], workspaceRoot) {
|
|
567
|
+
const npmKeys = new Set([
|
|
568
|
+
"npm_dep_chrome_remote_interface_search",
|
|
569
|
+
"npm_dep_chrome_remote_interface_screen",
|
|
570
|
+
"npm_dep_ws"
|
|
571
|
+
]);
|
|
572
|
+
const dirs = checks
|
|
573
|
+
.filter((item) => item && item.ok === false && npmKeys.has(item.key))
|
|
574
|
+
.map((item) => item.install_cwd)
|
|
575
|
+
.filter((item) => typeof item === "string" && item.trim())
|
|
576
|
+
.map((item) => path.resolve(item));
|
|
577
|
+
if (dirs.length > 0) {
|
|
578
|
+
return [...new Set(dirs)];
|
|
579
|
+
}
|
|
580
|
+
return [path.resolve(workspaceRoot)];
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function installNpmDependencies(checks, workspaceRoot) {
|
|
584
|
+
const dirs = collectNpmInstallDirsFromChecks(checks, workspaceRoot);
|
|
585
|
+
const commandResults = [];
|
|
586
|
+
let allOk = true;
|
|
587
|
+
for (const cwd of dirs) {
|
|
588
|
+
const result = runProcessSync({
|
|
589
|
+
command: "npm",
|
|
590
|
+
args: ["install"],
|
|
591
|
+
cwd
|
|
592
|
+
});
|
|
593
|
+
commandResults.push({
|
|
594
|
+
cwd,
|
|
595
|
+
ok: result.ok,
|
|
596
|
+
output: result.output || result.error_message || ""
|
|
597
|
+
});
|
|
598
|
+
if (!result.ok) allOk = false;
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
ok: allOk,
|
|
602
|
+
action: "install_npm_dependencies",
|
|
603
|
+
changed: true,
|
|
604
|
+
command_results: commandResults,
|
|
605
|
+
message: allOk ? "npm 依赖自动安装完成。" : "npm 依赖自动安装失败。"
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function installPillowIfPossible() {
|
|
610
|
+
const detected = detectPythonCommand();
|
|
611
|
+
if (!detected.ok || !detected.command) {
|
|
612
|
+
return {
|
|
613
|
+
ok: false,
|
|
614
|
+
action: "install_pillow",
|
|
615
|
+
changed: false,
|
|
616
|
+
message: "未检测到可用 python 命令,无法自动安装 Pillow。"
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
const install = runProcessSync({
|
|
620
|
+
command: detected.command,
|
|
621
|
+
args: ["-m", "pip", "install", "pillow"]
|
|
622
|
+
});
|
|
623
|
+
return {
|
|
624
|
+
ok: install.ok,
|
|
625
|
+
action: "install_pillow",
|
|
626
|
+
changed: install.ok,
|
|
627
|
+
message: install.ok ? "Pillow 自动安装完成。" : `Pillow 自动安装失败:${install.output || install.error_message || "unknown"}`
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
export function attemptPipelineAutoRepair(workspaceRoot, preflight = {}) {
|
|
632
|
+
const checks = Array.isArray(preflight.checks) ? preflight.checks : [];
|
|
633
|
+
const failed = collectFailedCheckKeys(checks);
|
|
634
|
+
const actions = [];
|
|
635
|
+
|
|
636
|
+
if (
|
|
637
|
+
failed.has("npm_dep_chrome_remote_interface_search")
|
|
638
|
+
|| failed.has("npm_dep_chrome_remote_interface_screen")
|
|
639
|
+
|| failed.has("npm_dep_ws")
|
|
640
|
+
) {
|
|
641
|
+
if (!failed.has("node_cli")) {
|
|
642
|
+
actions.push(installNpmDependencies(checks, workspaceRoot));
|
|
643
|
+
} else {
|
|
644
|
+
actions.push({
|
|
645
|
+
ok: false,
|
|
646
|
+
action: "install_npm_dependencies",
|
|
647
|
+
changed: false,
|
|
648
|
+
message: "Node 命令不可用,跳过 npm 自动安装。"
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (failed.has("python_pillow")) {
|
|
654
|
+
if (!failed.has("python_cli")) {
|
|
655
|
+
actions.push(installPillowIfPossible());
|
|
656
|
+
} else {
|
|
657
|
+
actions.push({
|
|
658
|
+
ok: false,
|
|
659
|
+
action: "install_pillow",
|
|
660
|
+
changed: false,
|
|
661
|
+
message: "python 命令不可用,跳过 Pillow 自动安装。"
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const attempted = actions.length > 0;
|
|
667
|
+
const nextPreflight = runPipelinePreflight(workspaceRoot);
|
|
668
|
+
return {
|
|
669
|
+
attempted,
|
|
670
|
+
actions,
|
|
671
|
+
preflight: nextPreflight
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
455
675
|
function sleep(ms) {
|
|
456
676
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
457
677
|
}
|
|
@@ -519,15 +739,18 @@ export async function inspectBossRecommendPageState(port, options = {}) {
|
|
|
519
739
|
(tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")
|
|
520
740
|
);
|
|
521
741
|
if (bossTab) {
|
|
742
|
+
const requiresLogin = bossLoginUrlPattern.test(bossTab.url);
|
|
522
743
|
return buildBossPageState({
|
|
523
744
|
ok: false,
|
|
524
|
-
state: "LOGIN_REQUIRED",
|
|
745
|
+
state: requiresLogin ? "LOGIN_REQUIRED" : "BOSS_NOT_ON_RECOMMEND",
|
|
525
746
|
path: bossTab.url,
|
|
526
747
|
current_url: bossTab.url,
|
|
527
748
|
title: bossTab.title || null,
|
|
528
|
-
requires_login:
|
|
749
|
+
requires_login: requiresLogin,
|
|
529
750
|
expected_url: expectedUrl,
|
|
530
|
-
message:
|
|
751
|
+
message: requiresLogin
|
|
752
|
+
? "Boss 页面未登录,需先完成登录后再进入 recommend 页面。"
|
|
753
|
+
: "Boss 已登录但当前不在 recommend 页面,将尝试自动跳转。"
|
|
531
754
|
});
|
|
532
755
|
}
|
|
533
756
|
} catch (error) {
|
|
@@ -647,13 +870,39 @@ export async function ensureBossRecommendPageReady(workspaceRoot, options = {})
|
|
|
647
870
|
page_state: stableState
|
|
648
871
|
};
|
|
649
872
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
873
|
+
|
|
874
|
+
let launchAttempt = null;
|
|
875
|
+
if (pageState.state === "DEBUG_PORT_UNREACHABLE") {
|
|
876
|
+
launchAttempt = launchChromeWithDebugPort(debugPort);
|
|
877
|
+
if (launchAttempt.ok) {
|
|
878
|
+
await sleep(settleMs + 1200);
|
|
879
|
+
pageState = await inspectBossRecommendPageState(debugPort, {
|
|
880
|
+
timeoutMs: inspectTimeoutMs,
|
|
881
|
+
pollMs
|
|
882
|
+
});
|
|
883
|
+
if (pageState.state === "RECOMMEND_READY") {
|
|
884
|
+
const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
|
|
885
|
+
return {
|
|
886
|
+
ok: stableState.state === "RECOMMEND_READY",
|
|
887
|
+
debug_port: debugPort,
|
|
888
|
+
state: stableState.state,
|
|
889
|
+
page_state: {
|
|
890
|
+
...stableState,
|
|
891
|
+
launch_attempt: launchAttempt
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
} else {
|
|
896
|
+
return {
|
|
897
|
+
ok: false,
|
|
898
|
+
debug_port: debugPort,
|
|
899
|
+
state: pageState.state,
|
|
900
|
+
page_state: {
|
|
901
|
+
...pageState,
|
|
902
|
+
launch_attempt: launchAttempt
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
}
|
|
657
906
|
}
|
|
658
907
|
|
|
659
908
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
@@ -672,15 +921,10 @@ export async function ensureBossRecommendPageReady(workspaceRoot, options = {})
|
|
|
672
921
|
ok: stableState.state === "RECOMMEND_READY",
|
|
673
922
|
debug_port: debugPort,
|
|
674
923
|
state: stableState.state,
|
|
675
|
-
page_state:
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
return {
|
|
680
|
-
ok: false,
|
|
681
|
-
debug_port: debugPort,
|
|
682
|
-
state: pageState.state,
|
|
683
|
-
page_state: pageState
|
|
924
|
+
page_state: {
|
|
925
|
+
...stableState,
|
|
926
|
+
launch_attempt: launchAttempt
|
|
927
|
+
}
|
|
684
928
|
};
|
|
685
929
|
}
|
|
686
930
|
}
|
|
@@ -689,7 +933,10 @@ export async function ensureBossRecommendPageReady(workspaceRoot, options = {})
|
|
|
689
933
|
ok: false,
|
|
690
934
|
debug_port: debugPort,
|
|
691
935
|
state: pageState.state || "UNKNOWN",
|
|
692
|
-
page_state:
|
|
936
|
+
page_state: {
|
|
937
|
+
...pageState,
|
|
938
|
+
launch_attempt: launchAttempt
|
|
939
|
+
}
|
|
693
940
|
};
|
|
694
941
|
}
|
|
695
942
|
|
package/src/cli.js
CHANGED
|
@@ -582,7 +582,7 @@ function printHelp() {
|
|
|
582
582
|
console.log(" boss-recommend-mcp Start the MCP server");
|
|
583
583
|
console.log(" boss-recommend-mcp start Start the MCP server");
|
|
584
584
|
console.log(" boss-recommend-mcp run Run the recommend pipeline once via CLI and print JSON");
|
|
585
|
-
console.log(" boss-recommend-mcp install Install Codex skill and
|
|
585
|
+
console.log(" boss-recommend-mcp install Install Codex skill and MCP config templates (does not create screening-config.json)");
|
|
586
586
|
console.log(" boss-recommend-mcp install-skill Install only the Codex skill");
|
|
587
587
|
console.log(" boss-recommend-mcp init-config Create ~/.codex/boss-recommend-mcp/screening-config.json if missing");
|
|
588
588
|
console.log(" boss-recommend-mcp set-port Persist preferred Chrome debug port to screening-config.json");
|
|
@@ -610,12 +610,11 @@ function printMcpConfig(options = {}) {
|
|
|
610
610
|
|
|
611
611
|
function installAll() {
|
|
612
612
|
const skillTarget = installSkill();
|
|
613
|
-
const configResult = ensureUserConfig();
|
|
614
613
|
const mcpTemplateResult = writeMcpConfigFiles({ client: "all" });
|
|
615
614
|
const externalMcpResult = installExternalMcpConfigs({});
|
|
616
615
|
const externalSkillResult = mirrorSkillToExternalDirs();
|
|
617
616
|
console.log(`Skill installed to: ${skillTarget}`);
|
|
618
|
-
console.log(
|
|
617
|
+
console.log("screening-config.json 不会在 install 阶段自动创建。首次运行请按 doctor / agent 提示填写 baseUrl、apiKey、model。");
|
|
619
618
|
console.log(`MCP config templates exported to: ${mcpTemplateResult.outputDir}`);
|
|
620
619
|
for (const item of mcpTemplateResult.files) {
|
|
621
620
|
console.log(`- ${item.client}: ${item.file}`);
|
package/src/pipeline.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { parseRecommendInstruction } from "./parser.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import {
|
|
3
|
+
attemptPipelineAutoRepair,
|
|
4
|
+
ensureBossRecommendPageReady,
|
|
5
|
+
runPipelinePreflight,
|
|
6
|
+
runRecommendSearchCli,
|
|
7
|
+
runRecommendScreenCli
|
|
8
|
+
} from "./adapters.js";
|
|
8
9
|
|
|
9
10
|
function dedupe(values = []) {
|
|
10
11
|
return [...new Set(values.filter(Boolean))];
|
|
@@ -46,25 +47,38 @@ function formatCommandBlock(commands = []) {
|
|
|
46
47
|
return commands.map((command) => `- ${command}`).join("\n");
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
function buildPreflightRecovery(checks = [], workspaceRoot) {
|
|
50
|
-
const failed = failedCheckSet(checks);
|
|
51
|
-
if (failed.size === 0) return null;
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|| failed.has("
|
|
50
|
+
function buildPreflightRecovery(checks = [], workspaceRoot) {
|
|
51
|
+
const failed = failedCheckSet(checks);
|
|
52
|
+
if (failed.size === 0) return null;
|
|
53
|
+
|
|
54
|
+
const needScreenConfig = failed.has("screen_config");
|
|
55
|
+
const needNode = failed.has("node_cli");
|
|
56
|
+
const needNpm = (
|
|
57
|
+
failed.has("npm_dep_chrome_remote_interface_search")
|
|
58
|
+
|| failed.has("npm_dep_chrome_remote_interface_screen")
|
|
59
|
+
|| failed.has("npm_dep_ws")
|
|
58
60
|
);
|
|
59
61
|
const needPython = failed.has("python_cli");
|
|
60
62
|
const needPillow = failed.has("python_pillow");
|
|
61
|
-
|
|
62
|
-
const ordered_steps = [];
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
|
|
64
|
+
const ordered_steps = [];
|
|
65
|
+
if (needScreenConfig) {
|
|
66
|
+
const configCheck = checks.find((item) => item?.key === "screen_config");
|
|
67
|
+
ordered_steps.push({
|
|
68
|
+
id: "fill_screening_config",
|
|
69
|
+
title: "填写 screening-config.json(baseUrl / apiKey / model)",
|
|
70
|
+
blocked_by: [],
|
|
71
|
+
commands: [
|
|
72
|
+
`打开并填写:${configCheck?.path || "~/.codex/boss-recommend-mcp/screening-config.json"}`,
|
|
73
|
+
"确认 baseUrl、apiKey、model 都是可用值(不要保留模板占位符)。"
|
|
74
|
+
]
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (needNode) {
|
|
78
|
+
ordered_steps.push({
|
|
79
|
+
id: "install_nodejs",
|
|
80
|
+
title: "安装 Node.js >= 18",
|
|
81
|
+
blocked_by: [],
|
|
68
82
|
commands: [
|
|
69
83
|
"winget install OpenJS.NodeJS.LTS",
|
|
70
84
|
"node --version"
|
|
@@ -102,14 +116,21 @@ function buildPreflightRecovery(checks = [], workspaceRoot) {
|
|
|
102
116
|
});
|
|
103
117
|
}
|
|
104
118
|
|
|
105
|
-
const promptLines = [
|
|
106
|
-
"你是环境修复 agent。请先读取 diagnostics.checks,再严格按下面顺序执行,不要并行跳步:",
|
|
107
|
-
"1) node_cli 失败 -> 先安装 Node.js,未成功前禁止执行 npm install。",
|
|
108
|
-
"2) npm_dep_* 失败 -> 再安装 npm 依赖(chrome-remote-interface / ws)。",
|
|
109
|
-
"3) python_cli 失败 -> 安装 Python 并确保 python 命令可用。",
|
|
110
|
-
"4) python_pillow 失败 -> 最后安装 Pillow。",
|
|
111
|
-
"每一步完成后都重新运行 doctor,直到所有检查通过后再重试流水线。"
|
|
112
|
-
];
|
|
119
|
+
const promptLines = [
|
|
120
|
+
"你是环境修复 agent。请先读取 diagnostics.checks,再严格按下面顺序执行,不要并行跳步:",
|
|
121
|
+
"1) node_cli 失败 -> 先安装 Node.js,未成功前禁止执行 npm install。",
|
|
122
|
+
"2) npm_dep_* 失败 -> 再安装 npm 依赖(chrome-remote-interface / ws)。",
|
|
123
|
+
"3) python_cli 失败 -> 安装 Python 并确保 python 命令可用。",
|
|
124
|
+
"4) python_pillow 失败 -> 最后安装 Pillow。",
|
|
125
|
+
"每一步完成后都重新运行 doctor,直到所有检查通过后再重试流水线。"
|
|
126
|
+
];
|
|
127
|
+
if (needScreenConfig) {
|
|
128
|
+
promptLines.splice(
|
|
129
|
+
1,
|
|
130
|
+
0,
|
|
131
|
+
"0) 若 screen_config 失败:先让用户提供并填写 baseUrl、apiKey、model(不得使用模板占位符)。"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
113
134
|
|
|
114
135
|
if (needNpm) {
|
|
115
136
|
const npmCommands = buildNpmInstallCommands(checks, workspaceRoot);
|
|
@@ -190,15 +211,23 @@ function buildChromeSetupGuidance({ debugPort, pageState }) {
|
|
|
190
211
|
const currentUrl = pageState?.current_url || null;
|
|
191
212
|
const state = pageState?.state || "UNKNOWN";
|
|
192
213
|
const isPortIssue = state === "DEBUG_PORT_UNREACHABLE";
|
|
214
|
+
const needsLogin = state === "LOGIN_REQUIRED" || state === "LOGIN_REQUIRED_AFTER_REDIRECT";
|
|
215
|
+
const launchAttempt = pageState?.launch_attempt || null;
|
|
216
|
+
const launchLine = launchAttempt?.ok
|
|
217
|
+
? `已自动启动 Chrome(--remote-debugging-port=${debugPort},--user-data-dir=${launchAttempt.user_data_dir || "auto"})。`
|
|
218
|
+
: null;
|
|
193
219
|
const steps = [
|
|
194
220
|
`请先在可连接到 DevTools 端口 ${debugPort} 的 Chrome 实例中完成以下操作:`,
|
|
221
|
+
...(launchLine ? [launchLine] : []),
|
|
195
222
|
"1) 确认当前 Chrome 与本次运行使用同一个远程调试端口。",
|
|
196
223
|
isPortIssue
|
|
197
224
|
? `2) 若端口不可连接,请用远程调试方式启动 Chrome(示例:chrome.exe --remote-debugging-port=${debugPort})。`
|
|
198
225
|
: "2) 确认端口可连接且浏览器窗口保持打开。",
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
226
|
+
needsLogin
|
|
227
|
+
? "3) 当前检测到 Boss 未登录,请先在该 Chrome 实例完成登录。"
|
|
228
|
+
: "3) 如 Boss 登录态失效,请先重新登录。",
|
|
229
|
+
`4) 登录完成后先导航并停留在推荐页:${expectedUrl}`,
|
|
230
|
+
"5) 完成后回复“已就绪”,我会继续执行并优先自动导航到推荐页。"
|
|
202
231
|
];
|
|
203
232
|
return {
|
|
204
233
|
debug_port: debugPort,
|
|
@@ -210,23 +239,25 @@ function buildChromeSetupGuidance({ debugPort, pageState }) {
|
|
|
210
239
|
}
|
|
211
240
|
|
|
212
241
|
const defaultDependencies = {
|
|
242
|
+
attemptPipelineAutoRepair,
|
|
213
243
|
parseRecommendInstruction,
|
|
214
|
-
ensureBossRecommendPageReady,
|
|
215
|
-
runPipelinePreflight,
|
|
216
|
-
runRecommendSearchCli,
|
|
217
|
-
runRecommendScreenCli
|
|
244
|
+
ensureBossRecommendPageReady,
|
|
245
|
+
runPipelinePreflight,
|
|
246
|
+
runRecommendSearchCli,
|
|
247
|
+
runRecommendScreenCli
|
|
218
248
|
};
|
|
219
249
|
|
|
220
|
-
export async function runRecommendPipeline(
|
|
221
|
-
{ workspaceRoot, instruction, confirmation, overrides },
|
|
222
|
-
dependencies = defaultDependencies
|
|
223
|
-
) {
|
|
224
|
-
const {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
250
|
+
export async function runRecommendPipeline(
|
|
251
|
+
{ workspaceRoot, instruction, confirmation, overrides },
|
|
252
|
+
dependencies = defaultDependencies
|
|
253
|
+
) {
|
|
254
|
+
const {
|
|
255
|
+
attemptPipelineAutoRepair: attemptAutoRepair,
|
|
256
|
+
parseRecommendInstruction: parseInstruction,
|
|
257
|
+
ensureBossRecommendPageReady: ensureRecommendPageReady,
|
|
258
|
+
runPipelinePreflight: runPreflight,
|
|
259
|
+
runRecommendSearchCli: searchCli,
|
|
260
|
+
runRecommendScreenCli: screenCli
|
|
230
261
|
} = dependencies;
|
|
231
262
|
const startedAt = Date.now();
|
|
232
263
|
const parsed = parseInstruction({ instruction, confirmation, overrides });
|
|
@@ -249,23 +280,49 @@ export async function runRecommendPipeline(
|
|
|
249
280
|
return buildNeedConfirmationResponse(parsed);
|
|
250
281
|
}
|
|
251
282
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
283
|
+
let preflight = runPreflight(workspaceRoot);
|
|
284
|
+
let autoRepair = null;
|
|
285
|
+
if (!preflight.ok) {
|
|
286
|
+
if (typeof attemptAutoRepair === "function") {
|
|
287
|
+
autoRepair = attemptAutoRepair(workspaceRoot, preflight);
|
|
288
|
+
if (autoRepair?.preflight) {
|
|
289
|
+
preflight = autoRepair.preflight;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (!preflight.ok) {
|
|
295
|
+
const screenConfigCheck = preflight.checks?.find((item) => item?.key === "screen_config" && item?.ok === false);
|
|
296
|
+
const recovery = buildPreflightRecovery(preflight.checks, workspaceRoot);
|
|
297
|
+
return buildFailedResponse(
|
|
298
|
+
"PIPELINE_PREFLIGHT_FAILED",
|
|
299
|
+
"Recommend 流水线运行前检查失败,请先修复缺失的本地依赖或配置文件。",
|
|
300
|
+
{
|
|
301
|
+
search_params: parsed.searchParams,
|
|
302
|
+
screen_params: parsed.screenParams,
|
|
303
|
+
required_user_action: screenConfigCheck ? "provide_screening_config" : undefined,
|
|
304
|
+
guidance: screenConfigCheck
|
|
305
|
+
? {
|
|
306
|
+
config_path: screenConfigCheck.path,
|
|
307
|
+
agent_prompt: [
|
|
308
|
+
"请先让用户填写 screening-config.json 的以下字段:",
|
|
309
|
+
"1) baseUrl",
|
|
310
|
+
"2) apiKey",
|
|
311
|
+
"3) model",
|
|
312
|
+
`配置文件路径:${screenConfigCheck.path}`,
|
|
313
|
+
"注意:不要使用模板占位符(例如 replace-with-openai-api-key)。填写完成后重试。"
|
|
314
|
+
].join("\n")
|
|
315
|
+
}
|
|
316
|
+
: undefined,
|
|
317
|
+
diagnostics: {
|
|
318
|
+
checks: preflight.checks,
|
|
319
|
+
debug_port: preflight.debug_port,
|
|
320
|
+
auto_repair: autoRepair,
|
|
321
|
+
recovery
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
}
|
|
269
326
|
|
|
270
327
|
const pageCheck = await ensureRecommendPageReady(workspaceRoot, {
|
|
271
328
|
port: preflight.debug_port
|
package/src/test-pipeline.js
CHANGED
|
@@ -353,6 +353,161 @@ async function testPreflightRecoveryPlanOrder() {
|
|
|
353
353
|
assert.equal(result.diagnostics.recovery.agent_prompt.includes("不要并行跳步"), true);
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
async function testPreflightAutoRepairCanUnblockPipeline() {
|
|
357
|
+
let repairCalled = false;
|
|
358
|
+
const result = await runRecommendPipeline(
|
|
359
|
+
{
|
|
360
|
+
workspaceRoot: process.cwd(),
|
|
361
|
+
instruction: "test",
|
|
362
|
+
confirmation: {},
|
|
363
|
+
overrides: {}
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
parseRecommendInstruction: () => createParsed(),
|
|
367
|
+
runPipelinePreflight: () => ({
|
|
368
|
+
ok: false,
|
|
369
|
+
debug_port: 9222,
|
|
370
|
+
checks: [{ key: "npm_dep_ws", ok: false, install_cwd: process.cwd() }]
|
|
371
|
+
}),
|
|
372
|
+
attemptPipelineAutoRepair: () => {
|
|
373
|
+
repairCalled = true;
|
|
374
|
+
return {
|
|
375
|
+
attempted: true,
|
|
376
|
+
actions: [{ ok: true, action: "install_npm_dependencies" }],
|
|
377
|
+
preflight: { ok: true, debug_port: 9222, checks: [] }
|
|
378
|
+
};
|
|
379
|
+
},
|
|
380
|
+
ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: {} }),
|
|
381
|
+
runRecommendSearchCli: async () => ({ ok: true, summary: { candidate_count: 1, applied_filters: {} } }),
|
|
382
|
+
runRecommendScreenCli: async () => ({ ok: true, summary: { processed_count: 1, passed_count: 1, skipped_count: 0 } })
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
assert.equal(repairCalled, true);
|
|
387
|
+
assert.equal(result.status, "COMPLETED");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function testPreflightAutoRepairStillFailShouldExposeDiagnostics() {
|
|
391
|
+
const result = await runRecommendPipeline(
|
|
392
|
+
{
|
|
393
|
+
workspaceRoot: process.cwd(),
|
|
394
|
+
instruction: "test",
|
|
395
|
+
confirmation: {},
|
|
396
|
+
overrides: {}
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
parseRecommendInstruction: () => createParsed(),
|
|
400
|
+
runPipelinePreflight: () => ({
|
|
401
|
+
ok: false,
|
|
402
|
+
debug_port: 9222,
|
|
403
|
+
checks: [{ key: "node_cli", ok: false }]
|
|
404
|
+
}),
|
|
405
|
+
attemptPipelineAutoRepair: () => ({
|
|
406
|
+
attempted: true,
|
|
407
|
+
actions: [{ ok: false, action: "install_npm_dependencies" }],
|
|
408
|
+
preflight: {
|
|
409
|
+
ok: false,
|
|
410
|
+
debug_port: 9222,
|
|
411
|
+
checks: [{ key: "node_cli", ok: false }]
|
|
412
|
+
}
|
|
413
|
+
}),
|
|
414
|
+
ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: {} }),
|
|
415
|
+
runRecommendSearchCli: async () => ({ ok: true, summary: {} }),
|
|
416
|
+
runRecommendScreenCli: async () => ({ ok: true, summary: {} })
|
|
417
|
+
}
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
assert.equal(result.status, "FAILED");
|
|
421
|
+
assert.equal(result.error.code, "PIPELINE_PREFLIGHT_FAILED");
|
|
422
|
+
assert.equal(result.diagnostics.auto_repair.attempted, true);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function testScreenConfigFailureShouldRequireUserProvidedConfig() {
|
|
426
|
+
const result = await runRecommendPipeline(
|
|
427
|
+
{
|
|
428
|
+
workspaceRoot: process.cwd(),
|
|
429
|
+
instruction: "test",
|
|
430
|
+
confirmation: {},
|
|
431
|
+
overrides: {}
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
parseRecommendInstruction: () => createParsed(),
|
|
435
|
+
runPipelinePreflight: () => ({
|
|
436
|
+
ok: false,
|
|
437
|
+
debug_port: 9222,
|
|
438
|
+
checks: [
|
|
439
|
+
{
|
|
440
|
+
key: "screen_config",
|
|
441
|
+
ok: false,
|
|
442
|
+
path: "C:/Users/test/.codex/boss-recommend-mcp/screening-config.json",
|
|
443
|
+
message: "screening-config.json 缺失或格式无效"
|
|
444
|
+
}
|
|
445
|
+
]
|
|
446
|
+
}),
|
|
447
|
+
attemptPipelineAutoRepair: () => ({
|
|
448
|
+
attempted: false,
|
|
449
|
+
actions: [],
|
|
450
|
+
preflight: {
|
|
451
|
+
ok: false,
|
|
452
|
+
debug_port: 9222,
|
|
453
|
+
checks: [
|
|
454
|
+
{
|
|
455
|
+
key: "screen_config",
|
|
456
|
+
ok: false,
|
|
457
|
+
path: "C:/Users/test/.codex/boss-recommend-mcp/screening-config.json",
|
|
458
|
+
message: "screening-config.json 缺失或格式无效"
|
|
459
|
+
}
|
|
460
|
+
]
|
|
461
|
+
}
|
|
462
|
+
}),
|
|
463
|
+
ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: {} }),
|
|
464
|
+
runRecommendSearchCli: async () => ({ ok: true, summary: {} }),
|
|
465
|
+
runRecommendScreenCli: async () => ({ ok: true, summary: {} })
|
|
466
|
+
}
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
assert.equal(result.status, "FAILED");
|
|
470
|
+
assert.equal(result.error.code, "PIPELINE_PREFLIGHT_FAILED");
|
|
471
|
+
assert.equal(result.required_user_action, "provide_screening_config");
|
|
472
|
+
assert.equal(result.guidance.config_path.includes("screening-config.json"), true);
|
|
473
|
+
assert.equal(result.guidance.agent_prompt.includes("baseUrl"), true);
|
|
474
|
+
assert.equal(result.guidance.agent_prompt.includes("apiKey"), true);
|
|
475
|
+
assert.equal(result.guidance.agent_prompt.includes("model"), true);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function testScreenConfigRecoveryStepShouldBeFirst() {
|
|
479
|
+
const result = await runRecommendPipeline(
|
|
480
|
+
{
|
|
481
|
+
workspaceRoot: "C:/workspace/boss-recommend-mcp",
|
|
482
|
+
instruction: "test",
|
|
483
|
+
confirmation: {},
|
|
484
|
+
overrides: {}
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
parseRecommendInstruction: () => createParsed(),
|
|
488
|
+
runPipelinePreflight: () => ({
|
|
489
|
+
ok: false,
|
|
490
|
+
debug_port: 9222,
|
|
491
|
+
checks: [
|
|
492
|
+
{
|
|
493
|
+
key: "screen_config",
|
|
494
|
+
ok: false,
|
|
495
|
+
path: "C:/Users/test/.codex/boss-recommend-mcp/screening-config.json",
|
|
496
|
+
message: "screening-config.json 缺失或格式无效"
|
|
497
|
+
},
|
|
498
|
+
{ key: "node_cli", ok: false }
|
|
499
|
+
]
|
|
500
|
+
}),
|
|
501
|
+
ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: {} }),
|
|
502
|
+
runRecommendSearchCli: async () => ({ ok: true, summary: {} }),
|
|
503
|
+
runRecommendScreenCli: async () => ({ ok: true, summary: {} })
|
|
504
|
+
}
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
assert.equal(result.status, "FAILED");
|
|
508
|
+
assert.equal(result.diagnostics.recovery.ordered_steps[0].id, "fill_screening_config");
|
|
509
|
+
}
|
|
510
|
+
|
|
356
511
|
async function main() {
|
|
357
512
|
await testNeedConfirmationGate();
|
|
358
513
|
await testNeedSchoolTagConfirmationGate();
|
|
@@ -364,6 +519,10 @@ async function main() {
|
|
|
364
519
|
await testLoginRequiredShouldReturnGuidance();
|
|
365
520
|
await testDebugPortUnreachableShouldReturnConnectionCode();
|
|
366
521
|
await testPreflightRecoveryPlanOrder();
|
|
522
|
+
await testPreflightAutoRepairCanUnblockPipeline();
|
|
523
|
+
await testPreflightAutoRepairStillFailShouldExposeDiagnostics();
|
|
524
|
+
await testScreenConfigFailureShouldRequireUserProvidedConfig();
|
|
525
|
+
await testScreenConfigRecoveryStepShouldBeFirst();
|
|
367
526
|
console.log("pipeline tests passed");
|
|
368
527
|
}
|
|
369
528
|
|