@hupan56/wlkj 2.7.11 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +375 -78
- package/package.json +29 -29
- package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
- package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
- package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
- package/templates/.qoder/learning/eval-history.jsonl +28 -0
- package/templates/data/index/wiki-index.json +8 -0
- package/templates/qoder/agents/insight-planning.md +1 -1
- package/templates/qoder/agents/insight-research.md +28 -15
- package/templates/qoder/commands/optional/wl-insight.md +159 -161
- package/templates/qoder/commands/optional/wl-report.md +4 -4
- package/templates/qoder/commands/optional/wl-status.md +2 -2
- package/templates/qoder/commands/wl-code.md +2 -2
- package/templates/qoder/commands/wl-design.md +2 -2
- package/templates/qoder/commands/wl-init.md +3 -3
- package/templates/qoder/commands/wl-prd.md +2 -2
- package/templates/qoder/commands/wl-req.md +43 -0
- package/templates/qoder/commands/wl-search.md +8 -8
- package/templates/qoder/commands/wl-task.md +17 -17
- package/templates/qoder/commands/wl-test.md +41 -15
- package/templates/qoder/config.yaml +17 -1
- package/templates/qoder/hooks/session-start.py +12 -22
- package/templates/qoder/nul +4 -0
- package/templates/qoder/rules/wl-pipeline.md +22 -48
- package/templates/qoder/scripts/README.md +139 -0
- package/templates/qoder/scripts/common/autotest_auth.py +109 -0
- package/templates/qoder/scripts/common/bootstrap.py +145 -0
- package/templates/qoder/scripts/common/check_publish.py +98 -0
- package/templates/qoder/scripts/common/cmd_registry.py +112 -0
- package/templates/qoder/scripts/common/config.py +187 -0
- package/templates/qoder/scripts/common/contract.py +317 -0
- package/templates/qoder/scripts/common/developer.py +2 -1
- package/templates/qoder/scripts/common/feishu.py +10 -9
- package/templates/qoder/scripts/common/guard.py +159 -0
- package/templates/qoder/scripts/common/identity.py +121 -2
- package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
- package/templates/qoder/scripts/common/mcp_base.py +268 -0
- package/templates/qoder/scripts/common/paths.py +187 -1
- package/templates/qoder/scripts/common/result.py +223 -0
- package/templates/qoder/scripts/common/roles.py +60 -0
- package/templates/qoder/scripts/common/task_utils.py +21 -9
- package/templates/qoder/scripts/common/test_extract.py +115 -0
- package/templates/qoder/scripts/kg/__init__.py +11 -0
- package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
- package/templates/qoder/scripts/kg/build_relations.py +127 -0
- package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
- package/templates/qoder/scripts/kg/build_workflows.py +144 -0
- package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
- package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
- package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
- package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
- package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
- package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
- package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
- package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
- package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
- package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
- package/templates/qoder/scripts/kg/prefetch.py +359 -0
- package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
- package/templates/qoder/scripts/mcp/__init__.py +11 -0
- package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
- package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
- package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
- package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
- package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
- package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
- package/templates/qoder/scripts/report/__init__.py +11 -0
- package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
- package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
- package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
- package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
- package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
- package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
- package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
- package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
- package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
- package/templates/qoder/scripts/report/req.py +222 -0
- package/templates/qoder/scripts/report/role.py +33 -0
- package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
- package/templates/qoder/scripts/setup/__init__.py +11 -0
- package/templates/qoder/scripts/setup/carriers.py +662 -0
- package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
- package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
- package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
- package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
- package/templates/qoder/scripts/{setup.py → setup/setup.py} +147 -0
- package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
- package/templates/qoder/scripts/task/__init__.py +11 -0
- package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
- package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
- package/templates/qoder/scripts/task/task.py +221 -0
- package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
- package/templates/qoder/scripts/task/task_query.py +161 -0
- package/templates/qoder/scripts/task/task_relations.py +424 -0
- package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
- package/templates/qoder/scripts/test/__init__.py +11 -0
- package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
- package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
- package/templates/qoder/scripts/test/autotest_data.py +675 -0
- package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
- package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
- package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
- package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
- package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
- package/templates/qoder/scripts/wlkj.py +116 -0
- package/templates/qoder/settings.json +1 -10
- package/templates/qoder/skills/design-import/SKILL.md +226 -226
- package/templates/qoder/skills/design-review/SKILL.md +82 -82
- package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
- package/templates/qoder/skills/prd-review/SKILL.md +5 -5
- package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
- package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
- package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
- package/templates/qoder/skills/test-generator/SKILL.md +5 -5
- package/templates/qoder/skills/wl-code/SKILL.md +4 -4
- package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
- package/templates/qoder/skills/wl-design/SKILL.md +3 -3
- package/templates/qoder/skills/wl-init/SKILL.md +8 -8
- package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
- package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
- package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
- package/templates/qoder/skills/wl-report/SKILL.md +7 -7
- package/templates/qoder/skills/wl-search/SKILL.md +13 -13
- package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
- package/templates/qoder/skills/wl-status/SKILL.md +5 -5
- package/templates/qoder/skills/wl-task/SKILL.md +6 -6
- package/templates/qoder/skills/wl-test/SKILL.md +102 -39
- package/templates/root/AGENTS.md +39 -40
- package/templates/qoder/hooks/inject-workflow-state.py +0 -169
- package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/check_carriers.py +0 -238
- package/templates/qoder/scripts/check_mcp_launch.py +0 -183
- package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
- package/templates/qoder/scripts/collect_prds.py +0 -31
- package/templates/qoder/scripts/common/mentions.py +0 -134
- package/templates/qoder/scripts/common/utf8.py +0 -38
- package/templates/qoder/scripts/extract_routes.py +0 -54
- package/templates/qoder/scripts/extract_routes_tree.py +0 -78
- package/templates/qoder/scripts/handoff.py +0 -22
- package/templates/qoder/scripts/init_developer.py +0 -76
- package/templates/qoder/scripts/parse_prds.py +0 -33
- package/templates/qoder/scripts/role.py +0 -51
- package/templates/qoder/scripts/sync_carriers.py +0 -259
- package/templates/qoder/scripts/task.py +0 -1261
- package/templates/qoder/scripts/workspace_init.py +0 -102
- package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
- package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
- /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
package/bin/cli.js
CHANGED
|
@@ -220,6 +220,36 @@ function normalizeRole(r) {
|
|
|
220
220
|
return ROLES.some(x => x.key === low) ? low : null;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
// reset --hard 前备份本地改过的团队数据文件 (防丢真实密码/配置)。
|
|
224
|
+
// 只备份 .qoder/config.yaml|config.toml|settings.json, 且仅当本地内容与远程不同时。
|
|
225
|
+
// 返回被备份的相对路径列表。引擎文件 (gitignored) 不备份, 反正由 update 重新生成。
|
|
226
|
+
function _backupLocalTeamData(cwd, remoteBranch, _es) {
|
|
227
|
+
const DATA_FILES = [".qoder/config.yaml", ".qoder/config.toml", ".qoder/settings.json"];
|
|
228
|
+
const backed = [];
|
|
229
|
+
const backupDir = path.join(cwd, ".wlkj-pre-reset");
|
|
230
|
+
try {
|
|
231
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
232
|
+
for (const rel of DATA_FILES) {
|
|
233
|
+
const local = path.join(cwd, rel);
|
|
234
|
+
if (!fs.existsSync(local)) continue;
|
|
235
|
+
let localContent = "";
|
|
236
|
+
try { localContent = fs.readFileSync(local, "utf-8"); } catch { continue; }
|
|
237
|
+
// 拿远程版本对比
|
|
238
|
+
let remoteContent = null;
|
|
239
|
+
try {
|
|
240
|
+
remoteContent = _es(`git show origin/${remoteBranch}:${rel}`, { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
|
|
241
|
+
} catch { /* 远程没这文件 */ }
|
|
242
|
+
const same = (remoteContent !== null && remoteContent.trim() === localContent.trim());
|
|
243
|
+
if (!same) {
|
|
244
|
+
const target = path.join(backupDir, rel.replace(/[\\/]/g, "__"));
|
|
245
|
+
fs.copyFileSync(local, target);
|
|
246
|
+
backed.push(rel);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch { /* 备份失败不阻塞, 但少一层保护 */ }
|
|
250
|
+
return backed;
|
|
251
|
+
}
|
|
252
|
+
|
|
223
253
|
async function doInit(name, roleArg) {
|
|
224
254
|
const cwd = process.cwd();
|
|
225
255
|
const hasExisting = fs.existsSync(path.join(cwd, ".qoder", "scripts", "setup.py"));
|
|
@@ -242,15 +272,20 @@ async function doInit(name, roleArg) {
|
|
|
242
272
|
|
|
243
273
|
// === 1. 拷贝完整引擎 (镜像 .qoder/ 结构) ===
|
|
244
274
|
// 源: templates/qoder/* 目标: .qoder/*
|
|
245
|
-
//
|
|
246
|
-
//
|
|
275
|
+
// init 复用 update 的三路快照保护: 新装时目标不存在, 保护不触发;
|
|
276
|
+
// 重跑 init 时用户改过的引擎文件 (如自定义 skill) 不会被覆盖。
|
|
247
277
|
const qoderSrc = path.join(T, "qoder");
|
|
248
278
|
let copied = 0;
|
|
249
279
|
if (fs.existsSync(qoderSrc)) {
|
|
250
|
-
const
|
|
280
|
+
const snap = readSnapshot(cwd);
|
|
281
|
+
const newSnap = {};
|
|
282
|
+
const r = copyDirRecursive(qoderSrc, path.join(cwd, ".qoder"), "update", snap, newSnap);
|
|
251
283
|
copied = r.copied;
|
|
284
|
+
writeSnapshot(cwd, newSnap);
|
|
252
285
|
if (r.protectedN > 0) {
|
|
253
|
-
console.log(` [保护] ${r.protectedN}
|
|
286
|
+
console.log(` [保护] ${r.protectedN} 个文件保留你的改动 (新版存为 .new):`);
|
|
287
|
+
for (const f of r.protectedFiles.slice(0, 8)) console.log(` .qoder/${f}`);
|
|
288
|
+
if (r.protectedFiles.length > 8) console.log(` ...等 ${r.protectedFiles.length} 个`);
|
|
254
289
|
}
|
|
255
290
|
}
|
|
256
291
|
console.log(` 引擎: ${copied} 个文件 (${hasExisting ? "更新" : "新建"})`);
|
|
@@ -285,15 +320,21 @@ async function doInit(name, roleArg) {
|
|
|
285
320
|
const gitignorePath = path.join(cwd, ".gitignore");
|
|
286
321
|
if (!fs.existsSync(gitignorePath)) {
|
|
287
322
|
const baseGitignore = [
|
|
323
|
+
"# 引擎代码 (由 npm 包 @hupan56/wlkj init/update 生成, 不进团队 git)",
|
|
324
|
+
".qoder/scripts/", ".qoder/skills/", ".qoder/commands/", ".qoder/agents/",
|
|
325
|
+
".qoder/contracts/", ".qoder/hooks/", ".qoder/templates/", ".qoder/rules/",
|
|
326
|
+
".qoder/repowiki/", ".qoder/archive/",
|
|
327
|
+
"",
|
|
328
|
+
"# 团队数据 (保持跟踪): .qoder/config.yaml, .qoder/settings.json, .qoder/learning/, data/, workspace/",
|
|
329
|
+
"",
|
|
288
330
|
"# Source code repos (cloned by git_sync.py)", "data/code/",
|
|
289
331
|
"", "# AI pipeline runtime", ".qoder/.developer", ".qoder/.current-task",
|
|
290
|
-
".qoder/.engine-version", ".qoder/.runtime/", "",
|
|
291
|
-
"#
|
|
292
|
-
"", "# Machine-local", "data/index/.last-sync", "data/index/.prd-collected.json",
|
|
332
|
+
".qoder/.engine-version", ".qoder/.engine-snapshot.json", ".qoder/.runtime/", "",
|
|
333
|
+
"# Machine-local", "data/index/.last-sync", "data/index/.prd-collected.json",
|
|
293
334
|
"data/index/.index-meta.json", "data/index/.sync-lock", "data/index/.file-keys.json",
|
|
294
335
|
"data/index/.inverted-cache.json", "data/index/*.corrupt",
|
|
295
336
|
"", "# Identity (secret)", "workspace/members/*/.signing_key", "",
|
|
296
|
-
"# Python", "__pycache__/", "*.pyc", "*.lock", "",
|
|
337
|
+
"# Python", "__pycache__/", "*.pyc", "*.lock", "*.tmp", "",
|
|
297
338
|
].join("\n");
|
|
298
339
|
fs.writeFileSync(gitignorePath, baseGitignore, "utf-8");
|
|
299
340
|
}
|
|
@@ -354,7 +395,7 @@ async function doInit(name, roleArg) {
|
|
|
354
395
|
console.log(` [OK] mcp.json 已生成 (选装了依赖的Python, MCP 不会黄)`);
|
|
355
396
|
} catch (e) {
|
|
356
397
|
console.log(` [WARN] MCP 配置未完全成功 (不阻塞): ${(e.message || "").slice(0, 80)}`);
|
|
357
|
-
console.log(` 可手动补: ${pyCmd} .qoder/scripts/install_qoderwork.py`);
|
|
398
|
+
console.log(` 可手动补: ${pyCmd} .qoder/scripts/setup/install_qoderwork.py`);
|
|
358
399
|
}
|
|
359
400
|
}
|
|
360
401
|
} catch (e) {
|
|
@@ -373,24 +414,31 @@ async function doInit(name, roleArg) {
|
|
|
373
414
|
console.log(` 或输 / 看命令列表 (/wl-prd /wl-search /wl-task)`);
|
|
374
415
|
console.log(`\n ✓ QoderWork MCP 已自动配好 (kg/mysql/lanhu/playwright)`);
|
|
375
416
|
console.log(` 用 QoderWork 桌面端想装技能(软链到 ~/.qoderwork/skills/)?`);
|
|
376
|
-
console.log(` python .qoder/scripts/install_qoderwork.py`);
|
|
417
|
+
console.log(` python .qoder/scripts/setup/install_qoderwork.py`);
|
|
377
418
|
console.log(`\n 看不到命令? 新建对话 / 重启 QoderWork`);
|
|
378
|
-
console.log(` 环境问题? python .qoder/scripts/init_doctor.py --fix\n`);
|
|
419
|
+
console.log(` 环境问题? python .qoder/scripts/setup/init_doctor.py --fix\n`);
|
|
379
420
|
}
|
|
380
421
|
|
|
381
|
-
// 递归拷贝目录, 返回 {copied, protected, skipped}
|
|
422
|
+
// 递归拷贝目录, 返回 {copied, protected, skipped, protectedFiles}
|
|
382
423
|
// mode: "init" (新装, 全拷) | "update" (升级, 保护用户数据)
|
|
383
|
-
|
|
424
|
+
// update 模式下采用三路快照保护 (见 readSnapshot/writeSnapshot):
|
|
425
|
+
// 用户改过的引擎文件 (与上次安装快照不同) → 保留用户的, 新版存 .new
|
|
426
|
+
// 用户没动过的引擎文件 → 正常升级
|
|
427
|
+
// 非引擎文件已存在 → 跳过 (留用户数据)
|
|
428
|
+
function copyDirRecursive(src, dst, mode = "init", snap = {}, newSnap = {}, relBase = "") {
|
|
384
429
|
let copied = 0, protectedN = 0, skipped = 0;
|
|
385
|
-
|
|
430
|
+
const protectedFiles = [];
|
|
431
|
+
if (!fs.existsSync(src)) return { copied, protectedN, skipped, protectedFiles };
|
|
386
432
|
fs.mkdirSync(dst, { recursive: true });
|
|
387
433
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
388
434
|
if (entry.name === "__pycache__" || entry.name.endsWith(".pyc")) continue;
|
|
389
435
|
const s = path.join(src, entry.name);
|
|
390
436
|
const d = path.join(dst, entry.name);
|
|
437
|
+
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
391
438
|
if (entry.isDirectory()) {
|
|
392
|
-
const r = copyDirRecursive(s, d, mode);
|
|
439
|
+
const r = copyDirRecursive(s, d, mode, snap, newSnap, rel);
|
|
393
440
|
copied += r.copied; protectedN += r.protectedN; skipped += r.skipped;
|
|
441
|
+
protectedFiles.push(...r.protectedFiles);
|
|
394
442
|
} else {
|
|
395
443
|
const isEngine = entry.name.endsWith(".py") || entry.name.endsWith(".md") ||
|
|
396
444
|
entry.name.endsWith(".yaml") || entry.name.endsWith(".yml") ||
|
|
@@ -398,24 +446,47 @@ function copyDirRecursive(src, dst, mode = "init") {
|
|
|
398
446
|
entry.name.endsWith(".html") || entry.name.endsWith(".txt");
|
|
399
447
|
|
|
400
448
|
if (mode === "update") {
|
|
401
|
-
//
|
|
449
|
+
// 受保护配置 (config.yaml/settings.json): 永不覆盖, 有差异存 .new
|
|
402
450
|
if (PROTECTED_FILES.has(entry.name) && fs.existsSync(d)) {
|
|
403
|
-
// 新版内容存为 .new 让用户手动合并(仅当内容不同)
|
|
404
451
|
if (!filesEqual(s, d)) {
|
|
405
452
|
fs.copyFileSync(s, d + ".new");
|
|
406
|
-
protectedN++;
|
|
453
|
+
protectedN++; protectedFiles.push(rel);
|
|
407
454
|
} else {
|
|
408
455
|
skipped++;
|
|
409
456
|
}
|
|
457
|
+
// 快照记上游 hash: 用户当前 == 上游 → 下次安全升级; 否则持续保护
|
|
458
|
+
const sh = fileHash(s); if (sh) newSnap[rel] = sh;
|
|
410
459
|
continue;
|
|
411
460
|
}
|
|
412
|
-
// 引擎文件:
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
461
|
+
// 引擎文件: 三路快照保护
|
|
462
|
+
if (isEngine) {
|
|
463
|
+
const userHash = fs.existsSync(d) ? fileHash(d) : null;
|
|
464
|
+
const snapHash = snap[rel] || null;
|
|
465
|
+
if (fs.existsSync(d) && userHash && snapHash && userHash !== snapHash) {
|
|
466
|
+
// 用户改过这个文件! 保留用户的, 新版存 .new (不覆盖)
|
|
467
|
+
if (!filesEqual(s, d)) {
|
|
468
|
+
fs.copyFileSync(s, d + ".new");
|
|
469
|
+
protectedN++; protectedFiles.push(rel);
|
|
470
|
+
} else {
|
|
471
|
+
skipped++;
|
|
472
|
+
}
|
|
473
|
+
// 关键: 快照记上游 hash (不是用户的 hash)。
|
|
474
|
+
// 这样下次升级: 用户文件若仍 != 上游 → 继续保护; 若用户手动改成上游 → 自动恢复正常升级。
|
|
475
|
+
const sh = fileHash(s); if (sh) newSnap[rel] = sh;
|
|
476
|
+
} else {
|
|
477
|
+
// 用户没动过 (或新文件), 正常升级, 刷新快照为新版 hash
|
|
478
|
+
fs.copyFileSync(s, d);
|
|
479
|
+
copied++;
|
|
480
|
+
const sh = fileHash(s); if (sh) newSnap[rel] = sh;
|
|
481
|
+
}
|
|
417
482
|
} else {
|
|
418
|
-
|
|
483
|
+
// 非引擎文件: 已存在跳过 (保护用户数据), 不存在才建
|
|
484
|
+
if (!fs.existsSync(d)) {
|
|
485
|
+
fs.copyFileSync(s, d);
|
|
486
|
+
copied++;
|
|
487
|
+
} else {
|
|
488
|
+
skipped++;
|
|
489
|
+
}
|
|
419
490
|
}
|
|
420
491
|
} else {
|
|
421
492
|
// init 模式: 引擎文件全覆盖, 非引擎已存在跳过 (保留用户改动)
|
|
@@ -423,10 +494,12 @@ function copyDirRecursive(src, dst, mode = "init") {
|
|
|
423
494
|
fs.copyFileSync(s, d);
|
|
424
495
|
}
|
|
425
496
|
copied++;
|
|
497
|
+
// 快照记上游 hash (基线), 供将来升级对比
|
|
498
|
+
const sh = fileHash(s); if (sh) newSnap[rel] = sh;
|
|
426
499
|
}
|
|
427
500
|
}
|
|
428
501
|
}
|
|
429
|
-
return { copied, protectedN, skipped };
|
|
502
|
+
return { copied, protectedN, skipped, protectedFiles };
|
|
430
503
|
}
|
|
431
504
|
|
|
432
505
|
// 两文件内容是否相同(先比大小再比内容)
|
|
@@ -455,6 +528,53 @@ function readEngineVersion(cwd) {
|
|
|
455
528
|
return null;
|
|
456
529
|
}
|
|
457
530
|
|
|
531
|
+
// ─────────────────────────────────────────────────────────────
|
|
532
|
+
// 三路快照保护 (dpkg/brew conffiles 同款机制, 彻底解决"我原本的技能呢?")
|
|
533
|
+
//
|
|
534
|
+
// 问题: update 用 isEngine 一刀切强制刷新 .md/.py/.yaml 等,
|
|
535
|
+
// 用户改过的 stock skill (如 .qoder/skills/wl-prd-full/SKILL.md)
|
|
536
|
+
// 被无声覆盖 → "更新完我的技能没了"。
|
|
537
|
+
//
|
|
538
|
+
// 解法: 每次安装/升级后, 把所有"从 npm 包拷过来的引擎文件"的 hash 记到
|
|
539
|
+
// .qoder/.engine-snapshot.json。下次升级时, 三路对比:
|
|
540
|
+
// 用户当前文件 hash == 快照 hash → 没动过, 安全升级
|
|
541
|
+
// 用户当前文件 hash != 快照 hash → 动过! 保留用户的, 新版存 .new
|
|
542
|
+
// 这就是 apt/dpkg 对 /etc 下 conffiles 的处理, 行业标准, 不瞎猜。
|
|
543
|
+
// ─────────────────────────────────────────────────────────────
|
|
544
|
+
const SNAPSHOT_FILE = ".engine-snapshot.json";
|
|
545
|
+
|
|
546
|
+
function snapshotPath(cwd) {
|
|
547
|
+
return path.join(cwd, ".qoder", SNAPSHOT_FILE);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// 读快照: { "skills/wl-prd-full/SKILL.md": "<sha256前16位>", ... }
|
|
551
|
+
function readSnapshot(cwd) {
|
|
552
|
+
try {
|
|
553
|
+
const p = snapshotPath(cwd);
|
|
554
|
+
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
555
|
+
} catch { /* 损坏的快照当空 */ }
|
|
556
|
+
return {};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// 写快照: 传入 {相对路径: hash} 的对象
|
|
560
|
+
function writeSnapshot(cwd, snap) {
|
|
561
|
+
try {
|
|
562
|
+
const p = snapshotPath(cwd);
|
|
563
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
564
|
+
fs.writeFileSync(p, JSON.stringify(snap, null, 2), "utf-8");
|
|
565
|
+
} catch { /* 快照写失败不阻塞升级 */ }
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// 计算文件内容 hash (sha256 前 16 位, 足够检测"是否动过")
|
|
569
|
+
function fileHash(p) {
|
|
570
|
+
try {
|
|
571
|
+
const crypto = require("crypto");
|
|
572
|
+
const h = crypto.createHash("sha256");
|
|
573
|
+
h.update(fs.readFileSync(p));
|
|
574
|
+
return h.digest("hex").slice(0, 16);
|
|
575
|
+
} catch { return null; }
|
|
576
|
+
}
|
|
577
|
+
|
|
458
578
|
// 跑 install_qoderwork.py(升级时强制刷新 commands)
|
|
459
579
|
function runQoderWorkInstall(cwd, forceCommands) {
|
|
460
580
|
const script = path.join(cwd, ".qoder", "scripts", "install_qoderwork.py");
|
|
@@ -524,32 +644,30 @@ function doUpdate() {
|
|
|
524
644
|
console.log(" .qoder/.developer (你的身份)");
|
|
525
645
|
console.log(" .qoder/.current-task (当前任务)");
|
|
526
646
|
console.log(" .qoder/.engine-version (版本戳)");
|
|
647
|
+
console.log(" 你改过的引擎文件 (三路快照: 改过的留你的, 新版存 .new)");
|
|
527
648
|
console.log(" workspace/ (你的全部产出)");
|
|
528
649
|
console.log(" data/ (代码仓库+知识图谱)");
|
|
529
650
|
console.log("");
|
|
530
651
|
|
|
531
|
-
// === 1.
|
|
652
|
+
// === 1. 刷新引擎文件 (三路快照保护用户改动) ===
|
|
532
653
|
let copied = 0, protectedN = 0, skipped = 0;
|
|
654
|
+
let protectedFiles = [];
|
|
533
655
|
if (fs.existsSync(qoderSrc)) {
|
|
534
|
-
const
|
|
656
|
+
const snap = readSnapshot(cwd);
|
|
657
|
+
const newSnap = {};
|
|
658
|
+
const r = copyDirRecursive(qoderSrc, path.join(cwd, ".qoder"), "update", snap, newSnap);
|
|
535
659
|
copied = r.copied; protectedN = r.protectedN; skipped = r.skipped;
|
|
660
|
+
protectedFiles = r.protectedFiles || [];
|
|
661
|
+
writeSnapshot(cwd, newSnap); // 刷新基线 (供下次升级对比)
|
|
536
662
|
}
|
|
537
663
|
console.log(` 引擎刷新: ${copied} 个文件${skipped ? ` / ${skipped} 无变化` : ""}`);
|
|
538
664
|
|
|
539
|
-
// 报告受保护文件
|
|
665
|
+
// 报告受保护文件 (用户的改动 + 配置文件)
|
|
540
666
|
if (protectedN > 0) {
|
|
541
|
-
console.log(` [保护] ${protectedN}
|
|
542
|
-
console.log(`
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
for (const f of fs.readdirSync(path.join(cwd, ".qoder"))) {
|
|
546
|
-
if (f.endsWith(".new")) {
|
|
547
|
-
console.log(` .qoder/${f}`);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
} catch { /* */ }
|
|
551
|
-
} else {
|
|
552
|
-
console.log(` config.yaml / settings.json: 已保护(未改动)`);
|
|
667
|
+
console.log(` [保护] ${protectedN} 个文件已保留你的改动 (新版存为 .new):`);
|
|
668
|
+
for (const f of protectedFiles.slice(0, 10)) console.log(` .qoder/${f}`);
|
|
669
|
+
if (protectedFiles.length > 10) console.log(` ...等 ${protectedFiles.length} 个`);
|
|
670
|
+
console.log(` 确认无需合并后可删除 .new; 或手动对比合并后删除。`);
|
|
553
671
|
}
|
|
554
672
|
|
|
555
673
|
// === 2. 根文件(文档 + requirements.txt, 始终覆盖保证最新)===
|
|
@@ -561,12 +679,45 @@ function doUpdate() {
|
|
|
561
679
|
});
|
|
562
680
|
console.log(` 根文档 + requirements.txt: 已更新`);
|
|
563
681
|
|
|
564
|
-
// === 2.5. git pull 拉最新团队数据 (
|
|
682
|
+
// === 2.5. git pull 拉最新团队数据 (Trellis: 引擎已由上面 copyDirRecursive 生成, git 只存数据) ===
|
|
683
|
+
// 团队数据 = config.yaml / settings.json / learning/ + data/ (kg.duckdb/kg_db.duckdb/索引) + workspace/ (PRD/任务)。
|
|
684
|
+
// 引擎代码 (scripts/skills/...) 已被 gitignore, 不在 git 里, 不会与 pull 冲突。
|
|
565
685
|
// git pull 失败会导致 kg.duckdb 拿不到 → kg MCP 无数据(但仍能连)。
|
|
566
|
-
// 失败原因常见: 未配 git 身份 /
|
|
686
|
+
// 失败原因常见: 未配 git 身份 / 远程仓库要认证 / 网络问题 / 分支没tracking / unrelated histories。
|
|
567
687
|
// 这里给出清晰诊断, 不让"git pull 失败"变成无声的静默跳过。
|
|
688
|
+
// 前置: 自愈分支 tracking (老版 init 配了 remote 但没 set-upstream, pull 会报
|
|
689
|
+
// "no tracking information"。这里检测到就补上, 老用户 update 也能自动修)
|
|
690
|
+
const { execSync: _es } = require("child_process"); // 提到 try 外, catch 块的自愈逻辑也要用
|
|
568
691
|
try {
|
|
569
|
-
|
|
692
|
+
// 先看当前分支有没有 upstream
|
|
693
|
+
let _trackOk = false;
|
|
694
|
+
try {
|
|
695
|
+
_es("git rev-parse --abbrev-ref --symbolic-full-name @{u}", { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
|
|
696
|
+
_trackOk = true; // 有 upstream, 正常
|
|
697
|
+
} catch {
|
|
698
|
+
// 没 tracking — 尝试补设 (有 origin remote 的前提下)
|
|
699
|
+
try {
|
|
700
|
+
const _remote = _es("git remote", { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" }).trim();
|
|
701
|
+
if (_remote.split(/\s+/).includes("origin")) {
|
|
702
|
+
_es("git fetch origin", { cwd, encoding: "utf-8", timeout: 30000, stdio: "pipe" });
|
|
703
|
+
// 确定本地分支名
|
|
704
|
+
const _branch = _es("git rev-parse --abbrev-ref HEAD", { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" }).trim();
|
|
705
|
+
// 远程用 master 还是 main
|
|
706
|
+
let _remoteBranch = null;
|
|
707
|
+
for (const _c of ["master", "main"]) {
|
|
708
|
+
try {
|
|
709
|
+
_es(`git rev-parse --verify remotes/origin/${_c}`, { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
|
|
710
|
+
_remoteBranch = _c; break;
|
|
711
|
+
} catch { /* 该分支不存在, 试下一个 */ }
|
|
712
|
+
}
|
|
713
|
+
if (_remoteBranch) {
|
|
714
|
+
_es(`git branch --set-upstream-to=origin/${_remoteBranch} ${_branch}`, { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
|
|
715
|
+
console.log(` git: 已自动设置上游 tracking (origin/${_remoteBranch})`);
|
|
716
|
+
_trackOk = true;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
} catch { /* 补 tracking 失败, 走下面的 pull 诊断 */ }
|
|
720
|
+
}
|
|
570
721
|
const pullR = _es("git pull --no-rebase 2>&1", { cwd, encoding: "utf-8", timeout: 30000 });
|
|
571
722
|
if (pullR.includes("Already up to date") || pullR.includes("已经是最新")) {
|
|
572
723
|
console.log(` git: 已是最新`);
|
|
@@ -594,10 +745,43 @@ function doUpdate() {
|
|
|
594
745
|
console.log(` 修复: npx @hupan56/wlkj init <你的名字> (会配团队远程仓库)`);
|
|
595
746
|
} else if (combined.includes("timed out") || combined.includes("timeout") ||
|
|
596
747
|
combined.includes("could not resolve host") || combined.includes("connection refused") ||
|
|
597
|
-
combined.includes("network is unreachable"))
|
|
748
|
+
combined.includes("network is unreachable") || combined.includes("unable to access") ||
|
|
749
|
+
combined.includes("ssl") || combined.includes("failed to connect")) {
|
|
598
750
|
console.log(` git: ⚠️ 网络问题 — 连不上远程仓库`);
|
|
599
751
|
console.log(` 原因: ${stdout.trim().slice(0, 80) || "网络超时/无法解析主机"}`);
|
|
600
752
|
console.log(` (kg.duckdb 拉不到, MCP 能连但搜索无结果)`);
|
|
753
|
+
} else if (combined.includes("unrelated histories") || combined.includes("refusing to merge")) {
|
|
754
|
+
// 自愈: 本地是空的 git init 历史, 与团队远程无公共祖先。
|
|
755
|
+
// 引擎代码现在由 npm 生成 (不在 git 里), 团队数据在远程 → 采用远程历史是正确的。
|
|
756
|
+
// 但 reset --hard 会覆盖工作树 → 先把本地改过的团队数据文件备份, 防丢密码/配置。
|
|
757
|
+
console.log(` git: ⚠️ 无关历史 (本地 git init 产生的平行历史与团队远程无公共祖先)`);
|
|
758
|
+
try {
|
|
759
|
+
// 探测远程分支名
|
|
760
|
+
let _rb = null;
|
|
761
|
+
for (const _c of ["master", "main"]) {
|
|
762
|
+
try {
|
|
763
|
+
_es(`git rev-parse --verify remotes/origin/${_c}`, { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
|
|
764
|
+
_rb = _c; break;
|
|
765
|
+
} catch { /* */ }
|
|
766
|
+
}
|
|
767
|
+
if (_rb) {
|
|
768
|
+
// 备份本地改过的团队数据 (config 等可能含未提交的真实密码)
|
|
769
|
+
const _backed = _backupLocalTeamData(cwd, _rb, _es);
|
|
770
|
+
_es(`git reset --hard origin/${_rb}`, { cwd, encoding: "utf-8", timeout: 30000, stdio: "pipe" });
|
|
771
|
+
console.log(` git: ✓ 已采用远程历史 (origin/${_rb}), 团队数据已落地`);
|
|
772
|
+
console.log(` (引擎随后由本次 update 重新生成)`);
|
|
773
|
+
if (_backed.length > 0) {
|
|
774
|
+
console.log(` ⚠️ reset 前已备份你本地改过的数据文件 (可能含未提交的真实密码/配置):`);
|
|
775
|
+
for (const rel of _backed) console.log(` → .wlkj-pre-reset/${rel.replace(/[\\/]/g, "__")}`);
|
|
776
|
+
console.log(` 检查这些文件, 把需要的配置/密码重新填回 .qoder/config.yaml 后删备份目录。`);
|
|
777
|
+
}
|
|
778
|
+
} else {
|
|
779
|
+
console.log(` git: 远程无 master/main 分支, 无法自愈。请确认团队仓库地址正确。`);
|
|
780
|
+
}
|
|
781
|
+
} catch (re) {
|
|
782
|
+
console.log(` git: 自愈失败 (${String(re.message || "").slice(0, 80)})`);
|
|
783
|
+
console.log(` 可手动: git fetch origin && git reset --hard origin/master`);
|
|
784
|
+
}
|
|
601
785
|
} else {
|
|
602
786
|
// 兜底: 显示 git 的真实报错, 不再笼统说 "Command failed"
|
|
603
787
|
const realErr = stdout.trim() || errMsg;
|
|
@@ -621,7 +805,10 @@ function doUpdate() {
|
|
|
621
805
|
}
|
|
622
806
|
|
|
623
807
|
// === 4. 刷新 QoderWork commands(强制覆盖已存在的)===
|
|
624
|
-
|
|
808
|
+
// WLKJ_SKIP_QW_INSTALL=1 时跳过 (测试隔离用, 防止测试目录污染 ~/.qoderwork/.repo-root)
|
|
809
|
+
if (process.env.WLKJ_SKIP_QW_INSTALL !== "1") {
|
|
810
|
+
runQoderWorkInstall(cwd, true);
|
|
811
|
+
}
|
|
625
812
|
|
|
626
813
|
// === 5. 写版本戳 ===
|
|
627
814
|
writeEngineVersion(cwd);
|
|
@@ -683,16 +870,6 @@ function doUpdate() {
|
|
|
683
870
|
} catch (e) { /* 删不掉跳过 */ }
|
|
684
871
|
}
|
|
685
872
|
}
|
|
686
|
-
// 清理 .new 临时文件 (上次升级生成的配置对比文件)
|
|
687
|
-
try {
|
|
688
|
-
for (const f of fs.readdirSync(path.join(cwd, ".qoder"))) {
|
|
689
|
-
if (f.endsWith(".new")) {
|
|
690
|
-
fs.unlinkSync(path.join(cwd, ".qoder", f));
|
|
691
|
-
cleaned++;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
} catch { /* */ }
|
|
695
|
-
|
|
696
873
|
// 清理用户级旧命令 (已合并的, ~/.qoderwork/commands/)
|
|
697
874
|
const home = process.env.USERPROFILE || require("os").homedir();
|
|
698
875
|
const userCmdDir = path.join(home, ".qoderwork", "commands");
|
|
@@ -707,16 +884,8 @@ function doUpdate() {
|
|
|
707
884
|
catch { /* */ }
|
|
708
885
|
}
|
|
709
886
|
}
|
|
710
|
-
//
|
|
711
|
-
|
|
712
|
-
for (const f of fs.readdirSync(path.join(cwd, ".qoder"))) {
|
|
713
|
-
if (f.endsWith(".new")) {
|
|
714
|
-
fs.unlinkSync(path.join(cwd, ".qoder", f));
|
|
715
|
-
console.log(` [清理] 删除临时文件: .qoder/${f}`);
|
|
716
|
-
cleaned++;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
} catch { /* */ }
|
|
887
|
+
// 注意: .new 文件不再自动删除! 它们是三路快照保护的"新版待对比"信号,
|
|
888
|
+
// 用户需要看到 .new 才知道"我的改动被保留了, 这里是新版"。用户手动合并后删。
|
|
720
889
|
// 清理缓存 (索引重建后失效的旧缓存)
|
|
721
890
|
const runtimeDir = path.join(cwd, ".qoder", ".runtime");
|
|
722
891
|
let cacheCleaned = 0;
|
|
@@ -732,8 +901,25 @@ function doUpdate() {
|
|
|
732
901
|
console.log(` [清理] 废弃文件 ${cleaned} 个 + 过期缓存 ${cacheCleaned} 个`);
|
|
733
902
|
}
|
|
734
903
|
|
|
904
|
+
// === 5c. 清理 .qoder/.qoder 嵌套残留 (致命 bug 源) ===
|
|
905
|
+
// 旧版 paths.py 的 _find_repo_root 会误把 .qoder 当 PROJECT_ROOT, 然后往
|
|
906
|
+
// "PROJECT_ROOT/.qoder/.runtime/" 写缓存 → 误建 .qoder/.qoder/ 目录 →
|
|
907
|
+
// 这个残留反过来又让 _find_repo_root 再次误判 → 死循环。
|
|
908
|
+
// 表现: MCP cookie 路径全错 (workspace 跑到 .qoder/workspace), lanhu 永远 cookie 未配置。
|
|
909
|
+
// v3.0 修了 paths.py 不再误建, 但老用户机器上的残留要清掉, 否则 bug 还在。
|
|
910
|
+
const nestedQoder = path.join(cwd, ".qoder", ".qoder");
|
|
911
|
+
if (fs.existsSync(nestedQoder)) {
|
|
912
|
+
try {
|
|
913
|
+
fs.rmSync(nestedQoder, { recursive: true, force: true });
|
|
914
|
+
console.log(` [清理] 删除嵌套残留 .qoder/.qoder/ (曾导致 MCP cookie 路径错位)`);
|
|
915
|
+
cleaned++;
|
|
916
|
+
} catch { /* 删不掉不阻塞 (可能被占用), 下次再清 */ }
|
|
917
|
+
}
|
|
918
|
+
|
|
735
919
|
// === 6. 刷新 launcher + mcp.json (老用户的可能是旧版/写死路径) ===
|
|
736
920
|
// 注意: pip 依赖已在第3步装好, 此处 install_qoderwork 会选到"已装依赖的Python"写进 mcp.json
|
|
921
|
+
// WLKJ_SKIP_QW_INSTALL=1 时跳过 (测试隔离, 防止污染全局 ~/.qoderwork/)
|
|
922
|
+
if (process.env.WLKJ_SKIP_QW_INSTALL !== "1") {
|
|
737
923
|
try {
|
|
738
924
|
const home = process.env.USERPROFILE || require("os").homedir();
|
|
739
925
|
const launcherSrc = path.join(cwd, ".qoder", "scripts", "mcp_launcher.py");
|
|
@@ -752,6 +938,7 @@ function doUpdate() {
|
|
|
752
938
|
} catch (e) { /* --mcp-only 可能不支持, 静默跳过 */ }
|
|
753
939
|
}
|
|
754
940
|
} catch (e) { /* 刷新失败不阻塞升级 */ }
|
|
941
|
+
} // end WLKJ_SKIP_QW_INSTALL guard
|
|
755
942
|
|
|
756
943
|
// === 6.5. MCP 启动实测 (发 initialize 看哪个起不来) ===
|
|
757
944
|
// 这是"根治黄色"的关键一步: 配完 mcp.json 后立即模拟 QoderWork 发 initialize,
|
|
@@ -793,7 +980,9 @@ function doUpdate() {
|
|
|
793
980
|
} else {
|
|
794
981
|
console.log(` [✗] ${label} 缺失! ${desc}`);
|
|
795
982
|
if (label.includes("duckdb")) {
|
|
796
|
-
|
|
983
|
+
// duckdb 是团队共享数据, 在 git 里 → 非管理员靠 git pull 拿; 管理员靠本地构建。
|
|
984
|
+
console.log(` → 修复: 先 npx @hupan56/wlkj doctor (拉团队数据); `);
|
|
985
|
+
console.log(` 管理员可 npx @hupan56/wlkj run kg_build.py --rebuild`);
|
|
797
986
|
} else if (label.includes("commands")) {
|
|
798
987
|
console.log(` → 修复: npx @hupan56/wlkj update`);
|
|
799
988
|
}
|
|
@@ -846,28 +1035,134 @@ function doUpdate() {
|
|
|
846
1035
|
console.log("");
|
|
847
1036
|
}
|
|
848
1037
|
|
|
1038
|
+
// ─────────────────────────────────────────────────────────────
|
|
1039
|
+
// 一次性迁移: 把引擎代码从团队 git 跟踪里摘出来 (Trellis 化)
|
|
1040
|
+
// 团队管理员只跑一次, 其余成员 wlkj update 后自动同步。
|
|
1041
|
+
// 原理: git rm -r --cached (只删 git 索引, 不删磁盘文件) + commit。
|
|
1042
|
+
// 之后引擎文件仍在磁盘 (由 wlkj update 重新生成), 但不再被 git 跟踪 →
|
|
1043
|
+
// 彻底消除"引擎更新污染团队 git"和"新机 init 产生 unrelated histories"。
|
|
1044
|
+
// 团队数据 (.qoder/config.yaml, settings.json, learning/, data/, workspace/) 保持不动。
|
|
1045
|
+
// ─────────────────────────────────────────────────────────────
|
|
1046
|
+
function doMigrate() {
|
|
1047
|
+
const { execSync } = require("child_process");
|
|
1048
|
+
const cwd = process.cwd();
|
|
1049
|
+
const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
|
|
1050
|
+
function _es(cmd, opts = {}) {
|
|
1051
|
+
return execSync(cmd, { cwd, encoding: "utf-8", timeout: 30000, stdio: "pipe", env, ...opts });
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
console.log("");
|
|
1055
|
+
console.log("=== wlkj migrate: 引擎代码改为 npm 生成 (Trellis 化) ===");
|
|
1056
|
+
console.log("");
|
|
1057
|
+
|
|
1058
|
+
// 1. 必须在 git 仓库里
|
|
1059
|
+
if (!fs.existsSync(path.join(cwd, ".git"))) {
|
|
1060
|
+
console.log(" ✗ 当前目录不是 git 仓库。请在团队仓库根目录运行。");
|
|
1061
|
+
console.log("");
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// 2. 要从 git 跟踪摘除的引擎目录 (团队数据 config/learning 等不在此列)
|
|
1066
|
+
const ENGINE_DIRS = [
|
|
1067
|
+
".qoder/scripts", ".qoder/skills", ".qoder/commands", ".qoder/agents",
|
|
1068
|
+
".qoder/contracts", ".qoder/hooks", ".qoder/templates", ".qoder/rules",
|
|
1069
|
+
];
|
|
1070
|
+
|
|
1071
|
+
// 3. 哪些目录当前确被跟踪 (只摘这些, 避免在已迁移过的仓库上空跑)
|
|
1072
|
+
const tracked = [];
|
|
1073
|
+
for (const d of ENGINE_DIRS) {
|
|
1074
|
+
let isTracked = false;
|
|
1075
|
+
try {
|
|
1076
|
+
// git ls-files 能列出被跟踪的文件; 有输出说明该目录还有文件在跟踪
|
|
1077
|
+
const out = _es(`git ls-files "${d}"`, { timeout: 10000 });
|
|
1078
|
+
if (out.trim()) isTracked = true;
|
|
1079
|
+
} catch { /* 命令失败当未跟踪 */ }
|
|
1080
|
+
if (isTracked) tracked.push(d);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (tracked.length === 0) {
|
|
1084
|
+
console.log(" ✓ 引擎目录已不在 git 跟踪中, 无需迁移。");
|
|
1085
|
+
console.log(" (这通常是已经迁移过了, 或是个全新仓库)");
|
|
1086
|
+
console.log("");
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
console.log(` 将从 git 跟踪中移除 (磁盘文件保留, 不删):`);
|
|
1091
|
+
for (const d of tracked) console.log(` - ${d}`);
|
|
1092
|
+
console.log("");
|
|
1093
|
+
|
|
1094
|
+
// 4. 先确保 .gitignore 已含引擎忽略规则 (防止 rm --cached 后又被加回)
|
|
1095
|
+
// 逐条补缺, 不重复不乱插。
|
|
1096
|
+
const giPath = path.join(cwd, ".gitignore");
|
|
1097
|
+
const RULES_TO_ENSURE = [
|
|
1098
|
+
"# 引擎代码 (由 npm 包 @hupan56/wlkj init/update 生成, 不进团队 git)",
|
|
1099
|
+
".qoder/scripts/", ".qoder/skills/", ".qoder/commands/", ".qoder/agents/",
|
|
1100
|
+
".qoder/contracts/", ".qoder/hooks/", ".qoder/templates/", ".qoder/rules/",
|
|
1101
|
+
".qoder/repowiki/", ".qoder/archive/",
|
|
1102
|
+
".wlkj-pre-reset/",
|
|
1103
|
+
];
|
|
1104
|
+
let gi = fs.existsSync(giPath) ? fs.readFileSync(giPath, "utf-8") : "";
|
|
1105
|
+
const existingLines = new Set(gi.split(/\r?\n/));
|
|
1106
|
+
const missing = RULES_TO_ENSURE.filter(rule => !existingLines.has(rule));
|
|
1107
|
+
if (missing.length > 0) {
|
|
1108
|
+
// 缺哪条补哪条 (注释行单独补, 规则行单独补), 补在最前面
|
|
1109
|
+
gi = missing.join("\n") + "\n" + (gi ? "\n" + gi : "");
|
|
1110
|
+
fs.writeFileSync(giPath, gi, "utf-8");
|
|
1111
|
+
console.log(` ✓ .gitignore 已补 ${missing.length} 条忽略规则`);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// 5. git rm -r --cached (只删索引, 不删磁盘文件)
|
|
1115
|
+
console.log(" 从 git 索引移除 (git rm -r --cached, 磁盘文件不动)...");
|
|
1116
|
+
try {
|
|
1117
|
+
_es(`git rm -r --cached ${tracked.map(t => `"${t}"`).join(" ")}`, { timeout: 60000 });
|
|
1118
|
+
console.log(" ✓ 已移除");
|
|
1119
|
+
} catch (e) {
|
|
1120
|
+
console.log(` ✗ git rm 失败: ${(e.stderr || e.message || "").slice(0, 200)}`);
|
|
1121
|
+
console.log(" 可手动跑: " + tracked.map(t => `git rm -r --cached ${t}`).join(" && "));
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// 6. 自动 commit (标记这次架构变更)
|
|
1126
|
+
console.log(" 提交迁移...");
|
|
1127
|
+
try {
|
|
1128
|
+
_es(`git add -A .gitignore`, { timeout: 10000 });
|
|
1129
|
+
_es(`git commit -m "chore(wlkj): 引擎代码改为 npm 生成, 不再进团队 git (Trellis 化)"`, { timeout: 30000 });
|
|
1130
|
+
console.log(" ✓ 已提交");
|
|
1131
|
+
} catch (e) {
|
|
1132
|
+
console.log(` ⚠ 自动提交失败 (可手动 commit): ${(e.stderr || e.message || "").slice(0, 150)}`);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
console.log("");
|
|
1136
|
+
console.log("=== 迁移完成 ===");
|
|
1137
|
+
console.log(" 接下来:");
|
|
1138
|
+
console.log(" 1. npx @hupan56/wlkj sync # 推到团队远程");
|
|
1139
|
+
console.log(" 2. 团队其他成员各自跑: npx @hupan56/wlkj update");
|
|
1140
|
+
console.log(" (本地引擎文件不会被删, 之后由 update 重新生成, 互不冲突)");
|
|
1141
|
+
console.log("");
|
|
1142
|
+
}
|
|
1143
|
+
|
|
849
1144
|
function doHelp() {
|
|
850
1145
|
console.log("");
|
|
851
1146
|
console.log(`wlkj - AI 产品研发工作流 (v${PKG_VERSION})`);
|
|
852
1147
|
console.log("");
|
|
853
|
-
console.log("===
|
|
854
|
-
console.log("");
|
|
855
|
-
console.log(" npx @hupan56/wlkj install-env ① 装环境 (Node/Python/git/pip依赖)");
|
|
856
|
-
console.log(" npx @hupan56/wlkj init [名字] ② 装引擎 + 初始化身份");
|
|
857
|
-
console.log(" npx @hupan56/wlkj update ③ 升级 (刷新引擎+补依赖+刷MCP)");
|
|
858
|
-
console.log(" npx @hupan56/wlkj doctor ④ 体检 (环境/依赖/图谱健康度)");
|
|
859
|
-
console.log(" npx @hupan56/wlkj sync ⑤ 推送产出 (PRD/任务/原型)");
|
|
1148
|
+
console.log("=== 终端只用 3 个 (从下载到日常) ===");
|
|
860
1149
|
console.log("");
|
|
861
|
-
console.log("
|
|
862
|
-
console.log(" npx @hupan56/wlkj
|
|
863
|
-
console.log("
|
|
864
|
-
console.log(" npx wlkj run eval_prd.py xxx.md");
|
|
1150
|
+
console.log(" npx @hupan56/wlkj install-env 首次: 装环境 (Node/Python/git/pip)");
|
|
1151
|
+
console.log(" npx @hupan56/wlkj init [名字] 首次: 装引擎 + 初始化身份");
|
|
1152
|
+
console.log(" npx @hupan56/wlkj update 日常: 升级 (刷新引擎+补依赖+刷MCP)");
|
|
865
1153
|
console.log("");
|
|
866
|
-
console.log("=== Qoder
|
|
1154
|
+
console.log("=== 日常工作全在 Qoder 里 (不用回终端) ===");
|
|
867
1155
|
console.log(" 说中文: '写个XX的需求' '查XX代码' '建任务' '测一下XX'");
|
|
868
1156
|
console.log(" 或斜杠: /wl-prd /wl-design /wl-task /wl-code /wl-test /wl-commit");
|
|
1157
|
+
console.log(" (提交/同步 /wl-task 和 /wl-commit 会自动做, 不用单独 npx sync)");
|
|
1158
|
+
console.log("");
|
|
1159
|
+
console.log("=== 进阶 (管理员/特殊情况才用, 不常用) ===");
|
|
1160
|
+
console.log(" npx @hupan56/wlkj doctor 体检 (或在 Qoder 里 /wl-init 无参数)");
|
|
1161
|
+
console.log(" npx @hupan56/wlkj migrate 老团队一次性升级到 Trellis 架构");
|
|
1162
|
+
console.log(" npx @hupan56/wlkj run <脚本> 万能跑脚本 (AI 内部用, 用户很少需要)");
|
|
869
1163
|
console.log("");
|
|
870
1164
|
console.log(" 看不到命令? 新建对话 / 重启 QoderWork");
|
|
1165
|
+
console.log(" 环境问题? python .qoder/scripts/setup/init_doctor.py --fix");
|
|
871
1166
|
console.log("");
|
|
872
1167
|
}
|
|
873
1168
|
|
|
@@ -1034,6 +1329,7 @@ const mapped = rest.map(a => a === "-p" ? "--priority" : a);
|
|
|
1034
1329
|
switch (cmd) {
|
|
1035
1330
|
case "init": doInit(rest[0], rest[1]); break;
|
|
1036
1331
|
case "update": case "upgrade": doUpdate(); break;
|
|
1332
|
+
case "migrate": doMigrate(); break;
|
|
1037
1333
|
case "install-env": {
|
|
1038
1334
|
const checkOnly = rest.includes("--check");
|
|
1039
1335
|
// 提取 --location <dir> 或 --location=<dir>
|
|
@@ -1065,6 +1361,7 @@ switch (cmd) {
|
|
|
1065
1361
|
// npx wlkj run eval_prd.py xxx.md
|
|
1066
1362
|
// npx wlkj run autotest.py quick
|
|
1067
1363
|
case "run": {
|
|
1364
|
+
// 万能跑脚本 (AI 内部用, 用户日常不直接用 — 用 /命令 或 init/update)
|
|
1068
1365
|
if (!rest[0]) { console.log("用法: npx wlkj run <脚本名> [参数...]\n例: npx wlkj run kg_build.py --rebuild"); break; }
|
|
1069
1366
|
runScript(rest[0], rest.slice(1));
|
|
1070
1367
|
break;
|