@hupan56/wlkj 2.7.12 → 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 +344 -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} +113 -4
  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,963 +1,973 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- setup_lanhu.py - 蓝湖 MCP 引导器 (幂等)
5
-
6
- 把第三方 dsphper/lanhu-mcp 引导成本机可用: clone + venv 依赖 + playwright + cookie + mcp.json 注册。
7
-
8
- 蓝湖 MCP 与我们自己的 kg_mcp_server.py 不同:
9
- - 它是 HTTP server (http://localhost:8000/mcp), 需要独立常驻启动
10
- (不像 kg_mcp_server.py QoderWork 通过 command/args 自动拉起)
11
- - cookie (LANHU_COOKIE) 在它的 .env 里, 必须用户登录 lanhuapp.com 后
12
- 从浏览器 DevTools 复制 (AI 拿不到)
13
- - QoderWork 的 mcp.json 用 url 字段注册 (Streamable HTTP), 不是 command/args
14
-
15
- 被谁调用:
16
- - setup.py Step 4.5: 主初始化流程
17
- - install_qoderwork.py: 技能安装时同步注册 mcp.json
18
- - 用户手动: python .qoder/scripts/setup_lanhu.py [--check|--uninstall]
19
-
20
- 用法:
21
- python .qoder/scripts/setup_lanhu.py # 引导 + 注册 (幂等)
22
- python .qoder/scripts/setup_lanhu.py --check # 只体检不改动
23
- python .qoder/scripts/setup_lanhu.py --uninstall # 删 external/lanhu-mcp/ + mcp.json 条目
24
- python .qoder/scripts/setup_lanhu.py --skip-install # 只注册 mcp.json, 不 clone/装依赖
25
-
26
- 幂等: 已 clone 跳过 clone; venv+依赖装过跳过; cookie 有效跳过; mcp.json 有 lanhu 跳过。
27
- """
28
-
29
- import argparse
30
- import json
31
- import os
32
- import shutil
33
- import socket
34
- import subprocess
35
- import sys
36
- from pathlib import Path
37
-
38
- try:
39
- sys.stdout.reconfigure(encoding='utf-8', errors='replace')
40
- sys.stderr.reconfigure(encoding='utf-8', errors='replace')
41
- except (AttributeError, TypeError, OSError):
42
- pass
43
-
44
- THIS_DIR = Path(__file__).resolve().parent
45
- BASE = THIS_DIR.parent.parent # repo root
46
-
47
- LANHU_REPO = 'https://github.com/dsphper/lanhu-mcp.git'
48
- EXTERNAL_DIR = BASE / 'external'
49
- LANHU_DIR = EXTERNAL_DIR / 'lanhu-mcp'
50
- LANHU_VENV = LANHU_DIR / '.venv'
51
- LANHU_SERVER_SCRIPT = LANHU_DIR / 'lanhu_mcp_server.py'
52
- COOKIE_PLACEHOLDER = 'PUT_YOUR_COOKIE_HERE'
53
- LANHU_PORT = 8000
54
- LANHU_MCP_PATH = '/mcp'
55
-
56
- # QoderWork mcp.json 位置
57
- if sys.platform == 'win32':
58
- _HOME = Path(os.environ.get('USERPROFILE', str(Path.home())))
59
- else:
60
- _HOME = Path.home()
61
- QODERWORK_MCP_FILE = _HOME / '.qoderwork' / 'mcp.json'
62
-
63
-
64
- def _current_developer():
65
- """读 .qoder/.developer 确定当前用户。没有返回 None。"""
66
- try:
67
- sys.path.insert(0, str(THIS_DIR))
68
- from common.paths import get_developer
69
- return get_developer(BASE)
70
- except Exception:
71
- return None
72
-
73
-
74
- def lanhu_secrets_dir(developer=None):
75
- """当前用户的私密信息目录: workspace/members/{dev}/.secrets/
76
-
77
- .developer 隔离: 一台机器多人切换时, 各自的 cookie 互不覆盖。
78
- 跟 .signing_key / autotest-data.yaml 同一套隔离机制。
79
- """
80
- dev = developer or _current_developer() or 'default'
81
- return BASE / 'workspace' / 'members' / dev / '.secrets'
82
-
83
-
84
- def lanhu_env_file(developer=None):
85
- """当前用户的蓝湖 cookie 文件: workspace/members/{dev}/.secrets/lanhu.env"""
86
- return lanhu_secrets_dir(developer) / 'lanhu.env'
87
-
88
-
89
- # ============================================================
90
- # 工具函数
91
- # ============================================================
92
-
93
- def ok(msg):
94
- print(f' [\u2713] {msg}') # ✓
95
-
96
-
97
- def warn(msg):
98
- print(f' [!] {msg}')
99
-
100
-
101
- def fail(msg):
102
- print(f' [\u2717] {msg}', file=sys.stderr)
103
-
104
-
105
- def ask(prompt, default='y'):
106
- """交互式 yes/no, 默认 default。非交互环境返回 default。"""
107
- if not sys.stdin.isatty():
108
- return default.lower() in ('y', 'yes')
109
- try:
110
- ans = input(f'{prompt} [{default}] ').strip().lower() or default
111
- return ans in ('y', 'yes')
112
- except (EOFError, KeyboardInterrupt):
113
- return default.lower() in ('y', 'yes')
114
-
115
-
116
- def run(cmd, **kwargs):
117
- """跑子进程, 返回 CompletedProcess。encoding 容错。"""
118
- kwargs.setdefault('capture_output', True)
119
- kwargs.setdefault('text', True)
120
- kwargs.setdefault('encoding', 'utf-8')
121
- kwargs.setdefault('errors', 'replace')
122
- return subprocess.run(cmd, **kwargs)
123
-
124
-
125
- def _venv_python(venv: Path) -> Path:
126
- """venv 内的 python 可执行路径 (跨平台)。"""
127
- if sys.platform == 'win32':
128
- return venv / 'Scripts' / 'python.exe'
129
- return venv / 'bin' / 'python'
130
-
131
-
132
- def _venv_pip(venv: Path) -> list:
133
- """venv 内的 pip 调用命令 (用 python -m pip, 更稳)。"""
134
- return [str(_venv_python(venv)), '-m', 'pip']
135
-
136
-
137
- # lanhu-mcp fastmcp 依赖需要 Python >= 3.10。
138
- # 不能盲用 sys.executable (本机可能是 3.9), 得找一个 3.10+ 的解释器建 venv。
139
- LANHU_MIN_PYTHON = (3, 10)
140
-
141
-
142
- def _python_version(exe):
143
- """返回解释器的 (major, minor) 元组, 探测失败返回 None。"""
144
- try:
145
- r = run([exe, '-c', 'import sys; print(sys.version_info[:2])'])
146
- if r.returncode == 0:
147
- import ast
148
- ver = ast.literal_eval(r.stdout.strip())
149
- if isinstance(ver, tuple) and len(ver) >= 2:
150
- return (ver[0], ver[1])
151
- except Exception:
152
- pass
153
- return None
154
-
155
-
156
- def find_python_310():
157
- """找一个 Python >= 3.10 的解释器路径, 优先级:
158
- 1. sys.executable (如果本身就 >= 3.10, 直接用, 最省事)
159
- 2. Windows py launcher: py -3.10 / -3.11 / -3.12 / -3.13
160
- 3. 常见命令: python3.10/3.11/3.12, python3
161
- 4. 兜底返回 sys.executable (让 pip 报错给用户看真原因)
162
- """
163
- # 1. 当前解释器够新
164
- if sys.version_info[:2] >= LANHU_MIN_PYTHON:
165
- return sys.executable
166
-
167
- # 2. Windows py launcher
168
- if sys.platform == 'win32':
169
- for v in ('3.13', '3.12', '3.11', '3.10'):
170
- launcher = 'py' if shutil.which('py') else ('py.exe' if shutil.which('py.exe') else None)
171
- if not launcher:
172
- break
173
- r = run([launcher, f'-{v}', '-c', 'import sys; print(sys.executable)'])
174
- if r.returncode == 0 and r.stdout.strip():
175
- exe = r.stdout.strip().strip('"')
176
- ver = _python_version(exe)
177
- if ver and ver >= LANHU_MIN_PYTHON:
178
- return exe
179
-
180
- # 3. 常见命令名 (跨平台)
181
- for cand in ('python3.13', 'python3.12', 'python3.11', 'python3.10',
182
- 'python3', 'python'):
183
- path = shutil.which(cand)
184
- if path:
185
- ver = _python_version(path)
186
- if ver and ver >= LANHU_MIN_PYTHON:
187
- return path
188
-
189
- # 4. 兜底
190
- return sys.executable
191
-
192
-
193
- # ============================================================
194
- # Step 1: clone / update lanhu-mcp
195
- # ============================================================
196
-
197
- def ensure_clone(dry=False, interactive=True):
198
- """clone lanhu-mcp 到 external/lanhu-mcp/, 已存在则 git pull 更新。
199
-
200
- 返回 (status, path): status in ('cloned', 'updated', 'ok', 'failed', 'would', 'absent')
201
- """
202
- print('\n--- 1. 蓝湖 MCP 源码 ---')
203
- if not shutil.which('git'):
204
- warn('git 未安装, 无法 clone lanhu-mcp (装 git 后重跑)')
205
- return ('failed', None)
206
-
207
- if not LANHU_DIR.is_dir():
208
- if dry:
209
- print(f' [MISS] 未 clone: {LANHU_DIR}')
210
- return ('would', None)
211
- EXTERNAL_DIR.mkdir(parents=True, exist_ok=True)
212
- print(f' git clone {LANHU_REPO} -> {LANHU_DIR}')
213
- r = run(['git', 'clone', '--depth', '1', LANHU_REPO, str(LANHU_DIR)])
214
- if r.returncode != 0:
215
- fail('clone 失败: ' + (r.stderr or '').strip()[:200])
216
- return ('failed', None)
217
- ok(f'已 clone: {LANHU_DIR}')
218
- return ('cloned', LANHU_DIR)
219
-
220
- # 已存在: 检查是否是有效 repo
221
- if not (LANHU_DIR / '.git').is_dir():
222
- warn(f'{LANHU_DIR} 存在但不是 git 仓库, 跳过更新 (手动处理)')
223
- return ('ok', LANHU_DIR)
224
-
225
- if dry:
226
- ok(f'已存在: {LANHU_DIR}')
227
- return ('ok', LANHU_DIR)
228
-
229
- # git pull 更新 (best-effort, 失败不阻塞)
230
- r = run(['git', '-C', str(LANHU_DIR), 'pull', '--ff-only'])
231
- if r.returncode == 0:
232
- ok(f'已更新: {LANHU_DIR}')
233
- return ('updated', LANHU_DIR)
234
- warn('git pull 失败 (本地可能有改动), 用现有版本继续: '
235
- + (r.stderr or '').strip()[:120])
236
- return ('ok', LANHU_DIR)
237
-
238
-
239
- # ============================================================
240
- # Step 2: venv + 依赖 + playwright chromium
241
- # ============================================================
242
-
243
- def ensure_deps(dry=False, _rebuilt=False):
244
- """建隔离 venv, requirements.txt + playwright chromium。
245
-
246
- 幂等: venv+依赖装过则跳过 (靠一个 marker 文件判断)
247
- _rebuilt: 内部防无限递归 (删旧 venv 重建只允许一次)。
248
- """
249
- print('\n--- 2. 蓝湖 MCP 依赖 ---')
250
- if not LANHU_DIR.is_dir():
251
- warn('lanhu-mcp 未 clone, 跳过依赖安装')
252
- return 'absent'
253
-
254
- marker = LANHU_DIR / '.deps-installed'
255
- req_file = LANHU_DIR / 'requirements.txt'
256
-
257
- # 已装过 (marker + requirements 没变) → 跳过
258
- if marker.is_file() and req_file.is_file():
259
- try:
260
- marker_mtime = marker.stat().st_mtime
261
- req_mtime = req_file.stat().st_mtime
262
- if marker_mtime > req_mtime:
263
- ok(f'依赖已装 (venv: {LANHU_VENV})')
264
- return 'ok'
265
- except OSError:
266
- pass
267
-
268
- if dry:
269
- print(f' [MISS] 依赖未装: {LANHU_VENV}')
270
- return 'would'
271
-
272
- # venv (用 3.10+ Python, fastmcp 要求)
273
- base_python = find_python_310()
274
- base_ver = _python_version(base_python)
275
- if not LANHU_VENV.is_dir():
276
- if base_ver and base_ver >= LANHU_MIN_PYTHON:
277
- print(f' 建 venv (Python {base_ver[0]}.{base_ver[1]}): '
278
- f'"{base_python}" -m venv {LANHU_VENV}')
279
- else:
280
- # 找不到 3.10+, 仍尝试建 (pip 会报错, 但给用户明确指引)
281
- warn(f'未找到 Python >= 3.10 (fastmcp 要求)。当前最佳: '
282
- f'{base_python} ({base_ver})')
283
- print(f' 建 venv: "{base_python}" -m venv {LANHU_VENV}')
284
- print(' ⚠ 预计 pip install 会失败 — 装 Python 3.10+ 后重跑')
285
- r = run([base_python, '-m', 'venv', str(LANHU_VENV)])
286
- if r.returncode != 0:
287
- fail('venv 创建失败: ' + (r.stderr or '').strip()[:200])
288
- return 'failed'
289
- elif base_ver and base_ver < LANHU_MIN_PYTHON and not _rebuilt:
290
- # venv 已存在但可能是旧 3.9 建的 (之前版本的 bug), 删了重建。
291
- # _rebuilt 防无限递归: 若本机没有 3.10+, 删了重建还是 3.9, 不再删第二次。
292
- venv_ver = _python_version(_venv_python(LANHU_VENV))
293
- if venv_ver and venv_ver < LANHU_MIN_PYTHON:
294
- warn(f'现有 venv Python {venv_ver[0]}.{venv_ver[1]} (fastmcp 3.10+), '
295
- f'删了重建')
296
- try:
297
- shutil.rmtree(LANHU_VENV)
298
- except OSError:
299
- pass
300
- return ensure_deps(dry=dry, _rebuilt=True)
301
-
302
- py = _venv_python(LANHU_VENV)
303
- if not py.is_file():
304
- fail(f'venv python 不存在: {py}')
305
- return 'failed'
306
-
307
- # 升级 pip
308
- print(' 升级 pip...')
309
- run(_venv_pip(LANHU_VENV) + ['install', '--upgrade', 'pip'])
310
-
311
- # 装 requirements.txt
312
- if req_file.is_file():
313
- print(f' pip install -r requirements.txt')
314
- r = run(_venv_pip(LANHU_VENV) + ['install', '-r', str(req_file)])
315
- if r.returncode != 0:
316
- fail('pip install requirements 失败: ' + (r.stderr or '').strip()[:200])
317
- return 'failed'
318
- ok('依赖已装')
319
- else:
320
- warn('requirements.txt 不存在, 跳过 (lanhu-mcp 目录结构可能变了)')
321
-
322
- # playwright install chromium (150-300MB, 一次性)
323
- print(' playwright install chromium (一次性, 约 150-300MB)...')
324
- r = run([str(py), '-m', 'playwright', 'install', 'chromium'])
325
- if r.returncode == 0:
326
- ok('chromium 已装')
327
- else:
328
- warn('playwright install chromium 失败 (可稍后手动跑: '
329
- f'{py} -m playwright install chromium)')
330
- warn(' 原因: ' + (r.stderr or '').strip()[:120])
331
- # 不阻塞: cookie/服务启动后 playwright 缺失会在用时才报错
332
-
333
- # marker
334
- try:
335
- marker.write_text('deps installed\n', encoding='utf-8')
336
- except OSError:
337
- pass
338
- return 'ok'
339
-
340
-
341
- # ============================================================
342
- # Step 3: cookie (按开发者隔离, workspace/members/{dev}/.secrets/lanhu.env)
343
- # ============================================================
344
- # 为什么不放 external/lanhu-mcp/.env:
345
- # 1. cookie 是个人登录凭证, 不该进仓库代码区 (即使 gitignore)
346
- # 2. 同一台机器多人切换 .developer 时, 单一 .env 会互相覆盖
347
- # 3. 跟 .signing_key / autotest-data.yaml 同一套隔离机制, 一致
348
- # 蓝湖服务端怎么读到这个 cookie:
349
- # lanhu_mcp_server.py 用 load_dotenv(override=False), 已有环境变量优先。
350
- # 所以启动时把 LANHU_COOKIE 作为环境变量注入即可 (见 start_lanhu_server)。
351
-
352
- def _read_env_cookie(developer=None):
353
- """读当前用户 lanhu.env 里的 LANHU_COOKIE, 没有返回 None。"""
354
- env_file = lanhu_env_file(developer)
355
- if not env_file.is_file():
356
- return None
357
- try:
358
- for line in env_file.read_text(encoding='utf-8', errors='replace').splitlines():
359
- line = line.strip()
360
- if line.startswith('LANHU_COOKIE') and '=' in line:
361
- val = line.split('=', 1)[1].strip().strip('"').strip("'")
362
- return val
363
- except OSError:
364
- pass
365
- return None
366
-
367
-
368
- def _write_env_cookie(cookie, developer=None):
369
- """把 LANHU_COOKIE 写进当前用户的 lanhu.env (保留其他行)
370
-
371
- 文件: workspace/members/{dev}/.secrets/lanhu.env ( gitignore)
372
- """
373
- env_file = lanhu_env_file(developer)
374
- try:
375
- env_file.parent.mkdir(parents=True, exist_ok=True)
376
- lines = []
377
- found = False
378
- if env_file.is_file():
379
- for line in env_file.read_text(encoding='utf-8', errors='replace').splitlines():
380
- if line.strip().startswith('LANHU_COOKIE') and '=' in line:
381
- lines.append(f'LANHU_COOKIE="{cookie}"')
382
- found = True
383
- else:
384
- lines.append(line)
385
- if not found:
386
- lines.append(f'LANHU_COOKIE="{cookie}"')
387
- env_file.write_text('\n'.join(lines) + '\n', encoding='utf-8')
388
- return True
389
- except OSError as e:
390
- fail(f' {env_file} 失败: {e}')
391
- return False
392
-
393
-
394
- def ensure_cookie(dry=False):
395
- """收 cookie 写 .env。已有真 cookie 跳过; 没有则交互式引导。
396
-
397
- 返回 status: 'ok' (有真 cookie) / 'placeholder' (占位) / 'would' / 'absent'
398
- """
399
- dev = _current_developer()
400
- env_file = lanhu_env_file(dev)
401
- print('\n--- 3. 蓝湖 Cookie ---')
402
- if dev:
403
- print(f' 当前开发者: {dev}')
404
- print(f' cookie 文件: {env_file} (按人隔离, 不进 git)')
405
- cookie = _read_env_cookie(dev)
406
- if cookie and cookie != COOKIE_PLACEHOLDER:
407
- ok(f'蓝湖 cookie 已配置 ({env_file.name})')
408
- return 'ok'
409
-
410
- if dry:
411
- if cookie == COOKIE_PLACEHOLDER:
412
- print(f' [WARN] {env_file.name} 里是占位 cookie, 需要换成真的')
413
- else:
414
- print(f' [MISS] 没有 LANHU_COOKIE ({env_file})')
415
- return 'would'
416
-
417
- # 交互式引导
418
- if sys.stdin.isatty():
419
- print()
420
- print(' 蓝湖 cookie 获取步骤:')
421
- print(' 1. 浏览器打开 https://lanhuapp.com 并登录')
422
- print(' 2. F12 打开开发者工具, 切到 Network 标签')
423
- print(' 3. 刷新页面 (F5), 点任意一个请求')
424
- print(' 4. Request Headers 里找 Cookie: 这一行, 复制【整行】')
425
- print(' (或 Application → Cookies → lanhuapp.com, 把所有项拼成')
426
- print(' name1=value1; name2=value2 格式)')
427
- print(' 必须【整行】, 不能只复制 user_token 一个!')
428
- print(' 实测: 只给 user_token 蓝湖 API 返回 418 (鉴权失败),')
429
- print(' 需要 user_token + session + 其他项的组合才认。')
430
- print()
431
- try:
432
- cookie = input(' 粘贴蓝湖 cookie (留空跳过, 之后编辑 lanhu.env): ').strip()
433
- except (EOFError, KeyboardInterrupt):
434
- cookie = ''
435
- if cookie and len(cookie) > 20:
436
- if _write_env_cookie(cookie, dev):
437
- ok(f'蓝湖 cookie 已写入 {env_file}')
438
- return 'ok'
439
- return 'placeholder'
440
- warn(f'未输入 cookie, 写入占位 (之后编辑 {env_file} 换成真的)')
441
- else:
442
- # 非交互环境 (AI 调用): 写占位
443
- warn('非交互环境, 无法收 cookie, 写入占位')
444
-
445
- if _write_env_cookie(COOKIE_PLACEHOLDER, dev):
446
- print(f' 占位已写入: {env_file}')
447
- print(f' 编辑该文件, {COOKIE_PLACEHOLDER} 换成真 cookie')
448
- return 'placeholder'
449
-
450
-
451
- # ============================================================
452
- # Step 4: mcp.json 注册 (url 模式)
453
- # ============================================================
454
-
455
- def lanhu_mcp_url(developer_name='Developer'):
456
- """生成蓝湖 MCP 的 url (HTTP Streamable, role/name 作为 query)。"""
457
- import urllib.parse
458
- name = urllib.parse.quote(developer_name or 'Developer')
459
- return f'http://localhost:{LANHU_PORT}{LANHU_MCP_PATH}?role=Developer&name={name}'
460
-
461
-
462
- def _detect_python_command():
463
- """探测本机可用的 python 命令 (跨平台)。win: py -3 > python; mac: python3。
464
- 复用 install_qoderwork 的逻辑, 但本文件独立实现避免循环依赖。"""
465
- for cand in (('py', ['-3']), ('python3', []), ('python', [])):
466
- name, extra = cand
467
- if not shutil.which(name):
468
- continue
469
- cmd = [name] + extra + ['--version']
470
- try:
471
- r = run(cmd)
472
- if r.returncode == 0:
473
- return ' '.join([name] + extra)
474
- except (OSError, subprocess.SubprocessError):
475
- continue
476
- return 'python'
477
-
478
-
479
- def _lanhu_stdio_entry():
480
- """构造 STDIO 模式的 mcp.json lanhu 条目 (launcher 模式, 不写死路径)。
481
-
482
- 用统一启动器 mcp_launcher.py: command=python命令名, args=[launcher, "lanhu"]。
483
- launcher 运行时自定位仓库根 + 找 venv python + 读角色 cookie。
484
- 这样 mcp.json 不含任何写死的仓库路径/venv路径, 跨机器通用。
485
- """
486
- py_cmd = _detect_python_command()
487
- # launcher 在 ~/.qoderwork/mcp_launcher.py (install 时拷)
488
- launcher = str(_HOME / '.qoderwork' / 'mcp_launcher.py')
489
- return {
490
- 'command': py_cmd,
491
- 'args': [launcher, 'lanhu'],
492
- }
493
-
494
-
495
- def _is_stdio_entry(entry):
496
- """判断 mcp.json 里 lanhu 条目是不是 launcher STDIO 格式 (args 含 mcp_launcher)。"""
497
- if not isinstance(entry, dict):
498
- return False
499
- args = entry.get('args', [])
500
- return 'command' in entry and any('mcp_launcher.py' in str(a) for a in args)
501
-
502
-
503
- def register_lanhu_mcp(developer_name='Developer', dry=False):
504
- """在 ~/.qoderwork/mcp.json 注册蓝湖 MCP (STDIO 模式, QoderWork 自动开关)。
505
-
506
- STDIO 模式: command 指向 wrapper, QoderWork 开自动拉起、关自动杀。
507
- cookie wrapper 运行时按角色读 .secrets, 不进 mcp.json (角色隔离)
508
-
509
- 迁移: 旧的 url 格式 (HTTP) → 升级成 STDIO。
510
- 幂等: 已是 STDIO 格式 跳过。
511
- 返回: 'ok-existing' / 'added' / 'upgraded' / 'would-add'
512
- """
513
- if not QODERWORK_MCP_FILE.is_file():
514
- if dry:
515
- return 'would-add'
516
- QODERWORK_MCP_FILE.parent.mkdir(parents=True, exist_ok=True)
517
-
518
- existing = {}
519
- if QODERWORK_MCP_FILE.is_file():
520
- try:
521
- existing = json.loads(QODERWORK_MCP_FILE.read_text(encoding='utf-8'))
522
- if not isinstance(existing, dict):
523
- existing = {}
524
- except (json.JSONDecodeError, OSError):
525
- existing = {}
526
-
527
- servers = existing.get('mcpServers', {})
528
- if not isinstance(servers, dict):
529
- servers = {}
530
-
531
- cur = servers.get('lanhu')
532
- stdio_entry = _lanhu_stdio_entry()
533
-
534
- if cur and _is_stdio_entry(cur):
535
- # 已是 STDIO, 但 command/args 可能因换机器变了 (venv 路径), 校准
536
- if cur.get('command') == stdio_entry['command'] and cur.get('args') == stdio_entry['args']:
537
- return 'ok-existing'
538
- # 路径漂移, 更新
539
- if dry:
540
- return 'would-add'
541
- servers['lanhu'] = stdio_entry
542
- existing['mcpServers'] = servers
543
- QODERWORK_MCP_FILE.write_text(
544
- json.dumps(existing, indent=2, ensure_ascii=False) + '\n',
545
- encoding='utf-8')
546
- return 'upgraded'
547
-
548
- if dry:
549
- return 'would-add'
550
-
551
- # 新增或从 url(HTTP) 升级到 STDIO
552
- was_http = bool(cur and 'url' in cur)
553
- servers['lanhu'] = stdio_entry
554
- existing['mcpServers'] = servers
555
- QODERWORK_MCP_FILE.write_text(
556
- json.dumps(existing, indent=2, ensure_ascii=False) + '\n',
557
- encoding='utf-8')
558
- return 'upgraded' if was_http else 'added'
559
-
560
-
561
- def unregister_lanhu_mcp():
562
- """从 mcp.json lanhu 条目 (best-effort)。"""
563
- if not QODERWORK_MCP_FILE.is_file():
564
- return False
565
- try:
566
- existing = json.loads(QODERWORK_MCP_FILE.read_text(encoding='utf-8'))
567
- servers = existing.get('mcpServers', {})
568
- if 'lanhu' in servers:
569
- del servers['lanhu']
570
- existing['mcpServers'] = servers
571
- QODERWORK_MCP_FILE.write_text(
572
- json.dumps(existing, indent=2, ensure_ascii=False) + '\n',
573
- encoding='utf-8',
574
- )
575
- return True
576
- except (json.JSONDecodeError, OSError):
577
- pass
578
- return False
579
-
580
-
581
- # ============================================================
582
- # 健康探测: 蓝湖 HTTP 服务是否在跑
583
- # ============================================================
584
-
585
- def is_lanhu_running():
586
- """探测 localhost:8000 是否在监听 (蓝湖服务是否启动)。"""
587
- try:
588
- with socket.create_connection(('127.0.0.1', LANHU_PORT), timeout=1):
589
- return True
590
- except (OSError, socket.timeout):
591
- return False
592
-
593
-
594
- # ============================================================
595
- # 打印启动指引
596
- # ============================================================
597
-
598
- def print_start_hint():
599
- """STDIO 模式下, 蓝湖由 QoderWork 自动管, 这个 hint 主要提示怎么验证。"""
600
- print('\n 蓝湖 MCP (STDIO 模式, QoderWork 自动开关):')
601
- print(' 开 QoderWork 自动起, 关自动停 — 无需手动 start')
602
- print(' 验证: 在 QoderWork 里 /wl-design-spec 录入 <蓝湖链接>, 能调通即正常')
603
- print(' 换角色: 改 .developer + 配新角色 cookie → 重启 QoderWork')
604
- print(' 调试/多客户端共享 (强制 HTTP): python .qoder/scripts/setup_lanhu.py start --http')
605
-
606
-
607
- # ============================================================
608
- # 启动蓝湖服务端 (读当前用户 cookie, 以环境变量注入)
609
- # ============================================================
610
-
611
- def start_lanhu_server(force_http=False):
612
- """启动蓝湖 MCP 服务。
613
-
614
- 默认 (STDIO 模式): 不需要手动 start, QoderWork 自动管。本函数只提示。
615
- force_http=True: 走旧 HTTP 模式 (前台常驻, 用于调试或多客户端共享)。
616
- """
617
- if not force_http:
618
- print('=' * 56)
619
- print(' 蓝湖 MCP 服务启动')
620
- print('=' * 56)
621
- # 检测当前 mcp.json 是不是 STDIO
622
- entry = None
623
- if QODERWORK_MCP_FILE.is_file():
624
- try:
625
- entry = json.loads(QODERWORK_MCP_FILE.read_text(encoding='utf-8')).get('mcpServers', {}).get('lanhu')
626
- except Exception:
627
- pass
628
- if _is_stdio_entry(entry) if entry else False:
629
- ok('已是 STDIO 模式 — QoderWork 自动开关, 无需手动 start')
630
- print(' QoderWork 自动拉起蓝湖 (wrapper 读当前角色 cookie), 关自动停')
631
- print(' 换角色: .developer + 配新角色 cookie → 重启 QoderWork')
632
- print('')
633
- print(' 要强制用 HTTP 模式 (调试/多客户端共享):')
634
- print(f' python .qoder/scripts/setup_lanhu.py start --http')
635
- return 0
636
- # 非 STDIO (旧 HTTP 或没注册), 提示升级
637
- warn('当前不是 STDIO 模式, 手动 start 只支持 HTTP')
638
- print(' 建议升级 STDIO (QoderWork 自动开关): python .qoder/scripts/setup_lanhu.py')
639
- print(' 或强制 HTTP: python .qoder/scripts/setup_lanhu.py start --http')
640
- return 1
641
-
642
- # === HTTP 模式 (force_http=True, 降级/调试用) ===
643
- print('=' * 56)
644
- print(' 启动蓝湖 MCP 服务端 (HTTP 模式, 降级/调试)')
645
- print('=' * 56)
646
-
647
- # 1. 源码 + venv 是否就绪
648
- if not LANHU_SERVER_SCRIPT.is_file():
649
- fail(f'蓝湖服务脚本不存在: {LANHU_SERVER_SCRIPT}')
650
- print(' 先跑: python .qoder/scripts/setup_lanhu.py')
651
- return 1
652
- py = _venv_python(LANHU_VENV)
653
- if not py.is_file():
654
- fail(f'venv python 不存在: {py}')
655
- print(' 先跑: python .qoder/scripts/setup_lanhu.py')
656
- return 1
657
-
658
- # 2. 当前用户 + cookie
659
- dev = _current_developer()
660
- env_file = lanhu_env_file(dev)
661
- env_cookie = os.environ.get('LANHU_COOKIE', '').strip()
662
- cookie_source = 'env' if env_cookie else 'file'
663
- cookie = env_cookie or _read_env_cookie(dev)
664
-
665
- print(f' 当前开发者: {dev or "(未初始化, 用 default)"}')
666
- if cookie_source == 'env':
667
- print(f' cookie 来源: 环境变量 LANHU_COOKIE (临时, 不落盘)')
668
- else:
669
- print(f' cookie 来源: {env_file} (主线, 落盘)')
670
-
671
- if not cookie or cookie == COOKIE_PLACEHOLDER:
672
- fail('cookie 未配置')
673
- if cookie_source == 'env':
674
- print(' 环境变量 LANHU_COOKIE 是空的或占位')
675
- else:
676
- print(f' 编辑 {env_file}, {COOKIE_PLACEHOLDER} 换成真 cookie')
677
- print(' 获取: lanhuapp.com 登录 F12 → Network → Cookie 头复制')
678
- return 1
679
- print(f' cookie: 已配置 (长度 {len(cookie)})')
680
-
681
- # 3. 已在跑就别重复启
682
- if is_lanhu_running():
683
- warn(f'蓝湖服务已在跑 (localhost:{LANHU_PORT}), 不重复启动')
684
- return 0
685
-
686
- # 4. 以环境变量注入 cookie 启动 (前台常驻, Ctrl+C 退出)
687
- print(f'\n 启动: {py} {LANHU_SERVER_SCRIPT}')
688
- print(f' HTTP: http://localhost:{LANHU_PORT}{LANHU_MCP_PATH}')
689
- print(' (常驻前台, 用完 Ctrl+C 关闭。别关这个终端。)\n')
690
- env = os.environ.copy()
691
- env['LANHU_COOKIE'] = cookie
692
- # 不捕获输出 —— 服务端日志直接打到当前终端, 用户能看到请求/报错
693
- try:
694
- return subprocess.call([str(py), str(LANHU_SERVER_SCRIPT)], env=env)
695
- except OSError as e:
696
- fail(f'启动失败: {e}')
697
- return 1
698
-
699
-
700
- # ============================================================
701
- # 主流程
702
- # ============================================================
703
-
704
- def configure_lanhu(developer_name='Developer', dry=False, skip_install=False):
705
- """完整引导: clone 依赖 → cookie → mcp.json 注册。
706
-
707
- 返回 dict 报告各步状态。
708
- """
709
- report = {'clone': 'skipped', 'deps': 'skipped',
710
- 'cookie': 'skipped', 'mcp': 'skipped'}
711
-
712
- if skip_install:
713
- print('\n--- 蓝湖 MCP (--skip-install, 只注册 mcp.json) ---')
714
- else:
715
- print('\n' + '=' * 56)
716
- print(' 蓝湖 MCP 引导')
717
- print('=' * 56)
718
-
719
- # 1. clone
720
- status, _ = ensure_clone(dry=dry)
721
- report['clone'] = status
722
- if status == 'failed':
723
- print('\n蓝湖源码获取失败, 后续步骤跳过。')
724
- return report
725
-
726
- # 2. 依赖
727
- report['deps'] = ensure_deps(dry=dry)
728
-
729
- # 3. cookie (即使 skip_install 也引导, 因为 mcp.json 注册后需要 cookie 才能用)
730
- if not skip_install:
731
- report['cookie'] = ensure_cookie(dry=dry)
732
-
733
- # 4. mcp.json 注册 (STDIO 模式, QoderWork 自动开关)
734
- print('\n--- 4. 注册到 QoderWork mcp.json (STDIO 自动开关) ---')
735
- status = register_lanhu_mcp(developer_name, dry=dry)
736
- report['mcp'] = status
737
- if status == 'ok-existing':
738
- ok(f'蓝湖 MCP 已注册 (STDIO): {QODERWORK_MCP_FILE}')
739
- print(' 开 QoderWork 自动起, 关自动停, 无需手动 start')
740
- elif status in ('added', 'upgraded'):
741
- tag = '已写入' if status == 'added' else '已升级(HTTP→STDIO)'
742
- ok(f'{tag} 蓝湖 MCP: {QODERWORK_MCP_FILE}')
743
- print(' STDIO 模式: QoderWork 自动起, 关自动停')
744
- print(f' command wrapper {developer_name} cookie 拉起 lanhu')
745
- elif status == 'would-add':
746
- print(f' [MISS] 未注册, 安装时会写入 {QODERWORK_MCP_FILE}')
747
-
748
- return report
749
-
750
-
751
- def print_check_report():
752
- """--check 模式: 体检报告 (不改动)。"""
753
- print('=' * 56)
754
- print(' 蓝湖 MCP 体检')
755
- print('=' * 56)
756
-
757
- # clone
758
- print('\n--- 1. 源码 ---')
759
- if LANHU_DIR.is_dir() and (LANHU_DIR / '.git').is_dir():
760
- ok(f'已 clone: {LANHU_DIR}')
761
- # 检查关键文件
762
- server_file = LANHU_DIR / 'lanhu_mcp_server.py'
763
- if server_file.is_file():
764
- ok(f'入口文件在: {server_file}')
765
- else:
766
- warn(f'入口文件缺失: {server_file} (lanhu-mcp 版本结构可能变了)')
767
- else:
768
- print(f' [MISS] clone (跑 setup.py 或本脚本安装)')
769
-
770
- # 依赖
771
- print('\n--- 2. 依赖 ---')
772
- if LANHU_VENV.is_dir() and _venv_python(LANHU_VENV).is_file():
773
- marker = LANHU_DIR / '.deps-installed'
774
- # 实测验证: 尝试导入 fastmcp (lanhu-mcp 的核心依赖, 缺它一定跑不起来)
775
- # marker 只是快速跳过标记, 真相以能否导入为准
776
- py = _venv_python(LANHU_VENV)
777
- r = run([str(py), '-c', 'import fastmcp; print(fastmcp.__version__)'])
778
- deps_importable = r.returncode == 0 and r.stdout.strip()
779
- if deps_importable:
780
- extra = f' (fastmcp {r.stdout.strip()})'
781
- if marker.is_file():
782
- ok(f'venv + 依赖已装: {LANHU_VENV}{extra}')
783
- else:
784
- ok(f'venv + 依赖已装 (marker 缺失但实测可导入): {LANHU_VENV}{extra}')
785
- print(f' (补写 marker 以便后续快速跳过)')
786
- try:
787
- marker.write_text('deps verified by import\n', encoding='utf-8')
788
- except OSError:
789
- pass
790
- elif marker.is_file():
791
- warn(f'marker 在但 fastmcp 导入失败, 依赖可能损坏: {LANHU_VENV}')
792
- print(f' 重装: python .qoder/scripts/setup_lanhu.py')
793
- else:
794
- warn(f'venv 存在但依赖未装完 (fastmcp 不可导入): {LANHU_VENV}')
795
- print(f' 重装: python .qoder/scripts/setup_lanhu.py')
796
- else:
797
- print(f' [MISS] venv 未建 (跑 setup.py 或本脚本安装)')
798
-
799
- # cookie (按开发者隔离)
800
- print('\n--- 3. Cookie ---')
801
- dev = _current_developer()
802
- env_file = lanhu_env_file(dev)
803
- if dev:
804
- print(f' 当前开发者: {dev}')
805
- cookie = _read_env_cookie(dev)
806
- if cookie and cookie != COOKIE_PLACEHOLDER:
807
- ok(f'蓝湖 cookie 已配置 ({env_file})')
808
- elif cookie == COOKIE_PLACEHOLDER:
809
- warn(f'占位 cookie ({COOKIE_PLACEHOLDER}), 需要换成真的')
810
- print(f' 编辑: {env_file}')
811
- else:
812
- print(f' [MISS] 没有 LANHU_COOKIE ({env_file})')
813
-
814
- # mcp.json (STDIO 模式检测)
815
- print('\n--- 4. QoderWork 注册 ---')
816
- if QODERWORK_MCP_FILE.is_file():
817
- try:
818
- cfg = json.loads(QODERWORK_MCP_FILE.read_text(encoding='utf-8'))
819
- servers = cfg.get('mcpServers', {})
820
- entry = servers.get('lanhu')
821
- if not entry:
822
- print(f' [MISS] mcp.json 没有 lanhu 条目')
823
- elif _is_stdio_entry(entry):
824
- launcher_ok = (_HOME / '.qoderwork' / 'mcp_launcher.py').is_file()
825
- ok(f'已注册 (STDIO launcher, 自动开关): command={entry.get("command","?")}')
826
- ok(f' launcher 存在: {"✓" if launcher_ok else "✗ 缺 ~/.qoderwork/mcp_launcher.py (重跑 install_qoderwork)"}')
827
- print(' 开 QoderWork 自动起, 关自动停 (无需手动 start)')
828
- elif 'command' in entry:
829
- # STDIO (写死 venv 路径) 或其他 command 格式
830
- warn(f'旧 STDIO 格式 (command 写死路径), 建议升级 launcher 模式')
831
- print(' 升级: python .qoder/scripts/install_qoderwork.py')
832
- else:
833
- warn(f'旧 HTTP 模式 (url={entry.get("url","?")[:50]}...), 需手动 start')
834
- print(' 升级 STDIO: python .qoder/scripts/setup_lanhu.py')
835
- except (json.JSONDecodeError, OSError):
836
- warn(f'mcp.json 解析失败: {QODERWORK_MCP_FILE}')
837
- else:
838
- print(f' [MISS] mcp.json 不存在: {QODERWORK_MCP_FILE}')
839
-
840
- # 服务状态 (STDIO 模式由 QoderWork 管, 不常驻端口; 这里只提示)
841
- print('\n--- 5. 服务状态 ---')
842
- entry = None
843
- if QODERWORK_MCP_FILE.is_file():
844
- try:
845
- entry = json.loads(QODERWORK_MCP_FILE.read_text(encoding='utf-8')).get('mcpServers', {}).get('lanhu')
846
- except Exception:
847
- pass
848
- if _is_stdio_entry(entry) if entry else False:
849
- ok('STDIO 模式: QoderWork 启动时自动拉起 (不占常驻端口)')
850
- print(' (要验证: QoderWork /wl-design-spec 录入 <蓝湖链接> 能调通即正常)')
851
- elif is_lanhu_running():
852
- ok(f'蓝湖服务在跑 (HTTP, localhost:{LANHU_PORT}) — 旧模式, 建议升级 STDIO')
853
- else:
854
- print(' STDIO 模式下服务由 QoderWork 按需拉起, 这里不显示常驻状态')
855
-
856
-
857
- def do_uninstall():
858
- """删 external/lanhu-mcp/ + mcp.json lanhu 条目。"""
859
- print('=' * 56)
860
- print(' 蓝湖 MCP 卸载')
861
- print('=' * 56)
862
-
863
- # 1. mcp.json 条目
864
- print('\n--- 1. 移除 mcp.json 条目 ---')
865
- if unregister_lanhu_mcp():
866
- ok(f'已从 mcp.json 删除 lanhu 条目')
867
- else:
868
- print(' mcp.json lanhu 条目 (无需删除)')
869
-
870
- # 2. external/lanhu-mcp/
871
- print('\n--- 2. 删除源码 ---')
872
- if LANHU_DIR.is_dir():
873
- if ask(f'删除 {LANHU_DIR}? (含 venv + chromium 缓存, 不可恢复)', 'n'):
874
- try:
875
- shutil.rmtree(LANHU_DIR)
876
- ok(f'已删除: {LANHU_DIR}')
877
- except OSError as e:
878
- fail(f'删除失败: {e}')
879
- else:
880
- print(' 跳过 (保留源码)')
881
- else:
882
- print(f' 源码目录不存在: {LANHU_DIR}')
883
-
884
- # 3. 清理空的 external/
885
- if EXTERNAL_DIR.is_dir():
886
- try:
887
- remaining = list(EXTERNAL_DIR.iterdir())
888
- if not remaining:
889
- EXTERNAL_DIR.rmdir()
890
- ok(f'已删除空目录: {EXTERNAL_DIR}')
891
- except OSError:
892
- pass
893
-
894
- print('\n✓ 蓝湖 MCP 卸载完成。')
895
-
896
-
897
- def main():
898
- parser = argparse.ArgumentParser(
899
- description='蓝湖 MCP 引导器: clone + 依赖 + cookie + mcp.json 注册',
900
- formatter_class=argparse.RawDescriptionHelpFormatter,
901
- epilog='''示例:
902
- python .qoder/scripts/setup_lanhu.py # 引导 + 注册 STDIO (幂等, 自动开关)
903
- python .qoder/scripts/setup_lanhu.py --check # 只体检
904
- python .qoder/scripts/setup_lanhu.py start # 提示 STDIO 状态 (默认无需手动)
905
- python .qoder/scripts/setup_lanhu.py start --http # 强制 HTTP 模式 (调试/多客户端)
906
- python .qoder/scripts/setup_lanhu.py --skip-install # 只注册 mcp.json
907
- python .qoder/scripts/setup_lanhu.py --uninstall # 卸载
908
- ''')
909
- parser.add_argument('command', nargs='?', default=None,
910
- help='子命令: start = 提示服务状态 (STDIO 自动, 加 --http 强制 HTTP)')
911
- parser.add_argument('--check', action='store_true', help='只体检不改动')
912
- parser.add_argument('--uninstall', action='store_true', help='卸载 (删源码 + mcp.json 条目)')
913
- parser.add_argument('--skip-install', action='store_true',
914
- help='只注册 mcp.json, clone/装依赖')
915
- parser.add_argument('--http', action='store_true',
916
- help='start 时强制走 HTTP 模式 (调试/多客户端共享; 默认 STDIO 自动开关)')
917
- parser.add_argument('--name', default='Developer', help='开发者名 (用于蓝湖 url 的 name 参数)')
918
- args = parser.parse_args()
919
-
920
- # 子命令: start (提示服务状态 / 强制 HTTP)
921
- if args.command == 'start':
922
- return start_lanhu_server(force_http=args.http)
923
- if args.command and args.command not in (None,):
924
- fail(f'未知子命令: {args.command}')
925
- print(' 可用: start [--http]')
926
- return 1
927
-
928
- if args.check:
929
- print_check_report()
930
- return 0
931
-
932
- if args.uninstall:
933
- do_uninstall()
934
- return 0
935
-
936
- report = configure_lanhu(
937
- developer_name=args.name,
938
- dry=False,
939
- skip_install=args.skip_install,
940
- )
941
-
942
- # 汇总
943
- print('\n' + '=' * 56)
944
- print(' 蓝湖 MCP 引导完成')
945
- print('=' * 56)
946
- print(f' 源码: {report["clone"]}')
947
- print(f' 依赖: {report["deps"]}')
948
- print(f' Cookie: {report["cookie"]}')
949
- print(f' 注册: {report["mcp"]}')
950
-
951
- if report['cookie'] == 'placeholder':
952
- print(f'\n ⚠ 下一步: 编辑 {lanhu_env_file()}, 把 {COOKIE_PLACEHOLDER} 换成真 cookie')
953
-
954
- if report['mcp'] in ('added', 'ok-existing') and not is_lanhu_running():
955
- print_start_hint()
956
-
957
- print('\n QoderWork 里用命令触发蓝湖 (非侵入式, 必须走命令):')
958
- print(' /wl-design-spec 录入 <蓝湖链接> design-import 调 mcp__lanhu__* (命令内授权)')
959
- return 0
960
-
961
-
962
- if __name__ == '__main__':
963
- sys.exit(main())
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
4
+ import os as _o, sys as _s
5
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
6
+ if _cp not in _s.path: _s.path.insert(0, _cp)
7
+ from bootstrap import setup; setup()
8
+
9
+ """
10
+ setup_lanhu.py - 蓝湖 MCP 引导器 (幂等)
11
+
12
+ 把第三方 dsphper/lanhu-mcp 引导成本机可用: clone + venv 依赖 + playwright + cookie + mcp.json 注册。
13
+
14
+ 蓝湖 MCP 与我们自己的 kg_mcp_server.py 不同:
15
+ - 它是 HTTP server (http://localhost:8000/mcp), 需要独立常驻启动
16
+ (不像 kg_mcp_server.py QoderWork 通过 command/args 自动拉起)
17
+ - cookie (LANHU_COOKIE) 在它的 .env 里, 必须用户登录 lanhuapp.com 后
18
+ 从浏览器 DevTools 复制 (AI 拿不到)
19
+ - QoderWork 的 mcp.json 用 url 字段注册 (Streamable HTTP), 不是 command/args
20
+
21
+ 被谁调用:
22
+ - setup.py Step 4.5: 主初始化流程
23
+ - install_qoderwork.py: 技能安装时同步注册 mcp.json
24
+ - 用户手动: python .qoder/scripts/setup_lanhu.py [--check|--uninstall]
25
+
26
+ 用法:
27
+ python .qoder/scripts/setup_lanhu.py # 引导 + 注册 (幂等)
28
+ python .qoder/scripts/setup_lanhu.py --check # 只体检不改动
29
+ python .qoder/scripts/setup_lanhu.py --uninstall # 删 external/lanhu-mcp/ + mcp.json 条目
30
+ python .qoder/scripts/setup_lanhu.py --skip-install # 只注册 mcp.json, 不 clone/装依赖
31
+
32
+ 幂等: 已 clone 跳过 clone; venv+依赖装过跳过; cookie 有效跳过; mcp.json 有 lanhu 跳过。
33
+ """
34
+
35
+ import argparse
36
+ import json
37
+ import os
38
+ import shutil
39
+ import socket
40
+ import subprocess
41
+ import sys
42
+ from pathlib import Path
43
+
44
+ try:
45
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
46
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
47
+ except (AttributeError, TypeError, OSError):
48
+ pass
49
+
50
+ THIS_DIR = Path(__file__).resolve().parent
51
+ BASE = THIS_DIR.parent.parent.parent # repo root (M2子目录化: setup/ → scripts/ → .qoder/ → repo)
52
+
53
+ LANHU_REPO = 'https://github.com/dsphper/lanhu-mcp.git'
54
+ EXTERNAL_DIR = BASE / 'external'
55
+ LANHU_DIR = EXTERNAL_DIR / 'lanhu-mcp'
56
+ LANHU_VENV = LANHU_DIR / '.venv'
57
+ LANHU_SERVER_SCRIPT = LANHU_DIR / 'lanhu_mcp_server.py'
58
+ COOKIE_PLACEHOLDER = 'PUT_YOUR_COOKIE_HERE'
59
+ LANHU_PORT = 8000
60
+ LANHU_MCP_PATH = '/mcp'
61
+
62
+ # QoderWork mcp.json 位置
63
+ if sys.platform == 'win32':
64
+ _HOME = Path(os.environ.get('USERPROFILE', str(Path.home())))
65
+ else:
66
+ _HOME = Path.home()
67
+ QODERWORK_MCP_FILE = _HOME / '.qoderwork' / 'mcp.json'
68
+
69
+
70
+ def _current_developer():
71
+ """读 .qoder/.developer 确定当前用户。没有返回 None。"""
72
+ try:
73
+ sys.path.insert(0, str(THIS_DIR))
74
+ from common.paths import get_developer
75
+ return get_developer(BASE)
76
+ except Exception:
77
+ return None
78
+
79
+
80
+ def lanhu_secrets_dir(developer=None):
81
+ """当前用户的私密信息目录 (v3.0: 收进 .private/secrets/, 向后兼容老 .secrets/)。
82
+
83
+ 按 .developer 隔离: 一台机器多人切换时, 各自的 cookie 互不覆盖。
84
+ """
85
+ dev = developer or _current_developer() or 'default'
86
+ try:
87
+ sys.path.insert(0, str(BASE / '.qoder' / 'scripts'))
88
+ from common.paths import get_secrets_dir
89
+ return Path(get_secrets_dir(dev))
90
+ except Exception:
91
+ return BASE / 'workspace' / 'members' / dev / '.secrets'
92
+
93
+
94
+ def lanhu_env_file(developer=None):
95
+ """当前用户的蓝湖 cookie 文件: workspace/members/{dev}/.secrets/lanhu.env"""
96
+ return lanhu_secrets_dir(developer) / 'lanhu.env'
97
+
98
+
99
+ # ============================================================
100
+ # 工具函数
101
+ # ============================================================
102
+
103
+ def ok(msg):
104
+ print(f' [\u2713] {msg}') # ✓
105
+
106
+
107
+ def warn(msg):
108
+ print(f' [!] {msg}')
109
+
110
+
111
+ def fail(msg):
112
+ print(f' [\u2717] {msg}', file=sys.stderr)
113
+
114
+
115
+ def ask(prompt, default='y'):
116
+ """交互式 yes/no, 默认 default。非交互环境返回 default。"""
117
+ if not sys.stdin.isatty():
118
+ return default.lower() in ('y', 'yes')
119
+ try:
120
+ ans = input(f'{prompt} [{default}] ').strip().lower() or default
121
+ return ans in ('y', 'yes')
122
+ except (EOFError, KeyboardInterrupt):
123
+ return default.lower() in ('y', 'yes')
124
+
125
+
126
+ def run(cmd, **kwargs):
127
+ """跑子进程, 返回 CompletedProcess。encoding 容错。"""
128
+ kwargs.setdefault('capture_output', True)
129
+ kwargs.setdefault('text', True)
130
+ kwargs.setdefault('encoding', 'utf-8')
131
+ kwargs.setdefault('errors', 'replace')
132
+ return subprocess.run(cmd, **kwargs)
133
+
134
+
135
+ def _venv_python(venv: Path) -> Path:
136
+ """venv 内的 python 可执行路径 (跨平台)。"""
137
+ if sys.platform == 'win32':
138
+ return venv / 'Scripts' / 'python.exe'
139
+ return venv / 'bin' / 'python'
140
+
141
+
142
+ def _venv_pip(venv: Path) -> list:
143
+ """venv 内的 pip 调用命令 ( python -m pip, 更稳)。"""
144
+ return [str(_venv_python(venv)), '-m', 'pip']
145
+
146
+
147
+ # lanhu-mcp 的 fastmcp 依赖需要 Python >= 3.10。
148
+ # 不能盲用 sys.executable (本机可能是 3.9), 得找一个 3.10+ 的解释器建 venv。
149
+ LANHU_MIN_PYTHON = (3, 10)
150
+
151
+
152
+ def _python_version(exe):
153
+ """返回解释器的 (major, minor) 元组, 探测失败返回 None。"""
154
+ try:
155
+ r = run([exe, '-c', 'import sys; print(sys.version_info[:2])'])
156
+ if r.returncode == 0:
157
+ import ast
158
+ ver = ast.literal_eval(r.stdout.strip())
159
+ if isinstance(ver, tuple) and len(ver) >= 2:
160
+ return (ver[0], ver[1])
161
+ except Exception:
162
+ pass
163
+ return None
164
+
165
+
166
+ def find_python_310():
167
+ """找一个 Python >= 3.10 的解释器路径, 优先级:
168
+ 1. sys.executable (如果本身就 >= 3.10, 直接用, 最省事)
169
+ 2. Windows py launcher: py -3.10 / -3.11 / -3.12 / -3.13
170
+ 3. 常见命令: python3.10/3.11/3.12, python3
171
+ 4. 兜底返回 sys.executable (让 pip 报错给用户看真原因)
172
+ """
173
+ # 1. 当前解释器够新
174
+ if sys.version_info[:2] >= LANHU_MIN_PYTHON:
175
+ return sys.executable
176
+
177
+ # 2. Windows py launcher
178
+ if sys.platform == 'win32':
179
+ for v in ('3.13', '3.12', '3.11', '3.10'):
180
+ launcher = 'py' if shutil.which('py') else ('py.exe' if shutil.which('py.exe') else None)
181
+ if not launcher:
182
+ break
183
+ r = run([launcher, f'-{v}', '-c', 'import sys; print(sys.executable)'])
184
+ if r.returncode == 0 and r.stdout.strip():
185
+ exe = r.stdout.strip().strip('"')
186
+ ver = _python_version(exe)
187
+ if ver and ver >= LANHU_MIN_PYTHON:
188
+ return exe
189
+
190
+ # 3. 常见命令名 (跨平台)
191
+ for cand in ('python3.13', 'python3.12', 'python3.11', 'python3.10',
192
+ 'python3', 'python'):
193
+ path = shutil.which(cand)
194
+ if path:
195
+ ver = _python_version(path)
196
+ if ver and ver >= LANHU_MIN_PYTHON:
197
+ return path
198
+
199
+ # 4. 兜底
200
+ return sys.executable
201
+
202
+
203
+ # ============================================================
204
+ # Step 1: clone / update lanhu-mcp
205
+ # ============================================================
206
+
207
+ def ensure_clone(dry=False, interactive=True):
208
+ """clone lanhu-mcp 到 external/lanhu-mcp/, 已存在则 git pull 更新。
209
+
210
+ 返回 (status, path): status in ('cloned', 'updated', 'ok', 'failed', 'would', 'absent')
211
+ """
212
+ print('\n--- 1. 蓝湖 MCP 源码 ---')
213
+ if not shutil.which('git'):
214
+ warn('git 未安装, 无法 clone lanhu-mcp (装 git 后重跑)')
215
+ return ('failed', None)
216
+
217
+ if not LANHU_DIR.is_dir():
218
+ if dry:
219
+ print(f' [MISS] 未 clone: {LANHU_DIR}')
220
+ return ('would', None)
221
+ EXTERNAL_DIR.mkdir(parents=True, exist_ok=True)
222
+ print(f' git clone {LANHU_REPO} -> {LANHU_DIR}')
223
+ r = run(['git', 'clone', '--depth', '1', LANHU_REPO, str(LANHU_DIR)])
224
+ if r.returncode != 0:
225
+ fail('clone 失败: ' + (r.stderr or '').strip()[:200])
226
+ return ('failed', None)
227
+ ok(f' clone: {LANHU_DIR}')
228
+ return ('cloned', LANHU_DIR)
229
+
230
+ # 已存在: 检查是否是有效 repo
231
+ if not (LANHU_DIR / '.git').is_dir():
232
+ warn(f'{LANHU_DIR} 存在但不是 git 仓库, 跳过更新 (手动处理)')
233
+ return ('ok', LANHU_DIR)
234
+
235
+ if dry:
236
+ ok(f'已存在: {LANHU_DIR}')
237
+ return ('ok', LANHU_DIR)
238
+
239
+ # git pull 更新 (best-effort, 失败不阻塞)
240
+ r = run(['git', '-C', str(LANHU_DIR), 'pull', '--ff-only'])
241
+ if r.returncode == 0:
242
+ ok(f'已更新: {LANHU_DIR}')
243
+ return ('updated', LANHU_DIR)
244
+ warn('git pull 失败 (本地可能有改动), 用现有版本继续: '
245
+ + (r.stderr or '').strip()[:120])
246
+ return ('ok', LANHU_DIR)
247
+
248
+
249
+ # ============================================================
250
+ # Step 2: venv + 依赖 + playwright chromium
251
+ # ============================================================
252
+
253
+ def ensure_deps(dry=False, _rebuilt=False):
254
+ """建隔离 venv, requirements.txt + playwright chromium。
255
+
256
+ 幂等: venv+依赖装过则跳过 (靠一个 marker 文件判断)。
257
+ _rebuilt: 内部防无限递归 (删旧 venv 重建只允许一次)
258
+ """
259
+ print('\n--- 2. 蓝湖 MCP 依赖 ---')
260
+ if not LANHU_DIR.is_dir():
261
+ warn('lanhu-mcp clone, 跳过依赖安装')
262
+ return 'absent'
263
+
264
+ marker = LANHU_DIR / '.deps-installed'
265
+ req_file = LANHU_DIR / 'requirements.txt'
266
+
267
+ # 已装过 (marker 在 + requirements 没变) → 跳过
268
+ if marker.is_file() and req_file.is_file():
269
+ try:
270
+ marker_mtime = marker.stat().st_mtime
271
+ req_mtime = req_file.stat().st_mtime
272
+ if marker_mtime > req_mtime:
273
+ ok(f'依赖已装 (venv: {LANHU_VENV})')
274
+ return 'ok'
275
+ except OSError:
276
+ pass
277
+
278
+ if dry:
279
+ print(f' [MISS] 依赖未装: {LANHU_VENV}')
280
+ return 'would'
281
+
282
+ # 建 venv (用 3.10+ Python, fastmcp 要求)
283
+ base_python = find_python_310()
284
+ base_ver = _python_version(base_python)
285
+ if not LANHU_VENV.is_dir():
286
+ if base_ver and base_ver >= LANHU_MIN_PYTHON:
287
+ print(f'venv (Python {base_ver[0]}.{base_ver[1]}): '
288
+ f'"{base_python}" -m venv {LANHU_VENV}')
289
+ else:
290
+ # 找不到 3.10+, 仍尝试建 (pip 会报错, 但给用户明确指引)
291
+ warn(f'未找到 Python >= 3.10 (fastmcp 要求)。当前最佳: '
292
+ f'{base_python} ({base_ver})')
293
+ print(f' 建 venv: "{base_python}" -m venv {LANHU_VENV}')
294
+ print(' 预计 pip install 会失败 Python 3.10+ 后重跑')
295
+ r = run([base_python, '-m', 'venv', str(LANHU_VENV)])
296
+ if r.returncode != 0:
297
+ fail('venv 创建失败: ' + (r.stderr or '').strip()[:200])
298
+ return 'failed'
299
+ elif base_ver and base_ver < LANHU_MIN_PYTHON and not _rebuilt:
300
+ # venv 已存在但可能是旧 3.9 建的 (之前版本的 bug), 删了重建。
301
+ # _rebuilt 防无限递归: 若本机没有 3.10+, 删了重建还是 3.9, 不再删第二次。
302
+ venv_ver = _python_version(_venv_python(LANHU_VENV))
303
+ if venv_ver and venv_ver < LANHU_MIN_PYTHON:
304
+ warn(f'现有 venv Python {venv_ver[0]}.{venv_ver[1]} (fastmcp 需 3.10+), '
305
+ f'删了重建')
306
+ try:
307
+ shutil.rmtree(LANHU_VENV)
308
+ except OSError:
309
+ pass
310
+ return ensure_deps(dry=dry, _rebuilt=True)
311
+
312
+ py = _venv_python(LANHU_VENV)
313
+ if not py.is_file():
314
+ fail(f'venv python 不存在: {py}')
315
+ return 'failed'
316
+
317
+ # 升级 pip
318
+ print(' 升级 pip...')
319
+ run(_venv_pip(LANHU_VENV) + ['install', '--upgrade', 'pip'])
320
+
321
+ # 装 requirements.txt
322
+ if req_file.is_file():
323
+ print(f' pip install -r requirements.txt')
324
+ r = run(_venv_pip(LANHU_VENV) + ['install', '-r', str(req_file)])
325
+ if r.returncode != 0:
326
+ fail('pip install requirements 失败: ' + (r.stderr or '').strip()[:200])
327
+ return 'failed'
328
+ ok('依赖已装')
329
+ else:
330
+ warn('requirements.txt 不存在, 跳过 (lanhu-mcp 目录结构可能变了)')
331
+
332
+ # playwright install chromium (150-300MB, 一次性)
333
+ print(' playwright install chromium (一次性, 约 150-300MB)...')
334
+ r = run([str(py), '-m', 'playwright', 'install', 'chromium'])
335
+ if r.returncode == 0:
336
+ ok('chromium 已装')
337
+ else:
338
+ warn('playwright install chromium 失败 (可稍后手动跑: '
339
+ f'{py} -m playwright install chromium)')
340
+ warn(' 原因: ' + (r.stderr or '').strip()[:120])
341
+ # 不阻塞: cookie/服务启动后 playwright 缺失会在用时才报错
342
+
343
+ # 写 marker
344
+ try:
345
+ marker.write_text('deps installed\n', encoding='utf-8')
346
+ except OSError:
347
+ pass
348
+ return 'ok'
349
+
350
+
351
+ # ============================================================
352
+ # Step 3: cookie (按开发者隔离, workspace/members/{dev}/.secrets/lanhu.env)
353
+ # ============================================================
354
+ # 为什么不放 external/lanhu-mcp/.env:
355
+ # 1. cookie 是个人登录凭证, 不该进仓库代码区 (即使 gitignore)
356
+ # 2. 同一台机器多人切换 .developer 时, 单一 .env 会互相覆盖
357
+ # 3. 跟 .signing_key / autotest-data.yaml 同一套隔离机制, 一致
358
+ # 蓝湖服务端怎么读到这个 cookie:
359
+ # lanhu_mcp_server.py load_dotenv(override=False), 已有环境变量优先。
360
+ # 所以启动时把 LANHU_COOKIE 作为环境变量注入即可 (见 start_lanhu_server)。
361
+
362
+ def _read_env_cookie(developer=None):
363
+ """读当前用户 lanhu.env 里的 LANHU_COOKIE, 没有返回 None。"""
364
+ env_file = lanhu_env_file(developer)
365
+ if not env_file.is_file():
366
+ return None
367
+ try:
368
+ for line in env_file.read_text(encoding='utf-8', errors='replace').splitlines():
369
+ line = line.strip()
370
+ if line.startswith('LANHU_COOKIE') and '=' in line:
371
+ val = line.split('=', 1)[1].strip().strip('"').strip("'")
372
+ return val
373
+ except OSError:
374
+ pass
375
+ return None
376
+
377
+
378
+ def _write_env_cookie(cookie, developer=None):
379
+ """把 LANHU_COOKIE 写进当前用户的 lanhu.env (保留其他行)
380
+
381
+ 文件: workspace/members/{dev}/.secrets/lanhu.env (已 gitignore)
382
+ """
383
+ env_file = lanhu_env_file(developer)
384
+ try:
385
+ env_file.parent.mkdir(parents=True, exist_ok=True)
386
+ lines = []
387
+ found = False
388
+ if env_file.is_file():
389
+ for line in env_file.read_text(encoding='utf-8', errors='replace').splitlines():
390
+ if line.strip().startswith('LANHU_COOKIE') and '=' in line:
391
+ lines.append(f'LANHU_COOKIE="{cookie}"')
392
+ found = True
393
+ else:
394
+ lines.append(line)
395
+ if not found:
396
+ lines.append(f'LANHU_COOKIE="{cookie}"')
397
+ env_file.write_text('\n'.join(lines) + '\n', encoding='utf-8')
398
+ return True
399
+ except OSError as e:
400
+ fail(f'写 {env_file} 失败: {e}')
401
+ return False
402
+
403
+
404
+ def ensure_cookie(dry=False):
405
+ """收 cookie .env。已有真 cookie 跳过; 没有则交互式引导。
406
+
407
+ 返回 status: 'ok' (有真 cookie) / 'placeholder' (占位) / 'would' / 'absent'
408
+ """
409
+ dev = _current_developer()
410
+ env_file = lanhu_env_file(dev)
411
+ print('\n--- 3. 蓝湖 Cookie ---')
412
+ if dev:
413
+ print(f' 当前开发者: {dev}')
414
+ print(f' cookie 文件: {env_file} (按人隔离, 不进 git)')
415
+ cookie = _read_env_cookie(dev)
416
+ if cookie and cookie != COOKIE_PLACEHOLDER:
417
+ ok(f'蓝湖 cookie 已配置 ({env_file.name})')
418
+ return 'ok'
419
+
420
+ if dry:
421
+ if cookie == COOKIE_PLACEHOLDER:
422
+ print(f' [WARN] {env_file.name} 里是占位 cookie, 需要换成真的')
423
+ else:
424
+ print(f' [MISS] 没有 LANHU_COOKIE ({env_file})')
425
+ return 'would'
426
+
427
+ # 交互式引导
428
+ if sys.stdin.isatty():
429
+ print()
430
+ print(' 蓝湖 cookie 获取步骤:')
431
+ print(' 1. 浏览器打开 https://lanhuapp.com 并登录')
432
+ print(' 2. F12 打开开发者工具, 切到 Network 标签')
433
+ print(' 3. 刷新页面 (F5), 点任意一个请求')
434
+ print(' 4. Request Headers 里找 Cookie: 这一行, 复制【整行】')
435
+ print(' (或 Application Cookies lanhuapp.com, 把所有项拼成')
436
+ print(' name1=value1; name2=value2 格式)')
437
+ print(' 必须【整行】, 不能只复制 user_token 一个!')
438
+ print(' 实测: 只给 user_token 蓝湖 API 返回 418 (鉴权失败),')
439
+ print(' 需要 user_token + session + 其他项的组合才认。')
440
+ print()
441
+ try:
442
+ cookie = input(' 粘贴蓝湖 cookie (留空跳过, 之后编辑 lanhu.env): ').strip()
443
+ except (EOFError, KeyboardInterrupt):
444
+ cookie = ''
445
+ if cookie and len(cookie) > 20:
446
+ if _write_env_cookie(cookie, dev):
447
+ ok(f'蓝湖 cookie 已写入 {env_file}')
448
+ return 'ok'
449
+ return 'placeholder'
450
+ warn(f'未输入 cookie, 写入占位 (之后编辑 {env_file} 换成真的)')
451
+ else:
452
+ # 非交互环境 (AI 调用): 写占位
453
+ warn('非交互环境, 无法收 cookie, 写入占位')
454
+
455
+ if _write_env_cookie(COOKIE_PLACEHOLDER, dev):
456
+ print(f' 占位已写入: {env_file}')
457
+ print(f' 编辑该文件, 把 {COOKIE_PLACEHOLDER} 换成真 cookie')
458
+ return 'placeholder'
459
+
460
+
461
+ # ============================================================
462
+ # Step 4: mcp.json 注册 (url 模式)
463
+ # ============================================================
464
+
465
+ def lanhu_mcp_url(developer_name='Developer'):
466
+ """生成蓝湖 MCP 的 url (HTTP Streamable, role/name 作为 query)。"""
467
+ import urllib.parse
468
+ name = urllib.parse.quote(developer_name or 'Developer')
469
+ return f'http://localhost:{LANHU_PORT}{LANHU_MCP_PATH}?role=Developer&name={name}'
470
+
471
+
472
+ def _detect_python_command():
473
+ """探测本机可用的 python 命令 (跨平台)。win: py -3 > python; mac: python3。
474
+ 复用 install_qoderwork 的逻辑, 但本文件独立实现避免循环依赖。"""
475
+ for cand in (('py', ['-3']), ('python3', []), ('python', [])):
476
+ name, extra = cand
477
+ if not shutil.which(name):
478
+ continue
479
+ cmd = [name] + extra + ['--version']
480
+ try:
481
+ r = run(cmd)
482
+ if r.returncode == 0:
483
+ return ' '.join([name] + extra)
484
+ except (OSError, subprocess.SubprocessError):
485
+ continue
486
+ return 'python'
487
+
488
+
489
+ def _lanhu_stdio_entry():
490
+ """构造 STDIO 模式的 mcp.json lanhu 条目 (launcher 模式, 不写死路径)。
491
+
492
+ 用统一启动器 mcp_launcher.py: command=python命令名, args=[launcher, "lanhu"]。
493
+ launcher 运行时自定位仓库根 + 找 venv python + 读角色 cookie。
494
+ 这样 mcp.json 不含任何写死的仓库路径/venv路径, 跨机器通用。
495
+ """
496
+ py_cmd = _detect_python_command()
497
+ # launcher 在 ~/.qoderwork/mcp_launcher.py (install 时拷)
498
+ launcher = str(_HOME / '.qoderwork' / 'mcp_launcher.py')
499
+ return {
500
+ 'command': py_cmd,
501
+ 'args': [launcher, 'lanhu'],
502
+ }
503
+
504
+
505
+ def _is_stdio_entry(entry):
506
+ """判断 mcp.json lanhu 条目是不是 launcher STDIO 格式 (args 含 mcp_launcher)。"""
507
+ if not isinstance(entry, dict):
508
+ return False
509
+ args = entry.get('args', [])
510
+ return 'command' in entry and any('mcp_launcher.py' in str(a) for a in args)
511
+
512
+
513
+ def register_lanhu_mcp(developer_name='Developer', dry=False):
514
+ """在 ~/.qoderwork/mcp.json 注册蓝湖 MCP (STDIO 模式, QoderWork 自动开关)。
515
+
516
+ STDIO 模式: command 指向 wrapper, QoderWork 开自动拉起、关自动杀。
517
+ cookie 由 wrapper 运行时按角色读 .secrets, 不进 mcp.json (角色隔离)。
518
+
519
+ 迁移: 旧的 url 格式 (HTTP) → 升级成 STDIO。
520
+ 幂等: 已是 STDIO 格式 → 跳过。
521
+ 返回: 'ok-existing' / 'added' / 'upgraded' / 'would-add'
522
+ """
523
+ if not QODERWORK_MCP_FILE.is_file():
524
+ if dry:
525
+ return 'would-add'
526
+ QODERWORK_MCP_FILE.parent.mkdir(parents=True, exist_ok=True)
527
+
528
+ existing = {}
529
+ if QODERWORK_MCP_FILE.is_file():
530
+ try:
531
+ existing = json.loads(QODERWORK_MCP_FILE.read_text(encoding='utf-8'))
532
+ if not isinstance(existing, dict):
533
+ existing = {}
534
+ except (json.JSONDecodeError, OSError):
535
+ existing = {}
536
+
537
+ servers = existing.get('mcpServers', {})
538
+ if not isinstance(servers, dict):
539
+ servers = {}
540
+
541
+ cur = servers.get('lanhu')
542
+ stdio_entry = _lanhu_stdio_entry()
543
+
544
+ if cur and _is_stdio_entry(cur):
545
+ # 已是 STDIO, 但 command/args 可能因换机器变了 (venv 路径), 校准
546
+ if cur.get('command') == stdio_entry['command'] and cur.get('args') == stdio_entry['args']:
547
+ return 'ok-existing'
548
+ # 路径漂移, 更新
549
+ if dry:
550
+ return 'would-add'
551
+ servers['lanhu'] = stdio_entry
552
+ existing['mcpServers'] = servers
553
+ QODERWORK_MCP_FILE.write_text(
554
+ json.dumps(existing, indent=2, ensure_ascii=False) + '\n',
555
+ encoding='utf-8')
556
+ return 'upgraded'
557
+
558
+ if dry:
559
+ return 'would-add'
560
+
561
+ # 新增或从 url(HTTP) 升级到 STDIO
562
+ was_http = bool(cur and 'url' in cur)
563
+ servers['lanhu'] = stdio_entry
564
+ existing['mcpServers'] = servers
565
+ QODERWORK_MCP_FILE.write_text(
566
+ json.dumps(existing, indent=2, ensure_ascii=False) + '\n',
567
+ encoding='utf-8')
568
+ return 'upgraded' if was_http else 'added'
569
+
570
+
571
+ def unregister_lanhu_mcp():
572
+ """从 mcp.json lanhu 条目 (best-effort)。"""
573
+ if not QODERWORK_MCP_FILE.is_file():
574
+ return False
575
+ try:
576
+ existing = json.loads(QODERWORK_MCP_FILE.read_text(encoding='utf-8'))
577
+ servers = existing.get('mcpServers', {})
578
+ if 'lanhu' in servers:
579
+ del servers['lanhu']
580
+ existing['mcpServers'] = servers
581
+ QODERWORK_MCP_FILE.write_text(
582
+ json.dumps(existing, indent=2, ensure_ascii=False) + '\n',
583
+ encoding='utf-8',
584
+ )
585
+ return True
586
+ except (json.JSONDecodeError, OSError):
587
+ pass
588
+ return False
589
+
590
+
591
+ # ============================================================
592
+ # 健康探测: 蓝湖 HTTP 服务是否在跑
593
+ # ============================================================
594
+
595
+ def is_lanhu_running():
596
+ """探测 localhost:8000 是否在监听 (蓝湖服务是否启动)。"""
597
+ try:
598
+ with socket.create_connection(('127.0.0.1', LANHU_PORT), timeout=1):
599
+ return True
600
+ except (OSError, socket.timeout):
601
+ return False
602
+
603
+
604
+ # ============================================================
605
+ # 打印启动指引
606
+ # ============================================================
607
+
608
+ def print_start_hint():
609
+ """STDIO 模式下, 蓝湖由 QoderWork 自动管, 这个 hint 主要提示怎么验证。"""
610
+ print('\n 蓝湖 MCP (STDIO 模式, QoderWork 自动开关):')
611
+ print(' 开 QoderWork 自动起, 关自动停 — 无需手动 start')
612
+ print(' 验证: QoderWork 里 /wl-design 预览 <蓝湖链接>, 能调通即正常')
613
+ print(' 换角色: 改 .developer + 配新角色 cookie → 重启 QoderWork')
614
+ print(' 调试/多客户端共享 (强制 HTTP): python .qoder/scripts/setup_lanhu.py start --http')
615
+
616
+
617
+ # ============================================================
618
+ # 启动蓝湖服务端 (读当前用户 cookie, 以环境变量注入)
619
+ # ============================================================
620
+
621
+ def start_lanhu_server(force_http=False):
622
+ """启动蓝湖 MCP 服务。
623
+
624
+ 默认 (STDIO 模式): 不需要手动 start, QoderWork 自动管。本函数只提示。
625
+ force_http=True: 走旧 HTTP 模式 (前台常驻, 用于调试或多客户端共享)
626
+ """
627
+ if not force_http:
628
+ print('=' * 56)
629
+ print(' 蓝湖 MCP 服务启动')
630
+ print('=' * 56)
631
+ # 检测当前 mcp.json 是不是 STDIO
632
+ entry = None
633
+ if QODERWORK_MCP_FILE.is_file():
634
+ try:
635
+ entry = json.loads(QODERWORK_MCP_FILE.read_text(encoding='utf-8')).get('mcpServers', {}).get('lanhu')
636
+ except Exception:
637
+ pass
638
+ if _is_stdio_entry(entry) if entry else False:
639
+ ok('已是 STDIO 模式 QoderWork 自动开关, 无需手动 start')
640
+ print(' 开 QoderWork 自动拉起蓝湖 (wrapper 读当前角色 cookie), 关自动停')
641
+ print(' 换角色: 改 .developer + 配新角色 cookie → 重启 QoderWork')
642
+ print('')
643
+ print(' 要强制用 HTTP 模式 (调试/多客户端共享):')
644
+ print(f' python .qoder/scripts/setup_lanhu.py start --http')
645
+ return 0
646
+ # 非 STDIO (旧 HTTP 或没注册), 提示升级
647
+ warn('当前不是 STDIO 模式, 手动 start 只支持 HTTP')
648
+ print(' 建议升级 STDIO (QoderWork 自动开关): python .qoder/scripts/setup_lanhu.py')
649
+ print(' 或强制 HTTP: python .qoder/scripts/setup_lanhu.py start --http')
650
+ return 1
651
+
652
+ # === HTTP 模式 (force_http=True, 降级/调试用) ===
653
+ print('=' * 56)
654
+ print(' 启动蓝湖 MCP 服务端 (HTTP 模式, 降级/调试)')
655
+ print('=' * 56)
656
+
657
+ # 1. 源码 + venv 是否就绪
658
+ if not LANHU_SERVER_SCRIPT.is_file():
659
+ fail(f'蓝湖服务脚本不存在: {LANHU_SERVER_SCRIPT}')
660
+ print(' 先跑: python .qoder/scripts/setup_lanhu.py')
661
+ return 1
662
+ py = _venv_python(LANHU_VENV)
663
+ if not py.is_file():
664
+ fail(f'venv python 不存在: {py}')
665
+ print(' 先跑: python .qoder/scripts/setup_lanhu.py')
666
+ return 1
667
+
668
+ # 2. 当前用户 + cookie
669
+ dev = _current_developer()
670
+ env_file = lanhu_env_file(dev)
671
+ env_cookie = os.environ.get('LANHU_COOKIE', '').strip()
672
+ cookie_source = 'env' if env_cookie else 'file'
673
+ cookie = env_cookie or _read_env_cookie(dev)
674
+
675
+ print(f' 当前开发者: {dev or "(未初始化, 用 default)"}')
676
+ if cookie_source == 'env':
677
+ print(f' cookie 来源: 环境变量 LANHU_COOKIE (临时, 不落盘)')
678
+ else:
679
+ print(f' cookie 来源: {env_file} (主线, 落盘)')
680
+
681
+ if not cookie or cookie == COOKIE_PLACEHOLDER:
682
+ fail('cookie 未配置')
683
+ if cookie_source == 'env':
684
+ print(' 环境变量 LANHU_COOKIE 是空的或占位')
685
+ else:
686
+ print(f' 编辑 {env_file}, {COOKIE_PLACEHOLDER} 换成真 cookie')
687
+ print(' 获取: lanhuapp.com 登录 → F12 → Network → Cookie 头复制')
688
+ return 1
689
+ print(f' cookie: 已配置 (长度 {len(cookie)})')
690
+
691
+ # 3. 已在跑就别重复启
692
+ if is_lanhu_running():
693
+ warn(f'蓝湖服务已在跑 (localhost:{LANHU_PORT}), 不重复启动')
694
+ return 0
695
+
696
+ # 4. 以环境变量注入 cookie 启动 (前台常驻, Ctrl+C 退出)
697
+ print(f'\n 启动: {py} {LANHU_SERVER_SCRIPT}')
698
+ print(f' HTTP: http://localhost:{LANHU_PORT}{LANHU_MCP_PATH}')
699
+ print(' (常驻前台, 用完 Ctrl+C 关闭。别关这个终端。)\n')
700
+ env = os.environ.copy()
701
+ env['LANHU_COOKIE'] = cookie
702
+ # 不捕获输出 —— 服务端日志直接打到当前终端, 用户能看到请求/报错
703
+ try:
704
+ return subprocess.call([str(py), str(LANHU_SERVER_SCRIPT)], env=env)
705
+ except OSError as e:
706
+ fail(f'启动失败: {e}')
707
+ return 1
708
+
709
+
710
+ # ============================================================
711
+ # 主流程
712
+ # ============================================================
713
+
714
+ def configure_lanhu(developer_name='Developer', dry=False, skip_install=False):
715
+ """完整引导: clone 依赖 → cookie → mcp.json 注册。
716
+
717
+ 返回 dict 报告各步状态。
718
+ """
719
+ report = {'clone': 'skipped', 'deps': 'skipped',
720
+ 'cookie': 'skipped', 'mcp': 'skipped'}
721
+
722
+ if skip_install:
723
+ print('\n--- 蓝湖 MCP (--skip-install, 只注册 mcp.json) ---')
724
+ else:
725
+ print('\n' + '=' * 56)
726
+ print(' 蓝湖 MCP 引导')
727
+ print('=' * 56)
728
+
729
+ # 1. clone
730
+ status, _ = ensure_clone(dry=dry)
731
+ report['clone'] = status
732
+ if status == 'failed':
733
+ print('\n蓝湖源码获取失败, 后续步骤跳过。')
734
+ return report
735
+
736
+ # 2. 依赖
737
+ report['deps'] = ensure_deps(dry=dry)
738
+
739
+ # 3. cookie (即使 skip_install 也引导, 因为 mcp.json 注册后需要 cookie 才能用)
740
+ if not skip_install:
741
+ report['cookie'] = ensure_cookie(dry=dry)
742
+
743
+ # 4. mcp.json 注册 (STDIO 模式, QoderWork 自动开关)
744
+ print('\n--- 4. 注册到 QoderWork mcp.json (STDIO 自动开关) ---')
745
+ status = register_lanhu_mcp(developer_name, dry=dry)
746
+ report['mcp'] = status
747
+ if status == 'ok-existing':
748
+ ok(f'蓝湖 MCP 已注册 (STDIO): {QODERWORK_MCP_FILE}')
749
+ print(' 开 QoderWork 自动起, 关自动停, 无需手动 start')
750
+ elif status in ('added', 'upgraded'):
751
+ tag = '已写入' if status == 'added' else '已升级(HTTP→STDIO)'
752
+ ok(f'{tag} 蓝湖 MCP: {QODERWORK_MCP_FILE}')
753
+ print(' STDIO 模式: 开 QoderWork 自动起, 关自动停')
754
+ print(f' command wrapper → 读 {developer_name} 的 cookie 拉起 lanhu')
755
+ elif status == 'would-add':
756
+ print(f' [MISS] 未注册, 安装时会写入 {QODERWORK_MCP_FILE}')
757
+
758
+ return report
759
+
760
+
761
+ def print_check_report():
762
+ """--check 模式: 体检报告 (不改动)。"""
763
+ print('=' * 56)
764
+ print(' 蓝湖 MCP 体检')
765
+ print('=' * 56)
766
+
767
+ # clone
768
+ print('\n--- 1. 源码 ---')
769
+ if LANHU_DIR.is_dir() and (LANHU_DIR / '.git').is_dir():
770
+ ok(f'已 clone: {LANHU_DIR}')
771
+ # 检查关键文件
772
+ server_file = LANHU_DIR / 'lanhu_mcp_server.py'
773
+ if server_file.is_file():
774
+ ok(f'入口文件在: {server_file}')
775
+ else:
776
+ warn(f'入口文件缺失: {server_file} (lanhu-mcp 版本结构可能变了)')
777
+ else:
778
+ print(f' [MISS] clone (跑 setup.py 或本脚本安装)')
779
+
780
+ # 依赖
781
+ print('\n--- 2. 依赖 ---')
782
+ if LANHU_VENV.is_dir() and _venv_python(LANHU_VENV).is_file():
783
+ marker = LANHU_DIR / '.deps-installed'
784
+ # 实测验证: 尝试导入 fastmcp (lanhu-mcp 的核心依赖, 缺它一定跑不起来)
785
+ # marker 只是快速跳过标记, 真相以能否导入为准
786
+ py = _venv_python(LANHU_VENV)
787
+ r = run([str(py), '-c', 'import fastmcp; print(fastmcp.__version__)'])
788
+ deps_importable = r.returncode == 0 and r.stdout.strip()
789
+ if deps_importable:
790
+ extra = f' (fastmcp {r.stdout.strip()})'
791
+ if marker.is_file():
792
+ ok(f'venv + 依赖已装: {LANHU_VENV}{extra}')
793
+ else:
794
+ ok(f'venv + 依赖已装 (marker 缺失但实测可导入): {LANHU_VENV}{extra}')
795
+ print(f' (补写 marker 以便后续快速跳过)')
796
+ try:
797
+ marker.write_text('deps verified by import\n', encoding='utf-8')
798
+ except OSError:
799
+ pass
800
+ elif marker.is_file():
801
+ warn(f'marker 在但 fastmcp 导入失败, 依赖可能损坏: {LANHU_VENV}')
802
+ print(f' 重装: python .qoder/scripts/setup_lanhu.py')
803
+ else:
804
+ warn(f'venv 存在但依赖未装完 (fastmcp 不可导入): {LANHU_VENV}')
805
+ print(f' 重装: python .qoder/scripts/setup_lanhu.py')
806
+ else:
807
+ print(f' [MISS] venv 未建 (跑 setup.py 或本脚本安装)')
808
+
809
+ # cookie (按开发者隔离)
810
+ print('\n--- 3. Cookie ---')
811
+ dev = _current_developer()
812
+ env_file = lanhu_env_file(dev)
813
+ if dev:
814
+ print(f' 当前开发者: {dev}')
815
+ cookie = _read_env_cookie(dev)
816
+ if cookie and cookie != COOKIE_PLACEHOLDER:
817
+ ok(f'蓝湖 cookie 已配置 ({env_file})')
818
+ elif cookie == COOKIE_PLACEHOLDER:
819
+ warn(f'占位 cookie ({COOKIE_PLACEHOLDER}), 需要换成真的')
820
+ print(f' 编辑: {env_file}')
821
+ else:
822
+ print(f' [MISS] 没有 LANHU_COOKIE ({env_file})')
823
+
824
+ # mcp.json (STDIO 模式检测)
825
+ print('\n--- 4. QoderWork 注册 ---')
826
+ if QODERWORK_MCP_FILE.is_file():
827
+ try:
828
+ cfg = json.loads(QODERWORK_MCP_FILE.read_text(encoding='utf-8'))
829
+ servers = cfg.get('mcpServers', {})
830
+ entry = servers.get('lanhu')
831
+ if not entry:
832
+ print(f' [MISS] mcp.json 没有 lanhu 条目')
833
+ elif _is_stdio_entry(entry):
834
+ launcher_ok = (_HOME / '.qoderwork' / 'mcp_launcher.py').is_file()
835
+ ok(f'已注册 (STDIO launcher, 自动开关): command={entry.get("command","?")}')
836
+ ok(f' launcher 存在: {"✓" if launcher_ok else "✗ 缺 ~/.qoderwork/mcp_launcher.py (重跑 install_qoderwork)"}')
837
+ print(' 开 QoderWork 自动起, 关自动停 (无需手动 start)')
838
+ elif 'command' in entry:
839
+ # 旧 STDIO (写死 venv 路径) 或其他 command 格式
840
+ warn(f'旧 STDIO 格式 (command 写死路径), 建议升级 launcher 模式')
841
+ print(' 升级: python .qoder/scripts/install_qoderwork.py')
842
+ else:
843
+ warn(f'旧 HTTP 模式 (url={entry.get("url","?")[:50]}...), 需手动 start')
844
+ print(' 升级 STDIO: python .qoder/scripts/setup_lanhu.py')
845
+ except (json.JSONDecodeError, OSError):
846
+ warn(f'mcp.json 解析失败: {QODERWORK_MCP_FILE}')
847
+ else:
848
+ print(f' [MISS] mcp.json 不存在: {QODERWORK_MCP_FILE}')
849
+
850
+ # 服务状态 (STDIO 模式由 QoderWork 管, 不常驻端口; 这里只提示)
851
+ print('\n--- 5. 服务状态 ---')
852
+ entry = None
853
+ if QODERWORK_MCP_FILE.is_file():
854
+ try:
855
+ entry = json.loads(QODERWORK_MCP_FILE.read_text(encoding='utf-8')).get('mcpServers', {}).get('lanhu')
856
+ except Exception:
857
+ pass
858
+ if _is_stdio_entry(entry) if entry else False:
859
+ ok('STDIO 模式: QoderWork 启动时自动拉起 (不占常驻端口)')
860
+ print(' (要验证: QoderWork 后 /wl-design 预览 <蓝湖链接> 能调通即正常)')
861
+ elif is_lanhu_running():
862
+ ok(f'蓝湖服务在跑 (HTTP, localhost:{LANHU_PORT}) — 旧模式, 建议升级 STDIO')
863
+ else:
864
+ print(' STDIO 模式下服务由 QoderWork 按需拉起, 这里不显示常驻状态')
865
+
866
+
867
+ def do_uninstall():
868
+ """删 external/lanhu-mcp/ + mcp.json lanhu 条目。"""
869
+ print('=' * 56)
870
+ print(' 蓝湖 MCP 卸载')
871
+ print('=' * 56)
872
+
873
+ # 1. mcp.json 条目
874
+ print('\n--- 1. 移除 mcp.json 条目 ---')
875
+ if unregister_lanhu_mcp():
876
+ ok(f'已从 mcp.json 删除 lanhu 条目')
877
+ else:
878
+ print(' mcp.json 无 lanhu 条目 (无需删除)')
879
+
880
+ # 2. external/lanhu-mcp/
881
+ print('\n--- 2. 删除源码 ---')
882
+ if LANHU_DIR.is_dir():
883
+ if ask(f'删除 {LANHU_DIR}? (含 venv + chromium 缓存, 不可恢复)', 'n'):
884
+ try:
885
+ shutil.rmtree(LANHU_DIR)
886
+ ok(f'已删除: {LANHU_DIR}')
887
+ except OSError as e:
888
+ fail(f'删除失败: {e}')
889
+ else:
890
+ print(' 跳过 (保留源码)')
891
+ else:
892
+ print(f' 源码目录不存在: {LANHU_DIR}')
893
+
894
+ # 3. 清理空的 external/
895
+ if EXTERNAL_DIR.is_dir():
896
+ try:
897
+ remaining = list(EXTERNAL_DIR.iterdir())
898
+ if not remaining:
899
+ EXTERNAL_DIR.rmdir()
900
+ ok(f'已删除空目录: {EXTERNAL_DIR}')
901
+ except OSError:
902
+ pass
903
+
904
+ print('\n✓ 蓝湖 MCP 卸载完成。')
905
+
906
+
907
+ def main():
908
+ parser = argparse.ArgumentParser(
909
+ description='蓝湖 MCP 引导器: clone + 依赖 + cookie + mcp.json 注册',
910
+ formatter_class=argparse.RawDescriptionHelpFormatter,
911
+ epilog='''示例:
912
+ python .qoder/scripts/setup_lanhu.py # 引导 + 注册 STDIO (幂等, 自动开关)
913
+ python .qoder/scripts/setup_lanhu.py --check # 只体检
914
+ python .qoder/scripts/setup_lanhu.py start # 提示 STDIO 状态 (默认无需手动)
915
+ python .qoder/scripts/setup_lanhu.py start --http # 强制 HTTP 模式 (调试/多客户端)
916
+ python .qoder/scripts/setup_lanhu.py --skip-install # 只注册 mcp.json
917
+ python .qoder/scripts/setup_lanhu.py --uninstall # 卸载
918
+ ''')
919
+ parser.add_argument('command', nargs='?', default=None,
920
+ help='子命令: start = 提示服务状态 (STDIO 自动, 加 --http 强制 HTTP)')
921
+ parser.add_argument('--check', action='store_true', help='只体检不改动')
922
+ parser.add_argument('--uninstall', action='store_true', help='卸载 (删源码 + mcp.json 条目)')
923
+ parser.add_argument('--skip-install', action='store_true',
924
+ help='只注册 mcp.json, 不 clone/装依赖')
925
+ parser.add_argument('--http', action='store_true',
926
+ help='start 时强制走 HTTP 模式 (调试/多客户端共享; 默认 STDIO 自动开关)')
927
+ parser.add_argument('--name', default='Developer', help='开发者名 (用于蓝湖 url 的 name 参数)')
928
+ args = parser.parse_args()
929
+
930
+ # 子命令: start (提示服务状态 / 强制 HTTP)
931
+ if args.command == 'start':
932
+ return start_lanhu_server(force_http=args.http)
933
+ if args.command and args.command not in (None,):
934
+ fail(f'未知子命令: {args.command}')
935
+ print(' 可用: start [--http]')
936
+ return 1
937
+
938
+ if args.check:
939
+ print_check_report()
940
+ return 0
941
+
942
+ if args.uninstall:
943
+ do_uninstall()
944
+ return 0
945
+
946
+ report = configure_lanhu(
947
+ developer_name=args.name,
948
+ dry=False,
949
+ skip_install=args.skip_install,
950
+ )
951
+
952
+ # 汇总
953
+ print('\n' + '=' * 56)
954
+ print(' 蓝湖 MCP 引导完成')
955
+ print('=' * 56)
956
+ print(f' 源码: {report["clone"]}')
957
+ print(f' 依赖: {report["deps"]}')
958
+ print(f' Cookie: {report["cookie"]}')
959
+ print(f' 注册: {report["mcp"]}')
960
+
961
+ if report['cookie'] == 'placeholder':
962
+ print(f'\n ⚠ 下一步: 编辑 {lanhu_env_file()}, 把 {COOKIE_PLACEHOLDER} 换成真 cookie')
963
+
964
+ if report['mcp'] in ('added', 'ok-existing') and not is_lanhu_running():
965
+ print_start_hint()
966
+
967
+ print('\n QoderWork 里用命令触发蓝湖 (非侵入式, 必须走命令):')
968
+ print(' /wl-design 预览 <蓝湖链接> → design-import 调 mcp__lanhu__* (命令内授权)')
969
+ return 0
970
+
971
+
972
+ if __name__ == '__main__':
973
+ sys.exit(main())