@hupan56/wlkj 3.0.0 → 3.1.3

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 (247) hide show
  1. package/bin/cli.js +1435 -1373
  2. package/package.json +1 -1
  3. package/templates/qoder/commands/optional/wl-insight.md +6 -4
  4. package/templates/qoder/commands/optional/wl-report.md +20 -39
  5. package/templates/qoder/commands/optional/wl-status.md +4 -4
  6. package/templates/qoder/commands/wl-code.md +2 -2
  7. package/templates/qoder/commands/wl-design.md +5 -5
  8. package/templates/qoder/commands/wl-init.md +3 -3
  9. package/templates/qoder/commands/wl-prd.md +26 -13
  10. package/templates/qoder/commands/wl-req.md +3 -3
  11. package/templates/qoder/commands/wl-search.md +78 -47
  12. package/templates/qoder/commands/wl-task.md +343 -40
  13. package/templates/qoder/commands/wl-test.md +10 -10
  14. package/templates/qoder/config.yaml +35 -13
  15. package/templates/qoder/hooks/post-tool-use.py +181 -0
  16. package/templates/qoder/hooks/session-start.py +82 -1
  17. package/templates/qoder/hooks/stop-eval.py +207 -0
  18. package/templates/qoder/hooks/user-prompt-submit.py +139 -0
  19. package/templates/qoder/rules/wl-pipeline.md +70 -8
  20. package/templates/qoder/scripts/README.md +78 -115
  21. package/templates/qoder/scripts/capability/__init__.py +26 -0
  22. package/templates/qoder/scripts/capability/__main__.py +72 -0
  23. package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
  24. package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
  25. package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
  26. package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
  27. package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
  28. package/templates/qoder/scripts/capability/caps/context.py +36 -0
  29. package/templates/qoder/scripts/capability/caps/cron.py +50 -0
  30. package/templates/qoder/scripts/capability/caps/identity.py +43 -0
  31. package/templates/qoder/scripts/capability/caps/memory.py +71 -0
  32. package/templates/qoder/scripts/capability/caps/notify.py +41 -0
  33. package/templates/qoder/scripts/capability/caps/present.py +48 -0
  34. package/templates/qoder/scripts/capability/caps/repo.py +31 -0
  35. package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
  36. package/templates/qoder/scripts/capability/chain.py +92 -0
  37. package/templates/qoder/scripts/capability/memory_chain.py +41 -0
  38. package/templates/qoder/scripts/capability/registry.py +246 -0
  39. package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
  40. package/templates/qoder/scripts/capability/smoke_test.py +211 -0
  41. package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
  42. package/templates/qoder/scripts/{report → deployment/setup}/__init__.py +11 -11
  43. package/templates/qoder/scripts/{setup → deployment/setup}/carriers.py +33 -26
  44. package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
  45. package/templates/qoder/scripts/{setup → deployment/setup}/init_doctor.py +55 -23
  46. package/templates/qoder/scripts/{setup → deployment/setup}/install_qoderwork.py +20 -12
  47. package/templates/qoder/scripts/{setup → deployment/setup}/platform_doctor.py +8 -2
  48. package/templates/qoder/scripts/{setup → deployment/setup}/repo_root.py +113 -112
  49. package/templates/qoder/scripts/{setup → deployment/setup}/setup.py +35 -12
  50. package/templates/qoder/scripts/{setup → deployment/setup}/setup_lanhu.py +13 -7
  51. package/templates/qoder/scripts/domain/__init__.py +0 -0
  52. package/templates/qoder/scripts/domain/design/__init__.py +0 -0
  53. package/templates/qoder/scripts/{report → domain/design}/fill_prototype.py +12 -6
  54. package/templates/qoder/scripts/{report → domain/design}/gen_design_doc.py +8 -2
  55. package/templates/qoder/scripts/{setup → domain/kg}/__init__.py +11 -11
  56. package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
  57. package/templates/qoder/scripts/{kg → domain/kg/build}/build_entity_registry.py +7 -2
  58. package/templates/qoder/scripts/{kg → domain/kg/build}/build_relations.py +7 -2
  59. package/templates/qoder/scripts/{kg → domain/kg/build}/build_style_index.py +13 -7
  60. package/templates/qoder/scripts/{kg → domain/kg/build}/build_workflows.py +7 -2
  61. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build.py +76 -9
  62. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build_db.py +7 -2
  63. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_incremental.py +11 -6
  64. package/templates/qoder/scripts/{report → domain/kg/build}/learn_aggregate.py +9 -4
  65. package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
  66. package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
  67. package/templates/qoder/scripts/{common → domain/kg/extract}/test_extract.py +115 -115
  68. package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
  69. package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
  70. package/templates/qoder/scripts/{kg → domain/kg/graph}/kg_link_db.py +7 -2
  71. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
  72. package/templates/qoder/scripts/{kg → domain/kg}/kg.py +263 -30
  73. package/templates/qoder/scripts/{common → domain/kg}/kg_capabilities.py +182 -182
  74. package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
  75. package/templates/qoder/scripts/{kg → domain/kg/search}/context_pack.py +30 -9
  76. package/templates/qoder/scripts/{kg → domain/kg/search}/enrich_prompt.py +10 -4
  77. package/templates/qoder/scripts/{kg → domain/kg/search}/prefetch.py +33 -8
  78. package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
  79. package/templates/qoder/scripts/{kg → domain/kg/search}/search_index.py +143 -77
  80. package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
  81. package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
  82. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  83. package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
  84. package/templates/qoder/scripts/{kg → domain/kg/storage}/kg_duckdb.py +38 -14
  85. package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
  86. package/templates/qoder/scripts/{report → domain/learning}/learn.py +8 -3
  87. package/templates/qoder/scripts/{mcp → domain/report}/__init__.py +11 -11
  88. package/templates/qoder/scripts/{report → domain/report}/add_session.py +9 -3
  89. package/templates/qoder/scripts/{report → domain/report}/export.py +10 -5
  90. package/templates/qoder/scripts/{report → domain/report}/report.py +9 -4
  91. package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
  92. package/templates/qoder/scripts/{report → domain/report}/role.py +10 -4
  93. package/templates/qoder/scripts/{report → domain/report}/status.py +112 -9
  94. package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
  95. package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
  96. package/templates/qoder/scripts/{report → domain/requirement}/archive_prd.py +8 -2
  97. package/templates/qoder/scripts/{report → domain/requirement}/req.py +228 -222
  98. package/templates/qoder/scripts/{kg → domain/task}/__init__.py +11 -11
  99. package/templates/qoder/scripts/domain/task/__pycache__/syncgate.cpython-39.pyc +0 -0
  100. package/templates/qoder/scripts/{task → domain/task}/git_sync.py +40 -21
  101. package/templates/qoder/scripts/{task → domain/task}/syncgate.py +13 -8
  102. package/templates/qoder/scripts/{task → domain/task}/task.py +13 -5
  103. package/templates/qoder/scripts/{task → domain/task}/task_lifecycle.py +606 -596
  104. package/templates/qoder/scripts/{task → domain/task}/task_query.py +162 -161
  105. package/templates/qoder/scripts/{task → domain/task}/task_relations.py +425 -424
  106. package/templates/qoder/scripts/{task → domain/task}/team_sync.py +9 -4
  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/__pycache__/__init__.cpython-39.pyc +0 -0
  110. package/templates/qoder/scripts/foundation/__pycache__/bootstrap.cpython-39.pyc +0 -0
  111. package/templates/qoder/scripts/{common → foundation}/bootstrap.py +10 -10
  112. package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
  113. package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
  114. package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
  115. package/templates/qoder/scripts/{common → foundation/core}/cmd_registry.py +1 -1
  116. package/templates/qoder/scripts/{common → foundation/core}/config.py +1 -1
  117. package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -586
  118. package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
  119. package/templates/qoder/scripts/{common → foundation/data}/contract.py +2 -2
  120. package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
  121. package/templates/qoder/scripts/{common → foundation/data}/result.py +1 -1
  122. package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -404
  123. package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
  124. package/templates/qoder/scripts/foundation/identity/__pycache__/__init__.cpython-39.pyc +0 -0
  125. package/templates/qoder/scripts/foundation/identity/__pycache__/identity.cpython-39.pyc +0 -0
  126. package/templates/qoder/scripts/{common → foundation/identity}/check_publish.py +103 -98
  127. package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -239
  128. package/templates/qoder/scripts/{common → foundation/identity}/guard.py +2 -2
  129. package/templates/qoder/scripts/{common → foundation/identity}/identity.py +14 -10
  130. package/templates/qoder/scripts/{common → foundation/identity}/roles.py +1 -1
  131. package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
  132. package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
  133. package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +2 -2
  134. package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
  135. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
  136. package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
  137. package/templates/qoder/scripts/foundation/io/__pycache__/__init__.cpython-39.pyc +0 -0
  138. package/templates/qoder/scripts/foundation/io/__pycache__/atomicio.cpython-39.pyc +0 -0
  139. package/templates/qoder/scripts/foundation/io/__pycache__/filelock.cpython-39.pyc +0 -0
  140. package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
  141. package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
  142. package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
  143. package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
  144. package/templates/qoder/scripts/{common → foundation/protocol}/mcp_base.py +1 -1
  145. package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
  146. package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
  147. package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
  148. package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
  149. package/templates/qoder/scripts/orchestration/__init__.py +0 -0
  150. package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
  151. package/templates/qoder/scripts/protocol/__init__.py +0 -0
  152. package/templates/qoder/scripts/protocol/browser/README.md +207 -0
  153. package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
  154. package/templates/qoder/scripts/protocol/browser/config.env +9 -0
  155. package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
  156. package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
  157. package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
  158. package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
  159. package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
  160. package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
  161. package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
  162. package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
  163. package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
  164. package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
  165. package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
  166. package/templates/qoder/scripts/{mcp → protocol/mcp}/lanhu_stdio_wrapper.py +11 -5
  167. package/templates/qoder/scripts/{mcp → protocol/mcp}/mcp_doctor.py +47 -14
  168. package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
  169. package/templates/qoder/scripts/{mcp → protocol/mcp}/mysql_mcp_server.py +181 -67
  170. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
  171. package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
  172. package/templates/qoder/scripts/protocol/transports/base.py +87 -0
  173. package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
  174. package/templates/qoder/scripts/protocol/transports/http.py +141 -0
  175. package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
  176. package/templates/qoder/scripts/run_weekly_update.bat +27 -18
  177. package/templates/qoder/scripts/run_weekly_update.sh +22 -13
  178. package/templates/qoder/scripts/validation/__init__.py +0 -0
  179. package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
  180. package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
  181. package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
  182. package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
  183. package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
  184. package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
  185. package/templates/qoder/scripts/{report → validation/metrics}/eval_prd.py +17 -10
  186. package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
  187. package/templates/qoder/scripts/validation/test/__init__.py +11 -0
  188. package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
  189. package/templates/qoder/scripts/{test → validation/test}/autotest.py +64 -4
  190. package/templates/qoder/scripts/{common → validation/test}/autotest_auth.py +1 -1
  191. package/templates/qoder/scripts/{test → validation/test}/autotest_batch.py +16 -3
  192. package/templates/qoder/scripts/{test → validation/test}/autotest_data.py +17 -12
  193. package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
  194. package/templates/qoder/scripts/{test → validation/test}/autotest_run.py +17 -3
  195. package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
  196. package/templates/qoder/scripts/{test → validation/test}/benchmark.py +12 -6
  197. package/templates/qoder/scripts/{test → validation/test}/kg_auto_login.py +8 -2
  198. package/templates/qoder/scripts/{test → validation/test}/kg_test_runner.py +7 -2
  199. package/templates/qoder/scripts/{test → validation/test}/page_probe.py +8 -3
  200. package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
  201. package/templates/qoder/settings.json +30 -0
  202. package/templates/qoder/skills/design-import/SKILL.md +22 -13
  203. package/templates/qoder/skills/design-review/SKILL.md +19 -10
  204. package/templates/qoder/skills/prd-generator/SKILL.md +22 -18
  205. package/templates/qoder/skills/prd-review/SKILL.md +6 -4
  206. package/templates/qoder/skills/prototype-generator/SKILL.md +63 -14
  207. package/templates/qoder/skills/spec-coder/SKILL.md +22 -13
  208. package/templates/qoder/skills/spec-generator/SKILL.md +22 -10
  209. package/templates/qoder/skills/test-generator/SKILL.md +7 -5
  210. package/templates/qoder/skills/wl-code/SKILL.md +23 -11
  211. package/templates/qoder/skills/wl-commit/SKILL.md +9 -8
  212. package/templates/qoder/skills/wl-design/SKILL.md +5 -3
  213. package/templates/qoder/skills/wl-init/SKILL.md +6 -6
  214. package/templates/qoder/skills/wl-insight/SKILL.md +23 -10
  215. package/templates/qoder/skills/wl-prd-full/SKILL.md +57 -6
  216. package/templates/qoder/skills/wl-prd-quick/SKILL.md +3 -3
  217. package/templates/qoder/skills/wl-prd-review/SKILL.md +16 -4
  218. package/templates/qoder/skills/wl-report/SKILL.md +25 -23
  219. package/templates/qoder/skills/wl-search/SKILL.md +149 -78
  220. package/templates/qoder/skills/wl-spec/SKILL.md +7 -5
  221. package/templates/qoder/skills/wl-status/SKILL.md +48 -16
  222. package/templates/qoder/skills/wl-task/SKILL.md +92 -10
  223. package/templates/qoder/skills/wl-test/SKILL.md +75 -29
  224. package/templates/qoder/templates/prd-full-template.md +7 -0
  225. package/templates/root/AGENTS.md +275 -236
  226. package/templates/root/requirements.txt +3 -0
  227. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
  228. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
  229. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +0 -23
  230. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +0 -23
  231. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +0 -1
  232. package/templates/.qoder/learning/eval-history.jsonl +0 -28
  233. package/templates/data/index/wiki-index.json +0 -8
  234. package/templates/qoder/nul +0 -4
  235. package/templates/qoder/scripts/kg/extract.py +0 -398
  236. package/templates/qoder/scripts/kg/kg_semantic.py +0 -156
  237. package/templates/qoder/scripts/mcp/kg_mcp_server.py +0 -606
  238. package/templates/qoder/scripts/mcp/mcp_launcher.py +0 -442
  239. package/templates/qoder/scripts/mcp/zentao_mcp_server.py +0 -384
  240. package/templates/qoder/scripts/task/__init__.py +0 -11
  241. package/templates/qoder/scripts/test/__init__.py +0 -11
  242. package/templates/qoder/scripts/wlkj.py +0 -116
  243. /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
  244. /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
  245. /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
  246. /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
  247. /package/templates/qoder/scripts/{test → validation/test}/secure-ls.js +0 -0
@@ -0,0 +1,600 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ⚠️ 本文件会被复制到 ~/.qoderwork/mcp_launcher.py, 在用户主目录运行!
4
+ # 不能依赖仓库内的 bootstrap/common (那里没有), 必须自包含路径定位。
5
+ import os, sys
6
+
7
+ try:
8
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
9
+ except (AttributeError, TypeError, OSError):
10
+ pass
11
+
12
+
13
+ """
14
+ mcp_launcher.py - 统一 MCP 启动器(跨 Win/Mac/Linux,消除写死路径)
15
+
16
+ 被谁调用:
17
+ QoderWork 按 ~/.qoderwork/mcp.json 的 command 拉起本脚本。
18
+ mcp.json 的 args = [<本脚本路径>, <MCP名>]
19
+ 例: {"command": "py", "args": ["C:/Users/HP/.qoderwork/mcp_launcher.py", "kg"]}
20
+
21
+ 为什么需要它:
22
+ 之前 mcp.json 写死绝对路径 (E:\\...\\python.exe + 仓库路径 + venv路径),
23
+ 换电脑/换系统/别人 clone 全废。本 launcher 运行时自定位仓库根 + 找对的 python,
24
+ mcp.json 只需写 python 命令名 (py/python3) + 本脚本路径 (在 ~/.qoderwork/)。
25
+
26
+ 做什么:
27
+ 1. 解析 argv[1] = MCP 名 (kg / mysql / zentao / lanhu)
28
+ 2. find_repo_root(): 环境变量 QODER_REPO > ~/.qoderwork/.repo-root 文件 > 向上找 .qoder/
29
+ 3. kg/mysql/zentao: 同进程执行 (in-proc) — 直接 import server 模块 + 调 ServerClass().run()。
30
+ 不再用 os.execv! 双冷启动 (launcher python + execv 后新 python) 是 MCP Pool 30s 超时
31
+ 和 Node-spawn 下 os.execv 丢 stdin 的根因, 改 in-proc 后只冷启动 1 次。
32
+ 4. lanhu: 必须用独立 venv 的 python 3.10+ (fastmcp 依赖), 仍 os.execv (venv 内, 无可避免)。
33
+
34
+ 跨平台:
35
+ find_repo_root 用 os.path, venv python 用 _venv_python (win Scripts/mac bin)。
36
+ 本脚本被 python3/py/python 跑都行 (kg/mysql/zentao 走 in-proc, lanhu 走 execv)。
37
+ """
38
+
39
+ import json
40
+ import os
41
+ import sys
42
+ from pathlib import Path
43
+
44
+ try:
45
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
46
+ except (AttributeError, TypeError, OSError):
47
+ pass
48
+
49
+
50
+ def _home():
51
+ """跨平台 home 目录。"""
52
+ return Path(os.environ.get('USERPROFILE') or os.path.expanduser('~'))
53
+
54
+
55
+ def _log(msg):
56
+ """调试日志到 stderr (MCP stdio 用 stdout, 不能污染)。"""
57
+ sys.stderr.write('[mcp-launcher] ' + msg + '\n')
58
+
59
+
60
+ def _launch_disabled_mcp(name, reason):
61
+ """没装/没配时, 启动一个空壳 MCP (只回 initialize + tools/list 空)。
62
+ 让 QoderWork 显示绿(已连接)但无工具, 而不是黄(报错)。
63
+ reason 写进 serverInfo 让用户知道为什么没工具。"""
64
+ import json as _j
65
+ _log(f'{name} MCP 以"未启用"模式启动: {reason}')
66
+ # 简单的 stdio 循环, 只响应 initialize 和 tools/list
67
+ while True:
68
+ try:
69
+ line = sys.stdin.buffer.readline()
70
+ except (EOFError, OSError):
71
+ break
72
+ if not line:
73
+ break
74
+ try:
75
+ req = _j.loads(line.decode('utf-8'))
76
+ except Exception:
77
+ continue
78
+ method = req.get('method', '')
79
+ rid = req.get('id')
80
+ if method == 'initialize':
81
+ sys.stdout.write(_j.dumps({
82
+ "jsonrpc": "2.0", "id": rid,
83
+ "result": {
84
+ "protocolVersion": "2024-11-05",
85
+ "capabilities": {"tools": {}},
86
+ "serverInfo": {"name": name, "version": "0.0.0-disabled",
87
+ "reason": reason}
88
+ }
89
+ }) + '\n')
90
+ sys.stdout.flush()
91
+ elif method == 'tools/list':
92
+ sys.stdout.write(_j.dumps({
93
+ "jsonrpc": "2.0", "id": rid,
94
+ "result": {"tools": []}
95
+ }) + '\n')
96
+ sys.stdout.flush()
97
+ elif method == 'notifications/initialized':
98
+ pass
99
+ elif rid is not None:
100
+ sys.stdout.write(_j.dumps({
101
+ "jsonrpc": "2.0", "id": rid,
102
+ "error": {"code": -32601, "message": reason}
103
+ }) + '\n')
104
+ sys.stdout.flush()
105
+
106
+
107
+ def find_repo_root():
108
+ """定位 QODER 仓库根。优先级:
109
+ 1. 环境变量 QODER_REPO (用户显式设, 最可靠)
110
+ 2. ~/.qoderwork/.repo-root 文件 (install 时写)
111
+ 3. 向上找 .qoder/scripts/ (cwd 在仓库内, IDE/CLI 场景)
112
+ 4. 本脚本若在 <repo>/.qoder/scripts/ 下 → 上三级 (开发态)
113
+ 返回 Path 或 None。
114
+
115
+ 全部失败时打一条 stderr 诊断 (不静默降级为"0 工具绿桩"),
116
+ 让用户知道是"仓库根没定位到"而不是 MCP 崩了。
117
+ """
118
+ # 1. 环境变量
119
+ env_repo = os.environ.get('QODER_REPO')
120
+ if env_repo and (Path(env_repo) / '.qoder' / 'scripts').is_dir():
121
+ return Path(env_repo)
122
+
123
+ # 2. ~/.qoderwork/.repo-root 文件 (install 写的, 主路径)
124
+ anchor = _home() / '.qoderwork' / '.repo-root'
125
+ if anchor.is_file():
126
+ try:
127
+ line = anchor.read_text(encoding='utf-8').strip()
128
+ if line and (Path(line) / '.qoder' / 'scripts').is_dir():
129
+ return Path(line)
130
+ except OSError:
131
+ pass
132
+
133
+ # 3. 向上找 .qoder/scripts/
134
+ here = Path(os.getcwd()).resolve()
135
+ for _ in range(8):
136
+ if (here / '.qoder' / 'scripts').is_dir():
137
+ return here
138
+ parent = here.parent
139
+ if parent == here:
140
+ break
141
+ here = parent
142
+
143
+ # 4. 本脚本位置 (仅当脚本真在 <repo>/.qoder/scripts/ 下时成立)。
144
+ # 注意: 安装到 ~/.qoderwork/mcp_launcher.py 的副本不能走这步 ——
145
+ # s.parent.parent.parent 会解析到用户主目录的父目录(非仓库), 误判。
146
+ # 必须用 .qoder/scripts 标志位校验, 校验不过就继续找其它线索。
147
+ try:
148
+ s = Path(__file__).resolve()
149
+ # 校验: 本脚本是否真在 .../<repo>/.qoder/scripts/ 结构内
150
+ if s.parent.name == 'scripts' and s.parent.parent.name == '.qoder':
151
+ root = s.parent.parent.parent # scripts/ -> .qoder/ -> repo
152
+ if (root / '.qoder' / 'scripts').is_dir():
153
+ return root
154
+ except Exception:
155
+ pass
156
+
157
+ # 全部失败 → 给清晰诊断, 不让 MCP 静默变 0 工具绿桩
158
+ _log('find_repo_root: 所有定位策略均失败, MCP 将以禁用桩启动。')
159
+ _log(' cwd=%s' % os.getcwd())
160
+ _log(' 锚点 %s 存在? %s' % (anchor, anchor.is_file()))
161
+ _log(' 修复: 在项目根目录跑 "npx @hupan56/wlkj init" (会写 .repo-root 锚点),')
162
+ _log(' 或设环境变量 QODER_REPO=<项目根目录> 后重启 QoderWork。')
163
+ return None
164
+
165
+
166
+ def _venv_python(venv):
167
+ """venv 内 python 路径 (跨平台: win Scripts/python.exe, mac bin/python)。"""
168
+ if sys.platform == 'win32':
169
+ return venv / 'Scripts' / 'python.exe'
170
+ return venv / 'bin' / 'python'
171
+
172
+
173
+ def _run_server_inproc(server_path):
174
+ """同进程运行 MCP server (替代 os.execv)。
175
+
176
+ 为什么不用 os.execv:
177
+ ① 双冷启动: launcher 的 Python 冷启动完, execv 又拉起一个新 Python 冷启动,
178
+ 3 个 server 同时启动 + Anaconda 冷启动慢 → MCP Pool 30s 超时 → 黄灯(0 工具)。
179
+ ② Node.js spawn 下 os.execv 丢 stdin: Windows 上 os.execv 是"新进程"不是真 execv,
180
+ A 进程 stdin buffer 里的 initialize 字节不会传给 B 进程 → B readline 读到空 → 立即退出。
181
+ in-proc 在当前进程直接跑 server, 既省一次冷启动又无 stdin 丢失。
182
+
183
+ 用 runpy.run_path(run_name='__main__') 等价于"直接运行那个 .py 文件":
184
+ - 会触发 server 顶部的 bootstrap.setup() (路径自举)
185
+ - 会执行 if __name__ == '__main__': ServerClass().run() (已有, 无需反射找类名)
186
+ - 比 importlib+反射更简单且不耦合 server 类名 (类改名也不影响 launcher)
187
+ """
188
+ import runpy
189
+ _log('in-proc 运行 %s' % os.path.basename(str(server_path)))
190
+ runpy.run_path(str(server_path), run_name='__main__') # 阻塞: stdio 主循环, 直到 stdin 关闭
191
+
192
+
193
+ def _read_current_developer(repo):
194
+ """读 .qoder/.developer 当前角色。"""
195
+ try:
196
+ _scripts = str(repo / '.qoder' / 'scripts')
197
+ if _scripts not in sys.path:
198
+ sys.path.insert(0, _scripts)
199
+ from foundation.core.paths import get_developer
200
+ return get_developer(repo)
201
+ except Exception:
202
+ return None
203
+
204
+
205
+ def _read_lanhu_cookie(repo, dev):
206
+ """读角色的蓝湖 cookie。
207
+
208
+ M2 子目录化后 setup_lanhu 在 scripts/setup/ 子包, 需把 setup/ 也加进 path。
209
+ """
210
+ try:
211
+ _scripts = str(repo / '.qoder' / 'scripts')
212
+ _setup_sub = str(repo / '.qoder' / 'scripts' / 'deployment' / 'setup')
213
+ for _p in (_scripts, _setup_sub):
214
+ if _p not in sys.path:
215
+ sys.path.insert(0, _p)
216
+ from setup_lanhu import _read_env_cookie, lanhu_env_file, COOKIE_PLACEHOLDER
217
+ ck = _read_env_cookie(dev)
218
+ return ck if (ck and ck != COOKIE_PLACEHOLDER) else None
219
+ except Exception:
220
+ return None
221
+
222
+
223
+ def launch_kg(repo):
224
+ """启动知识图谱 MCP。需 duckdb (本进程 import 即校验)。
225
+
226
+ v3.0: in-proc 执行 (替代 os.execv) — 省一次 Python 冷启动 + 消除 Node-spawn 下
227
+ os.execv 丢 stdin 问题。duckdb 缺失 → 降级空壳。
228
+ """
229
+ server = repo / '.qoder' / 'scripts' / 'protocol' / 'mcp' / 'kg_mcp_server.py'
230
+ if not server.is_file():
231
+ _log(f'kg server 不存在: {server}')
232
+ _launch_disabled_mcp('qoder-knowledge-graph',
233
+ 'kg_mcp_server.py 缺失。跑 npx @hupan56/wlkj update')
234
+ return
235
+ # 校验 duckdb (本进程 import, 无 subprocess 开销)
236
+ try:
237
+ import duckdb # noqa: F401
238
+ except ImportError:
239
+ _log('⚠️ 当前 Python 无 duckdb! 跑: pip install duckdb')
240
+ _log(' 或: npx @hupan56/wlkj install-env')
241
+ _launch_disabled_mcp('qoder-knowledge-graph',
242
+ 'duckdb 未装。跑 npx @hupan56/wlkj install-env (自动装依赖)')
243
+ return
244
+ _log('启动 kg (in-proc)')
245
+ _run_server_inproc(server)
246
+
247
+
248
+ def launch_mysql(repo):
249
+ """启动 MySQL MCP。连接信息优先从 config.yaml 读 (团队共享, git 里有)。
250
+ 回退: ~/.qoderwork/.mcp-mysql-env.json (旧版个人文件)。"""
251
+ server = repo / '.qoder' / 'scripts' / 'protocol' / 'mcp' / 'mysql_mcp_server.py'
252
+ if not server.is_file():
253
+ _log(f'mysql server 不存在: {server}')
254
+ _launch_disabled_mcp('qoder-mysql',
255
+ 'mysql_mcp_server.py 缺失。跑 npx @hupan56/wlkj update')
256
+ return
257
+ # 优先: 从 config.yaml 读 (团队共享, clone 就有)
258
+ cfg_path = repo / '.qoder' / 'config.yaml'
259
+ mysql_cfg = {}
260
+ if cfg_path.is_file():
261
+ try:
262
+ import yaml
263
+ d = yaml.safe_load(cfg_path.read_text(encoding='utf-8')) or {}
264
+ mysql_cfg = d.get('mysql', {}) or {}
265
+ except Exception:
266
+ pass
267
+ # 注入策略 (v3.x 多环境): 把全部环境序列化成 JSON 给 server,
268
+ # server 按工具调用的 env 参数取对应环境建连, 运行时切库。
269
+ if mysql_cfg.get('environments'):
270
+ # 新结构: default_env + environments
271
+ os.environ['MYSQL_ENVIRONMENTS'] = json.dumps(
272
+ mysql_cfg['environments'], ensure_ascii=False)
273
+ os.environ['MYSQL_DEFAULT_ENV'] = str(
274
+ mysql_cfg.get('default_env') or
275
+ next(iter(mysql_cfg['environments']), 'test'))
276
+ _log(f'mysql 多环境注入 ({len(mysql_cfg["environments"])} 个环境: '
277
+ f'{", ".join(mysql_cfg["environments"].keys())})')
278
+ elif mysql_cfg.get('host'):
279
+ # 兼容旧扁平结构 → 包成单环境 test
280
+ os.environ['MYSQL_ENVIRONMENTS'] = json.dumps(
281
+ {'test': mysql_cfg}, ensure_ascii=False)
282
+ os.environ['MYSQL_DEFAULT_ENV'] = 'test'
283
+ _log('mysql 旧扁平结构 → 包成单环境 test')
284
+ else:
285
+ # 回退: 旧版个人 env 文件 (~/.qoderwork/.mcp-mysql-env.json)
286
+ env_file = _home() / '.qoderwork' / '.mcp-mysql-env.json'
287
+ if env_file.is_file():
288
+ try:
289
+ cfg = json.loads(env_file.read_text(encoding='utf-8'))
290
+ # 旧个人文件是扁平的 MYSQL_HOST/... → 包成单环境 test
291
+ flat = {k[len('MYSQL_'):].lower(): v for k, v in cfg.items()
292
+ if k.startswith('MYSQL_') and v}
293
+ if flat.get('host'):
294
+ if 'env_name' not in flat:
295
+ flat['env_name'] = flat.get('host', '')
296
+ os.environ['MYSQL_ENVIRONMENTS'] = json.dumps(
297
+ {'test': flat}, ensure_ascii=False)
298
+ os.environ['MYSQL_DEFAULT_ENV'] = 'test'
299
+ _log(f'mysql 个人文件 → 包成单环境 test ({len(flat)} 项)')
300
+ except (json.JSONDecodeError, OSError):
301
+ pass
302
+ if 'MYSQL_ENVIRONMENTS' not in os.environ:
303
+ _log('⚠️ mysql 配置缺失: config.yaml 无 mysql.environments/host, '
304
+ '且 ~/.qoderwork/.mcp-mysql-env.json 不存在')
305
+ _log(f'启动 mysql (in-proc)')
306
+ # 校验 pymysql (本进程 import)
307
+ try:
308
+ import pymysql # noqa: F401
309
+ except ImportError:
310
+ _log('⚠️ 当前 Python 无 pymysql! 跑: pip install pymysql')
311
+ _launch_disabled_mcp('qoder-mysql',
312
+ 'pymysql 未装。跑 pip install pymysql 或 npx @hupan56/wlkj install-env')
313
+ return
314
+ _run_server_inproc(server)
315
+
316
+
317
+ ZENTAO_URL_DEFAULT = 'http://10.89.2.91/zentao'
318
+
319
+
320
+ def launch_zentao(repo):
321
+ """启动禅道 MCP。
322
+
323
+ 凭据架构 (v3.1.4): 禅道是【每人一个账号】, 不是团队共享。
324
+ - URL (服务地址): 从 config.yaml 读 (团队共享, 这个确实是共享的)
325
+ - user/password (个人凭据): 从 .private/secrets/zentao.env 读 (gitignore, 私有)
326
+ —— 与蓝湖 cookie 同一套成熟模式 (get_secrets_dir 按开发者隔离)。
327
+ - 账号名映射 zentao_account: 从 member.json 读 (进 git, 非密钥, 团队需要知道来过滤归属)。
328
+ """
329
+ server = repo / '.qoder' / 'scripts' / 'protocol' / 'mcp' / 'zentao_mcp_server.py'
330
+ if not server.is_file():
331
+ _log(f'zentao server 不存在: {server}')
332
+ _launch_disabled_mcp('qoder-zentao', 'zentao_mcp_server.py 缺失')
333
+ return
334
+
335
+ # ① URL: 团队共享服务地址, 从 config.yaml 读
336
+ zentao_url = ZENTAO_URL_DEFAULT
337
+ cfg_path = repo / '.qoder' / 'config.yaml'
338
+ if cfg_path.is_file():
339
+ try:
340
+ import yaml
341
+ d = yaml.safe_load(cfg_path.read_text(encoding='utf-8')) or {}
342
+ zt = d.get('zentao', {}) or {}
343
+ if zt.get('url'):
344
+ zentao_url = str(zt['url'])
345
+ except Exception:
346
+ pass
347
+ os.environ['ZENTAO_URL'] = zentao_url
348
+
349
+ # ② user/password: 个人凭据, 从当前开发者的 .private/secrets/zentao.env 读
350
+ dev = _read_current_developer(repo)
351
+ zt_user = zt_pwd = ''
352
+ if dev:
353
+ try:
354
+ from foundation.core.paths import get_secrets_dir
355
+ env_file = Path(get_secrets_dir(dev)) / 'zentao.env'
356
+ if env_file.is_file():
357
+ for line in env_file.read_text(encoding='utf-8').splitlines():
358
+ line = line.strip()
359
+ if line.startswith('ZENTAO_USER='):
360
+ zt_user = line.split('=', 1)[1].strip().strip('"\'')
361
+ elif line.startswith('ZENTAO_PASSWORD='):
362
+ zt_pwd = line.split('=', 1)[1].strip().strip('"\'')
363
+ except Exception:
364
+ pass
365
+
366
+ if not zt_user or not zt_pwd:
367
+ # 个人凭据未配 → 降级 disabled MCP, 提示配置路径
368
+ hint = ('workspace/members/%s/.private/secrets/zentao.env' % (dev or '<developer>'))
369
+ _log(f'禅道个人凭据未配置 (每人一个账号, 不再从 config.yaml 读)')
370
+ _log(f' 编辑 {hint}, 填 ZENTAO_USER=<你的禅道账号> 和 ZENTAO_PASSWORD=<你的密码>')
371
+ _launch_disabled_mcp('qoder-zentao', '禅道凭据未配置: %s' % hint)
372
+ return
373
+
374
+ os.environ['ZENTAO_USER'] = zt_user
375
+ os.environ['ZENTAO_PASSWORD'] = zt_pwd
376
+ _log(f'zentao 凭据已注入 (来自 {dev} 的 .private/secrets/zentao.env)')
377
+
378
+ # ③ 账号名映射: 从 member.json 读 (用于归属过滤, 进 git 非密钥)
379
+ if dev:
380
+ try:
381
+ from foundation.identity.identity import get_member
382
+ mem = get_member(dev)
383
+ acc = (mem.get('zentao_account') or '').strip() if mem else ''
384
+ if acc:
385
+ os.environ['ZENTAO_MY_ACCOUNT'] = acc
386
+ _log(f'zentao 归属账号: {acc} (来自 {dev} 的 member.json)')
387
+ except Exception:
388
+ pass
389
+ _log(f'启动 zentao (in-proc)')
390
+ # 校验 requests (本进程 import)
391
+ try:
392
+ import requests # noqa: F401
393
+ except ImportError:
394
+ _log('⚠️ 当前 Python 无 requests! pip install requests')
395
+ _launch_disabled_mcp('qoder-zentao', 'requests 未装。pip install requests')
396
+ return
397
+ _run_server_inproc(server)
398
+
399
+
400
+ def launch_lanhu(repo):
401
+ """启动蓝湖 MCP。用 venv 的 3.10+ python (fastmcp 要求), 读角色 cookie。
402
+ 没装 lanhu venv 或没配 cookie 时, 输出友好提示并启动一个空壳 MCP
403
+ (让 QoderWork 显示"未启用"而不是黄/报错)。"""
404
+ venv = repo / 'external' / 'lanhu-mcp' / '.venv'
405
+ server = repo / 'external' / 'lanhu-mcp' / 'lanhu_mcp_server.py'
406
+
407
+ # 检查 venv
408
+ if not venv.is_dir():
409
+ _log('lanhu venv 未安装 — MCP 将以"未启用"模式启动')
410
+ _launch_disabled_mcp('lanhu',
411
+ '蓝湖 MCP 未安装。跑 python .qoder/scripts/setup_lanhu.py 安装')
412
+ return
413
+ py = _venv_python(venv)
414
+ if not py.is_file():
415
+ _log(f'lanhu venv python 不存在: {py}')
416
+ _launch_disabled_mcp('lanhu', '蓝湖 MCP venv 异常, 重装: setup_lanhu.py')
417
+ return
418
+ if not server.is_file():
419
+ _log(f'lanhu server 不存在: {server}')
420
+ _launch_disabled_mcp('lanhu', '蓝湖 MCP server 文件缺失')
421
+ return
422
+
423
+ # 读角色 cookie
424
+ dev = _read_current_developer(repo)
425
+ if not dev:
426
+ _log('未初始化开发者 (.qoder/.developer), 跑 /wl-init')
427
+ _launch_disabled_mcp('lanhu', '未初始化开发者, 跑 /wl-init')
428
+ return
429
+ cookie = _read_lanhu_cookie(repo, dev)
430
+ if not cookie:
431
+ _log(f'角色 {dev} 的蓝湖 cookie 未配置 (占位/缺失)')
432
+ _log(f' 编辑 workspace/members/{dev}/.private/secrets/lanhu.env')
433
+ _launch_disabled_mcp('lanhu',
434
+ f'蓝湖 cookie 未配置: workspace/members/{dev}/.private/secrets/lanhu.env')
435
+ return
436
+
437
+ # 注入 env (角色隔离: cookie + 用户名都是当前角色的)
438
+ env = dict(os.environ)
439
+ env['LANHU_COOKIE'] = cookie
440
+ env['MCP_TRANSPORT'] = 'stdio'
441
+ env['LANHU_USER_NAME'] = dev or 'Developer'
442
+ env['LANHU_USER_ROLE'] = 'Developer'
443
+ _log(f'启动 lanhu (角色={dev}, python={py}, 中继模式)')
444
+ # ⚠️ 不用 os.execv! Node.js spawn 下 os.execv 丢 stdin (Windows 的 execv 是新进程,
445
+ # A 进程 stdin buffer 里的 initialize 字节传不到 venv 子进程 → 立即 EOF 退出)。
446
+ # lanhu 必须用独立 venv (fastmcp 要 3.10+), 没法像 kg/mysql/zentao 那样 in-proc,
447
+ # 所以用 subprocess + 管道双向转发: launcher 当 stdio 中继, 子进程跑真实 server。
448
+ _relay_subprocess([str(py), str(server)], env)
449
+
450
+
451
+ def _relay_subprocess(cmd, env):
452
+ """spawn 子进程并双向转发 stdio (替代 os.execv, 规避 Node-spawn 丢 stdin)。
453
+
454
+ 用于 lanhu (需独立 venv, 无法 in-proc)。launcher 进程:
455
+ - 把自己的 stdin (QoderWork 发来的 JSON-RPC) 转发给子进程 stdin
456
+ - 把子进程 stdout (JSON-RPC 响应) 转发给自己的 stdout (回 QoderWork)
457
+ - stderr 各自独立 (诊断日志)
458
+ 子进程退出 → launcher 退出 (QoderWork 管生死)。
459
+ """
460
+ import subprocess
461
+ import threading
462
+ child = subprocess.Popen(
463
+ cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
464
+ stderr=sys.stderr, env=env)
465
+ _log('lanhu 子进程已起 (pid=%s), 转发 stdio' % child.pid)
466
+
467
+ def pump_in():
468
+ """QoderWork stdin → 子进程 stdin (按行转发)。
469
+
470
+ ⚠️ 不能用 read(4096)! MCP 客户端发的一行 JSON-RPC 只有 ~150 字节,
471
+ read(4096) 会阻塞等满 4096 字节 → 永远不把单行转发出去 → 子进程收不到
472
+ initialize → 不响应 → 握手超时。必须用 readline() 按行转发 (MCP stdio
473
+ 协议每条消息一行)。
474
+ """
475
+ try:
476
+ while True:
477
+ line = sys.stdin.buffer.readline()
478
+ if not line:
479
+ break
480
+ child.stdin.write(line)
481
+ child.stdin.flush()
482
+ except (BrokenPipeError, OSError, ValueError):
483
+ pass
484
+ finally:
485
+ try:
486
+ child.stdin.close()
487
+ except Exception:
488
+ pass
489
+
490
+ def pump_out():
491
+ """子进程 stdout → QoderWork stdout (块读 + 按行过滤, 只放行 JSON-RPC)。
492
+
493
+ fastmcp 启动会往 stderr 打 ASCII banner (不污染 stdout), 但保险起见
494
+ 仍过滤 stdout: 块读 4096, 按换行切行, 只转发能 JSON parse 的行。
495
+ """
496
+ import json as _json
497
+ buf = ''
498
+ try:
499
+ while True:
500
+ chunk = child.stdout.read(1)
501
+ if not chunk:
502
+ # flush 残留 buf
503
+ if buf.strip().startswith('{'):
504
+ try:
505
+ _json.loads(buf.strip())
506
+ sys.stdout.buffer.write((buf.strip() + '\n').encode('utf-8'))
507
+ sys.stdout.buffer.flush()
508
+ except Exception:
509
+ pass
510
+ break
511
+ try:
512
+ buf += chunk.decode('utf-8', errors='replace')
513
+ except Exception:
514
+ continue
515
+ # 按换行处理完整行
516
+ while '\n' in buf:
517
+ line, buf = buf.split('\n', 1)
518
+ text = line.strip()
519
+ if not text:
520
+ continue
521
+ if not text.startswith('{'):
522
+ try:
523
+ sys.stderr.write('[lanhu-stdout-noise] ' + line + '\n')
524
+ except Exception:
525
+ pass
526
+ continue
527
+ try:
528
+ _json.loads(text)
529
+ except _json.JSONDecodeError:
530
+ try:
531
+ sys.stderr.write('[lanhu-stdout-noise] ' + line + '\n')
532
+ except Exception:
533
+ pass
534
+ continue
535
+ try:
536
+ sys.stdout.buffer.write((text + '\n').encode('utf-8'))
537
+ sys.stdout.buffer.flush()
538
+ except (BrokenPipeError, OSError, ValueError):
539
+ return
540
+ except (BrokenPipeError, OSError, ValueError):
541
+ pass
542
+
543
+ t1 = threading.Thread(target=pump_in, daemon=True)
544
+ t2 = threading.Thread(target=pump_out, daemon=True)
545
+ t1.start()
546
+ t2.start()
547
+ child.wait() # 子进程退出前不返回
548
+ _log('lanhu 子进程退出 (code=%s)' % child.returncode)
549
+
550
+
551
+ LAUNCHERS = {
552
+ 'kg': launch_kg,
553
+ 'knowledge-graph': launch_kg,
554
+ 'mysql': launch_mysql,
555
+ 'zentao': launch_zentao,
556
+ 'lanhu': launch_lanhu,
557
+ }
558
+
559
+
560
+ def main():
561
+ # ⚠️ 关键: 任何失败都走 _launch_disabled_mcp(), 绝不 sys.exit()。
562
+ # sys.exit(1) 会让进程退出 → QoderWork 显示黄色(连接错误), 用户以为坏了。
563
+ # _launch_disabled_mcp() 启动一个干净 stdio 循环响应 initialize →
564
+ # QoderWork 显示绿色(已连接, 0工具), 用户知道是"未启用"而非"坏了"。
565
+ if len(sys.argv) < 2:
566
+ _log('用法: mcp_launcher.py <kg|mysql|zentao|lanhu>')
567
+ _log(f' argv={sys.argv}')
568
+ _launch_disabled_mcp('qoder-mcp',
569
+ '启动参数缺失: mcp_launcher.py <kg|mysql|zentao|lanhu>')
570
+ return
571
+ name = sys.argv[1].lower()
572
+
573
+ repo = find_repo_root()
574
+ if not repo:
575
+ _log('无法定位 QODER 仓库根。')
576
+ _log(' 方式1: 设环境变量 QODER_REPO=<仓库根>')
577
+ _log(' 方式2: 重跑 npx @hupan56/wlkj update (刷新 .repo-root)')
578
+ _log(' 方式3: 在仓库目录内运行')
579
+ _launch_disabled_mcp(name,
580
+ '未定位到 QODER 仓库根。跑 npx @hupan56/wlkj update')
581
+ return
582
+ _log(f'仓库根: {repo}')
583
+
584
+ fn = LAUNCHERS.get(name)
585
+ if not fn:
586
+ _log(f'未知 MCP 名: {name} (支持: {list(LAUNCHERS)})')
587
+ _launch_disabled_mcp(name,
588
+ '未知 MCP 名: %s (支持: %s)' % (name, ', '.join(LAUNCHERS)))
589
+ return
590
+ try:
591
+ fn(repo)
592
+ except Exception as e:
593
+ # 启动函数抛异常也不能让进程死 (死=黄, 捕获=绿)
594
+ _log(f'启动 {name} 时异常: {e}')
595
+ _launch_disabled_mcp(name,
596
+ '%s MCP 启动异常: %s。跑 npx @hupan56/wlkj update' % (name, e))
597
+
598
+
599
+ if __name__ == '__main__':
600
+ main()