@hupan56/wlkj 2.7.11 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/bin/cli.js +375 -78
  2. package/package.json +29 -29
  3. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
  4. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
  5. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
  6. package/templates/.qoder/learning/eval-history.jsonl +28 -0
  7. package/templates/data/index/wiki-index.json +8 -0
  8. package/templates/qoder/agents/insight-planning.md +1 -1
  9. package/templates/qoder/agents/insight-research.md +28 -15
  10. package/templates/qoder/commands/optional/wl-insight.md +159 -161
  11. package/templates/qoder/commands/optional/wl-report.md +4 -4
  12. package/templates/qoder/commands/optional/wl-status.md +2 -2
  13. package/templates/qoder/commands/wl-code.md +2 -2
  14. package/templates/qoder/commands/wl-design.md +2 -2
  15. package/templates/qoder/commands/wl-init.md +3 -3
  16. package/templates/qoder/commands/wl-prd.md +2 -2
  17. package/templates/qoder/commands/wl-req.md +43 -0
  18. package/templates/qoder/commands/wl-search.md +8 -8
  19. package/templates/qoder/commands/wl-task.md +17 -17
  20. package/templates/qoder/commands/wl-test.md +41 -15
  21. package/templates/qoder/config.yaml +17 -1
  22. package/templates/qoder/hooks/session-start.py +12 -22
  23. package/templates/qoder/nul +4 -0
  24. package/templates/qoder/rules/wl-pipeline.md +22 -48
  25. package/templates/qoder/scripts/README.md +139 -0
  26. package/templates/qoder/scripts/common/autotest_auth.py +109 -0
  27. package/templates/qoder/scripts/common/bootstrap.py +145 -0
  28. package/templates/qoder/scripts/common/check_publish.py +98 -0
  29. package/templates/qoder/scripts/common/cmd_registry.py +112 -0
  30. package/templates/qoder/scripts/common/config.py +187 -0
  31. package/templates/qoder/scripts/common/contract.py +317 -0
  32. package/templates/qoder/scripts/common/developer.py +2 -1
  33. package/templates/qoder/scripts/common/feishu.py +10 -9
  34. package/templates/qoder/scripts/common/guard.py +159 -0
  35. package/templates/qoder/scripts/common/identity.py +121 -2
  36. package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
  37. package/templates/qoder/scripts/common/mcp_base.py +268 -0
  38. package/templates/qoder/scripts/common/paths.py +187 -1
  39. package/templates/qoder/scripts/common/result.py +223 -0
  40. package/templates/qoder/scripts/common/roles.py +60 -0
  41. package/templates/qoder/scripts/common/task_utils.py +21 -9
  42. package/templates/qoder/scripts/common/test_extract.py +115 -0
  43. package/templates/qoder/scripts/kg/__init__.py +11 -0
  44. package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
  45. package/templates/qoder/scripts/kg/build_relations.py +127 -0
  46. package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
  47. package/templates/qoder/scripts/kg/build_workflows.py +144 -0
  48. package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
  49. package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
  50. package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
  51. package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
  52. package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
  53. package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
  54. package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
  55. package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
  56. package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
  57. package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
  58. package/templates/qoder/scripts/kg/prefetch.py +359 -0
  59. package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
  60. package/templates/qoder/scripts/mcp/__init__.py +11 -0
  61. package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
  62. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
  63. package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
  64. package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
  65. package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
  66. package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
  67. package/templates/qoder/scripts/report/__init__.py +11 -0
  68. package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
  69. package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
  70. package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
  71. package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
  72. package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
  73. package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
  74. package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
  75. package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
  76. package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
  77. package/templates/qoder/scripts/report/req.py +222 -0
  78. package/templates/qoder/scripts/report/role.py +33 -0
  79. package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
  80. package/templates/qoder/scripts/setup/__init__.py +11 -0
  81. package/templates/qoder/scripts/setup/carriers.py +662 -0
  82. package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
  83. package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
  84. package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
  85. package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
  86. package/templates/qoder/scripts/{setup.py → setup/setup.py} +147 -0
  87. package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
  88. package/templates/qoder/scripts/task/__init__.py +11 -0
  89. package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
  90. package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
  91. package/templates/qoder/scripts/task/task.py +221 -0
  92. package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
  93. package/templates/qoder/scripts/task/task_query.py +161 -0
  94. package/templates/qoder/scripts/task/task_relations.py +424 -0
  95. package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
  96. package/templates/qoder/scripts/test/__init__.py +11 -0
  97. package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
  98. package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
  99. package/templates/qoder/scripts/test/autotest_data.py +675 -0
  100. package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
  101. package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
  102. package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
  103. package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
  104. package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
  105. package/templates/qoder/scripts/wlkj.py +116 -0
  106. package/templates/qoder/settings.json +1 -10
  107. package/templates/qoder/skills/design-import/SKILL.md +226 -226
  108. package/templates/qoder/skills/design-review/SKILL.md +82 -82
  109. package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
  110. package/templates/qoder/skills/prd-review/SKILL.md +5 -5
  111. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
  112. package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
  113. package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
  114. package/templates/qoder/skills/test-generator/SKILL.md +5 -5
  115. package/templates/qoder/skills/wl-code/SKILL.md +4 -4
  116. package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
  117. package/templates/qoder/skills/wl-design/SKILL.md +3 -3
  118. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  119. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  120. package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
  121. package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
  122. package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
  123. package/templates/qoder/skills/wl-report/SKILL.md +7 -7
  124. package/templates/qoder/skills/wl-search/SKILL.md +13 -13
  125. package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
  126. package/templates/qoder/skills/wl-status/SKILL.md +5 -5
  127. package/templates/qoder/skills/wl-task/SKILL.md +6 -6
  128. package/templates/qoder/skills/wl-test/SKILL.md +102 -39
  129. package/templates/root/AGENTS.md +39 -40
  130. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  131. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  132. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  133. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  135. package/templates/qoder/scripts/check_carriers.py +0 -238
  136. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  137. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  138. package/templates/qoder/scripts/collect_prds.py +0 -31
  139. package/templates/qoder/scripts/common/mentions.py +0 -134
  140. package/templates/qoder/scripts/common/utf8.py +0 -38
  141. package/templates/qoder/scripts/extract_routes.py +0 -54
  142. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  143. package/templates/qoder/scripts/handoff.py +0 -22
  144. package/templates/qoder/scripts/init_developer.py +0 -76
  145. package/templates/qoder/scripts/parse_prds.py +0 -33
  146. package/templates/qoder/scripts/role.py +0 -51
  147. package/templates/qoder/scripts/sync_carriers.py +0 -259
  148. package/templates/qoder/scripts/task.py +0 -1261
  149. package/templates/qoder/scripts/workspace_init.py +0 -102
  150. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  151. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  152. /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
@@ -1,414 +1,442 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- mcp_launcher.py - 统一 MCP 启动器(跨 Win/Mac/Linux,消除写死路径)
5
-
6
- 被谁调用:
7
- QoderWork 按 ~/.qoderwork/mcp.json 的 command 拉起本脚本。
8
- mcp.json 的 args = [<本脚本路径>, <MCP名>]
9
- 例: {"command": "py", "args": ["C:/Users/HP/.qoderwork/mcp_launcher.py", "kg"]}
10
-
11
- 为什么需要它:
12
- 之前 mcp.json 写死绝对路径 (E:\\...\\python.exe + 仓库路径 + venv路径),
13
- 换电脑/换系统/别人 clone 全废。本 launcher 运行时自定位仓库根 + 找对的 python,
14
- mcp.json 只需写 python 命令名 (py/python3) + 本脚本路径 (在 ~/.qoderwork/)。
15
-
16
- 做什么:
17
- 1. 解析 argv[1] = MCP 名 (kg / mysql / lanhu)
18
- 2. find_repo_root(): 环境变量 QODER_REPO > ~/.qoderwork/.repo-root 文件 > 向上找 .qoder/
19
- 3. MCP 名选 python + server 脚本:
20
- - kg: sys.executable, <repo>/.qoder/scripts/kg_mcp_server.py
21
- - mysql: sys.executable, <repo>/.qoder/scripts/mysql_mcp_server.py (env 从 .mcp-mysql-env.json 读)
22
- - lanhu: <repo>/external/lanhu-mcp/.venv python (3.10+), 复用 lanhu_stdio_wrapper 逻辑 (读角色 cookie)
23
- 4. os.execv 启动 (接管进程, QoderWork 管生死)
24
-
25
- 跨平台:
26
- find_repo_root 用 os.path, venv python 用 _venv_python (win Scripts/mac bin)。
27
- 本脚本被 python3/py/python 跑都行 (它只做定位+execv, 不依赖特定版本)
28
- """
29
-
30
- import json
31
- import os
32
- import sys
33
- from pathlib import Path
34
-
35
- try:
36
- sys.stderr.reconfigure(encoding='utf-8', errors='replace')
37
- except (AttributeError, TypeError, OSError):
38
- pass
39
-
40
-
41
- def _home():
42
- """跨平台 home 目录。"""
43
- return Path(os.environ.get('USERPROFILE') or os.path.expanduser('~'))
44
-
45
-
46
- def _log(msg):
47
- """调试日志到 stderr (MCP stdio 用 stdout, 不能污染)。"""
48
- sys.stderr.write('[mcp-launcher] ' + msg + '\n')
49
-
50
-
51
- def _launch_disabled_mcp(name, reason):
52
- """没装/没配时, 启动一个空壳 MCP (只回 initialize + tools/list 空)。
53
- QoderWork 显示绿(已连接)但无工具, 而不是黄(报错)
54
- reason 写进 serverInfo 让用户知道为什么没工具。"""
55
- import json as _j
56
- _log(f'{name} MCP 以"未启用"模式启动: {reason}')
57
- # 简单的 stdio 循环, 只响应 initialize 和 tools/list
58
- while True:
59
- try:
60
- line = sys.stdin.buffer.readline()
61
- except (EOFError, OSError):
62
- break
63
- if not line:
64
- break
65
- try:
66
- req = _j.loads(line.decode('utf-8'))
67
- except Exception:
68
- continue
69
- method = req.get('method', '')
70
- rid = req.get('id')
71
- if method == 'initialize':
72
- sys.stdout.write(_j.dumps({
73
- "jsonrpc": "2.0", "id": rid,
74
- "result": {
75
- "protocolVersion": "2024-11-05",
76
- "capabilities": {"tools": {}},
77
- "serverInfo": {"name": name, "version": "0.0.0-disabled",
78
- "reason": reason}
79
- }
80
- }) + '\n')
81
- sys.stdout.flush()
82
- elif method == 'tools/list':
83
- sys.stdout.write(_j.dumps({
84
- "jsonrpc": "2.0", "id": rid,
85
- "result": {"tools": []}
86
- }) + '\n')
87
- sys.stdout.flush()
88
- elif method == 'notifications/initialized':
89
- pass
90
- elif rid is not None:
91
- sys.stdout.write(_j.dumps({
92
- "jsonrpc": "2.0", "id": rid,
93
- "error": {"code": -32601, "message": reason}
94
- }) + '\n')
95
- sys.stdout.flush()
96
-
97
-
98
- def _find_python_with_deps(required_module='duckdb'):
99
- """找一个装了指定依赖的 Python。优先级:
100
- 1. sys.executable (当前 Python, 先测它有没有)
101
- 2. 常见 Python 路径 (anaconda/python3等) 逐个测
102
- 返回 python 路径(str) 或 None。
103
- 解决"py -3 选的 Python 没装 duckdb"问题。
104
- """
105
- # 1. 当前 Python 有没有
106
- try:
107
- __import__(required_module)
108
- return sys.executable
109
- except ImportError:
110
- pass
111
- # 2. 找其它 Python
112
- import shutil
113
- candidates = []
114
- for name in ('python', 'python3', 'py'):
115
- p = shutil.which(name)
116
- if p and p != sys.executable:
117
- candidates.append(p)
118
- # 常见安装路径 (Windows)
119
- if sys.platform == 'win32':
120
- for p in [
121
- os.path.join(os.environ.get('USERPROFILE', ''), 'Anaconda3', 'python.exe'),
122
- os.path.join(os.environ.get('USERPROFILE', ''), 'anaconda3', 'python.exe'),
123
- os.path.join(os.environ.get('USERPROFILE', ''), 'miniconda3', 'python.exe'),
124
- r'C:\ProgramData\Anaconda3\python.exe',
125
- ]:
126
- if os.path.isfile(p) and p not in candidates:
127
- candidates.append(p)
128
- for py in candidates:
129
- try:
130
- r = __import__('subprocess').run(
131
- [py, '-c', 'import %s' % required_module],
132
- capture_output=True, timeout=5)
133
- if r.returncode == 0:
134
- _log(f'找到装了 {required_module} 的 Python: {py}')
135
- return py
136
- except Exception:
137
- continue
138
- return None
139
-
140
-
141
- def find_repo_root():
142
- """定位 QODER 仓库根。优先级:
143
- 1. 环境变量 QODER_REPO (用户显式设, 最可靠)
144
- 2. ~/.qoderwork/.repo-root 文件 (install 时写)
145
- 3. 向上找 .qoder/scripts/ (cwd 在仓库内, IDE/CLI 场景)
146
- 4. 本脚本若在 <repo>/.qoder/scripts/ 下 → 上三级 (开发态)
147
- 返回 Path None。
148
- """
149
- # 1. 环境变量
150
- env_repo = os.environ.get('QODER_REPO')
151
- if env_repo and (Path(env_repo) / '.qoder' / 'scripts').is_dir():
152
- return Path(env_repo)
153
-
154
- # 2. ~/.qoderwork/.repo-root 文件 (install 写的, 主路径)
155
- anchor = _home() / '.qoderwork' / '.repo-root'
156
- if anchor.is_file():
157
- try:
158
- line = anchor.read_text(encoding='utf-8').strip()
159
- if line and (Path(line) / '.qoder' / 'scripts').is_dir():
160
- return Path(line)
161
- except OSError:
162
- pass
163
-
164
- # 3. 向上找 .qoder/scripts/
165
- here = Path(os.getcwd()).resolve()
166
- for _ in range(8):
167
- if (here / '.qoder' / 'scripts').is_dir():
168
- return here
169
- parent = here.parent
170
- if parent == here:
171
- break
172
- here = parent
173
-
174
- # 4. 本脚本位置 (开发态: 在 <repo>/.qoder/scripts/mcp_launcher.py)
175
- try:
176
- s = Path(__file__).resolve()
177
- root = s.parent.parent.parent # scripts/ -> .qoder/ -> repo
178
- if (root / '.qoder' / 'scripts').is_dir():
179
- return root
180
- except Exception:
181
- pass
182
-
183
- return None
184
-
185
-
186
- def _venv_python(venv):
187
- """venv python 路径 (跨平台: win Scripts/python.exe, mac bin/python)。"""
188
- if sys.platform == 'win32':
189
- return venv / 'Scripts' / 'python.exe'
190
- return venv / 'bin' / 'python'
191
-
192
-
193
- def _read_current_developer(repo):
194
- """读 .qoder/.developer 当前角色。"""
195
- try:
196
- sys.path.insert(0, str(repo / '.qoder' / 'scripts'))
197
- from common.paths import get_developer
198
- return get_developer(repo)
199
- except Exception:
200
- return None
201
-
202
-
203
- def _read_lanhu_cookie(repo, dev):
204
- """读角色的蓝湖 cookie。"""
205
- try:
206
- sys.path.insert(0, str(repo / '.qoder' / 'scripts'))
207
- from setup_lanhu import _read_env_cookie, lanhu_env_file, COOKIE_PLACEHOLDER
208
- ck = _read_env_cookie(dev)
209
- return ck if (ck and ck != COOKIE_PLACEHOLDER) else None
210
- except Exception:
211
- return None
212
-
213
-
214
- def launch_kg(repo):
215
- """启动知识图谱 MCP。找装了 duckdb Python (不是盲目用 sys.executable)。"""
216
- server = repo / '.qoder' / 'scripts' / 'kg_mcp_server.py'
217
- if not server.is_file():
218
- _log(f'kg server 不存在: {server}')
219
- _launch_disabled_mcp('qoder-knowledge-graph',
220
- 'kg_mcp_server.py 缺失。跑 npx @hupan56/wlkj update')
221
- return
222
- py = _find_python_with_deps('duckdb')
223
- if not py:
224
- _log('⚠️ 找不到装了 duckdb Python! 跑: pip install duckdb')
225
- _log(' 或: npx @hupan56/wlkj install-env (自动装依赖)')
226
- _launch_disabled_mcp('qoder-knowledge-graph',
227
- 'duckdb 未装。跑 npx @hupan56/wlkj install-env (自动装依赖)')
228
- return
229
- _log(f'启动 kg (python={py})')
230
- os.execv(py, [py, str(server)])
231
-
232
-
233
- def launch_mysql(repo):
234
- """启动 MySQL MCP。连接信息优先从 config.yaml (团队共享, git 里有)
235
- 回退: ~/.qoderwork/.mcp-mysql-env.json (旧版个人文件)。"""
236
- server = repo / '.qoder' / 'scripts' / 'mysql_mcp_server.py'
237
- if not server.is_file():
238
- _log(f'mysql server 不存在: {server}')
239
- _launch_disabled_mcp('qoder-mysql',
240
- 'mysql_mcp_server.py 缺失。跑 npx @hupan56/wlkj update')
241
- return
242
- # 优先: 从 config.yaml 读 (团队共享, clone 就有)
243
- cfg_path = repo / '.qoder' / 'config.yaml'
244
- mysql_cfg = {}
245
- if cfg_path.is_file():
246
- try:
247
- import yaml
248
- d = yaml.safe_load(cfg_path.read_text(encoding='utf-8')) or {}
249
- mysql_cfg = d.get('mysql', {}) or {}
250
- except Exception:
251
- pass
252
- if mysql_cfg:
253
- for k in ('host', 'port', 'user', 'password'):
254
- v = mysql_cfg.get(k)
255
- if v:
256
- os.environ['MYSQL_' + k.upper()] = str(v)
257
- if mysql_cfg.get('env_name'):
258
- os.environ['MYSQL_ENV_NAME'] = str(mysql_cfg['env_name'])
259
- _log(f'mysql env 从 config.yaml 注入 ({len(mysql_cfg)} 项)')
260
- else:
261
- # 回退: 旧版个人 env 文件 (~/.qoderwork/.mcp-mysql-env.json)
262
- env_file = _home() / '.qoderwork' / '.mcp-mysql-env.json'
263
- if env_file.is_file():
264
- try:
265
- cfg = json.loads(env_file.read_text(encoding='utf-8'))
266
- for k, v in cfg.items():
267
- if v:
268
- os.environ[k] = str(v)
269
- _log(f'mysql env 从个人文件注入 ({len(cfg)} 项)')
270
- except (json.JSONDecodeError, OSError):
271
- pass
272
- else:
273
- _log('⚠️ mysql 配置缺失: config.yaml 无 mysql 段, 且 ~/.qoderwork/.mcp-mysql-env.json 不存在')
274
- _log(f'启动 mysql')
275
- py = _find_python_with_deps('pymysql')
276
- if not py:
277
- _log('⚠️ 找不到装了 pymysql 的 Python! 跑: pip install pymysql')
278
- _launch_disabled_mcp('qoder-mysql',
279
- 'pymysql 未装。跑 pip install pymysql 或 npx @hupan56/wlkj install-env')
280
- return
281
- _log(f' python={py}')
282
- os.execv(py, [py, str(server)])
283
-
284
-
285
- ZENTAO_URL_DEFAULT = 'http://10.89.2.91/zentao'
286
-
287
-
288
- def launch_zentao(repo):
289
- """启动禅道 MCP。连接信息从 config.yaml (团队共享)。"""
290
- server = repo / '.qoder' / 'scripts' / 'zentao_mcp_server.py'
291
- if not server.is_file():
292
- _log(f'zentao server 不存在: {server}')
293
- _launch_disabled_mcp('qoder-zentao', 'zentao_mcp_server.py 缺失')
294
- return
295
- # 从 config.yaml 读禅道配置
296
- cfg_path = repo / '.qoder' / 'config.yaml'
297
- if cfg_path.is_file():
298
- try:
299
- import yaml
300
- d = yaml.safe_load(cfg_path.read_text(encoding='utf-8')) or {}
301
- zt = d.get('zentao', {}) or {}
302
- if zt:
303
- os.environ['ZENTAO_URL'] = str(zt.get('url', ZENTAO_URL_DEFAULT))
304
- os.environ['ZENTAO_USER'] = str(zt.get('user', ''))
305
- os.environ['ZENTAO_PASSWORD'] = str(zt.get('password', ''))
306
- _log(f'zentao env 从 config.yaml 注入')
307
- except Exception:
308
- pass
309
- _log(f'启动 zentao')
310
- py = _find_python_with_deps('requests')
311
- if not py:
312
- _log('⚠️ 找不到装了 requests 的 Python!')
313
- _launch_disabled_mcp('qoder-zentao', 'requests 未装。pip install requests')
314
- return
315
- _log(f' python={py}')
316
- os.execv(py, [py, str(server)])
317
-
318
-
319
- def launch_lanhu(repo):
320
- """启动蓝湖 MCP。用 venv 的 3.10+ python (fastmcp 要求), 读角色 cookie。
321
- 没装 lanhu venv 或没配 cookie 时, 输出友好提示并启动一个空壳 MCP
322
- (让 QoderWork 显示"未启用"而不是黄/报错)。"""
323
- venv = repo / 'external' / 'lanhu-mcp' / '.venv'
324
- server = repo / 'external' / 'lanhu-mcp' / 'lanhu_mcp_server.py'
325
-
326
- # 检查 venv
327
- if not venv.is_dir():
328
- _log('lanhu venv 未安装 — MCP 将以"未启用"模式启动')
329
- _launch_disabled_mcp('lanhu',
330
- '蓝湖 MCP 未安装。跑 python .qoder/scripts/setup_lanhu.py 安装')
331
- return
332
- py = _venv_python(venv)
333
- if not py.is_file():
334
- _log(f'lanhu venv python 不存在: {py}')
335
- _launch_disabled_mcp('lanhu', '蓝湖 MCP venv 异常, 重装: setup_lanhu.py')
336
- return
337
- if not server.is_file():
338
- _log(f'lanhu server 不存在: {server}')
339
- _launch_disabled_mcp('lanhu', '蓝湖 MCP server 文件缺失')
340
- return
341
-
342
- # 读角色 cookie
343
- dev = _read_current_developer(repo)
344
- if not dev:
345
- _log('未初始化开发者 (.qoder/.developer), 跑 /wl-init')
346
- _launch_disabled_mcp('lanhu', '未初始化开发者, 跑 /wl-init')
347
- return
348
- cookie = _read_lanhu_cookie(repo, dev)
349
- if not cookie:
350
- _log(f'角色 {dev} 的蓝湖 cookie 未配置 (占位/缺失)')
351
- _log(f' 编辑 workspace/members/{dev}/.secrets/lanhu.env')
352
- _launch_disabled_mcp('lanhu',
353
- f'蓝湖 cookie 未配置: workspace/members/{dev}/.secrets/lanhu.env')
354
- return
355
-
356
- # 注入 env (角色隔离: cookie + 用户名都是当前角色的)
357
- os.environ['LANHU_COOKIE'] = cookie
358
- os.environ['MCP_TRANSPORT'] = 'stdio'
359
- os.environ['LANHU_USER_NAME'] = dev or 'Developer'
360
- os.environ['LANHU_USER_ROLE'] = 'Developer'
361
- _log(f'启动 lanhu (角色={dev}, python={py})')
362
- os.execv(str(py), [str(py), str(server)])
363
-
364
-
365
- LAUNCHERS = {
366
- 'kg': launch_kg,
367
- 'knowledge-graph': launch_kg,
368
- 'mysql': launch_mysql,
369
- 'zentao': launch_zentao,
370
- 'lanhu': launch_lanhu,
371
- }
372
-
373
-
374
- def main():
375
- # ⚠️ 关键: 任何失败都走 _launch_disabled_mcp(), 绝不 sys.exit()。
376
- # sys.exit(1) 会让进程退出 → QoderWork 显示黄色(连接错误), 用户以为坏了。
377
- # _launch_disabled_mcp() 启动一个干净 stdio 循环响应 initialize →
378
- # QoderWork 显示绿色(已连接, 0工具), 用户知道是"未启用"而非"坏了"。
379
- if len(sys.argv) < 2:
380
- _log('用法: mcp_launcher.py <kg|mysql|zentao|lanhu>')
381
- _log(f' argv={sys.argv}')
382
- _launch_disabled_mcp('qoder-mcp',
383
- '启动参数缺失: mcp_launcher.py <kg|mysql|zentao|lanhu>')
384
- return
385
- name = sys.argv[1].lower()
386
-
387
- repo = find_repo_root()
388
- if not repo:
389
- _log('无法定位 QODER 仓库根。')
390
- _log(' 方式1: 设环境变量 QODER_REPO=<仓库根>')
391
- _log(' 方式2: 重跑 npx @hupan56/wlkj update (刷新 .repo-root)')
392
- _log(' 方式3: 在仓库目录内运行')
393
- _launch_disabled_mcp(name,
394
- '未定位到 QODER 仓库根。跑 npx @hupan56/wlkj update')
395
- return
396
- _log(f'仓库根: {repo}')
397
-
398
- fn = LAUNCHERS.get(name)
399
- if not fn:
400
- _log(f'未知 MCP 名: {name} (支持: {list(LAUNCHERS)})')
401
- _launch_disabled_mcp(name,
402
- '未知 MCP 名: %s (支持: %s)' % (name, ', '.join(LAUNCHERS)))
403
- return
404
- try:
405
- fn(repo)
406
- except Exception as e:
407
- # 启动函数抛异常也不能让进程死 (死=黄, 捕获=绿)
408
- _log(f'启动 {name} 时异常: {e}')
409
- _launch_disabled_mcp(name,
410
- '%s MCP 启动异常: %s。跑 npx @hupan56/wlkj update' % (name, e))
411
-
412
-
413
- if __name__ == '__main__':
414
- main()
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 / lanhu)
28
+ 2. find_repo_root(): 环境变量 QODER_REPO > ~/.qoderwork/.repo-root 文件 > 向上找 .qoder/
29
+ 3. 按 MCP 名选 python + server 脚本:
30
+ - kg: sys.executable, <repo>/.qoder/scripts/kg_mcp_server.py
31
+ - mysql: sys.executable, <repo>/.qoder/scripts/mysql_mcp_server.py (env 从 .mcp-mysql-env.json 读)
32
+ - lanhu: <repo>/external/lanhu-mcp/.venv python (3.10+), 复用 lanhu_stdio_wrapper 逻辑 (读角色 cookie)
33
+ 4. os.execv 启动 (接管进程, QoderWork 管生死)
34
+
35
+ 跨平台:
36
+ find_repo_root 用 os.path, venv python 用 _venv_python (win Scripts/mac bin)
37
+ 本脚本被 python3/py/python 跑都行 (它只做定位+execv, 不依赖特定版本)
38
+ """
39
+
40
+ import json
41
+ import os
42
+ import sys
43
+ from pathlib import Path
44
+
45
+ try:
46
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
47
+ except (AttributeError, TypeError, OSError):
48
+ pass
49
+
50
+
51
+ def _home():
52
+ """跨平台 home 目录。"""
53
+ return Path(os.environ.get('USERPROFILE') or os.path.expanduser('~'))
54
+
55
+
56
+ def _log(msg):
57
+ """调试日志到 stderr (MCP stdio stdout, 不能污染)。"""
58
+ sys.stderr.write('[mcp-launcher] ' + msg + '\n')
59
+
60
+
61
+ def _launch_disabled_mcp(name, reason):
62
+ """没装/没配时, 启动一个空壳 MCP (只回 initialize + tools/list 空)。
63
+ QoderWork 显示绿(已连接)但无工具, 而不是黄(报错)。
64
+ reason 写进 serverInfo 让用户知道为什么没工具。"""
65
+ import json as _j
66
+ _log(f'{name} MCP 以"未启用"模式启动: {reason}')
67
+ # 简单的 stdio 循环, 只响应 initialize 和 tools/list
68
+ while True:
69
+ try:
70
+ line = sys.stdin.buffer.readline()
71
+ except (EOFError, OSError):
72
+ break
73
+ if not line:
74
+ break
75
+ try:
76
+ req = _j.loads(line.decode('utf-8'))
77
+ except Exception:
78
+ continue
79
+ method = req.get('method', '')
80
+ rid = req.get('id')
81
+ if method == 'initialize':
82
+ sys.stdout.write(_j.dumps({
83
+ "jsonrpc": "2.0", "id": rid,
84
+ "result": {
85
+ "protocolVersion": "2024-11-05",
86
+ "capabilities": {"tools": {}},
87
+ "serverInfo": {"name": name, "version": "0.0.0-disabled",
88
+ "reason": reason}
89
+ }
90
+ }) + '\n')
91
+ sys.stdout.flush()
92
+ elif method == 'tools/list':
93
+ sys.stdout.write(_j.dumps({
94
+ "jsonrpc": "2.0", "id": rid,
95
+ "result": {"tools": []}
96
+ }) + '\n')
97
+ sys.stdout.flush()
98
+ elif method == 'notifications/initialized':
99
+ pass
100
+ elif rid is not None:
101
+ sys.stdout.write(_j.dumps({
102
+ "jsonrpc": "2.0", "id": rid,
103
+ "error": {"code": -32601, "message": reason}
104
+ }) + '\n')
105
+ sys.stdout.flush()
106
+
107
+
108
+ def _find_python_with_deps(required_module='duckdb'):
109
+ """找一个装了指定依赖的 Python。优先级:
110
+ 1. sys.executable (当前 Python, 先测它有没有)
111
+ 2. 常见 Python 路径 (anaconda/python3等) 逐个测
112
+ 返回 python 路径(str) 或 None。
113
+ 解决"py -3 选的 Python 没装 duckdb"问题。
114
+ """
115
+ # 1. 当前 Python 有没有
116
+ try:
117
+ __import__(required_module)
118
+ return sys.executable
119
+ except ImportError:
120
+ pass
121
+ # 2. 找其它 Python
122
+ import shutil
123
+ candidates = []
124
+ for name in ('python', 'python3', 'py'):
125
+ p = shutil.which(name)
126
+ if p and p != sys.executable:
127
+ candidates.append(p)
128
+ # 常见安装路径 (Windows)
129
+ if sys.platform == 'win32':
130
+ for p in [
131
+ os.path.join(os.environ.get('USERPROFILE', ''), 'Anaconda3', 'python.exe'),
132
+ os.path.join(os.environ.get('USERPROFILE', ''), 'anaconda3', 'python.exe'),
133
+ os.path.join(os.environ.get('USERPROFILE', ''), 'miniconda3', 'python.exe'),
134
+ r'C:\ProgramData\Anaconda3\python.exe',
135
+ ]:
136
+ if os.path.isfile(p) and p not in candidates:
137
+ candidates.append(p)
138
+ else:
139
+ # macOS / Linux: 补 conda 路径候选 (治"Mac+conda 必爆"隐患)
140
+ # 场景: Mac 同事把 duckdb 装进 conda base, 而 which python3 指向系统 python
141
+ # → 之前探测不到 conda python, KG MCP 静默降级
142
+ home = os.path.expanduser('~')
143
+ for p in [
144
+ '/opt/anaconda3/bin/python',
145
+ '/opt/miniconda3/bin/python',
146
+ '/usr/local/anaconda3/bin/python',
147
+ os.path.join(home, 'anaconda3', 'bin', 'python'),
148
+ os.path.join(home, 'miniconda3', 'bin', 'python'),
149
+ os.path.join(home, 'opt', 'anaconda3', 'bin', 'python'),
150
+ os.path.join(home, 'opt', 'miniconda3', 'bin', 'python'),
151
+ '/opt/homebrew/bin/python3', # Apple Silicon Homebrew
152
+ '/usr/local/bin/python3', # Intel Mac Homebrew
153
+ ]:
154
+ if os.path.isfile(p) and p not in candidates:
155
+ candidates.append(p)
156
+ for py in candidates:
157
+ try:
158
+ r = __import__('subprocess').run(
159
+ [py, '-c', 'import %s' % required_module],
160
+ capture_output=True, timeout=5)
161
+ if r.returncode == 0:
162
+ _log(f'找到装了 {required_module} 的 Python: {py}')
163
+ return py
164
+ except Exception:
165
+ continue
166
+ return None
167
+
168
+
169
+ def find_repo_root():
170
+ """定位 QODER 仓库根。优先级:
171
+ 1. 环境变量 QODER_REPO (用户显式设, 最可靠)
172
+ 2. ~/.qoderwork/.repo-root 文件 (install 时写)
173
+ 3. 向上找 .qoder/scripts/ (cwd 在仓库内, IDE/CLI 场景)
174
+ 4. 本脚本若在 <repo>/.qoder/scripts/ 下 → 上三级 (开发态)
175
+ 返回 Path 或 None。
176
+ """
177
+ # 1. 环境变量
178
+ env_repo = os.environ.get('QODER_REPO')
179
+ if env_repo and (Path(env_repo) / '.qoder' / 'scripts').is_dir():
180
+ return Path(env_repo)
181
+
182
+ # 2. ~/.qoderwork/.repo-root 文件 (install 写的, 主路径)
183
+ anchor = _home() / '.qoderwork' / '.repo-root'
184
+ if anchor.is_file():
185
+ try:
186
+ line = anchor.read_text(encoding='utf-8').strip()
187
+ if line and (Path(line) / '.qoder' / 'scripts').is_dir():
188
+ return Path(line)
189
+ except OSError:
190
+ pass
191
+
192
+ # 3. 向上找 .qoder/scripts/
193
+ here = Path(os.getcwd()).resolve()
194
+ for _ in range(8):
195
+ if (here / '.qoder' / 'scripts').is_dir():
196
+ return here
197
+ parent = here.parent
198
+ if parent == here:
199
+ break
200
+ here = parent
201
+
202
+ # 4. 本脚本位置 (开发态: 在 <repo>/.qoder/scripts/mcp_launcher.py)
203
+ try:
204
+ s = Path(__file__).resolve()
205
+ root = s.parent.parent.parent # scripts/ -> .qoder/ -> repo
206
+ if (root / '.qoder' / 'scripts').is_dir():
207
+ return root
208
+ except Exception:
209
+ pass
210
+
211
+ return None
212
+
213
+
214
+ def _venv_python(venv):
215
+ """venv python 路径 (跨平台: win Scripts/python.exe, mac bin/python)。"""
216
+ if sys.platform == 'win32':
217
+ return venv / 'Scripts' / 'python.exe'
218
+ return venv / 'bin' / 'python'
219
+
220
+
221
+ def _read_current_developer(repo):
222
+ """读 .qoder/.developer 当前角色。"""
223
+ try:
224
+ sys.path.insert(0, str(repo / '.qoder' / 'scripts'))
225
+ from common.paths import get_developer
226
+ return get_developer(repo)
227
+ except Exception:
228
+ return None
229
+
230
+
231
+ def _read_lanhu_cookie(repo, dev):
232
+ """读角色的蓝湖 cookie。"""
233
+ try:
234
+ sys.path.insert(0, str(repo / '.qoder' / 'scripts'))
235
+ from setup_lanhu import _read_env_cookie, lanhu_env_file, COOKIE_PLACEHOLDER
236
+ ck = _read_env_cookie(dev)
237
+ return ck if (ck and ck != COOKIE_PLACEHOLDER) else None
238
+ except Exception:
239
+ return None
240
+
241
+
242
+ def launch_kg(repo):
243
+ """启动知识图谱 MCP。找装了 duckdb Python (不是盲目用 sys.executable)。"""
244
+ server = repo / '.qoder' / 'scripts' / 'mcp' / 'kg_mcp_server.py'
245
+ if not server.is_file():
246
+ _log(f'kg server 不存在: {server}')
247
+ _launch_disabled_mcp('qoder-knowledge-graph',
248
+ 'kg_mcp_server.py 缺失。跑 npx @hupan56/wlkj update')
249
+ return
250
+ py = _find_python_with_deps('duckdb')
251
+ if not py:
252
+ _log('⚠️ 找不到装了 duckdb 的 Python! 跑: pip install duckdb')
253
+ _log(' 或: npx @hupan56/wlkj install-env (自动装依赖)')
254
+ _launch_disabled_mcp('qoder-knowledge-graph',
255
+ 'duckdb 未装。跑 npx @hupan56/wlkj install-env (自动装依赖)')
256
+ return
257
+ _log(f'启动 kg (python={py})')
258
+ os.execv(py, [py, str(server)])
259
+
260
+
261
+ def launch_mysql(repo):
262
+ """启动 MySQL MCP。连接信息优先从 config.yaml (团队共享, git 里有)。
263
+ 回退: ~/.qoderwork/.mcp-mysql-env.json (旧版个人文件)。"""
264
+ server = repo / '.qoder' / 'scripts' / 'mcp' / 'mysql_mcp_server.py' # v3.0: mcp子包
265
+ if not server.is_file():
266
+ _log(f'mysql server 不存在: {server}')
267
+ _launch_disabled_mcp('qoder-mysql',
268
+ 'mysql_mcp_server.py 缺失。跑 npx @hupan56/wlkj update')
269
+ return
270
+ # 优先: 从 config.yaml 读 (团队共享, clone 就有)
271
+ cfg_path = repo / '.qoder' / 'config.yaml'
272
+ mysql_cfg = {}
273
+ if cfg_path.is_file():
274
+ try:
275
+ import yaml
276
+ d = yaml.safe_load(cfg_path.read_text(encoding='utf-8')) or {}
277
+ mysql_cfg = d.get('mysql', {}) or {}
278
+ except Exception:
279
+ pass
280
+ if mysql_cfg:
281
+ for k in ('host', 'port', 'user', 'password'):
282
+ v = mysql_cfg.get(k)
283
+ if v:
284
+ os.environ['MYSQL_' + k.upper()] = str(v)
285
+ if mysql_cfg.get('env_name'):
286
+ os.environ['MYSQL_ENV_NAME'] = str(mysql_cfg['env_name'])
287
+ _log(f'mysql env 从 config.yaml 注入 ({len(mysql_cfg)} 项)')
288
+ else:
289
+ # 回退: 旧版个人 env 文件 (~/.qoderwork/.mcp-mysql-env.json)
290
+ env_file = _home() / '.qoderwork' / '.mcp-mysql-env.json'
291
+ if env_file.is_file():
292
+ try:
293
+ cfg = json.loads(env_file.read_text(encoding='utf-8'))
294
+ for k, v in cfg.items():
295
+ if v:
296
+ os.environ[k] = str(v)
297
+ _log(f'mysql env 从个人文件注入 ({len(cfg)} 项)')
298
+ except (json.JSONDecodeError, OSError):
299
+ pass
300
+ else:
301
+ _log('⚠️ mysql 配置缺失: config.yaml mysql 段, 且 ~/.qoderwork/.mcp-mysql-env.json 不存在')
302
+ _log(f'启动 mysql')
303
+ py = _find_python_with_deps('pymysql')
304
+ if not py:
305
+ _log('⚠️ 找不到装了 pymysql 的 Python! 跑: pip install pymysql')
306
+ _launch_disabled_mcp('qoder-mysql',
307
+ 'pymysql 未装。跑 pip install pymysql 或 npx @hupan56/wlkj install-env')
308
+ return
309
+ _log(f' python={py}')
310
+ os.execv(py, [py, str(server)])
311
+
312
+
313
+ ZENTAO_URL_DEFAULT = 'http://10.89.2.91/zentao'
314
+
315
+
316
+ def launch_zentao(repo):
317
+ """启动禅道 MCP。连接信息从 config.yaml 读(团队共享)。"""
318
+ server = repo / '.qoder' / 'scripts' / 'mcp' / 'zentao_mcp_server.py' # v3.0: mcp子包
319
+ if not server.is_file():
320
+ _log(f'zentao server 不存在: {server}')
321
+ _launch_disabled_mcp('qoder-zentao', 'zentao_mcp_server.py 缺失')
322
+ return
323
+ # config.yaml 读禅道配置
324
+ cfg_path = repo / '.qoder' / 'config.yaml'
325
+ if cfg_path.is_file():
326
+ try:
327
+ import yaml
328
+ d = yaml.safe_load(cfg_path.read_text(encoding='utf-8')) or {}
329
+ zt = d.get('zentao', {}) or {}
330
+ if zt:
331
+ os.environ['ZENTAO_URL'] = str(zt.get('url', ZENTAO_URL_DEFAULT))
332
+ os.environ['ZENTAO_USER'] = str(zt.get('user', ''))
333
+ os.environ['ZENTAO_PASSWORD'] = str(zt.get('password', ''))
334
+ _log(f'zentao env config.yaml 注入')
335
+ except Exception:
336
+ pass
337
+ _log(f'启动 zentao')
338
+ py = _find_python_with_deps('requests')
339
+ if not py:
340
+ _log('⚠️ 找不到装了 requests 的 Python!')
341
+ _launch_disabled_mcp('qoder-zentao', 'requests 未装。pip install requests')
342
+ return
343
+ _log(f' python={py}')
344
+ os.execv(py, [py, str(server)])
345
+
346
+
347
+ def launch_lanhu(repo):
348
+ """启动蓝湖 MCP。用 venv 的 3.10+ python (fastmcp 要求), 读角色 cookie。
349
+ 没装 lanhu venv 或没配 cookie 时, 输出友好提示并启动一个空壳 MCP
350
+ ( QoderWork 显示"未启用"而不是黄/报错)。"""
351
+ venv = repo / 'external' / 'lanhu-mcp' / '.venv'
352
+ server = repo / 'external' / 'lanhu-mcp' / 'lanhu_mcp_server.py'
353
+
354
+ # 检查 venv
355
+ if not venv.is_dir():
356
+ _log('lanhu venv 未安装 MCP 将以"未启用"模式启动')
357
+ _launch_disabled_mcp('lanhu',
358
+ '蓝湖 MCP 未安装。跑 python .qoder/scripts/setup_lanhu.py 安装')
359
+ return
360
+ py = _venv_python(venv)
361
+ if not py.is_file():
362
+ _log(f'lanhu venv python 不存在: {py}')
363
+ _launch_disabled_mcp('lanhu', '蓝湖 MCP venv 异常, 重装: setup_lanhu.py')
364
+ return
365
+ if not server.is_file():
366
+ _log(f'lanhu server 不存在: {server}')
367
+ _launch_disabled_mcp('lanhu', '蓝湖 MCP server 文件缺失')
368
+ return
369
+
370
+ # 读角色 cookie
371
+ dev = _read_current_developer(repo)
372
+ if not dev:
373
+ _log('未初始化开发者 (.qoder/.developer), 跑 /wl-init')
374
+ _launch_disabled_mcp('lanhu', '未初始化开发者, 跑 /wl-init')
375
+ return
376
+ cookie = _read_lanhu_cookie(repo, dev)
377
+ if not cookie:
378
+ _log(f'角色 {dev} 的蓝湖 cookie 未配置 (占位/缺失)')
379
+ _log(f' 编辑 workspace/members/{dev}/.secrets/lanhu.env')
380
+ _launch_disabled_mcp('lanhu',
381
+ f'蓝湖 cookie 未配置: workspace/members/{dev}/.secrets/lanhu.env')
382
+ return
383
+
384
+ # 注入 env (角色隔离: cookie + 用户名都是当前角色的)
385
+ os.environ['LANHU_COOKIE'] = cookie
386
+ os.environ['MCP_TRANSPORT'] = 'stdio'
387
+ os.environ['LANHU_USER_NAME'] = dev or 'Developer'
388
+ os.environ['LANHU_USER_ROLE'] = 'Developer'
389
+ _log(f'启动 lanhu (角色={dev}, python={py})')
390
+ os.execv(str(py), [str(py), str(server)])
391
+
392
+
393
+ LAUNCHERS = {
394
+ 'kg': launch_kg,
395
+ 'knowledge-graph': launch_kg,
396
+ 'mysql': launch_mysql,
397
+ 'zentao': launch_zentao,
398
+ 'lanhu': launch_lanhu,
399
+ }
400
+
401
+
402
+ def main():
403
+ # ⚠️ 关键: 任何失败都走 _launch_disabled_mcp(), 绝不 sys.exit()。
404
+ # sys.exit(1) 会让进程退出 → QoderWork 显示黄色(连接错误), 用户以为坏了。
405
+ # _launch_disabled_mcp() 启动一个干净 stdio 循环响应 initialize →
406
+ # QoderWork 显示绿色(已连接, 0工具), 用户知道是"未启用"而非"坏了"。
407
+ if len(sys.argv) < 2:
408
+ _log('用法: mcp_launcher.py <kg|mysql|zentao|lanhu>')
409
+ _log(f' argv={sys.argv}')
410
+ _launch_disabled_mcp('qoder-mcp',
411
+ '启动参数缺失: mcp_launcher.py <kg|mysql|zentao|lanhu>')
412
+ return
413
+ name = sys.argv[1].lower()
414
+
415
+ repo = find_repo_root()
416
+ if not repo:
417
+ _log('无法定位 QODER 仓库根。')
418
+ _log(' 方式1: 设环境变量 QODER_REPO=<仓库根>')
419
+ _log(' 方式2: 重跑 npx @hupan56/wlkj update (刷新 .repo-root)')
420
+ _log(' 方式3: 在仓库目录内运行')
421
+ _launch_disabled_mcp(name,
422
+ '未定位到 QODER 仓库根。跑 npx @hupan56/wlkj update')
423
+ return
424
+ _log(f'仓库根: {repo}')
425
+
426
+ fn = LAUNCHERS.get(name)
427
+ if not fn:
428
+ _log(f'未知 MCP 名: {name} (支持: {list(LAUNCHERS)})')
429
+ _launch_disabled_mcp(name,
430
+ '未知 MCP 名: %s (支持: %s)' % (name, ', '.join(LAUNCHERS)))
431
+ return
432
+ try:
433
+ fn(repo)
434
+ except Exception as e:
435
+ # 启动函数抛异常也不能让进程死 (死=黄, 捕获=绿)
436
+ _log(f'启动 {name} 时异常: {e}')
437
+ _launch_disabled_mcp(name,
438
+ '%s MCP 启动异常: %s。跑 npx @hupan56/wlkj update' % (name, e))
439
+
440
+
441
+ if __name__ == '__main__':
442
+ main()