@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
@@ -0,0 +1,461 @@
1
+ #!/usr/bin/env python
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
+
14
+ """MySQL Read-Only MCP Server (pure Python stdio, no external SDK).
15
+
16
+ 提供 3 个语义化只读工具查询 MySQL 数据库 (真实数据层真相源):
17
+ - query_schema: 查表结构 (列名/类型/注释/约束)
18
+ - query_data: 查数据 (自动 LIMIT, 防 SELECT * 拉爆)
19
+ - query_distinct:查枚举值 (DISTINCT 某字段的去重值 + 计数)
20
+
21
+ 安全设计 (硬约束):
22
+ 1. 启动前 ping 内网, 不通 → 输出"请连接内网"并拒绝启动
23
+ 2. SQL 只读白名单: 只允许 SELECT/SHOW/DESCRIBE/EXPLAIN/WITH
24
+ 任何写操作 (INSERT/UPDATE/DELETE/DROP/ALTER/...) 直接拒绝
25
+ 3. query_data 自动注入 LIMIT, 上限 200 行
26
+ 4. 账号密码从环境变量读 (mcp.json env 配置, 不落代码)
27
+
28
+ 注册: ~/.qoderwork/mcp.json 的 "qoder-mysql"。
29
+ 工具名: mcp__qoder-mysql__query_schema / query_data / query_distinct
30
+ """
31
+ import io
32
+ import os
33
+ import re
34
+ import sys
35
+ import json
36
+ import socket
37
+ import asyncio
38
+
39
+ # UTF-8
40
+ try:
41
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
42
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
43
+ except Exception:
44
+ pass
45
+
46
+ # ── 配置 (v3.x 多环境: 全部环境从 MYSQL_ENVIRONMENTS JSON 读, 运行时按 env 取) ──
47
+ # launcher 把 config.yaml 的 mysql.environments 整体序列化成 JSON 注入这里。
48
+ # server 启动后不再锁定单一连接, 而是按工具调用的 env 参数取对应环境建连。
49
+ PING_TIMEOUT = 3 # 秒
50
+
51
+
52
+ def _load_environments():
53
+ """解析全部环境配置。返回 dict[env_key -> {host,port,user,password,env_name}]。
54
+ 数据源优先级: MYSQL_ENVIRONMENTS (JSON) > 旧扁平 MYSQL_HOST/... (包成单环境)。
55
+ 解析结果缓存到模块级 _envs。"""
56
+ global _envs
57
+ if _envs is not None:
58
+ return _envs
59
+ _envs = {}
60
+ raw = os.environ.get('MYSQL_ENVIRONMENTS', '')
61
+ if raw:
62
+ try:
63
+ data = json.loads(raw)
64
+ if isinstance(data, dict):
65
+ for k, v in data.items():
66
+ if isinstance(v, dict) and v.get('host'):
67
+ _envs[k] = v
68
+ except (json.JSONDecodeError, TypeError):
69
+ pass
70
+ # 兼容旧扁平 env (launcher 没注入 JSON 时, 直接从 MYSQL_HOST 等读)
71
+ if not _envs:
72
+ h = os.environ.get('MYSQL_HOST', '')
73
+ if h:
74
+ _envs = {'test': {
75
+ 'host': h,
76
+ 'port': int(os.environ.get('MYSQL_PORT', '3306')),
77
+ 'user': os.environ.get('MYSQL_USER', ''),
78
+ 'password': os.environ.get('MYSQL_PASSWORD', ''),
79
+ 'env_name': os.environ.get('MYSQL_ENV_NAME', h),
80
+ }}
81
+ return _envs
82
+
83
+
84
+ def _default_env():
85
+ """默认环境 key。优先 MYSQL_DEFAULT_ENV, 否则取第一个环境。"""
86
+ d = os.environ.get('MYSQL_DEFAULT_ENV', '')
87
+ if d and d in _load_environments():
88
+ return d
89
+ envs = _load_environments()
90
+ return next(iter(envs), 'test') if envs else 'test'
91
+
92
+
93
+ def _env_config(env_key):
94
+ """取某环境的配置 dict。不存在返回 None。"""
95
+ return _load_environments().get(env_key)
96
+
97
+
98
+ def _env_notice(env_key, kind='data'):
99
+ """生成当前环境可信度声明 (随查询结果返回, AI 不可能"忘记"边界)。
100
+ kind='schema' = 表结构 (可信); kind='data' = 数据行 (测试/生产语义不同)。"""
101
+ cfg = _env_config(env_key) or {}
102
+ name = cfg.get('env_name', env_key)
103
+ is_prod = 'prod' in str(env_key).lower() or '生产' in name
104
+ if kind == 'schema':
105
+ if is_prod:
106
+ return '\n🔴 [环境: %s] 表结构/字段/枚举定义 (生产环境, 仅核对用)。' % name
107
+ return '\n[环境: %s] 表结构/字段/枚举定义与生产一致,可作为字段规格的真源。' % name
108
+ # data
109
+ if is_prod:
110
+ return ('\n🔴⚠️ [环境: %s] 以下为生产真实数据! 仅核对字段含义/格式, '
111
+ '禁止作为业务结论外传。' % name)
112
+ return ('\n⚠️ [环境: %s] 以下为测试数据样本,不代表线上真实分布。'
113
+ '字段取值范围可信,但具体数值/计数仅测试环境参考。' % name)
114
+
115
+
116
+ _envs = None # 缓存: 解析一次
117
+
118
+
119
+ # stdout 重定向 + 协议层由 BaseMCPServer 接管 (v3.0 重构)
120
+
121
+
122
+ # ── 安全: 内网连通性校验 ──────────────────────────────────────────────
123
+ def check_intranet(env_key=None):
124
+ """TCP 连通性校验 (比 ICMP ping 更可靠, 不依赖管理员权限)。
125
+ 连目标环境 3306 端口, 通=内网可达, 不通=提示连内网。
126
+ env_key=None 时用默认环境。返回 (ok: bool, msg: str)。"""
127
+ env_key = env_key or _default_env()
128
+ cfg = _env_config(env_key)
129
+ if not cfg:
130
+ return False, '未知环境 "%s",可选: %s' % (env_key, list(_load_environments()))
131
+ host, port = cfg.get('host', ''), int(cfg.get('port', 3306))
132
+ try:
133
+ with socket.create_connection((host, port), timeout=PING_TIMEOUT):
134
+ return True, ''
135
+ except (socket.timeout, OSError) as e:
136
+ return False, (
137
+ '无法连接 MySQL [%s] (%s:%s): %s\n'
138
+ '⚠️ 请确认你已连接公司内网 (VPN/办公网) 且该环境地址可达, 然后重试。'
139
+ ) % (env_key, host, port, str(e)[:80])
140
+
141
+
142
+ # ── 安全: SQL 只读白名单 ──────────────────────────────────────────────
143
+ # 允许的 SQL 首关键字 (只读操作)
144
+ READONLY_PREFIXES = {
145
+ 'select', 'show', 'describe', 'desc', 'explain', 'with',
146
+ }
147
+ # 明确禁止的关键字 (即使出现在子查询也拦截, 防注入写操作)
148
+ FORBIDDEN_KEYWORDS = {
149
+ 'insert', 'update', 'delete', 'drop', 'alter', 'create', 'truncate',
150
+ 'replace', 'rename', 'grant', 'revoke', 'lock', 'unlock', 'call',
151
+ 'handler', 'load', 'outfile', 'dumpfile',
152
+ }
153
+
154
+
155
+ def validate_readonly(sql):
156
+ """校验 SQL 是否只读。返回 (ok: bool, reason: str)。"""
157
+ if not sql or not sql.strip():
158
+ return False, '空 SQL'
159
+ s = sql.strip().lower().lstrip('(').strip()
160
+ # 去掉开头的 WITH ... AS ( ... ) 后看主语句
161
+ # 简化: 取第一个词
162
+ first_word = re.split(r'[\s(]', s, 1)[0]
163
+ if first_word not in READONLY_PREFIXES:
164
+ return False, '非只读操作: 首关键字 "%s" 不在白名单 %s' % (
165
+ first_word, sorted(READONLY_PREFIXES))
166
+ # 扫描是否含危险关键字 (作为独立词, 防 delete 嵌在字段名里)
167
+ tokens = set(re.findall(r'\b\w+\b', s))
168
+ hit = tokens & FORBIDDEN_KEYWORDS
169
+ if hit:
170
+ return False, 'SQL 含禁止的关键字: %s (只读模式拒绝写操作)' % sorted(hit)
171
+ return True, ''
172
+
173
+
174
+ def inject_limit(sql, max_rows=200):
175
+ """给 SELECT 自动加 LIMIT (若用户没写)。防拉爆。"""
176
+ s = sql.strip().rstrip(';')
177
+ # 已有 limit → 不动 (但截断到 max_rows 由 fetch 控制)
178
+ if re.search(r'\blimit\s+\d+', s, re.IGNORECASE):
179
+ return s
180
+ return s + ' LIMIT %d' % max_rows
181
+
182
+
183
+ # ── MySQL 连接 (惰性, 多环境连接池) ────────────────────────────────────
184
+ # 每个环境一个连接, 按 env_key 缓存。运行时切库 = 取不同 env_key 的连接。
185
+ _conns = {} # env_key -> connection
186
+
187
+
188
+ def _get_conn(env_key):
189
+ """惰性建连 (首次查某环境时才连)。连接按 env_key 缓存复用。"""
190
+ if env_key in _conns:
191
+ return _conns[env_key]
192
+ cfg = _env_config(env_key)
193
+ if not cfg:
194
+ raise ValueError('未知环境 "%s",可选: %s' % (env_key, list(_load_environments())))
195
+ # 用最普及的 pymysql, 纯 Python, 无编译依赖
196
+ import pymysql
197
+ _conns[env_key] = pymysql.connect(
198
+ host=cfg.get('host', ''), port=int(cfg.get('port', 3306)),
199
+ user=cfg.get('user', ''), password=cfg.get('password', ''),
200
+ charset='utf8mb4',
201
+ cursorclass=pymysql.cursors.DictCursor,
202
+ connect_timeout=5, read_timeout=10,
203
+ )
204
+ return _conns[env_key]
205
+
206
+
207
+ def _execute_query(sql, db=None, max_rows=200, env_key=None):
208
+ """执行只读查询, 返回 (columns, rows)。失败抛异常。
209
+ env_key 指定走哪个环境 (默认 default_env)。"""
210
+ ok, reason = validate_readonly(sql)
211
+ if not ok:
212
+ raise ValueError('SQL 被拒: ' + reason)
213
+ env_key = env_key or _default_env()
214
+ conn = _get_conn(env_key)
215
+ if db:
216
+ conn.select_db(db)
217
+ cur = conn.cursor()
218
+ try:
219
+ cur.execute(sql)
220
+ rows = cur.fetchmany(max_rows) # 硬上限, 不全拉
221
+ cols = [d[0] for d in cur.description] if cur.description else []
222
+ return cols, rows
223
+ finally:
224
+ cur.close()
225
+
226
+
227
+ # ── MCP 工具定义 ──────────────────────────────────────────────────────
228
+ # 所有查询工具都带可选 env 参数, 取 config.yaml mysql.environments 的 key
229
+ # (如 test/pre/prod)。不填用 default_env。调用方 (AI) 应先问用户查哪个环境。
230
+ _ENV_PROP = {"type": "string", "description":
231
+ "环境标签 (config.yaml mysql.environments 的 key, 如 test/pre/prod)。"
232
+ "不填用默认环境。先问用户查哪个环境再传。"}
233
+
234
+ TOOLS = [
235
+ {
236
+ "name": "list_envs",
237
+ "description": "列出所有可用的数据库环境 (config.yaml mysql.environments)。AI 查库前先调用此工具, 把可选环境列给用户选。例: list_envs()",
238
+ "inputSchema": {"type": "object", "properties": {}},
239
+ },
240
+ {
241
+ "name": "query_schema",
242
+ "description": "查表结构: 列名/类型/注释/是否可空/主键。用于写 PRD/spec 时核对真实字段。例: query_schema(table='t_quality_case', env='test')",
243
+ "inputSchema": {
244
+ "type": "object",
245
+ "properties": {
246
+ "table": {"type": "string", "description": "表名 (如 t_quality_case)"},
247
+ "db": {"type": "string", "description": "库名 (可选, 不填用默认库)"},
248
+ "env": _ENV_PROP,
249
+ },
250
+ "required": ["table"],
251
+ },
252
+ },
253
+ {
254
+ "name": "query_data",
255
+ "description": "查数据: 按条件查表数据, 自动 LIMIT 200 行。数据行语义随环境不同 (测试=样本, 生产=真实数据)。例: query_data(table='t_quality_case', columns='id,case_status', env='test', limit=10)",
256
+ "inputSchema": {
257
+ "type": "object",
258
+ "properties": {
259
+ "table": {"type": "string", "description": "表名"},
260
+ "columns": {"type": "string", "description": "列名 (逗号分隔, 默认 *)"},
261
+ "where": {"type": "string", "description": "WHERE 条件 (不带 WHERE 关键字)"},
262
+ "db": {"type": "string", "description": "库名 (可选)"},
263
+ "limit": {"type": "integer", "description": "行数上限 (默认200, 最大200)"},
264
+ "env": _ENV_PROP,
265
+ },
266
+ "required": ["table"],
267
+ },
268
+ },
269
+ {
270
+ "name": "query_distinct",
271
+ "description": "查枚举值: 某字段的去重值。取值范围可信(各环境一致), 计数语义随环境不同。例: query_distinct(table='t_quality_case', column='case_status', env='test')",
272
+ "inputSchema": {
273
+ "type": "object",
274
+ "properties": {
275
+ "table": {"type": "string", "description": "表名"},
276
+ "column": {"type": "string", "description": "要查枚举的列名"},
277
+ "db": {"type": "string", "description": "库名 (可选)"},
278
+ "limit": {"type": "integer", "description": "返回枚举值上限 (默认50)"},
279
+ "env": _ENV_PROP,
280
+ },
281
+ "required": ["table", "column"],
282
+ },
283
+ },
284
+ {
285
+ "name": "list_databases",
286
+ "description": "列出某环境所有可见的数据库。用于发现有哪些库可查。例: list_databases(env='test')",
287
+ "inputSchema": {"type": "object", "properties": {"env": _ENV_PROP}},
288
+ },
289
+ {
290
+ "name": "list_tables",
291
+ "description": "列出某环境某库的表 (支持模糊匹配)。用于发现有哪些表。例: list_tables(db='fywl_ics', pattern='quality%', env='test')",
292
+ "inputSchema": {
293
+ "type": "object",
294
+ "properties": {
295
+ "db": {"type": "string", "description": "库名"},
296
+ "pattern": {"type": "string", "description": "表名 LIKE 模式 (可选, 如 quality%)"},
297
+ "env": _ENV_PROP,
298
+ },
299
+ "required": ["db"],
300
+ },
301
+ },
302
+ ]
303
+
304
+
305
+ # ── 工具执行 ──────────────────────────────────────────────────────────
306
+ def _execute_tool(name, args):
307
+ args = args or {}
308
+
309
+ # list_envs: 不需要连库, 直接返回可选环境 (供 AI 列给用户选)
310
+ if name == 'list_envs':
311
+ envs = _load_environments()
312
+ if not envs:
313
+ return ('未配置任何数据库环境。请在 .qoder/config.yaml 的 mysql.environments '
314
+ '里配置 (参考 mysql.default_env + environments 结构)。')
315
+ default = _default_env()
316
+ lines = ['可选数据库环境 (%d 个, 标 * 为默认):' % len(envs)]
317
+ for k, v in envs.items():
318
+ mark = ' *' if k == default else ''
319
+ lines.append(' %s%s - %s (%s)' % (
320
+ k, mark, v.get('env_name', k), v.get('host', '?')))
321
+ return '\n'.join(lines)
322
+
323
+ # 其它工具: 解析 env 参数, 校验环境存在
324
+ env_key = args.get('env') or _default_env()
325
+ if not _env_config(env_key):
326
+ return '❌ 未知环境 "%s",可选: %s\n(在 config.yaml mysql.environments 里配置)' % (
327
+ env_key, list(_load_environments()))
328
+
329
+ # 内网/连通性校验 (按目标环境, 每次调用都校验, 防 VPN 中途断开)
330
+ ok, msg = check_intranet(env_key)
331
+ if not ok:
332
+ return msg
333
+
334
+ if name == 'list_databases':
335
+ cols, rows = _execute_query('SHOW DATABASES', env_key=env_key)
336
+ dbs = [list(r.values())[0] for r in rows]
337
+ return '数据库 (%d 个, 环境=%s):\n %s%s' % (
338
+ len(dbs), env_key, '\n '.join(dbs), _env_notice(env_key, 'schema'))
339
+
340
+ if name == 'list_tables':
341
+ db = args.get('db', '')
342
+ pattern = args.get('pattern', '')
343
+ sql = 'SHOW TABLES'
344
+ if pattern:
345
+ sql += ' LIKE "%s"' % pattern.replace('"', '')
346
+ cols, rows = _execute_query(sql, db=db, env_key=env_key)
347
+ tables = [list(r.values())[0] for r in rows]
348
+ return '表 (%d 个, 库=%s, 环境=%s):\n %s%s' % (
349
+ len(tables), db, env_key, '\n '.join(tables[:100]),
350
+ _env_notice(env_key, 'schema'))
351
+
352
+ if name == 'query_schema':
353
+ table = args.get('table', '').replace('"', '').replace("'", '')
354
+ db = args.get('db')
355
+ sql = (
356
+ 'SELECT column_name, data_type, column_type, is_nullable, '
357
+ 'column_key, column_default, column_comment '
358
+ 'FROM information_schema.columns '
359
+ 'WHERE table_name = "%s" ' % table
360
+ )
361
+ if db:
362
+ sql += 'AND table_schema = "%s" ' % db.replace('"', '')
363
+ sql += 'ORDER BY ordinal_position'
364
+ cols, rows = _execute_query(sql, env_key=env_key)
365
+ if not rows:
366
+ return '未找到表: %s (检查表名或库名, 环境=%s)' % (table, env_key)
367
+ lines = ['%-20s %-12s %-6s %-6s %s' % (
368
+ '列名', '类型', '可空', '键', '注释')]
369
+ for r in rows:
370
+ lines.append('%-20s %-12s %-6s %-6s %s' % (
371
+ r.get('column_name', ''), r.get('column_type', '')[:12],
372
+ r.get('is_nullable', '')[:3], r.get('column_key', ''),
373
+ (r.get('column_comment', '') or '')[:30]))
374
+ return '\n'.join(lines) + _env_notice(env_key, 'schema')
375
+
376
+ if name == 'query_data':
377
+ table = args.get('table', '').replace('"', '').replace("'", '')
378
+ columns = args.get('columns') or '*'
379
+ where = args.get('where', '').strip()
380
+ db = args.get('db')
381
+ limit = min(int(args.get('limit', 200)), 200)
382
+ sql = 'SELECT %s FROM `%s`' % (columns, table)
383
+ if where:
384
+ sql += ' WHERE %s' % where
385
+ sql = inject_limit(sql, limit)
386
+ cols, rows = _execute_query(sql, db=db, max_rows=limit, env_key=env_key)
387
+ if not rows:
388
+ return '无数据 (表=%s, 环境=%s)' % (table, env_key)
389
+ lines = [' | '.join(cols)]
390
+ lines.append('-' * 40)
391
+ for r in rows[:limit]:
392
+ lines.append(' | '.join(str(r.get(c, ''))[:30] for c in cols))
393
+ return '\n'.join(lines) + _env_notice(env_key, 'data')
394
+
395
+ if name == 'query_distinct':
396
+ table = args.get('table', '').replace('"', '').replace("'", '')
397
+ column = args.get('column', '').replace('"', '').replace("'", '')
398
+ db = args.get('db')
399
+ limit = min(int(args.get('limit', 50)), 50)
400
+ sql = (
401
+ 'SELECT `%s` AS val, COUNT(*) AS cnt FROM `%s` '
402
+ 'GROUP BY `%s` ORDER BY cnt DESC LIMIT %d'
403
+ ) % (column, table, column, limit)
404
+ cols, rows = _execute_query(sql, db=db, max_rows=limit, env_key=env_key)
405
+ if not rows:
406
+ return '无数据 (表=%s 列=%s, 环境=%s)' % (table, column, env_key)
407
+ # 取值范围可信(各环境一致), 计数语义随环境 (测试=样本/生产=真实)
408
+ lines = ['枚举值 (表=%s 列=%s, 取值范围可信):' % (table, column)]
409
+ for r in rows:
410
+ lines.append(' %s: %s 次' % (r.get('val', ''), r.get('cnt', '')))
411
+ return '\n'.join(lines) + _env_notice(env_key, 'data')
412
+
413
+ return '未知工具: %s' % name
414
+
415
+
416
+ # ── MCP Server (继承 BaseMCPServer, 协议层由基类接管) ──────────────────
417
+ # v3.0 重构: 删除 _send/_handle/main 三段逐字复制代码 (~90行),
418
+ # 改为继承 BaseMCPServer, 覆写 on_tool_error 处理 ValueError(只读拒绝)。
419
+
420
+ def _bootstrap_base():
421
+ """延迟 import BaseMCPServer (scripts/ 在 sys.path)。"""
422
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
423
+ from foundation.protocol.mcp_base import BaseMCPServer
424
+ return BaseMCPServer
425
+
426
+
427
+ class MysqlMCPServer(_bootstrap_base()):
428
+ """MySQL 只读 MCP Server。继承基类协议层, 只填工具逻辑 + 异常分类。"""
429
+
430
+ SERVER_NAME = "qoder-mysql"
431
+ SERVER_VERSION = "1.0.0"
432
+ TOOLS = TOOLS # 引用上方模块级 TOOLS
433
+
434
+ def handle_tool(self, name, args):
435
+ return _execute_tool(name, args)
436
+
437
+ def on_initialize_hook(self, info):
438
+ # 启动时校验默认环境连通性, 不通塞 warning。
439
+ # 附带把可用环境列出来, 方便调用方知道有哪些环境可查。
440
+ envs = _load_environments()
441
+ if not envs:
442
+ info['warning'] = ('未配置任何 mysql 环境。请在 config.yaml 的 '
443
+ 'mysql.environments 里配置。')
444
+ return
445
+ default = _default_env()
446
+ ok, msg = check_intranet(default)
447
+ if not ok:
448
+ info['warning'] = msg
449
+ info['mysqlEnvs'] = list(envs.keys())
450
+ info['mysqlDefaultEnv'] = default
451
+
452
+ def on_tool_error(self, e):
453
+ # mysql 特有: ValueError = SQL 只读校验拒绝, 单独友好提示
454
+ if isinstance(e, ValueError):
455
+ return ("❌ " + str(e), True)
456
+ # 其它异常走基类默认
457
+ return super().on_tool_error(e)
458
+
459
+
460
+ if __name__ == '__main__':
461
+ MysqlMCPServer().run()