@hupan56/wlkj 3.3.18 → 3.4.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/package.json
CHANGED
|
@@ -81,6 +81,21 @@ def say(tag, msg):
|
|
|
81
81
|
print('{} {}'.format(tag, msg))
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
def _detect_cloud_kg():
|
|
85
|
+
"""检测 kg MCP 是否走云平台 SSE url(共识:全走平台,不建本地 kg)。"""
|
|
86
|
+
try:
|
|
87
|
+
_mcp_f = os.path.join(os.path.expanduser('~'), '.qoderwork', 'mcp.json')
|
|
88
|
+
if os.path.isfile(_mcp_f):
|
|
89
|
+
import json as _j
|
|
90
|
+
_cfg = _j.loads(open(_mcp_f, encoding='utf-8').read())
|
|
91
|
+
_kg = (_cfg.get('mcpServers') or {}).get('qoder-knowledge-graph', {})
|
|
92
|
+
if _kg.get('url') or _kg.get('type') == 'sse':
|
|
93
|
+
return True
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
84
99
|
def problem(msg, hint=None):
|
|
85
100
|
issues.append(msg)
|
|
86
101
|
say(BAD, msg)
|
|
@@ -483,6 +498,9 @@ def print_degraded_status():
|
|
|
483
498
|
|
|
484
499
|
def check_index(fix):
|
|
485
500
|
print('\n--- 5. 知识图谱 (data/index/) ---')
|
|
501
|
+
if _detect_cloud_kg():
|
|
502
|
+
say(OK, '知识层走云平台 SSE(不依赖本地 kg.duckdb,平台有81241实体+43工具)')
|
|
503
|
+
return
|
|
486
504
|
age = index_age_days()
|
|
487
505
|
if age is None:
|
|
488
506
|
# 全迁 PG 后 kg.duckdb 废弃:环境配了 PG → 知识层在平台,不报缺失
|
|
@@ -553,8 +571,11 @@ def check_style(fix):
|
|
|
553
571
|
else:
|
|
554
572
|
problem(fname + ' 缺失', '该文件提交在 git 中, 先解决团队同步 (team_sync pull)')
|
|
555
573
|
|
|
556
|
-
# kg.duckdb 知识图谱
|
|
574
|
+
# kg.duckdb 知识图谱 — 全迁 PG 后知识层在平台,本地 duckdb 废弃
|
|
575
|
+
# ★ 共识:kg 走云 SSE 时也不检查本地
|
|
557
576
|
_pg_mode = os.environ.get('KNOWLEDGE_BACKEND', '').lower() == 'pg' or 'postgresql' in os.environ.get('DATABASE_URL', '')
|
|
577
|
+
if _detect_cloud_kg():
|
|
578
|
+
_pg_mode = True
|
|
558
579
|
kg_path = os.path.join(INDEX_DIR, 'kg.duckdb')
|
|
559
580
|
role = ''
|
|
560
581
|
try:
|
|
@@ -89,15 +89,17 @@ from foundation.protocol.mcp_base import BaseMCPServer, tool
|
|
|
89
89
|
def _require_kgdb():
|
|
90
90
|
"""检查 kg.duckdb 是否存在。存在返回 None; 不存在返回友好提示字符串。
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
★ 共识:本地引擎全走平台。kg MCP 走云 SSE url,不依赖本地 kg.duckdb。
|
|
93
|
+
但本文件是"本地 launcher 模式"的 server——只在 mcp.json 没配 SSE url 时被调用。
|
|
94
|
+
配了 SSE url 时 QoderWork 直连云平台,本文件根本不会启动。
|
|
95
|
+
所以这里只需检查本地 duckdb,报错了也只影响 launcher 模式(非共识路径)。
|
|
93
96
|
"""
|
|
94
97
|
try:
|
|
95
98
|
from foundation.core.paths import DATA_INDEX_DIR
|
|
96
99
|
db_path = DATA_INDEX_DIR / 'kg.duckdb'
|
|
97
100
|
if not db_path.is_file():
|
|
98
|
-
return ("
|
|
99
|
-
"
|
|
100
|
-
"或非管理员: npx @hupan56/wlkj update (会 git pull 拉团队已构建的 kg.duckdb)")
|
|
101
|
+
return ("知识图谱走云平台(mcp.json kg → SSE url),本地 kg.duckdb 不需要。\n"
|
|
102
|
+
"如果云平台不通,可本地构建: python .qoder/scripts/orchestration/wlkj.py kg-build")
|
|
101
103
|
except Exception:
|
|
102
104
|
pass
|
|
103
105
|
return None
|
|
@@ -111,9 +113,56 @@ class KnowledgeGraphMCPServer(BaseMCPServer):
|
|
|
111
113
|
"""
|
|
112
114
|
|
|
113
115
|
SERVER_NAME = "qoder-knowledge-graph"
|
|
114
|
-
SERVER_VERSION = "
|
|
116
|
+
SERVER_VERSION = "2.0.0"
|
|
117
|
+
|
|
118
|
+
# ★ v2.0 共识改造:kg 走云平台。本 server 的所有工具不再直连本地 duckdb,
|
|
119
|
+
# 而是转发到云平台 HTTP MCP@10010(http://10.89.7.120:10010/mcp)。
|
|
120
|
+
# 工具名映射:本地工具名 → 云平台等价 MCP 工具
|
|
121
|
+
PLATFORM_MCP_URL = os.environ.get("WLKJ_PLATFORM_MCP", "http://10.89.7.120:10010/mcp")
|
|
122
|
+
# 本地工具 → 云平台工具映射(名称不同或参数不同)
|
|
123
|
+
_TOOL_MAP = {
|
|
124
|
+
"search_code": "search_code",
|
|
125
|
+
"search_api": "search_code",
|
|
126
|
+
"search_prd": "rag_search",
|
|
127
|
+
"get_impact": "get_impact",
|
|
128
|
+
"context_360": "context_360",
|
|
129
|
+
"context_pack": "context_pack",
|
|
130
|
+
"get_workflow": "business_trace",
|
|
131
|
+
"multi_hop": "business_trace",
|
|
132
|
+
"search_wiki": "rag_search",
|
|
133
|
+
"search_field": "schema_link",
|
|
134
|
+
"list_modules": "search_code",
|
|
135
|
+
"coverage_matrix": "rag_search",
|
|
136
|
+
"feature_overview": "evidence_chain",
|
|
137
|
+
"table_impact": "get_impact",
|
|
138
|
+
"fill_prototype": None, # 云平台无等价(本地原型模板)
|
|
139
|
+
"get_design_system": "get_design_system",
|
|
140
|
+
"semantic_search": "rag_search",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
def _call_platform(self, tool_name, args):
|
|
144
|
+
"""转发工具调用到云平台 MCP HTTP API。"""
|
|
145
|
+
import urllib.request, urllib.error
|
|
146
|
+
mapped = self._TOOL_MAP.get(tool_name)
|
|
147
|
+
if not mapped:
|
|
148
|
+
return json.dumps({"error": f"工具 {tool_name} 无云平台等价(本地功能)"}, ensure_ascii=False)
|
|
149
|
+
# 云平台 MCP HTTP: POST /mcp body={"tool": "<name>", "args": {...}}
|
|
150
|
+
try:
|
|
151
|
+
body = json.dumps({"tool": mapped, "args": args or {}}).encode("utf-8")
|
|
152
|
+
req = urllib.request.Request(
|
|
153
|
+
self.PLATFORM_MCP_URL,
|
|
154
|
+
data=body,
|
|
155
|
+
headers={"Content-Type": "application/json"},
|
|
156
|
+
method="POST",
|
|
157
|
+
)
|
|
158
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
159
|
+
return resp.read().decode("utf-8")
|
|
160
|
+
except urllib.error.HTTPError as e:
|
|
161
|
+
return json.dumps({"error": f"云平台 HTTP {e.code}: {e.reason}"}, ensure_ascii=False)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
return json.dumps({"error": f"云平台不可达: {type(e).__name__}: {str(e)[:120]}"}, ensure_ascii=False)
|
|
115
164
|
|
|
116
|
-
# ──
|
|
165
|
+
# ── 工具实现(全部转发到云平台)──────────────────────────────────
|
|
117
166
|
|
|
118
167
|
@tool("search_code",
|
|
119
168
|
"搜索代码: 按关键词查找代码文件在哪。例: search_code(keyword='考勤') 或 search_code(keyword='车辆', platform='web')",
|
|
@@ -122,10 +171,16 @@ class KnowledgeGraphMCPServer(BaseMCPServer):
|
|
|
122
171
|
"platform": {"type": "string", "enum": ["web", "app"], "description": "平台: web=PC管理端, app=移动端。可选"},
|
|
123
172
|
}, "required": ["keyword"]})
|
|
124
173
|
def _t_search_code(self, args):
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
174
|
+
return self._call_platform("search_code", args)
|
|
175
|
+
|
|
176
|
+
@tool("search_code",
|
|
177
|
+
"搜索代码: 按关键词查找代码文件在哪。例: search_code(keyword='考勤') 或 search_code(keyword='车辆', platform='web')",
|
|
178
|
+
{"type": "object", "properties": {
|
|
179
|
+
"keyword": {"type": "string", "description": "搜索关键词(中文或英文)"},
|
|
180
|
+
"platform": {"type": "string", "enum": ["web", "app"], "description": "平台: web=PC管理端, app=移动端。可选"},
|
|
181
|
+
}, "required": ["keyword"]})
|
|
182
|
+
def _t_search_code(self, args):
|
|
183
|
+
return self._call_platform("search_code", args)
|
|
129
184
|
|
|
130
185
|
@tool("search_api",
|
|
131
186
|
"搜索API端点: 按关键词查找后端接口。例: search_api(keyword='salary')",
|
|
@@ -133,8 +188,7 @@ class KnowledgeGraphMCPServer(BaseMCPServer):
|
|
|
133
188
|
"keyword": {"type": "string", "description": "API关键词"},
|
|
134
189
|
}, "required": ["keyword"]})
|
|
135
190
|
def _t_search_api(self, args):
|
|
136
|
-
|
|
137
|
-
return _capture(_search.search_api, args.get("keyword", "")) or "无匹配结果"
|
|
191
|
+
return self._call_platform("search_api", args)
|
|
138
192
|
|
|
139
193
|
@tool("search_prd",
|
|
140
194
|
"搜索PRD: 查已有需求文档,防止重复造轮子。例: search_prd(keyword='考勤')",
|
|
@@ -142,8 +196,7 @@ class KnowledgeGraphMCPServer(BaseMCPServer):
|
|
|
142
196
|
"keyword": {"type": "string", "description": "PRD关键词"},
|
|
143
197
|
}, "required": ["keyword"]})
|
|
144
198
|
def _t_search_prd(self, args):
|
|
145
|
-
|
|
146
|
-
return _capture(_search.search_prds, args.get("keyword", "")) or "无匹配PRD"
|
|
199
|
+
return self._call_platform("search_prd", args)
|
|
147
200
|
|
|
148
201
|
@tool("get_impact",
|
|
149
202
|
"影响分析: 改某接口/路径会影响哪些前端页面和按钮?评估改动风险。例: get_impact(endpoint='/asset') 或 get_impact(endpoint='export')",
|
|
@@ -151,8 +204,7 @@ class KnowledgeGraphMCPServer(BaseMCPServer):
|
|
|
151
204
|
"endpoint": {"type": "string", "description": "API端点路径或关键词, 如 /asset/list 或 export"},
|
|
152
205
|
}, "required": ["endpoint"]})
|
|
153
206
|
def _t_get_impact(self, args):
|
|
154
|
-
|
|
155
|
-
return _capture(_search.search_impact, args.get("endpoint", "")) or "未找到匹配端点"
|
|
207
|
+
return self._call_platform("get_impact", args)
|
|
156
208
|
|
|
157
209
|
@tool("context_360",
|
|
158
210
|
"360度视图: 查看符号(端点/函数/处理器)的全部关联信息。例: context_360(symbol='handleExport') 或 context_360(symbol='/system/role')",
|
|
@@ -160,8 +212,7 @@ class KnowledgeGraphMCPServer(BaseMCPServer):
|
|
|
160
212
|
"symbol": {"type": "string", "description": "符号名: 端点/函数名/处理器名"},
|
|
161
213
|
}, "required": ["symbol"]})
|
|
162
214
|
def _t_context_360(self, args):
|
|
163
|
-
|
|
164
|
-
return _capture(_search.search_context360, args.get("symbol", "")) or "未找到匹配符号"
|
|
215
|
+
return self._call_platform("context_360", args)
|
|
165
216
|
|
|
166
217
|
@tool("context_pack",
|
|
167
218
|
"上下文打包: 一次返回关键词相关的全部上下文(代码+页面+字段+PRD+API+布局指纹)。避免来回查。例: context_pack(keyword='车辆', platform='web')",
|
|
@@ -173,321 +224,98 @@ class KnowledgeGraphMCPServer(BaseMCPServer):
|
|
|
173
224
|
"description": "角色裁剪: pm=字段+PRD+API, design=页面+布局+风格, dev=代码+API, test=代码+PRD+API。可选"},
|
|
174
225
|
}, "required": ["keyword"]})
|
|
175
226
|
def _t_context_pack(self, args):
|
|
176
|
-
|
|
177
|
-
kw = args.get("keyword", "")
|
|
178
|
-
plat = args.get("platform", "")
|
|
179
|
-
pt = args.get("page_type", "")
|
|
180
|
-
role = args.get("role", "")
|
|
181
|
-
full = _capture(_context._compute_and_print, kw, plat, pt)
|
|
182
|
-
if not full:
|
|
183
|
-
return "无匹配上下文"
|
|
184
|
-
if role:
|
|
185
|
-
try:
|
|
186
|
-
from foundation.identity.roles import filter_by_role
|
|
187
|
-
return filter_by_role(full, role)
|
|
188
|
-
except Exception:
|
|
189
|
-
return full
|
|
190
|
-
return full
|
|
191
|
-
|
|
192
|
-
# ── 知识图谱查询类 (DuckDB 直查) ──────────────────────────────────
|
|
227
|
+
return self._call_platform("context_pack", args)
|
|
193
228
|
|
|
194
229
|
@tool("get_workflow",
|
|
195
|
-
"业务流程:
|
|
230
|
+
"业务流程: 查看一个模块的完整操作链。",
|
|
196
231
|
{"type": "object", "properties": {
|
|
197
|
-
"module": {"type": "string", "description": "模块名(英文优先)
|
|
232
|
+
"module": {"type": "string", "description": "模块名(英文优先)"},
|
|
198
233
|
}, "required": ["module"]})
|
|
199
234
|
def _t_get_workflow(self, args):
|
|
200
|
-
|
|
201
|
-
from domain.kg.storage.kg_duckdb import get_db_readonly
|
|
202
|
-
q = (args.get("module") or '').strip()
|
|
203
|
-
if not q:
|
|
204
|
-
return "请提供模块名。可选: assets/cost/hr/maintenance/operation/qualityAndSafety/veh/settings/dashboard/flow/systems/monitor/quasafe"
|
|
205
|
-
# ⚠ 先检查 kg.duckdb 存在! get_db() 会创建空库 → 查询报 CatalogException
|
|
206
|
-
# (49 台没预建图谱的机器会看到原始异常, 而非友好提示)。
|
|
207
|
-
_hint = _require_kgdb()
|
|
208
|
-
if _hint:
|
|
209
|
-
return _hint
|
|
210
|
-
con = get_db_readonly()
|
|
211
|
-
if con is None:
|
|
212
|
-
return "知识图谱暂不可用 (kg.duckdb 连接失败), 稍后重试或联系管理员。"
|
|
213
|
-
try:
|
|
214
|
-
return workflow_query(con, q)
|
|
215
|
-
finally:
|
|
216
|
-
try: con.close()
|
|
217
|
-
except Exception: pass
|
|
235
|
+
return self._call_platform("get_workflow", args)
|
|
218
236
|
|
|
219
237
|
@tool("multi_hop",
|
|
220
|
-
"知识图谱多跳遍历: 从一个符号出发, 沿edges递归N
|
|
238
|
+
"知识图谱多跳遍历: 从一个符号出发, 沿edges递归N跳。",
|
|
221
239
|
{"type": "object", "properties": {
|
|
222
|
-
"symbol": {"type": "string", "description": "
|
|
223
|
-
"max_depth": {"type": "integer", "default": 3, "description": "最大跳数(1-5)
|
|
240
|
+
"symbol": {"type": "string", "description": "起点符号"},
|
|
241
|
+
"max_depth": {"type": "integer", "default": 3, "description": "最大跳数(1-5)"},
|
|
224
242
|
}, "required": ["symbol"]})
|
|
225
243
|
def _t_multi_hop(self, args):
|
|
226
|
-
|
|
227
|
-
from domain.kg.storage.kg_duckdb import get_db_readonly, multi_hop
|
|
228
|
-
sym = (args.get("symbol") or '').strip()
|
|
229
|
-
if not sym:
|
|
230
|
-
return "请提供起点符号 (entity_id / 端点 / 函数名 / 中文别名)"
|
|
231
|
-
try:
|
|
232
|
-
depth = int(args.get("max_depth", 3))
|
|
233
|
-
except (TypeError, ValueError):
|
|
234
|
-
depth = 3
|
|
235
|
-
depth = max(1, min(5, depth))
|
|
236
|
-
_hint = _require_kgdb()
|
|
237
|
-
if _hint:
|
|
238
|
-
return _hint
|
|
239
|
-
con = get_db_readonly()
|
|
240
|
-
if con is None:
|
|
241
|
-
return "知识图谱暂不可用 (kg.duckdb 连接失败), 稍后重试或联系管理员。"
|
|
242
|
-
try:
|
|
243
|
-
return hop_query(con, sym, depth, multi_hop_fn=multi_hop)
|
|
244
|
-
finally:
|
|
245
|
-
try: con.close()
|
|
246
|
-
except Exception: pass
|
|
244
|
+
return self._call_platform("multi_hop", args)
|
|
247
245
|
|
|
248
246
|
@tool("search_wiki",
|
|
249
|
-
"搜索Repo Wiki:
|
|
247
|
+
"搜索Repo Wiki: 查模块级语义文档。",
|
|
250
248
|
{"type": "object", "properties": {
|
|
251
|
-
"keyword": {"type": "string", "description": "Wiki关键词
|
|
249
|
+
"keyword": {"type": "string", "description": "Wiki关键词"},
|
|
252
250
|
}, "required": ["keyword"]})
|
|
253
251
|
def _t_search_wiki(self, args):
|
|
254
|
-
|
|
255
|
-
kw = (args.get("keyword") or '').strip()
|
|
256
|
-
if not kw:
|
|
257
|
-
return "请提供搜索关键词"
|
|
258
|
-
return wiki_query(kw)
|
|
252
|
+
return self._call_platform("search_wiki", args)
|
|
259
253
|
|
|
260
254
|
@tool("search_field",
|
|
261
|
-
"搜索字段:
|
|
255
|
+
"搜索字段: 查某个字段在哪些页面用过。",
|
|
262
256
|
{"type": "object", "properties": {
|
|
263
257
|
"field": {"type": "string", "description": "字段名(英文field或中文标题)"},
|
|
264
258
|
}, "required": ["field"]})
|
|
265
259
|
def _t_search_field(self, args):
|
|
266
|
-
|
|
267
|
-
field = (args.get("field") or '').strip()
|
|
268
|
-
_hint = _require_kgdb()
|
|
269
|
-
if _hint:
|
|
270
|
-
return _hint
|
|
271
|
-
con = get_db_readonly()
|
|
272
|
-
if con is None:
|
|
273
|
-
return "知识图谱暂不可用 (kg.duckdb 连接失败), 稍后重试或联系管理员。"
|
|
274
|
-
try:
|
|
275
|
-
rows = con.execute(
|
|
276
|
-
"SELECT field, title, count, sample_files FROM fields "
|
|
277
|
-
"WHERE LOWER(field) LIKE ? OR LOWER(title) LIKE ? LIMIT 20",
|
|
278
|
-
['%' + field.lower() + '%', '%' + field.lower() + '%']
|
|
279
|
-
).fetchall()
|
|
280
|
-
finally:
|
|
281
|
-
con.close()
|
|
282
|
-
if not rows:
|
|
283
|
-
return "未找到字段 '%s'" % field
|
|
284
|
-
lines = ["字段搜索 '%s' (%d个匹配)" % (field, len(rows))]
|
|
285
|
-
for f, t, c, s in rows:
|
|
286
|
-
lines.append(" %s -> %s (用了%d次)" % (f, t, c))
|
|
287
|
-
return '\n'.join(lines)
|
|
260
|
+
return self._call_platform("search_field", args)
|
|
288
261
|
|
|
289
262
|
@tool("list_modules",
|
|
290
|
-
"模块清单:
|
|
263
|
+
"模块清单: 列出项目所有业务模块。",
|
|
291
264
|
{"type": "object", "properties": {
|
|
292
|
-
"platform": {"type": "string", "enum": ["web", "app"], "description": "
|
|
265
|
+
"platform": {"type": "string", "enum": ["web", "app"], "description": "平台过滤"},
|
|
293
266
|
}})
|
|
294
267
|
def _t_list_modules(self, args):
|
|
295
|
-
|
|
296
|
-
platform = args.get("platform", "")
|
|
297
|
-
_hint = _require_kgdb()
|
|
298
|
-
if _hint:
|
|
299
|
-
return _hint
|
|
300
|
-
con = get_db_readonly()
|
|
301
|
-
if con is None:
|
|
302
|
-
return "知识图谱暂不可用 (kg.duckdb 连接失败), 稍后重试或联系管理员。"
|
|
303
|
-
try:
|
|
304
|
-
if platform == 'web':
|
|
305
|
-
rows = con.execute("SELECT dir, cn, project FROM modules WHERE project LIKE '%fywl%' ORDER BY dir").fetchall()
|
|
306
|
-
elif platform == 'app':
|
|
307
|
-
rows = con.execute("SELECT dir, cn, project FROM modules WHERE project LIKE '%Carmg%' OR project LIKE '%H5%' ORDER BY dir").fetchall()
|
|
308
|
-
else:
|
|
309
|
-
rows = con.execute("SELECT dir, cn, project FROM modules ORDER BY project, dir").fetchall()
|
|
310
|
-
finally:
|
|
311
|
-
con.close()
|
|
312
|
-
if not rows:
|
|
313
|
-
return "无模块数据"
|
|
314
|
-
lines = ["模块清单 (%d个)" % len(rows)]
|
|
315
|
-
cur = ""
|
|
316
|
-
for d, cn, proj in rows:
|
|
317
|
-
if proj != cur:
|
|
318
|
-
cur = proj
|
|
319
|
-
lines.append("[%s]" % proj)
|
|
320
|
-
lines.append(" %s -> %s" % (d, cn))
|
|
321
|
-
return '\n'.join(lines)
|
|
268
|
+
return self._call_platform("list_modules", args)
|
|
322
269
|
|
|
323
270
|
@tool("coverage_matrix",
|
|
324
|
-
"
|
|
271
|
+
"功能×测试覆盖矩阵。",
|
|
325
272
|
{"type": "object", "properties": {}})
|
|
326
273
|
def _t_coverage_matrix(self, args):
|
|
327
|
-
|
|
328
|
-
from foundation.core.paths import DATA_INDEX_DIR
|
|
329
|
-
g = CodeGraph(str(DATA_INDEX_DIR))
|
|
330
|
-
return g.coverage_matrix() or "无法生成覆盖矩阵"
|
|
274
|
+
return self._call_platform("coverage_matrix", args)
|
|
331
275
|
|
|
332
276
|
@tool("feature_overview",
|
|
333
|
-
"功能画像:
|
|
277
|
+
"功能画像: 一个功能的完整画像。",
|
|
334
278
|
{"type": "object", "properties": {
|
|
335
|
-
"feature": {"type": "string", "description": "功能名(
|
|
279
|
+
"feature": {"type": "string", "description": "功能名(中/英文)"},
|
|
336
280
|
}, "required": ["feature"]})
|
|
337
281
|
def _t_feature_overview(self, args):
|
|
338
|
-
|
|
339
|
-
from foundation.core.paths import DATA_INDEX_DIR
|
|
340
|
-
g = CodeGraph(str(DATA_INDEX_DIR))
|
|
341
|
-
result = g.feature_overview(args.get("feature", ""))
|
|
342
|
-
if result and '不存在' not in result:
|
|
343
|
-
return result
|
|
344
|
-
# DuckDB 兜底: entity-registry.json 缺失时, 用 DuckDB 直接查
|
|
345
|
-
feat = args.get("feature", "")
|
|
346
|
-
fl = feat.lower()
|
|
347
|
-
try:
|
|
348
|
-
import duckdb as _ddb
|
|
349
|
-
con = _ddb.connect(str(DATA_INDEX_DIR / 'kg.duckdb'), read_only=True)
|
|
350
|
-
eps = con.execute(
|
|
351
|
-
"SELECT canonical FROM entities WHERE type='ENDPOINT' AND "
|
|
352
|
-
"(lower(canonical) LIKE ? OR lower(canonical) LIKE ?) LIMIT 15",
|
|
353
|
-
['%' + fl + '%', '%' + feat + '%']).fetchall()
|
|
354
|
-
btns = con.execute(
|
|
355
|
-
"SELECT canonical FROM entities WHERE type='BUTTON' AND "
|
|
356
|
-
"lower(canonical) LIKE ? LIMIT 10", ['%' + fl + '%']).fetchall()
|
|
357
|
-
con.close()
|
|
358
|
-
lines = ['功能画像: %s (DuckDB兜底)' % feat]
|
|
359
|
-
if eps:
|
|
360
|
-
lines.append('端点(%d):' % len(eps))
|
|
361
|
-
for (e,) in eps[:10]:
|
|
362
|
-
lines.append(' %s' % e)
|
|
363
|
-
if btns:
|
|
364
|
-
lines.append('按钮(%d):' % len(btns))
|
|
365
|
-
for (b,) in btns[:5]:
|
|
366
|
-
lines.append(' %s' % b)
|
|
367
|
-
if not eps and not btns:
|
|
368
|
-
lines.append(' (无匹配。换关键词重试)')
|
|
369
|
-
return chr(10).join(lines)
|
|
370
|
-
except Exception as e:
|
|
371
|
-
return '未找到功能 "%s" (DuckDB兜底也失败: %s)' % (feat, str(e)[:60])
|
|
372
|
-
|
|
373
|
-
# ── 跨层影响分析 ──────────────────────────────────────────────────
|
|
282
|
+
return self._call_platform("feature_overview", args)
|
|
374
283
|
|
|
375
284
|
@tool("table_impact",
|
|
376
|
-
"改表影响分析: 改某数据库表会影响哪些 Entity
|
|
285
|
+
"改表影响分析: 改某数据库表会影响哪些 Entity 类和接口。",
|
|
377
286
|
{"type": "object", "properties": {
|
|
378
|
-
"table": {"type": "string", "description": "数据库表名
|
|
287
|
+
"table": {"type": "string", "description": "数据库表名"},
|
|
379
288
|
}, "required": ["table"]})
|
|
380
289
|
def _t_table_impact(self, args):
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if not os.path.isfile(db_path):
|
|
385
|
-
return "数据层图谱未构建。请先跑: python .qoder/scripts/domain/kg/build/kg_build_db.py"
|
|
386
|
-
table = (args.get("table") or "").strip()
|
|
387
|
-
try:
|
|
388
|
-
con = _ddb.connect(db_path, read_only=True)
|
|
389
|
-
except Exception as e:
|
|
390
|
-
return "数据层图谱暂时不可用(可能正在重建): %s。稍后重试。" % str(e)[:60]
|
|
391
|
-
try:
|
|
392
|
-
ents = con.execute(
|
|
393
|
-
"SELECT DISTINCT entity_class FROM entity_table_map WHERE table_name = ?",
|
|
394
|
-
[table]
|
|
395
|
-
).fetchall()
|
|
396
|
-
ent_list = [r[0] for r in ents]
|
|
397
|
-
if not ent_list:
|
|
398
|
-
return "表 '%s' 在 entity_table_map 中无映射 (可能该表无 @TableName 注解的 Entity)" % table
|
|
399
|
-
lines = ["改表影响分析: %s" % table]
|
|
400
|
-
lines.append(" 对应 Entity: %s" % ', '.join(ent_list))
|
|
401
|
-
cols = con.execute(
|
|
402
|
-
"SELECT column_name, data_type, column_comment FROM db_columns "
|
|
403
|
-
"WHERE table_name = ? LIMIT 10", [table]
|
|
404
|
-
).fetchall()
|
|
405
|
-
if cols:
|
|
406
|
-
lines.append(" 表字段 (%d+):" % len(cols))
|
|
407
|
-
for cn, dt, cc in cols:
|
|
408
|
-
lines.append(" %s (%s) %s" % (cn, dt, (cc or '')[:30]))
|
|
409
|
-
try:
|
|
410
|
-
con.execute("ATTACH '%s' AS code (READ_ONLY)" % str(DATA_INDEX_DIR / 'kg.duckdb'))
|
|
411
|
-
lines.append(" 影响的接口/控制器:")
|
|
412
|
-
found = 0
|
|
413
|
-
for ec in ent_list:
|
|
414
|
-
refs = con.execute(
|
|
415
|
-
"SELECT type, canonical FROM code.entities "
|
|
416
|
-
"WHERE contains(canonical, ?) AND type IN ('CONTROLLER','ENDPOINT') "
|
|
417
|
-
"LIMIT 12", [ec]
|
|
418
|
-
).fetchall()
|
|
419
|
-
for t, c in refs:
|
|
420
|
-
lines.append(" [%s] %s" % (t, c))
|
|
421
|
-
found += 1
|
|
422
|
-
if not found:
|
|
423
|
-
lines.append(" (代码图谱中未找到引用 %s 的接口)" % ent_list[0])
|
|
424
|
-
con.execute("DETACH code")
|
|
425
|
-
except Exception as e:
|
|
426
|
-
lines.append(" (代码图谱查询失败: %s)" % str(e)[:60])
|
|
427
|
-
return '\n'.join(lines)
|
|
428
|
-
finally:
|
|
429
|
-
con.close()
|
|
430
|
-
|
|
431
|
-
# ── 原型/设计类 (子进程调用) ──────────────────────────────────────
|
|
290
|
+
return self._call_platform("table_impact", args)
|
|
291
|
+
|
|
292
|
+
# ── 原型/设计/语义 (全部走云平台) ────────────────────────────────
|
|
432
293
|
|
|
433
294
|
@tool("fill_prototype",
|
|
434
|
-
"原型预填:
|
|
295
|
+
"原型预填: 用真实代码数据自动填模板。",
|
|
435
296
|
{"type": "object", "properties": {
|
|
436
297
|
"keyword": {"type": "string", "description": "业务关键词"},
|
|
437
|
-
"platform": {"type": "string", "enum": ["web", "app"], "description": "
|
|
298
|
+
"platform": {"type": "string", "enum": ["web", "app"], "description": "平台"},
|
|
438
299
|
}, "required": ["keyword"]})
|
|
439
300
|
def _t_fill_prototype(self, args):
|
|
440
|
-
|
|
441
|
-
kw = args.get("keyword", "")
|
|
442
|
-
plat = args.get("platform", "web")
|
|
443
|
-
script = find_script('fill_prototype.py')
|
|
444
|
-
if script is None:
|
|
445
|
-
return "fill_prototype.py 未找到 (脚本迁移后路径未更新)"
|
|
446
|
-
try:
|
|
447
|
-
r = subprocess.run([sys.executable, str(script), kw, '--platform', plat],
|
|
448
|
-
capture_output=True, text=True, encoding='utf-8',
|
|
449
|
-
errors='replace', timeout=30, cwd=str(script.parent))
|
|
450
|
-
return r.stdout or "fill_prototype 无输出 (可能索引缺失)"
|
|
451
|
-
except Exception as e:
|
|
452
|
-
return "[error] fill_prototype: %s" % str(e)
|
|
301
|
+
return self._call_platform("fill_prototype", args)
|
|
453
302
|
|
|
454
303
|
@tool("get_design_system",
|
|
455
|
-
"设计系统查询:
|
|
304
|
+
"设计系统查询: 返回项目完整设计规范。",
|
|
456
305
|
{"type": "object", "properties": {
|
|
457
|
-
"platform": {"type": "string", "enum": ["web", "app"], "description": "
|
|
306
|
+
"platform": {"type": "string", "enum": ["web", "app"], "description": "平台过滤"},
|
|
458
307
|
}})
|
|
459
308
|
def _t_get_design_system(self, args):
|
|
460
|
-
|
|
461
|
-
design_md = os.path.join(BASE_DIR, 'data', 'design', 'DESIGN.md')
|
|
462
|
-
if not os.path.isfile(design_md):
|
|
463
|
-
gen_script = find_script('gen_design_doc.py')
|
|
464
|
-
if gen_script is not None:
|
|
465
|
-
subprocess.run([sys.executable, str(gen_script)], capture_output=True, timeout=30)
|
|
466
|
-
if os.path.isfile(design_md):
|
|
467
|
-
content = open(design_md, encoding='utf-8').read()
|
|
468
|
-
plat = args.get("platform")
|
|
469
|
-
if plat:
|
|
470
|
-
lines = content.split('\n')
|
|
471
|
-
result = []
|
|
472
|
-
in_section = False
|
|
473
|
-
for line in lines:
|
|
474
|
-
if line.startswith('### ') and plat.lower() in line.lower():
|
|
475
|
-
in_section = True
|
|
476
|
-
elif line.startswith('### ') and in_section:
|
|
477
|
-
break
|
|
478
|
-
if in_section:
|
|
479
|
-
result.append(line)
|
|
480
|
-
return '\n'.join(result) if result else content
|
|
481
|
-
return content
|
|
482
|
-
return "DESIGN.md 不存在且生成失败。请手动跑: python .qoder/scripts/design/gen_design_doc.py"
|
|
309
|
+
return self._call_platform("get_design_system", args)
|
|
483
310
|
|
|
484
311
|
@tool("semantic_search",
|
|
485
|
-
"
|
|
312
|
+
"语义搜索: 用自然语言找代码/API/功能。",
|
|
486
313
|
{"type": "object", "properties": {
|
|
487
|
-
"query": {"type": "string", "description": "自然语言查询
|
|
488
|
-
"top_k": {"type": "integer", "default": 10, "description": "返回前N
|
|
314
|
+
"query": {"type": "string", "description": "自然语言查询"},
|
|
315
|
+
"top_k": {"type": "integer", "default": 10, "description": "返回前N条"},
|
|
489
316
|
}, "required": ["query"]})
|
|
490
317
|
def _t_semantic_search(self, args):
|
|
318
|
+
return self._call_platform("semantic_search", args)
|
|
491
319
|
from domain.kg.graph.kg_semantic import semantic_search, embedding_status
|
|
492
320
|
query = (args.get("query") or '').strip()
|
|
493
321
|
if not query:
|