@hupan56/wlkj 3.3.19 → 3.4.1

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 CHANGED
@@ -1,6 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  // wlkj - workflow toolkit
3
3
 
4
+ // Windows 中文乱码修复
5
+ if (process.platform === "win32") {
6
+ try { execSync("chcp 65001", { stdio: "ignore" }); } catch (e) {}
7
+ process.stdout.setDefaultEncoding("utf-8");
8
+ process.stderr.setDefaultEncoding("utf-8");
9
+ }
10
+
4
11
  const path = require("path");
5
12
  const fs = require("fs");
6
13
  const { execSync } = require("child_process");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hupan56/wlkj",
3
- "version": "3.3.19",
3
+ "version": "3.4.1",
4
4
  "description": "AI Product R&D Workflow - PRD/Prototype/Search/Task/Report",
5
5
  "bin": {
6
6
  "wlkj": "bin/cli.js"
@@ -1,5 +1,16 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
+ # Windows 控制台 UTF-8 修复(ctypes 直调 Windows API)
4
+ import sys as _s
5
+ if _s.platform == 'win32':
6
+ import ctypes as _ct, io as _io
7
+ try:
8
+ _ct.windll.kernel32.SetConsoleOutputCP(65001)
9
+ _ct.windll.kernel32.SetConsoleCP(65001)
10
+ _s.stdout = _io.TextIOWrapper(_s.stdout.buffer, encoding='utf-8', errors='replace')
11
+ _s.stderr = _io.TextIOWrapper(_s.stderr.buffer, encoding='utf-8', errors='replace')
12
+ except Exception:
13
+ pass
3
14
  # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
4
15
  import os as _o, sys as _s
5
16
  _f = _o.path.abspath(__file__)
@@ -1,4 +1,15 @@
1
1
  # -*- coding: utf-8 -*-
2
+ # Windows 控制台 UTF-8 修复(ctypes 直调 Windows API;所有经 bootstrap 引导的脚本都受益)
3
+ import sys as _s
4
+ if _s.platform == 'win32':
5
+ import ctypes as _ct, io as _io
6
+ try:
7
+ _ct.windll.kernel32.SetConsoleOutputCP(65001)
8
+ _ct.windll.kernel32.SetConsoleCP(65001)
9
+ _s.stdout = _io.TextIOWrapper(_s.stdout.buffer, encoding='utf-8', errors='replace')
10
+ _s.stderr = _io.TextIOWrapper(_s.stderr.buffer, encoding='utf-8', errors='replace')
11
+ except Exception:
12
+ pass
2
13
  """
3
14
  bootstrap.py - 统一 CLI 启动样板 (DRY) + 子目录化路径自举 (v3.0)
4
15
 
@@ -1,5 +1,16 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
+ # Windows 控制台 UTF-8 修复(ctypes 直调 Windows API)
4
+ import sys as _s
5
+ if _s.platform == 'win32':
6
+ import ctypes as _ct, io as _io
7
+ try:
8
+ _ct.windll.kernel32.SetConsoleOutputCP(65001)
9
+ _ct.windll.kernel32.SetConsoleCP(65001)
10
+ _s.stdout = _io.TextIOWrapper(_s.stdout.buffer, encoding='utf-8', errors='replace')
11
+ _s.stderr = _io.TextIOWrapper(_s.stderr.buffer, encoding='utf-8', errors='replace')
12
+ except Exception:
13
+ pass
3
14
  """
4
15
  wlkj.py - 工作流顶层调度器 (Facade 统一入口)
5
16
 
@@ -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
- 防止 get_db() 创建空库后查询报 CatalogException (49 台没预建图谱的机器)
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 ("知识图谱未构建 (kg.duckdb 不存在)。请管理员跑:\n"
99
- " python .qoder/scripts/orchestration/wlkj.py kg-build\n"
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 = "1.1.0"
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
- # ── 搜索类 (转发到 search_index) ──────────────────────────────────
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
- _ensure_imports()
126
- kw = args.get("keyword", "")
127
- plat = args.get("platform") or ""
128
- return _capture(_search.search_keywords, kw, plat) or "无匹配结果"
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
- _ensure_imports()
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
- _ensure_imports()
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
- _ensure_imports()
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
- _ensure_imports()
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
- _ensure_imports()
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
- "业务流程: 查看一个模块的完整操作链(query→create→audit→archive→export→delete)。数据源: DuckDB workflows 表。优先匹配英文模块名, 中文输入会返回候选模块列表。例: get_workflow(module='assets') 或 get_workflow(module='cost')",
230
+ "业务流程: 查看一个模块的完整操作链。",
196
231
  {"type": "object", "properties": {
197
- "module": {"type": "string", "description": "模块名(英文优先): assets/cost/hr/maintenance/operation/qualityAndSafety/veh/settings/dashboard/flow/systems/monitor/quasafe"},
232
+ "module": {"type": "string", "description": "模块名(英文优先)"},
198
233
  }, "required": ["module"]})
199
234
  def _t_get_workflow(self, args):
200
- from domain.kg.kg_capabilities import workflow_query
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跳, 找出全部可达实体(页面→接口→控制器→测试)。知识图谱核心能力。支持别名解析(传中文/端点/函数名自动转entity_id)。例: multi_hop(symbol='资产管理') 或 multi_hop(symbol='handleExport', max_depth=3)",
238
+ "知识图谱多跳遍历: 从一个符号出发, 沿edges递归N跳。",
221
239
  {"type": "object", "properties": {
222
- "symbol": {"type": "string", "description": "起点符号: 可传entity_id、别名(资产管理)、端点(/asset/list)或函数名"},
223
- "max_depth": {"type": "integer", "default": 3, "description": "最大跳数(1-5), 默认3。越大越全但越慢"},
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
- from domain.kg.kg_capabilities import hop_query
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: 查Qoder IDE生成的模块级语义文档(架构/职责/依赖), 比读源码省token。写PRD前优先查。例: search_wiki(keyword='考勤') 或 search_wiki(keyword='权限')",
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
- from domain.kg.kg_capabilities import wiki_query
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
- "搜索字段: 查某个字段在哪些页面用过、中文标题是什么。例: search_field(field='plateNo') 或 search_field(field='车牌号')",
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
- from domain.kg.storage.kg_duckdb import get_db_readonly
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
- "模块清单: 列出项目所有业务模块及其中文名。例: list_modules() 或 list_modules(platform='web')",
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
- from domain.kg.storage.kg_duckdb import get_db_readonly
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
- "功能×测试覆盖矩阵: 哪些功能有测试覆盖, 哪些是空白? 标出回归测试盲区。例: coverage_matrix()",
271
+ "功能×测试覆盖矩阵。",
325
272
  {"type": "object", "properties": {}})
326
273
  def _t_coverage_matrix(self, args):
327
- from domain.kg.graph.graph_traverse import CodeGraph
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
- "功能画像: 一个功能的完整画像(端点+按钮+测试+页面)。例: feature_overview(feature='资产管理') 或 feature_overview(feature='asset')",
277
+ "功能画像: 一个功能的完整画像。",
334
278
  {"type": "object", "properties": {
335
- "feature": {"type": "string", "description": "功能名(中/英文都行): 资产管理/asset/hr"},
279
+ "feature": {"type": "string", "description": "功能名(中/英文)"},
336
280
  }, "required": ["feature"]})
337
281
  def _t_feature_overview(self, args):
338
- from domain.kg.graph.graph_traverse import CodeGraph
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 类和接口? 跨层遍历(数据层→代码层)。例: table_impact(table='quality_case') → 找到 QualityCase 类 + 影响的 Controller/接口",
285
+ "改表影响分析: 改某数据库表会影响哪些 Entity 类和接口。",
377
286
  {"type": "object", "properties": {
378
- "table": {"type": "string", "description": "数据库表名 (如 quality_case)"},
287
+ "table": {"type": "string", "description": "数据库表名"},
379
288
  }, "required": ["table"]})
380
289
  def _t_table_impact(self, args):
381
- from foundation.core.paths import DATA_INDEX_DIR
382
- import duckdb as _ddb
383
- db_path = str(DATA_INDEX_DIR / 'kg_db.duckdb')
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
- "原型预填: 用真实代码数据(表格列/表单字段/布局参数)自动填模板,输出80%完成的HTML草稿+数据清单。AI只需微调20%。出原型前必须调。例: fill_prototype(keyword='营业外合同', platform='web')",
295
+ "原型预填: 用真实代码数据自动填模板。",
435
296
  {"type": "object", "properties": {
436
297
  "keyword": {"type": "string", "description": "业务关键词"},
437
- "platform": {"type": "string", "enum": ["web", "app"], "description": "平台: web或app。默认web"},
298
+ "platform": {"type": "string", "enum": ["web", "app"], "description": "平台"},
438
299
  }, "required": ["keyword"]})
439
300
  def _t_fill_prototype(self, args):
440
- from foundation.core.paths import find_script
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
- "设计系统查询: 返回项目完整设计规范(布局指纹/颜色token/页面模式/组件表/图标规则/behaviors)。出任何原型前必须先调。例: get_design_system() 或 get_design_system(platform='web')",
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
- from foundation.core.paths import find_script
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
- "语义搜索(概念检索): 用自然语言找代码/API/功能,不依赖精确关键词。查'报销'能命中 expense/reimburse。需先构建embedding(管理员跑 kg_semantic.py build-embeddings)。例: semantic_search(query='报销流程') 或 semantic_search(query='车辆调度')",
312
+ "语义搜索: 用自然语言找代码/API/功能。",
486
313
  {"type": "object", "properties": {
487
- "query": {"type": "string", "description": "自然语言查询(中/英文均可)"},
488
- "top_k": {"type": "integer", "default": 10, "description": "返回前N条, 默认10"},
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: