@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.
- package/bin/cli.js +117 -0
- package/package.json +1 -1
- package/templates/qoder/commands/optional/wl-insight.md +276 -276
- package/templates/qoder/commands/optional/wl-report.md +1 -1
- package/templates/qoder/commands/optional/wl-spec.md +13 -1
- package/templates/qoder/commands/optional/wl-status.md +12 -1
- package/templates/qoder/commands/wl-code.md +76 -4
- package/templates/qoder/commands/wl-commit.md +12 -1
- package/templates/qoder/commands/wl-design.md +69 -5
- package/templates/qoder/commands/wl-init.md +27 -0
- package/templates/qoder/commands/wl-prd.md +145 -4
- package/templates/qoder/commands/wl-search.md +74 -20
- package/templates/qoder/commands/wl-task.md +613 -613
- package/templates/qoder/commands/wl-test.md +16 -1
- package/templates/qoder/contracts/spec.md +4 -0
- package/templates/qoder/hooks/post-tool-use.py +41 -0
- package/templates/qoder/hooks/session-start.py +34 -66
- package/templates/qoder/scripts/capability/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/__pycache__/registry_mcp.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__init__.py +1 -1
- package/templates/qoder/scripts/capability/adapters/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/cli.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/mcp.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/qw.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/mcp.py +76 -100
- package/templates/qoder/scripts/capability/adapters/qw.py +295 -295
- package/templates/qoder/scripts/capability/caps/__init__.py +1 -1
- package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/context.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/cron.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/identity.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/memory.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/present.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/repo.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/memory.py +1 -1
- package/templates/qoder/scripts/capability/registry.py +21 -23
- package/templates/qoder/scripts/capability/registry_mcp.py +69 -5
- package/templates/qoder/scripts/capability/smoke_test_report.json +34 -20
- package/templates/qoder/scripts/deployment/setup/carriers.py +3 -1
- package/templates/qoder/scripts/deployment/setup/init_doctor.py +10 -3
- package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/integration/__init__.py +0 -0
- package/templates/qoder/scripts/domain/integration/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/integration/return_to_platform.py +392 -0
- package/templates/qoder/scripts/domain/integration/spec_upload.py +209 -0
- package/templates/qoder/scripts/domain/kg/build/kg_build.py +27 -3
- package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
- package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
- package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
- package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
- package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
- package/templates/qoder/scripts/domain/kg/extract/extract.py +84 -0
- package/templates/qoder/scripts/domain/kg/extract/extract.py.bak +430 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
- package/templates/qoder/scripts/domain/kg/extract/java/__init__.py +15 -0
- package/templates/qoder/scripts/domain/kg/extract/java/_parser.py +271 -0
- package/templates/qoder/scripts/domain/kg/extract/java/all.py +145 -0
- package/templates/qoder/scripts/domain/kg/extract/java/build_java_to_pg.py +102 -0
- package/templates/qoder/scripts/domain/kg/extract/java/call_chain.py +49 -0
- package/templates/qoder/scripts/domain/kg/extract/java/class_extractor.py +141 -0
- package/templates/qoder/scripts/domain/kg/extract/java/domain_extractor.py +148 -0
- package/templates/qoder/scripts/domain/kg/extract/java/dubbo_extractor.py +33 -0
- package/templates/qoder/scripts/domain/kg/extract/java/endpoint_extractor.py +36 -0
- package/templates/qoder/scripts/domain/kg/extract/java/javadoc_extractor.py +110 -0
- package/templates/qoder/scripts/domain/kg/extract/java/llm_cn_filler.py +150 -0
- package/templates/qoder/scripts/domain/kg/extract/java/member_extractor.py +157 -0
- package/templates/qoder/scripts/domain/kg/extract/java/mybatisplus_extractor.py +34 -0
- package/templates/qoder/scripts/domain/kg/extract/java/pg_upsert.py +165 -0
- package/templates/qoder/scripts/domain/kg/extract/java/satoken_extractor.py +30 -0
- package/templates/qoder/scripts/domain/kg/extract/java/spring_extractor.py +39 -0
- package/templates/qoder/scripts/domain/kg/extract/java/validation_extractor.py +33 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +4 -2
- package/templates/qoder/scripts/domain/kg/kg.py +42 -5
- package/templates/qoder/scripts/domain/kg/search/_remote.py +187 -0
- package/templates/qoder/scripts/domain/kg/search/context_pack.py +32 -2
- package/templates/qoder/scripts/domain/kg/search/search_index.py +74 -20
- package/templates/qoder/scripts/domain/kg/switch_project.py +159 -0
- package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
- package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/task/{zentao_panel.py → wlkj_panel.py} +315 -53
- package/templates/qoder/scripts/engine/poller.py +219 -0
- package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/integrations/active_task.py +2 -1
- package/templates/qoder/scripts/orchestration/wlkj.py +4 -0
- package/templates/qoder/scripts/protocol/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1 -1
- package/templates/qoder/scripts/protocol/transports/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/base.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/cli.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/http.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/stdio.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/http.py +7 -1
- 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 计算
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
"""
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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()
|