@hupan56/wlkj 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/bin/cli.js +117 -0
  2. package/package.json +1 -1
  3. package/templates/qoder/commands/optional/wl-insight.md +276 -276
  4. package/templates/qoder/commands/optional/wl-report.md +1 -1
  5. package/templates/qoder/commands/optional/wl-spec.md +13 -1
  6. package/templates/qoder/commands/optional/wl-status.md +12 -1
  7. package/templates/qoder/commands/wl-code.md +76 -4
  8. package/templates/qoder/commands/wl-commit.md +12 -1
  9. package/templates/qoder/commands/wl-design.md +69 -5
  10. package/templates/qoder/commands/wl-init.md +27 -0
  11. package/templates/qoder/commands/wl-prd.md +145 -4
  12. package/templates/qoder/commands/wl-search.md +74 -20
  13. package/templates/qoder/commands/wl-task.md +613 -613
  14. package/templates/qoder/commands/wl-test.md +16 -1
  15. package/templates/qoder/contracts/spec.md +4 -0
  16. package/templates/qoder/hooks/post-tool-use.py +41 -0
  17. package/templates/qoder/hooks/session-start.py +34 -66
  18. package/templates/qoder/scripts/capability/__pycache__/__init__.cpython-39.pyc +0 -0
  19. package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
  20. package/templates/qoder/scripts/capability/__pycache__/registry_mcp.cpython-39.pyc +0 -0
  21. package/templates/qoder/scripts/capability/adapters/__init__.py +1 -1
  22. package/templates/qoder/scripts/capability/adapters/__pycache__/__init__.cpython-39.pyc +0 -0
  23. package/templates/qoder/scripts/capability/adapters/__pycache__/cli.cpython-39.pyc +0 -0
  24. package/templates/qoder/scripts/capability/adapters/__pycache__/mcp.cpython-39.pyc +0 -0
  25. package/templates/qoder/scripts/capability/adapters/__pycache__/qw.cpython-39.pyc +0 -0
  26. package/templates/qoder/scripts/capability/adapters/mcp.py +76 -100
  27. package/templates/qoder/scripts/capability/adapters/qw.py +295 -295
  28. package/templates/qoder/scripts/capability/caps/__init__.py +1 -1
  29. package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
  30. package/templates/qoder/scripts/capability/caps/__pycache__/context.cpython-39.pyc +0 -0
  31. package/templates/qoder/scripts/capability/caps/__pycache__/cron.cpython-39.pyc +0 -0
  32. package/templates/qoder/scripts/capability/caps/__pycache__/identity.cpython-39.pyc +0 -0
  33. package/templates/qoder/scripts/capability/caps/__pycache__/memory.cpython-39.pyc +0 -0
  34. package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
  35. package/templates/qoder/scripts/capability/caps/__pycache__/present.cpython-39.pyc +0 -0
  36. package/templates/qoder/scripts/capability/caps/__pycache__/repo.cpython-39.pyc +0 -0
  37. package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
  38. package/templates/qoder/scripts/capability/caps/memory.py +1 -1
  39. package/templates/qoder/scripts/capability/registry.py +21 -23
  40. package/templates/qoder/scripts/capability/registry_mcp.py +69 -5
  41. package/templates/qoder/scripts/capability/smoke_test_report.json +34 -20
  42. package/templates/qoder/scripts/deployment/setup/carriers.py +3 -1
  43. package/templates/qoder/scripts/deployment/setup/init_doctor.py +10 -3
  44. package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
  45. package/templates/qoder/scripts/domain/integration/__init__.py +0 -0
  46. package/templates/qoder/scripts/domain/integration/__pycache__/__init__.cpython-39.pyc +0 -0
  47. package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
  48. package/templates/qoder/scripts/domain/integration/return_to_platform.py +392 -0
  49. package/templates/qoder/scripts/domain/integration/spec_upload.py +209 -0
  50. package/templates/qoder/scripts/domain/kg/build/kg_build.py +27 -3
  51. package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
  52. package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
  53. package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
  54. package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
  55. package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
  56. package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
  57. package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
  58. package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
  59. package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
  60. package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
  61. package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
  62. package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
  63. package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
  64. package/templates/qoder/scripts/domain/kg/extract/extract.py +84 -0
  65. package/templates/qoder/scripts/domain/kg/extract/extract.py.bak +430 -0
  66. package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
  67. package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
  68. package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
  69. package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
  70. package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
  71. package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
  72. package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
  73. package/templates/qoder/scripts/domain/kg/extract/java/__init__.py +15 -0
  74. package/templates/qoder/scripts/domain/kg/extract/java/_parser.py +271 -0
  75. package/templates/qoder/scripts/domain/kg/extract/java/all.py +145 -0
  76. package/templates/qoder/scripts/domain/kg/extract/java/build_java_to_pg.py +102 -0
  77. package/templates/qoder/scripts/domain/kg/extract/java/call_chain.py +49 -0
  78. package/templates/qoder/scripts/domain/kg/extract/java/class_extractor.py +141 -0
  79. package/templates/qoder/scripts/domain/kg/extract/java/domain_extractor.py +148 -0
  80. package/templates/qoder/scripts/domain/kg/extract/java/dubbo_extractor.py +33 -0
  81. package/templates/qoder/scripts/domain/kg/extract/java/endpoint_extractor.py +36 -0
  82. package/templates/qoder/scripts/domain/kg/extract/java/javadoc_extractor.py +110 -0
  83. package/templates/qoder/scripts/domain/kg/extract/java/llm_cn_filler.py +150 -0
  84. package/templates/qoder/scripts/domain/kg/extract/java/member_extractor.py +157 -0
  85. package/templates/qoder/scripts/domain/kg/extract/java/mybatisplus_extractor.py +34 -0
  86. package/templates/qoder/scripts/domain/kg/extract/java/pg_upsert.py +165 -0
  87. package/templates/qoder/scripts/domain/kg/extract/java/satoken_extractor.py +30 -0
  88. package/templates/qoder/scripts/domain/kg/extract/java/spring_extractor.py +39 -0
  89. package/templates/qoder/scripts/domain/kg/extract/java/validation_extractor.py +33 -0
  90. package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
  91. package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
  92. package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
  93. package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
  94. package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
  95. package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
  96. package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
  97. package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
  98. package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
  99. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +4 -2
  100. package/templates/qoder/scripts/domain/kg/kg.py +42 -5
  101. package/templates/qoder/scripts/domain/kg/search/_remote.py +187 -0
  102. package/templates/qoder/scripts/domain/kg/search/context_pack.py +32 -2
  103. package/templates/qoder/scripts/domain/kg/search/search_index.py +74 -20
  104. package/templates/qoder/scripts/domain/kg/switch_project.py +159 -0
  105. package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
  106. package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
  107. package/templates/qoder/scripts/domain/task/{zentao_panel.py → wlkj_panel.py} +315 -53
  108. package/templates/qoder/scripts/engine/poller.py +219 -0
  109. package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
  110. package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
  111. package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
  112. package/templates/qoder/scripts/foundation/integrations/active_task.py +2 -1
  113. package/templates/qoder/scripts/orchestration/wlkj.py +4 -0
  114. package/templates/qoder/scripts/protocol/__pycache__/__init__.cpython-39.pyc +0 -0
  115. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1 -1
  116. package/templates/qoder/scripts/protocol/transports/__pycache__/__init__.cpython-39.pyc +0 -0
  117. package/templates/qoder/scripts/protocol/transports/__pycache__/base.cpython-39.pyc +0 -0
  118. package/templates/qoder/scripts/protocol/transports/__pycache__/cli.cpython-39.pyc +0 -0
  119. package/templates/qoder/scripts/protocol/transports/__pycache__/http.cpython-39.pyc +0 -0
  120. package/templates/qoder/scripts/protocol/transports/__pycache__/stdio.cpython-39.pyc +0 -0
  121. package/templates/qoder/scripts/protocol/transports/http.py +7 -1
  122. package/templates/root/AGENTS.md +9 -10
@@ -0,0 +1,187 @@
1
+ """本地引擎 → 工作台 MCP 远程查询层。
2
+
3
+ 替代直连本地 kg.duckdb(已删),所有知识查询走 MCP HTTP → 工作台 PostgreSQL。
4
+ 下游命令(kg.py / wl-search / context_pack)零改动:函数签名不变,print 格式不变。
5
+
6
+ 用法(search_index.py 内):
7
+ from domain.kg.search._remote import call_remote, render_search_code
8
+ data = call_remote("search_code", {"keyword": query})
9
+ sys.stdout.write(render_search_code(data))
10
+ """
11
+ import json
12
+ import sys
13
+ import os
14
+
15
+ # McpCap 路径(相对于 .qoder/scripts/)
16
+ _scripts = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
17
+ if _scripts not in sys.path:
18
+ sys.path.insert(0, _scripts)
19
+
20
+ _mcp = None
21
+
22
+
23
+ def _get_mcp():
24
+ """惰性初始化 McpCap(首次调用连工作台 MCP server)。"""
25
+ global _mcp
26
+ if _mcp is None:
27
+ from capability.adapters.mcp import McpCap
28
+ _mcp = McpCap()
29
+ return _mcp
30
+
31
+
32
+ def call_remote(tool, args):
33
+ """调工作台 MCP 工具,返回 dict。失败抛 RuntimeError(无本地降级,kg.duckdb 已删)。"""
34
+ cap = _get_mcp()
35
+ result = cap.call(tool, args)
36
+ if result.is_error:
37
+ raise RuntimeError("MCP工具 %s 调用失败: %s" % (tool, getattr(result, 'text', '')[:200]))
38
+ try:
39
+ data = json.loads(result.text)
40
+ except Exception:
41
+ raise RuntimeError("MCP返回非JSON: %s" % result.text[:200])
42
+ if isinstance(data, dict) and data.get("degraded") and data.get("error"):
43
+ raise RuntimeError("工作台降级: %s" % data["error"])
44
+ return data
45
+
46
+
47
+ # ============================================================
48
+ # 渲染器:把 MCP JSON → 原本地 print 文本格式(下游命令零改动)
49
+ # ============================================================
50
+
51
+ def render_search_code(data):
52
+ """渲染 search_code 结果(仿 search_index.py _search_keywords_impl 格式)。"""
53
+ items = data.get("items", [])
54
+ if not items:
55
+ return "No matches.\n"
56
+ lines = ["[%d matches]" % len(items)]
57
+ for it in items[:20]:
58
+ t = it.get("type", "")
59
+ name = it.get("name", "")
60
+ label = it.get("label", "")
61
+ f = it.get("file", "")
62
+ plat = it.get("platform", "")
63
+ short = f.replace("\\", "/").split("/")[-1] if f else ""
64
+ suffix = " -> data/code/%s" % f.replace("\\", "/") if f else ""
65
+ line = " [%s] %s" % (t, name)
66
+ if label and label != name:
67
+ line += "(%s)" % label
68
+ if f:
69
+ line += " %s%s" % (short, suffix)
70
+ lines.append(line)
71
+ return "\n".join(lines) + "\n"
72
+
73
+
74
+ def render_search_api(data):
75
+ """渲染 search_api 结果。"""
76
+ items = data.get("items", [])
77
+ if not items:
78
+ return "No API matches.\n"
79
+ lines = ["[%d API matches]" % len(items)]
80
+ for it in items[:20]:
81
+ verb = (it.get("verb") or "").upper()
82
+ url = it.get("url", "")
83
+ fn = it.get("fn", "")
84
+ lines.append(" %s %s -> %s" % (verb or "GET", url, fn))
85
+ return "\n".join(lines) + "\n"
86
+
87
+
88
+ def render_search_field(data):
89
+ """渲染 search_field 结果。"""
90
+ items = data.get("items", [])
91
+ if not items:
92
+ return "No field matches.\n"
93
+ lines = ["[%d field matches]" % len(items)]
94
+ for it in items[:20]:
95
+ field = it.get("field", "")
96
+ title = it.get("title", "")
97
+ count = it.get("count", 0)
98
+ lines.append(" %s = %s (用%d次)" % (field, title, count))
99
+ return "\n".join(lines) + "\n"
100
+
101
+
102
+ def render_search_prds(data):
103
+ """渲染 PRD 搜索结果。"""
104
+ items = data.get("items", [])
105
+ if not items:
106
+ return "No PRD matches.\n"
107
+ lines = ["[%d PRD matches]" % len(items)]
108
+ for it in items[:10]:
109
+ title = it.get("title", "")
110
+ status = it.get("status", "")
111
+ lines.append(" [%s] %s" % (status, title))
112
+ return "\n".join(lines) + "\n"
113
+
114
+
115
+ def render_search_style(data):
116
+ """渲染风格搜索结果。"""
117
+ items = data.get("items", [])
118
+ if not items:
119
+ return "No style matches.\n"
120
+ lines = ["[%d style matches]" % len(items)]
121
+ for it in items[:10]:
122
+ lines.append(" %s: %s" % (it.get("title", ""), it.get("description", "")[:60]))
123
+ return "\n".join(lines) + "\n"
124
+
125
+
126
+ def render_impact(data):
127
+ """渲染影响分析结果。"""
128
+ entity = data.get("entity")
129
+ if not entity:
130
+ return "Entity not found.\n"
131
+ totals = data.get("totals", {})
132
+ by_type = data.get("byType", {})
133
+ lines = [
134
+ "影响分析: %s (%s)" % (entity.get("name", ""), entity.get("type", "")),
135
+ " 影响范围: %s 节点 / %s 关系" % (totals.get("nodes", 0), totals.get("links", 0)),
136
+ ]
137
+ if by_type:
138
+ lines.append(" 按类型: " + ", ".join("%s·%d" % (k, v) for k, v in by_type.items()))
139
+ for section in [("upstream", "⬆ 谁依赖它"), ("downstream", "⬇ 它依赖谁")]:
140
+ key, label = section
141
+ layers = data.get(key, [])
142
+ if layers:
143
+ lines.append(label)
144
+ for layer in layers:
145
+ depth = layer.get("depth", "?")
146
+ items = layer.get("items", [])
147
+ names = [it.get("name", "")[:30] for it in items[:5]]
148
+ lines.append(" L%s: %s" % (depth, ", ".join(names)))
149
+ if len(items) > 5:
150
+ lines.append(" +%d more" % (len(items) - 5))
151
+ return "\n".join(lines) + "\n"
152
+
153
+
154
+ def render_context360(data):
155
+ """渲染 context_360 结果。"""
156
+ parts = []
157
+ for key, label in [("code", "CODE"), ("api", "API"), ("field", "FIELD")]:
158
+ section = data.get(key, {})
159
+ # context_360 返回 {code: {items:[...], total:N}, ...} 不是直接 list
160
+ items = section.get("items", []) if isinstance(section, dict) else (section if isinstance(section, list) else [])
161
+ if items:
162
+ parts.append("[%s] %d matches" % (label, len(items)))
163
+ for it in items[:5]:
164
+ if isinstance(it, dict):
165
+ name = it.get("name", it.get("url", it.get("field", "")))
166
+ t = it.get("type", "")
167
+ extra = it.get("title", it.get("label", ""))
168
+ line = " %s %s" % (t, name) if t else " %s" % name
169
+ if extra and extra != name:
170
+ line += "(%s)" % extra
171
+ parts.append(line)
172
+ if not parts:
173
+ return "No context found.\n"
174
+ return "\n".join(parts) + "\n"
175
+
176
+
177
+ def render_rag_search(data):
178
+ """渲染 RAG 语义检索结果。"""
179
+ items = data.get("items", [])
180
+ if not items:
181
+ return "No semantic matches.\n"
182
+ lines = ["[%d semantic matches] vec=%s" % (len(items), data.get("vector_ok"))]
183
+ for it in items[:10]:
184
+ name = it.get("name", "")
185
+ score = it.get("vec_score", 0)
186
+ lines.append(" [%.2f] %s (%s)" % (score, name, it.get("type", "")))
187
+ return "\n".join(lines) + "\n"
@@ -198,8 +198,38 @@ def main():
198
198
 
199
199
 
200
200
  def _compute_and_print(query, platform, page_type):
201
- """实际的 context_pack 计算 (原 main 逻辑)。"""
202
-
201
+ """实际的 context_pack 计算 走工作台 MCP context_pack(本地 JSON 索引已废弃)。"""
202
+ try:
203
+ from domain.kg.search._remote import call_remote
204
+ data = call_remote("context_pack", {"query": query, "platform": platform})
205
+ # MCP context_pack 返回聚合结果,直接渲染
206
+ print('# Context Pack: {} (platform: {})'.format(query, platform or 'all'))
207
+ for section_key, section_label in [("code", "相关代码"), ("api", "API"),
208
+ ("field", "字段"), ("wiki", "Wiki"),
209
+ ("prd", "PRD")]:
210
+ items = data.get(section_key, [])
211
+ if not items:
212
+ continue
213
+ print('\n## %s (%d)' % (section_label, len(items)))
214
+ for it in items[:8]:
215
+ if isinstance(it, dict):
216
+ name = it.get("name", it.get("url", it.get("field", "")))
217
+ label = it.get("label", it.get("title", ""))
218
+ line = "- %s" % name
219
+ if label and label != name:
220
+ line += "(%s)" % label
221
+ print(line)
222
+ print("\n(数据来自工作台 PostgreSQL,MCP context_pack 工具)")
223
+ return
224
+ except Exception as e:
225
+ print("# Context Pack: MCP 调用失败(工作台未启动?)")
226
+ print("# 错误: %s" % str(e)[:200])
227
+ print("# 提示: 确保工作台后端在跑(start_pg.bat),mcp_config.json 配置正确")
228
+ return
229
+
230
+
231
+ def _compute_and_print_local(query, platform, page_type):
232
+ """[已废弃] 原本地 JSON 索引逻辑(kg.duckdb 已删,load_index 会失败)。"""
203
233
  targets = []
204
234
  if platform and platform.lower() not in ('both', '两端'):
205
235
  targets = [PLATFORM_MAP.get(platform.lower(), platform)]
@@ -413,6 +413,22 @@ def _check_index_freshness():
413
413
 
414
414
 
415
415
  def search_keywords(query, platform=None):
416
+ """搜代码关键词 → 走工作台 MCP search_code(本地 kg.duckdb 已删,不再直连)。"""
417
+ try:
418
+ from domain.kg.search._remote import call_remote, render_search_code
419
+ data = call_remote("search_code", {"keyword": query, "platform": platform})
420
+ sys.stdout.write(render_search_code(data))
421
+ return
422
+ except Exception as e:
423
+ print("MCP search_code 失败(工作台未启动?): %s" % str(e)[:200])
424
+ print("提示: 确保工作台后端在跑(start_pg.bat),mcp_config.json 配置正确")
425
+ return
426
+
427
+
428
+ def search_keywords_local(query, platform=None):
429
+ """[已废弃] 原本地直连 kg.duckdb 的搜索逻辑。保留仅供回退参考。
430
+ 本地 kg.duckdb 已删除,此函数会报 code-keyword.json not found。
431
+ """
416
432
  # 先查缓存 (命中即数据未变, 直接还)。缓存 key 已含 kg.duckdb mtime, 命中就说明
417
433
  # 数据源没变, 跳过 _check_index_freshness 的 os.walk —— 50 人团队每天几十次
418
434
  # 搜索, 每次都 walk 目录是纯浪费。未命中才做新鲜度检查 + 计算。
@@ -581,6 +597,17 @@ def _semantic_fallback(query, platform=None):
581
597
 
582
598
 
583
599
  def search_prds(query):
600
+ """搜 PRD → 走工作台 MCP search_prds。"""
601
+ try:
602
+ from domain.kg.search._remote import call_remote, render_search_prds
603
+ data = call_remote("search_prds", {"query": query})
604
+ sys.stdout.write(render_search_prds(data))
605
+ except Exception as e:
606
+ print("MCP search_prds 失败: %s" % str(e)[:200])
607
+
608
+
609
+ def search_prds_local(query):
610
+ """[已废弃] 原本地逻辑。"""
584
611
  prd_index = load_index('prd-index.json', hint='Run: python .qoder/scripts/domain/task/git_sync.py --prd-only')
585
612
  if prd_index is None:
586
613
  return
@@ -627,7 +654,17 @@ def search_prds(query):
627
654
 
628
655
 
629
656
  def search_style(style_type, platform=None):
630
- """Find pages of a given UI type (table/form/detail/modal/dashboard)."""
657
+ """搜风格 走工作台 MCP search_style。"""
658
+ try:
659
+ from domain.kg.search._remote import call_remote, render_search_style
660
+ data = call_remote("search_style", {"style_type": style_type, "platform": platform})
661
+ sys.stdout.write(render_search_style(data))
662
+ except Exception as e:
663
+ print("MCP search_style 失败: %s" % str(e)[:200])
664
+
665
+
666
+ def search_style_local(style_type, platform=None):
667
+ """[已废弃] 原本地逻辑。"""
631
668
  si = load_index('ui-style.json', hint='Run: python build_style_index.py')
632
669
  if si is None:
633
670
  return
@@ -663,7 +700,17 @@ def search_style(style_type, platform=None):
663
700
 
664
701
 
665
702
  def search_field(field_name):
666
- """Search field usage in ui-style.json field_map."""
703
+ """搜字段 走工作台 MCP search_field。"""
704
+ try:
705
+ from domain.kg.search._remote import call_remote, render_search_field
706
+ data = call_remote("search_field", {"name": field_name})
707
+ sys.stdout.write(render_search_field(data))
708
+ except Exception as e:
709
+ print("MCP search_field 失败: %s" % str(e)[:200])
710
+
711
+
712
+ def search_field_local(field_name):
713
+ """[已废弃] 原本地逻辑。"""
667
714
  si = load_index('ui-style.json', hint='Run: python build_style_index.py')
668
715
  if si is None:
669
716
  return
@@ -681,7 +728,17 @@ def search_field(field_name):
681
728
 
682
729
 
683
730
  def search_api(query):
684
- """Search API endpoints in code-api.json."""
731
+ """ API 走工作台 MCP search_api。"""
732
+ try:
733
+ from domain.kg.search._remote import call_remote, render_search_api
734
+ data = call_remote("search_api", {"keyword": query})
735
+ sys.stdout.write(render_search_api(data))
736
+ except Exception as e:
737
+ print("MCP search_api 失败: %s" % str(e)[:200])
738
+
739
+
740
+ def search_api_local(query):
741
+ """[已废弃] 原本地逻辑。"""
685
742
  ai = load_index('code-api.json', hint='Run: python .qoder/scripts/domain/task/git_sync.py --index-only')
686
743
  if ai is None:
687
744
  return
@@ -813,26 +870,23 @@ def search_wiki_cli(query):
813
870
 
814
871
 
815
872
  def search_impact(query):
816
- """改这个接口/符号影响哪些页面/按钮/模块? (U1: 内存图遍历)"""
817
- from domain.kg.graph.graph_traverse import CodeGraph
818
- g = CodeGraph(INDEX_DIR)
819
- result = g.impact(query)
820
- if result:
821
- print(result)
822
- else:
823
- print('未找到匹配 "{}" 的端点。尝试用部分路径, 如 /veh 或 export'.format(query))
824
- print('提示: trace-chain 当前覆盖率 31%, 未覆盖的调用不会被列出')
873
+ """影响分析 走工作台 MCP get_impact。"""
874
+ try:
875
+ from domain.kg.search._remote import call_remote, render_impact
876
+ data = call_remote("get_impact", {"entity": query, "depth": 2})
877
+ sys.stdout.write(render_impact(data))
878
+ except Exception as e:
879
+ print("MCP get_impact 失败: %s" % str(e)[:200])
825
880
 
826
881
 
827
882
  def search_context360(query):
828
- """符号的360度视图: 端点/函数/处理器/PRD (U1: 内存图遍历)"""
829
- from domain.kg.graph.graph_traverse import CodeGraph
830
- g = CodeGraph(INDEX_DIR)
831
- result = g.context360(query)
832
- if result:
833
- print(result)
834
- else:
835
- print('未找到匹配 "{}" 的符号。'.format(query))
883
+ """360度视图 走工作台 MCP context_360。"""
884
+ try:
885
+ from domain.kg.search._remote import call_remote, render_context360
886
+ data = call_remote("context_360", {"symbol": query})
887
+ sys.stdout.write(render_context360(data))
888
+ except Exception as e:
889
+ print("MCP context_360 失败: %s" % str(e)[:200])
836
890
 
837
891
 
838
892
  def print_usage():
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env python
2
+ """wlkj 项目切换脚本:选项目 → 拉平台 token → 写 mcp_config.json。
3
+
4
+ 用法:
5
+ python switch_project.py # 交互式:列出项目 → 选 → 写配置
6
+ python switch_project.py --email a@b.c --pw xxx # 非交互:直接指定
7
+ python switch_project.py --project-uuid <UUID> # 非交互:直接指定项目
8
+
9
+ 需工作台后端在跑(默认 http://127.0.0.1:10010)。
10
+ """
11
+ import json
12
+ import os
13
+ import sys
14
+ import urllib.request
15
+ import urllib.error
16
+
17
+ # 宿主根:脚本部署在 {host}/scripts/domain/kg/,4 级 dirname = {host}(.qoder 或 wlinkj-workflow)
18
+ HOST_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
19
+ REPO_ROOT = os.path.dirname(HOST_DIR) # qoderAll/
20
+
21
+
22
+ def _find_config_path():
23
+ """按宿主探测 mcp_config.json:当前宿主根 → wlinkj-workflow → .qoder(适配多宿主部署)。"""
24
+ for p in (
25
+ os.path.join(HOST_DIR, "mcp_config.json"), # 当前宿主根(.qoder 或 wlinkj-workflow)
26
+ os.path.join(REPO_ROOT, "wlinkj-workflow", "mcp_config.json"),
27
+ os.path.join(REPO_ROOT, ".qoder", "mcp_config.json"),
28
+ ):
29
+ if os.path.isfile(p):
30
+ return p
31
+ return os.path.join(HOST_DIR, "mcp_config.json") # 都没有则写当前宿主根
32
+
33
+
34
+ CONFIG_PATH = _find_config_path()
35
+
36
+ # 平台地址(默认 10010,可 env 覆盖)
37
+ BASE_URL = os.environ.get("WLKJ_BASE_URL", "http://127.0.0.1:10010")
38
+
39
+
40
+ def bind(email, password, base_url=BASE_URL):
41
+ """调平台 /api/auth/bind,返回 {projects:[{id,name,token,mcp_config}], base_url}。"""
42
+ data = json.dumps({"email": email, "password": password, "base_url": base_url}).encode()
43
+ req = urllib.request.Request(
44
+ f"{base_url}/api/auth/bind",
45
+ data=data, method="POST",
46
+ headers={"Content-Type": "application/json"},
47
+ )
48
+ try:
49
+ resp = urllib.request.urlopen(req, timeout=10)
50
+ return json.loads(resp.read())
51
+ except urllib.error.HTTPError as e:
52
+ body = e.read().decode("utf-8", errors="replace")[:200]
53
+ print(f"ERROR: 平台返回 {e.code}: {body}", file=sys.stderr)
54
+ sys.exit(1)
55
+ except urllib.error.URLError as e:
56
+ print(f"ERROR: 连不上平台 {base_url}: {e}", file=sys.stderr)
57
+ print(f"提示: 确保工作台在跑(start_pg.bat),或设 WLKJ_BASE_URL 环境变量", file=sys.stderr)
58
+ sys.exit(1)
59
+
60
+
61
+ def write_config(project, base_url):
62
+ """把选中项目的 mcp_config 写入 mcp_config.json(保留 return_endpoint)。"""
63
+ # 读现有配置(保留注释和非 mcpServers 字段)
64
+ existing = {}
65
+ if os.path.isfile(CONFIG_PATH):
66
+ try:
67
+ with open(CONFIG_PATH, encoding="utf-8") as f:
68
+ existing = json.load(f)
69
+ except Exception:
70
+ pass
71
+
72
+ # 用平台的 mcp_config 覆盖
73
+ mcp_cfg = project.get("mcp_config", {})
74
+ if not mcp_cfg:
75
+ # 平台没返回完整 config,手动拼
76
+ mcp_cfg = {
77
+ "mcpServers": {"wlinkj-knowledge": {
78
+ "kind": "http",
79
+ "url": f"{base_url}/mcp",
80
+ "token": project.get("token", ""),
81
+ "env": {"WLKJ_PROJECT_ID": project.get("id", "")},
82
+ }},
83
+ "return_endpoint": {
84
+ "url": f"{base_url}/api/projects/{project.get('id','')}/returns",
85
+ "method": "POST",
86
+ "headers": {"X-Engine-Token": project.get("token", "")},
87
+ },
88
+ }
89
+
90
+ existing.update(mcp_cfg)
91
+ # 确保 return_endpoint 的端口正确
92
+ if "return_endpoint" in existing:
93
+ existing["return_endpoint"]["url"] = f"{base_url}/api/projects/{project['id']}/returns"
94
+
95
+ os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
96
+ with open(CONFIG_PATH, "w", encoding="utf-8") as f:
97
+ json.dump(existing, f, ensure_ascii=False, indent=2)
98
+
99
+ print(f"OK: 已绑定项目「{project.get('name', project.get('id',''))}」")
100
+ print(f" token: {project.get('token','')[:12]}...")
101
+ print(f" project_id: {project.get('id','')}")
102
+ print(f" 配置写入: {CONFIG_PATH}")
103
+ print(f" 平台地址: {base_url}")
104
+
105
+
106
+ def main():
107
+ import argparse
108
+ parser = argparse.ArgumentParser(description="切换 wlkj 项目绑定")
109
+ parser.add_argument("--email", help="平台邮箱")
110
+ parser.add_argument("--pw", "--password", dest="password", help="平台密码")
111
+ parser.add_argument("--project-uuid", help="直接指定项目 UUID(跳过选择)")
112
+ parser.add_argument("--base-url", default=BASE_URL, help=f"平台地址(默认 {BASE_URL})")
113
+ parser.add_argument("--list", action="store_true", help="只列出项目,不写配置")
114
+ args = parser.parse_args()
115
+
116
+ # 1. 收集邮箱密码
117
+ email = args.email or os.environ.get("WLKJ_EMAIL") or input("平台邮箱: ").strip()
118
+ password = args.password or os.environ.get("WLKJ_PASSWORD") or input("平台密码: ").strip()
119
+
120
+ # 2. 拉项目列表
121
+ print(f"连接平台 {args.base_url} ...")
122
+ result = bind(email, password, args.base_url)
123
+ projects = result.get("projects", [])
124
+ if not projects:
125
+ print("该用户没有任何项目,请先在平台创建项目。")
126
+ sys.exit(1)
127
+
128
+ # 3. 列出项目
129
+ print(f"\n找到 {len(projects)} 个项目:")
130
+ for i, p in enumerate(projects):
131
+ print(f" {i+1}. {p.get('name','?')} (id={p.get('id','?')[:8]})")
132
+
133
+ if args.list:
134
+ return
135
+
136
+ # 4. 选择项目
137
+ chosen = None
138
+ if args.project_uuid:
139
+ for p in projects:
140
+ if p.get("id") == args.project_uuid:
141
+ chosen = p
142
+ break
143
+ if not chosen:
144
+ print(f"ERROR: 项目 UUID '{args.project_uuid}' 不在列表中", file=sys.stderr)
145
+ sys.exit(1)
146
+ else:
147
+ try:
148
+ idx = int(input(f"\n选哪个项目?(1-{len(projects)}): ")) - 1
149
+ chosen = projects[idx]
150
+ except (ValueError, IndexError):
151
+ print("ERROR: 无效选择", file=sys.stderr)
152
+ sys.exit(1)
153
+
154
+ # 5. 写配置
155
+ write_config(chosen, args.base_url)
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env python
2
+ """同步 Qoder Repo Wiki → 平台 PG wiki 表。
3
+
4
+ Qoder 自动为代码仓库生成 Repo Wiki(750+篇业务文档),存在:
5
+ data/code/{repo}/.qoder/repowiki/knowledge/zh/**/*.md
6
+
7
+ 本脚本读这些 md → UPSERT 到 PG wiki 表(repo + keyword + title + description)。
8
+ 这样知识层的 wiki 搜索(graphrag_ask/rag_search)能命中 Qoder 自动生成的高质量文档。
9
+
10
+ 用法:
11
+ python sync_repowiki.py # 同步全部仓库
12
+ python sync_repowiki.py --repo fywl-ics # 只同步指定仓库
13
+ python sync_repowiki.py --dry-run # 只统计不写
14
+ """
15
+ import os
16
+ import sys
17
+ import glob
18
+
19
+ _THIS_DIR = os.path.dirname(os.path.abspath(__file__))
20
+ _up1 = os.path.dirname(_THIS_DIR)
21
+ _up2 = os.path.dirname(_up1)
22
+ SCRIPTS_DIR = os.path.dirname(_up2)
23
+ REPO_ROOT = os.path.dirname(SCRIPTS_DIR)
24
+ CODE_ROOT = os.path.join(REPO_ROOT, "data", "code")
25
+
26
+ PG = dict(host="127.0.0.1", port=5432, user="wlinkj", password="changeme", dbname="wlinkj")
27
+
28
+
29
+ def scan_repowiki(repo):
30
+ """扫描某仓库的 Repo Wiki,返回 [{path, title, content, dir_path}]。"""
31
+ wiki_base = os.path.join(CODE_ROOT, repo, ".qoder", "repowiki")
32
+ if not os.path.isdir(wiki_base):
33
+ return []
34
+ results = []
35
+ for md_path in glob.glob(os.path.join(wiki_base, "**", "*.md"), recursive=True):
36
+ try:
37
+ with open(md_path, encoding="utf-8") as f:
38
+ content = f.read()
39
+ # 标题:md 文件名或第一行 #
40
+ title = os.path.splitext(os.path.basename(md_path))[0]
41
+ for line in content.split("\n")[:5]:
42
+ if line.strip().startswith("#"):
43
+ title = line.strip().lstrip("#").strip()
44
+ break
45
+ # keyword:从目录路径提取业务模块名
46
+ rel = os.path.relpath(md_path, wiki_base)
47
+ parts = rel.replace("\\", "/").split("/")
48
+ keyword = parts[-2] if len(parts) >= 2 else title
49
+ results.append({
50
+ "path": md_path,
51
+ "rel_path": rel,
52
+ "title": title,
53
+ "keyword": keyword,
54
+ "content": content[:2000], # 取前2000字
55
+ })
56
+ except Exception:
57
+ continue
58
+ return results
59
+
60
+
61
+ def sync_to_pg(wiki_items, repo):
62
+ """UPSERT 到 PG wiki 表。"""
63
+ import psycopg2
64
+ from psycopg2.extras import execute_values
65
+
66
+ conn = psycopg2.connect(**PG)
67
+ conn.autocommit = True
68
+ cur = conn.cursor()
69
+
70
+ # ★ 不删旧数据(原 wiki 表有业务文档),只删 repowiki 来源的记录
71
+ # 用 md_path 前缀 'repowiki:' 标识(避免路径分隔符差异)
72
+ cur.execute("DELETE FROM wiki WHERE project = %s AND md_path LIKE 'repowiki:%%'", (repo,))
73
+
74
+ rows = [(w["keyword"], repo, w["title"], "repowiki:" + w["rel_path"], w["content"][:500])
75
+ for w in wiki_items]
76
+ if rows:
77
+ execute_values(
78
+ cur,
79
+ "INSERT INTO wiki(keyword, project, title, md_path, description) VALUES %s",
80
+ rows,
81
+ )
82
+
83
+ print(f"[{repo}] 同步 {len(rows)} 篇 Wiki 到 PG")
84
+ conn.close()
85
+
86
+
87
+ def main():
88
+ import argparse
89
+ parser = argparse.ArgumentParser(description="同步 Qoder Repo Wiki → PG")
90
+ parser.add_argument("--repo", help="指定仓库名")
91
+ parser.add_argument("--dry-run", action="store_true", help="只统计不写")
92
+ args = parser.parse_args()
93
+
94
+ repos = [args.repo] if args.repo else ["fywl-ui", "fywl-ics", "Carmg-H5"]
95
+ total = 0
96
+ for repo in repos:
97
+ items = scan_repowiki(repo)
98
+ print(f" {repo}: 扫到 {len(items)} 篇 Repo Wiki")
99
+ if items:
100
+ for it in items[:3]:
101
+ print(f" [{it['keyword']}] {it['title']}")
102
+ if not args.dry_run:
103
+ sync_to_pg(items, repo)
104
+ total += len(items)
105
+ print(f"\n总计: {total} 篇 Wiki{'(dry-run 未写入)' if args.dry_run else ' 已同步到 PG'}")
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()