@hupan56/wlkj 2.7.12 → 3.1.2

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