@hupan56/wlkj 2.6.0 → 2.7.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.
- package/bin/cli.js +261 -11
- package/package.json +1 -1
- package/templates/qoder/agents/insight-planning.md +67 -0
- package/templates/qoder/agents/insight-research.md +61 -0
- package/templates/qoder/agents/prd-reference.md +8 -2
- package/templates/qoder/commands/optional/wl-insight.md +275 -0
- package/templates/qoder/commands/{wl-report.md → optional/wl-report.md} +13 -5
- package/templates/qoder/commands/{wl-spec.md → optional/wl-spec.md} +1 -1
- package/templates/qoder/commands/{wl-status.md → optional/wl-status.md} +28 -2
- package/templates/qoder/commands/wl-code.md +10 -2
- package/templates/qoder/commands/wl-commit.md +1 -1
- package/templates/qoder/commands/wl-design-draw.md +78 -0
- package/templates/qoder/commands/wl-design-scan.md +108 -0
- package/templates/qoder/commands/wl-design-spec.md +154 -0
- package/templates/qoder/commands/wl-design.md +32 -0
- package/templates/qoder/commands/wl-init.md +24 -3
- package/templates/qoder/commands/wl-prd-full.md +226 -0
- package/templates/qoder/commands/wl-prd-quick.md +134 -0
- package/templates/qoder/commands/wl-prd-review.md +104 -0
- package/templates/qoder/commands/wl-prd.md +17 -311
- package/templates/qoder/commands/wl-search.md +66 -30
- package/templates/qoder/commands/wl-task.md +290 -59
- package/templates/qoder/commands/wl-test.md +92 -24
- package/templates/qoder/config.yaml +51 -15
- package/templates/qoder/hooks/inject-workflow-state.py +2 -2
- package/templates/qoder/hooks/session-start.py +82 -51
- package/templates/qoder/rules/wl-pipeline.md +216 -105
- package/templates/qoder/scripts/__pycache__/search_index.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/archive_prd.py +377 -0
- package/templates/qoder/scripts/autotest.py +1715 -0
- package/templates/qoder/scripts/autotest_batch.py +224 -0
- package/templates/qoder/scripts/autotest_run.py +297 -0
- package/templates/qoder/scripts/benchmark.py +210 -209
- package/templates/qoder/scripts/build_style_index.py +444 -4
- package/templates/qoder/scripts/check_carriers.py +238 -0
- package/templates/qoder/scripts/check_mcp.py +298 -0
- package/templates/qoder/scripts/check_qoderwork_consistency.py +166 -0
- package/templates/qoder/scripts/common/events.py +46 -0
- package/templates/qoder/scripts/common/extract.py +419 -0
- package/templates/qoder/scripts/common/graph_traverse.py +533 -0
- package/templates/qoder/scripts/common/paths.py +89 -0
- package/templates/qoder/scripts/common/pip_install.py +144 -0
- package/templates/qoder/scripts/common/platform_guard.py +61 -0
- package/templates/qoder/scripts/common/search_engine.py +205 -205
- package/templates/qoder/scripts/common/terms.py +57 -0
- package/templates/qoder/scripts/common/ts_extract.py +536 -0
- package/templates/qoder/scripts/context_pack.py +73 -13
- package/templates/qoder/scripts/enrich_prompt.py +226 -0
- package/templates/qoder/scripts/eval_prd.py +318 -231
- package/templates/qoder/scripts/export.py +487 -487
- package/templates/qoder/scripts/extract_api_params.py +246 -0
- package/templates/qoder/scripts/extract_routes.py +54 -0
- package/templates/qoder/scripts/extract_routes_tree.py +78 -0
- package/templates/qoder/scripts/fill_prototype.py +707 -0
- package/templates/qoder/scripts/gen_design_doc.py +394 -0
- package/templates/qoder/scripts/git_sync.py +27 -15
- package/templates/qoder/scripts/init_doctor.py +292 -40
- package/templates/qoder/scripts/install_qoderwork.py +366 -9
- package/templates/qoder/scripts/kg.py +708 -0
- package/templates/qoder/scripts/kg_auto_login.py +196 -0
- package/templates/qoder/scripts/kg_build.py +612 -0
- package/templates/qoder/scripts/kg_build_db.py +327 -0
- package/templates/qoder/scripts/kg_duckdb.py +549 -0
- package/templates/qoder/scripts/kg_incremental.py +393 -0
- package/templates/qoder/scripts/kg_link_db.py +224 -0
- package/templates/qoder/scripts/kg_mcp_server.py +801 -0
- package/templates/qoder/scripts/kg_semantic.py +150 -0
- package/templates/qoder/scripts/kg_test_runner.py +241 -0
- package/templates/qoder/scripts/lanhu_stdio_wrapper.py +119 -0
- package/templates/qoder/scripts/learn.py +118 -39
- package/templates/qoder/scripts/learn_aggregate.py +201 -0
- package/templates/qoder/scripts/mcp_launcher.py +359 -0
- package/templates/qoder/scripts/mysql_mcp_server.py +396 -0
- package/templates/qoder/scripts/repo_root.py +106 -0
- package/templates/qoder/scripts/role.py +12 -0
- package/templates/qoder/scripts/run_weekly_update.bat +5 -0
- package/templates/qoder/scripts/run_weekly_update.sh +5 -0
- package/templates/qoder/scripts/search_index.py +307 -60
- package/templates/qoder/scripts/secure-ls.js +5640 -0
- package/templates/qoder/scripts/setup.py +706 -641
- package/templates/qoder/scripts/setup_lanhu.py +963 -0
- package/templates/qoder/scripts/status.py +250 -11
- package/templates/qoder/scripts/sync_carriers.py +259 -0
- package/templates/qoder/scripts/syncgate.py +2 -2
- package/templates/qoder/scripts/task.py +75 -0
- package/templates/qoder/scripts/team_sync.py +60 -4
- package/templates/qoder/scripts/workspace_init.py +1 -1
- package/templates/qoder/skills/design-import/SKILL.md +226 -0
- package/templates/qoder/skills/design-import/figma-workflow.md +81 -0
- package/templates/qoder/skills/design-review/SKILL.md +82 -25
- package/templates/qoder/skills/prd-generator/SKILL.md +184 -60
- package/templates/qoder/skills/prd-review/SKILL.md +18 -1
- package/templates/qoder/skills/prompt-enrich/SKILL.md +90 -0
- package/templates/qoder/skills/prototype-generator/SKILL.md +256 -141
- package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
- package/templates/qoder/skills/spec-coder/SKILL.md +18 -1
- package/templates/qoder/skills/spec-generator/SKILL.md +18 -1
- package/templates/qoder/skills/test-generator/SKILL.md +15 -2
- package/templates/qoder/skills/wl-code/SKILL.md +55 -36
- package/templates/qoder/skills/wl-commit/SKILL.md +89 -76
- package/templates/qoder/skills/wl-design/SKILL.md +55 -0
- package/templates/qoder/skills/wl-init/SKILL.md +76 -67
- package/templates/qoder/skills/wl-insight/SKILL.md +201 -81
- package/templates/qoder/skills/wl-prd-full/SKILL.md +69 -0
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +49 -0
- package/templates/qoder/skills/wl-prd-review/SKILL.md +34 -0
- package/templates/qoder/skills/wl-report/SKILL.md +131 -107
- package/templates/qoder/skills/wl-search/SKILL.md +141 -75
- package/templates/qoder/skills/wl-spec/SKILL.md +49 -39
- package/templates/qoder/skills/wl-status/SKILL.md +83 -61
- package/templates/qoder/skills/wl-task/SKILL.md +132 -58
- package/templates/qoder/skills/wl-test/SKILL.md +406 -40
- package/templates/qoder/templates/prototype-app.html +13 -8
- package/templates/qoder/templates/prototype-web.html +376 -93
- package/templates/root/AGENTS.md +89 -34
- package/templates/root/requirements.txt +21 -0
- package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +259 -259
- package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -186
- package/templates/qoder/agents/prd-planning.md +0 -57
- package/templates/qoder/agents/prd-research.md +0 -34
- package/templates/qoder/commands/wl-insight.md +0 -51
- package/templates/qoder/scripts/__pycache__/init_doctor.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/setup.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/common/__pycache__/developer.cpython-39.pyc +0 -0
- package/templates/qoder/skills/wl-prd/SKILL.md +0 -92
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""MySQL Read-Only MCP Server (pure Python stdio, no external SDK).
|
|
4
|
+
|
|
5
|
+
提供 3 个语义化只读工具查询 MySQL 数据库 (真实数据层真相源):
|
|
6
|
+
- query_schema: 查表结构 (列名/类型/注释/约束)
|
|
7
|
+
- query_data: 查数据 (自动 LIMIT, 防 SELECT * 拉爆)
|
|
8
|
+
- query_distinct:查枚举值 (DISTINCT 某字段的去重值 + 计数)
|
|
9
|
+
|
|
10
|
+
安全设计 (硬约束):
|
|
11
|
+
1. 启动前 ping 内网, 不通 → 输出"请连接内网"并拒绝启动
|
|
12
|
+
2. SQL 只读白名单: 只允许 SELECT/SHOW/DESCRIBE/EXPLAIN/WITH
|
|
13
|
+
任何写操作 (INSERT/UPDATE/DELETE/DROP/ALTER/...) 直接拒绝
|
|
14
|
+
3. query_data 自动注入 LIMIT, 上限 200 行
|
|
15
|
+
4. 账号密码从环境变量读 (mcp.json env 配置, 不落代码)
|
|
16
|
+
|
|
17
|
+
注册: ~/.qoderwork/mcp.json 的 "qoder-mysql"。
|
|
18
|
+
工具名: mcp__qoder-mysql__query_schema / query_data / query_distinct
|
|
19
|
+
"""
|
|
20
|
+
import io
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
import json
|
|
25
|
+
import socket
|
|
26
|
+
import asyncio
|
|
27
|
+
|
|
28
|
+
# UTF-8
|
|
29
|
+
try:
|
|
30
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
31
|
+
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
# ── 配置 (从环境变量, mcp.json env 注入) ──────────────────────────────
|
|
36
|
+
MYSQL_HOST = os.environ.get('MYSQL_HOST', '10.54.6.9')
|
|
37
|
+
MYSQL_PORT = int(os.environ.get('MYSQL_PORT', '3306'))
|
|
38
|
+
MYSQL_USER = os.environ.get('MYSQL_USER', 'fywl_ics_test')
|
|
39
|
+
MYSQL_PASSWORD = os.environ.get('MYSQL_PASSWORD', 'Fywl_ics_test56476')
|
|
40
|
+
PING_HOST = MYSQL_HOST # 连通性校验目标
|
|
41
|
+
PING_TIMEOUT = 3 # 秒
|
|
42
|
+
|
|
43
|
+
# ── 环境可信度声明 (随每次工具返回, AI 不可能"忘记"边界) ──────────────
|
|
44
|
+
# QAS 测试环境: 表结构/字段/枚举取值范围 = 生产一致 (可信);
|
|
45
|
+
# 数据行/计数分布 = 测试数据, 不代表线上 (不可信)。
|
|
46
|
+
ENV_NAME = os.environ.get('MYSQL_ENV_NAME', 'QAS 测试环境')
|
|
47
|
+
ENV_NOTICE_SCHEMA = (
|
|
48
|
+
'\n[环境: %s] 表结构/字段/枚举定义与生产一致,可作为字段规格的真源。' % ENV_NAME
|
|
49
|
+
)
|
|
50
|
+
ENV_NOTICE_DATA = (
|
|
51
|
+
'\n⚠️ [环境: %s] 以下为测试数据样本,不代表线上真实分布。'
|
|
52
|
+
'字段取值范围可信,但具体数值/计数仅测试环境参考。' % ENV_NAME
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Suppress stdout non-JSON (MCP 用 stdout 传 JSON-RPC)
|
|
56
|
+
_real_stdout = sys.stdout
|
|
57
|
+
sys.stdout = sys.stderr
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ── 安全: 内网连通性校验 ──────────────────────────────────────────────
|
|
61
|
+
def check_intranet():
|
|
62
|
+
"""TCP 连通性校验 (比 ICMP ping 更可靠, 不依赖管理员权限)。
|
|
63
|
+
连 3306 端口, 通=内网可达, 不通=提示连内网。
|
|
64
|
+
返回 (ok: bool, msg: str)。
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
with socket.create_connection((MYSQL_HOST, MYSQL_PORT), timeout=PING_TIMEOUT):
|
|
68
|
+
return True, ''
|
|
69
|
+
except (socket.timeout, OSError) as e:
|
|
70
|
+
return False, (
|
|
71
|
+
'无法连接内网 MySQL (%s:%s): %s\n'
|
|
72
|
+
'⚠️ 请确认你已连接公司内网 (VPN/办公网), 然后重试。'
|
|
73
|
+
'MySQL MCP 仅在内网可用。'
|
|
74
|
+
) % (MYSQL_HOST, MYSQL_PORT, str(e)[:80])
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ── 安全: SQL 只读白名单 ──────────────────────────────────────────────
|
|
78
|
+
# 允许的 SQL 首关键字 (只读操作)
|
|
79
|
+
READONLY_PREFIXES = {
|
|
80
|
+
'select', 'show', 'describe', 'desc', 'explain', 'with',
|
|
81
|
+
}
|
|
82
|
+
# 明确禁止的关键字 (即使出现在子查询也拦截, 防注入写操作)
|
|
83
|
+
FORBIDDEN_KEYWORDS = {
|
|
84
|
+
'insert', 'update', 'delete', 'drop', 'alter', 'create', 'truncate',
|
|
85
|
+
'replace', 'rename', 'grant', 'revoke', 'lock', 'unlock', 'call',
|
|
86
|
+
'handler', 'load', 'outfile', 'dumpfile',
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def validate_readonly(sql):
|
|
91
|
+
"""校验 SQL 是否只读。返回 (ok: bool, reason: str)。"""
|
|
92
|
+
if not sql or not sql.strip():
|
|
93
|
+
return False, '空 SQL'
|
|
94
|
+
s = sql.strip().lower().lstrip('(').strip()
|
|
95
|
+
# 去掉开头的 WITH ... AS ( ... ) 后看主语句
|
|
96
|
+
# 简化: 取第一个词
|
|
97
|
+
first_word = re.split(r'[\s(]', s, 1)[0]
|
|
98
|
+
if first_word not in READONLY_PREFIXES:
|
|
99
|
+
return False, '非只读操作: 首关键字 "%s" 不在白名单 %s' % (
|
|
100
|
+
first_word, sorted(READONLY_PREFIXES))
|
|
101
|
+
# 扫描是否含危险关键字 (作为独立词, 防 delete 嵌在字段名里)
|
|
102
|
+
tokens = set(re.findall(r'\b\w+\b', s))
|
|
103
|
+
hit = tokens & FORBIDDEN_KEYWORDS
|
|
104
|
+
if hit:
|
|
105
|
+
return False, 'SQL 含禁止的关键字: %s (只读模式拒绝写操作)' % sorted(hit)
|
|
106
|
+
return True, ''
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def inject_limit(sql, max_rows=200):
|
|
110
|
+
"""给 SELECT 自动加 LIMIT (若用户没写)。防拉爆。"""
|
|
111
|
+
s = sql.strip().rstrip(';')
|
|
112
|
+
# 已有 limit → 不动 (但截断到 max_rows 由 fetch 控制)
|
|
113
|
+
if re.search(r'\blimit\s+\d+', s, re.IGNORECASE):
|
|
114
|
+
return s
|
|
115
|
+
return s + ' LIMIT %d' % max_rows
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ── MySQL 连接 (惰性) ─────────────────────────────────────────────────
|
|
119
|
+
_conn = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _get_conn():
|
|
123
|
+
"""惰性建连 (首次查询时才连)。"""
|
|
124
|
+
global _conn
|
|
125
|
+
if _conn is not None:
|
|
126
|
+
return _conn
|
|
127
|
+
# 用最普及的 pymysql, 纯 Python, 无编译依赖
|
|
128
|
+
import pymysql
|
|
129
|
+
_conn = pymysql.connect(
|
|
130
|
+
host=MYSQL_HOST, port=MYSQL_PORT,
|
|
131
|
+
user=MYSQL_USER, password=MYSQL_PASSWORD,
|
|
132
|
+
charset='utf8mb4',
|
|
133
|
+
cursorclass=pymysql.cursors.DictCursor,
|
|
134
|
+
connect_timeout=5, read_timeout=10,
|
|
135
|
+
)
|
|
136
|
+
return _conn
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _execute_query(sql, db=None, max_rows=200):
|
|
140
|
+
"""执行只读查询, 返回 (columns, rows)。失败抛异常。"""
|
|
141
|
+
ok, reason = validate_readonly(sql)
|
|
142
|
+
if not ok:
|
|
143
|
+
raise ValueError('SQL 被拒: ' + reason)
|
|
144
|
+
conn = _get_conn()
|
|
145
|
+
if db:
|
|
146
|
+
conn.select_db(db)
|
|
147
|
+
cur = conn.cursor()
|
|
148
|
+
try:
|
|
149
|
+
cur.execute(sql)
|
|
150
|
+
rows = cur.fetchmany(max_rows) # 硬上限, 不全拉
|
|
151
|
+
cols = [d[0] for d in cur.description] if cur.description else []
|
|
152
|
+
return cols, rows
|
|
153
|
+
finally:
|
|
154
|
+
cur.close()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ── MCP 工具定义 ──────────────────────────────────────────────────────
|
|
158
|
+
TOOLS = [
|
|
159
|
+
{
|
|
160
|
+
"name": "query_schema",
|
|
161
|
+
"description": "查表结构: 列名/类型/注释/是否可空/主键。用于写 PRD/spec 时核对真实字段。例: query_schema(table='t_quality_case') 或 query_schema(table='t_quality_case', db='fywl_ics')",
|
|
162
|
+
"inputSchema": {
|
|
163
|
+
"type": "object",
|
|
164
|
+
"properties": {
|
|
165
|
+
"table": {"type": "string", "description": "表名 (如 t_quality_case)"},
|
|
166
|
+
"db": {"type": "string", "description": "库名 (可选, 不填用默认库)"},
|
|
167
|
+
},
|
|
168
|
+
"required": ["table"],
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"name": "query_data",
|
|
173
|
+
"description": "查数据: 按条件查表数据, 自动 LIMIT 200 行。[QAS测试环境] 数据行是测试数据, 不代表线上真实分布, 仅参考字段含义/格式。例: query_data(table='t_quality_case', columns='id,case_status', where='create_time > \"2026-06-01\"', limit=10)",
|
|
174
|
+
"inputSchema": {
|
|
175
|
+
"type": "object",
|
|
176
|
+
"properties": {
|
|
177
|
+
"table": {"type": "string", "description": "表名"},
|
|
178
|
+
"columns": {"type": "string", "description": "列名 (逗号分隔, 默认 *)"},
|
|
179
|
+
"where": {"type": "string", "description": "WHERE 条件 (不带 WHERE 关键字)"},
|
|
180
|
+
"db": {"type": "string", "description": "库名 (可选)"},
|
|
181
|
+
"limit": {"type": "integer", "description": "行数上限 (默认200, 最大200)"},
|
|
182
|
+
},
|
|
183
|
+
"required": ["table"],
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"name": "query_distinct",
|
|
188
|
+
"description": "查枚举值: 某字段的去重值。[QAS测试环境] 取值范围可信(与生产一致), 计数不可信(测试分布)。用于确认状态/类型字段有哪些取值。例: query_distinct(table='t_quality_case', column='case_status')",
|
|
189
|
+
"inputSchema": {
|
|
190
|
+
"type": "object",
|
|
191
|
+
"properties": {
|
|
192
|
+
"table": {"type": "string", "description": "表名"},
|
|
193
|
+
"column": {"type": "string", "description": "要查枚举的列名"},
|
|
194
|
+
"db": {"type": "string", "description": "库名 (可选)"},
|
|
195
|
+
"limit": {"type": "integer", "description": "返回枚举值上限 (默认50)"},
|
|
196
|
+
},
|
|
197
|
+
"required": ["table", "column"],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"name": "list_databases",
|
|
202
|
+
"description": "列出所有可见的数据库。用于发现有哪些库可查。例: list_databases()",
|
|
203
|
+
"inputSchema": {"type": "object", "properties": {}},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"name": "list_tables",
|
|
207
|
+
"description": "列出某库的表 (支持模糊匹配)。用于发现有哪些表。例: list_tables(db='fywl_ics', pattern='quality%')",
|
|
208
|
+
"inputSchema": {
|
|
209
|
+
"type": "object",
|
|
210
|
+
"properties": {
|
|
211
|
+
"db": {"type": "string", "description": "库名"},
|
|
212
|
+
"pattern": {"type": "string", "description": "表名 LIKE 模式 (可选, 如 quality%)"},
|
|
213
|
+
},
|
|
214
|
+
"required": ["db"],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ── 工具执行 ──────────────────────────────────────────────────────────
|
|
221
|
+
def _execute_tool(name, args):
|
|
222
|
+
args = args or {}
|
|
223
|
+
# 内网校验 (每次工具调用都校验, 防 VPN 中途断开)
|
|
224
|
+
ok, msg = check_intranet()
|
|
225
|
+
if not ok:
|
|
226
|
+
return msg
|
|
227
|
+
|
|
228
|
+
if name == 'list_databases':
|
|
229
|
+
cols, rows = _execute_query('SHOW DATABASES')
|
|
230
|
+
dbs = [list(r.values())[0] for r in rows]
|
|
231
|
+
return '数据库 (%d 个):\n %s' % (len(dbs), '\n '.join(dbs))
|
|
232
|
+
|
|
233
|
+
if name == 'list_tables':
|
|
234
|
+
db = args.get('db', '')
|
|
235
|
+
pattern = args.get('pattern', '')
|
|
236
|
+
sql = 'SHOW TABLES'
|
|
237
|
+
if pattern:
|
|
238
|
+
sql += ' LIKE "%s"' % pattern.replace('"', '')
|
|
239
|
+
cols, rows = _execute_query(sql, db=db)
|
|
240
|
+
tables = [list(r.values())[0] for r in rows]
|
|
241
|
+
return '表 (%d 个, 库=%s):\n %s' % (len(tables), db, '\n '.join(tables[:100]))
|
|
242
|
+
|
|
243
|
+
if name == 'query_schema':
|
|
244
|
+
table = args.get('table', '').replace('"', '').replace("'", '')
|
|
245
|
+
db = args.get('db')
|
|
246
|
+
sql = (
|
|
247
|
+
'SELECT column_name, data_type, column_type, is_nullable, '
|
|
248
|
+
'column_key, column_default, column_comment '
|
|
249
|
+
'FROM information_schema.columns '
|
|
250
|
+
'WHERE table_name = "%s" ' % table
|
|
251
|
+
)
|
|
252
|
+
if db:
|
|
253
|
+
sql += 'AND table_schema = "%s" ' % db.replace('"', '')
|
|
254
|
+
sql += 'ORDER BY ordinal_position'
|
|
255
|
+
cols, rows = _execute_query(sql)
|
|
256
|
+
if not rows:
|
|
257
|
+
return '未找到表: %s (检查表名或库名)' % table
|
|
258
|
+
lines = ['%-20s %-12s %-6s %-6s %s' % (
|
|
259
|
+
'列名', '类型', '可空', '键', '注释')]
|
|
260
|
+
for r in rows:
|
|
261
|
+
lines.append('%-20s %-12s %-6s %-6s %s' % (
|
|
262
|
+
r.get('column_name', ''), r.get('column_type', '')[:12],
|
|
263
|
+
r.get('is_nullable', '')[:3], r.get('column_key', ''),
|
|
264
|
+
(r.get('column_comment', '') or '')[:30]))
|
|
265
|
+
return '\n'.join(lines) + ENV_NOTICE_SCHEMA
|
|
266
|
+
|
|
267
|
+
if name == 'query_data':
|
|
268
|
+
table = args.get('table', '').replace('"', '').replace("'", '')
|
|
269
|
+
columns = args.get('columns') or '*'
|
|
270
|
+
where = args.get('where', '').strip()
|
|
271
|
+
db = args.get('db')
|
|
272
|
+
limit = min(int(args.get('limit', 200)), 200)
|
|
273
|
+
sql = 'SELECT %s FROM `%s`' % (columns, table)
|
|
274
|
+
if where:
|
|
275
|
+
sql += ' WHERE %s' % where
|
|
276
|
+
sql = inject_limit(sql, limit)
|
|
277
|
+
cols, rows = _execute_query(sql, db=db, max_rows=limit)
|
|
278
|
+
if not rows:
|
|
279
|
+
return '无数据 (表=%s)' % table
|
|
280
|
+
lines = [' | '.join(cols)]
|
|
281
|
+
lines.append('-' * 40)
|
|
282
|
+
for r in rows[:limit]:
|
|
283
|
+
lines.append(' | '.join(str(r.get(c, ''))[:30] for c in cols))
|
|
284
|
+
return '\n'.join(lines) + ENV_NOTICE_DATA
|
|
285
|
+
|
|
286
|
+
if name == 'query_distinct':
|
|
287
|
+
table = args.get('table', '').replace('"', '').replace("'", '')
|
|
288
|
+
column = args.get('column', '').replace('"', '').replace("'", '')
|
|
289
|
+
db = args.get('db')
|
|
290
|
+
limit = min(int(args.get('limit', 50)), 50)
|
|
291
|
+
sql = (
|
|
292
|
+
'SELECT `%s` AS val, COUNT(*) AS cnt FROM `%s` '
|
|
293
|
+
'GROUP BY `%s` ORDER BY cnt DESC LIMIT %d'
|
|
294
|
+
) % (column, table, column, limit)
|
|
295
|
+
cols, rows = _execute_query(sql, db=db, max_rows=limit)
|
|
296
|
+
if not rows:
|
|
297
|
+
return '无数据 (表=%s 列=%s)' % (table, column)
|
|
298
|
+
# 取值范围可信(和生产一致), 但计数是测试环境分布(不可信)
|
|
299
|
+
lines = ['枚举值 (表=%s 列=%s, 取值范围可信):' % (table, column)]
|
|
300
|
+
for r in rows:
|
|
301
|
+
lines.append(' %s: %s 次 (计数仅测试环境)' % (r.get('val', ''), r.get('cnt', '')))
|
|
302
|
+
return '\n'.join(lines) + ENV_NOTICE_DATA
|
|
303
|
+
|
|
304
|
+
return '未知工具: %s' % name
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# ── MCP Stdio Protocol (JSON-RPC 2.0, 同构 kg_mcp_server.py) ──────────
|
|
308
|
+
def _send(msg):
|
|
309
|
+
_real_stdout.write(json.dumps(msg, ensure_ascii=False) + '\n')
|
|
310
|
+
_real_stdout.flush()
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
async def _handle(req):
|
|
314
|
+
method = req.get('method', '')
|
|
315
|
+
req_id = req.get('id')
|
|
316
|
+
params = req.get('params', {})
|
|
317
|
+
|
|
318
|
+
if method == 'initialize':
|
|
319
|
+
# 启动时做一次内网校验, 不通就在 serverInfo 里提示
|
|
320
|
+
ok, msg = check_intranet()
|
|
321
|
+
info = {"name": "qoder-mysql", "version": "1.0.0"}
|
|
322
|
+
if not ok:
|
|
323
|
+
info['warning'] = msg
|
|
324
|
+
_send({
|
|
325
|
+
"jsonrpc": "2.0", "id": req_id,
|
|
326
|
+
"result": {
|
|
327
|
+
"protocolVersion": "2024-11-05",
|
|
328
|
+
"capabilities": {"tools": {}},
|
|
329
|
+
"serverInfo": info,
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
elif method == 'notifications/initialized':
|
|
333
|
+
pass
|
|
334
|
+
elif method == 'tools/list':
|
|
335
|
+
_send({"jsonrpc": "2.0", "id": req_id, "result": {"tools": TOOLS}})
|
|
336
|
+
elif method == 'tools/call':
|
|
337
|
+
tool_name = params.get('name', '')
|
|
338
|
+
arguments = params.get('arguments', {})
|
|
339
|
+
try:
|
|
340
|
+
result_text = await asyncio.get_event_loop().run_in_executor(
|
|
341
|
+
None, _execute_tool, tool_name, arguments)
|
|
342
|
+
_send({
|
|
343
|
+
"jsonrpc": "2.0", "id": req_id,
|
|
344
|
+
"result": {"content": [{"type": "text", "text": result_text}],
|
|
345
|
+
"isError": False}
|
|
346
|
+
})
|
|
347
|
+
except ValueError as e:
|
|
348
|
+
# SQL 被拒 (只读校验)
|
|
349
|
+
_send({
|
|
350
|
+
"jsonrpc": "2.0", "id": req_id,
|
|
351
|
+
"result": {"content": [{"type": "text",
|
|
352
|
+
"text": "❌ " + str(e)}],
|
|
353
|
+
"isError": True}
|
|
354
|
+
})
|
|
355
|
+
except Exception as e:
|
|
356
|
+
_send({
|
|
357
|
+
"jsonrpc": "2.0", "id": req_id,
|
|
358
|
+
"result": {"content": [{"type": "text",
|
|
359
|
+
"text": "[error] %s" % str(e)[:200]}],
|
|
360
|
+
"isError": True}
|
|
361
|
+
})
|
|
362
|
+
elif method == 'ping':
|
|
363
|
+
_send({"jsonrpc": "2.0", "id": req_id, "result": {}})
|
|
364
|
+
elif req_id is not None:
|
|
365
|
+
_send({"jsonrpc": "2.0", "id": req_id,
|
|
366
|
+
"error": {"code": -32601, "message": "Method not found: %s" % method}})
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def main():
|
|
370
|
+
import threading
|
|
371
|
+
while True:
|
|
372
|
+
try:
|
|
373
|
+
line = sys.stdin.buffer.readline()
|
|
374
|
+
except (EOFError, OSError):
|
|
375
|
+
break
|
|
376
|
+
if not line:
|
|
377
|
+
break
|
|
378
|
+
line = line.strip()
|
|
379
|
+
if not line:
|
|
380
|
+
continue
|
|
381
|
+
try:
|
|
382
|
+
req = json.loads(line.decode('utf-8'))
|
|
383
|
+
loop = asyncio.new_event_loop()
|
|
384
|
+
loop.run_until_complete(_handle(req))
|
|
385
|
+
loop.close()
|
|
386
|
+
except json.JSONDecodeError:
|
|
387
|
+
pass
|
|
388
|
+
except Exception as e:
|
|
389
|
+
print('[mysql-mcp] error: %s' % str(e), file=sys.stderr)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
if __name__ == '__main__':
|
|
393
|
+
try:
|
|
394
|
+
main()
|
|
395
|
+
except KeyboardInterrupt:
|
|
396
|
+
pass
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
repo_root.py — 自适应定位 QODER 流水线仓库根(QoderWork 桌面端专用)。
|
|
4
|
+
|
|
5
|
+
为什么需要它:
|
|
6
|
+
QoderWork 桌面端的工作目录是临时会话目录(workspace/xxx), 不是仓库根。
|
|
7
|
+
命令文件里的相对路径 .qoder/scripts/xxx.py 全部失效。
|
|
8
|
+
本脚本从 ~/.qoderwork/mcp.json 的 kg_mcp_server.py 绝对路径反推仓库根,
|
|
9
|
+
自适应(换机器重装 install 后 mcp.json 会更新, 无需硬编码)。
|
|
10
|
+
|
|
11
|
+
用法(命令文件里):
|
|
12
|
+
python ~/.qoderwork/repo_root.py # 打印仓库根绝对路径
|
|
13
|
+
python ~/.qoderwork/repo_root.py --script kg.py # 打印仓库内脚本的绝对路径
|
|
14
|
+
python ~/.qoderwork/repo_root.py --template prd-quick-template.md
|
|
15
|
+
|
|
16
|
+
R=$(python ~/.qoderwork/repo_root.py) # 在 Bash 里捕获仓库根
|
|
17
|
+
python "$R/.qoder/scripts/kg.py" context 保险 --platform web
|
|
18
|
+
"""
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def find_repo_root():
|
|
25
|
+
"""定位 QODER 仓库根。优先级:
|
|
26
|
+
1. ~/.qoderwork/.repo-root 文件 (install 时写, 主路径)
|
|
27
|
+
2. 环境变量 QODER_REPO
|
|
28
|
+
3. 向上找 .qoder/scripts/ (cwd 在仓库内)
|
|
29
|
+
4. 本脚本所在位置 (开发态)
|
|
30
|
+
失败返回 None。
|
|
31
|
+
"""
|
|
32
|
+
home = os.path.expanduser('~')
|
|
33
|
+
|
|
34
|
+
# 方法1: .repo-root 锚点文件 (install_qoderwork 写的, 最可靠)
|
|
35
|
+
anchor = os.path.join(home, '.qoderwork', '.repo-root')
|
|
36
|
+
if os.path.isfile(anchor):
|
|
37
|
+
try:
|
|
38
|
+
line = open(anchor, encoding='utf-8').read().strip()
|
|
39
|
+
if line and os.path.isdir(os.path.join(line, '.qoder', 'scripts')):
|
|
40
|
+
return line
|
|
41
|
+
except OSError:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
# 方法2: 环境变量
|
|
45
|
+
env_repo = os.environ.get('QODER_REPO')
|
|
46
|
+
if env_repo and os.path.isdir(os.path.join(env_repo, '.qoder', 'scripts')):
|
|
47
|
+
return env_repo
|
|
48
|
+
|
|
49
|
+
# 方法3: 向上找 .qoder 目录(适用于工作目录在仓库内的情况, 如 IDE)
|
|
50
|
+
here = os.path.abspath(os.getcwd())
|
|
51
|
+
for _ in range(8):
|
|
52
|
+
if os.path.isdir(os.path.join(here, '.qoder', 'scripts')):
|
|
53
|
+
return here
|
|
54
|
+
parent = os.path.dirname(here)
|
|
55
|
+
if parent == here:
|
|
56
|
+
break
|
|
57
|
+
here = parent
|
|
58
|
+
|
|
59
|
+
# 方法4: 本脚本所在位置(若它被放到仓库 .qoder/scripts/ 下)
|
|
60
|
+
try:
|
|
61
|
+
s = os.path.abspath(__file__)
|
|
62
|
+
root = os.path.dirname(os.path.dirname(os.path.dirname(s)))
|
|
63
|
+
if os.path.isdir(os.path.join(root, '.qoder', 'scripts')):
|
|
64
|
+
return root
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main():
|
|
72
|
+
root = find_repo_root()
|
|
73
|
+
if not root:
|
|
74
|
+
print('ERROR: 无法定位 QODER 仓库根。请确认 ~/.qoderwork/mcp.json 存在且指向有效的 kg_mcp_server.py', file=sys.stderr)
|
|
75
|
+
return 1
|
|
76
|
+
|
|
77
|
+
# 子路径模式: --script / --template
|
|
78
|
+
args = sys.argv[1:]
|
|
79
|
+
if args and args[0] in ('--script', '--scripts'):
|
|
80
|
+
# 打印仓库内 .qoder/scripts/<name> 的绝对路径
|
|
81
|
+
name = args[1] if len(args) > 1 else ''
|
|
82
|
+
print(os.path.join(root, '.qoder', 'scripts', name))
|
|
83
|
+
elif args and args[0] in ('--template', '--templates'):
|
|
84
|
+
name = args[1] if len(args) > 1 else ''
|
|
85
|
+
print(os.path.join(root, '.qoder', 'templates', name))
|
|
86
|
+
elif args and args[0] == '--test':
|
|
87
|
+
# 自检: 验证关键文件存在
|
|
88
|
+
checks = [
|
|
89
|
+
('kg.py', '.qoder/scripts/kg.py'),
|
|
90
|
+
('eval_prd.py', '.qoder/scripts/eval_prd.py'),
|
|
91
|
+
('quick模板', '.qoder/templates/prd-quick-template.md'),
|
|
92
|
+
('full模板', '.qoder/templates/prd-full-template.md'),
|
|
93
|
+
('kg.duckdb', 'data/index/kg.duckdb'),
|
|
94
|
+
]
|
|
95
|
+
print('仓库根: %s' % root)
|
|
96
|
+
for label, rel in checks:
|
|
97
|
+
ok = os.path.isfile(os.path.join(root, rel))
|
|
98
|
+
print(' %s %s' % ('✅' if ok else '❌', label))
|
|
99
|
+
else:
|
|
100
|
+
# 默认: 只打印仓库根
|
|
101
|
+
print(root)
|
|
102
|
+
return 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == '__main__':
|
|
106
|
+
sys.exit(main())
|
|
@@ -37,3 +37,15 @@ def get_permissions(role):
|
|
|
37
37
|
roles = config.get('roles', {})
|
|
38
38
|
r = roles.get(role, {})
|
|
39
39
|
return r.get('permissions', [])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == '__main__':
|
|
43
|
+
# CLI: 输出当前角色名 (供 skill 脚本动态读角色用)
|
|
44
|
+
# 用法: ROLE=$(python role.py)
|
|
45
|
+
try:
|
|
46
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
47
|
+
except (AttributeError, IOError):
|
|
48
|
+
pass
|
|
49
|
+
r = get_role()
|
|
50
|
+
print(r or 'pm') # 无角色默认 pm (软引导, 不报错)
|
|
51
|
+
|
|
@@ -10,6 +10,11 @@ echo [%date% %time%] Starting weekly update... >> .qoder\logs\weekly-update.log
|
|
|
10
10
|
REM Step 1: Git pull + PRD collect + Index rebuild
|
|
11
11
|
python .qoder\scripts\git_sync.py >> .qoder\logs\weekly-update.log 2>&1
|
|
12
12
|
|
|
13
|
+
REM Step 2: Rebuild style index (layout fingerprint + module index + page patterns).
|
|
14
|
+
REM git_sync only rebuilds keyword/api/module indexes; style index must be rebuilt
|
|
15
|
+
REM separately so layout_fingerprint/module-index pick up code changes each week.
|
|
16
|
+
python .qoder\scripts\build_style_index.py >> .qoder\logs\weekly-update.log 2>&1
|
|
17
|
+
|
|
13
18
|
if errorlevel 1 (
|
|
14
19
|
echo [%date% %time%] UPDATE FAILED - check log above >> .qoder\logs\weekly-update.log
|
|
15
20
|
) else (
|
|
@@ -10,6 +10,11 @@ LOG=.qoder/logs/weekly-update.log
|
|
|
10
10
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting weekly update..." >> "$LOG"
|
|
11
11
|
|
|
12
12
|
python3 .qoder/scripts/git_sync.py >> "$LOG" 2>&1
|
|
13
|
+
|
|
14
|
+
# Step 2: Rebuild style index (layout fingerprint + module index + page patterns).
|
|
15
|
+
# git_sync only rebuilds keyword/api/module indexes; style index must be rebuilt
|
|
16
|
+
# separately so layout_fingerprint/module-index pick up code changes each week.
|
|
17
|
+
python3 .qoder/scripts/build_style_index.py >> "$LOG" 2>&1
|
|
13
18
|
STATUS=$?
|
|
14
19
|
|
|
15
20
|
if [ $STATUS -ne 0 ]; then
|