@hupan56/wlkj 3.4.9 → 3.5.1

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/bin/cli.js CHANGED
@@ -1,1714 +1,1714 @@
1
- #!/usr/bin/env node
2
- // wlkj - workflow toolkit
3
-
4
- // Windows 中文乱码修复
5
- if (process.platform === "win32") {
6
- try { execSync("chcp 65001", { stdio: "ignore" }); } catch (e) {}
7
- process.stdout.setDefaultEncoding("utf-8");
8
- process.stderr.setDefaultEncoding("utf-8");
9
- }
10
-
11
- const path = require("path");
12
- const fs = require("fs");
13
- const { execSync } = require("child_process");
14
-
15
- const T = path.join(__dirname, "..", "templates");
16
-
17
- // 当前 npm 包版本(从 package.json 读,不硬编码)
18
- const PKG_VERSION = JSON.parse(
19
- fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")
20
- ).version;
21
-
22
- // 升级时受保护的用户数据文件(绝不覆盖)
23
- // 这些文件含团队/机器特定配置,覆盖 = 数据丢失
24
- const PROTECTED_FILES = new Set([
25
- "config.yaml", // 团队 git 仓库 URL、平台映射
26
- "settings.json", // 本地权限/hook 配置
27
- ]);
28
-
29
- // 引擎产物目录:强制覆盖(不走三路快照保护)
30
- // commands+skills 是纯引擎定义文件,用户不该改,强制覆盖安全
31
- // scripts/rules/hooks/contracts/templates 走三路快照(可能被另一个会话/用户改过)
32
- const FORCE_OVERWRITE_DIRS = new Set([
33
- "commands", // /wl-*.md 命令定义(引擎产物,MCP铁律等)
34
- "skills", // SKILL.md 技能定义(AI读这个决定流程)
35
- ]);
36
-
37
- // 本地状态文件(升级时也绝不碰,但不算"受保护配置",不提示合并)
38
- const LOCAL_STATE_FILES = new Set([
39
- ".developer", ".current-task", ".engine-version",
40
- ]);
41
-
42
- // 探测可用的 Python 命令 (python 或 python3)。
43
- // macOS 系统通常只有 python3, 硬编码 python 会让 Mac 全员失败。
44
- // 探测结果缓存到全局, 避免每次调用都 fork 进程。
45
- let _PY_CMD_CACHE = null;
46
- function detectPyCmd() {
47
- if (_PY_CMD_CACHE) return _PY_CMD_CACHE;
48
- // ⚠ 必须校验是 Python 3.8+! 不能只看 exit 0 ——
49
- // 老企业机/某些 RHEL 上 `python` 仍指向 2.7 (exit 0), 引擎要 3.8+,
50
- // 误选 2.7 会被缓存 → 后续所有 py()/runScript 全用 2.7 跑 → setup.py 直接失败。
51
- // 检查 --version 输出以 "Python 3." 开头, 且次版本 >=8。
52
- for (const c of ["python", "python3"]) {
53
- try {
54
- const out = execSync(`${c} --version`, { encoding: "utf-8", timeout: 5000, stdio: "pipe" });
55
- const m = /Python\s+3\.(\d+)/i.exec((out || "").trim());
56
- if (m && parseInt(m[1], 10) >= 8) {
57
- _PY_CMD_CACHE = c;
58
- return c;
59
- }
60
- // 是 python 但版本不够 (2.7 或 3.0-3.7) → 跳过试下一个候选
61
- } catch { /* try next */ }
62
- }
63
- _PY_CMD_CACHE = null;
64
- return null;
65
- }
66
-
67
- function py(script, args = []) {
68
- // v3.x: 不再直接找扁平路径 (scripts/<script>), 改为转发 wlkj.py 单一真源调度器。
69
- // 根治 "引擎分层重组后 cli.js 路径全部失效" 的载体漂移。
70
- const wlkj = path.join(process.cwd(), ".qoder", "scripts", "orchestration", "wlkj.py");
71
- if (!fs.existsSync(wlkj)) { console.log("请先运行: npx wlkj init"); process.exit(1); }
72
- const pyCmd = detectPyCmd();
73
- if (!pyCmd) { console.log("Python 未安装, 无法运行脚本。跑: npx @hupan56/wlkj install-env"); return ""; }
74
- // 把脚本名当命令传给 wlkj.py (DISPATCH 命中则路由, 否则 run fallback 递归找)
75
- try {
76
- const cmdName = script.replace(/\.py$/, "").replace(/\//g, "-");
77
- return execSync(`${pyCmd} "${wlkj}" ${cmdName} ${args.map(a => `"${a}"`).join(" ")}`, { cwd: process.cwd(), encoding: "utf-8", timeout: 15000, env: { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" } });
78
- } catch (e) { return (e.stdout || e.message); }
79
- }
80
-
81
- // 透传执行: 直接 spawn (不捕获输出, 实时显示, 支持长超时和交互式)
82
- // 用于 kg_build/eval_prd/autotest 等耗时脚本, 以及需要交互的场景
83
- function runScript(scriptName, args = [], timeoutMs = 600000) {
84
- // v3.x: 转发 wlkj.py 单一真源 (不再直接找扁平 scripts/<scriptName>)
85
- const wlkj = path.join(process.cwd(), ".qoder", "scripts", "orchestration", "wlkj.py");
86
- if (!fs.existsSync(wlkj)) { console.log("引擎未安装。跑: npx @hupan56/wlkj init"); process.exit(1); }
87
- const pyCmd = detectPyCmd();
88
- if (!pyCmd) { console.log("Python 未安装。跑: npx @hupan56/wlkj install-env"); process.exit(1); }
89
- const { spawnSync } = require("child_process");
90
- const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
91
- // 脚本名作为命令传给 wlkj.py (DISPATCH 命中则路由, 否则 run fallback 递归找)
92
- const cmdName = scriptName.replace(/\.py$/, "").replace(/\//g, "-");
93
- const r = spawnSync(pyCmd, [wlkj, cmdName, ...args], { cwd: process.cwd(), stdio: "inherit", timeout: timeoutMs, encoding: "utf-8", env });
94
- process.exit(r.status || 0);
95
- }
96
-
97
- // 带镜像兜底的 pip 安装 —— 复用 Python 的 common/pip_install.py (单一事实源)
98
- // 为什么不复用 Python: 上一轮给 init_doctor 做了 common/pip_install.py (失败切清华/阿里/腾讯)。
99
- // cli.js 的 install-env/update 是独立 JS 路径, 之前没享受镜像兜底 → 50 人里弱网用户全崩。
100
- // 这里调用同一个 Python 工具, 保证两路径行为一致。
101
- //
102
- // 优先: 引擎已装 → 调 foundation.utils.pip_install (有镜像兜底)
103
- // 回退: 引擎没装 → 裸 pip install (install-env 首次场景, 无 Python 工具可用)
104
- //
105
- // 实现注意 (修 2.7.0 的真 bug): 不能用字符串拼接构造 Python -c 代码 ——
106
- // Windows 路径含反斜杠, 拼进 Python 字符串后 \Y \n 等成了非法转义 + 引号丢失,
107
- // 导致 SyntaxError → 镜像兜底静默失败 → 回退裸 pip → 弱网用户全崩。
108
- // 改用环境变量传参 (WLKJ_REQ_PATH), Python 侧 os.environ 读, 彻底避开转义地狱。
109
- function installPipDeps(pyCmd, reqPath) {
110
- // ⚠ v3.x 重写: 直接用 Node spawnSync 跑 pip (不嵌套 Python 子进程)。
111
- // 原方案 `python -c "...install_with_fallback..."` 在 Anaconda 3.9+ Windows 下,
112
- // pip 进镜像重试(长时间编译 tree-sitter/playwright)退出时会触发
113
- // "PyEval_SaveThread: GIL released" 致命崩溃 → Python 死、结果丢失、回退裸 pip、
114
- // 弱网新机装不上。Node 直接跑 pip 不嵌套 Python, 无此 GIL 问题。
115
- // 镜像兜底逻辑在 JS 侧复刻同一策略 (默认源→清华→阿里→腾讯), 行为与 Python 版一致。
116
- const { spawnSync } = require("child_process");
117
- const MIRRORS = [
118
- ["清华", "https://pypi.tuna.tsinghua.edu.cn/simple"],
119
- ["阿里", "https://mirrors.aliyun.com/pypi/simple/"],
120
- ["腾讯", "https://mirrors.cloud.tencent.com/simple"],
121
- ];
122
- const installEnv = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
123
-
124
- function runPip(extraArgs, timeoutMs) {
125
- // stdio inherit 让用户看到进度; 不捕获输出 = 不撑管道、不触发任何崩溃
126
- const r = spawnSync(pyCmd, ["-m", "pip", "install", "--no-input", "--progress-bar", "off", ...extraArgs, "-r", reqPath],
127
- { stdio: "inherit", timeout: timeoutMs || 300000, encoding: "utf-8", env: installEnv });
128
- return r.status === 0;
129
- }
130
-
131
- function pkgsInstalled() {
132
- // 轻量 import 验证: 不跑 pip 子进程, 只 python -c "import ...", 无 GIL 风险。
133
- // 核心包都 import 成功就算装好 (即使某些可选包编译失败 pip 返回非0)。
134
- const coreMods = ["duckdb", "pymysql", "yaml", "requests"];
135
- const mod = coreMods.map(m => "__import__('%s')".replace("%s", m)).join(";");
136
- try {
137
- const r = spawnSync(pyCmd, ["-c", mod], { encoding: "utf-8", timeout: 15000, env: installEnv, stdio: "pipe" });
138
- return r.status === 0;
139
- } catch { return false; }
140
- }
141
-
142
- // 已装好就不折腾 (常见: 老机器 update, 依赖都在)
143
- if (pkgsInstalled()) {
144
- console.log(" [OK] Python 依赖已就绪");
145
- return true;
146
- }
147
-
148
- // 1. 默认源 (国内常被拦, 短超时快速判定)
149
- console.log(" 试默认源(PyPI)...");
150
- let ok = runPip([], 60000);
151
- if (ok || pkgsInstalled()) { console.log(" [OK] Python 依赖已装"); return true; }
152
-
153
- // 2. 默认源失败 → 逐个国内镜像
154
- for (const [name, url] of MIRRORS) {
155
- console.log(` 默认源失败, 切镜像(${name})...`);
156
- ok = runPip(["-i", url, "--trusted-host", url.replace(/^https?:\/\//, "").split("/")[0]], 300000);
157
- if (ok || pkgsInstalled()) {
158
- console.log(` [OK] Python 依赖已装 (镜像源: ${name})`);
159
- return true;
160
- }
161
- }
162
-
163
- // 3. 全失败
164
- console.log(" [WARN] pip 安装失败 (尝试了所有镜像)");
165
- console.log(" 可能原因: 网络/防火墙/公司代理拦截 PyPI。");
166
- console.log(" 手动装 (用清华镜像): pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt");
167
- console.log(" 或配全局镜像: pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple");
168
- return false;
169
- }
170
-
171
- function cp(src, dest) {
172
- const s = path.join(T, src), d = path.join(process.cwd(), dest);
173
- if (!fs.existsSync(s)) return;
174
- fs.mkdirSync(path.dirname(d), { recursive: true });
175
- if (!fs.existsSync(d)) { fs.copyFileSync(s, d); return true; }
176
- return false;
177
- }
178
-
179
- function gitOp(op) {
180
- const cwd = process.cwd();
181
- const cmds = {
182
- "提交": 'git add . && git commit -m "update [ai-assisted]" && git push',
183
- "推送": "git push",
184
- "拉取最新": "git pull",
185
- "同步": "git pull",
186
- "查看状态": "git status",
187
- "提交PRD": 'git add workspace/specs/prd/ && git commit -m "docs(ai): PRD update [ai-generated]" && git push',
188
- "提交Spec": 'git add workspace/specs/ && git commit -m "docs(ai): Spec update [ai-generated]" && git push',
189
- "提交任务": 'git add workspace/tasks/ && git commit -m "chore: task update [ai-assisted]" && git push',
190
- };
191
- const cmd = cmds[op];
192
- if (!cmd) { console.log("可用: 提交 推送 拉取最新 同步 查看状态 提交PRD 提交Spec 提交任务"); return; }
193
- try { console.log(execSync(cmd, { cwd, encoding: "utf-8", timeout: 30000, stdio: "pipe" }) || `${op} done`); }
194
- catch (e) { console.log(e.stdout || e.message); }
195
- }
196
-
197
- // 快速环境检测 (init 前置): 缺 Python 致命退出, 缺 git 警告不阻塞
198
- // 目的: 杜绝"裸机没 Python 时 init 静默失败仍宣布成功"的假成功
199
- function checkEnvQuick() {
200
- console.log("--- 环境检测 ---");
201
- // Node 一定有 (npx 在跑), 输出 version 确认
202
- console.log(` Node.js: [OK] ${process.version}`);
203
-
204
- // Python: python 或 python3 任一可用即可
205
- let pyCmd = null, pyVer = null;
206
- for (const c of ["python", "python3"]) {
207
- try {
208
- const v = execSync(`${c} --version`, { encoding: "utf-8", timeout: 5000, stdio: "pipe" }).trim();
209
- pyCmd = c; pyVer = v; break;
210
- } catch { /* try next */ }
211
- }
212
- if (!pyCmd) {
213
- console.log(" Python: [缺失] (引擎必需)");
214
- console.log("\n ⚠ 没装 Python, 无法继续。请先装环境:");
215
- console.log(" npx @hupan56/wlkj install-env");
216
- console.log(" (自动装 Node/Python/git + pip 依赖; 装完重开终端再 init)\n");
217
- return { ok: false, pyCmd: null };
218
- }
219
- console.log(` Python: [OK] ${pyVer} (${pyCmd})`);
220
-
221
- // git: 可选, 缺了只警告 (与 init_doctor 口径一致)
222
- try {
223
- const gv = execSync("git --version", { encoding: "utf-8", timeout: 5000, stdio: "pipe" }).trim();
224
- console.log(` git: [OK] ${gv}`);
225
- } catch {
226
- console.log(" git: [缺失] (可选, 团队同步/源码克隆将禁用, 本地 PRD/搜索/任务仍可用)");
227
- console.log(" 想启用: npx @hupan56/wlkj install-env");
228
- }
229
- console.log("");
230
- return { ok: true, pyCmd };
231
- }
232
-
233
- // 交互式问角色 (50 人团队角色各异, 不能写死 pm)
234
- // 支持的角色 (与 config.yaml / kg.py ROLE_SECTIONS 对齐)
235
- const ROLES = [
236
- { key: "pm", label: "产品经理 (PM) — 写需求/PRD/任务" },
237
- { key: "design", label: "设计师 (UI) — 录入设计稿/出原型" },
238
- { key: "dev", label: "开发 (前端/后端) — 写代码/提交/测试" },
239
- { key: "test", label: "测试 (QA) — 跑测试用例/验证" },
240
- { key: "admin", label: "管理员 — 管理/维护工作流" },
241
- ];
242
-
243
- function askRole() {
244
- // 非交互环境 (CI/AI 调用) 默认 pm
245
- if (!process.stdin.isTTY) {
246
- console.log(" (非交互环境, 默认角色: pm)");
247
- return "pm";
248
- }
249
- console.log("\n 你的角色是? (决定命令视角和建议)");
250
- ROLES.forEach((r, i) => console.log(` ${i + 1}. ${r.label}`));
251
- const readline = require("readline").createInterface({ input: process.stdin, output: process.stdout });
252
- return new Promise((resolve) => {
253
- readline.question("\n 选择 (1-5, 默认1): ", (ans) => {
254
- readline.close();
255
- const idx = parseInt(ans, 10) - 1;
256
- const role = (idx >= 0 && idx < ROLES.length) ? ROLES[idx].key : "pm";
257
- console.log(` 角色: ${role}\n`);
258
- resolve(role);
259
- });
260
- });
261
- }
262
-
263
- // 校验角色参数合法性 (大小写无关: PM/pm/Dev/dev 都认)
264
- function normalizeRole(r) {
265
- if (!r) return null;
266
- const low = String(r).toLowerCase();
267
- return ROLES.some(x => x.key === low) ? low : null;
268
- }
269
-
270
- // reset --hard 前备份本地改过的团队数据文件 (防丢真实密码/配置)。
271
- // 只备份 .qoder/config.yaml|config.toml|settings.json, 且仅当本地内容与远程不同时。
272
- // 返回被备份的相对路径列表。引擎文件 (gitignored) 不备份, 反正由 update 重新生成。
273
- function _backupLocalTeamData(cwd, remoteBranch, _es) {
274
- const DATA_FILES = [".qoder/config.yaml", ".qoder/config.toml", ".qoder/settings.json"];
275
- const backed = [];
276
- const backupDir = path.join(cwd, ".wlkj-pre-reset");
277
- try {
278
- fs.mkdirSync(backupDir, { recursive: true });
279
- for (const rel of DATA_FILES) {
280
- const local = path.join(cwd, rel);
281
- if (!fs.existsSync(local)) continue;
282
- let localContent = "";
283
- try { localContent = fs.readFileSync(local, "utf-8"); } catch { continue; }
284
- // 拿远程版本对比
285
- let remoteContent = null;
286
- try {
287
- remoteContent = _es(`git show origin/${remoteBranch}:${rel}`, { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
288
- } catch { /* 远程没这文件 */ }
289
- const same = (remoteContent !== null && remoteContent.trim() === localContent.trim());
290
- if (!same) {
291
- const target = path.join(backupDir, rel.replace(/[\\/]/g, "__"));
292
- fs.copyFileSync(local, target);
293
- backed.push(rel);
294
- }
295
- }
296
- } catch { /* 备份失败不阻塞, 但少一层保护 */ }
297
- return backed;
298
- }
299
-
300
- async function doInit(name, roleArg) {
301
- const cwd = process.cwd();
302
- const hasExisting = fs.existsSync(path.join(cwd, ".qoder", "scripts", "setup.py"));
303
- console.log(`\nwlkj init -> ${cwd}${hasExisting ? " (已存在, 增量更新)" : ""}\n`);
304
-
305
- // === 0. 环境前置检测 (避免裸机无 Python 时假成功) ===
306
- const env = checkEnvQuick();
307
- if (!env.ok) process.exit(1);
308
- const { pyCmd } = env;
309
-
310
- // === 0.5. 确定角色 (参数 > 交互式, 不再写死 pm)
311
- // 50 人团队角色各异: 产品/UI/前后端/测试/管理, 每个角色的命令视角不同。
312
- // 写死 pm = 设计师和开发拿不到匹配的上下文 (见 wl-prd-full 的 --role 适配)。
313
- let role = normalizeRole(roleArg);
314
- if (!role) {
315
- role = await askRole();
316
- } else {
317
- console.log(` 角色: ${role} (来自参数)\n`);
318
- }
319
-
320
- // === 1. 拷贝完整引擎 (镜像 .qoder/ 结构) ===
321
- // 源: templates/qoder/* 目标: .qoder/*
322
- // init 复用 update 的三路快照保护: 新装时目标不存在, 保护不触发;
323
- // 重跑 init 时用户改过的引擎文件 (如自定义 skill) 不会被覆盖。
324
- // ⚠ 但如果快照缺失/损坏(如 git checkout 后), 三路保护会误保护所有文件
325
- // → 修复: 快照缺失时, init 强制全覆盖(重新建立基线), 不保护。
326
- const snapshotPath = path.join(cwd, ".qoder", ".engine-snapshot.json");
327
- const hasSnapshot = fs.existsSync(snapshotPath);
328
- if (hasExisting && !hasSnapshot) {
329
- console.log(" [注意] 快照缺失, 引擎文件将强制全量覆盖 (重新建立基线)");
330
- }
331
- const useMode = (hasExisting && hasSnapshot) ? "update" : "init";
332
- const qoderSrc = path.join(T, "qoder");
333
- let copied = 0;
334
- if (fs.existsSync(qoderSrc)) {
335
- const snap = readSnapshot(cwd);
336
- const newSnap = {};
337
- const r = copyDirRecursive(qoderSrc, path.join(cwd, ".qoder"), useMode, snap, newSnap);
338
- copied = r.copied;
339
- writeSnapshot(cwd, newSnap);
340
- if (r.protectedN > 0) {
341
- console.log(` [保护] ${r.protectedN} 个文件保留你的改动 (新版存为 .new):`);
342
- for (const f of r.protectedFiles.slice(0, 8)) console.log(` .qoder/${f}`);
343
- if (r.protectedFiles.length > 8) console.log(` ...等 ${r.protectedFiles.length} 个`);
344
- }
345
- }
346
- console.log(` 引擎: ${copied} 个文件 (${hasExisting ? "更新" : "新建"})`);
347
-
348
- // === 2. 根文件 (AGENTS.md, 新手指南.md, requirements.txt) ===
349
- ["root/AGENTS.md", "root/新手指南.md", "root/requirements.txt"].forEach(f => {
350
- const src = path.join(T, f);
351
- if (fs.existsSync(src)) {
352
- const dstName = path.basename(f);
353
- const dst = path.join(cwd, dstName);
354
- // requirements.txt 始终覆盖(版本可能更新), 其它不存在才拷
355
- if (dstName === "requirements.txt" || !fs.existsSync(dst)) {
356
- fs.copyFileSync(src, dst);
357
- }
358
- }
359
- });
360
-
361
- // === 3. workspace 目录结构 ===
362
- ["workspace/specs/prd", "workspace/tasks", "workspace/constitution",
363
- "workspace/members", "data/docs/prd", "data/index", "data/code"].forEach(d => {
364
- fs.mkdirSync(path.join(cwd, d), { recursive: true });
365
- });
366
- console.log(` workspace 目录就绪`);
367
-
368
- // === 4. git init (若没有) ===
369
- if (!fs.existsSync(path.join(cwd, ".git"))) {
370
- try { execSync("git init", { cwd, stdio: "pipe" }); console.log(` git init done`); }
371
- catch (_) { /* git 可能没装, 不阻塞 */ }
372
- }
373
-
374
- // === 5. .gitignore (若没有) ===
375
- const gitignorePath = path.join(cwd, ".gitignore");
376
- if (!fs.existsSync(gitignorePath)) {
377
- const baseGitignore = [
378
- "# 引擎代码 (由 npm 包 @hupan56/wlkj init/update 生成, 不进团队 git)",
379
- ".qoder/scripts/", ".qoder/skills/", ".qoder/commands/", ".qoder/agents/",
380
- ".qoder/contracts/", ".qoder/hooks/", ".qoder/templates/", ".qoder/rules/",
381
- ".qoder/repowiki/", ".qoder/archive/",
382
- "",
383
- "# 团队数据 (保持跟踪): .qoder/config.yaml, .qoder/settings.json, data/learning/, data/index/, data/, workspace/",
384
- "",
385
- "# Source code repos (cloned by git_sync.py)", "data/code/",
386
- "", "# AI pipeline runtime", ".qoder/.developer", ".qoder/.current-task",
387
- ".qoder/.engine-version", ".qoder/.engine-snapshot.json", ".qoder/.runtime/", "",
388
- "# Machine-local", "data/index/.last-sync", "data/index/.prd-collected.json",
389
- "data/index/.index-meta.json", "data/index/.sync-lock", "data/index/.file-keys.json",
390
- "data/index/.inverted-cache.json", "data/index/*.corrupt",
391
- "", "# Identity (secret)", "workspace/members/*/.signing_key", "",
392
- "# Python", "__pycache__/", "*.pyc", "*.lock", "*.tmp", "",
393
- ].join("\n");
394
- fs.writeFileSync(gitignorePath, baseGitignore, "utf-8");
395
- }
396
-
397
- // === 6. 自动跑 setup.py (名字探测/git/索引/cron/QoderWork) ===
398
- console.log(`\n--- 自动初始化 ---`);
399
- const setupPath = path.join(cwd, ".qoder", "scripts", "deployment", "setup", "setup.py");
400
- if (fs.existsSync(setupPath)) {
401
- const setupArgs = [name, role, "--skip-cron", "--skip-qoderwork", "--skip-code"].filter(Boolean);
402
- try {
403
- const cmd = `python "${setupPath}" ${setupArgs.map(a => `"${a}"`).join(" ")}`;
404
- console.log(` 运行: setup.py ${setupArgs.join(" ")}`);
405
- // 强制 UTF-8 环境 (Windows cmd 默认 GBK, 会导致中文输出乱码)
406
- const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
407
- execSync(cmd, { cwd, stdio: "inherit", timeout: 300000, env });
408
- } catch (e) {
409
- console.log(` setup 部分失败 (不阻塞): ${(e.message || "").slice(0, 100)}`);
410
- console.log(` 可重新运行: npx @hupan56/wlkj init ${name || "<你的名字>"}`);
411
- }
412
- } else {
413
- console.log(` setup.py 未找到, 跳过自动初始化`);
414
- }
415
-
416
- // === 6.4. 装 pip 依赖 (必须在配 MCP 之前!)
417
- // 顺序关键: 先装依赖 → 再写 mcp.json → mcp.json 选到"装了依赖的Python" → MCP 秒绿
418
- // 旧版 init 不装依赖直接配 mcp.json → 新机 MCP 全黄 (没装 duckdb/pymysql)
419
- console.log(`\n--- Python 依赖安装 (失败自动切镜像) ---`);
420
- const reqFileInit = path.join(cwd, "requirements.txt");
421
- if (pyCmd && fs.existsSync(reqFileInit)) {
422
- installPipDeps(pyCmd, reqFileInit);
423
- } else if (!pyCmd) {
424
- console.log(` [跳过] Python 未装`);
425
- } else {
426
- console.log(` [跳过] requirements.txt 不存在`);
427
- }
428
-
429
- // === 6.5. 自动配 QoderWork MCP (新机关键: 否则 mcp.json 不生成, 4 个 MCP 全连不上) ===
430
- // 复刻 doUpdate 第 6 步的逻辑: 拷 launcher + install_qoderwork --mcp-only
431
- // setup.py 带了 --skip-qoderwork (offer_qoderwork 是交互式+装skill, 不适合 npx),
432
- // 这里用 --mcp-only (纯文件操作, 无交互) 把 mcp.json 配好
433
- // 注意: 此时依赖已装好(6.4步), install_qoderwork 会选到装了依赖的 Python
434
- if (pyCmd) {
435
- console.log(`\n--- QoderWork MCP 自动配置 ---`);
436
- const home = process.env.USERPROFILE || require("os").homedir();
437
- const launcherSrc = path.join(cwd, ".qoder", "scripts", "protocol", "mcp", "mcp_launcher.py");
438
- const launcherDst = path.join(home, ".qoderwork", "mcp_launcher.py");
439
- try {
440
- if (fs.existsSync(launcherSrc)) {
441
- fs.mkdirSync(path.dirname(launcherDst), { recursive: true });
442
- fs.copyFileSync(launcherSrc, launcherDst);
443
- console.log(` [OK] launcher 已装: ~/.qoderwork/mcp_launcher.py`);
444
- }
445
- const instScript = path.join(cwd, ".qoder", "scripts", "deployment", "setup", "install_qoderwork.py");
446
- if (fs.existsSync(instScript)) {
447
- try {
448
- const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
449
- execSync(`${pyCmd} "${instScript}" --mcp-only`, { stdio: "inherit", timeout: 20000, env });
450
- console.log(` [OK] mcp.json 已生成 (选装了依赖的Python, MCP 不会黄)`);
451
- } catch (e) {
452
- console.log(` [WARN] MCP 配置未完全成功 (不阻塞): ${(e.message || "").slice(0, 80)}`);
453
- console.log(` 可手动补: ${pyCmd} .qoder/scripts/deployment/setup/install_qoderwork.py`);
454
- }
455
- }
456
- } catch (e) {
457
- console.log(` [WARN] MCP 配置跳过: ${(e.message || "").slice(0, 80)}`);
458
- }
459
- }
460
-
461
- // === 6.6. 连云平台 bind 项目(云模式:KG 在云平台,引擎调云 MCP)===
462
- if (pyCmd) {
463
- const switchScript = path.join(cwd, ".qoder", "scripts", "domain", "kg", "switch_project.py");
464
- if (fs.existsSync(switchScript)) {
465
- console.log(`\n--- 连云平台 bind 项目 ---`);
466
- console.log(` 问平台邮箱/密码 → 拉项目列表 → 选项目 → 写 mcp_config(连云)`);
467
- try {
468
- const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
469
- execSync(`${pyCmd} "${switchScript}"`, { cwd, stdio: "inherit", timeout: 180000, env });
470
- } catch (e) {
471
- console.log(` [WARN] 云 bind 未完成,可手动: python .qoder/scripts/domain/kg/switch_project.py`);
472
- }
473
- }
474
- }
475
-
476
- // === 7. 写版本戳 ===
477
- writeEngineVersion(cwd);
478
-
479
- console.log(`\n${"=".repeat(50)}`);
480
- console.log(` 安装完成! (v${PKG_VERSION})`);
481
- console.log(`${"=".repeat(50)}`);
482
- console.log(`\n 现在可以开始了:`);
483
- console.log(` 在 Qoder 里说 "写个 XX 的需求"`);
484
- console.log(` 或输 / 看命令列表 (/wl-prd /wl-search /wl-task)`);
485
- console.log(`\n ✓ QoderWork MCP 已自动配好 (kg/mysql/lanhu/playwright)`);
486
- console.log(` 用 QoderWork 桌面端想装技能(软链到 ~/.qoderwork/skills/)?`);
487
- console.log(` python .qoder/scripts/deployment/setup/install_qoderwork.py`);
488
- console.log(`\n 看不到命令? 新建对话 / 重启 QoderWork`);
489
- console.log(` 环境问题? python .qoder/scripts/deployment/setup/init_doctor.py --fix\n`);
490
- }
491
-
492
- // 递归拷贝目录, 返回 {copied, protected, skipped, protectedFiles}
493
- // mode: "init" (新装, 全拷) | "update" (升级, 保护用户数据)
494
- // update 模式下采用三路快照保护 (见 readSnapshot/writeSnapshot):
495
- // 用户改过的引擎文件 (与上次安装快照不同) → 保留用户的, 新版存 .new
496
- // 用户没动过的引擎文件 → 正常升级
497
- // 非引擎文件已存在 → 跳过 (留用户数据)
498
- function copyDirRecursive(src, dst, mode = "init", snap = {}, newSnap = {}, relBase = "") {
499
- let copied = 0, protectedN = 0, skipped = 0;
500
- const protectedFiles = [];
501
- if (!fs.existsSync(src)) return { copied, protectedN, skipped, protectedFiles };
502
- fs.mkdirSync(dst, { recursive: true });
503
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
504
- if (entry.name === "__pycache__" || entry.name.endsWith(".pyc")) continue;
505
- const s = path.join(src, entry.name);
506
- const d = path.join(dst, entry.name);
507
- const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
508
-
509
- // commands/ 目录强制覆盖(不走三路快照,引擎产物不该被保护)
510
- const inForceOverwriteDir = FORCE_OVERWRITE_DIRS.has(relBase);
511
- if (entry.isDirectory() && inForceOverwriteDir) {
512
- // 直接全量拷贝目录(不保护,不快照)
513
- const r = copyDirRecursive(s, d, "init", {}, {}, rel);
514
- copied += r.copied; protectedN += r.protectedN; skipped += r.skipped;
515
- protectedFiles.push(...r.protectedFiles);
516
- continue;
517
- }
518
- if (entry.isDirectory()) {
519
- const r = copyDirRecursive(s, d, mode, snap, newSnap, rel);
520
- copied += r.copied; protectedN += r.protectedN; skipped += r.skipped;
521
- protectedFiles.push(...r.protectedFiles);
522
- } else {
523
- const isEngine = entry.name.endsWith(".py") || entry.name.endsWith(".md") ||
524
- entry.name.endsWith(".yaml") || entry.name.endsWith(".yml") ||
525
- entry.name.endsWith(".toml") || entry.name.endsWith(".json") ||
526
- entry.name.endsWith(".html") || entry.name.endsWith(".txt");
527
-
528
- if (mode === "update") {
529
- // 受保护配置 (config.yaml/settings.json): 永不覆盖, 但自动修复已知问题
530
- if (PROTECTED_FILES.has(entry.name) && fs.existsSync(d)) {
531
- // ★ config.yaml url 行内注释修复:自动清理 url 行的 # 注释(防被当 git url)
532
- if (entry.name === "config.yaml") {
533
- try {
534
- let cfgContent = fs.readFileSync(d, "utf-8");
535
- // 匹配 url: "xxx" # 注释 → 去掉注释
536
- const fixedContent = cfgContent.replace(
537
- /^(\s*url:\s*["']?[^"'\n]*["']?)\s+#.*$/gm, "$1"
538
- );
539
- if (fixedContent !== cfgContent) {
540
- fs.writeFileSync(d, fixedContent, "utf-8");
541
- console.log(` [FIX] config.yaml url 行内注释已自动清理`);
542
- }
543
- } catch { /* 修复失败不阻塞 */ }
544
- }
545
- if (!filesEqual(s, d)) {
546
- fs.copyFileSync(s, d + ".new");
547
- protectedN++; protectedFiles.push(rel);
548
- } else {
549
- skipped++;
550
- }
551
- // 快照记上游 hash: 用户当前 == 上游 → 下次安全升级; 否则持续保护
552
- const sh = fileHash(s); if (sh) newSnap[rel] = sh;
553
- continue;
554
- }
555
- // 引擎文件: 三路快照保护
556
- if (isEngine) {
557
- const userHash = fs.existsSync(d) ? fileHash(d) : null;
558
- const snapHash = snap[rel] || null;
559
- // 保护判定: 用户当前文件与上游源不同, 且要么(a)与上次快照也不同(确属用户改过),
560
- // 要么(b)快照缺失/损坏(无法判定来源 → 保守保护, 不覆盖, 避免静默丢用户改动)。
561
- // ⚠ 旧逻辑要求 snapHash 非 null 才保护 → 快照缺失/损坏时全量覆盖, 静默吞掉用户改动。
562
- const userModifiedKnownSnap = (fs.existsSync(d) && userHash && snapHash && userHash !== snapHash);
563
- const userDiffersFromUpstream = (fs.existsSync(d) && !filesEqual(s, d));
564
- const snapshotMissing = !snapHash;
565
- const shouldProtect = userModifiedKnownSnap || (userDiffersFromUpstream && snapshotMissing);
566
- if (shouldProtect) {
567
- // 保留用户的, 新版存 .new (不覆盖)
568
- if (!filesEqual(s, d)) {
569
- fs.copyFileSync(s, d + ".new");
570
- protectedN++; protectedFiles.push(rel);
571
- } else {
572
- skipped++;
573
- }
574
- // 关键: 快照记上游 hash (不是用户的 hash)。
575
- // 这样下次升级: 用户文件若仍 != 上游 → 继续保护; 若用户手动改成上游 → 自动恢复正常升级。
576
- const sh = fileHash(s); if (sh) newSnap[rel] = sh;
577
- } else {
578
- // 用户没动过 (或新文件), 正常升级, 刷新快照为新版 hash
579
- fs.copyFileSync(s, d);
580
- copied++;
581
- const sh = fileHash(s); if (sh) newSnap[rel] = sh;
582
- }
583
- } else {
584
- // 非引擎文件: 已存在跳过 (保护用户数据), 不存在才建
585
- if (!fs.existsSync(d)) {
586
- fs.copyFileSync(s, d);
587
- copied++;
588
- } else {
589
- skipped++;
590
- }
591
- }
592
- } else {
593
- // init 模式: 引擎文件全覆盖, 非引擎已存在跳过 (保留用户改动)
594
- if (isEngine || !fs.existsSync(d)) {
595
- fs.copyFileSync(s, d);
596
- }
597
- copied++;
598
- // 快照记上游 hash (基线), 供将来升级对比
599
- const sh = fileHash(s); if (sh) newSnap[rel] = sh;
600
- }
601
- }
602
- }
603
- return { copied, protectedN, skipped, protectedFiles };
604
- }
605
-
606
- // 两文件内容是否相同(先比大小再比内容)
607
- function filesEqual(a, b) {
608
- try {
609
- const sa = fs.statSync(a), sb = fs.statSync(b);
610
- if (sa.size !== sb.size) return false;
611
- return fs.readFileSync(a).equals(fs.readFileSync(b));
612
- } catch { return false; }
613
- }
614
-
615
- // 版本戳: .qoder/.engine-version 存当前引擎版本
616
- function writeEngineVersion(cwd) {
617
- try {
618
- const p = path.join(cwd, ".qoder", ".engine-version");
619
- fs.mkdirSync(path.dirname(p), { recursive: true });
620
- fs.writeFileSync(p, PKG_VERSION + "\n", "utf-8");
621
- } catch { /* 不阻塞 */ }
622
- }
623
-
624
- function readEngineVersion(cwd) {
625
- try {
626
- const p = path.join(cwd, ".qoder", ".engine-version");
627
- if (fs.existsSync(p)) return fs.readFileSync(p, "utf-8").trim();
628
- } catch { /* */ }
629
- return null;
630
- }
631
-
632
- // ─────────────────────────────────────────────────────────────
633
- // 三路快照保护 (dpkg/brew conffiles 同款机制, 彻底解决"我原本的技能呢?")
634
- //
635
- // 问题: update 用 isEngine 一刀切强制刷新 .md/.py/.yaml 等,
636
- // 用户改过的 stock skill (如 .qoder/skills/wl-prd-full/SKILL.md)
637
- // 被无声覆盖 → "更新完我的技能没了"。
638
- //
639
- // 解法: 每次安装/升级后, 把所有"从 npm 包拷过来的引擎文件"的 hash 记到
640
- // .qoder/.engine-snapshot.json。下次升级时, 三路对比:
641
- // 用户当前文件 hash == 快照 hash → 没动过, 安全升级
642
- // 用户当前文件 hash != 快照 hash → 动过! 保留用户的, 新版存 .new
643
- // 这就是 apt/dpkg 对 /etc 下 conffiles 的处理, 行业标准, 不瞎猜。
644
- // ─────────────────────────────────────────────────────────────
645
- const SNAPSHOT_FILE = ".engine-snapshot.json";
646
-
647
- function snapshotPath(cwd) {
648
- return path.join(cwd, ".qoder", SNAPSHOT_FILE);
649
- }
650
-
651
- // 读快照: { "skills/wl-prd-full/SKILL.md": "<sha256前16位>", ... }
652
- function readSnapshot(cwd) {
653
- try {
654
- const p = snapshotPath(cwd);
655
- if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, "utf-8"));
656
- } catch { /* 损坏的快照当空 */ }
657
- return {};
658
- }
659
-
660
- // 写快照: 传入 {相对路径: hash} 的对象
661
- function writeSnapshot(cwd, snap) {
662
- try {
663
- const p = snapshotPath(cwd);
664
- fs.mkdirSync(path.dirname(p), { recursive: true });
665
- fs.writeFileSync(p, JSON.stringify(snap, null, 2), "utf-8");
666
- } catch { /* 快照写失败不阻塞升级 */ }
667
- }
668
-
669
- // 计算文件内容 hash (sha256 前 16 位, 足够检测"是否动过")
670
- function fileHash(p) {
671
- try {
672
- const crypto = require("crypto");
673
- const h = crypto.createHash("sha256");
674
- h.update(fs.readFileSync(p));
675
- return h.digest("hex").slice(0, 16);
676
- } catch { return null; }
677
- }
678
-
679
- // 跑 install_qoderwork.py(升级时强制刷新 commands)
680
- // v3.x: 脚本已分子目录, 路径为 scripts/deployment/setup/install_qoderwork.py
681
- function runQoderWorkInstall(cwd, forceCommands) {
682
- const script = path.join(cwd, ".qoder", "scripts", "deployment", "setup", "install_qoderwork.py");
683
- if (!fs.existsSync(script)) {
684
- console.log(" [跳过] install_qoderwork.py 不存在");
685
- return;
686
- }
687
- // ⚠ 必须用 detectPyCmd(), 不能硬编码 "python"!
688
- // macOS 默认只有 python3, 硬编码 python 会让 Mac 全员 update 时
689
- // "刷新 QoderWork" 步骤静默失败 (commands/skills 不刷新)。
690
- // 用 spawnSync(list-form) 而非 args.join(" ") → 路径含空格/中文也不裂。
691
- const { spawnSync } = require("child_process");
692
- const pyCmd = detectPyCmd();
693
- if (!pyCmd) {
694
- console.log(" [跳过] Python 未安装, 无法刷新 QoderWork (npx @hupan56/wlkj install-env)");
695
- return;
696
- }
697
- const args = [script];
698
- if (forceCommands) args.push("--force-commands");
699
- try {
700
- console.log(`\n--- 刷新 QoderWork ---`);
701
- const r = spawnSync(pyCmd, args, {
702
- cwd, stdio: "inherit", timeout: 60000,
703
- env: { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" },
704
- });
705
- if (r.status !== 0 && r.status !== null) {
706
- console.log(` QoderWork 刷新返回非0 (不阻塞): exit ${r.status}`);
707
- }
708
- } catch (e) {
709
- console.log(` QoderWork 刷新失败 (不阻塞): ${(e.message || "").slice(0, 80)}`);
710
- }
711
- }
712
-
713
- function doStatus() {
714
- console.log("");
715
- // v3.x: 转调 wlkj.py status (单一真源 domain/report/status.py)。
716
- // 旧版 doStatus 自带 .developer 解析, 但 role 实际存在 member.json,
717
- // 旧解析读不到 role 行就误报"角色未设置", 与 status.py 自相矛盾。
718
- // 统一走 Python 入口, 消除"两套读取逻辑不一致"。
719
- const wlkj = path.join(process.cwd(), ".qoder", "scripts", "orchestration", "wlkj.py");
720
- if (!fs.existsSync(wlkj)) {
721
- console.log("引擎未安装。跑: npx @hupan56/wlkj init");
722
- return;
723
- }
724
- const pyCmd = detectPyCmd();
725
- if (!pyCmd) {
726
- console.log("Python 未安装。跑: npx @hupan56/wlkj install-env");
727
- return;
728
- }
729
- try {
730
- const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
731
- const out = execSync(`${pyCmd} "${wlkj}" status`, { cwd: process.cwd(), encoding: "utf-8", timeout: 20000, env });
732
- process.stdout.write(out);
733
- } catch (e) {
734
- process.stdout.write((e.stdout || e.message || ""));
735
- }
736
- // 版本戳 (status.py 不管这层, 保留在 cli 侧)
737
- const ver = readEngineVersion(process.cwd());
738
- console.log(`engine: ${ver || "未知"} (最新: v${PKG_VERSION})${ver === PKG_VERSION ? " [最新]" : ver ? " [可升级: npx @hupan56/wlkj update]" : ""}`);
739
- console.log("");
740
- }
741
-
742
- function doUpdate() {
743
- const cwd = process.cwd();
744
- const oldVer = readEngineVersion(cwd);
745
- const qoderSrc = path.join(T, "qoder");
746
-
747
- // 前置检查: 必须已装过
748
- if (!fs.existsSync(path.join(cwd, ".qoder", "scripts"))) {
749
- console.log("\n 未检测到已安装的引擎。首次安装请用:");
750
- console.log(" npx @hupan56/wlkj init [你的名字]\n");
751
- return;
752
- }
753
-
754
- console.log(`\nwlkj update${oldVer ? " " + oldVer : ""} -> v${PKG_VERSION}\n`);
755
-
756
- // P1-15: 显式声明 update 绝不触碰的路径
757
- console.log(" 受保护路径 (绝不覆盖):");
758
- console.log(" .qoder/config.yaml (团队 git 配置)");
759
- console.log(" .qoder/settings.json (权限/hook 配置)");
760
- console.log(" .qoder/.developer (你的身份)");
761
- console.log(" .qoder/.current-task (当前任务)");
762
- console.log(" .qoder/.engine-version (版本戳)");
763
- console.log(" 你改过的引擎文件 (三路快照: 改过的留你的, 新版存 .new)");
764
- console.log(" workspace/ (你的全部产出)");
765
- console.log(" data/ (代码仓库+知识图谱)");
766
- console.log("");
767
-
768
- // === 1. 刷新引擎文件 (三路快照保护用户改动) ===
769
- let copied = 0, protectedN = 0, skipped = 0;
770
- let protectedFiles = [];
771
- if (fs.existsSync(qoderSrc)) {
772
- const snap = readSnapshot(cwd);
773
- const newSnap = {};
774
- const r = copyDirRecursive(qoderSrc, path.join(cwd, ".qoder"), "update", snap, newSnap);
775
- copied = r.copied; protectedN = r.protectedN; skipped = r.skipped;
776
- protectedFiles = r.protectedFiles || [];
777
- writeSnapshot(cwd, newSnap); // 刷新基线 (供下次升级对比)
778
- }
779
- console.log(` 引擎刷新: ${copied} 个文件${skipped ? ` / ${skipped} 无变化` : ""}`);
780
-
781
- // 报告受保护文件 (用户的改动 + 配置文件)
782
- if (protectedN > 0) {
783
- console.log(` [保护] ${protectedN} 个文件已保留你的改动 (新版存为 .new):`);
784
- for (const f of protectedFiles.slice(0, 10)) console.log(` .qoder/${f}`);
785
- if (protectedFiles.length > 10) console.log(` ...等 ${protectedFiles.length} 个`);
786
- console.log(` 确认无需合并后可删除 .new; 或手动对比合并后删除。`);
787
- }
788
-
789
- // === 2. 根文件(文档 + requirements.txt, 始终覆盖保证最新)===
790
- ["root/AGENTS.md", "root/新手指南.md", "root/requirements.txt"].forEach(f => {
791
- const src = path.join(T, f);
792
- if (fs.existsSync(src)) {
793
- fs.copyFileSync(src, path.join(cwd, path.basename(f)));
794
- }
795
- });
796
- console.log(` 根文档 + requirements.txt: 已更新`);
797
-
798
- // === 2.5. git pull 拉最新团队数据 (引擎数据分离: 引擎已由上面 copyDirRecursive 生成, git 只存数据) ===
799
- // 团队数据 = config.yaml / settings.json / data/learning/ + data/ (kg.duckdb/kg_db.duckdb/索引) + workspace/ (PRD/任务)。
800
- // 引擎代码 (scripts/skills/...) 已被 gitignore, 不在 git 里, 不会与 pull 冲突。
801
- // git pull 失败会导致 kg.duckdb 拿不到 → kg MCP 无数据(但仍能连)。
802
- // 失败原因常见: 未配 git 身份 / 远程仓库要认证 / 网络问题 / 分支没tracking / unrelated histories。
803
- // 这里给出清晰诊断, 不让"git pull 失败"变成无声的静默跳过。
804
- // 前置: 自愈分支 tracking (老版 init 配了 remote 但没 set-upstream, pull 会报
805
- // "no tracking information"。这里检测到就补上, 老用户 update 也能自动修)
806
- const { execSync: _es } = require("child_process"); // 提到 try 外, catch 块的自愈逻辑也要用
807
- try {
808
- // 先看当前分支有没有 upstream
809
- let _trackOk = false;
810
- try {
811
- _es("git rev-parse --abbrev-ref --symbolic-full-name @{u}", { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
812
- _trackOk = true; // 有 upstream, 正常
813
- } catch {
814
- // 没 tracking — 尝试补设 (有 origin remote 的前提下)
815
- try {
816
- const _remote = _es("git remote", { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" }).trim();
817
- if (_remote.split(/\s+/).includes("origin")) {
818
- _es("git fetch origin", { cwd, encoding: "utf-8", timeout: 30000, stdio: "pipe" });
819
- // 确定本地分支名
820
- const _branch = _es("git rev-parse --abbrev-ref HEAD", { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" }).trim();
821
- // 远程用 master 还是 main
822
- let _remoteBranch = null;
823
- for (const _c of ["master", "main"]) {
824
- try {
825
- _es(`git rev-parse --verify remotes/origin/${_c}`, { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
826
- _remoteBranch = _c; break;
827
- } catch { /* 该分支不存在, 试下一个 */ }
828
- }
829
- if (_remoteBranch) {
830
- _es(`git branch --set-upstream-to=origin/${_remoteBranch} ${_branch}`, { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
831
- console.log(` git: 已自动设置上游 tracking (origin/${_remoteBranch})`);
832
- _trackOk = true;
833
- }
834
- }
835
- } catch { /* 补 tracking 失败, 走下面的 pull 诊断 */ }
836
- }
837
- const pullR = _es("git pull --no-rebase 2>&1", { cwd, encoding: "utf-8", timeout: 30000 });
838
- if (pullR.includes("Already up to date") || pullR.includes("已经是最新")) {
839
- console.log(` git: 已是最新`);
840
- } else if (pullR.includes("CONFLICT") || pullR.includes("conflict")) {
841
- console.log(` git: ⚠️ 有冲突, 跳过pull(不影响本地工作)`);
842
- } else {
843
- console.log(` git: 已拉取最新`);
844
- }
845
- } catch (e) {
846
- // git pull 失败诊断: 区分"没配git身份"/"远程要认证"/"网络问题"/"非git仓库"
847
- // node execSync 抛异常时, git 的真实报错在 e.stderr/e.stdout, 不能只看 e.message
848
- const errMsg = String(e.message || "");
849
- const stdout = String((e.stdout || "") + " " + (e.stderr || ""));
850
- const combined = (errMsg + " " + stdout).toLowerCase();
851
- if (combined.includes("authentication failed") || combined.includes("permission denied") ||
852
- combined.includes("could not read username") || combined.includes("不支持密码") ||
853
- combined.includes("credentials") || combined.includes("403") || combined.includes("401")) {
854
- console.log(` git: ⚠️ 认证失败 — kg.duckdb 等团队数据拉不到`);
855
- console.log(` 原因: git 身份/凭证未配。MCP 能连但无数据。`);
856
- console.log(` 修复: npx @hupan56/wlkj init <你的名字> (交互式配 git 账号)`);
857
- } else if (combined.includes("not a git repository") || combined.includes("not a git dir") ||
858
- combined.includes("unknown revision") || combined.includes("does not have any commits")) {
859
- console.log(` git: ⚠️ 非git仓库或空仓库 — 团队数据拉不到`);
860
- console.log(` 原因: 此目录不是 git clone 来的, 或没有 remote。`);
861
- console.log(` 修复: npx @hupan56/wlkj init <你的名字> (会配团队远程仓库)`);
862
- } else if (combined.includes("timed out") || combined.includes("timeout") ||
863
- combined.includes("could not resolve host") || combined.includes("connection refused") ||
864
- combined.includes("network is unreachable") || combined.includes("unable to access") ||
865
- combined.includes("ssl") || combined.includes("failed to connect")) {
866
- console.log(` git: ⚠️ 网络问题 — 连不上远程仓库`);
867
- console.log(` 原因: ${stdout.trim().slice(0, 80) || "网络超时/无法解析主机"}`);
868
- console.log(` (kg.duckdb 拉不到, MCP 能连但搜索无结果)`);
869
- } else if (combined.includes("unrelated histories") || combined.includes("refusing to merge")) {
870
- // 自愈: 本地是空的 git init 历史, 与团队远程无公共祖先。
871
- // 引擎代码现在由 npm 生成 (不在 git 里), 团队数据在远程 → 采用远程历史是正确的。
872
- // 但 reset --hard 会覆盖工作树 → 先把本地改过的团队数据文件备份, 防丢密码/配置。
873
- console.log(` git: ⚠️ 无关历史 (本地 git init 产生的平行历史与团队远程无公共祖先)`);
874
- try {
875
- // 探测远程分支名
876
- let _rb = null;
877
- for (const _c of ["master", "main"]) {
878
- try {
879
- _es(`git rev-parse --verify remotes/origin/${_c}`, { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
880
- _rb = _c; break;
881
- } catch { /* */ }
882
- }
883
- if (_rb) {
884
- // 备份本地改过的团队数据 (config 等可能含未提交的真实密码)
885
- const _backed = _backupLocalTeamData(cwd, _rb, _es);
886
- _es(`git reset --hard origin/${_rb}`, { cwd, encoding: "utf-8", timeout: 30000, stdio: "pipe" });
887
- console.log(` git: ✓ 已采用远程历史 (origin/${_rb}), 团队数据已落地`);
888
- console.log(` (引擎随后由本次 update 重新生成)`);
889
- if (_backed.length > 0) {
890
- console.log(` ⚠️ reset 前已备份你本地改过的数据文件 (可能含未提交的真实密码/配置):`);
891
- for (const rel of _backed) console.log(` → .wlkj-pre-reset/${rel.replace(/[\\/]/g, "__")}`);
892
- console.log(` 检查这些文件, 把需要的配置/密码重新填回 .qoder/config.yaml 后删备份目录。`);
893
- }
894
- } else {
895
- console.log(` git: 远程无 master/main 分支, 无法自愈。请确认团队仓库地址正确。`);
896
- }
897
- } catch (re) {
898
- console.log(` git: 自愈失败 (${String(re.message || "").slice(0, 80)})`);
899
- console.log(` 可手动: git fetch origin && git reset --hard origin/master`);
900
- }
901
- } else {
902
- // 兜底: 显示 git 的真实报错, 不再笼统说 "Command failed"
903
- const realErr = stdout.trim() || errMsg;
904
- console.log(` git: ⚠️ 拉取失败 — ${realErr.slice(0, 100)}`);
905
- console.log(` (kg.duckdb 拉不到, MCP 能连但搜索无结果)`);
906
- }
907
- }
908
-
909
- // === 3. 补装 pip 依赖 (必须在刷 mcp.json 之前!)
910
- // 顺序很关键: 先装依赖 → 再写 mcp.json → mcp.json 能选到"已装依赖的Python"
911
- // 旧版顺序反了(先写mcp.json再装依赖), 导致 mcp.json 指向没装依赖的Python → MCP 黄。
912
- console.log(`\n--- Python 依赖补装 (失败自动切镜像) ---`);
913
- const reqFile = path.join(cwd, "requirements.txt");
914
- const pyCmd = detectPyCmd();
915
- if (pyCmd && fs.existsSync(reqFile)) {
916
- installPipDeps(pyCmd, reqFile);
917
- } else if (!pyCmd) {
918
- console.log(` [跳过] Python 未装`);
919
- } else {
920
- console.log(` [跳过] requirements.txt 不存在`);
921
- }
922
-
923
- // === 4. 刷新 QoderWork commands(强制覆盖已存在的)===
924
- // WLKJ_SKIP_QW_INSTALL=1 时跳过 (测试隔离用, 防止测试目录污染 ~/.qoderwork/.repo-root)
925
- if (process.env.WLKJ_SKIP_QW_INSTALL !== "1") {
926
- runQoderWorkInstall(cwd, true);
927
- }
928
-
929
- // === 5. 写版本戳 ===
930
- writeEngineVersion(cwd);
931
-
932
- // === 5b. 清理旧版废弃文件 ===
933
- const OBSOLETE_FILES = [
934
- // 旧脚本(已替代)
935
- ".qoder/workflow.md", // 被 rules/wl-pipeline.md 替代
936
- ".qoder/scripts/code_index.py", // 被 git_sync.py 替代
937
- ".qoder/scripts/notify.py", // 被 common/feishu.py 替代
938
- ".qoder/scripts/team.py", // 被 task.py + team_sync.py 替代
939
- "packages/wlkj/templates/cli.js", // 旧版 CLI
940
- ".qoder/templates/qoder/config.toml", // 旧版配置
941
- ".qoder/templates/qoder/scripts/code_index.py",
942
- ".qoder/templates/qoder/scripts/notify.py",
943
- ".qoder/templates/qoder/scripts/team.py",
944
- // 旧命令(已合并进 /wl-prd 和 /wl-design)
945
- ".qoder/commands/wl-prd-full.md",
946
- ".qoder/commands/wl-prd-quick.md",
947
- ".qoder/commands/wl-prd-review.md",
948
- ".qoder/commands/wl-design-draw.md",
949
- ".qoder/commands/wl-design-scan.md",
950
- ".qoder/commands/wl-design-spec.md",
951
- // 旧版索引JSON(已迁移到DuckDB, 残留在老用户的data/index/)
952
- "data/index/keyword-index.json",
953
- "data/index/api-index.json",
954
- "data/index/prd-index.json",
955
- "data/index/module-map.json",
956
- "data/index/prd-code-map.json",
957
- "data/index/callgraph.json",
958
- "data/index/style-index.json",
959
- "data/index/style-meta.json",
960
- "data/index/vben-style-reference.json",
961
- "data/index/chart-style-reference.json",
962
- "data/index/icon-reference.json",
963
- "data/index/wiki-index.json",
964
- // 旧运行时缓存(自动重建)
965
- "data/index/.file-keys.json",
966
- "data/index/.inverted-cache.json",
967
- "data/index/.api-table-cache.json",
968
- // 旧learning空壳(未实现的功能占位)
969
- ".qoder/learning/patterns/code-patterns.md",
970
- ".qoder/learning/patterns/prd-patterns.md",
971
- ".qoder/learning/patterns/review-fixes.md",
972
- // 用户级旧命令(已合并的, 清理~/.qoderwork/commands/)
973
- ];
974
- let cleaned = 0;
975
- for (const f of OBSOLETE_FILES) {
976
- const fp = path.join(cwd, f);
977
- if (fs.existsSync(fp)) {
978
- try {
979
- if (fs.statSync(fp).isDirectory()) {
980
- fs.rmSync(fp, { recursive: true, force: true });
981
- } else {
982
- fs.unlinkSync(fp);
983
- }
984
- console.log(` [清理] 删除废弃文件: ${f}`);
985
- cleaned++;
986
- } catch (e) { /* 删不掉跳过 */ }
987
- }
988
- }
989
- // 清理用户级旧命令 (已合并的, ~/.qoderwork/commands/)
990
- const home = process.env.USERPROFILE || require("os").homedir();
991
- const userCmdDir = path.join(home, ".qoderwork", "commands");
992
- const OBSOLETE_USER_CMDS = [
993
- "wl-prd-full.md", "wl-prd-quick.md", "wl-prd-review.md",
994
- "wl-design-draw.md", "wl-design-scan.md", "wl-design-spec.md",
995
- ];
996
- for (const f of OBSOLETE_USER_CMDS) {
997
- const fp = path.join(userCmdDir, f);
998
- if (fs.existsSync(fp)) {
999
- try { fs.unlinkSync(fp); console.log(` [清理] 用户级旧命令: ${f}`); cleaned++; }
1000
- catch { /* */ }
1001
- }
1002
- }
1003
- // 注意: .new 文件不再自动删除! 它们是三路快照保护的"新版待对比"信号,
1004
- // 用户需要看到 .new 才知道"我的改动被保留了, 这里是新版"。用户手动合并后删。
1005
- // 清理缓存 (索引重建后失效的旧缓存)
1006
- const runtimeDir = path.join(cwd, ".qoder", ".runtime");
1007
- let cacheCleaned = 0;
1008
- try {
1009
- for (const f of fs.readdirSync(runtimeDir)) {
1010
- if (f.startsWith("search-cache-") || f.startsWith("ctx-cache-")) {
1011
- fs.unlinkSync(path.join(runtimeDir, f));
1012
- cacheCleaned++;
1013
- }
1014
- }
1015
- } catch { /* */ }
1016
- // 清理引擎 __pycache__ (v3.x 加): 脚本重组后, 旧 .pyc 可能指向已删/已移的 .py
1017
- // → 某些机器 Python 加载死字节码 → 行为漂移 (机器特定, 难排查)。全删, 自动重建。
1018
- let pycCleaned = 0;
1019
- function _purgePycache(dir) {
1020
- try {
1021
- for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
1022
- const p = path.join(dir, e.name);
1023
- if (e.isDirectory()) {
1024
- if (e.name === "__pycache__") {
1025
- try { fs.rmSync(p, { recursive: true, force: true }); pycCleaned++; } catch { /* 占用 */ }
1026
- } else {
1027
- _purgePycache(p);
1028
- }
1029
- }
1030
- }
1031
- } catch { /* */ }
1032
- }
1033
- _purgePycache(path.join(cwd, ".qoder", "scripts"));
1034
- if (cleaned > 0 || cacheCleaned > 0 || pycCleaned > 0) {
1035
- console.log(` [清理] 废弃文件 ${cleaned} 个 + 过期缓存 ${cacheCleaned} 个 + __pycache__ ${pycCleaned} 个`);
1036
- }
1037
-
1038
- // === 5c. 清理 .qoder 嵌套残留 (致命 bug 源) ===
1039
- // 旧版 paths.py 的 _find_repo_root 会误把 .qoder 当 PROJECT_ROOT, 然后往
1040
- // "PROJECT_ROOT/.qoder/.runtime/" 写缓存 → 误建嵌套 .qoder/ 目录 →
1041
- // 这个残留反过来又让 _find_repo_root 再次误判 → 死循环。
1042
- // 表现: MCP cookie 路径全错 (workspace 跑到 .qoder/workspace), lanhu 永远 cookie 未配置。
1043
- // v3.0 修了 paths.py 不再误建, 但老用户机器上的残留要清掉, 否则 bug 还在。
1044
- // 注意: 残留可能出现在两处 —— .qoder/.qoder 和 .qoder/scripts/.qoder (后者是
1045
- // search 缓存写到错误 PROJECT_ROOT 时建的), 两处都要扫干净。
1046
- const nestedCandidates = [
1047
- path.join(cwd, ".qoder", ".qoder"),
1048
- path.join(cwd, ".qoder", "scripts", ".qoder"),
1049
- ];
1050
- for (const nestedQoder of nestedCandidates) {
1051
- if (fs.existsSync(nestedQoder)) {
1052
- try {
1053
- fs.rmSync(nestedQoder, { recursive: true, force: true });
1054
- console.log(` [清理] 删除嵌套残留 ${path.relative(cwd, nestedQoder) || nestedQoder}/ (曾导致 MCP 路径错位)`);
1055
- cleaned++;
1056
- } catch { /* 删不掉不阻塞 (可能被占用), 下次再清 */ }
1057
- }
1058
- }
1059
-
1060
- // === 6. 刷新 launcher + mcp.json (老用户的可能是旧版/写死路径) ===
1061
- // 注意: pip 依赖已在第3步装好, 此处 install_qoderwork 会选到"已装依赖的Python"写进 mcp.json
1062
- // WLKJ_SKIP_QW_INSTALL=1 时跳过 (测试隔离, 防止污染全局 ~/.qoderwork/)
1063
- if (process.env.WLKJ_SKIP_QW_INSTALL !== "1") {
1064
- try {
1065
- const home = process.env.USERPROFILE || require("os").homedir();
1066
- const launcherSrc = path.join(cwd, ".qoder", "scripts", "protocol", "mcp", "mcp_launcher.py");
1067
- const launcherDst = path.join(home, ".qoderwork", "mcp_launcher.py");
1068
- if (fs.existsSync(launcherSrc)) {
1069
- fs.mkdirSync(path.dirname(launcherDst), { recursive: true });
1070
- fs.copyFileSync(launcherSrc, launcherDst);
1071
- console.log(` [OK] launcher 已刷新`);
1072
- }
1073
- // 刷新 mcp.json 为 launcher 模式 (install_qoderwork.py 的 rewrite 逻辑)
1074
- const instScript = path.join(cwd, ".qoder", "scripts", "deployment", "setup", "install_qoderwork.py");
1075
- if (pyCmd && fs.existsSync(instScript)) {
1076
- try {
1077
- execSync(`${pyCmd} "${instScript}" --mcp-only`, { stdio: "pipe", timeout: 15000 });
1078
- console.log(` [OK] mcp.json 已刷新 (选装了依赖的Python, 避免MCP黄)`);
1079
- } catch (e) { /* --mcp-only 可能不支持, 静默跳过 */ }
1080
- }
1081
-
1082
- // ★ 共识:kg 走云平台 SSE(cli.js 最保险——npx 入口永远最新,不依赖旧版 install_qoderwork)
1083
- // 在 install_qoderwork 写完 mcp.json 后,cli.js 强制把 kg 改为 SSE url
1084
- const mcpFile = path.join(home, ".qoderwork", "mcp.json");
1085
- if (fs.existsSync(mcpFile)) {
1086
- try {
1087
- const mcp = JSON.parse(fs.readFileSync(mcpFile, "utf-8"));
1088
- if (mcp.mcpServers && mcp.mcpServers["qoder-knowledge-graph"]) {
1089
- mcp.mcpServers["qoder-knowledge-graph"] = {
1090
- "url": "http://10.89.7.120:10010/mcp",
1091
- "type": "sse",
1092
- "enabled": true,
1093
- };
1094
- fs.writeFileSync(mcpFile, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
1095
- console.log(` [OK] kg MCP → 云平台 SSE (http://10.89.7.120:10010/mcp)`);
1096
- }
1097
- } catch (e) { /* mcp.json 解析失败不阻塞 */ }
1098
- }
1099
- } catch (e) { /* 刷新失败不阻塞升级 */ }
1100
- } // end WLKJ_SKIP_QW_INSTALL guard
1101
-
1102
- // === 6.5. MCP 启动实测 (发 initialize 看哪个起不来) ===
1103
- // 这是"根治黄色"的关键一步: 配完 mcp.json 后立即模拟 QoderWork 发 initialize,
1104
- // 看每个 MCP 是 OK / DISABLED(绿) / CRASHED(黄)。有问题当场报, 不让用户重启后才发现。
1105
- // v3.x: check_mcp_launch.py 已合并进 protocol/mcp/mcp_doctor.py (实际启动诊断)。
1106
- console.log(`\n--- MCP 启动实测 (模拟 QoderWork 连接) ---`);
1107
- const diagScript = path.join(cwd, ".qoder", "scripts", "protocol", "mcp", "mcp_doctor.py");
1108
- if (pyCmd && fs.existsSync(diagScript)) {
1109
- try {
1110
- const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
1111
- execSync(`${pyCmd} "${diagScript}"`, { cwd, stdio: "inherit", timeout: 60000, env });
1112
- } catch (e) {
1113
- console.log(` [WARN] MCP 实测脚本异常 (不阻塞): ${(e.message || "").slice(0, 80)}`);
1114
- }
1115
- }
1116
-
1117
- // === 7. 自检: 关键文件到底齐不齐 ===
1118
- console.log(`\n--- 自检 (关键文件验证) ---`);
1119
- const checks = [
1120
- ["requirements.txt", path.join(cwd, "requirements.txt"), "依赖清单(没它pip装不了)"],
1121
- ["duckdb(pip)", null, "知识图谱核心依赖"],
1122
- ["kg.duckdb", path.join(cwd, "data", "index", "kg.duckdb"), "代码图谱(MCP需要)"],
1123
- ["kg_db.duckdb", path.join(cwd, "data", "index", "kg_db.duckdb"), "数据库图谱(table_impact需要)"],
1124
- [".qoder/commands/", path.join(cwd, ".qoder", "commands"), "斜杠命令"],
1125
- ];
1126
- let allOk = true;
1127
- for (const [label, p, desc] of checks) {
1128
- if (p === null) {
1129
- // duckdb 用 python 检测
1130
- try {
1131
- execSync(`${pyCmd || "python"} -c "import duckdb"`, { stdio: "pipe", timeout: 5000 });
1132
- console.log(` [✓] ${label} - ${desc}`);
1133
- } catch {
1134
- console.log(` [✗] ${label} 缺失! ${desc}`);
1135
- console.log(` → 修复: pip install duckdb -i https://pypi.tuna.tsinghua.edu.cn/simple`);
1136
- allOk = false;
1137
- }
1138
- } else if (fs.existsSync(p)) {
1139
- console.log(` [✓] ${label}`);
1140
- } else {
1141
- console.log(` [✗] ${label} 缺失! ${desc}`);
1142
- if (label.includes("duckdb")) {
1143
- // duckdb 是团队共享数据, 在 git 里 → 非管理员靠 git pull 拿; 管理员靠本地构建。
1144
- console.log(` → 修复: 先 npx @hupan56/wlkj doctor (拉团队数据); `);
1145
- console.log(` 管理员可 npx @hupan56/wlkj run kg_build.py --rebuild`);
1146
- } else if (label.includes("commands")) {
1147
- console.log(` → 修复: npx @hupan56/wlkj update`);
1148
- }
1149
- allOk = false;
1150
- }
1151
- }
1152
- if (!allOk) {
1153
- console.log(`\n ⚠️ 有缺失项, 按上面的提示修复后重跑 update。`);
1154
- } else {
1155
- console.log(` 全部关键文件就绪 ✓`);
1156
- }
1157
-
1158
- console.log(`\n${"=".repeat(50)}`);
1159
- console.log(` 升级完成! (v${PKG_VERSION})`);
1160
- console.log(`${"=".repeat(50)}`);
1161
- console.log(`\n 生效方式:`);
1162
- console.log(` Qoder IDE / Quest: 新建对话即可`);
1163
- console.log(` QoderWork: 重启应用或新建对话`);
1164
-
1165
- // 检查 git 身份是否需要配置 (只检测不引导, update 保持轻量)
1166
- // 老用户如果之前 init 时塞了假账号, 这里提示重跑 init 修正
1167
- let gitWarn = false;
1168
- try {
1169
- const devFile = path.join(cwd, ".qoder", ".developer");
1170
- if (fs.existsSync(devFile)) {
1171
- const devName = fs.readFileSync(devFile, "utf-8").trim();
1172
- const mj = path.join(cwd, "workspace", "members", devName, "member.json");
1173
- if (fs.existsSync(mj)) {
1174
- const m = JSON.parse(fs.readFileSync(mj, "utf-8"));
1175
- const ga = m.git_author || "";
1176
- if (!ga || ga.includes("@local") || ga.includes("@unknown")) {
1177
- gitWarn = true;
1178
- }
1179
- }
1180
- }
1181
- } catch { /* 检测失败不阻塞 */ }
1182
-
1183
- if (gitWarn) {
1184
- console.log(`\n ⚠ git 账号未配置或为假账号 (push/同步会失败)`);
1185
- console.log(` 重跑初始化可修复: npx @hupan56/wlkj init <你的名字>`);
1186
- console.log(` (init 会交互式引导你配置团队 git 账号和团队仓库)`);
1187
- } else {
1188
- console.log(`\n 如果还没配过 git 账号或团队仓库, 重跑: npx @hupan56/wlkj init <你的名字>`);
1189
- }
1190
-
1191
- if (protectedN > 0) {
1192
- console.log(`\n ⚠ 有配置文件新版有变化, 生成 .new 文件:`);
1193
- console.log(` 确认无需合并后可直接删除, 或手动合并后删除`);
1194
- }
1195
- console.log("");
1196
- }
1197
-
1198
- // ─────────────────────────────────────────────────────────────
1199
- // 一次性迁移: 把引擎代码从团队 git 跟踪里摘出来 (引擎数据分离)
1200
- // 团队管理员只跑一次, 其余成员 wlkj update 后自动同步。
1201
- // 原理: git rm -r --cached (只删 git 索引, 不删磁盘文件) + commit。
1202
- // 之后引擎文件仍在磁盘 (由 wlkj update 重新生成), 但不再被 git 跟踪 →
1203
- // 彻底消除"引擎更新污染团队 git"和"新机 init 产生 unrelated histories"。
1204
- // 团队数据 (.qoder/config.yaml, settings.json, data/learning/, data/, workspace/) 保持不动。
1205
- // ─────────────────────────────────────────────────────────────
1206
- function doMigrate() {
1207
- const { execSync } = require("child_process");
1208
- const cwd = process.cwd();
1209
- const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
1210
- function _es(cmd, opts = {}) {
1211
- return execSync(cmd, { cwd, encoding: "utf-8", timeout: 30000, stdio: "pipe", env, ...opts });
1212
- }
1213
-
1214
- console.log("");
1215
- console.log("=== wlkj migrate: 引擎代码改为 npm 生成 (引擎数据分离) ===");
1216
- console.log("");
1217
-
1218
- // 1. 必须在 git 仓库里
1219
- if (!fs.existsSync(path.join(cwd, ".git"))) {
1220
- console.log(" ✗ 当前目录不是 git 仓库。请在团队仓库根目录运行。");
1221
- console.log("");
1222
- return;
1223
- }
1224
-
1225
- // 2. 要从 git 跟踪摘除的引擎目录 (团队数据 config/data/learning 等不在此列)
1226
- const ENGINE_DIRS = [
1227
- ".qoder/scripts", ".qoder/skills", ".qoder/commands", ".qoder/agents",
1228
- ".qoder/contracts", ".qoder/hooks", ".qoder/templates", ".qoder/rules",
1229
- ];
1230
-
1231
- // 3. 哪些目录当前确被跟踪 (只摘这些, 避免在已迁移过的仓库上空跑)
1232
- const tracked = [];
1233
- for (const d of ENGINE_DIRS) {
1234
- let isTracked = false;
1235
- try {
1236
- // git ls-files 能列出被跟踪的文件; 有输出说明该目录还有文件在跟踪
1237
- const out = _es(`git ls-files "${d}"`, { timeout: 10000 });
1238
- if (out.trim()) isTracked = true;
1239
- } catch { /* 命令失败当未跟踪 */ }
1240
- if (isTracked) tracked.push(d);
1241
- }
1242
-
1243
- if (tracked.length === 0) {
1244
- console.log(" ✓ 引擎目录已不在 git 跟踪中, 无需迁移。");
1245
- console.log(" (这通常是已经迁移过了, 或是个全新仓库)");
1246
- console.log("");
1247
- console.log(" 下一步:");
1248
- console.log(" 全新机器: npx @hupan56/wlkj init <名字> <角色>");
1249
- console.log(" 老用户: npx @hupan56/wlkj update");
1250
- console.log(" 自检: npx @hupan56/wlkj selftest");
1251
- console.log("");
1252
- return;
1253
- }
1254
-
1255
- console.log(` 将从 git 跟踪中移除 (磁盘文件保留, 不删):`);
1256
- for (const d of tracked) console.log(` - ${d}`);
1257
- console.log("");
1258
-
1259
- // 4. 先确保 .gitignore 已含引擎忽略规则 (防止 rm --cached 后又被加回)
1260
- // 逐条补缺, 不重复不乱插。
1261
- const giPath = path.join(cwd, ".gitignore");
1262
- const RULES_TO_ENSURE = [
1263
- "# 引擎代码 (由 npm 包 @hupan56/wlkj init/update 生成, 不进团队 git)",
1264
- ".qoder/scripts/", ".qoder/skills/", ".qoder/commands/", ".qoder/agents/",
1265
- ".qoder/contracts/", ".qoder/hooks/", ".qoder/templates/", ".qoder/rules/",
1266
- ".qoder/repowiki/", ".qoder/archive/",
1267
- ".wlkj-pre-reset/",
1268
- ];
1269
- let gi = fs.existsSync(giPath) ? fs.readFileSync(giPath, "utf-8") : "";
1270
- const existingLines = new Set(gi.split(/\r?\n/));
1271
- const missing = RULES_TO_ENSURE.filter(rule => !existingLines.has(rule));
1272
- if (missing.length > 0) {
1273
- // 缺哪条补哪条 (注释行单独补, 规则行单独补), 补在最前面
1274
- gi = missing.join("\n") + "\n" + (gi ? "\n" + gi : "");
1275
- fs.writeFileSync(giPath, gi, "utf-8");
1276
- console.log(` ✓ .gitignore 已补 ${missing.length} 条忽略规则`);
1277
- }
1278
-
1279
- // 5. git rm -r --cached (只删索引, 不删磁盘文件)
1280
- console.log(" 从 git 索引移除 (git rm -r --cached, 磁盘文件不动)...");
1281
- try {
1282
- _es(`git rm -r --cached ${tracked.map(t => `"${t}"`).join(" ")}`, { timeout: 60000 });
1283
- console.log(" ✓ 已移除");
1284
- } catch (e) {
1285
- console.log(` ✗ git rm 失败: ${(e.stderr || e.message || "").slice(0, 200)}`);
1286
- console.log(" 可手动跑: " + tracked.map(t => `git rm -r --cached ${t}`).join(" && "));
1287
- return;
1288
- }
1289
-
1290
- // 6. 自动 commit (标记这次架构变更)
1291
- console.log(" 提交迁移...");
1292
- try {
1293
- _es(`git add -A .gitignore`, { timeout: 10000 });
1294
- _es(`git commit -m "chore(wlkj): 引擎代码改为 npm 生成, 不再进团队 git (引擎数据分离)"`, { timeout: 30000 });
1295
- console.log(" ✓ 已提交");
1296
- } catch (e) {
1297
- console.log(` ⚠ 自动提交失败 (可手动 commit): ${(e.stderr || e.message || "").slice(0, 150)}`);
1298
- }
1299
-
1300
- console.log("");
1301
- console.log("=== 迁移完成 ===");
1302
- console.log(" 接下来:");
1303
- console.log(" 1. npx @hupan56/wlkj sync # 推到团队远程");
1304
- console.log(" 2. 团队其他成员各自跑: npx @hupan56/wlkj update");
1305
- console.log(" (本地引擎文件不会被删, 之后由 update 重新生成, 互不冲突)");
1306
- console.log("");
1307
- }
1308
-
1309
- function doHelp() {
1310
- console.log("");
1311
- console.log(`wlkj - AI 产品研发工作流 (v${PKG_VERSION})`);
1312
- console.log("");
1313
- console.log("=== 终端只用 3 个 (从下载到日常) ===");
1314
- console.log("");
1315
- console.log(" npx @hupan56/wlkj install-env 首次: 装环境 (Node/Python/git/pip)");
1316
- console.log(" npx @hupan56/wlkj init [名字] 首次: 装引擎 + 初始化身份");
1317
- console.log(" npx @hupan56/wlkj update 日常: 升级 (刷新引擎+补依赖+刷MCP)");
1318
- console.log("");
1319
- console.log("=== 日常工作全在 Qoder 里 (不用回终端) ===");
1320
- console.log(" 说中文: '写个XX的需求' '查XX代码' '建任务' '测一下XX'");
1321
- console.log(" 或斜杠: /wl-prd /wl-design /wl-task /wl-code /wl-test /wl-commit");
1322
- console.log(" (提交/同步 /wl-task 和 /wl-commit 会自动做, 不用单独 npx sync)");
1323
- console.log("");
1324
- console.log("=== 能力市场 ===");
1325
- console.log(" npx @hupan56/wlkj install skill <token> --host <url> 从市场装 Skill 到本地 IDE");
1326
- console.log(" npx @hupan56/wlkj install agent <token> --host <url> 从市场装 Agent (含依赖 Skill)");
1327
- console.log("");
1328
- console.log("=== 进阶 (管理员/特殊情况才用, 不常用) ===");
1329
- console.log(" npx @hupan56/wlkj selftest 全套件自检 (跨环境+QoderWork工具协议+smoke+体检+MCP)");
1330
- console.log(" npx @hupan56/wlkj doctor 体检 (或在 Qoder 里 /wl-init 无参数)");
1331
- console.log(" npx @hupan56/wlkj migrate 老团队一次性升级到 引擎数据分离架构");
1332
- console.log(" npx @hupan56/wlkj run <脚本> 万能跑脚本 (AI 内部用, 用户很少需要)");
1333
- console.log("");
1334
- console.log(" 看不到命令? 新建对话 / 重启 QoderWork");
1335
- console.log(" 环境问题? python .qoder/scripts/setup/init_doctor.py --fix");
1336
- console.log("");
1337
- }
1338
-
1339
- function doInstallEnv(checkOnly = false, location = null) {
1340
- const { execSync } = require("child_process");
1341
- const isWin = process.platform === "win32";
1342
- const isMac = process.platform === "darwin";
1343
-
1344
- // 解析 --location(CLI 入口已提取,这里兜底)
1345
- console.log("\n=== 环境检测 ===\n");
1346
- if (location) {
1347
- console.log(` 安装位置: ${location}${isWin ? "" : " (仅 Windows winget 支持指定目录)"}`);
1348
- // Windows: 预建目录
1349
- if (isWin) {
1350
- try { fs.mkdirSync(location, { recursive: true }); } catch { /* */ }
1351
- }
1352
- }
1353
-
1354
- // 检测函数
1355
- function check(cmd) {
1356
- try { return execSync(`${cmd} --version`, { encoding: "utf-8", timeout: 5000, stdio: "pipe" }).trim().split("\n")[0]; }
1357
- catch { return null; }
1358
- }
1359
- function has(cmd) {
1360
- try { execSync(`where ${cmd}`, { encoding: "utf-8", timeout: 3000, stdio: "pipe" }); return true; }
1361
- catch { try { execSync(`which ${cmd}`, { encoding: "utf-8", timeout: 3000, stdio: "pipe" }); return true; } catch { return false; } }
1362
- }
1363
- // Windows: 把目录加到用户 PATH(幂等,已存在不重复加)
1364
- function addToPathWin(dir) {
1365
- if (!isWin) return false;
1366
- try {
1367
- // 用 setx 会截断超长 PATH,改用 PowerShell 读 User PATH 追加
1368
- const ps = `powershell -NoProfile -Command "$p=[Environment]::GetEnvironmentVariable('Path','User'); if($p -notlike '*{dir}*'){[Environment]::SetEnvironmentVariable('Path',$p.TrimEnd(';')+';{dir}','User')}"`.replace(/{dir}/g, dir.replace(/\\/g, "\\\\"));
1369
- execSync(ps, { stdio: "pipe", timeout: 15000 });
1370
- return true;
1371
- } catch (e) { console.log(` [WARN] 加 PATH 失败(可手动加): ${dir}`); return false; }
1372
- }
1373
- function install(name, wingetId, brewPkg) {
1374
- if (checkOnly) return false;
1375
- try {
1376
- if (isWin && has("winget")) {
1377
- console.log(` 用 winget 装 ${name}...`);
1378
- // winget --location 不一定被安装包尊重,但先传上
1379
- let cmd = `winget install --id ${wingetId} -e --source winget --accept-source-agreements --accept-package-agreements`;
1380
- if (location) cmd += ` --location "${location}"`;
1381
- execSync(cmd, { stdio: "inherit", timeout: 300000 });
1382
- return true;
1383
- } else if (isMac && has("brew")) {
1384
- console.log(` 用 brew 装 ${name}...`);
1385
- execSync(`brew install ${brewPkg}`, { stdio: "inherit", timeout: 300000 });
1386
- return true;
1387
- } else if (!isWin && !isMac) {
1388
- // Linux: 尝试 apt/dnf/yum
1389
- for (const [mgr, pkg] of [["apt", brewPkg], ["dnf", brewPkg]]) {
1390
- if (has(mgr)) {
1391
- console.log(` 用 ${mgr} 装 ${name}...`);
1392
- execSync(`sudo ${mgr} install -y ${pkg}`, { stdio: "inherit", timeout: 300000 });
1393
- return true;
1394
- }
1395
- }
1396
- }
1397
- } catch (e) { console.log(` 自动安装失败: ${(e.message || "").slice(0, 80)}`); }
1398
- return false;
1399
- }
1400
-
1401
- const nodeVer = check("node");
1402
- const pyVer = check("python") || check("python3");
1403
- const gitVer = check("git");
1404
-
1405
- console.log(` Node.js: ${nodeVer ? "[OK] " + nodeVer : "[缺失] (npx 命令需要)"}`);
1406
- console.log(` Python: ${pyVer ? "[OK] " + pyVer : "[缺失] (引擎需要)"}`);
1407
- console.log(` git: ${gitVer ? "[OK] " + gitVer : "[缺失] (可选, 核心功能不依赖)"}`);
1408
-
1409
- if (checkOnly) {
1410
- console.log("\n仅检测。去掉 --check 自动安装。");
1411
- return;
1412
- }
1413
-
1414
- const need = [];
1415
- if (!nodeVer) {
1416
- console.log("");
1417
- if (nodeVer || install("Node.js", "OpenJS.NodeJS.LTS", "node")) { console.log(` [OK] Node.js 已装`); }
1418
- else { need.push("Node.js (手动: https://nodejs.org)"); }
1419
- }
1420
- if (!pyVer) {
1421
- console.log("");
1422
- if (install("Python", "Python.Python.3.12", "python@3.12")) { console.log(` [OK] Python 已装`); }
1423
- else { need.push("Python (手动: https://python.org, 勾 Add to PATH)"); }
1424
- }
1425
- if (!gitVer) {
1426
- console.log("");
1427
- if (install("git", "Git.Git", "git")) { console.log(` [OK] git 已装`); }
1428
- else { need.push("git (手动: https://git-scm.com, 可选)"); }
1429
- }
1430
-
1431
- // --location 模式: 提示用户验证安装位置 + 手动加 PATH
1432
- // (winget 的 --location 对 Node/Python 不一定生效,需用户确认)
1433
- if (location && isWin) {
1434
- console.log(`\n--- 安装位置确认 ---`);
1435
- console.log(` 你指定了: ${location}`);
1436
- console.log(` 注意: winget 的 --location 对 Node.js/Python 不一定生效`);
1437
- console.log(` (取决于安装包是否支持自定义路径)`);
1438
- console.log(` 请检查实际装到哪了:`);
1439
- console.log(` 重新打开终端后跑: node --version python --version git --version`);
1440
- console.log(` 如果某软件没进 PATH, 手动把它加进去:`);
1441
- console.log(` 设置 → 系统 → 关于 → 高级系统设置 → 环境变量 → Path → 新建`);
1442
- console.log(` 或用命令: npx @hupan56/wlkj add-path "D:\\wldev\\nodejs"`);
1443
- }
1444
-
1445
- // Python pip 依赖 (知识图谱/MCP/测试需要)
1446
- console.log("\n--- Python 依赖 ---");
1447
- const pyCmd = check("python") ? "python" : (check("python3") ? "python3" : null);
1448
- if (pyCmd) {
1449
- // 找 requirements.txt: 引擎已安装则用 .qoder/scripts/../requirements.txt
1450
- const reqPaths = [
1451
- path.join(process.cwd(), "requirements.txt"),
1452
- path.join(process.cwd(), ".qoder", "..", "requirements.txt"),
1453
- ].filter(p => fs.existsSync(p));
1454
- if (reqPaths.length > 0) {
1455
- console.log(` 安装 pip 依赖: ${path.basename(reqPaths[0])} (失败自动切国内镜像)`);
1456
- installPipDeps(pyCmd, reqPaths[0]);
1457
- } else {
1458
- console.log(" [跳过] 未找到 requirements.txt (先 npx @hupan56/wlkj init 安装引擎)");
1459
- }
1460
- } else {
1461
- console.log(" [跳过] Python 未装, 无法装 pip 依赖");
1462
- }
1463
-
1464
- console.log("\n=== 结果 ===");
1465
- if (need.length === 0) {
1466
- console.log(" 环境就绪! 下一步: npx @hupan56/wlkj init");
1467
- console.log(" (新装的可能需要重新打开终端)");
1468
- } else {
1469
- console.log(" 以下需手动安装:");
1470
- need.forEach(n => console.log(" - " + n));
1471
- }
1472
- }
1473
-
1474
- // add-path: 把目录加到 Windows 用户 PATH(幂等)
1475
- function doAddPath(dir) {
1476
- if (!dir) { console.log("用法: npx @hupan56/wlkj add-path <目录>"); return; }
1477
- if (process.platform !== "win32") {
1478
- console.log("add-path 仅支持 Windows。Mac/Linux 请手动加到 ~/.zshrc 或 ~/.bashrc");
1479
- return;
1480
- }
1481
- const abs = path.resolve(dir);
1482
- if (!fs.existsSync(abs)) {
1483
- console.log(`[警告] 目录不存在: ${abs}(仍会加到 PATH)`);
1484
- }
1485
- try {
1486
- const ps = `powershell -NoProfile -Command "$p=[Environment]::GetEnvironmentVariable('Path','User'); if($p -notlike '*${abs.replace(/\\/g,"\\\\")}*'){[Environment]::SetEnvironmentVariable('Path',$p.TrimEnd(';')+';${abs.replace(/\\/g,"\\\\")}','User'); Write-Output 'ADDED'} else { Write-Output 'EXISTS' }"`;
1487
- const out = execSync(ps, { encoding: "utf-8", timeout: 15000 }).trim();
1488
- console.log(` ${out === "ADDED" ? "已加入" : "已存在(无需重复加)"}: ${abs}`);
1489
- console.log(` ⚠ 需要重新打开终端才生效`);
1490
- } catch (e) {
1491
- console.log(` 失败: ${(e.message || "").slice(0, 100)}`);
1492
- console.log(` 手动加: 设置 → 系统 → 关于 → 高级系统设置 → 环境变量 → Path → 新建 → ${abs}`);
1493
- }
1494
- }
1495
-
1496
- const [,, cmd, ...rest] = process.argv;
1497
- const mapped = rest.map(a => a === "-p" ? "--priority" : a);
1498
-
1499
- switch (cmd) {
1500
- case "init": doInit(rest[0], rest[1]); break;
1501
- case "update": case "upgrade": doUpdate(); break;
1502
- case "migrate": doMigrate(); break;
1503
- case "install-env": {
1504
- const checkOnly = rest.includes("--check");
1505
- // 提取 --location <dir> 或 --location=<dir>
1506
- let loc = null;
1507
- const li = rest.indexOf("--location");
1508
- if (li !== -1 && rest[li + 1]) loc = rest[li + 1];
1509
- const eq = rest.find(a => a.startsWith("--location="));
1510
- if (eq) loc = eq.slice("--location=".length);
1511
- doInstallEnv(checkOnly, loc);
1512
- break;
1513
- }
1514
- case "add-path": doAddPath(rest[0]); break;
1515
- case "task": process.stdout.write(py("task.py", mapped)); break;
1516
- case "status": doStatus(); break;
1517
- case "session": process.stdout.write(py("add_session.py", rest)); break;
1518
- case "help": case "--help": case "-h": doHelp(); break;
1519
-
1520
- // ── 核心 5 个: 装/升/体检/同步/万能跑 ──
1521
- case "doctor": case "体检":
1522
- runScript("init_doctor.py", rest); break;
1523
- case "sync": case "push": case "commit":
1524
- runScript("team_sync.py", ["push", ...rest.filter(a => a !== "push")]); break;
1525
- case "pull":
1526
- runScript("team_sync.py", ["pull"]); break;
1527
-
1528
- // ── 万能透传: npx wlkj run <脚本名> [参数...] ──
1529
- // 任何 .qoder/scripts/ 下的脚本都能跑, 不用记命令名
1530
- // 例: npx wlkj run kg_build.py --rebuild
1531
- // npx wlkj run eval_prd.py xxx.md
1532
- // npx wlkj run autotest.py quick
1533
- case "run": {
1534
- // 万能跑脚本 (AI 内部用, 用户日常不直接用 — 用 /命令 或 init/update)
1535
- if (!rest[0]) { console.log("用法: npx wlkj run <脚本名> [参数...]\n例: npx wlkj run kg_build.py --rebuild"); break; }
1536
- runScript(rest[0], rest.slice(1));
1537
- break;
1538
- }
1539
-
1540
- case "提交": case "拉取最新": case "同步":
1541
- case "查看状态": case "提交PRD": case "提交Spec": case "提交任务":
1542
- gitOp(cmd); break;
1543
-
1544
- // ── 自检: 一条命令跑完所有验证套件 (替代 7 步手动清单的"环境验证"部分) ──
1545
- // 用法: npx @hupan56/wlkj selftest
1546
- // 跑: cross-env 模拟 + QoderWork 工具协议 + smoke + doctor + mcp_doctor
1547
- case "selftest": case "自检": {
1548
- const { spawnSync: _ss } = require("child_process");
1549
- const cwd = process.cwd();
1550
- const pyCmd = detectPyCmd();
1551
-
1552
- // ⚠ 引擎未安装时不能跑 selftest (所有路径都不存在, 5 FAIL 无意义)
1553
- const engineDir = path.join(cwd, ".qoder", "scripts");
1554
- if (!fs.existsSync(engineDir)) {
1555
- console.log("\n⚠ 引擎未安装! 先跑:");
1556
- console.log(" 全新机器: npx @hupan56/wlkj init <名字> <角色>");
1557
- console.log(" 老用户: npx @hupan56/wlkj update");
1558
- console.log("");
1559
- process.exit(1);
1560
- break;
1561
- }
1562
-
1563
- const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1", PYTHONPATH: path.join(cwd, ".qoder", "scripts") };
1564
- console.log("\n" + "=".repeat(56));
1565
- console.log(" wlkj selftest — 全套件自检 (验证跨环境 + 第一宿主)");
1566
- console.log("=".repeat(56));
1567
- let totalFail = 0;
1568
- // 找 packages/wlkj/tests/ 目录:可能在 cwd 下(项目根),也可能在 npx 缓存里
1569
- // 用 __dirname (cli.js 所在目录) 往上找
1570
- let testsDir = path.join(__dirname, "..", "tests");
1571
- if (!fs.existsSync(testsDir)) {
1572
- // fallback: cwd 下找
1573
- testsDir = path.join(cwd, "packages", "wlkj", "tests");
1574
- }
1575
- const suites = [
1576
- ["1. 跨环境模拟验证", [path.join(testsDir, "test_cross_env.py")]],
1577
- ["2. QoderWork 工具协议 (真实 MCP JSON-RPC)", [path.join(testsDir, "test_qoderwork_tools.py")]],
1578
- ["3. 命令底层脚本 smoke", ["-c", "import sys, os; p=os.path.join(os.getcwd(), '.qoder', 'scripts', 'validation', 'test', 'smoke_all_commands.py'); sys.path.insert(0, os.path.join(os.getcwd(), '.qoder', 'scripts')); exec(open(p).read()) if os.path.isfile(p) else print('[SKIP] smoke_all_commands.py 不存在 (init 后才有)')"]],
1579
- ["4. 环境体检 (doctor)", [path.join(cwd, ".qoder", "scripts", "orchestration", "wlkj.py"), "doctor"]],
1580
- ["5. MCP 实测 (mcp_doctor)", [path.join(cwd, ".qoder", "scripts", "protocol", "mcp", "mcp_doctor.py")]],
1581
- ];
1582
- for (const [label, args] of suites) {
1583
- console.log("\n--- " + label + " ---");
1584
- if (!pyCmd) { console.log(" [SKIP] Python 未装"); continue; }
1585
- // suites 1-2 在 packages/wlkj/tests/ 下 (相对 cwd), 3-5 用绝对路径
1586
- const isTestFile = args[0] && args[0].endsWith(".py") && !args[0].startsWith(path.sep) && !args[0].includes(":");
1587
- const fullArgs = isTestFile ? [path.join(cwd, args[0])] : args;
1588
- try {
1589
- const r = _ss(pyCmd, fullArgs, { cwd, stdio: "inherit", timeout: 300000, env });
1590
- if (r.status !== 0) { console.log(" [FAIL] exit " + r.status); totalFail++; }
1591
- } catch (e) { console.log(" [FAIL] " + (e.message || "").slice(0, 80)); totalFail++; }
1592
- }
1593
- console.log("\n" + "=".repeat(56));
1594
- console.log(totalFail === 0 ? " ✓ 全部自检通过" : " ⚠ " + totalFail + " 个套件失败, 见上方");
1595
- console.log("=".repeat(56) + "\n");
1596
- process.exit(totalFail === 0 ? 0 : 1);
1597
- break;
1598
- }
1599
-
1600
- // ── 能力市场安装: npx @hupan56/wlkj install <skill|agent> <token> --host <url> ──
1601
- case "install": {
1602
- const kind = rest[0]; // skill / agent
1603
- const token = rest[1]; // share_token
1604
- const hostIdx = rest.indexOf("--host");
1605
- const host = hostIdx !== -1 && rest[hostIdx + 1] ? rest[hostIdx + 1] : "http://127.0.0.1:10010";
1606
-
1607
- if (!kind || !token || !["skill", "agent"].includes(kind)) {
1608
- console.log("用法: npx @hupan56/wlkj install <skill|agent> <token> [--host <url>]");
1609
- console.log("例: npx @hupan56/wlkj install skill abc123 --host http://127.0.0.1:10010");
1610
- process.exit(1);
1611
- }
1612
-
1613
- (async () => {
1614
- try {
1615
- // 探测本地 IDE
1616
- const cwd = process.cwd();
1617
- let ide = "claude";
1618
- if (fs.existsSync(path.join(cwd, ".cursor"))) ide = "cursor";
1619
- else if (fs.existsSync(path.join(cwd, ".qoder"))) ide = "qoder";
1620
- else if (fs.existsSync(path.join(cwd, ".claude"))) ide = "claude";
1621
- else if (fs.existsSync(path.join(cwd, "CLAUDE.md"))) ide = "claude";
1622
- else if (fs.existsSync(path.join(cwd, "AGENTS.md"))) ide = "codex";
1623
-
1624
- if (kind === "agent") {
1625
- console.log(`→ 从 ${host} 拉取 Agent ${token}...`);
1626
- const res = await fetch(`${host}/api/market/agent/${token}`);
1627
- if (!res.ok) throw new Error(`API 返回 ${res.status}`);
1628
- const agent = await res.json();
1629
- console.log(`✓ ${agent.icon || "🤖"} ${agent.title}`);
1630
-
1631
- // 写入 IDE
1632
- if (ide === "claude" || ide === "codex") {
1633
- const dir = path.join(cwd, ".claude", "commands");
1634
- fs.mkdirSync(dir, { recursive: true });
1635
- const fname = (agent.name || agent.title || "agent") + ".md";
1636
- let content = `---\ndescription: ${agent.title || ""}\n---\n\n`;
1637
- content += agent.rolePrompt || agent.role_prompt || `你是${agent.title || "AI助手"}。\n`;
1638
- if ((agent.toolIds || []).length) {
1639
- content += `\n## 可用工具\n${(agent.toolIds || []).join(", ")}\n`;
1640
- }
1641
- if ((agent.skillIds || []).length) {
1642
- content += `\n## 可用技能\n${(agent.skillIds || []).join(", ")}\n`;
1643
- }
1644
- fs.writeFileSync(path.join(dir, fname), content, "utf-8");
1645
- console.log(`✓ 已写入: ${path.join(dir, fname)}`);
1646
- } else if (ide === "qoder") {
1647
- const dir = path.join(cwd, ".qoder", "agents");
1648
- fs.mkdirSync(dir, { recursive: true });
1649
- const fname = (agent.name || agent.title || "agent") + ".json";
1650
- fs.writeFileSync(path.join(dir, fname), JSON.stringify({
1651
- name: agent.name, title: agent.title,
1652
- systemPrompt: agent.rolePrompt || agent.role_prompt || "",
1653
- tools: agent.toolIds || [], skills: agent.skillIds || [],
1654
- model: agent.model || null,
1655
- }, null, 2), "utf-8");
1656
- console.log(`✓ 已写入: ${path.join(dir, fname)}`);
1657
- } else if (ide === "cursor") {
1658
- const dir = path.join(cwd, ".cursor", "rules");
1659
- fs.mkdirSync(dir, { recursive: true });
1660
- const fname = (agent.name || agent.title || "agent") + ".mdc";
1661
- let content = `---\ndescription: ${agent.title || ""}\n---\n`;
1662
- content += agent.rolePrompt || agent.role_prompt || "";
1663
- fs.writeFileSync(path.join(dir, fname), content, "utf-8");
1664
- console.log(`✓ 已写入: ${path.join(dir, fname)}`);
1665
- }
1666
-
1667
- // 递归装依赖 Skill
1668
- const skillIds = agent.skillIds || [];
1669
- for (const sid of skillIds) {
1670
- if (typeof sid === "string" && sid.length > 5) {
1671
- console.log(`→ 递归安装 Skill: ${sid}`);
1672
- try {
1673
- const sres = await fetch(`${host}/api/market/skill/${sid}`);
1674
- if (sres.ok) {
1675
- const skill = await sres.json();
1676
- const sdir = path.join(cwd, ".claude", "skills", skill.name || sid);
1677
- fs.mkdirSync(sdir, { recursive: true });
1678
- let smd = `---\nname: ${skill.name || sid}\ndescription: ${skill.description || ""}\n---\n`;
1679
- smd += skill.promptTemplate || skill.prompt_template || "";
1680
- fs.writeFileSync(path.join(sdir, "SKILL.md"), smd, "utf-8");
1681
- console.log(` ✓ ${skill.title || sid}`);
1682
- } else { console.warn(` ⚠ Skill ${sid} 不存在或未发布`); }
1683
- } catch (e) { console.warn(` ⚠ Skill ${sid} 安装失败: ${e.message}`); }
1684
- }
1685
- }
1686
- console.log(`\n✅ Agent "${agent.title}" 已安装(${ide} 格式)`);
1687
- }
1688
-
1689
- else if (kind === "skill") {
1690
- console.log(`→ 从 ${host} 拉取 Skill ${token}...`);
1691
- const res = await fetch(`${host}/api/market/skill/${token}`);
1692
- if (!res.ok) throw new Error(`API 返回 ${res.status}`);
1693
- const skill = await res.json();
1694
- console.log(`✓ ${skill.icon || "🛠"} ${skill.title}`);
1695
-
1696
- const sname = skill.name || token;
1697
- const sdir = path.join(cwd, ".claude", "skills", sname);
1698
- fs.mkdirSync(sdir, { recursive: true });
1699
- let smd = `---\nname: ${sname}\ntitle: ${skill.title || ""}\ndescription: ${skill.description || ""}\nicon: ${skill.icon || ""}\nversion: ${skill.version || "1.0.0"}\n---\n`;
1700
- smd += skill.promptTemplate || skill.prompt_template || "";
1701
- fs.writeFileSync(path.join(sdir, "SKILL.md"), smd, "utf-8");
1702
- console.log(`✓ 已写入: ${path.join(sdir, "SKILL.md")}`);
1703
- console.log(`\n✅ Skill "${skill.title}" 已安装`);
1704
- }
1705
- } catch (e) {
1706
- console.error(`✗ 安装失败: ${e.message}`);
1707
- process.exit(1);
1708
- }
1709
- })();
1710
- break;
1711
- }
1712
-
1713
- default: doHelp();
1
+ #!/usr/bin/env node
2
+ // wlkj - workflow toolkit
3
+
4
+ // Windows 中文乱码修复
5
+ if (process.platform === "win32") {
6
+ try { execSync("chcp 65001", { stdio: "ignore" }); } catch (e) {}
7
+ process.stdout.setDefaultEncoding("utf-8");
8
+ process.stderr.setDefaultEncoding("utf-8");
9
+ }
10
+
11
+ const path = require("path");
12
+ const fs = require("fs");
13
+ const { execSync } = require("child_process");
14
+
15
+ const T = path.join(__dirname, "..", "templates");
16
+
17
+ // 当前 npm 包版本(从 package.json 读,不硬编码)
18
+ const PKG_VERSION = JSON.parse(
19
+ fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")
20
+ ).version;
21
+
22
+ // 升级时受保护的用户数据文件(绝不覆盖)
23
+ // 这些文件含团队/机器特定配置,覆盖 = 数据丢失
24
+ const PROTECTED_FILES = new Set([
25
+ "config.yaml", // 团队 git 仓库 URL、平台映射
26
+ "settings.json", // 本地权限/hook 配置
27
+ ]);
28
+
29
+ // 引擎产物目录:强制覆盖(不走三路快照保护)
30
+ // commands+skills 是纯引擎定义文件,用户不该改,强制覆盖安全
31
+ // scripts/rules/hooks/contracts/templates 走三路快照(可能被另一个会话/用户改过)
32
+ const FORCE_OVERWRITE_DIRS = new Set([
33
+ "commands", // /wl-*.md 命令定义(引擎产物,MCP铁律等)
34
+ "skills", // SKILL.md 技能定义(AI读这个决定流程)
35
+ ]);
36
+
37
+ // 本地状态文件(升级时也绝不碰,但不算"受保护配置",不提示合并)
38
+ const LOCAL_STATE_FILES = new Set([
39
+ ".developer", ".current-task", ".engine-version",
40
+ ]);
41
+
42
+ // 探测可用的 Python 命令 (python 或 python3)。
43
+ // macOS 系统通常只有 python3, 硬编码 python 会让 Mac 全员失败。
44
+ // 探测结果缓存到全局, 避免每次调用都 fork 进程。
45
+ let _PY_CMD_CACHE = null;
46
+ function detectPyCmd() {
47
+ if (_PY_CMD_CACHE) return _PY_CMD_CACHE;
48
+ // ⚠ 必须校验是 Python 3.8+! 不能只看 exit 0 ——
49
+ // 老企业机/某些 RHEL 上 `python` 仍指向 2.7 (exit 0), 引擎要 3.8+,
50
+ // 误选 2.7 会被缓存 → 后续所有 py()/runScript 全用 2.7 跑 → setup.py 直接失败。
51
+ // 检查 --version 输出以 "Python 3." 开头, 且次版本 >=8。
52
+ for (const c of ["python", "python3"]) {
53
+ try {
54
+ const out = execSync(`${c} --version`, { encoding: "utf-8", timeout: 5000, stdio: "pipe" });
55
+ const m = /Python\s+3\.(\d+)/i.exec((out || "").trim());
56
+ if (m && parseInt(m[1], 10) >= 8) {
57
+ _PY_CMD_CACHE = c;
58
+ return c;
59
+ }
60
+ // 是 python 但版本不够 (2.7 或 3.0-3.7) → 跳过试下一个候选
61
+ } catch { /* try next */ }
62
+ }
63
+ _PY_CMD_CACHE = null;
64
+ return null;
65
+ }
66
+
67
+ function py(script, args = []) {
68
+ // v3.x: 不再直接找扁平路径 (scripts/<script>), 改为转发 wlkj.py 单一真源调度器。
69
+ // 根治 "引擎分层重组后 cli.js 路径全部失效" 的载体漂移。
70
+ const wlkj = path.join(process.cwd(), ".qoder", "scripts", "orchestration", "wlkj.py");
71
+ if (!fs.existsSync(wlkj)) { console.log("请先运行: npx wlkj init"); process.exit(1); }
72
+ const pyCmd = detectPyCmd();
73
+ if (!pyCmd) { console.log("Python 未安装, 无法运行脚本。跑: npx @hupan56/wlkj install-env"); return ""; }
74
+ // 把脚本名当命令传给 wlkj.py (DISPATCH 命中则路由, 否则 run fallback 递归找)
75
+ try {
76
+ const cmdName = script.replace(/\.py$/, "").replace(/\//g, "-");
77
+ return execSync(`${pyCmd} "${wlkj}" ${cmdName} ${args.map(a => `"${a}"`).join(" ")}`, { cwd: process.cwd(), encoding: "utf-8", timeout: 15000, env: { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" } });
78
+ } catch (e) { return (e.stdout || e.message); }
79
+ }
80
+
81
+ // 透传执行: 直接 spawn (不捕获输出, 实时显示, 支持长超时和交互式)
82
+ // 用于 kg_build/eval_prd/autotest 等耗时脚本, 以及需要交互的场景
83
+ function runScript(scriptName, args = [], timeoutMs = 600000) {
84
+ // v3.x: 转发 wlkj.py 单一真源 (不再直接找扁平 scripts/<scriptName>)
85
+ const wlkj = path.join(process.cwd(), ".qoder", "scripts", "orchestration", "wlkj.py");
86
+ if (!fs.existsSync(wlkj)) { console.log("引擎未安装。跑: npx @hupan56/wlkj init"); process.exit(1); }
87
+ const pyCmd = detectPyCmd();
88
+ if (!pyCmd) { console.log("Python 未安装。跑: npx @hupan56/wlkj install-env"); process.exit(1); }
89
+ const { spawnSync } = require("child_process");
90
+ const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
91
+ // 脚本名作为命令传给 wlkj.py (DISPATCH 命中则路由, 否则 run fallback 递归找)
92
+ const cmdName = scriptName.replace(/\.py$/, "").replace(/\//g, "-");
93
+ const r = spawnSync(pyCmd, [wlkj, cmdName, ...args], { cwd: process.cwd(), stdio: "inherit", timeout: timeoutMs, encoding: "utf-8", env });
94
+ process.exit(r.status || 0);
95
+ }
96
+
97
+ // 带镜像兜底的 pip 安装 —— 复用 Python 的 common/pip_install.py (单一事实源)
98
+ // 为什么不复用 Python: 上一轮给 init_doctor 做了 common/pip_install.py (失败切清华/阿里/腾讯)。
99
+ // cli.js 的 install-env/update 是独立 JS 路径, 之前没享受镜像兜底 → 50 人里弱网用户全崩。
100
+ // 这里调用同一个 Python 工具, 保证两路径行为一致。
101
+ //
102
+ // 优先: 引擎已装 → 调 foundation.utils.pip_install (有镜像兜底)
103
+ // 回退: 引擎没装 → 裸 pip install (install-env 首次场景, 无 Python 工具可用)
104
+ //
105
+ // 实现注意 (修 2.7.0 的真 bug): 不能用字符串拼接构造 Python -c 代码 ——
106
+ // Windows 路径含反斜杠, 拼进 Python 字符串后 \Y \n 等成了非法转义 + 引号丢失,
107
+ // 导致 SyntaxError → 镜像兜底静默失败 → 回退裸 pip → 弱网用户全崩。
108
+ // 改用环境变量传参 (WLKJ_REQ_PATH), Python 侧 os.environ 读, 彻底避开转义地狱。
109
+ function installPipDeps(pyCmd, reqPath) {
110
+ // ⚠ v3.x 重写: 直接用 Node spawnSync 跑 pip (不嵌套 Python 子进程)。
111
+ // 原方案 `python -c "...install_with_fallback..."` 在 Anaconda 3.9+ Windows 下,
112
+ // pip 进镜像重试(长时间编译 tree-sitter/playwright)退出时会触发
113
+ // "PyEval_SaveThread: GIL released" 致命崩溃 → Python 死、结果丢失、回退裸 pip、
114
+ // 弱网新机装不上。Node 直接跑 pip 不嵌套 Python, 无此 GIL 问题。
115
+ // 镜像兜底逻辑在 JS 侧复刻同一策略 (默认源→清华→阿里→腾讯), 行为与 Python 版一致。
116
+ const { spawnSync } = require("child_process");
117
+ const MIRRORS = [
118
+ ["清华", "https://pypi.tuna.tsinghua.edu.cn/simple"],
119
+ ["阿里", "https://mirrors.aliyun.com/pypi/simple/"],
120
+ ["腾讯", "https://mirrors.cloud.tencent.com/simple"],
121
+ ];
122
+ const installEnv = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
123
+
124
+ function runPip(extraArgs, timeoutMs) {
125
+ // stdio inherit 让用户看到进度; 不捕获输出 = 不撑管道、不触发任何崩溃
126
+ const r = spawnSync(pyCmd, ["-m", "pip", "install", "--no-input", "--progress-bar", "off", ...extraArgs, "-r", reqPath],
127
+ { stdio: "inherit", timeout: timeoutMs || 300000, encoding: "utf-8", env: installEnv });
128
+ return r.status === 0;
129
+ }
130
+
131
+ function pkgsInstalled() {
132
+ // 轻量 import 验证: 不跑 pip 子进程, 只 python -c "import ...", 无 GIL 风险。
133
+ // 核心包都 import 成功就算装好 (即使某些可选包编译失败 pip 返回非0)。
134
+ const coreMods = ["duckdb", "pymysql", "yaml", "requests"];
135
+ const mod = coreMods.map(m => "__import__('%s')".replace("%s", m)).join(";");
136
+ try {
137
+ const r = spawnSync(pyCmd, ["-c", mod], { encoding: "utf-8", timeout: 15000, env: installEnv, stdio: "pipe" });
138
+ return r.status === 0;
139
+ } catch { return false; }
140
+ }
141
+
142
+ // 已装好就不折腾 (常见: 老机器 update, 依赖都在)
143
+ if (pkgsInstalled()) {
144
+ console.log(" [OK] Python 依赖已就绪");
145
+ return true;
146
+ }
147
+
148
+ // 1. 默认源 (国内常被拦, 短超时快速判定)
149
+ console.log(" 试默认源(PyPI)...");
150
+ let ok = runPip([], 60000);
151
+ if (ok || pkgsInstalled()) { console.log(" [OK] Python 依赖已装"); return true; }
152
+
153
+ // 2. 默认源失败 → 逐个国内镜像
154
+ for (const [name, url] of MIRRORS) {
155
+ console.log(` 默认源失败, 切镜像(${name})...`);
156
+ ok = runPip(["-i", url, "--trusted-host", url.replace(/^https?:\/\//, "").split("/")[0]], 300000);
157
+ if (ok || pkgsInstalled()) {
158
+ console.log(` [OK] Python 依赖已装 (镜像源: ${name})`);
159
+ return true;
160
+ }
161
+ }
162
+
163
+ // 3. 全失败
164
+ console.log(" [WARN] pip 安装失败 (尝试了所有镜像)");
165
+ console.log(" 可能原因: 网络/防火墙/公司代理拦截 PyPI。");
166
+ console.log(" 手动装 (用清华镜像): pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt");
167
+ console.log(" 或配全局镜像: pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple");
168
+ return false;
169
+ }
170
+
171
+ function cp(src, dest) {
172
+ const s = path.join(T, src), d = path.join(process.cwd(), dest);
173
+ if (!fs.existsSync(s)) return;
174
+ fs.mkdirSync(path.dirname(d), { recursive: true });
175
+ if (!fs.existsSync(d)) { fs.copyFileSync(s, d); return true; }
176
+ return false;
177
+ }
178
+
179
+ function gitOp(op) {
180
+ const cwd = process.cwd();
181
+ const cmds = {
182
+ "提交": 'git add . && git commit -m "update [ai-assisted]" && git push',
183
+ "推送": "git push",
184
+ "拉取最新": "git pull",
185
+ "同步": "git pull",
186
+ "查看状态": "git status",
187
+ "提交PRD": 'git add workspace/specs/prd/ && git commit -m "docs(ai): PRD update [ai-generated]" && git push',
188
+ "提交Spec": 'git add workspace/specs/ && git commit -m "docs(ai): Spec update [ai-generated]" && git push',
189
+ "提交任务": 'git add workspace/tasks/ && git commit -m "chore: task update [ai-assisted]" && git push',
190
+ };
191
+ const cmd = cmds[op];
192
+ if (!cmd) { console.log("可用: 提交 推送 拉取最新 同步 查看状态 提交PRD 提交Spec 提交任务"); return; }
193
+ try { console.log(execSync(cmd, { cwd, encoding: "utf-8", timeout: 30000, stdio: "pipe" }) || `${op} done`); }
194
+ catch (e) { console.log(e.stdout || e.message); }
195
+ }
196
+
197
+ // 快速环境检测 (init 前置): 缺 Python 致命退出, 缺 git 警告不阻塞
198
+ // 目的: 杜绝"裸机没 Python 时 init 静默失败仍宣布成功"的假成功
199
+ function checkEnvQuick() {
200
+ console.log("--- 环境检测 ---");
201
+ // Node 一定有 (npx 在跑), 输出 version 确认
202
+ console.log(` Node.js: [OK] ${process.version}`);
203
+
204
+ // Python: python 或 python3 任一可用即可
205
+ let pyCmd = null, pyVer = null;
206
+ for (const c of ["python", "python3"]) {
207
+ try {
208
+ const v = execSync(`${c} --version`, { encoding: "utf-8", timeout: 5000, stdio: "pipe" }).trim();
209
+ pyCmd = c; pyVer = v; break;
210
+ } catch { /* try next */ }
211
+ }
212
+ if (!pyCmd) {
213
+ console.log(" Python: [缺失] (引擎必需)");
214
+ console.log("\n ⚠ 没装 Python, 无法继续。请先装环境:");
215
+ console.log(" npx @hupan56/wlkj install-env");
216
+ console.log(" (自动装 Node/Python/git + pip 依赖; 装完重开终端再 init)\n");
217
+ return { ok: false, pyCmd: null };
218
+ }
219
+ console.log(` Python: [OK] ${pyVer} (${pyCmd})`);
220
+
221
+ // git: 可选, 缺了只警告 (与 init_doctor 口径一致)
222
+ try {
223
+ const gv = execSync("git --version", { encoding: "utf-8", timeout: 5000, stdio: "pipe" }).trim();
224
+ console.log(` git: [OK] ${gv}`);
225
+ } catch {
226
+ console.log(" git: [缺失] (可选, 团队同步/源码克隆将禁用, 本地 PRD/搜索/任务仍可用)");
227
+ console.log(" 想启用: npx @hupan56/wlkj install-env");
228
+ }
229
+ console.log("");
230
+ return { ok: true, pyCmd };
231
+ }
232
+
233
+ // 交互式问角色 (50 人团队角色各异, 不能写死 pm)
234
+ // 支持的角色 (与 config.yaml / kg.py ROLE_SECTIONS 对齐)
235
+ const ROLES = [
236
+ { key: "pm", label: "产品经理 (PM) — 写需求/PRD/任务" },
237
+ { key: "design", label: "设计师 (UI) — 录入设计稿/出原型" },
238
+ { key: "dev", label: "开发 (前端/后端) — 写代码/提交/测试" },
239
+ { key: "test", label: "测试 (QA) — 跑测试用例/验证" },
240
+ { key: "admin", label: "管理员 — 管理/维护工作流" },
241
+ ];
242
+
243
+ function askRole() {
244
+ // 非交互环境 (CI/AI 调用) 默认 pm
245
+ if (!process.stdin.isTTY) {
246
+ console.log(" (非交互环境, 默认角色: pm)");
247
+ return "pm";
248
+ }
249
+ console.log("\n 你的角色是? (决定命令视角和建议)");
250
+ ROLES.forEach((r, i) => console.log(` ${i + 1}. ${r.label}`));
251
+ const readline = require("readline").createInterface({ input: process.stdin, output: process.stdout });
252
+ return new Promise((resolve) => {
253
+ readline.question("\n 选择 (1-5, 默认1): ", (ans) => {
254
+ readline.close();
255
+ const idx = parseInt(ans, 10) - 1;
256
+ const role = (idx >= 0 && idx < ROLES.length) ? ROLES[idx].key : "pm";
257
+ console.log(` 角色: ${role}\n`);
258
+ resolve(role);
259
+ });
260
+ });
261
+ }
262
+
263
+ // 校验角色参数合法性 (大小写无关: PM/pm/Dev/dev 都认)
264
+ function normalizeRole(r) {
265
+ if (!r) return null;
266
+ const low = String(r).toLowerCase();
267
+ return ROLES.some(x => x.key === low) ? low : null;
268
+ }
269
+
270
+ // reset --hard 前备份本地改过的团队数据文件 (防丢真实密码/配置)。
271
+ // 只备份 .qoder/config.yaml|config.toml|settings.json, 且仅当本地内容与远程不同时。
272
+ // 返回被备份的相对路径列表。引擎文件 (gitignored) 不备份, 反正由 update 重新生成。
273
+ function _backupLocalTeamData(cwd, remoteBranch, _es) {
274
+ const DATA_FILES = [".qoder/config.yaml", ".qoder/config.toml", ".qoder/settings.json"];
275
+ const backed = [];
276
+ const backupDir = path.join(cwd, ".wlkj-pre-reset");
277
+ try {
278
+ fs.mkdirSync(backupDir, { recursive: true });
279
+ for (const rel of DATA_FILES) {
280
+ const local = path.join(cwd, rel);
281
+ if (!fs.existsSync(local)) continue;
282
+ let localContent = "";
283
+ try { localContent = fs.readFileSync(local, "utf-8"); } catch { continue; }
284
+ // 拿远程版本对比
285
+ let remoteContent = null;
286
+ try {
287
+ remoteContent = _es(`git show origin/${remoteBranch}:${rel}`, { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
288
+ } catch { /* 远程没这文件 */ }
289
+ const same = (remoteContent !== null && remoteContent.trim() === localContent.trim());
290
+ if (!same) {
291
+ const target = path.join(backupDir, rel.replace(/[\\/]/g, "__"));
292
+ fs.copyFileSync(local, target);
293
+ backed.push(rel);
294
+ }
295
+ }
296
+ } catch { /* 备份失败不阻塞, 但少一层保护 */ }
297
+ return backed;
298
+ }
299
+
300
+ async function doInit(name, roleArg) {
301
+ const cwd = process.cwd();
302
+ const hasExisting = fs.existsSync(path.join(cwd, ".qoder", "scripts", "setup.py"));
303
+ console.log(`\nwlkj init -> ${cwd}${hasExisting ? " (已存在, 增量更新)" : ""}\n`);
304
+
305
+ // === 0. 环境前置检测 (避免裸机无 Python 时假成功) ===
306
+ const env = checkEnvQuick();
307
+ if (!env.ok) process.exit(1);
308
+ const { pyCmd } = env;
309
+
310
+ // === 0.5. 确定角色 (参数 > 交互式, 不再写死 pm)
311
+ // 50 人团队角色各异: 产品/UI/前后端/测试/管理, 每个角色的命令视角不同。
312
+ // 写死 pm = 设计师和开发拿不到匹配的上下文 (见 wl-prd-full 的 --role 适配)。
313
+ let role = normalizeRole(roleArg);
314
+ if (!role) {
315
+ role = await askRole();
316
+ } else {
317
+ console.log(` 角色: ${role} (来自参数)\n`);
318
+ }
319
+
320
+ // === 1. 拷贝完整引擎 (镜像 .qoder/ 结构) ===
321
+ // 源: templates/qoder/* 目标: .qoder/*
322
+ // init 复用 update 的三路快照保护: 新装时目标不存在, 保护不触发;
323
+ // 重跑 init 时用户改过的引擎文件 (如自定义 skill) 不会被覆盖。
324
+ // ⚠ 但如果快照缺失/损坏(如 git checkout 后), 三路保护会误保护所有文件
325
+ // → 修复: 快照缺失时, init 强制全覆盖(重新建立基线), 不保护。
326
+ const snapshotPath = path.join(cwd, ".qoder", ".engine-snapshot.json");
327
+ const hasSnapshot = fs.existsSync(snapshotPath);
328
+ if (hasExisting && !hasSnapshot) {
329
+ console.log(" [注意] 快照缺失, 引擎文件将强制全量覆盖 (重新建立基线)");
330
+ }
331
+ const useMode = (hasExisting && hasSnapshot) ? "update" : "init";
332
+ const qoderSrc = path.join(T, "qoder");
333
+ let copied = 0;
334
+ if (fs.existsSync(qoderSrc)) {
335
+ const snap = readSnapshot(cwd);
336
+ const newSnap = {};
337
+ const r = copyDirRecursive(qoderSrc, path.join(cwd, ".qoder"), useMode, snap, newSnap);
338
+ copied = r.copied;
339
+ writeSnapshot(cwd, newSnap);
340
+ if (r.protectedN > 0) {
341
+ console.log(` [保护] ${r.protectedN} 个文件保留你的改动 (新版存为 .new):`);
342
+ for (const f of r.protectedFiles.slice(0, 8)) console.log(` .qoder/${f}`);
343
+ if (r.protectedFiles.length > 8) console.log(` ...等 ${r.protectedFiles.length} 个`);
344
+ }
345
+ }
346
+ console.log(` 引擎: ${copied} 个文件 (${hasExisting ? "更新" : "新建"})`);
347
+
348
+ // === 2. 根文件 (AGENTS.md, 新手指南.md, requirements.txt) ===
349
+ ["root/AGENTS.md", "root/新手指南.md", "root/requirements.txt"].forEach(f => {
350
+ const src = path.join(T, f);
351
+ if (fs.existsSync(src)) {
352
+ const dstName = path.basename(f);
353
+ const dst = path.join(cwd, dstName);
354
+ // requirements.txt 始终覆盖(版本可能更新), 其它不存在才拷
355
+ if (dstName === "requirements.txt" || !fs.existsSync(dst)) {
356
+ fs.copyFileSync(src, dst);
357
+ }
358
+ }
359
+ });
360
+
361
+ // === 3. workspace 目录结构 ===
362
+ ["workspace/specs/prd", "workspace/tasks", "workspace/constitution",
363
+ "workspace/members", "data/docs/prd", "data/index", "data/code"].forEach(d => {
364
+ fs.mkdirSync(path.join(cwd, d), { recursive: true });
365
+ });
366
+ console.log(` workspace 目录就绪`);
367
+
368
+ // === 4. git init (若没有) ===
369
+ if (!fs.existsSync(path.join(cwd, ".git"))) {
370
+ try { execSync("git init", { cwd, stdio: "pipe" }); console.log(` git init done`); }
371
+ catch (_) { /* git 可能没装, 不阻塞 */ }
372
+ }
373
+
374
+ // === 5. .gitignore (若没有) ===
375
+ const gitignorePath = path.join(cwd, ".gitignore");
376
+ if (!fs.existsSync(gitignorePath)) {
377
+ const baseGitignore = [
378
+ "# 引擎代码 (由 npm 包 @hupan56/wlkj init/update 生成, 不进团队 git)",
379
+ ".qoder/scripts/", ".qoder/skills/", ".qoder/commands/", ".qoder/agents/",
380
+ ".qoder/contracts/", ".qoder/hooks/", ".qoder/templates/", ".qoder/rules/",
381
+ ".qoder/repowiki/", ".qoder/archive/",
382
+ "",
383
+ "# 团队数据 (保持跟踪): .qoder/config.yaml, .qoder/settings.json, data/learning/, data/index/, data/, workspace/",
384
+ "",
385
+ "# Source code repos (cloned by git_sync.py)", "data/code/",
386
+ "", "# AI pipeline runtime", ".qoder/.developer", ".qoder/.current-task",
387
+ ".qoder/.engine-version", ".qoder/.engine-snapshot.json", ".qoder/.runtime/", "",
388
+ "# Machine-local", "data/index/.last-sync", "data/index/.prd-collected.json",
389
+ "data/index/.index-meta.json", "data/index/.sync-lock", "data/index/.file-keys.json",
390
+ "data/index/.inverted-cache.json", "data/index/*.corrupt",
391
+ "", "# Identity (secret)", "workspace/members/*/.signing_key", "",
392
+ "# Python", "__pycache__/", "*.pyc", "*.lock", "*.tmp", "",
393
+ ].join("\n");
394
+ fs.writeFileSync(gitignorePath, baseGitignore, "utf-8");
395
+ }
396
+
397
+ // === 6. 自动跑 setup.py (名字探测/git/索引/cron/QoderWork) ===
398
+ console.log(`\n--- 自动初始化 ---`);
399
+ const setupPath = path.join(cwd, ".qoder", "scripts", "deployment", "setup", "setup.py");
400
+ if (fs.existsSync(setupPath)) {
401
+ const setupArgs = [name, role, "--skip-cron", "--skip-qoderwork", "--skip-code"].filter(Boolean);
402
+ try {
403
+ const cmd = `python "${setupPath}" ${setupArgs.map(a => `"${a}"`).join(" ")}`;
404
+ console.log(` 运行: setup.py ${setupArgs.join(" ")}`);
405
+ // 强制 UTF-8 环境 (Windows cmd 默认 GBK, 会导致中文输出乱码)
406
+ const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
407
+ execSync(cmd, { cwd, stdio: "inherit", timeout: 300000, env });
408
+ } catch (e) {
409
+ console.log(` setup 部分失败 (不阻塞): ${(e.message || "").slice(0, 100)}`);
410
+ console.log(` 可重新运行: npx @hupan56/wlkj init ${name || "<你的名字>"}`);
411
+ }
412
+ } else {
413
+ console.log(` setup.py 未找到, 跳过自动初始化`);
414
+ }
415
+
416
+ // === 6.4. 装 pip 依赖 (必须在配 MCP 之前!)
417
+ // 顺序关键: 先装依赖 → 再写 mcp.json → mcp.json 选到"装了依赖的Python" → MCP 秒绿
418
+ // 旧版 init 不装依赖直接配 mcp.json → 新机 MCP 全黄 (没装 duckdb/pymysql)
419
+ console.log(`\n--- Python 依赖安装 (失败自动切镜像) ---`);
420
+ const reqFileInit = path.join(cwd, "requirements.txt");
421
+ if (pyCmd && fs.existsSync(reqFileInit)) {
422
+ installPipDeps(pyCmd, reqFileInit);
423
+ } else if (!pyCmd) {
424
+ console.log(` [跳过] Python 未装`);
425
+ } else {
426
+ console.log(` [跳过] requirements.txt 不存在`);
427
+ }
428
+
429
+ // === 6.5. 自动配 QoderWork MCP (新机关键: 否则 mcp.json 不生成, 4 个 MCP 全连不上) ===
430
+ // 复刻 doUpdate 第 6 步的逻辑: 拷 launcher + install_qoderwork --mcp-only
431
+ // setup.py 带了 --skip-qoderwork (offer_qoderwork 是交互式+装skill, 不适合 npx),
432
+ // 这里用 --mcp-only (纯文件操作, 无交互) 把 mcp.json 配好
433
+ // 注意: 此时依赖已装好(6.4步), install_qoderwork 会选到装了依赖的 Python
434
+ if (pyCmd) {
435
+ console.log(`\n--- QoderWork MCP 自动配置 ---`);
436
+ const home = process.env.USERPROFILE || require("os").homedir();
437
+ const launcherSrc = path.join(cwd, ".qoder", "scripts", "protocol", "mcp", "mcp_launcher.py");
438
+ const launcherDst = path.join(home, ".qoderwork", "mcp_launcher.py");
439
+ try {
440
+ if (fs.existsSync(launcherSrc)) {
441
+ fs.mkdirSync(path.dirname(launcherDst), { recursive: true });
442
+ fs.copyFileSync(launcherSrc, launcherDst);
443
+ console.log(` [OK] launcher 已装: ~/.qoderwork/mcp_launcher.py`);
444
+ }
445
+ const instScript = path.join(cwd, ".qoder", "scripts", "deployment", "setup", "install_qoderwork.py");
446
+ if (fs.existsSync(instScript)) {
447
+ try {
448
+ const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
449
+ execSync(`${pyCmd} "${instScript}" --mcp-only`, { stdio: "inherit", timeout: 20000, env });
450
+ console.log(` [OK] mcp.json 已生成 (选装了依赖的Python, MCP 不会黄)`);
451
+ } catch (e) {
452
+ console.log(` [WARN] MCP 配置未完全成功 (不阻塞): ${(e.message || "").slice(0, 80)}`);
453
+ console.log(` 可手动补: ${pyCmd} .qoder/scripts/deployment/setup/install_qoderwork.py`);
454
+ }
455
+ }
456
+ } catch (e) {
457
+ console.log(` [WARN] MCP 配置跳过: ${(e.message || "").slice(0, 80)}`);
458
+ }
459
+ }
460
+
461
+ // === 6.6. 连云平台 bind 项目(云模式:KG 在云平台,引擎调云 MCP)===
462
+ if (pyCmd) {
463
+ const switchScript = path.join(cwd, ".qoder", "scripts", "domain", "kg", "switch_project.py");
464
+ if (fs.existsSync(switchScript)) {
465
+ console.log(`\n--- 连云平台 bind 项目 ---`);
466
+ console.log(` 问平台邮箱/密码 → 拉项目列表 → 选项目 → 写 mcp_config(连云)`);
467
+ try {
468
+ const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
469
+ execSync(`${pyCmd} "${switchScript}"`, { cwd, stdio: "inherit", timeout: 180000, env });
470
+ } catch (e) {
471
+ console.log(` [WARN] 云 bind 未完成,可手动: python .qoder/scripts/domain/kg/switch_project.py`);
472
+ }
473
+ }
474
+ }
475
+
476
+ // === 7. 写版本戳 ===
477
+ writeEngineVersion(cwd);
478
+
479
+ console.log(`\n${"=".repeat(50)}`);
480
+ console.log(` 安装完成! (v${PKG_VERSION})`);
481
+ console.log(`${"=".repeat(50)}`);
482
+ console.log(`\n 现在可以开始了:`);
483
+ console.log(` 在 Qoder 里说 "写个 XX 的需求"`);
484
+ console.log(` 或输 / 看命令列表 (/wl-prd /wl-search /wl-task)`);
485
+ console.log(`\n ✓ QoderWork MCP 已自动配好 (kg/mysql/lanhu/playwright)`);
486
+ console.log(` 用 QoderWork 桌面端想装技能(软链到 ~/.qoderwork/skills/)?`);
487
+ console.log(` python .qoder/scripts/deployment/setup/install_qoderwork.py`);
488
+ console.log(`\n 看不到命令? 新建对话 / 重启 QoderWork`);
489
+ console.log(` 环境问题? python .qoder/scripts/deployment/setup/init_doctor.py --fix\n`);
490
+ }
491
+
492
+ // 递归拷贝目录, 返回 {copied, protected, skipped, protectedFiles}
493
+ // mode: "init" (新装, 全拷) | "update" (升级, 保护用户数据)
494
+ // update 模式下采用三路快照保护 (见 readSnapshot/writeSnapshot):
495
+ // 用户改过的引擎文件 (与上次安装快照不同) → 保留用户的, 新版存 .new
496
+ // 用户没动过的引擎文件 → 正常升级
497
+ // 非引擎文件已存在 → 跳过 (留用户数据)
498
+ function copyDirRecursive(src, dst, mode = "init", snap = {}, newSnap = {}, relBase = "") {
499
+ let copied = 0, protectedN = 0, skipped = 0;
500
+ const protectedFiles = [];
501
+ if (!fs.existsSync(src)) return { copied, protectedN, skipped, protectedFiles };
502
+ fs.mkdirSync(dst, { recursive: true });
503
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
504
+ if (entry.name === "__pycache__" || entry.name.endsWith(".pyc")) continue;
505
+ const s = path.join(src, entry.name);
506
+ const d = path.join(dst, entry.name);
507
+ const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
508
+
509
+ // commands/ 目录强制覆盖(不走三路快照,引擎产物不该被保护)
510
+ const inForceOverwriteDir = FORCE_OVERWRITE_DIRS.has(relBase);
511
+ if (entry.isDirectory() && inForceOverwriteDir) {
512
+ // 直接全量拷贝目录(不保护,不快照)
513
+ const r = copyDirRecursive(s, d, "init", {}, {}, rel);
514
+ copied += r.copied; protectedN += r.protectedN; skipped += r.skipped;
515
+ protectedFiles.push(...r.protectedFiles);
516
+ continue;
517
+ }
518
+ if (entry.isDirectory()) {
519
+ const r = copyDirRecursive(s, d, mode, snap, newSnap, rel);
520
+ copied += r.copied; protectedN += r.protectedN; skipped += r.skipped;
521
+ protectedFiles.push(...r.protectedFiles);
522
+ } else {
523
+ const isEngine = entry.name.endsWith(".py") || entry.name.endsWith(".md") ||
524
+ entry.name.endsWith(".yaml") || entry.name.endsWith(".yml") ||
525
+ entry.name.endsWith(".toml") || entry.name.endsWith(".json") ||
526
+ entry.name.endsWith(".html") || entry.name.endsWith(".txt");
527
+
528
+ if (mode === "update") {
529
+ // 受保护配置 (config.yaml/settings.json): 永不覆盖, 但自动修复已知问题
530
+ if (PROTECTED_FILES.has(entry.name) && fs.existsSync(d)) {
531
+ // ★ config.yaml url 行内注释修复:自动清理 url 行的 # 注释(防被当 git url)
532
+ if (entry.name === "config.yaml") {
533
+ try {
534
+ let cfgContent = fs.readFileSync(d, "utf-8");
535
+ // 匹配 url: "xxx" # 注释 → 去掉注释
536
+ const fixedContent = cfgContent.replace(
537
+ /^(\s*url:\s*["']?[^"'\n]*["']?)\s+#.*$/gm, "$1"
538
+ );
539
+ if (fixedContent !== cfgContent) {
540
+ fs.writeFileSync(d, fixedContent, "utf-8");
541
+ console.log(` [FIX] config.yaml url 行内注释已自动清理`);
542
+ }
543
+ } catch { /* 修复失败不阻塞 */ }
544
+ }
545
+ if (!filesEqual(s, d)) {
546
+ fs.copyFileSync(s, d + ".new");
547
+ protectedN++; protectedFiles.push(rel);
548
+ } else {
549
+ skipped++;
550
+ }
551
+ // 快照记上游 hash: 用户当前 == 上游 → 下次安全升级; 否则持续保护
552
+ const sh = fileHash(s); if (sh) newSnap[rel] = sh;
553
+ continue;
554
+ }
555
+ // 引擎文件: 三路快照保护
556
+ if (isEngine) {
557
+ const userHash = fs.existsSync(d) ? fileHash(d) : null;
558
+ const snapHash = snap[rel] || null;
559
+ // 保护判定: 用户当前文件与上游源不同, 且要么(a)与上次快照也不同(确属用户改过),
560
+ // 要么(b)快照缺失/损坏(无法判定来源 → 保守保护, 不覆盖, 避免静默丢用户改动)。
561
+ // ⚠ 旧逻辑要求 snapHash 非 null 才保护 → 快照缺失/损坏时全量覆盖, 静默吞掉用户改动。
562
+ const userModifiedKnownSnap = (fs.existsSync(d) && userHash && snapHash && userHash !== snapHash);
563
+ const userDiffersFromUpstream = (fs.existsSync(d) && !filesEqual(s, d));
564
+ const snapshotMissing = !snapHash;
565
+ const shouldProtect = userModifiedKnownSnap || (userDiffersFromUpstream && snapshotMissing);
566
+ if (shouldProtect) {
567
+ // 保留用户的, 新版存 .new (不覆盖)
568
+ if (!filesEqual(s, d)) {
569
+ fs.copyFileSync(s, d + ".new");
570
+ protectedN++; protectedFiles.push(rel);
571
+ } else {
572
+ skipped++;
573
+ }
574
+ // 关键: 快照记上游 hash (不是用户的 hash)。
575
+ // 这样下次升级: 用户文件若仍 != 上游 → 继续保护; 若用户手动改成上游 → 自动恢复正常升级。
576
+ const sh = fileHash(s); if (sh) newSnap[rel] = sh;
577
+ } else {
578
+ // 用户没动过 (或新文件), 正常升级, 刷新快照为新版 hash
579
+ fs.copyFileSync(s, d);
580
+ copied++;
581
+ const sh = fileHash(s); if (sh) newSnap[rel] = sh;
582
+ }
583
+ } else {
584
+ // 非引擎文件: 已存在跳过 (保护用户数据), 不存在才建
585
+ if (!fs.existsSync(d)) {
586
+ fs.copyFileSync(s, d);
587
+ copied++;
588
+ } else {
589
+ skipped++;
590
+ }
591
+ }
592
+ } else {
593
+ // init 模式: 引擎文件全覆盖, 非引擎已存在跳过 (保留用户改动)
594
+ if (isEngine || !fs.existsSync(d)) {
595
+ fs.copyFileSync(s, d);
596
+ }
597
+ copied++;
598
+ // 快照记上游 hash (基线), 供将来升级对比
599
+ const sh = fileHash(s); if (sh) newSnap[rel] = sh;
600
+ }
601
+ }
602
+ }
603
+ return { copied, protectedN, skipped, protectedFiles };
604
+ }
605
+
606
+ // 两文件内容是否相同(先比大小再比内容)
607
+ function filesEqual(a, b) {
608
+ try {
609
+ const sa = fs.statSync(a), sb = fs.statSync(b);
610
+ if (sa.size !== sb.size) return false;
611
+ return fs.readFileSync(a).equals(fs.readFileSync(b));
612
+ } catch { return false; }
613
+ }
614
+
615
+ // 版本戳: .qoder/.engine-version 存当前引擎版本
616
+ function writeEngineVersion(cwd) {
617
+ try {
618
+ const p = path.join(cwd, ".qoder", ".engine-version");
619
+ fs.mkdirSync(path.dirname(p), { recursive: true });
620
+ fs.writeFileSync(p, PKG_VERSION + "\n", "utf-8");
621
+ } catch { /* 不阻塞 */ }
622
+ }
623
+
624
+ function readEngineVersion(cwd) {
625
+ try {
626
+ const p = path.join(cwd, ".qoder", ".engine-version");
627
+ if (fs.existsSync(p)) return fs.readFileSync(p, "utf-8").trim();
628
+ } catch { /* */ }
629
+ return null;
630
+ }
631
+
632
+ // ─────────────────────────────────────────────────────────────
633
+ // 三路快照保护 (dpkg/brew conffiles 同款机制, 彻底解决"我原本的技能呢?")
634
+ //
635
+ // 问题: update 用 isEngine 一刀切强制刷新 .md/.py/.yaml 等,
636
+ // 用户改过的 stock skill (如 .qoder/skills/wl-prd-full/SKILL.md)
637
+ // 被无声覆盖 → "更新完我的技能没了"。
638
+ //
639
+ // 解法: 每次安装/升级后, 把所有"从 npm 包拷过来的引擎文件"的 hash 记到
640
+ // .qoder/.engine-snapshot.json。下次升级时, 三路对比:
641
+ // 用户当前文件 hash == 快照 hash → 没动过, 安全升级
642
+ // 用户当前文件 hash != 快照 hash → 动过! 保留用户的, 新版存 .new
643
+ // 这就是 apt/dpkg 对 /etc 下 conffiles 的处理, 行业标准, 不瞎猜。
644
+ // ─────────────────────────────────────────────────────────────
645
+ const SNAPSHOT_FILE = ".engine-snapshot.json";
646
+
647
+ function snapshotPath(cwd) {
648
+ return path.join(cwd, ".qoder", SNAPSHOT_FILE);
649
+ }
650
+
651
+ // 读快照: { "skills/wl-prd-full/SKILL.md": "<sha256前16位>", ... }
652
+ function readSnapshot(cwd) {
653
+ try {
654
+ const p = snapshotPath(cwd);
655
+ if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, "utf-8"));
656
+ } catch { /* 损坏的快照当空 */ }
657
+ return {};
658
+ }
659
+
660
+ // 写快照: 传入 {相对路径: hash} 的对象
661
+ function writeSnapshot(cwd, snap) {
662
+ try {
663
+ const p = snapshotPath(cwd);
664
+ fs.mkdirSync(path.dirname(p), { recursive: true });
665
+ fs.writeFileSync(p, JSON.stringify(snap, null, 2), "utf-8");
666
+ } catch { /* 快照写失败不阻塞升级 */ }
667
+ }
668
+
669
+ // 计算文件内容 hash (sha256 前 16 位, 足够检测"是否动过")
670
+ function fileHash(p) {
671
+ try {
672
+ const crypto = require("crypto");
673
+ const h = crypto.createHash("sha256");
674
+ h.update(fs.readFileSync(p));
675
+ return h.digest("hex").slice(0, 16);
676
+ } catch { return null; }
677
+ }
678
+
679
+ // 跑 install_qoderwork.py(升级时强制刷新 commands)
680
+ // v3.x: 脚本已分子目录, 路径为 scripts/deployment/setup/install_qoderwork.py
681
+ function runQoderWorkInstall(cwd, forceCommands) {
682
+ const script = path.join(cwd, ".qoder", "scripts", "deployment", "setup", "install_qoderwork.py");
683
+ if (!fs.existsSync(script)) {
684
+ console.log(" [跳过] install_qoderwork.py 不存在");
685
+ return;
686
+ }
687
+ // ⚠ 必须用 detectPyCmd(), 不能硬编码 "python"!
688
+ // macOS 默认只有 python3, 硬编码 python 会让 Mac 全员 update 时
689
+ // "刷新 QoderWork" 步骤静默失败 (commands/skills 不刷新)。
690
+ // 用 spawnSync(list-form) 而非 args.join(" ") → 路径含空格/中文也不裂。
691
+ const { spawnSync } = require("child_process");
692
+ const pyCmd = detectPyCmd();
693
+ if (!pyCmd) {
694
+ console.log(" [跳过] Python 未安装, 无法刷新 QoderWork (npx @hupan56/wlkj install-env)");
695
+ return;
696
+ }
697
+ const args = [script];
698
+ if (forceCommands) args.push("--force-commands");
699
+ try {
700
+ console.log(`\n--- 刷新 QoderWork ---`);
701
+ const r = spawnSync(pyCmd, args, {
702
+ cwd, stdio: "inherit", timeout: 60000,
703
+ env: { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" },
704
+ });
705
+ if (r.status !== 0 && r.status !== null) {
706
+ console.log(` QoderWork 刷新返回非0 (不阻塞): exit ${r.status}`);
707
+ }
708
+ } catch (e) {
709
+ console.log(` QoderWork 刷新失败 (不阻塞): ${(e.message || "").slice(0, 80)}`);
710
+ }
711
+ }
712
+
713
+ function doStatus() {
714
+ console.log("");
715
+ // v3.x: 转调 wlkj.py status (单一真源 domain/report/status.py)。
716
+ // 旧版 doStatus 自带 .developer 解析, 但 role 实际存在 member.json,
717
+ // 旧解析读不到 role 行就误报"角色未设置", 与 status.py 自相矛盾。
718
+ // 统一走 Python 入口, 消除"两套读取逻辑不一致"。
719
+ const wlkj = path.join(process.cwd(), ".qoder", "scripts", "orchestration", "wlkj.py");
720
+ if (!fs.existsSync(wlkj)) {
721
+ console.log("引擎未安装。跑: npx @hupan56/wlkj init");
722
+ return;
723
+ }
724
+ const pyCmd = detectPyCmd();
725
+ if (!pyCmd) {
726
+ console.log("Python 未安装。跑: npx @hupan56/wlkj install-env");
727
+ return;
728
+ }
729
+ try {
730
+ const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
731
+ const out = execSync(`${pyCmd} "${wlkj}" status`, { cwd: process.cwd(), encoding: "utf-8", timeout: 20000, env });
732
+ process.stdout.write(out);
733
+ } catch (e) {
734
+ process.stdout.write((e.stdout || e.message || ""));
735
+ }
736
+ // 版本戳 (status.py 不管这层, 保留在 cli 侧)
737
+ const ver = readEngineVersion(process.cwd());
738
+ console.log(`engine: ${ver || "未知"} (最新: v${PKG_VERSION})${ver === PKG_VERSION ? " [最新]" : ver ? " [可升级: npx @hupan56/wlkj update]" : ""}`);
739
+ console.log("");
740
+ }
741
+
742
+ function doUpdate() {
743
+ const cwd = process.cwd();
744
+ const oldVer = readEngineVersion(cwd);
745
+ const qoderSrc = path.join(T, "qoder");
746
+
747
+ // 前置检查: 必须已装过
748
+ if (!fs.existsSync(path.join(cwd, ".qoder", "scripts"))) {
749
+ console.log("\n 未检测到已安装的引擎。首次安装请用:");
750
+ console.log(" npx @hupan56/wlkj init [你的名字]\n");
751
+ return;
752
+ }
753
+
754
+ console.log(`\nwlkj update${oldVer ? " " + oldVer : ""} -> v${PKG_VERSION}\n`);
755
+
756
+ // P1-15: 显式声明 update 绝不触碰的路径
757
+ console.log(" 受保护路径 (绝不覆盖):");
758
+ console.log(" .qoder/config.yaml (团队 git 配置)");
759
+ console.log(" .qoder/settings.json (权限/hook 配置)");
760
+ console.log(" .qoder/.developer (你的身份)");
761
+ console.log(" .qoder/.current-task (当前任务)");
762
+ console.log(" .qoder/.engine-version (版本戳)");
763
+ console.log(" 你改过的引擎文件 (三路快照: 改过的留你的, 新版存 .new)");
764
+ console.log(" workspace/ (你的全部产出)");
765
+ console.log(" data/ (代码仓库+知识图谱)");
766
+ console.log("");
767
+
768
+ // === 1. 刷新引擎文件 (三路快照保护用户改动) ===
769
+ let copied = 0, protectedN = 0, skipped = 0;
770
+ let protectedFiles = [];
771
+ if (fs.existsSync(qoderSrc)) {
772
+ const snap = readSnapshot(cwd);
773
+ const newSnap = {};
774
+ const r = copyDirRecursive(qoderSrc, path.join(cwd, ".qoder"), "update", snap, newSnap);
775
+ copied = r.copied; protectedN = r.protectedN; skipped = r.skipped;
776
+ protectedFiles = r.protectedFiles || [];
777
+ writeSnapshot(cwd, newSnap); // 刷新基线 (供下次升级对比)
778
+ }
779
+ console.log(` 引擎刷新: ${copied} 个文件${skipped ? ` / ${skipped} 无变化` : ""}`);
780
+
781
+ // 报告受保护文件 (用户的改动 + 配置文件)
782
+ if (protectedN > 0) {
783
+ console.log(` [保护] ${protectedN} 个文件已保留你的改动 (新版存为 .new):`);
784
+ for (const f of protectedFiles.slice(0, 10)) console.log(` .qoder/${f}`);
785
+ if (protectedFiles.length > 10) console.log(` ...等 ${protectedFiles.length} 个`);
786
+ console.log(` 确认无需合并后可删除 .new; 或手动对比合并后删除。`);
787
+ }
788
+
789
+ // === 2. 根文件(文档 + requirements.txt, 始终覆盖保证最新)===
790
+ ["root/AGENTS.md", "root/新手指南.md", "root/requirements.txt"].forEach(f => {
791
+ const src = path.join(T, f);
792
+ if (fs.existsSync(src)) {
793
+ fs.copyFileSync(src, path.join(cwd, path.basename(f)));
794
+ }
795
+ });
796
+ console.log(` 根文档 + requirements.txt: 已更新`);
797
+
798
+ // === 2.5. git pull 拉最新团队数据 (引擎数据分离: 引擎已由上面 copyDirRecursive 生成, git 只存数据) ===
799
+ // 团队数据 = config.yaml / settings.json / data/learning/ + data/ (kg.duckdb/kg_db.duckdb/索引) + workspace/ (PRD/任务)。
800
+ // 引擎代码 (scripts/skills/...) 已被 gitignore, 不在 git 里, 不会与 pull 冲突。
801
+ // git pull 失败会导致 kg.duckdb 拿不到 → kg MCP 无数据(但仍能连)。
802
+ // 失败原因常见: 未配 git 身份 / 远程仓库要认证 / 网络问题 / 分支没tracking / unrelated histories。
803
+ // 这里给出清晰诊断, 不让"git pull 失败"变成无声的静默跳过。
804
+ // 前置: 自愈分支 tracking (老版 init 配了 remote 但没 set-upstream, pull 会报
805
+ // "no tracking information"。这里检测到就补上, 老用户 update 也能自动修)
806
+ const { execSync: _es } = require("child_process"); // 提到 try 外, catch 块的自愈逻辑也要用
807
+ try {
808
+ // 先看当前分支有没有 upstream
809
+ let _trackOk = false;
810
+ try {
811
+ _es("git rev-parse --abbrev-ref --symbolic-full-name @{u}", { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
812
+ _trackOk = true; // 有 upstream, 正常
813
+ } catch {
814
+ // 没 tracking — 尝试补设 (有 origin remote 的前提下)
815
+ try {
816
+ const _remote = _es("git remote", { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" }).trim();
817
+ if (_remote.split(/\s+/).includes("origin")) {
818
+ _es("git fetch origin", { cwd, encoding: "utf-8", timeout: 30000, stdio: "pipe" });
819
+ // 确定本地分支名
820
+ const _branch = _es("git rev-parse --abbrev-ref HEAD", { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" }).trim();
821
+ // 远程用 master 还是 main
822
+ let _remoteBranch = null;
823
+ for (const _c of ["master", "main"]) {
824
+ try {
825
+ _es(`git rev-parse --verify remotes/origin/${_c}`, { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
826
+ _remoteBranch = _c; break;
827
+ } catch { /* 该分支不存在, 试下一个 */ }
828
+ }
829
+ if (_remoteBranch) {
830
+ _es(`git branch --set-upstream-to=origin/${_remoteBranch} ${_branch}`, { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
831
+ console.log(` git: 已自动设置上游 tracking (origin/${_remoteBranch})`);
832
+ _trackOk = true;
833
+ }
834
+ }
835
+ } catch { /* 补 tracking 失败, 走下面的 pull 诊断 */ }
836
+ }
837
+ const pullR = _es("git pull --no-rebase 2>&1", { cwd, encoding: "utf-8", timeout: 30000 });
838
+ if (pullR.includes("Already up to date") || pullR.includes("已经是最新")) {
839
+ console.log(` git: 已是最新`);
840
+ } else if (pullR.includes("CONFLICT") || pullR.includes("conflict")) {
841
+ console.log(` git: ⚠️ 有冲突, 跳过pull(不影响本地工作)`);
842
+ } else {
843
+ console.log(` git: 已拉取最新`);
844
+ }
845
+ } catch (e) {
846
+ // git pull 失败诊断: 区分"没配git身份"/"远程要认证"/"网络问题"/"非git仓库"
847
+ // node execSync 抛异常时, git 的真实报错在 e.stderr/e.stdout, 不能只看 e.message
848
+ const errMsg = String(e.message || "");
849
+ const stdout = String((e.stdout || "") + " " + (e.stderr || ""));
850
+ const combined = (errMsg + " " + stdout).toLowerCase();
851
+ if (combined.includes("authentication failed") || combined.includes("permission denied") ||
852
+ combined.includes("could not read username") || combined.includes("不支持密码") ||
853
+ combined.includes("credentials") || combined.includes("403") || combined.includes("401")) {
854
+ console.log(` git: ⚠️ 认证失败 — kg.duckdb 等团队数据拉不到`);
855
+ console.log(` 原因: git 身份/凭证未配。MCP 能连但无数据。`);
856
+ console.log(` 修复: npx @hupan56/wlkj init <你的名字> (交互式配 git 账号)`);
857
+ } else if (combined.includes("not a git repository") || combined.includes("not a git dir") ||
858
+ combined.includes("unknown revision") || combined.includes("does not have any commits")) {
859
+ console.log(` git: ⚠️ 非git仓库或空仓库 — 团队数据拉不到`);
860
+ console.log(` 原因: 此目录不是 git clone 来的, 或没有 remote。`);
861
+ console.log(` 修复: npx @hupan56/wlkj init <你的名字> (会配团队远程仓库)`);
862
+ } else if (combined.includes("timed out") || combined.includes("timeout") ||
863
+ combined.includes("could not resolve host") || combined.includes("connection refused") ||
864
+ combined.includes("network is unreachable") || combined.includes("unable to access") ||
865
+ combined.includes("ssl") || combined.includes("failed to connect")) {
866
+ console.log(` git: ⚠️ 网络问题 — 连不上远程仓库`);
867
+ console.log(` 原因: ${stdout.trim().slice(0, 80) || "网络超时/无法解析主机"}`);
868
+ console.log(` (kg.duckdb 拉不到, MCP 能连但搜索无结果)`);
869
+ } else if (combined.includes("unrelated histories") || combined.includes("refusing to merge")) {
870
+ // 自愈: 本地是空的 git init 历史, 与团队远程无公共祖先。
871
+ // 引擎代码现在由 npm 生成 (不在 git 里), 团队数据在远程 → 采用远程历史是正确的。
872
+ // 但 reset --hard 会覆盖工作树 → 先把本地改过的团队数据文件备份, 防丢密码/配置。
873
+ console.log(` git: ⚠️ 无关历史 (本地 git init 产生的平行历史与团队远程无公共祖先)`);
874
+ try {
875
+ // 探测远程分支名
876
+ let _rb = null;
877
+ for (const _c of ["master", "main"]) {
878
+ try {
879
+ _es(`git rev-parse --verify remotes/origin/${_c}`, { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
880
+ _rb = _c; break;
881
+ } catch { /* */ }
882
+ }
883
+ if (_rb) {
884
+ // 备份本地改过的团队数据 (config 等可能含未提交的真实密码)
885
+ const _backed = _backupLocalTeamData(cwd, _rb, _es);
886
+ _es(`git reset --hard origin/${_rb}`, { cwd, encoding: "utf-8", timeout: 30000, stdio: "pipe" });
887
+ console.log(` git: ✓ 已采用远程历史 (origin/${_rb}), 团队数据已落地`);
888
+ console.log(` (引擎随后由本次 update 重新生成)`);
889
+ if (_backed.length > 0) {
890
+ console.log(` ⚠️ reset 前已备份你本地改过的数据文件 (可能含未提交的真实密码/配置):`);
891
+ for (const rel of _backed) console.log(` → .wlkj-pre-reset/${rel.replace(/[\\/]/g, "__")}`);
892
+ console.log(` 检查这些文件, 把需要的配置/密码重新填回 .qoder/config.yaml 后删备份目录。`);
893
+ }
894
+ } else {
895
+ console.log(` git: 远程无 master/main 分支, 无法自愈。请确认团队仓库地址正确。`);
896
+ }
897
+ } catch (re) {
898
+ console.log(` git: 自愈失败 (${String(re.message || "").slice(0, 80)})`);
899
+ console.log(` 可手动: git fetch origin && git reset --hard origin/master`);
900
+ }
901
+ } else {
902
+ // 兜底: 显示 git 的真实报错, 不再笼统说 "Command failed"
903
+ const realErr = stdout.trim() || errMsg;
904
+ console.log(` git: ⚠️ 拉取失败 — ${realErr.slice(0, 100)}`);
905
+ console.log(` (kg.duckdb 拉不到, MCP 能连但搜索无结果)`);
906
+ }
907
+ }
908
+
909
+ // === 3. 补装 pip 依赖 (必须在刷 mcp.json 之前!)
910
+ // 顺序很关键: 先装依赖 → 再写 mcp.json → mcp.json 能选到"已装依赖的Python"
911
+ // 旧版顺序反了(先写mcp.json再装依赖), 导致 mcp.json 指向没装依赖的Python → MCP 黄。
912
+ console.log(`\n--- Python 依赖补装 (失败自动切镜像) ---`);
913
+ const reqFile = path.join(cwd, "requirements.txt");
914
+ const pyCmd = detectPyCmd();
915
+ if (pyCmd && fs.existsSync(reqFile)) {
916
+ installPipDeps(pyCmd, reqFile);
917
+ } else if (!pyCmd) {
918
+ console.log(` [跳过] Python 未装`);
919
+ } else {
920
+ console.log(` [跳过] requirements.txt 不存在`);
921
+ }
922
+
923
+ // === 4. 刷新 QoderWork commands(强制覆盖已存在的)===
924
+ // WLKJ_SKIP_QW_INSTALL=1 时跳过 (测试隔离用, 防止测试目录污染 ~/.qoderwork/.repo-root)
925
+ if (process.env.WLKJ_SKIP_QW_INSTALL !== "1") {
926
+ runQoderWorkInstall(cwd, true);
927
+ }
928
+
929
+ // === 5. 写版本戳 ===
930
+ writeEngineVersion(cwd);
931
+
932
+ // === 5b. 清理旧版废弃文件 ===
933
+ const OBSOLETE_FILES = [
934
+ // 旧脚本(已替代)
935
+ ".qoder/workflow.md", // 被 rules/wl-pipeline.md 替代
936
+ ".qoder/scripts/code_index.py", // 被 git_sync.py 替代
937
+ ".qoder/scripts/notify.py", // 被 common/feishu.py 替代
938
+ ".qoder/scripts/team.py", // 被 task.py + team_sync.py 替代
939
+ "packages/wlkj/templates/cli.js", // 旧版 CLI
940
+ ".qoder/templates/qoder/config.toml", // 旧版配置
941
+ ".qoder/templates/qoder/scripts/code_index.py",
942
+ ".qoder/templates/qoder/scripts/notify.py",
943
+ ".qoder/templates/qoder/scripts/team.py",
944
+ // 旧命令(已合并进 /wl-prd 和 /wl-design)
945
+ ".qoder/commands/wl-prd-full.md",
946
+ ".qoder/commands/wl-prd-quick.md",
947
+ ".qoder/commands/wl-prd-review.md",
948
+ ".qoder/commands/wl-design-draw.md",
949
+ ".qoder/commands/wl-design-scan.md",
950
+ ".qoder/commands/wl-design-spec.md",
951
+ // 旧版索引JSON(已迁移到DuckDB, 残留在老用户的data/index/)
952
+ "data/index/keyword-index.json",
953
+ "data/index/api-index.json",
954
+ "data/index/prd-index.json",
955
+ "data/index/module-map.json",
956
+ "data/index/prd-code-map.json",
957
+ "data/index/callgraph.json",
958
+ "data/index/style-index.json",
959
+ "data/index/style-meta.json",
960
+ "data/index/vben-style-reference.json",
961
+ "data/index/chart-style-reference.json",
962
+ "data/index/icon-reference.json",
963
+ "data/index/wiki-index.json",
964
+ // 旧运行时缓存(自动重建)
965
+ "data/index/.file-keys.json",
966
+ "data/index/.inverted-cache.json",
967
+ "data/index/.api-table-cache.json",
968
+ // 旧learning空壳(未实现的功能占位)
969
+ ".qoder/learning/patterns/code-patterns.md",
970
+ ".qoder/learning/patterns/prd-patterns.md",
971
+ ".qoder/learning/patterns/review-fixes.md",
972
+ // 用户级旧命令(已合并的, 清理~/.qoderwork/commands/)
973
+ ];
974
+ let cleaned = 0;
975
+ for (const f of OBSOLETE_FILES) {
976
+ const fp = path.join(cwd, f);
977
+ if (fs.existsSync(fp)) {
978
+ try {
979
+ if (fs.statSync(fp).isDirectory()) {
980
+ fs.rmSync(fp, { recursive: true, force: true });
981
+ } else {
982
+ fs.unlinkSync(fp);
983
+ }
984
+ console.log(` [清理] 删除废弃文件: ${f}`);
985
+ cleaned++;
986
+ } catch (e) { /* 删不掉跳过 */ }
987
+ }
988
+ }
989
+ // 清理用户级旧命令 (已合并的, ~/.qoderwork/commands/)
990
+ const home = process.env.USERPROFILE || require("os").homedir();
991
+ const userCmdDir = path.join(home, ".qoderwork", "commands");
992
+ const OBSOLETE_USER_CMDS = [
993
+ "wl-prd-full.md", "wl-prd-quick.md", "wl-prd-review.md",
994
+ "wl-design-draw.md", "wl-design-scan.md", "wl-design-spec.md",
995
+ ];
996
+ for (const f of OBSOLETE_USER_CMDS) {
997
+ const fp = path.join(userCmdDir, f);
998
+ if (fs.existsSync(fp)) {
999
+ try { fs.unlinkSync(fp); console.log(` [清理] 用户级旧命令: ${f}`); cleaned++; }
1000
+ catch { /* */ }
1001
+ }
1002
+ }
1003
+ // 注意: .new 文件不再自动删除! 它们是三路快照保护的"新版待对比"信号,
1004
+ // 用户需要看到 .new 才知道"我的改动被保留了, 这里是新版"。用户手动合并后删。
1005
+ // 清理缓存 (索引重建后失效的旧缓存)
1006
+ const runtimeDir = path.join(cwd, ".qoder", ".runtime");
1007
+ let cacheCleaned = 0;
1008
+ try {
1009
+ for (const f of fs.readdirSync(runtimeDir)) {
1010
+ if (f.startsWith("search-cache-") || f.startsWith("ctx-cache-")) {
1011
+ fs.unlinkSync(path.join(runtimeDir, f));
1012
+ cacheCleaned++;
1013
+ }
1014
+ }
1015
+ } catch { /* */ }
1016
+ // 清理引擎 __pycache__ (v3.x 加): 脚本重组后, 旧 .pyc 可能指向已删/已移的 .py
1017
+ // → 某些机器 Python 加载死字节码 → 行为漂移 (机器特定, 难排查)。全删, 自动重建。
1018
+ let pycCleaned = 0;
1019
+ function _purgePycache(dir) {
1020
+ try {
1021
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
1022
+ const p = path.join(dir, e.name);
1023
+ if (e.isDirectory()) {
1024
+ if (e.name === "__pycache__") {
1025
+ try { fs.rmSync(p, { recursive: true, force: true }); pycCleaned++; } catch { /* 占用 */ }
1026
+ } else {
1027
+ _purgePycache(p);
1028
+ }
1029
+ }
1030
+ }
1031
+ } catch { /* */ }
1032
+ }
1033
+ _purgePycache(path.join(cwd, ".qoder", "scripts"));
1034
+ if (cleaned > 0 || cacheCleaned > 0 || pycCleaned > 0) {
1035
+ console.log(` [清理] 废弃文件 ${cleaned} 个 + 过期缓存 ${cacheCleaned} 个 + __pycache__ ${pycCleaned} 个`);
1036
+ }
1037
+
1038
+ // === 5c. 清理 .qoder 嵌套残留 (致命 bug 源) ===
1039
+ // 旧版 paths.py 的 _find_repo_root 会误把 .qoder 当 PROJECT_ROOT, 然后往
1040
+ // "PROJECT_ROOT/.qoder/.runtime/" 写缓存 → 误建嵌套 .qoder/ 目录 →
1041
+ // 这个残留反过来又让 _find_repo_root 再次误判 → 死循环。
1042
+ // 表现: MCP cookie 路径全错 (workspace 跑到 .qoder/workspace), lanhu 永远 cookie 未配置。
1043
+ // v3.0 修了 paths.py 不再误建, 但老用户机器上的残留要清掉, 否则 bug 还在。
1044
+ // 注意: 残留可能出现在两处 —— .qoder/.qoder 和 .qoder/scripts/.qoder (后者是
1045
+ // search 缓存写到错误 PROJECT_ROOT 时建的), 两处都要扫干净。
1046
+ const nestedCandidates = [
1047
+ path.join(cwd, ".qoder", ".qoder"),
1048
+ path.join(cwd, ".qoder", "scripts", ".qoder"),
1049
+ ];
1050
+ for (const nestedQoder of nestedCandidates) {
1051
+ if (fs.existsSync(nestedQoder)) {
1052
+ try {
1053
+ fs.rmSync(nestedQoder, { recursive: true, force: true });
1054
+ console.log(` [清理] 删除嵌套残留 ${path.relative(cwd, nestedQoder) || nestedQoder}/ (曾导致 MCP 路径错位)`);
1055
+ cleaned++;
1056
+ } catch { /* 删不掉不阻塞 (可能被占用), 下次再清 */ }
1057
+ }
1058
+ }
1059
+
1060
+ // === 6. 刷新 launcher + mcp.json (老用户的可能是旧版/写死路径) ===
1061
+ // 注意: pip 依赖已在第3步装好, 此处 install_qoderwork 会选到"已装依赖的Python"写进 mcp.json
1062
+ // WLKJ_SKIP_QW_INSTALL=1 时跳过 (测试隔离, 防止污染全局 ~/.qoderwork/)
1063
+ if (process.env.WLKJ_SKIP_QW_INSTALL !== "1") {
1064
+ try {
1065
+ const home = process.env.USERPROFILE || require("os").homedir();
1066
+ const launcherSrc = path.join(cwd, ".qoder", "scripts", "protocol", "mcp", "mcp_launcher.py");
1067
+ const launcherDst = path.join(home, ".qoderwork", "mcp_launcher.py");
1068
+ if (fs.existsSync(launcherSrc)) {
1069
+ fs.mkdirSync(path.dirname(launcherDst), { recursive: true });
1070
+ fs.copyFileSync(launcherSrc, launcherDst);
1071
+ console.log(` [OK] launcher 已刷新`);
1072
+ }
1073
+ // 刷新 mcp.json 为 launcher 模式 (install_qoderwork.py 的 rewrite 逻辑)
1074
+ const instScript = path.join(cwd, ".qoder", "scripts", "deployment", "setup", "install_qoderwork.py");
1075
+ if (pyCmd && fs.existsSync(instScript)) {
1076
+ try {
1077
+ execSync(`${pyCmd} "${instScript}" --mcp-only`, { stdio: "pipe", timeout: 15000 });
1078
+ console.log(` [OK] mcp.json 已刷新 (选装了依赖的Python, 避免MCP黄)`);
1079
+ } catch (e) { /* --mcp-only 可能不支持, 静默跳过 */ }
1080
+ }
1081
+
1082
+ // ★ 共识:kg 走云平台 SSE(cli.js 最保险——npx 入口永远最新,不依赖旧版 install_qoderwork)
1083
+ // 在 install_qoderwork 写完 mcp.json 后,cli.js 强制把 kg 改为 SSE url
1084
+ const mcpFile = path.join(home, ".qoderwork", "mcp.json");
1085
+ if (fs.existsSync(mcpFile)) {
1086
+ try {
1087
+ const mcp = JSON.parse(fs.readFileSync(mcpFile, "utf-8"));
1088
+ if (mcp.mcpServers && mcp.mcpServers["qoder-knowledge-graph"]) {
1089
+ mcp.mcpServers["qoder-knowledge-graph"] = {
1090
+ "url": "http://10.87.106.252/mcp",
1091
+ "type": "sse",
1092
+ "enabled": true,
1093
+ };
1094
+ fs.writeFileSync(mcpFile, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
1095
+ console.log(` [OK] kg MCP → 云平台 SSE (http://10.87.106.252/mcp)`);
1096
+ }
1097
+ } catch (e) { /* mcp.json 解析失败不阻塞 */ }
1098
+ }
1099
+ } catch (e) { /* 刷新失败不阻塞升级 */ }
1100
+ } // end WLKJ_SKIP_QW_INSTALL guard
1101
+
1102
+ // === 6.5. MCP 启动实测 (发 initialize 看哪个起不来) ===
1103
+ // 这是"根治黄色"的关键一步: 配完 mcp.json 后立即模拟 QoderWork 发 initialize,
1104
+ // 看每个 MCP 是 OK / DISABLED(绿) / CRASHED(黄)。有问题当场报, 不让用户重启后才发现。
1105
+ // v3.x: check_mcp_launch.py 已合并进 protocol/mcp/mcp_doctor.py (实际启动诊断)。
1106
+ console.log(`\n--- MCP 启动实测 (模拟 QoderWork 连接) ---`);
1107
+ const diagScript = path.join(cwd, ".qoder", "scripts", "protocol", "mcp", "mcp_doctor.py");
1108
+ if (pyCmd && fs.existsSync(diagScript)) {
1109
+ try {
1110
+ const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
1111
+ execSync(`${pyCmd} "${diagScript}"`, { cwd, stdio: "inherit", timeout: 60000, env });
1112
+ } catch (e) {
1113
+ console.log(` [WARN] MCP 实测脚本异常 (不阻塞): ${(e.message || "").slice(0, 80)}`);
1114
+ }
1115
+ }
1116
+
1117
+ // === 7. 自检: 关键文件到底齐不齐 ===
1118
+ console.log(`\n--- 自检 (关键文件验证) ---`);
1119
+ const checks = [
1120
+ ["requirements.txt", path.join(cwd, "requirements.txt"), "依赖清单(没它pip装不了)"],
1121
+ ["duckdb(pip)", null, "知识图谱核心依赖"],
1122
+ ["kg.duckdb", path.join(cwd, "data", "index", "kg.duckdb"), "代码图谱(MCP需要)"],
1123
+ ["kg_db.duckdb", path.join(cwd, "data", "index", "kg_db.duckdb"), "数据库图谱(table_impact需要)"],
1124
+ [".qoder/commands/", path.join(cwd, ".qoder", "commands"), "斜杠命令"],
1125
+ ];
1126
+ let allOk = true;
1127
+ for (const [label, p, desc] of checks) {
1128
+ if (p === null) {
1129
+ // duckdb 用 python 检测
1130
+ try {
1131
+ execSync(`${pyCmd || "python"} -c "import duckdb"`, { stdio: "pipe", timeout: 5000 });
1132
+ console.log(` [✓] ${label} - ${desc}`);
1133
+ } catch {
1134
+ console.log(` [✗] ${label} 缺失! ${desc}`);
1135
+ console.log(` → 修复: pip install duckdb -i https://pypi.tuna.tsinghua.edu.cn/simple`);
1136
+ allOk = false;
1137
+ }
1138
+ } else if (fs.existsSync(p)) {
1139
+ console.log(` [✓] ${label}`);
1140
+ } else {
1141
+ console.log(` [✗] ${label} 缺失! ${desc}`);
1142
+ if (label.includes("duckdb")) {
1143
+ // duckdb 是团队共享数据, 在 git 里 → 非管理员靠 git pull 拿; 管理员靠本地构建。
1144
+ console.log(` → 修复: 先 npx @hupan56/wlkj doctor (拉团队数据); `);
1145
+ console.log(` 管理员可 npx @hupan56/wlkj run kg_build.py --rebuild`);
1146
+ } else if (label.includes("commands")) {
1147
+ console.log(` → 修复: npx @hupan56/wlkj update`);
1148
+ }
1149
+ allOk = false;
1150
+ }
1151
+ }
1152
+ if (!allOk) {
1153
+ console.log(`\n ⚠️ 有缺失项, 按上面的提示修复后重跑 update。`);
1154
+ } else {
1155
+ console.log(` 全部关键文件就绪 ✓`);
1156
+ }
1157
+
1158
+ console.log(`\n${"=".repeat(50)}`);
1159
+ console.log(` 升级完成! (v${PKG_VERSION})`);
1160
+ console.log(`${"=".repeat(50)}`);
1161
+ console.log(`\n 生效方式:`);
1162
+ console.log(` Qoder IDE / Quest: 新建对话即可`);
1163
+ console.log(` QoderWork: 重启应用或新建对话`);
1164
+
1165
+ // 检查 git 身份是否需要配置 (只检测不引导, update 保持轻量)
1166
+ // 老用户如果之前 init 时塞了假账号, 这里提示重跑 init 修正
1167
+ let gitWarn = false;
1168
+ try {
1169
+ const devFile = path.join(cwd, ".qoder", ".developer");
1170
+ if (fs.existsSync(devFile)) {
1171
+ const devName = fs.readFileSync(devFile, "utf-8").trim();
1172
+ const mj = path.join(cwd, "workspace", "members", devName, "member.json");
1173
+ if (fs.existsSync(mj)) {
1174
+ const m = JSON.parse(fs.readFileSync(mj, "utf-8"));
1175
+ const ga = m.git_author || "";
1176
+ if (!ga || ga.includes("@local") || ga.includes("@unknown")) {
1177
+ gitWarn = true;
1178
+ }
1179
+ }
1180
+ }
1181
+ } catch { /* 检测失败不阻塞 */ }
1182
+
1183
+ if (gitWarn) {
1184
+ console.log(`\n ⚠ git 账号未配置或为假账号 (push/同步会失败)`);
1185
+ console.log(` 重跑初始化可修复: npx @hupan56/wlkj init <你的名字>`);
1186
+ console.log(` (init 会交互式引导你配置团队 git 账号和团队仓库)`);
1187
+ } else {
1188
+ console.log(`\n 如果还没配过 git 账号或团队仓库, 重跑: npx @hupan56/wlkj init <你的名字>`);
1189
+ }
1190
+
1191
+ if (protectedN > 0) {
1192
+ console.log(`\n ⚠ 有配置文件新版有变化, 生成 .new 文件:`);
1193
+ console.log(` 确认无需合并后可直接删除, 或手动合并后删除`);
1194
+ }
1195
+ console.log("");
1196
+ }
1197
+
1198
+ // ─────────────────────────────────────────────────────────────
1199
+ // 一次性迁移: 把引擎代码从团队 git 跟踪里摘出来 (引擎数据分离)
1200
+ // 团队管理员只跑一次, 其余成员 wlkj update 后自动同步。
1201
+ // 原理: git rm -r --cached (只删 git 索引, 不删磁盘文件) + commit。
1202
+ // 之后引擎文件仍在磁盘 (由 wlkj update 重新生成), 但不再被 git 跟踪 →
1203
+ // 彻底消除"引擎更新污染团队 git"和"新机 init 产生 unrelated histories"。
1204
+ // 团队数据 (.qoder/config.yaml, settings.json, data/learning/, data/, workspace/) 保持不动。
1205
+ // ─────────────────────────────────────────────────────────────
1206
+ function doMigrate() {
1207
+ const { execSync } = require("child_process");
1208
+ const cwd = process.cwd();
1209
+ const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
1210
+ function _es(cmd, opts = {}) {
1211
+ return execSync(cmd, { cwd, encoding: "utf-8", timeout: 30000, stdio: "pipe", env, ...opts });
1212
+ }
1213
+
1214
+ console.log("");
1215
+ console.log("=== wlkj migrate: 引擎代码改为 npm 生成 (引擎数据分离) ===");
1216
+ console.log("");
1217
+
1218
+ // 1. 必须在 git 仓库里
1219
+ if (!fs.existsSync(path.join(cwd, ".git"))) {
1220
+ console.log(" ✗ 当前目录不是 git 仓库。请在团队仓库根目录运行。");
1221
+ console.log("");
1222
+ return;
1223
+ }
1224
+
1225
+ // 2. 要从 git 跟踪摘除的引擎目录 (团队数据 config/data/learning 等不在此列)
1226
+ const ENGINE_DIRS = [
1227
+ ".qoder/scripts", ".qoder/skills", ".qoder/commands", ".qoder/agents",
1228
+ ".qoder/contracts", ".qoder/hooks", ".qoder/templates", ".qoder/rules",
1229
+ ];
1230
+
1231
+ // 3. 哪些目录当前确被跟踪 (只摘这些, 避免在已迁移过的仓库上空跑)
1232
+ const tracked = [];
1233
+ for (const d of ENGINE_DIRS) {
1234
+ let isTracked = false;
1235
+ try {
1236
+ // git ls-files 能列出被跟踪的文件; 有输出说明该目录还有文件在跟踪
1237
+ const out = _es(`git ls-files "${d}"`, { timeout: 10000 });
1238
+ if (out.trim()) isTracked = true;
1239
+ } catch { /* 命令失败当未跟踪 */ }
1240
+ if (isTracked) tracked.push(d);
1241
+ }
1242
+
1243
+ if (tracked.length === 0) {
1244
+ console.log(" ✓ 引擎目录已不在 git 跟踪中, 无需迁移。");
1245
+ console.log(" (这通常是已经迁移过了, 或是个全新仓库)");
1246
+ console.log("");
1247
+ console.log(" 下一步:");
1248
+ console.log(" 全新机器: npx @hupan56/wlkj init <名字> <角色>");
1249
+ console.log(" 老用户: npx @hupan56/wlkj update");
1250
+ console.log(" 自检: npx @hupan56/wlkj selftest");
1251
+ console.log("");
1252
+ return;
1253
+ }
1254
+
1255
+ console.log(` 将从 git 跟踪中移除 (磁盘文件保留, 不删):`);
1256
+ for (const d of tracked) console.log(` - ${d}`);
1257
+ console.log("");
1258
+
1259
+ // 4. 先确保 .gitignore 已含引擎忽略规则 (防止 rm --cached 后又被加回)
1260
+ // 逐条补缺, 不重复不乱插。
1261
+ const giPath = path.join(cwd, ".gitignore");
1262
+ const RULES_TO_ENSURE = [
1263
+ "# 引擎代码 (由 npm 包 @hupan56/wlkj init/update 生成, 不进团队 git)",
1264
+ ".qoder/scripts/", ".qoder/skills/", ".qoder/commands/", ".qoder/agents/",
1265
+ ".qoder/contracts/", ".qoder/hooks/", ".qoder/templates/", ".qoder/rules/",
1266
+ ".qoder/repowiki/", ".qoder/archive/",
1267
+ ".wlkj-pre-reset/",
1268
+ ];
1269
+ let gi = fs.existsSync(giPath) ? fs.readFileSync(giPath, "utf-8") : "";
1270
+ const existingLines = new Set(gi.split(/\r?\n/));
1271
+ const missing = RULES_TO_ENSURE.filter(rule => !existingLines.has(rule));
1272
+ if (missing.length > 0) {
1273
+ // 缺哪条补哪条 (注释行单独补, 规则行单独补), 补在最前面
1274
+ gi = missing.join("\n") + "\n" + (gi ? "\n" + gi : "");
1275
+ fs.writeFileSync(giPath, gi, "utf-8");
1276
+ console.log(` ✓ .gitignore 已补 ${missing.length} 条忽略规则`);
1277
+ }
1278
+
1279
+ // 5. git rm -r --cached (只删索引, 不删磁盘文件)
1280
+ console.log(" 从 git 索引移除 (git rm -r --cached, 磁盘文件不动)...");
1281
+ try {
1282
+ _es(`git rm -r --cached ${tracked.map(t => `"${t}"`).join(" ")}`, { timeout: 60000 });
1283
+ console.log(" ✓ 已移除");
1284
+ } catch (e) {
1285
+ console.log(` ✗ git rm 失败: ${(e.stderr || e.message || "").slice(0, 200)}`);
1286
+ console.log(" 可手动跑: " + tracked.map(t => `git rm -r --cached ${t}`).join(" && "));
1287
+ return;
1288
+ }
1289
+
1290
+ // 6. 自动 commit (标记这次架构变更)
1291
+ console.log(" 提交迁移...");
1292
+ try {
1293
+ _es(`git add -A .gitignore`, { timeout: 10000 });
1294
+ _es(`git commit -m "chore(wlkj): 引擎代码改为 npm 生成, 不再进团队 git (引擎数据分离)"`, { timeout: 30000 });
1295
+ console.log(" ✓ 已提交");
1296
+ } catch (e) {
1297
+ console.log(` ⚠ 自动提交失败 (可手动 commit): ${(e.stderr || e.message || "").slice(0, 150)}`);
1298
+ }
1299
+
1300
+ console.log("");
1301
+ console.log("=== 迁移完成 ===");
1302
+ console.log(" 接下来:");
1303
+ console.log(" 1. npx @hupan56/wlkj sync # 推到团队远程");
1304
+ console.log(" 2. 团队其他成员各自跑: npx @hupan56/wlkj update");
1305
+ console.log(" (本地引擎文件不会被删, 之后由 update 重新生成, 互不冲突)");
1306
+ console.log("");
1307
+ }
1308
+
1309
+ function doHelp() {
1310
+ console.log("");
1311
+ console.log(`wlkj - AI 产品研发工作流 (v${PKG_VERSION})`);
1312
+ console.log("");
1313
+ console.log("=== 终端只用 3 个 (从下载到日常) ===");
1314
+ console.log("");
1315
+ console.log(" npx @hupan56/wlkj install-env 首次: 装环境 (Node/Python/git/pip)");
1316
+ console.log(" npx @hupan56/wlkj init [名字] 首次: 装引擎 + 初始化身份");
1317
+ console.log(" npx @hupan56/wlkj update 日常: 升级 (刷新引擎+补依赖+刷MCP)");
1318
+ console.log("");
1319
+ console.log("=== 日常工作全在 Qoder 里 (不用回终端) ===");
1320
+ console.log(" 说中文: '写个XX的需求' '查XX代码' '建任务' '测一下XX'");
1321
+ console.log(" 或斜杠: /wl-prd /wl-design /wl-task /wl-code /wl-test /wl-commit");
1322
+ console.log(" (提交/同步 /wl-task 和 /wl-commit 会自动做, 不用单独 npx sync)");
1323
+ console.log("");
1324
+ console.log("=== 能力市场 ===");
1325
+ console.log(" npx @hupan56/wlkj install skill <token> --host <url> 从市场装 Skill 到本地 IDE");
1326
+ console.log(" npx @hupan56/wlkj install agent <token> --host <url> 从市场装 Agent (含依赖 Skill)");
1327
+ console.log("");
1328
+ console.log("=== 进阶 (管理员/特殊情况才用, 不常用) ===");
1329
+ console.log(" npx @hupan56/wlkj selftest 全套件自检 (跨环境+QoderWork工具协议+smoke+体检+MCP)");
1330
+ console.log(" npx @hupan56/wlkj doctor 体检 (或在 Qoder 里 /wl-init 无参数)");
1331
+ console.log(" npx @hupan56/wlkj migrate 老团队一次性升级到 引擎数据分离架构");
1332
+ console.log(" npx @hupan56/wlkj run <脚本> 万能跑脚本 (AI 内部用, 用户很少需要)");
1333
+ console.log("");
1334
+ console.log(" 看不到命令? 新建对话 / 重启 QoderWork");
1335
+ console.log(" 环境问题? python .qoder/scripts/setup/init_doctor.py --fix");
1336
+ console.log("");
1337
+ }
1338
+
1339
+ function doInstallEnv(checkOnly = false, location = null) {
1340
+ const { execSync } = require("child_process");
1341
+ const isWin = process.platform === "win32";
1342
+ const isMac = process.platform === "darwin";
1343
+
1344
+ // 解析 --location(CLI 入口已提取,这里兜底)
1345
+ console.log("\n=== 环境检测 ===\n");
1346
+ if (location) {
1347
+ console.log(` 安装位置: ${location}${isWin ? "" : " (仅 Windows winget 支持指定目录)"}`);
1348
+ // Windows: 预建目录
1349
+ if (isWin) {
1350
+ try { fs.mkdirSync(location, { recursive: true }); } catch { /* */ }
1351
+ }
1352
+ }
1353
+
1354
+ // 检测函数
1355
+ function check(cmd) {
1356
+ try { return execSync(`${cmd} --version`, { encoding: "utf-8", timeout: 5000, stdio: "pipe" }).trim().split("\n")[0]; }
1357
+ catch { return null; }
1358
+ }
1359
+ function has(cmd) {
1360
+ try { execSync(`where ${cmd}`, { encoding: "utf-8", timeout: 3000, stdio: "pipe" }); return true; }
1361
+ catch { try { execSync(`which ${cmd}`, { encoding: "utf-8", timeout: 3000, stdio: "pipe" }); return true; } catch { return false; } }
1362
+ }
1363
+ // Windows: 把目录加到用户 PATH(幂等,已存在不重复加)
1364
+ function addToPathWin(dir) {
1365
+ if (!isWin) return false;
1366
+ try {
1367
+ // 用 setx 会截断超长 PATH,改用 PowerShell 读 User PATH 追加
1368
+ const ps = `powershell -NoProfile -Command "$p=[Environment]::GetEnvironmentVariable('Path','User'); if($p -notlike '*{dir}*'){[Environment]::SetEnvironmentVariable('Path',$p.TrimEnd(';')+';{dir}','User')}"`.replace(/{dir}/g, dir.replace(/\\/g, "\\\\"));
1369
+ execSync(ps, { stdio: "pipe", timeout: 15000 });
1370
+ return true;
1371
+ } catch (e) { console.log(` [WARN] 加 PATH 失败(可手动加): ${dir}`); return false; }
1372
+ }
1373
+ function install(name, wingetId, brewPkg) {
1374
+ if (checkOnly) return false;
1375
+ try {
1376
+ if (isWin && has("winget")) {
1377
+ console.log(` 用 winget 装 ${name}...`);
1378
+ // winget --location 不一定被安装包尊重,但先传上
1379
+ let cmd = `winget install --id ${wingetId} -e --source winget --accept-source-agreements --accept-package-agreements`;
1380
+ if (location) cmd += ` --location "${location}"`;
1381
+ execSync(cmd, { stdio: "inherit", timeout: 300000 });
1382
+ return true;
1383
+ } else if (isMac && has("brew")) {
1384
+ console.log(` 用 brew 装 ${name}...`);
1385
+ execSync(`brew install ${brewPkg}`, { stdio: "inherit", timeout: 300000 });
1386
+ return true;
1387
+ } else if (!isWin && !isMac) {
1388
+ // Linux: 尝试 apt/dnf/yum
1389
+ for (const [mgr, pkg] of [["apt", brewPkg], ["dnf", brewPkg]]) {
1390
+ if (has(mgr)) {
1391
+ console.log(` 用 ${mgr} 装 ${name}...`);
1392
+ execSync(`sudo ${mgr} install -y ${pkg}`, { stdio: "inherit", timeout: 300000 });
1393
+ return true;
1394
+ }
1395
+ }
1396
+ }
1397
+ } catch (e) { console.log(` 自动安装失败: ${(e.message || "").slice(0, 80)}`); }
1398
+ return false;
1399
+ }
1400
+
1401
+ const nodeVer = check("node");
1402
+ const pyVer = check("python") || check("python3");
1403
+ const gitVer = check("git");
1404
+
1405
+ console.log(` Node.js: ${nodeVer ? "[OK] " + nodeVer : "[缺失] (npx 命令需要)"}`);
1406
+ console.log(` Python: ${pyVer ? "[OK] " + pyVer : "[缺失] (引擎需要)"}`);
1407
+ console.log(` git: ${gitVer ? "[OK] " + gitVer : "[缺失] (可选, 核心功能不依赖)"}`);
1408
+
1409
+ if (checkOnly) {
1410
+ console.log("\n仅检测。去掉 --check 自动安装。");
1411
+ return;
1412
+ }
1413
+
1414
+ const need = [];
1415
+ if (!nodeVer) {
1416
+ console.log("");
1417
+ if (nodeVer || install("Node.js", "OpenJS.NodeJS.LTS", "node")) { console.log(` [OK] Node.js 已装`); }
1418
+ else { need.push("Node.js (手动: https://nodejs.org)"); }
1419
+ }
1420
+ if (!pyVer) {
1421
+ console.log("");
1422
+ if (install("Python", "Python.Python.3.12", "python@3.12")) { console.log(` [OK] Python 已装`); }
1423
+ else { need.push("Python (手动: https://python.org, 勾 Add to PATH)"); }
1424
+ }
1425
+ if (!gitVer) {
1426
+ console.log("");
1427
+ if (install("git", "Git.Git", "git")) { console.log(` [OK] git 已装`); }
1428
+ else { need.push("git (手动: https://git-scm.com, 可选)"); }
1429
+ }
1430
+
1431
+ // --location 模式: 提示用户验证安装位置 + 手动加 PATH
1432
+ // (winget 的 --location 对 Node/Python 不一定生效,需用户确认)
1433
+ if (location && isWin) {
1434
+ console.log(`\n--- 安装位置确认 ---`);
1435
+ console.log(` 你指定了: ${location}`);
1436
+ console.log(` 注意: winget 的 --location 对 Node.js/Python 不一定生效`);
1437
+ console.log(` (取决于安装包是否支持自定义路径)`);
1438
+ console.log(` 请检查实际装到哪了:`);
1439
+ console.log(` 重新打开终端后跑: node --version python --version git --version`);
1440
+ console.log(` 如果某软件没进 PATH, 手动把它加进去:`);
1441
+ console.log(` 设置 → 系统 → 关于 → 高级系统设置 → 环境变量 → Path → 新建`);
1442
+ console.log(` 或用命令: npx @hupan56/wlkj add-path "D:\\wldev\\nodejs"`);
1443
+ }
1444
+
1445
+ // Python pip 依赖 (知识图谱/MCP/测试需要)
1446
+ console.log("\n--- Python 依赖 ---");
1447
+ const pyCmd = check("python") ? "python" : (check("python3") ? "python3" : null);
1448
+ if (pyCmd) {
1449
+ // 找 requirements.txt: 引擎已安装则用 .qoder/scripts/../requirements.txt
1450
+ const reqPaths = [
1451
+ path.join(process.cwd(), "requirements.txt"),
1452
+ path.join(process.cwd(), ".qoder", "..", "requirements.txt"),
1453
+ ].filter(p => fs.existsSync(p));
1454
+ if (reqPaths.length > 0) {
1455
+ console.log(` 安装 pip 依赖: ${path.basename(reqPaths[0])} (失败自动切国内镜像)`);
1456
+ installPipDeps(pyCmd, reqPaths[0]);
1457
+ } else {
1458
+ console.log(" [跳过] 未找到 requirements.txt (先 npx @hupan56/wlkj init 安装引擎)");
1459
+ }
1460
+ } else {
1461
+ console.log(" [跳过] Python 未装, 无法装 pip 依赖");
1462
+ }
1463
+
1464
+ console.log("\n=== 结果 ===");
1465
+ if (need.length === 0) {
1466
+ console.log(" 环境就绪! 下一步: npx @hupan56/wlkj init");
1467
+ console.log(" (新装的可能需要重新打开终端)");
1468
+ } else {
1469
+ console.log(" 以下需手动安装:");
1470
+ need.forEach(n => console.log(" - " + n));
1471
+ }
1472
+ }
1473
+
1474
+ // add-path: 把目录加到 Windows 用户 PATH(幂等)
1475
+ function doAddPath(dir) {
1476
+ if (!dir) { console.log("用法: npx @hupan56/wlkj add-path <目录>"); return; }
1477
+ if (process.platform !== "win32") {
1478
+ console.log("add-path 仅支持 Windows。Mac/Linux 请手动加到 ~/.zshrc 或 ~/.bashrc");
1479
+ return;
1480
+ }
1481
+ const abs = path.resolve(dir);
1482
+ if (!fs.existsSync(abs)) {
1483
+ console.log(`[警告] 目录不存在: ${abs}(仍会加到 PATH)`);
1484
+ }
1485
+ try {
1486
+ const ps = `powershell -NoProfile -Command "$p=[Environment]::GetEnvironmentVariable('Path','User'); if($p -notlike '*${abs.replace(/\\/g,"\\\\")}*'){[Environment]::SetEnvironmentVariable('Path',$p.TrimEnd(';')+';${abs.replace(/\\/g,"\\\\")}','User'); Write-Output 'ADDED'} else { Write-Output 'EXISTS' }"`;
1487
+ const out = execSync(ps, { encoding: "utf-8", timeout: 15000 }).trim();
1488
+ console.log(` ${out === "ADDED" ? "已加入" : "已存在(无需重复加)"}: ${abs}`);
1489
+ console.log(` ⚠ 需要重新打开终端才生效`);
1490
+ } catch (e) {
1491
+ console.log(` 失败: ${(e.message || "").slice(0, 100)}`);
1492
+ console.log(` 手动加: 设置 → 系统 → 关于 → 高级系统设置 → 环境变量 → Path → 新建 → ${abs}`);
1493
+ }
1494
+ }
1495
+
1496
+ const [,, cmd, ...rest] = process.argv;
1497
+ const mapped = rest.map(a => a === "-p" ? "--priority" : a);
1498
+
1499
+ switch (cmd) {
1500
+ case "init": doInit(rest[0], rest[1]); break;
1501
+ case "update": case "upgrade": doUpdate(); break;
1502
+ case "migrate": doMigrate(); break;
1503
+ case "install-env": {
1504
+ const checkOnly = rest.includes("--check");
1505
+ // 提取 --location <dir> 或 --location=<dir>
1506
+ let loc = null;
1507
+ const li = rest.indexOf("--location");
1508
+ if (li !== -1 && rest[li + 1]) loc = rest[li + 1];
1509
+ const eq = rest.find(a => a.startsWith("--location="));
1510
+ if (eq) loc = eq.slice("--location=".length);
1511
+ doInstallEnv(checkOnly, loc);
1512
+ break;
1513
+ }
1514
+ case "add-path": doAddPath(rest[0]); break;
1515
+ case "task": process.stdout.write(py("task.py", mapped)); break;
1516
+ case "status": doStatus(); break;
1517
+ case "session": process.stdout.write(py("add_session.py", rest)); break;
1518
+ case "help": case "--help": case "-h": doHelp(); break;
1519
+
1520
+ // ── 核心 5 个: 装/升/体检/同步/万能跑 ──
1521
+ case "doctor": case "体检":
1522
+ runScript("init_doctor.py", rest); break;
1523
+ case "sync": case "push": case "commit":
1524
+ runScript("team_sync.py", ["push", ...rest.filter(a => a !== "push")]); break;
1525
+ case "pull":
1526
+ runScript("team_sync.py", ["pull"]); break;
1527
+
1528
+ // ── 万能透传: npx wlkj run <脚本名> [参数...] ──
1529
+ // 任何 .qoder/scripts/ 下的脚本都能跑, 不用记命令名
1530
+ // 例: npx wlkj run kg_build.py --rebuild
1531
+ // npx wlkj run eval_prd.py xxx.md
1532
+ // npx wlkj run autotest.py quick
1533
+ case "run": {
1534
+ // 万能跑脚本 (AI 内部用, 用户日常不直接用 — 用 /命令 或 init/update)
1535
+ if (!rest[0]) { console.log("用法: npx wlkj run <脚本名> [参数...]\n例: npx wlkj run kg_build.py --rebuild"); break; }
1536
+ runScript(rest[0], rest.slice(1));
1537
+ break;
1538
+ }
1539
+
1540
+ case "提交": case "拉取最新": case "同步":
1541
+ case "查看状态": case "提交PRD": case "提交Spec": case "提交任务":
1542
+ gitOp(cmd); break;
1543
+
1544
+ // ── 自检: 一条命令跑完所有验证套件 (替代 7 步手动清单的"环境验证"部分) ──
1545
+ // 用法: npx @hupan56/wlkj selftest
1546
+ // 跑: cross-env 模拟 + QoderWork 工具协议 + smoke + doctor + mcp_doctor
1547
+ case "selftest": case "自检": {
1548
+ const { spawnSync: _ss } = require("child_process");
1549
+ const cwd = process.cwd();
1550
+ const pyCmd = detectPyCmd();
1551
+
1552
+ // ⚠ 引擎未安装时不能跑 selftest (所有路径都不存在, 5 FAIL 无意义)
1553
+ const engineDir = path.join(cwd, ".qoder", "scripts");
1554
+ if (!fs.existsSync(engineDir)) {
1555
+ console.log("\n⚠ 引擎未安装! 先跑:");
1556
+ console.log(" 全新机器: npx @hupan56/wlkj init <名字> <角色>");
1557
+ console.log(" 老用户: npx @hupan56/wlkj update");
1558
+ console.log("");
1559
+ process.exit(1);
1560
+ break;
1561
+ }
1562
+
1563
+ const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1", PYTHONPATH: path.join(cwd, ".qoder", "scripts") };
1564
+ console.log("\n" + "=".repeat(56));
1565
+ console.log(" wlkj selftest — 全套件自检 (验证跨环境 + 第一宿主)");
1566
+ console.log("=".repeat(56));
1567
+ let totalFail = 0;
1568
+ // 找 packages/wlkj/tests/ 目录:可能在 cwd 下(项目根),也可能在 npx 缓存里
1569
+ // 用 __dirname (cli.js 所在目录) 往上找
1570
+ let testsDir = path.join(__dirname, "..", "tests");
1571
+ if (!fs.existsSync(testsDir)) {
1572
+ // fallback: cwd 下找
1573
+ testsDir = path.join(cwd, "packages", "wlkj", "tests");
1574
+ }
1575
+ const suites = [
1576
+ ["1. 跨环境模拟验证", [path.join(testsDir, "test_cross_env.py")]],
1577
+ ["2. QoderWork 工具协议 (真实 MCP JSON-RPC)", [path.join(testsDir, "test_qoderwork_tools.py")]],
1578
+ ["3. 命令底层脚本 smoke", ["-c", "import sys, os; p=os.path.join(os.getcwd(), '.qoder', 'scripts', 'validation', 'test', 'smoke_all_commands.py'); sys.path.insert(0, os.path.join(os.getcwd(), '.qoder', 'scripts')); exec(open(p).read()) if os.path.isfile(p) else print('[SKIP] smoke_all_commands.py 不存在 (init 后才有)')"]],
1579
+ ["4. 环境体检 (doctor)", [path.join(cwd, ".qoder", "scripts", "orchestration", "wlkj.py"), "doctor"]],
1580
+ ["5. MCP 实测 (mcp_doctor)", [path.join(cwd, ".qoder", "scripts", "protocol", "mcp", "mcp_doctor.py")]],
1581
+ ];
1582
+ for (const [label, args] of suites) {
1583
+ console.log("\n--- " + label + " ---");
1584
+ if (!pyCmd) { console.log(" [SKIP] Python 未装"); continue; }
1585
+ // suites 1-2 在 packages/wlkj/tests/ 下 (相对 cwd), 3-5 用绝对路径
1586
+ const isTestFile = args[0] && args[0].endsWith(".py") && !args[0].startsWith(path.sep) && !args[0].includes(":");
1587
+ const fullArgs = isTestFile ? [path.join(cwd, args[0])] : args;
1588
+ try {
1589
+ const r = _ss(pyCmd, fullArgs, { cwd, stdio: "inherit", timeout: 300000, env });
1590
+ if (r.status !== 0) { console.log(" [FAIL] exit " + r.status); totalFail++; }
1591
+ } catch (e) { console.log(" [FAIL] " + (e.message || "").slice(0, 80)); totalFail++; }
1592
+ }
1593
+ console.log("\n" + "=".repeat(56));
1594
+ console.log(totalFail === 0 ? " ✓ 全部自检通过" : " ⚠ " + totalFail + " 个套件失败, 见上方");
1595
+ console.log("=".repeat(56) + "\n");
1596
+ process.exit(totalFail === 0 ? 0 : 1);
1597
+ break;
1598
+ }
1599
+
1600
+ // ── 能力市场安装: npx @hupan56/wlkj install <skill|agent> <token> --host <url> ──
1601
+ case "install": {
1602
+ const kind = rest[0]; // skill / agent
1603
+ const token = rest[1]; // share_token
1604
+ const hostIdx = rest.indexOf("--host");
1605
+ const host = hostIdx !== -1 && rest[hostIdx + 1] ? rest[hostIdx + 1] : "http://127.0.0.1:10010";
1606
+
1607
+ if (!kind || !token || !["skill", "agent"].includes(kind)) {
1608
+ console.log("用法: npx @hupan56/wlkj install <skill|agent> <token> [--host <url>]");
1609
+ console.log("例: npx @hupan56/wlkj install skill abc123 --host http://127.0.0.1:10010");
1610
+ process.exit(1);
1611
+ }
1612
+
1613
+ (async () => {
1614
+ try {
1615
+ // 探测本地 IDE
1616
+ const cwd = process.cwd();
1617
+ let ide = "claude";
1618
+ if (fs.existsSync(path.join(cwd, ".cursor"))) ide = "cursor";
1619
+ else if (fs.existsSync(path.join(cwd, ".qoder"))) ide = "qoder";
1620
+ else if (fs.existsSync(path.join(cwd, ".claude"))) ide = "claude";
1621
+ else if (fs.existsSync(path.join(cwd, "CLAUDE.md"))) ide = "claude";
1622
+ else if (fs.existsSync(path.join(cwd, "AGENTS.md"))) ide = "codex";
1623
+
1624
+ if (kind === "agent") {
1625
+ console.log(`→ 从 ${host} 拉取 Agent ${token}...`);
1626
+ const res = await fetch(`${host}/api/market/agent/${token}`);
1627
+ if (!res.ok) throw new Error(`API 返回 ${res.status}`);
1628
+ const agent = await res.json();
1629
+ console.log(`✓ ${agent.icon || "🤖"} ${agent.title}`);
1630
+
1631
+ // 写入 IDE
1632
+ if (ide === "claude" || ide === "codex") {
1633
+ const dir = path.join(cwd, ".claude", "commands");
1634
+ fs.mkdirSync(dir, { recursive: true });
1635
+ const fname = (agent.name || agent.title || "agent") + ".md";
1636
+ let content = `---\ndescription: ${agent.title || ""}\n---\n\n`;
1637
+ content += agent.rolePrompt || agent.role_prompt || `你是${agent.title || "AI助手"}。\n`;
1638
+ if ((agent.toolIds || []).length) {
1639
+ content += `\n## 可用工具\n${(agent.toolIds || []).join(", ")}\n`;
1640
+ }
1641
+ if ((agent.skillIds || []).length) {
1642
+ content += `\n## 可用技能\n${(agent.skillIds || []).join(", ")}\n`;
1643
+ }
1644
+ fs.writeFileSync(path.join(dir, fname), content, "utf-8");
1645
+ console.log(`✓ 已写入: ${path.join(dir, fname)}`);
1646
+ } else if (ide === "qoder") {
1647
+ const dir = path.join(cwd, ".qoder", "agents");
1648
+ fs.mkdirSync(dir, { recursive: true });
1649
+ const fname = (agent.name || agent.title || "agent") + ".json";
1650
+ fs.writeFileSync(path.join(dir, fname), JSON.stringify({
1651
+ name: agent.name, title: agent.title,
1652
+ systemPrompt: agent.rolePrompt || agent.role_prompt || "",
1653
+ tools: agent.toolIds || [], skills: agent.skillIds || [],
1654
+ model: agent.model || null,
1655
+ }, null, 2), "utf-8");
1656
+ console.log(`✓ 已写入: ${path.join(dir, fname)}`);
1657
+ } else if (ide === "cursor") {
1658
+ const dir = path.join(cwd, ".cursor", "rules");
1659
+ fs.mkdirSync(dir, { recursive: true });
1660
+ const fname = (agent.name || agent.title || "agent") + ".mdc";
1661
+ let content = `---\ndescription: ${agent.title || ""}\n---\n`;
1662
+ content += agent.rolePrompt || agent.role_prompt || "";
1663
+ fs.writeFileSync(path.join(dir, fname), content, "utf-8");
1664
+ console.log(`✓ 已写入: ${path.join(dir, fname)}`);
1665
+ }
1666
+
1667
+ // 递归装依赖 Skill
1668
+ const skillIds = agent.skillIds || [];
1669
+ for (const sid of skillIds) {
1670
+ if (typeof sid === "string" && sid.length > 5) {
1671
+ console.log(`→ 递归安装 Skill: ${sid}`);
1672
+ try {
1673
+ const sres = await fetch(`${host}/api/market/skill/${sid}`);
1674
+ if (sres.ok) {
1675
+ const skill = await sres.json();
1676
+ const sdir = path.join(cwd, ".claude", "skills", skill.name || sid);
1677
+ fs.mkdirSync(sdir, { recursive: true });
1678
+ let smd = `---\nname: ${skill.name || sid}\ndescription: ${skill.description || ""}\n---\n`;
1679
+ smd += skill.promptTemplate || skill.prompt_template || "";
1680
+ fs.writeFileSync(path.join(sdir, "SKILL.md"), smd, "utf-8");
1681
+ console.log(` ✓ ${skill.title || sid}`);
1682
+ } else { console.warn(` ⚠ Skill ${sid} 不存在或未发布`); }
1683
+ } catch (e) { console.warn(` ⚠ Skill ${sid} 安装失败: ${e.message}`); }
1684
+ }
1685
+ }
1686
+ console.log(`\n✅ Agent "${agent.title}" 已安装(${ide} 格式)`);
1687
+ }
1688
+
1689
+ else if (kind === "skill") {
1690
+ console.log(`→ 从 ${host} 拉取 Skill ${token}...`);
1691
+ const res = await fetch(`${host}/api/market/skill/${token}`);
1692
+ if (!res.ok) throw new Error(`API 返回 ${res.status}`);
1693
+ const skill = await res.json();
1694
+ console.log(`✓ ${skill.icon || "🛠"} ${skill.title}`);
1695
+
1696
+ const sname = skill.name || token;
1697
+ const sdir = path.join(cwd, ".claude", "skills", sname);
1698
+ fs.mkdirSync(sdir, { recursive: true });
1699
+ let smd = `---\nname: ${sname}\ntitle: ${skill.title || ""}\ndescription: ${skill.description || ""}\nicon: ${skill.icon || ""}\nversion: ${skill.version || "1.0.0"}\n---\n`;
1700
+ smd += skill.promptTemplate || skill.prompt_template || "";
1701
+ fs.writeFileSync(path.join(sdir, "SKILL.md"), smd, "utf-8");
1702
+ console.log(`✓ 已写入: ${path.join(sdir, "SKILL.md")}`);
1703
+ console.log(`\n✅ Skill "${skill.title}" 已安装`);
1704
+ }
1705
+ } catch (e) {
1706
+ console.error(`✗ 安装失败: ${e.message}`);
1707
+ process.exit(1);
1708
+ }
1709
+ })();
1710
+ break;
1711
+ }
1712
+
1713
+ default: doHelp();
1714
1714
  }