@reconcrap/boss-recommend-mcp 1.3.34 → 1.3.35
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 +12 -7
- package/package.json +1 -1
- package/src/boss-chat.js +265 -64
- package/src/cli.js +34 -24
- package/src/test-boss-chat.js +213 -84
- package/vendor/boss-chat-cli/src/cli.js +45 -30
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Boss 推荐页自动化流水线 MCP(stdio)服务。
|
|
|
16
16
|
|
|
17
17
|
- 单独运行 chat-only 任务,不需要再单独安装 `boss-chat`
|
|
18
18
|
- 在 recommend screen 完成后,通过同一个父 run 自动进入 `chat_followup`
|
|
19
|
-
-
|
|
19
|
+
- 继续把聊天页状态保存在用户目录下的 `~/.boss-recommend-mcp/boss-chat/`(可通过 `BOSS_CHAT_HOME` 覆盖)
|
|
20
20
|
|
|
21
21
|
MCP 工具:
|
|
22
22
|
|
|
@@ -68,7 +68,7 @@ MCP 工具:
|
|
|
68
68
|
- 在真正开始 search/screen 前,会进行最后一轮全参数总确认(岗位 + 全部筛选参数 + criteria + target_count + post_action + max_greet_count)
|
|
69
69
|
- npm 全局安装后会自动执行 install:生成 skill、导出 MCP 模板,并自动尝试写入已检测到的外部 agent MCP 配置(含 Trae / trae-cn / Cursor / Claude / OpenClaw)
|
|
70
70
|
- npm / npx 安装后会自动初始化 `screening-config.json` 模板(优先写入 workspace 的 `config/`,不可写时回退到用户目录)
|
|
71
|
-
- npm 安装流程会预创建运行目录(跨平台):`~/.boss-recommend-mcp`、`~/.boss-recommend-mcp/runs
|
|
71
|
+
- npm 安装流程会预创建运行目录(跨平台):`~/.boss-recommend-mcp`、`~/.boss-recommend-mcp/runs`、`~/.boss-recommend-mcp/boss-chat` 及其 `logs/runs/profiles/reports/artifacts/state`
|
|
72
72
|
- `post_action` 必须在每次完整运行开始时确认一次
|
|
73
73
|
- `target_count` 会在每次运行开始时询问一次(可留空,不设上限)
|
|
74
74
|
- 当 `post_action=greet` 时,必须在运行开始时确认 `max_greet_count`
|
|
@@ -111,6 +111,7 @@ node src/cli.js start
|
|
|
111
111
|
|
|
112
112
|
```bash
|
|
113
113
|
BOSS_RECOMMEND_HOME # 统一状态目录,默认 ~/.boss-recommend-mcp
|
|
114
|
+
BOSS_CHAT_HOME # 覆盖 boss-chat 运行态目录;默认 ~/.boss-recommend-mcp/boss-chat
|
|
114
115
|
BOSS_RECOMMEND_SCREEN_CONFIG # 显式指定 screening-config.json 路径(最高优先级)
|
|
115
116
|
BOSS_RECOMMEND_MCP_CONFIG_TARGETS # JSON 数组或系统 path 分隔路径列表,指定额外 mcp.json 目标文件
|
|
116
117
|
BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS # JSON 数组或系统 path 分隔路径列表,指定额外 skills 根目录
|
|
@@ -149,6 +150,8 @@ node src/cli.js run --instruction "推荐页筛选985男生,近14天没有,
|
|
|
149
150
|
- `doctor` / `run` 默认优先读取 `~/.boss-recommend-mcp/screening-config.json`;如需强制其它路径,请设置 `BOSS_RECOMMEND_SCREEN_CONFIG`
|
|
150
151
|
- 首次运行时,若仍检测到默认占位词(如 `replace-with-openai-api-key`),pipeline 会返回配置目录并要求用户修改后确认“已修改完成”再继续
|
|
151
152
|
- 在 `npx` 临时目录(如 `AppData\\Local\\npm-cache\\_npx\\...`)执行时,不会再把该临时目录当作 `screening-config.json` 目标路径
|
|
153
|
+
- `boss-chat` 运行态默认固定写入 `~/.boss-recommend-mcp/boss-chat`;即使宿主把 MCP 进程启动在系统根目录,也不会再尝试写入 `/.boss-chat`
|
|
154
|
+
- 若当前工作区存在历史 `.boss-chat` 且新用户目录尚未初始化,首次运行会自动把 `logs/runs/profiles/reports/artifacts/state` 迁移到新目录,并保留旧目录作为只读历史来源
|
|
152
155
|
|
|
153
156
|
配置样例见:
|
|
154
157
|
|
|
@@ -217,6 +220,7 @@ node src/cli.js chat run --job "算法工程师" --start-from unread --criteria
|
|
|
217
220
|
- `baseUrl` / `apiKey` / `model` 不再单独传入,固定复用 recommend 的 `screening-config.json`
|
|
218
221
|
- 若缺少 `follow_up.chat` 必填项,pipeline 会返回 `NEED_INPUT`
|
|
219
222
|
- recommend 成功后,父 run 继续存活并进入 `chat_followup`;chat 结束后父 run 才会进入最终终态
|
|
223
|
+
- `boss-chat` 子任务状态统一写入 `~/.boss-recommend-mcp/boss-chat`(或 `BOSS_CHAT_HOME` 指定目录),不再依赖工作区 `cwd`
|
|
220
224
|
|
|
221
225
|
## Chat-only
|
|
222
226
|
|
|
@@ -230,11 +234,12 @@ node src/cli.js chat run --job "算法工程师" --start-from unread --criteria
|
|
|
230
234
|
- MCP:
|
|
231
235
|
- `boss_chat_health_check`
|
|
232
236
|
- `prepare_boss_chat_run`
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
237
|
+
- `start_boss_chat_run`
|
|
238
|
+
- `get_boss_chat_run`
|
|
239
|
+
- `pause_boss_chat_run`
|
|
240
|
+
- `resume_boss_chat_run`
|
|
241
|
+
- `cancel_boss_chat_run`
|
|
242
|
+
- vendored `boss-chat` CLI 还支持 `--data-dir <path>` 与 `BOSS_CHAT_HOME`,优先级高于兼容旧行为的 `<cwd>/.boss-chat`
|
|
238
243
|
|
|
239
244
|
chat-only 交互建议:
|
|
240
245
|
|
package/package.json
CHANGED
package/src/boss-chat.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
|
|
7
8
|
import { getScreenConfigResolution, resolveSharedLlmTransportConfig } from "./adapters.js";
|
|
8
9
|
|
|
@@ -11,12 +12,14 @@ const packageRoot = path.resolve(path.dirname(currentFilePath), "..");
|
|
|
11
12
|
const VENDORED_BOSS_CHAT_DIR = path.join(packageRoot, "vendor", "boss-chat-cli");
|
|
12
13
|
const DEFAULT_BOSS_CHAT_POLL_MS = 1500;
|
|
13
14
|
const PREPARE_BOSS_CHAT_MAX_ATTEMPTS = 3;
|
|
14
|
-
const PREPARE_BOSS_CHAT_RETRY_DELAY_MS = 1200;
|
|
15
|
-
const BOSS_CHAT_TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
|
|
16
|
-
const CHAT_REQUIRED_FIELDS = ["job", "start_from", "target_count", "criteria"];
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
15
|
+
const PREPARE_BOSS_CHAT_RETRY_DELAY_MS = 1200;
|
|
16
|
+
const BOSS_CHAT_TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
|
|
17
|
+
const CHAT_REQUIRED_FIELDS = ["job", "start_from", "target_count", "criteria"];
|
|
18
|
+
const BOSS_CHAT_RUNTIME_SUBDIR = "boss-chat";
|
|
19
|
+
const BOSS_CHAT_RUNTIME_CHILD_DIRS = ["logs", "runs", "profiles", "reports", "artifacts", "state"];
|
|
20
|
+
export const TARGET_COUNT_CANONICAL_ALL = "all";
|
|
21
|
+
export const TARGET_COUNT_ACCEPTED_EXAMPLES = [TARGET_COUNT_CANONICAL_ALL, -1, 20, "全部候选人"];
|
|
22
|
+
const TARGET_COUNT_WRAPPER_KEYS = ["target_count", "targetCount", "value", "count", "limit"];
|
|
20
23
|
const LLM_THINKING_LEVEL_FIELDS = [
|
|
21
24
|
"llmThinkingLevel",
|
|
22
25
|
"thinkingLevel",
|
|
@@ -28,13 +31,173 @@ function normalizeText(value) {
|
|
|
28
31
|
return String(value || "").replace(/\s+/g, " ").trim();
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
function pathExists(targetPath) {
|
|
32
|
-
try {
|
|
33
|
-
return fs.existsSync(targetPath);
|
|
34
|
+
function pathExists(targetPath) {
|
|
35
|
+
try {
|
|
36
|
+
return fs.existsSync(targetPath);
|
|
34
37
|
} catch {
|
|
35
38
|
return false;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getStateHome() {
|
|
43
|
+
return process.env.BOSS_RECOMMEND_HOME
|
|
44
|
+
? path.resolve(process.env.BOSS_RECOMMEND_HOME)
|
|
45
|
+
: path.join(os.homedir(), ".boss-recommend-mcp");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isRootDirectory(targetPath) {
|
|
49
|
+
const resolved = path.resolve(String(targetPath || ""));
|
|
50
|
+
const parsed = path.parse(resolved);
|
|
51
|
+
return resolved.toLowerCase() === String(parsed.root || "").toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isSystemDirectoryWorkspaceRoot(workspaceRoot) {
|
|
55
|
+
const root = path.resolve(String(workspaceRoot || ""));
|
|
56
|
+
const normalized = root.replace(/\\/g, "/").toLowerCase();
|
|
57
|
+
if (process.platform === "win32") {
|
|
58
|
+
return (
|
|
59
|
+
normalized.endsWith("/windows")
|
|
60
|
+
|| normalized.endsWith("/windows/system32")
|
|
61
|
+
|| normalized.endsWith("/windows/syswow64")
|
|
62
|
+
|| normalized.endsWith("/program files")
|
|
63
|
+
|| normalized.endsWith("/program files (x86)")
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return (
|
|
67
|
+
normalized === "/system"
|
|
68
|
+
|| normalized.startsWith("/system/")
|
|
69
|
+
|| normalized === "/usr"
|
|
70
|
+
|| normalized.startsWith("/usr/")
|
|
71
|
+
|| normalized === "/bin"
|
|
72
|
+
|| normalized.startsWith("/bin/")
|
|
73
|
+
|| normalized === "/sbin"
|
|
74
|
+
|| normalized.startsWith("/sbin/")
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isEphemeralWorkspaceRoot(workspaceRoot) {
|
|
79
|
+
const normalized = path.resolve(String(workspaceRoot || ""))
|
|
80
|
+
.replace(/\\/g, "/")
|
|
81
|
+
.toLowerCase();
|
|
82
|
+
return (
|
|
83
|
+
normalized.includes("/appdata/local/npm-cache/_npx/")
|
|
84
|
+
|| normalized.includes("/node_modules/@reconcrap/boss-recommend-mcp")
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isSafeBossChatLegacyWorkspaceRoot(workspaceRoot) {
|
|
89
|
+
const root = path.resolve(String(workspaceRoot || ""));
|
|
90
|
+
if (!root) return false;
|
|
91
|
+
const home = path.resolve(os.homedir());
|
|
92
|
+
return !(
|
|
93
|
+
isEphemeralWorkspaceRoot(root)
|
|
94
|
+
|| isRootDirectory(root)
|
|
95
|
+
|| root.toLowerCase() === home.toLowerCase()
|
|
96
|
+
|| isSystemDirectoryWorkspaceRoot(root)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getBossChatDataDir() {
|
|
101
|
+
if (process.env.BOSS_CHAT_HOME) {
|
|
102
|
+
return path.resolve(process.env.BOSS_CHAT_HOME);
|
|
103
|
+
}
|
|
104
|
+
return path.join(getStateHome(), BOSS_CHAT_RUNTIME_SUBDIR);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getLegacyBossChatWorkspaceDataDir(workspaceRoot) {
|
|
108
|
+
if (!isSafeBossChatLegacyWorkspaceRoot(workspaceRoot)) return null;
|
|
109
|
+
return path.join(path.resolve(String(workspaceRoot)), ".boss-chat");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function resolveBossChatRuntimeLayout(workspaceRoot) {
|
|
113
|
+
const dataDir = getBossChatDataDir();
|
|
114
|
+
const legacyWorkspaceDir = getLegacyBossChatWorkspaceDataDir(workspaceRoot);
|
|
115
|
+
const migrationSourceDir =
|
|
116
|
+
legacyWorkspaceDir && pathExists(legacyWorkspaceDir) && !pathExists(dataDir)
|
|
117
|
+
? legacyWorkspaceDir
|
|
118
|
+
: null;
|
|
119
|
+
return {
|
|
120
|
+
workspace_root: workspaceRoot ? path.resolve(String(workspaceRoot)) : null,
|
|
121
|
+
data_dir: dataDir,
|
|
122
|
+
legacy_workspace_dir: legacyWorkspaceDir,
|
|
123
|
+
migration_source_dir: migrationSourceDir,
|
|
124
|
+
migration_pending: Boolean(migrationSourceDir),
|
|
125
|
+
directories: [
|
|
126
|
+
dataDir,
|
|
127
|
+
...BOSS_CHAT_RUNTIME_CHILD_DIRS.map((name) => path.join(dataDir, name))
|
|
128
|
+
]
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function ensureBossChatRuntimeReady(workspaceRoot) {
|
|
133
|
+
const runtime = resolveBossChatRuntimeLayout(workspaceRoot);
|
|
134
|
+
const created = [];
|
|
135
|
+
const existed = [];
|
|
136
|
+
const failed = [];
|
|
137
|
+
let migration = {
|
|
138
|
+
attempted: false,
|
|
139
|
+
performed: false,
|
|
140
|
+
source: runtime.migration_source_dir,
|
|
141
|
+
target: runtime.data_dir,
|
|
142
|
+
message: runtime.migration_source_dir
|
|
143
|
+
? `Pending legacy boss-chat migration from ${runtime.migration_source_dir}`
|
|
144
|
+
: ""
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (runtime.migration_source_dir) {
|
|
148
|
+
try {
|
|
149
|
+
fs.cpSync(runtime.migration_source_dir, runtime.data_dir, {
|
|
150
|
+
recursive: true,
|
|
151
|
+
force: false,
|
|
152
|
+
errorOnExist: false
|
|
153
|
+
});
|
|
154
|
+
migration = {
|
|
155
|
+
attempted: true,
|
|
156
|
+
performed: true,
|
|
157
|
+
source: runtime.migration_source_dir,
|
|
158
|
+
target: runtime.data_dir,
|
|
159
|
+
message: `Migrated legacy boss-chat runtime from ${runtime.migration_source_dir} to ${runtime.data_dir}. Legacy source was preserved.`
|
|
160
|
+
};
|
|
161
|
+
} catch (error) {
|
|
162
|
+
migration = {
|
|
163
|
+
attempted: true,
|
|
164
|
+
performed: false,
|
|
165
|
+
source: runtime.migration_source_dir,
|
|
166
|
+
target: runtime.data_dir,
|
|
167
|
+
message: error?.message || "Legacy boss-chat migration failed."
|
|
168
|
+
};
|
|
169
|
+
failed.push({
|
|
170
|
+
path: runtime.data_dir,
|
|
171
|
+
message: `Legacy migration failed: ${migration.message}`
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const directory of runtime.directories) {
|
|
177
|
+
try {
|
|
178
|
+
const existedBefore = pathExists(directory);
|
|
179
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
180
|
+
if (existedBefore) {
|
|
181
|
+
existed.push(directory);
|
|
182
|
+
} else {
|
|
183
|
+
created.push(directory);
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
failed.push({
|
|
187
|
+
path: directory,
|
|
188
|
+
message: error?.message || String(error)
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
...runtime,
|
|
195
|
+
created,
|
|
196
|
+
existed,
|
|
197
|
+
failed,
|
|
198
|
+
migration
|
|
199
|
+
};
|
|
200
|
+
}
|
|
38
201
|
|
|
39
202
|
function parsePositiveInteger(value, fallback = null) {
|
|
40
203
|
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
@@ -469,9 +632,12 @@ function buildTargetCountNeedInputDiagnostics(input = {}, missingFields = []) {
|
|
|
469
632
|
};
|
|
470
633
|
}
|
|
471
634
|
|
|
472
|
-
function buildBossChatCliArgs(command, input, resolvedConfig) {
|
|
473
|
-
const args = [command, "--json"];
|
|
474
|
-
if (
|
|
635
|
+
function buildBossChatCliArgs(command, input, resolvedConfig, runtimeLayout = null) {
|
|
636
|
+
const args = [command, "--json"];
|
|
637
|
+
if (runtimeLayout?.data_dir) {
|
|
638
|
+
args.push("--data-dir", runtimeLayout.data_dir);
|
|
639
|
+
}
|
|
640
|
+
if (command === "prepare-run") {
|
|
475
641
|
const normalized = normalizeBossChatStartInput(input);
|
|
476
642
|
args.push("--profile", normalized.profile);
|
|
477
643
|
if (normalized.job) args.push("--job", normalized.job);
|
|
@@ -535,9 +701,9 @@ function buildBossChatCliArgs(command, input, resolvedConfig) {
|
|
|
535
701
|
return args;
|
|
536
702
|
}
|
|
537
703
|
|
|
538
|
-
async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
|
|
539
|
-
const cliPath = resolveBossChatCliPath(workspaceRoot);
|
|
540
|
-
if (!cliPath) {
|
|
704
|
+
async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
|
|
705
|
+
const cliPath = resolveBossChatCliPath(workspaceRoot);
|
|
706
|
+
if (!cliPath) {
|
|
541
707
|
return {
|
|
542
708
|
ok: false,
|
|
543
709
|
exitCode: -1,
|
|
@@ -549,12 +715,36 @@ async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
|
|
|
549
715
|
code: "BOSS_CHAT_CLI_MISSING",
|
|
550
716
|
message: "未找到 vendored boss-chat CLI。"
|
|
551
717
|
}
|
|
552
|
-
}
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const runtimeLayout = ensureBossChatRuntimeReady(workspaceRoot);
|
|
723
|
+
const runtimeInitFailed = runtimeLayout.failed.some((item) => item.path === runtimeLayout.data_dir)
|
|
724
|
+
&& !pathExists(runtimeLayout.data_dir);
|
|
725
|
+
if (runtimeInitFailed) {
|
|
726
|
+
return {
|
|
727
|
+
ok: false,
|
|
728
|
+
exitCode: 1,
|
|
729
|
+
stdout: "",
|
|
730
|
+
stderr: "",
|
|
731
|
+
payload: {
|
|
732
|
+
status: "FAILED",
|
|
733
|
+
error: {
|
|
734
|
+
code: "BOSS_CHAT_RUNTIME_INIT_FAILED",
|
|
735
|
+
message: runtimeLayout.failed
|
|
736
|
+
.filter((item) => item.path === runtimeLayout.data_dir)
|
|
737
|
+
.map((item) => item.message)
|
|
738
|
+
.join("; ") || "无法初始化 boss-chat runtime 目录。"
|
|
739
|
+
},
|
|
740
|
+
data_dir: runtimeLayout.data_dir,
|
|
741
|
+
migration: runtimeLayout.migration
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
let configResolution = null;
|
|
747
|
+
if (command === "start-run" || command === "prepare-run") {
|
|
558
748
|
configResolution = resolveBossChatScreenConfig(workspaceRoot);
|
|
559
749
|
if (!configResolution.ok) {
|
|
560
750
|
return {
|
|
@@ -568,13 +758,13 @@ async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
|
|
|
568
758
|
config_path: configResolution.config_path,
|
|
569
759
|
config_dir: configResolution.config_dir
|
|
570
760
|
}
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
const args = [cliPath, ...buildBossChatCliArgs(command, input, configResolution?.config || {})];
|
|
576
|
-
const cwd = path.resolve(String(workspaceRoot || process.cwd()));
|
|
577
|
-
return new Promise((resolve) => {
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const args = [cliPath, ...buildBossChatCliArgs(command, input, configResolution?.config || {}, runtimeLayout)];
|
|
766
|
+
const cwd = path.resolve(String(workspaceRoot || process.cwd()));
|
|
767
|
+
return new Promise((resolve) => {
|
|
578
768
|
const child = spawn(process.execPath, args, {
|
|
579
769
|
cwd,
|
|
580
770
|
env: process.env,
|
|
@@ -639,41 +829,52 @@ async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
|
|
|
639
829
|
});
|
|
640
830
|
}
|
|
641
831
|
|
|
642
|
-
export function getBossChatHealthCheck(workspaceRoot, input = {}) {
|
|
643
|
-
const cliDir = resolveBossChatCliDir(workspaceRoot);
|
|
644
|
-
const cliPath = resolveBossChatCliPath(workspaceRoot);
|
|
645
|
-
const configResolution = resolveBossChatScreenConfig(workspaceRoot);
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
832
|
+
export function getBossChatHealthCheck(workspaceRoot, input = {}) {
|
|
833
|
+
const cliDir = resolveBossChatCliDir(workspaceRoot);
|
|
834
|
+
const cliPath = resolveBossChatCliPath(workspaceRoot);
|
|
835
|
+
const configResolution = resolveBossChatScreenConfig(workspaceRoot);
|
|
836
|
+
const runtimeLayout = resolveBossChatRuntimeLayout(workspaceRoot);
|
|
837
|
+
const resolvedPort = parsePositiveInteger(input.port)
|
|
838
|
+
|| (configResolution.ok ? configResolution.config.debugPort : 9222);
|
|
839
|
+
if (!cliDir || !cliPath) {
|
|
649
840
|
return {
|
|
650
841
|
status: "FAILED",
|
|
651
|
-
error: {
|
|
652
|
-
code: "BOSS_CHAT_CLI_MISSING",
|
|
653
|
-
message: "未找到 vendored boss-chat CLI。"
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
842
|
+
error: {
|
|
843
|
+
code: "BOSS_CHAT_CLI_MISSING",
|
|
844
|
+
message: "未找到 vendored boss-chat CLI。"
|
|
845
|
+
},
|
|
846
|
+
data_dir: runtimeLayout.data_dir,
|
|
847
|
+
legacy_workspace_dir: runtimeLayout.legacy_workspace_dir,
|
|
848
|
+
migration_pending: runtimeLayout.migration_pending
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
if (!configResolution.ok) {
|
|
658
852
|
return {
|
|
659
853
|
status: "FAILED",
|
|
660
|
-
error: configResolution.error,
|
|
661
|
-
config_path: configResolution.config_path,
|
|
662
|
-
config_dir: configResolution.config_dir,
|
|
663
|
-
cli_dir: cliDir,
|
|
664
|
-
cli_path: cliPath
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
854
|
+
error: configResolution.error,
|
|
855
|
+
config_path: configResolution.config_path,
|
|
856
|
+
config_dir: configResolution.config_dir,
|
|
857
|
+
cli_dir: cliDir,
|
|
858
|
+
cli_path: cliPath,
|
|
859
|
+
data_dir: runtimeLayout.data_dir,
|
|
860
|
+
legacy_workspace_dir: runtimeLayout.legacy_workspace_dir,
|
|
861
|
+
migration_pending: runtimeLayout.migration_pending
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
return {
|
|
668
865
|
status: "OK",
|
|
669
866
|
server: "boss-chat",
|
|
670
|
-
cli_dir: cliDir,
|
|
671
|
-
cli_path: cliPath,
|
|
672
|
-
config_path: configResolution.config_path,
|
|
673
|
-
debug_port: resolvedPort,
|
|
674
|
-
shared_llm_config: true
|
|
675
|
-
|
|
676
|
-
|
|
867
|
+
cli_dir: cliDir,
|
|
868
|
+
cli_path: cliPath,
|
|
869
|
+
config_path: configResolution.config_path,
|
|
870
|
+
debug_port: resolvedPort,
|
|
871
|
+
shared_llm_config: true,
|
|
872
|
+
data_dir: runtimeLayout.data_dir,
|
|
873
|
+
legacy_workspace_dir: runtimeLayout.legacy_workspace_dir,
|
|
874
|
+
migration_source_dir: runtimeLayout.migration_source_dir,
|
|
875
|
+
migration_pending: runtimeLayout.migration_pending
|
|
876
|
+
};
|
|
877
|
+
}
|
|
677
878
|
|
|
678
879
|
export async function startBossChatRun({ workspaceRoot, input = {} }) {
|
|
679
880
|
const missingFields = getMissingBossChatStartFields(input);
|
package/src/cli.js
CHANGED
|
@@ -18,10 +18,12 @@ import {
|
|
|
18
18
|
} from "./adapters.js";
|
|
19
19
|
import {
|
|
20
20
|
cancelBossChatRun,
|
|
21
|
+
ensureBossChatRuntimeReady,
|
|
21
22
|
getBossChatHealthCheck,
|
|
22
23
|
getBossChatRun,
|
|
23
24
|
pauseBossChatRun,
|
|
24
25
|
prepareBossChatRun,
|
|
26
|
+
resolveBossChatRuntimeLayout,
|
|
25
27
|
resumeBossChatRun,
|
|
26
28
|
startBossChatRun
|
|
27
29
|
} from "./boss-chat.js";
|
|
@@ -742,30 +744,22 @@ function ensureUserConfig(options = {}) {
|
|
|
742
744
|
throw lastError || new Error("No writable target for screening-config.json");
|
|
743
745
|
}
|
|
744
746
|
|
|
745
|
-
function getBossChatDataDir(workspaceRoot) {
|
|
746
|
-
return path.join(path.resolve(String(workspaceRoot || process.cwd())), ".boss-chat");
|
|
747
|
-
}
|
|
748
|
-
|
|
749
747
|
function collectRuntimeDirectories(options = {}) {
|
|
750
748
|
const workspaceRoot = getWorkspaceRoot(options);
|
|
751
749
|
const stateHome = getStateHome();
|
|
752
|
-
const
|
|
750
|
+
const runtime = resolveBossChatRuntimeLayout(workspaceRoot);
|
|
751
|
+
const bossChatRoot = runtime.data_dir;
|
|
753
752
|
const recommendRuntimeDirs = [
|
|
754
753
|
stateHome,
|
|
755
754
|
path.join(stateHome, "runs")
|
|
756
755
|
];
|
|
757
|
-
const bossChatRuntimeDirs = [
|
|
758
|
-
bossChatRoot,
|
|
759
|
-
path.join(bossChatRoot, "logs"),
|
|
760
|
-
path.join(bossChatRoot, "runs"),
|
|
761
|
-
path.join(bossChatRoot, "profiles"),
|
|
762
|
-
path.join(bossChatRoot, "reports"),
|
|
763
|
-
path.join(bossChatRoot, "artifacts")
|
|
764
|
-
];
|
|
756
|
+
const bossChatRuntimeDirs = runtime.directories || [bossChatRoot];
|
|
765
757
|
return {
|
|
766
758
|
workspaceRoot,
|
|
767
759
|
stateHome,
|
|
768
760
|
bossChatRoot,
|
|
761
|
+
legacyBossChatRoot: runtime.legacy_workspace_dir,
|
|
762
|
+
migrationPending: runtime.migration_pending,
|
|
769
763
|
directories: dedupePaths([
|
|
770
764
|
...recommendRuntimeDirs,
|
|
771
765
|
...bossChatRuntimeDirs
|
|
@@ -774,19 +768,20 @@ function collectRuntimeDirectories(options = {}) {
|
|
|
774
768
|
}
|
|
775
769
|
|
|
776
770
|
function ensureRuntimeDirectories(options = {}) {
|
|
777
|
-
const { workspaceRoot, stateHome
|
|
778
|
-
const
|
|
779
|
-
const
|
|
780
|
-
const
|
|
771
|
+
const { workspaceRoot, stateHome } = collectRuntimeDirectories(options);
|
|
772
|
+
const runtime = ensureBossChatRuntimeReady(workspaceRoot);
|
|
773
|
+
const recommendCreated = [];
|
|
774
|
+
const recommendExisted = [];
|
|
775
|
+
const failed = [...runtime.failed];
|
|
781
776
|
|
|
782
|
-
for (const directory of
|
|
777
|
+
for (const directory of [stateHome, path.join(stateHome, "runs")]) {
|
|
783
778
|
try {
|
|
784
779
|
const existedBefore = fs.existsSync(directory);
|
|
785
780
|
ensureDir(directory);
|
|
786
781
|
if (existedBefore) {
|
|
787
|
-
|
|
782
|
+
recommendExisted.push(directory);
|
|
788
783
|
} else {
|
|
789
|
-
|
|
784
|
+
recommendCreated.push(directory);
|
|
790
785
|
}
|
|
791
786
|
} catch (error) {
|
|
792
787
|
failed.push({
|
|
@@ -799,9 +794,12 @@ function ensureRuntimeDirectories(options = {}) {
|
|
|
799
794
|
return {
|
|
800
795
|
workspaceRoot,
|
|
801
796
|
stateHome,
|
|
802
|
-
bossChatRoot,
|
|
803
|
-
|
|
804
|
-
|
|
797
|
+
bossChatRoot: runtime.data_dir,
|
|
798
|
+
legacyBossChatRoot: runtime.legacy_workspace_dir,
|
|
799
|
+
migrationPending: runtime.migration_pending,
|
|
800
|
+
migration: runtime.migration,
|
|
801
|
+
created: dedupePaths([...recommendCreated, ...runtime.created]),
|
|
802
|
+
existed: dedupePaths([...recommendExisted, ...runtime.existed]),
|
|
805
803
|
failed
|
|
806
804
|
};
|
|
807
805
|
}
|
|
@@ -1343,10 +1341,13 @@ function printPaths() {
|
|
|
1343
1341
|
const codexHome = getCodexHome();
|
|
1344
1342
|
const stateHome = getStateHome();
|
|
1345
1343
|
const calibrationResolution = getFeaturedCalibrationResolution(process.cwd());
|
|
1344
|
+
const bossChatRuntime = resolveBossChatRuntimeLayout(getWorkspaceRoot({}));
|
|
1346
1345
|
console.log(`package_root=${packageRoot}`);
|
|
1347
1346
|
console.log(`skill_sources=${bundledSkillNames.map((name) => getSkillSourceDir(name)).join(" | ")}`);
|
|
1348
1347
|
console.log(`codex_home=${codexHome}`);
|
|
1349
1348
|
console.log(`state_home=${stateHome}`);
|
|
1349
|
+
console.log(`boss_chat_runtime=${bossChatRuntime.data_dir}`);
|
|
1350
|
+
console.log(`boss_chat_legacy_workspace_runtime=${bossChatRuntime.legacy_workspace_dir || ""}`);
|
|
1350
1351
|
console.log(`skill_targets=${bundledSkillNames.map((name) => path.join(codexHome, "skills", name)).join(" | ")}`);
|
|
1351
1352
|
console.log(`config_target=${getUserConfigPath()}`);
|
|
1352
1353
|
console.log(`legacy_config_target=${getLegacyUserConfigPath()}`);
|
|
@@ -1408,6 +1409,9 @@ function installAll(options = {}) {
|
|
|
1408
1409
|
);
|
|
1409
1410
|
console.log(`- recommend runtime: ${runtimeDirsResult.stateHome}`);
|
|
1410
1411
|
console.log(`- boss-chat runtime: ${runtimeDirsResult.bossChatRoot}`);
|
|
1412
|
+
if (runtimeDirsResult.migration?.performed) {
|
|
1413
|
+
console.log(`- boss-chat migration: ${runtimeDirsResult.migration.message}`);
|
|
1414
|
+
}
|
|
1411
1415
|
if (runtimeDirsResult.failed.length > 0) {
|
|
1412
1416
|
for (const item of runtimeDirsResult.failed) {
|
|
1413
1417
|
console.warn(`Runtime dir warning: ${item.path} -> ${item.message}`);
|
|
@@ -1628,6 +1632,9 @@ export async function runCli(argv = process.argv) {
|
|
|
1628
1632
|
);
|
|
1629
1633
|
console.log(`- recommend runtime: ${runtimeDirsResult.stateHome}`);
|
|
1630
1634
|
console.log(`- boss-chat runtime: ${runtimeDirsResult.bossChatRoot}`);
|
|
1635
|
+
if (runtimeDirsResult.migration?.performed) {
|
|
1636
|
+
console.log(`- boss-chat migration: ${runtimeDirsResult.migration.message}`);
|
|
1637
|
+
}
|
|
1631
1638
|
if (runtimeDirsResult.failed.length > 0) {
|
|
1632
1639
|
for (const item of runtimeDirsResult.failed) {
|
|
1633
1640
|
console.warn(`Runtime dir warning: ${item.path} -> ${item.message}`);
|
|
@@ -1716,12 +1723,15 @@ export const __testables = {
|
|
|
1716
1723
|
buildBossChatCliInput,
|
|
1717
1724
|
buildDefaultMcpArgs,
|
|
1718
1725
|
buildMcpLaunchConfig,
|
|
1726
|
+
collectRuntimeDirectories,
|
|
1727
|
+
ensureBossChatRuntimeReady,
|
|
1728
|
+
ensureRuntimeDirectories,
|
|
1719
1729
|
getBossChatCliRunTarget,
|
|
1720
1730
|
getDefaultMcpPackageSpecifier,
|
|
1721
1731
|
getRunFollowUp,
|
|
1722
1732
|
installSkill,
|
|
1723
1733
|
isInstalledPackageRoot,
|
|
1724
|
-
|
|
1734
|
+
resolveBossChatRuntimeLayout,
|
|
1725
1735
|
runBossChatCliCommand,
|
|
1726
1736
|
runPipelineOnce
|
|
1727
1737
|
};
|
package/src/test-boss-chat.js
CHANGED
|
@@ -4,16 +4,18 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { mkdir } from "node:fs/promises";
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
cancelBossChatRun,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
import {
|
|
8
|
+
cancelBossChatRun,
|
|
9
|
+
ensureBossChatRuntimeReady,
|
|
10
|
+
getBossChatHealthCheck,
|
|
11
|
+
getBossChatRun,
|
|
12
|
+
pauseBossChatRun,
|
|
13
|
+
prepareBossChatRun,
|
|
14
|
+
resolveBossChatRuntimeLayout,
|
|
15
|
+
resumeBossChatRun,
|
|
16
|
+
startBossChatRun
|
|
17
|
+
} from "./boss-chat.js";
|
|
18
|
+
import { __testables as cliTestables, runCli } from "./cli.js";
|
|
17
19
|
import { __testables as indexTestables } from "./index.js";
|
|
18
20
|
import { BossChatApp } from "../vendor/boss-chat-cli/src/app.js";
|
|
19
21
|
import { __testables as vendorCliTestables } from "../vendor/boss-chat-cli/src/cli.js";
|
|
@@ -56,8 +58,8 @@ async function callTool(workspaceRoot, name, args = {}, id = 1) {
|
|
|
56
58
|
return response?.result?.structuredContent;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
function createBossChatTestWorkspace() {
|
|
60
|
-
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-boss-chat-"));
|
|
61
|
+
function createBossChatTestWorkspace() {
|
|
62
|
+
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-boss-chat-"));
|
|
61
63
|
const configDir = path.join(workspaceRoot, "config");
|
|
62
64
|
const cliDir = path.join(workspaceRoot, "boss-chat-cli", "src");
|
|
63
65
|
fs.mkdirSync(configDir, { recursive: true });
|
|
@@ -72,26 +74,13 @@ function createBossChatTestWorkspace() {
|
|
|
72
74
|
debugPort: 9666
|
|
73
75
|
}, null, 2));
|
|
74
76
|
|
|
75
|
-
fs.writeFileSync(path.join(cliDir, "cli.js"), [
|
|
76
|
-
"#!/usr/bin/env node",
|
|
77
|
-
"const fs = require('node:fs');",
|
|
78
|
-
"const path = require('node:path');",
|
|
79
|
-
"const
|
|
80
|
-
"const
|
|
81
|
-
"
|
|
82
|
-
"const raw = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf8') : '{}';",
|
|
83
|
-
"const state = JSON.parse(raw || '{}');",
|
|
84
|
-
"state.counter = Number.isInteger(state.counter) ? state.counter : 0;",
|
|
85
|
-
"state.prepare_calls = Number.isInteger(state.prepare_calls) ? state.prepare_calls : 0;",
|
|
86
|
-
"if (!Number.isInteger(state.prepare_fail_budget)) {",
|
|
87
|
-
" const configured = Number.parseInt(process.env.BOSS_CHAT_STUB_PREPARE_FAILS || '0', 10);",
|
|
88
|
-
" state.prepare_fail_budget = Number.isFinite(configured) && configured > 0 ? configured : 0;",
|
|
89
|
-
"}",
|
|
90
|
-
"state.runs = state.runs && typeof state.runs === 'object' ? state.runs : {};",
|
|
91
|
-
"state.get_calls = state.get_calls && typeof state.get_calls === 'object' ? state.get_calls : {};",
|
|
92
|
-
"const argv = process.argv.slice(2);",
|
|
93
|
-
"const command = String(argv[0] || '').trim();",
|
|
94
|
-
"const options = {};",
|
|
77
|
+
fs.writeFileSync(path.join(cliDir, "cli.js"), [
|
|
78
|
+
"#!/usr/bin/env node",
|
|
79
|
+
"const fs = require('node:fs');",
|
|
80
|
+
"const path = require('node:path');",
|
|
81
|
+
"const argv = process.argv.slice(2);",
|
|
82
|
+
"const command = String(argv[0] || '').trim();",
|
|
83
|
+
"const options = {};",
|
|
95
84
|
"for (let index = 1; index < argv.length; index += 1) {",
|
|
96
85
|
" const token = String(argv[index] || '');",
|
|
97
86
|
" if (!token.startsWith('--')) continue;",
|
|
@@ -102,11 +91,25 @@ function createBossChatTestWorkspace() {
|
|
|
102
91
|
" index += 1;",
|
|
103
92
|
" } else {",
|
|
104
93
|
" options[key] = true;",
|
|
105
|
-
" }",
|
|
106
|
-
"}",
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
94
|
+
" }",
|
|
95
|
+
"}",
|
|
96
|
+
"const cwd = process.cwd();",
|
|
97
|
+
"const dataDir = String(options['data-dir'] || process.env.BOSS_CHAT_HOME || path.join(cwd, '.boss-chat'));",
|
|
98
|
+
"const statePath = path.join(dataDir, 'stub-state.json');",
|
|
99
|
+
"fs.mkdirSync(path.dirname(statePath), { recursive: true });",
|
|
100
|
+
"const raw = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf8') : '{}';",
|
|
101
|
+
"const state = JSON.parse(raw || '{}');",
|
|
102
|
+
"state.counter = Number.isInteger(state.counter) ? state.counter : 0;",
|
|
103
|
+
"state.prepare_calls = Number.isInteger(state.prepare_calls) ? state.prepare_calls : 0;",
|
|
104
|
+
"if (!Number.isInteger(state.prepare_fail_budget)) {",
|
|
105
|
+
" const configured = Number.parseInt(process.env.BOSS_CHAT_STUB_PREPARE_FAILS || '0', 10);",
|
|
106
|
+
" state.prepare_fail_budget = Number.isFinite(configured) && configured > 0 ? configured : 0;",
|
|
107
|
+
"}",
|
|
108
|
+
"state.runs = state.runs && typeof state.runs === 'object' ? state.runs : {};",
|
|
109
|
+
"state.get_calls = state.get_calls && typeof state.get_calls === 'object' ? state.get_calls : {};",
|
|
110
|
+
"function saveAndPrint(payload) {",
|
|
111
|
+
" fs.writeFileSync(statePath, JSON.stringify(state, null, 2));",
|
|
112
|
+
" process.stdout.write(`${JSON.stringify(payload)}\\n`);",
|
|
110
113
|
"}",
|
|
111
114
|
"if (command === 'prepare-run') {",
|
|
112
115
|
" state.prepare_calls += 1;",
|
|
@@ -186,29 +189,40 @@ function createBossChatTestWorkspace() {
|
|
|
186
189
|
"process.exit(1);"
|
|
187
190
|
].join("\n"), "utf8");
|
|
188
191
|
|
|
189
|
-
return workspaceRoot;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
192
|
+
return workspaceRoot;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function getTestChatDataDir(workspaceRoot) {
|
|
196
|
+
return resolveBossChatRuntimeLayout(workspaceRoot).data_dir;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function readStubState(workspaceRoot) {
|
|
200
|
+
const statePath = path.join(getTestChatDataDir(workspaceRoot), "stub-state.json");
|
|
201
|
+
return JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function withBossChatWorkspace(testFn) {
|
|
205
|
+
const workspaceRoot = createBossChatTestWorkspace();
|
|
206
|
+
const previousScreenConfig = process.env.BOSS_RECOMMEND_SCREEN_CONFIG;
|
|
207
|
+
const previousBossChatHome = process.env.BOSS_CHAT_HOME;
|
|
208
|
+
process.env.BOSS_RECOMMEND_SCREEN_CONFIG = path.join(workspaceRoot, "config", "screening-config.json");
|
|
209
|
+
process.env.BOSS_CHAT_HOME = path.join(workspaceRoot, "user-boss-chat");
|
|
210
|
+
try {
|
|
211
|
+
await testFn(workspaceRoot);
|
|
212
|
+
} finally {
|
|
213
|
+
if (previousScreenConfig === undefined) {
|
|
214
|
+
delete process.env.BOSS_RECOMMEND_SCREEN_CONFIG;
|
|
215
|
+
} else {
|
|
216
|
+
process.env.BOSS_RECOMMEND_SCREEN_CONFIG = previousScreenConfig;
|
|
217
|
+
}
|
|
218
|
+
if (previousBossChatHome === undefined) {
|
|
219
|
+
delete process.env.BOSS_CHAT_HOME;
|
|
220
|
+
} else {
|
|
221
|
+
process.env.BOSS_CHAT_HOME = previousBossChatHome;
|
|
222
|
+
}
|
|
223
|
+
fs.rmSync(workspaceRoot, { recursive: true, force: true });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
212
226
|
|
|
213
227
|
async function captureConsoleLogs(fn) {
|
|
214
228
|
const messages = [];
|
|
@@ -224,12 +238,15 @@ async function captureConsoleLogs(fn) {
|
|
|
224
238
|
return messages;
|
|
225
239
|
}
|
|
226
240
|
|
|
227
|
-
async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
|
|
228
|
-
await withBossChatWorkspace(async (workspaceRoot) => {
|
|
229
|
-
const health = getBossChatHealthCheck(workspaceRoot);
|
|
230
|
-
assert.equal(health.status, "OK");
|
|
231
|
-
assert.equal(health.shared_llm_config, true);
|
|
232
|
-
assert.equal(health.debug_port, 9666);
|
|
241
|
+
async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
|
|
242
|
+
await withBossChatWorkspace(async (workspaceRoot) => {
|
|
243
|
+
const health = getBossChatHealthCheck(workspaceRoot);
|
|
244
|
+
assert.equal(health.status, "OK");
|
|
245
|
+
assert.equal(health.shared_llm_config, true);
|
|
246
|
+
assert.equal(health.debug_port, 9666);
|
|
247
|
+
assert.equal(health.data_dir, getTestChatDataDir(workspaceRoot));
|
|
248
|
+
assert.equal(health.legacy_workspace_dir, path.join(workspaceRoot, ".boss-chat"));
|
|
249
|
+
assert.equal(health.migration_pending, false);
|
|
233
250
|
|
|
234
251
|
const prepared = await prepareBossChatRun({
|
|
235
252
|
workspaceRoot,
|
|
@@ -259,9 +276,10 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
|
|
|
259
276
|
assert.equal(preflightTargetQuestion.recommended_argument_patch.target_count, "all");
|
|
260
277
|
assert.equal(Array.isArray(preflightTargetQuestion.options), true);
|
|
261
278
|
|
|
262
|
-
const stateAfterPrepare = readStubState(workspaceRoot);
|
|
263
|
-
assert.equal(stateAfterPrepare.last_prepare_args.profile, "default");
|
|
264
|
-
assert.equal(stateAfterPrepare.last_prepare_args
|
|
279
|
+
const stateAfterPrepare = readStubState(workspaceRoot);
|
|
280
|
+
assert.equal(stateAfterPrepare.last_prepare_args.profile, "default");
|
|
281
|
+
assert.equal(stateAfterPrepare.last_prepare_args["data-dir"], getTestChatDataDir(workspaceRoot));
|
|
282
|
+
assert.equal(stateAfterPrepare.last_prepare_args.port, "9666");
|
|
265
283
|
assert.equal(stateAfterPrepare.last_prepare_args.baseurl, "https://api.example.com/v1");
|
|
266
284
|
assert.equal(stateAfterPrepare.last_prepare_args.apikey, "sk-test-key");
|
|
267
285
|
assert.equal(stateAfterPrepare.last_prepare_args.model, "gpt-4.1-mini");
|
|
@@ -281,9 +299,10 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
|
|
|
281
299
|
assert.equal(started.status, "ACCEPTED");
|
|
282
300
|
assert.equal(Boolean(started.run_id), true);
|
|
283
301
|
|
|
284
|
-
const stateAfterStart = readStubState(workspaceRoot);
|
|
285
|
-
assert.equal(stateAfterStart.last_start_args.profile, "default");
|
|
286
|
-
assert.equal(stateAfterStart.last_start_args
|
|
302
|
+
const stateAfterStart = readStubState(workspaceRoot);
|
|
303
|
+
assert.equal(stateAfterStart.last_start_args.profile, "default");
|
|
304
|
+
assert.equal(stateAfterStart.last_start_args["data-dir"], getTestChatDataDir(workspaceRoot));
|
|
305
|
+
assert.equal(stateAfterStart.last_start_args.job, "算法工程师");
|
|
287
306
|
assert.equal(stateAfterStart.last_start_args["start-from"], "unread");
|
|
288
307
|
assert.equal(stateAfterStart.last_start_args.criteria, "有 AI Agent 经验");
|
|
289
308
|
assert.equal(stateAfterStart.last_start_args.targetCount, "2");
|
|
@@ -389,11 +408,99 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
|
|
|
389
408
|
}
|
|
390
409
|
});
|
|
391
410
|
assert.equal(canceled.run.state, "canceled");
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
async function
|
|
396
|
-
await withBossChatWorkspace(async (workspaceRoot) => {
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function testBossChatRuntimeShouldMigrateLegacyWorkspaceDataOnce() {
|
|
415
|
+
await withBossChatWorkspace(async (workspaceRoot) => {
|
|
416
|
+
const legacyDir = path.join(workspaceRoot, ".boss-chat");
|
|
417
|
+
const legacyStatePath = path.join(legacyDir, "state", "default.json");
|
|
418
|
+
const legacyRunPath = path.join(legacyDir, "runs", "legacy-run.json");
|
|
419
|
+
fs.mkdirSync(path.dirname(legacyStatePath), { recursive: true });
|
|
420
|
+
fs.mkdirSync(path.dirname(legacyRunPath), { recursive: true });
|
|
421
|
+
fs.writeFileSync(legacyStatePath, JSON.stringify({ cursor: 7 }, null, 2));
|
|
422
|
+
fs.writeFileSync(legacyRunPath, JSON.stringify({ run_id: "legacy-run" }, null, 2));
|
|
423
|
+
|
|
424
|
+
const before = resolveBossChatRuntimeLayout(workspaceRoot);
|
|
425
|
+
assert.equal(before.data_dir, getTestChatDataDir(workspaceRoot));
|
|
426
|
+
assert.equal(before.legacy_workspace_dir, legacyDir);
|
|
427
|
+
assert.equal(before.migration_source_dir, legacyDir);
|
|
428
|
+
assert.equal(before.migration_pending, true);
|
|
429
|
+
|
|
430
|
+
const ready = ensureBossChatRuntimeReady(workspaceRoot);
|
|
431
|
+
assert.equal(ready.migration.attempted, true);
|
|
432
|
+
assert.equal(ready.migration.performed, true);
|
|
433
|
+
assert.equal(fs.existsSync(path.join(ready.data_dir, "state", "default.json")), true);
|
|
434
|
+
assert.equal(fs.existsSync(path.join(ready.data_dir, "runs", "legacy-run.json")), true);
|
|
435
|
+
assert.deepEqual(
|
|
436
|
+
JSON.parse(fs.readFileSync(path.join(ready.data_dir, "state", "default.json"), "utf8")),
|
|
437
|
+
{ cursor: 7 }
|
|
438
|
+
);
|
|
439
|
+
assert.equal(fs.existsSync(legacyStatePath), true);
|
|
440
|
+
|
|
441
|
+
const after = resolveBossChatRuntimeLayout(workspaceRoot);
|
|
442
|
+
assert.equal(after.migration_pending, false);
|
|
443
|
+
assert.equal(after.migration_source_dir, null);
|
|
444
|
+
|
|
445
|
+
const secondReady = ensureBossChatRuntimeReady(workspaceRoot);
|
|
446
|
+
assert.equal(secondReady.migration.attempted, false);
|
|
447
|
+
assert.equal(secondReady.migration.performed, false);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function testBossChatRuntimeShouldResolveUserDirForRootWorkspace() {
|
|
452
|
+
const previousBossChatHome = process.env.BOSS_CHAT_HOME;
|
|
453
|
+
const previousRecommendHome = process.env.BOSS_RECOMMEND_HOME;
|
|
454
|
+
const runtimeRoot = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-root-runtime-"));
|
|
455
|
+
try {
|
|
456
|
+
delete process.env.BOSS_CHAT_HOME;
|
|
457
|
+
process.env.BOSS_RECOMMEND_HOME = runtimeRoot;
|
|
458
|
+
const rootWorkspace = path.parse(process.cwd()).root;
|
|
459
|
+
const runtime = resolveBossChatRuntimeLayout(rootWorkspace);
|
|
460
|
+
assert.equal(runtime.data_dir, path.join(runtimeRoot, "boss-chat"));
|
|
461
|
+
assert.equal(runtime.legacy_workspace_dir, null);
|
|
462
|
+
assert.equal(runtime.migration_pending, false);
|
|
463
|
+
|
|
464
|
+
const ready = ensureBossChatRuntimeReady(rootWorkspace);
|
|
465
|
+
assert.equal(fs.existsSync(ready.data_dir), true);
|
|
466
|
+
assert.equal(fs.existsSync(path.join(ready.data_dir, "runs")), true);
|
|
467
|
+
} finally {
|
|
468
|
+
if (previousBossChatHome === undefined) {
|
|
469
|
+
delete process.env.BOSS_CHAT_HOME;
|
|
470
|
+
} else {
|
|
471
|
+
process.env.BOSS_CHAT_HOME = previousBossChatHome;
|
|
472
|
+
}
|
|
473
|
+
if (previousRecommendHome === undefined) {
|
|
474
|
+
delete process.env.BOSS_RECOMMEND_HOME;
|
|
475
|
+
} else {
|
|
476
|
+
process.env.BOSS_RECOMMEND_HOME = previousRecommendHome;
|
|
477
|
+
}
|
|
478
|
+
fs.rmSync(runtimeRoot, { recursive: true, force: true });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function testBossChatWhereShouldPrintUserRuntimePath() {
|
|
483
|
+
await withBossChatWorkspace(async (workspaceRoot) => {
|
|
484
|
+
const previousWorkspaceRoot = process.env.BOSS_WORKSPACE_ROOT;
|
|
485
|
+
process.env.BOSS_WORKSPACE_ROOT = workspaceRoot;
|
|
486
|
+
try {
|
|
487
|
+
const logs = await captureConsoleLogs(async () => {
|
|
488
|
+
await runCli(["node", "src/cli.js", "where"]);
|
|
489
|
+
});
|
|
490
|
+
assert.equal(logs.some((line) => line.includes(`boss_chat_runtime=${getTestChatDataDir(workspaceRoot)}`)), true);
|
|
491
|
+
assert.equal(logs.some((line) => line.includes(`boss_chat_legacy_workspace_runtime=${path.join(workspaceRoot, ".boss-chat")}`)), true);
|
|
492
|
+
} finally {
|
|
493
|
+
if (previousWorkspaceRoot === undefined) {
|
|
494
|
+
delete process.env.BOSS_WORKSPACE_ROOT;
|
|
495
|
+
} else {
|
|
496
|
+
process.env.BOSS_WORKSPACE_ROOT = previousWorkspaceRoot;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function testBossChatPrepareShouldRetryWhenChatPageIsNotReady() {
|
|
503
|
+
await withBossChatWorkspace(async (workspaceRoot) => {
|
|
397
504
|
const previousPrepareFails = process.env.BOSS_CHAT_STUB_PREPARE_FAILS;
|
|
398
505
|
process.env.BOSS_CHAT_STUB_PREPARE_FAILS = "2";
|
|
399
506
|
try {
|
|
@@ -412,8 +519,26 @@ async function testBossChatPrepareShouldRetryWhenChatPageIsNotReady() {
|
|
|
412
519
|
process.env.BOSS_CHAT_STUB_PREPARE_FAILS = previousPrepareFails;
|
|
413
520
|
}
|
|
414
521
|
}
|
|
415
|
-
});
|
|
416
|
-
}
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function testVendorBossChatCliShouldResolveExplicitDataDir() {
|
|
526
|
+
const cwd = path.join(path.parse(process.cwd()).root, "workspace");
|
|
527
|
+
const args = vendorCliTestables.parseArgs(["start-run", "--data-dir", "/tmp/boss-chat-data"]);
|
|
528
|
+
assert.equal(args.dataDir, "/tmp/boss-chat-data");
|
|
529
|
+
assert.equal(
|
|
530
|
+
vendorCliTestables.resolveDataDir(args, { BOSS_CHAT_HOME: "/tmp/ignored" }, cwd),
|
|
531
|
+
path.resolve("/tmp/boss-chat-data")
|
|
532
|
+
);
|
|
533
|
+
assert.equal(
|
|
534
|
+
vendorCliTestables.resolveDataDir({}, { BOSS_CHAT_HOME: "/tmp/from-env" }, cwd),
|
|
535
|
+
path.resolve("/tmp/from-env")
|
|
536
|
+
);
|
|
537
|
+
assert.equal(
|
|
538
|
+
vendorCliTestables.resolveDataDir({}, {}, cwd),
|
|
539
|
+
path.join(path.resolve(cwd), ".boss-chat")
|
|
540
|
+
);
|
|
541
|
+
}
|
|
417
542
|
|
|
418
543
|
async function testBossChatPageShouldTreatBlankChatShellAsOnChatPage() {
|
|
419
544
|
const fakeChromeClient = {
|
|
@@ -2879,9 +3004,12 @@ async function testBossChatReportStoreShouldWriteReadableMarkdownAndCsv() {
|
|
|
2879
3004
|
assert.match(csvContent, /先看项目经历,再看实习时长/);
|
|
2880
3005
|
}
|
|
2881
3006
|
|
|
2882
|
-
async function main() {
|
|
2883
|
-
await testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli();
|
|
2884
|
-
await
|
|
3007
|
+
async function main() {
|
|
3008
|
+
await testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli();
|
|
3009
|
+
await testBossChatRuntimeShouldMigrateLegacyWorkspaceDataOnce();
|
|
3010
|
+
testBossChatRuntimeShouldResolveUserDirForRootWorkspace();
|
|
3011
|
+
await testBossChatWhereShouldPrintUserRuntimePath();
|
|
3012
|
+
await testBossChatPrepareShouldRetryWhenChatPageIsNotReady();
|
|
2885
3013
|
await testBossChatPageShouldTreatBlankChatShellAsOnChatPage();
|
|
2886
3014
|
await testBossChatRecoverToChatIndexShouldForceNavigateAndWaitForCompleteLoad();
|
|
2887
3015
|
await testBossChatPageShouldFallbackToEscapeWhenClosingCandidateDetail();
|
|
@@ -2889,8 +3017,9 @@ async function main() {
|
|
|
2889
3017
|
await testBossChatPageShouldWaitForPanelsClosedInStrictConversationReady();
|
|
2890
3018
|
await testBossChatPageShouldSurfaceCandidateDetailOverlayAndContentState();
|
|
2891
3019
|
await testBossChatMcpToolsShouldValidateAndRoute();
|
|
2892
|
-
await testBossChatCliShouldSupportRunAndFollowUpParsing();
|
|
2893
|
-
|
|
3020
|
+
await testBossChatCliShouldSupportRunAndFollowUpParsing();
|
|
3021
|
+
testVendorBossChatCliShouldResolveExplicitDataDir();
|
|
3022
|
+
await testVendorBossChatCliShouldWaitForHydratedChatShell();
|
|
2894
3023
|
await testVendorBossChatCliShouldRetryJobListDuringPromptRunProfile();
|
|
2895
3024
|
testCliShouldPinInstalledPackageVersionInGeneratedMcpConfig();
|
|
2896
3025
|
testVendorBossChatCliShouldParseSharedLlmTransportArgs();
|
|
@@ -178,13 +178,14 @@ function parseTargetCount(value) {
|
|
|
178
178
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
function parseArgs(argv) {
|
|
182
|
-
const args = {
|
|
183
|
-
command: 'run',
|
|
184
|
-
profile: 'default',
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
function parseArgs(argv) {
|
|
182
|
+
const args = {
|
|
183
|
+
command: 'run',
|
|
184
|
+
profile: 'default',
|
|
185
|
+
dataDir: '',
|
|
186
|
+
dryRun: false,
|
|
187
|
+
noState: false,
|
|
188
|
+
json: false,
|
|
188
189
|
runId: '',
|
|
189
190
|
detachedWorker: false,
|
|
190
191
|
overrides: {
|
|
@@ -213,13 +214,17 @@ function parseArgs(argv) {
|
|
|
213
214
|
index += 1;
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
switch (name) {
|
|
217
|
-
case 'profile':
|
|
218
|
-
args.profile = value || args.profile;
|
|
219
|
-
break;
|
|
220
|
-
case '
|
|
221
|
-
|
|
222
|
-
|
|
217
|
+
switch (name) {
|
|
218
|
+
case 'profile':
|
|
219
|
+
args.profile = value || args.profile;
|
|
220
|
+
break;
|
|
221
|
+
case 'data-dir':
|
|
222
|
+
case 'dataDir':
|
|
223
|
+
args.dataDir = String(value || '').trim();
|
|
224
|
+
break;
|
|
225
|
+
case 'dry-run':
|
|
226
|
+
args.dryRun = true;
|
|
227
|
+
break;
|
|
223
228
|
case 'no-state':
|
|
224
229
|
args.noState = true;
|
|
225
230
|
break;
|
|
@@ -293,8 +298,16 @@ function parseArgs(argv) {
|
|
|
293
298
|
if (positionals.length > 0) {
|
|
294
299
|
args.command = positionals[0];
|
|
295
300
|
}
|
|
296
|
-
return args;
|
|
297
|
-
}
|
|
301
|
+
return args;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function resolveDataDir(args = {}, env = process.env, cwd = process.cwd()) {
|
|
305
|
+
const explicit = String(args?.dataDir || '').trim();
|
|
306
|
+
if (explicit) return path.resolve(explicit);
|
|
307
|
+
const fromEnv = String(env?.BOSS_CHAT_HOME || '').trim();
|
|
308
|
+
if (fromEnv) return path.resolve(fromEnv);
|
|
309
|
+
return path.join(path.resolve(String(cwd || process.cwd())), '.boss-chat');
|
|
310
|
+
}
|
|
298
311
|
|
|
299
312
|
function printUsage() {
|
|
300
313
|
console.log('Usage: boss-chat <command> [options]');
|
|
@@ -308,11 +321,12 @@ function printUsage() {
|
|
|
308
321
|
console.log(' resume-run Resume paused async run');
|
|
309
322
|
console.log(' cancel-run Cancel async run');
|
|
310
323
|
console.log('');
|
|
311
|
-
console.log('Common options:');
|
|
312
|
-
console.log(' --profile <name> Profile name (default: default)');
|
|
313
|
-
console.log(' --
|
|
314
|
-
console.log(' --
|
|
315
|
-
console.log('');
|
|
324
|
+
console.log('Common options:');
|
|
325
|
+
console.log(' --profile <name> Profile name (default: default)');
|
|
326
|
+
console.log(' --data-dir <path> Runtime data dir (default: $BOSS_CHAT_HOME or <cwd>/.boss-chat)');
|
|
327
|
+
console.log(' --json JSON output for agent integration');
|
|
328
|
+
console.log(' --run-id <id> Target async run_id (for get/pause/resume/cancel)');
|
|
329
|
+
console.log('');
|
|
316
330
|
console.log('Run options:');
|
|
317
331
|
console.log(' --dry-run Evaluate and click, but do not request resume');
|
|
318
332
|
console.log(' --no-state Disable in-run candidate deduplication');
|
|
@@ -1511,10 +1525,10 @@ async function executeRunCommand(args, dataDir) {
|
|
|
1511
1525
|
}
|
|
1512
1526
|
}
|
|
1513
1527
|
|
|
1514
|
-
async function main() {
|
|
1515
|
-
const args = parseArgs(process.argv.slice(2));
|
|
1516
|
-
const dataDir =
|
|
1517
|
-
await mkdir(dataDir, { recursive: true });
|
|
1528
|
+
async function main() {
|
|
1529
|
+
const args = parseArgs(process.argv.slice(2));
|
|
1530
|
+
const dataDir = resolveDataDir(args);
|
|
1531
|
+
await mkdir(dataDir, { recursive: true });
|
|
1518
1532
|
|
|
1519
1533
|
if (args.command === 'help') {
|
|
1520
1534
|
printUsage();
|
|
@@ -1558,11 +1572,12 @@ async function main() {
|
|
|
1558
1572
|
}
|
|
1559
1573
|
}
|
|
1560
1574
|
|
|
1561
|
-
export const __testables = {
|
|
1562
|
-
parseArgs,
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1575
|
+
export const __testables = {
|
|
1576
|
+
parseArgs,
|
|
1577
|
+
resolveDataDir,
|
|
1578
|
+
connectBossChatPage,
|
|
1579
|
+
hasHydratedChatShell,
|
|
1580
|
+
promptRunProfile,
|
|
1566
1581
|
resolveJobsWithRetry,
|
|
1567
1582
|
waitForChatShellHydration,
|
|
1568
1583
|
};
|