@hupan56/wlkj 2.7.12 → 3.1.2

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