@hupan56/wlkj 2.7.11 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/bin/cli.js +375 -78
  2. package/package.json +29 -29
  3. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
  4. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
  5. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
  6. package/templates/.qoder/learning/eval-history.jsonl +28 -0
  7. package/templates/data/index/wiki-index.json +8 -0
  8. package/templates/qoder/agents/insight-planning.md +1 -1
  9. package/templates/qoder/agents/insight-research.md +28 -15
  10. package/templates/qoder/commands/optional/wl-insight.md +159 -161
  11. package/templates/qoder/commands/optional/wl-report.md +4 -4
  12. package/templates/qoder/commands/optional/wl-status.md +2 -2
  13. package/templates/qoder/commands/wl-code.md +2 -2
  14. package/templates/qoder/commands/wl-design.md +2 -2
  15. package/templates/qoder/commands/wl-init.md +3 -3
  16. package/templates/qoder/commands/wl-prd.md +2 -2
  17. package/templates/qoder/commands/wl-req.md +43 -0
  18. package/templates/qoder/commands/wl-search.md +8 -8
  19. package/templates/qoder/commands/wl-task.md +17 -17
  20. package/templates/qoder/commands/wl-test.md +41 -15
  21. package/templates/qoder/config.yaml +17 -1
  22. package/templates/qoder/hooks/session-start.py +12 -22
  23. package/templates/qoder/nul +4 -0
  24. package/templates/qoder/rules/wl-pipeline.md +22 -48
  25. package/templates/qoder/scripts/README.md +139 -0
  26. package/templates/qoder/scripts/common/autotest_auth.py +109 -0
  27. package/templates/qoder/scripts/common/bootstrap.py +145 -0
  28. package/templates/qoder/scripts/common/check_publish.py +98 -0
  29. package/templates/qoder/scripts/common/cmd_registry.py +112 -0
  30. package/templates/qoder/scripts/common/config.py +187 -0
  31. package/templates/qoder/scripts/common/contract.py +317 -0
  32. package/templates/qoder/scripts/common/developer.py +2 -1
  33. package/templates/qoder/scripts/common/feishu.py +10 -9
  34. package/templates/qoder/scripts/common/guard.py +159 -0
  35. package/templates/qoder/scripts/common/identity.py +121 -2
  36. package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
  37. package/templates/qoder/scripts/common/mcp_base.py +268 -0
  38. package/templates/qoder/scripts/common/paths.py +187 -1
  39. package/templates/qoder/scripts/common/result.py +223 -0
  40. package/templates/qoder/scripts/common/roles.py +60 -0
  41. package/templates/qoder/scripts/common/task_utils.py +21 -9
  42. package/templates/qoder/scripts/common/test_extract.py +115 -0
  43. package/templates/qoder/scripts/kg/__init__.py +11 -0
  44. package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
  45. package/templates/qoder/scripts/kg/build_relations.py +127 -0
  46. package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
  47. package/templates/qoder/scripts/kg/build_workflows.py +144 -0
  48. package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
  49. package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
  50. package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
  51. package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
  52. package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
  53. package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
  54. package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
  55. package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
  56. package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
  57. package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
  58. package/templates/qoder/scripts/kg/prefetch.py +359 -0
  59. package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
  60. package/templates/qoder/scripts/mcp/__init__.py +11 -0
  61. package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
  62. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
  63. package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
  64. package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
  65. package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
  66. package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
  67. package/templates/qoder/scripts/report/__init__.py +11 -0
  68. package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
  69. package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
  70. package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
  71. package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
  72. package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
  73. package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
  74. package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
  75. package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
  76. package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
  77. package/templates/qoder/scripts/report/req.py +222 -0
  78. package/templates/qoder/scripts/report/role.py +33 -0
  79. package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
  80. package/templates/qoder/scripts/setup/__init__.py +11 -0
  81. package/templates/qoder/scripts/setup/carriers.py +662 -0
  82. package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
  83. package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
  84. package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
  85. package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
  86. package/templates/qoder/scripts/{setup.py → setup/setup.py} +147 -0
  87. package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
  88. package/templates/qoder/scripts/task/__init__.py +11 -0
  89. package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
  90. package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
  91. package/templates/qoder/scripts/task/task.py +221 -0
  92. package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
  93. package/templates/qoder/scripts/task/task_query.py +161 -0
  94. package/templates/qoder/scripts/task/task_relations.py +424 -0
  95. package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
  96. package/templates/qoder/scripts/test/__init__.py +11 -0
  97. package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
  98. package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
  99. package/templates/qoder/scripts/test/autotest_data.py +675 -0
  100. package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
  101. package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
  102. package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
  103. package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
  104. package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
  105. package/templates/qoder/scripts/wlkj.py +116 -0
  106. package/templates/qoder/settings.json +1 -10
  107. package/templates/qoder/skills/design-import/SKILL.md +226 -226
  108. package/templates/qoder/skills/design-review/SKILL.md +82 -82
  109. package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
  110. package/templates/qoder/skills/prd-review/SKILL.md +5 -5
  111. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
  112. package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
  113. package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
  114. package/templates/qoder/skills/test-generator/SKILL.md +5 -5
  115. package/templates/qoder/skills/wl-code/SKILL.md +4 -4
  116. package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
  117. package/templates/qoder/skills/wl-design/SKILL.md +3 -3
  118. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  119. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  120. package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
  121. package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
  122. package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
  123. package/templates/qoder/skills/wl-report/SKILL.md +7 -7
  124. package/templates/qoder/skills/wl-search/SKILL.md +13 -13
  125. package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
  126. package/templates/qoder/skills/wl-status/SKILL.md +5 -5
  127. package/templates/qoder/skills/wl-task/SKILL.md +6 -6
  128. package/templates/qoder/skills/wl-test/SKILL.md +102 -39
  129. package/templates/root/AGENTS.md +39 -40
  130. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  131. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  132. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  133. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  135. package/templates/qoder/scripts/check_carriers.py +0 -238
  136. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  137. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  138. package/templates/qoder/scripts/collect_prds.py +0 -31
  139. package/templates/qoder/scripts/common/mentions.py +0 -134
  140. package/templates/qoder/scripts/common/utf8.py +0 -38
  141. package/templates/qoder/scripts/extract_routes.py +0 -54
  142. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  143. package/templates/qoder/scripts/handoff.py +0 -22
  144. package/templates/qoder/scripts/init_developer.py +0 -76
  145. package/templates/qoder/scripts/parse_prds.py +0 -33
  146. package/templates/qoder/scripts/role.py +0 -51
  147. package/templates/qoder/scripts/sync_carriers.py +0 -259
  148. package/templates/qoder/scripts/task.py +0 -1261
  149. package/templates/qoder/scripts/workspace_init.py +0 -102
  150. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  151. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  152. /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
@@ -1,298 +1,515 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- check_mcp.py - MCP 启动健康检查 (验证 init/download 后全系列可用)
5
-
6
- 为什么需要它:
7
- install_qoderwork.py --mcp-only 会写 mcp.json + launcher, 但"写了配置"不等于
8
- "MCP 能起来"。50 人里有人 pip 没装齐、有人 .repo-root 写错、有人 lanhu venv 没建,
9
- 这些都要等用的时候才发现 MCP 挂了。本脚本主动验证每个 MCP 能否真正启动。
10
-
11
- 对应需求: "确保 MCP 等 init/download 后, 所有人 qoder 全系列正常运行"
12
-
13
- 检查什么 (不真启动 server, 只验证启动条件):
14
- 1. mcp.json 存在 + 各 server 注册了 launcher 模式
15
- 2. launcher 能定位仓库根 (.repo-root 锚点 / 环境变量 / 向上找)
16
- 3. MCP 的依赖就绪:
17
- - kg: 装了 duckdb 的 Python + kg_mcp_server.py 存在
18
- - mysql: 装了 pymysql 的 Python + config.yaml 有 mysql 段
19
- - lanhu: venv 建了 + 当前角色 cookie 配了
20
- - playwright: node 在 + cli.js / npx 可用
21
-
22
- Usage:
23
- python .qoder/scripts/check_mcp.py # 全量检查
24
- python .qoder/scripts/check_mcp.py --fix # 检查 + 尝试修复 (.repo-root 等)
25
- Exit: 0 = 全部就绪, 1 = 有 MCP 未就绪
26
- """
27
- import json
28
- import os
29
- import subprocess
30
- import sys
31
- from pathlib import Path
32
-
33
- if sys.platform == 'win32':
34
- try:
35
- sys.stdout.reconfigure(encoding='utf-8')
36
- except (AttributeError, IOError):
37
- pass
38
-
39
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
40
- from common.paths import get_repo_root
41
-
42
- BASE = get_repo_root()
43
- HOME = Path(os.environ.get('USERPROFILE') or os.path.expanduser('~'))
44
- QODERWORK_DIR = HOME / '.qoderwork'
45
- MCP_FILE = QODERWORK_DIR / 'mcp.json'
46
- REPO_ROOT_ANCHOR = QODERWORK_DIR / '.repo-root'
47
-
48
- OK_TAG = '[OK] '
49
- WARN_TAG = '[WARN]'
50
- MISS_TAG = '[MISS]'
51
- BAD_TAG = '[BAD] '
52
-
53
- issues = []
54
-
55
-
56
- def say(tag, msg):
57
- print('{} {}'.format(tag, msg))
58
-
59
-
60
- def problem(msg, hint=None):
61
- issues.append(msg)
62
- say(BAD_TAG, msg)
63
- if hint:
64
- print(' -> ' + hint)
65
-
66
-
67
- def _run(cmd, timeout=10):
68
- """跑命令, 返回 (returncode, output)。"""
69
- try:
70
- r = subprocess.run(cmd, capture_output=True, text=True,
71
- encoding='utf-8', errors='replace', timeout=timeout)
72
- return r.returncode, (r.stdout or '') + (r.stderr or '')
73
- except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
74
- return 1, ''
75
-
76
-
77
- def _find_python_with(mod):
78
- """找一个装了指定模块的 Python。返回路径或 None。"""
79
- # 当前 Python
80
- try:
81
- __import__(mod)
82
- return sys.executable
83
- except ImportError:
84
- pass
85
- # 其他 Python
86
- import shutil
87
- for name in ('python', 'python3', 'py'):
88
- p = shutil.which(name)
89
- if p and p != sys.executable:
90
- rc, _ = _run([p, '-c', 'import %s' % mod], timeout=8)
91
- if rc == 0:
92
- return p
93
- return None
94
-
95
-
96
- # ============================================================
97
- # 检查项
98
- # ============================================================
99
-
100
- def check_mcp_json(fix=False):
101
- """1. mcp.json 存在 + launcher 模式。"""
102
- print('\n--- 1. mcp.json 配置 ---')
103
- if not MCP_FILE.is_file():
104
- problem('mcp.json 不存在 (%s)' % MCP_FILE,
105
- '跑: python .qoder/scripts/install_qoderwork.py --mcp-only')
106
- return False
107
- try:
108
- cfg = json.loads(MCP_FILE.read_text(encoding='utf-8'))
109
- except (ValueError, OSError):
110
- problem('mcp.json 解析失败', '文件损坏, 重跑 install_qoderwork.py --mcp-only')
111
- return False
112
- servers = cfg.get('mcpServers', {})
113
- if not servers:
114
- problem('mcp.json 无 mcpServers', '重跑 install_qoderwork.py --mcp-only')
115
- return False
116
- say(OK_TAG, 'mcp.json 存在, 注册了 %d 个 server: %s' % (
117
- len(servers), ', '.join(servers.keys())))
118
- # 检查是否 launcher 模式 (command 用 mcp_launcher.py)
119
- launcher_count = 0
120
- for name, entry in servers.items():
121
- args = entry.get('args', []) if isinstance(entry, dict) else []
122
- if any('mcp_launcher.py' in str(a) for a in args):
123
- launcher_count += 1
124
- if launcher_count > 0:
125
- say(OK_TAG, '%d 个 server 用 launcher 模式 (跨机器通用)' % launcher_count)
126
- else:
127
- say(WARN_TAG, '无 launcher 模式 server (可能写死路径), 升级: install_qoderwork.py --mcp-only')
128
- return True
129
-
130
-
131
- def check_repo_root(fix=False):
132
- """2. launcher 能定位仓库根 (.repo-root 锚点)。"""
133
- print('\n--- 2. 仓库根定位 ---')
134
- if REPO_ROOT_ANCHOR.is_file():
135
- try:
136
- root_val = REPO_ROOT_ANCHOR.read_text(encoding='utf-8').strip()
137
- if (Path(root_val) / '.qoder' / 'scripts').is_dir():
138
- say(OK_TAG, '.repo-root 锚点正确: %s' % root_val)
139
- return True
140
- else:
141
- problem('.repo-root 指向的目录不是仓库根: %s' % root_val,
142
- '重跑 install_qoderwork.py --mcp-only 会用 PROJECT_ROOT 修正')
143
- except OSError:
144
- pass
145
- # 锚点缺失或错误
146
- if fix:
147
- try:
148
- REPO_ROOT_ANCHOR.parent.mkdir(parents=True, exist_ok=True)
149
- REPO_ROOT_ANCHOR.write_text(str(BASE), encoding='utf-8')
150
- say(OK_TAG, '.repo-root 锚点已修复: %s -> %s' % (REPO_ROOT_ANCHOR, BASE))
151
- return True
152
- except OSError as e:
153
- problem('修复 .repo-root 失败: %s' % e)
154
- else:
155
- problem('.repo-root 锚点缺失或错误',
156
- ' --fix 自动修复, 或重跑 install_qoderwork.py --mcp-only')
157
- return False
158
-
159
-
160
- def check_launcher_file(fix=False):
161
- """3. launcher 脚本存在于 ~/.qoderwork/。"""
162
- print('\n--- 3. MCP launcher ---')
163
- launcher = QODERWORK_DIR / 'mcp_launcher.py'
164
- if launcher.is_file():
165
- say(OK_TAG, 'launcher 存在: %s' % launcher)
166
- return True
167
- problem('launcher 不存在: %s' % launcher,
168
- '重跑 install_qoderwork.py --mcp-only (会复制 launcher)')
169
- return False
170
-
171
-
172
- def check_kg():
173
- """4. kg MCP: duckdb + kg_mcp_server.py。"""
174
- print('\n--- 4. 知识图谱 MCP (kg) ---')
175
- server = BASE / '.qoder' / 'scripts' / 'kg_mcp_server.py'
176
- if not server.is_file():
177
- problem('kg_mcp_server.py 不存在', '引擎不完整, 重跑 npx wlkj update')
178
- return
179
- py = _find_python_with('duckdb')
180
- if py:
181
- say(OK_TAG, 'kg 就绪 (python=%s, 装了 duckdb)' % py)
182
- else:
183
- problem('找不到装了 duckdb Python',
184
- 'pip install duckdb 或 npx @hupan56/wlkj install-env')
185
-
186
-
187
- def check_mysql():
188
- """5. mysql MCP: pymysql + config.yaml mysql 段。"""
189
- print('\n--- 5. MySQL MCP ---')
190
- server = BASE / '.qoder' / 'scripts' / 'mysql_mcp_server.py'
191
- if not server.is_file():
192
- problem('mysql_mcp_server.py 不存在')
193
- return
194
- py = _find_python_with('pymysql')
195
- if not py:
196
- problem('找不到装了 pymysql 的 Python', 'pip install pymysql')
197
- return
198
- # config.yaml mysql 段
199
- cfg_path = BASE / '.qoder' / 'config.yaml'
200
- has_mysql = False
201
- if cfg_path.is_file():
202
- try:
203
- import yaml
204
- d = yaml.safe_load(cfg_path.read_text(encoding='utf-8')) or {}
205
- has_mysql = bool(d.get('mysql', {}).get('host'))
206
- except Exception:
207
- pass
208
- if has_mysql:
209
- say(OK_TAG, 'mysql 就绪 (config.yaml 已配连接信息)')
210
- else:
211
- say(WARN_TAG, 'mysql 依赖就绪, 但 config.yaml 无 mysql 段 (按需配)')
212
-
213
-
214
- def check_lanhu():
215
- """6. lanhu MCP: venv + cookie。三态 (就绪/未装/未配)。"""
216
- print('\n--- 6. 蓝湖 MCP (lanhu, 可选) ---')
217
- venv = BASE / 'external' / 'lanhu-mcp' / '.venv'
218
- server = BASE / 'external' / 'lanhu-mcp' / 'lanhu_mcp_server.py'
219
- if not venv.is_dir() or not server.is_file():
220
- say(WARN_TAG, 'lanhu 未安装 (团队不用蓝湖可忽略; 用的话: setup_lanhu.py)')
221
- return
222
- # venv python
223
- venv_py = venv / ('Scripts' if sys.platform == 'win32' else 'bin') / ('python.exe' if sys.platform == 'win32' else 'python')
224
- if not venv_py.is_file():
225
- problem('lanhu venv python 不存在', '重装: setup_lanhu.py')
226
- return
227
- # 当前角色 cookie
228
- try:
229
- from common.paths import get_developer
230
- dev = get_developer(BASE)
231
- except Exception:
232
- dev = None
233
- if not dev:
234
- say(WARN_TAG, 'lanhu venv 就绪, 但未初始化开发者 ( /wl-init)')
235
- return
236
- cookie_file = BASE / 'workspace' / 'members' / dev / '.secrets' / 'lanhu.env'
237
- if cookie_file.is_file():
238
- say(OK_TAG, 'lanhu 就绪 (角色=%s, cookie 已配)' % dev)
239
- else:
240
- say(WARN_TAG, 'lanhu venv 就绪, 但角色 %s 的 cookie 未配 (%s)' % (dev, cookie_file))
241
-
242
-
243
- def check_playwright():
244
- """7. playwright MCP: node + cli.js/npx。"""
245
- print('\n--- 7. Playwright MCP (浏览器测试) ---')
246
- import shutil
247
- node = shutil.which('node')
248
- if not node:
249
- problem('node 未安装 (playwright MCP 需要)', '装 Node.js: npx wlkj install-env')
250
- return
251
- # 优先 cli.js (快), 回退 npx (慢)
252
- try:
253
- out = subprocess.run('npm root -g', shell=True, capture_output=True,
254
- text=True, timeout=10)
255
- root = out.stdout.strip()
256
- cli = os.path.join(root, '@playwright', 'mcp', 'cli.js')
257
- if os.path.isfile(cli):
258
- say(OK_TAG, 'playwright 就绪 (node + cli.js, 快启动)')
259
- return
260
- except Exception:
261
- pass
262
- if shutil.which('npx'):
263
- say(WARN_TAG, 'playwright 可用但走 npx (冷启动慢, 可能被 QoderWork 超时杀)')
264
- say(WARN_TAG, ' 建议全局装: npm i -g @playwright/mcp')
265
- else:
266
- problem('npx 不可用', '装 Node.js')
267
-
268
-
269
- def main():
270
- fix = '--fix' in sys.argv
271
- print('=' * 56)
272
- print('MCP 健康检查 (验证 init/download 后全系列可用)')
273
- print('=' * 56)
274
-
275
- check_mcp_json(fix)
276
- check_repo_root(fix)
277
- check_launcher_file(fix)
278
- check_kg()
279
- check_mysql()
280
- check_lanhu()
281
- check_playwright()
282
-
283
- print('\n' + '=' * 56)
284
- if issues:
285
- print('未就绪 (%d):' % len(issues))
286
- for i in issues:
287
- print(' - ' + i)
288
- print('=' * 56)
289
- print('一键修复: python .qoder/scripts/check_mcp.py --fix')
290
- print('重配 MCP: python .qoder/scripts/install_qoderwork.py --mcp-only')
291
- return 1
292
- print('所有 MCP 就绪, Qoder 全系列可正常调用。')
293
- print('=' * 56)
294
- return 0
295
-
296
-
297
- if __name__ == '__main__':
298
- sys.exit(main())
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """mcp_doctor.py - MCP 诊断 (Facade 外观模式)
4
+
5
+ v3.0 合并自: check_mcp(启动条件) + check_mcp_launch(实际启动)。
6
+ 两者都是"MCP诊断"一个职责的两个阶段, 对外应是1个命令的子操作。
7
+
8
+ 用法:
9
+ python mcp_doctor.py conditions [--fix] 启动条件检查 (依赖/配置/mcp.json)
10
+ python mcp_doctor.py launch 实际启动诊断 (逐个发initialize看谁黄)
11
+ """
12
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
13
+ import os as _o, sys as _s
14
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
15
+ if _cp not in _s.path: _s.path.insert(0, _cp)
16
+ from bootstrap import setup; setup()
17
+
18
+
19
+
20
+ # ==================== 来自 check_mcp.py (启动条件检查) ====================
21
+ """
22
+ check_mcp.py - MCP 启动健康检查 (验证 init/download 后全系列可用)
23
+
24
+ 为什么需要它:
25
+ install_qoderwork.py --mcp-only 会写 mcp.json + launcher, 但"写了配置"不等于
26
+ "MCP 能起来"。50 人里有人 pip 没装齐、有人 .repo-root 写错、有人 lanhu venv 没建,
27
+ 这些都要等用的时候才发现 MCP 挂了。本脚本主动验证每个 MCP 能否真正启动。
28
+
29
+ 对应需求: "确保 MCP 等 init/download 后, 所有人 qoder 全系列正常运行"。
30
+
31
+ 检查什么 (不真启动 server, 只验证启动条件):
32
+ 1. mcp.json 存在 + 各 server 注册了 launcher 模式
33
+ 2. launcher 能定位仓库根 (.repo-root 锚点 / 环境变量 / 向上找)
34
+ 3. 各 MCP 的依赖就绪:
35
+ - kg: 装了 duckdb 的 Python + kg_mcp_server.py 存在
36
+ - mysql: 装了 pymysql 的 Python + config.yaml 有 mysql 段
37
+ - lanhu: venv 建了 + 当前角色 cookie 配了
38
+ - playwright: node 在 + cli.js / npx 可用
39
+
40
+ Usage:
41
+ python .qoder/scripts/check_mcp.py # 全量检查
42
+ python .qoder/scripts/check_mcp.py --fix # 检查 + 尝试修复 (.repo-root 等)
43
+ Exit: 0 = 全部就绪, 1 = 有 MCP 未就绪
44
+ """
45
+ import json
46
+ import os
47
+ import subprocess
48
+ import sys
49
+ from pathlib import Path
50
+
51
+ if sys.platform == 'win32':
52
+ try:
53
+ sys.stdout.reconfigure(encoding='utf-8')
54
+ except (AttributeError, IOError):
55
+ pass
56
+
57
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
58
+ from common.paths import get_repo_root
59
+
60
+ BASE = get_repo_root()
61
+ HOME = Path(os.environ.get('USERPROFILE') or os.path.expanduser('~'))
62
+ QODERWORK_DIR = HOME / '.qoderwork'
63
+ MCP_FILE = QODERWORK_DIR / 'mcp.json'
64
+ REPO_ROOT_ANCHOR = QODERWORK_DIR / '.repo-root'
65
+
66
+ OK_TAG = '[OK] '
67
+ WARN_TAG = '[WARN]'
68
+ MISS_TAG = '[MISS]'
69
+ BAD_TAG = '[BAD] '
70
+
71
+ issues = []
72
+
73
+
74
+ def say(tag, msg):
75
+ print('{} {}'.format(tag, msg))
76
+
77
+
78
+ def problem(msg, hint=None):
79
+ issues.append(msg)
80
+ say(BAD_TAG, msg)
81
+ if hint:
82
+ print(' -> ' + hint)
83
+
84
+
85
+ def _run(cmd, timeout=10):
86
+ """跑命令, 返回 (returncode, output)。"""
87
+ try:
88
+ r = subprocess.run(cmd, capture_output=True, text=True,
89
+ encoding='utf-8', errors='replace', timeout=timeout)
90
+ return r.returncode, (r.stdout or '') + (r.stderr or '')
91
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
92
+ return 1, ''
93
+
94
+
95
+ def _find_python_with(mod):
96
+ """找一个装了指定模块的 Python。返回路径或 None。"""
97
+ # 当前 Python
98
+ try:
99
+ __import__(mod)
100
+ return sys.executable
101
+ except ImportError:
102
+ pass
103
+ # 其他 Python
104
+ import shutil
105
+ for name in ('python', 'python3', 'py'):
106
+ p = shutil.which(name)
107
+ if p and p != sys.executable:
108
+ rc, _ = _run([p, '-c', 'import %s' % mod], timeout=8)
109
+ if rc == 0:
110
+ return p
111
+ return None
112
+
113
+
114
+ # ============================================================
115
+ # 检查项
116
+ # ============================================================
117
+
118
+ def check_mcp_json(fix=False):
119
+ """1. mcp.json 存在 + launcher 模式。"""
120
+ print('\n--- 1. mcp.json 配置 ---')
121
+ if not MCP_FILE.is_file():
122
+ problem('mcp.json 不存在 (%s)' % MCP_FILE,
123
+ '跑: python .qoder/scripts/install_qoderwork.py --mcp-only')
124
+ return False
125
+ try:
126
+ cfg = json.loads(MCP_FILE.read_text(encoding='utf-8'))
127
+ except (ValueError, OSError):
128
+ problem('mcp.json 解析失败', '文件损坏, 重跑 install_qoderwork.py --mcp-only')
129
+ return False
130
+ servers = cfg.get('mcpServers', {})
131
+ if not servers:
132
+ problem('mcp.json mcpServers', '重跑 install_qoderwork.py --mcp-only')
133
+ return False
134
+ say(OK_TAG, 'mcp.json 存在, 注册了 %d 个 server: %s' % (
135
+ len(servers), ', '.join(servers.keys())))
136
+ # 检查是否 launcher 模式 (command 用 mcp_launcher.py)
137
+ launcher_count = 0
138
+ for name, entry in servers.items():
139
+ args = entry.get('args', []) if isinstance(entry, dict) else []
140
+ if any('mcp_launcher.py' in str(a) for a in args):
141
+ launcher_count += 1
142
+ if launcher_count > 0:
143
+ say(OK_TAG, '%d 个 server 用 launcher 模式 (跨机器通用)' % launcher_count)
144
+ else:
145
+ say(WARN_TAG, '无 launcher 模式 server (可能写死路径), 升级: install_qoderwork.py --mcp-only')
146
+ return True
147
+
148
+
149
+ def check_repo_root(fix=False):
150
+ """2. launcher 能定位仓库根 (.repo-root 锚点)。"""
151
+ print('\n--- 2. 仓库根定位 ---')
152
+ if REPO_ROOT_ANCHOR.is_file():
153
+ try:
154
+ root_val = REPO_ROOT_ANCHOR.read_text(encoding='utf-8').strip()
155
+ if (Path(root_val) / '.qoder' / 'scripts').is_dir():
156
+ say(OK_TAG, '.repo-root 锚点正确: %s' % root_val)
157
+ return True
158
+ else:
159
+ problem('.repo-root 指向的目录不是仓库根: %s' % root_val,
160
+ '重跑 install_qoderwork.py --mcp-only 会用 PROJECT_ROOT 修正')
161
+ except OSError:
162
+ pass
163
+ # 锚点缺失或错误
164
+ if fix:
165
+ try:
166
+ REPO_ROOT_ANCHOR.parent.mkdir(parents=True, exist_ok=True)
167
+ REPO_ROOT_ANCHOR.write_text(str(BASE), encoding='utf-8')
168
+ say(OK_TAG, '.repo-root 锚点已修复: %s -> %s' % (REPO_ROOT_ANCHOR, BASE))
169
+ return True
170
+ except OSError as e:
171
+ problem('修复 .repo-root 失败: %s' % e)
172
+ else:
173
+ problem('.repo-root 锚点缺失或错误',
174
+ ' --fix 自动修复, 或重跑 install_qoderwork.py --mcp-only')
175
+ return False
176
+
177
+
178
+ def check_launcher_file(fix=False):
179
+ """3. launcher 脚本存在于 ~/.qoderwork/。"""
180
+ print('\n--- 3. MCP launcher ---')
181
+ launcher = QODERWORK_DIR / 'mcp_launcher.py'
182
+ if launcher.is_file():
183
+ say(OK_TAG, 'launcher 存在: %s' % launcher)
184
+ return True
185
+ problem('launcher 不存在: %s' % launcher,
186
+ '重跑 install_qoderwork.py --mcp-only (会复制 launcher)')
187
+ return False
188
+
189
+
190
+ def check_kg():
191
+ """4. kg MCP: duckdb + kg_mcp_server.py。"""
192
+ print('\n--- 4. 知识图谱 MCP (kg) ---')
193
+ server = BASE / '.qoder' / 'scripts' / 'kg_mcp_server.py'
194
+ if not server.is_file():
195
+ problem('kg_mcp_server.py 不存在', '引擎不完整, 重跑 npx wlkj update')
196
+ return
197
+ py = _find_python_with('duckdb')
198
+ if py:
199
+ say(OK_TAG, 'kg 就绪 (python=%s, 装了 duckdb)' % py)
200
+ else:
201
+ problem('找不到装了 duckdb 的 Python',
202
+ 'pip install duckdb 或 npx @hupan56/wlkj install-env')
203
+
204
+
205
+ def check_mysql():
206
+ """5. mysql MCP: pymysql + config.yaml mysql 段。"""
207
+ print('\n--- 5. MySQL MCP ---')
208
+ server = BASE / '.qoder' / 'scripts' / 'mysql_mcp_server.py'
209
+ if not server.is_file():
210
+ problem('mysql_mcp_server.py 不存在')
211
+ return
212
+ py = _find_python_with('pymysql')
213
+ if not py:
214
+ problem('找不到装了 pymysql 的 Python', 'pip install pymysql')
215
+ return
216
+ # config.yaml mysql
217
+ cfg_path = BASE / '.qoder' / 'config.yaml'
218
+ has_mysql = False
219
+ if cfg_path.is_file():
220
+ try:
221
+ import yaml
222
+ d = yaml.safe_load(cfg_path.read_text(encoding='utf-8')) or {}
223
+ has_mysql = bool(d.get('mysql', {}).get('host'))
224
+ except Exception:
225
+ pass
226
+ if has_mysql:
227
+ say(OK_TAG, 'mysql 就绪 (config.yaml 已配连接信息)')
228
+ else:
229
+ say(WARN_TAG, 'mysql 依赖就绪, 但 config.yaml mysql 段 (按需配)')
230
+
231
+
232
+ def check_lanhu():
233
+ """6. lanhu MCP: venv + cookie。三态 (就绪/未装/未配)。"""
234
+ print('\n--- 6. 蓝湖 MCP (lanhu, 可选) ---')
235
+ venv = BASE / 'external' / 'lanhu-mcp' / '.venv'
236
+ server = BASE / 'external' / 'lanhu-mcp' / 'lanhu_mcp_server.py'
237
+ if not venv.is_dir() or not server.is_file():
238
+ say(WARN_TAG, 'lanhu 未安装 (团队不用蓝湖可忽略; 用的话: setup_lanhu.py)')
239
+ return
240
+ # venv python
241
+ venv_py = venv / ('Scripts' if sys.platform == 'win32' else 'bin') / ('python.exe' if sys.platform == 'win32' else 'python')
242
+ if not venv_py.is_file():
243
+ problem('lanhu venv python 不存在', '重装: setup_lanhu.py')
244
+ return
245
+ # 当前角色 cookie
246
+ try:
247
+ from common.paths import get_developer
248
+ dev = get_developer(BASE)
249
+ except Exception:
250
+ dev = None
251
+ if not dev:
252
+ say(WARN_TAG, 'lanhu venv 就绪, 但未初始化开发者 (跑 /wl-init)')
253
+ return
254
+ cookie_file = BASE / 'workspace' / 'members' / dev / '.secrets' / 'lanhu.env'
255
+ if cookie_file.is_file():
256
+ say(OK_TAG, 'lanhu 就绪 (角色=%s, cookie 已配)' % dev)
257
+ else:
258
+ say(WARN_TAG, 'lanhu venv 就绪, 但角色 %s 的 cookie 未配 (%s)' % (dev, cookie_file))
259
+
260
+
261
+ def check_playwright():
262
+ """7. playwright MCP: node + cli.js/npx。"""
263
+ print('\n--- 7. Playwright MCP (浏览器测试) ---')
264
+ import shutil
265
+ node = shutil.which('node')
266
+ if not node:
267
+ problem('node 未安装 (playwright MCP 需要)', '装 Node.js: npx wlkj install-env')
268
+ return
269
+ # 优先 cli.js (), 回退 npx (慢)
270
+ try:
271
+ out = subprocess.run('npm root -g', shell=True, capture_output=True,
272
+ text=True, timeout=10)
273
+ root = out.stdout.strip()
274
+ cli = os.path.join(root, '@playwright', 'mcp', 'cli.js')
275
+ if os.path.isfile(cli):
276
+ say(OK_TAG, 'playwright 就绪 (node + cli.js, 快启动)')
277
+ return
278
+ except Exception:
279
+ pass
280
+ if shutil.which('npx'):
281
+ say(WARN_TAG, 'playwright 可用但走 npx (冷启动慢, 可能被 QoderWork 超时杀)')
282
+ say(WARN_TAG, ' 建议全局装: npm i -g @playwright/mcp')
283
+ else:
284
+ problem('npx 不可用', '装 Node.js')
285
+
286
+
287
+ def main_conditions():
288
+ fix = '--fix' in sys.argv
289
+ print('=' * 56)
290
+ print('MCP 健康检查 (验证 init/download 后全系列可用)')
291
+ print('=' * 56)
292
+
293
+ check_mcp_json(fix)
294
+ check_repo_root(fix)
295
+ check_launcher_file(fix)
296
+ check_kg()
297
+ check_mysql()
298
+ check_lanhu()
299
+ check_playwright()
300
+
301
+ print('\n' + '=' * 56)
302
+ if issues:
303
+ print('未就绪 (%d):' % len(issues))
304
+ for i in issues:
305
+ print(' - ' + i)
306
+ print('=' * 56)
307
+ print('一键修复: python .qoder/scripts/check_mcp.py --fix')
308
+ print('重配 MCP: python .qoder/scripts/install_qoderwork.py --mcp-only')
309
+ return 1
310
+ print('所有 MCP 就绪, Qoder 全系列可正常调用。')
311
+ print('=' * 56)
312
+ return 0
313
+
314
+
315
+
316
+
317
+ # ==================== 来自 check_mcp_launch.py (实际启动诊断) ====================
318
+ """
319
+ check_mcp_launch.py - 诊断 MCP 为什么黄色
320
+
321
+ 逐个模拟 mcp_launcher.py 启动每个 MCP, 发送 initialize 请求,
322
+ 看哪个 MCP 启动失败/崩溃/不响应。输出明确的诊断结论。
323
+
324
+ 用法: python .qoder/scripts/check_mcp_launch.py
325
+ """
326
+ import json
327
+ import os
328
+ import subprocess
329
+ import sys
330
+ from pathlib import Path
331
+
332
+ SCRIPTS_DIR = Path(__file__).resolve().parent
333
+ BASE = SCRIPTS_DIR.parent.parent # repo root
334
+
335
+ MCP_NAMES = ['kg', 'mysql', 'zentao', 'lanhu']
336
+
337
+
338
+ def find_python_with(mod):
339
+ """找一个能 import mod 的 python."""
340
+ for py in [sys.executable, 'python', 'python3']:
341
+ try:
342
+ r = subprocess.run([py, '-c', f'import {mod}'],
343
+ capture_output=True, timeout=8)
344
+ if r.returncode == 0:
345
+ return py
346
+ except Exception:
347
+ continue
348
+ return None
349
+
350
+
351
+ def test_mcp(name):
352
+ """测试一个 MCP 能否正常响应 initialize."""
353
+ launcher = SCRIPTS_DIR / 'mcp_launcher.py'
354
+ if not launcher.is_file():
355
+ return {'name': name, 'status': 'ERROR',
356
+ 'detail': f'launcher 不存在: {launcher}'}
357
+
358
+ init_req = json.dumps({
359
+ 'jsonrpc': '2.0', 'id': 1, 'method': 'initialize',
360
+ 'params': {'capabilities': {}, 'protocolVersion': '2024-11-05',
361
+ 'clientInfo': {'name': 'diag', 'version': '1.0'}}
362
+ }) + '\n'
363
+
364
+ try:
365
+ r = subprocess.run(
366
+ [sys.executable, str(launcher), name],
367
+ input=init_req,
368
+ capture_output=True, text=True, timeout=10,
369
+ cwd=str(BASE), encoding='utf-8', errors='replace'
370
+ )
371
+ except subprocess.TimeoutExpired:
372
+ return {'name': name, 'status': 'TIMEOUT',
373
+ 'detail': '启动后 10s 无响应 (可能卡在等 stdin)'}
374
+ except Exception as e:
375
+ return {'name': name, 'status': 'ERROR',
376
+ 'detail': f'启动异常: {e}'}
377
+
378
+ stdout = r.stdout or ''
379
+ stderr = r.stderr or ''
380
+
381
+ # 解析 stdout 找 initialize 响应
382
+ init_ok = False
383
+ disabled = False
384
+ reason = ''
385
+ for line in stdout.split('\n'):
386
+ line = line.strip()
387
+ if not line:
388
+ continue
389
+ try:
390
+ resp = json.loads(line)
391
+ if resp.get('id') == 1 and 'result' in resp:
392
+ init_ok = True
393
+ si = resp['result'].get('serverInfo', {})
394
+ ver = si.get('version', '')
395
+ if 'disabled' in ver:
396
+ disabled = True
397
+ reason = si.get('reason', '')
398
+ break
399
+ except json.JSONDecodeError:
400
+ continue
401
+
402
+ if init_ok and not disabled:
403
+ return {'name': name, 'status': 'OK',
404
+ 'detail': '正常启动, 工具可用'}
405
+ elif init_ok and disabled:
406
+ return {'name': name, 'status': 'DISABLED',
407
+ 'detail': f'以"未启用"模式启动 (绿但0工具): {reason}'}
408
+ elif r.returncode != 0:
409
+ # 进程崩溃了
410
+ return {'name': name, 'status': 'CRASHED',
411
+ 'detail': f'进程退出(rc={r.returncode})。stderr:\n{stderr[-500:]}'}
412
+ else:
413
+ return {'name': name, 'status': 'NO_RESPONSE',
414
+ 'detail': f'没崩溃但没响应 initialize。stdout:\n{stdout[-300:]}\nstderr:\n{stderr[-300:]}'}
415
+
416
+
417
+ def main_launch():
418
+ print('=' * 56)
419
+ print('MCP 启动诊断')
420
+ print(f'仓库: {BASE}')
421
+ print(f'Python: {sys.executable} ({sys.version.split()[0]})')
422
+ print('=' * 56)
423
+
424
+ # 基础检查
425
+ print('\n--- 环境检查 ---')
426
+ for mod, label in [('duckdb', 'kg 依赖'), ('pymysql', 'mysql 依赖'),
427
+ ('requests', 'zentao 依赖'), ('yaml', '配置解析')]:
428
+ try:
429
+ __import__(mod)
430
+ print(f' [OK] {mod} ({label})')
431
+ except ImportError:
432
+ py = find_python_with(mod)
433
+ if py:
434
+ print(f' [WARN] {mod} 在当前Python没有, 但 {py} 有')
435
+ else:
436
+ print(f' [MISS] {mod} ({label}) — pip install {mod}')
437
+
438
+ kg_path = BASE / 'data' / 'index' / 'kg.duckdb'
439
+ print(f'\n kg.duckdb: {"存在" if kg_path.is_file() else "缺失 (git pull 失败? 非管理员跑 team_sync pull)"}')
440
+
441
+ # mcp.json 检查
442
+ home = Path(os.environ.get('USERPROFILE') or os.path.expanduser('~'))
443
+ mcp_file = home / '.qoderwork' / 'mcp.json'
444
+ print(f'\n mcp.json: {mcp_file}')
445
+ if mcp_file.is_file():
446
+ try:
447
+ cfg = json.loads(mcp_file.read_text(encoding='utf-8'))
448
+ servers = cfg.get('mcpServers', {})
449
+ for srv in ['qoder-knowledge-graph', 'qoder-mysql', 'qoder-zentao', 'lanhu']:
450
+ entry = servers.get(srv)
451
+ if entry:
452
+ cmd = entry.get('command', '?')
453
+ args = entry.get('args', [])
454
+ has_launcher = any('mcp_launcher.py' in str(a) for a in args)
455
+ print(f' {srv}: command={cmd!r} launcher={"是" if has_launcher else "否(旧模式)"}')
456
+ else:
457
+ print(f' {srv}: 未注册')
458
+ except Exception as e:
459
+ print(f' [ERR] mcp.json 解析失败: {e}')
460
+ else:
461
+ print(f' [MISS] mcp.json 不存在 (跑 npx @hupan56/wlkj update)')
462
+
463
+ # 逐个测试 MCP
464
+ print('\n--- MCP 启动测试 (模拟 QoderWork 发 initialize) ---')
465
+ results = []
466
+ for name in MCP_NAMES:
467
+ print(f'\n 测试 {name}...')
468
+ r = test_mcp(name)
469
+ results.append(r)
470
+ icon = {'OK': '🟢', 'DISABLED': '🟢(无工具)', 'CRASHED': '🔴',
471
+ 'TIMEOUT': '🟡', 'NO_RESPONSE': '🟡', 'ERROR': '🔴'}
472
+ print(f' {icon.get(r["status"], "?")} {r["status"]}: {r["name"]}')
473
+ if r['status'] not in ('OK', 'DISABLED'):
474
+ print(f' 详情: {r["detail"]}')
475
+
476
+ # 总结
477
+ print('\n' + '=' * 56)
478
+ print('诊断结论:')
479
+ ok = sum(1 for r in results if r['status'] in ('OK', 'DISABLED'))
480
+ bad = sum(1 for r in results if r['status'] not in ('OK', 'DISABLED'))
481
+ if bad == 0:
482
+ print(f' 全部 MCP 启动正常 ({ok}/{len(results)})。')
483
+ print(' 如果 QoderWork 仍显示黄色: 重启 QoderWork 或新建对话。')
484
+ print(' (mcp.json 改动需要重启才生效)')
485
+ else:
486
+ print(f' {bad} 个 MCP 启动有问题:')
487
+ for r in results:
488
+ if r['status'] not in ('OK', 'DISABLED'):
489
+ print(f' - {r["name"]}: {r["status"]}')
490
+ print('\n 常见原因:')
491
+ print(' CRASHED: Python 版本不兼容(如3.14) 或 脚本语法错误 → 看详情里的 stderr')
492
+ print(' TIMEOUT: 脚本卡住了 → 查 DuckDB 文件锁 或 网络等待')
493
+ print(' NO_RESPONSE: stdout 被 print 污染 → 看 stdout 里的非 JSON 内容')
494
+ print('=' * 56)
495
+
496
+
497
+
498
+
499
+ # ============================================================
500
+ # Facade: 统一入口分发
501
+ # ============================================================
502
+ def main():
503
+ cmd = sys.argv[1] if len(sys.argv) > 1 else 'conditions'
504
+ sys.argv = [sys.argv[0]] + sys.argv[2:] # 去掉子命令, 让各main的argparse正确解析
505
+ if cmd in ('conditions', 'check'):
506
+ sys.exit(main_conditions())
507
+ elif cmd == 'launch':
508
+ sys.exit(main_launch())
509
+ else:
510
+ print('用法: mcp_doctor.py [conditions|launch]')
511
+ print(' conditions [--fix] 启动条件检查')
512
+ print(' launch 实际启动诊断')
513
+
514
+ if __name__ == '__main__':
515
+ main()