@hupan56/wlkj 3.1.32 → 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/agents/insight-planning.md +67 -67
- package/templates/qoder/agents/prd-reference.md +47 -47
- package/templates/qoder/commands/optional/wl-insight.md +4 -4
- package/templates/qoder/commands/optional/wl-report.md +1 -1
- package/templates/qoder/commands/optional/wl-spec.md +23 -3
- package/templates/qoder/commands/optional/wl-status.md +12 -1
- package/templates/qoder/commands/wl-code.md +138 -7
- package/templates/qoder/commands/wl-commit.md +12 -1
- package/templates/qoder/commands/wl-design.md +70 -6
- package/templates/qoder/commands/wl-init.md +27 -0
- package/templates/qoder/commands/wl-prd.md +230 -15
- package/templates/qoder/commands/wl-req.md +10 -3
- package/templates/qoder/commands/wl-search.md +74 -20
- package/templates/qoder/commands/wl-task.md +3 -3
- package/templates/qoder/commands/wl-test.md +17 -2
- package/templates/qoder/contracts/CHANGELOG.md +418 -0
- package/templates/qoder/contracts/README.md +180 -0
- package/templates/qoder/contracts/code.md +82 -0
- package/templates/qoder/contracts/commit.md +86 -0
- package/templates/qoder/contracts/contract-header.md +76 -0
- package/templates/qoder/contracts/design.md +106 -0
- package/templates/qoder/contracts/fallback.md +126 -0
- package/templates/qoder/contracts/isolation.md +119 -0
- package/templates/qoder/contracts/prd.md +118 -0
- package/templates/qoder/contracts/schemas/design-spec.schema.json +46 -0
- package/templates/qoder/contracts/schemas/prd.schema.json +36 -0
- package/templates/qoder/contracts/schemas/test-cases.schema.json +40 -0
- package/templates/qoder/contracts/spec.md +116 -0
- package/templates/qoder/contracts/task.md +125 -0
- package/templates/qoder/contracts/test.md +112 -0
- package/templates/qoder/hooks/post-tool-use.py +61 -0
- package/templates/qoder/hooks/session-start.py +34 -66
- package/templates/qoder/hooks/stop-eval.py +47 -0
- package/templates/qoder/rules/wl-pipeline.md +37 -0
- 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/deployment/setup/install_qoderwork.py +11 -0
- package/templates/qoder/scripts/deployment/setup/wlkj_shim.py +104 -0
- package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/deployment/deploy_to_test.py +298 -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 +268 -25
- package/templates/qoder/scripts/domain/kg/build/kg_incremental.py +108 -3
- package/templates/qoder/scripts/domain/kg/build/kg_signatures.py +169 -0
- 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/extract/ts_extract.py +111 -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/storage/kg_duckdb.py +43 -0
- 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/requirement/req.py +134 -28
- package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/task/wlkj_panel.py +1348 -0
- 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/core/paths.py +102 -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 +24 -11
- 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/qoder/scripts/validation/eval/qwork_harness.py +1 -1
- package/templates/qoder/scripts/validation/eval/report-commands.md +2 -2
- package/templates/qoder/settings.json +27 -9
- package/templates/qoder/skills/design-import/SKILL.md +3 -3
- package/templates/qoder/skills/design-review/SKILL.md +1 -1
- package/templates/qoder/skills/prd-generator/SKILL.md +4 -4
- package/templates/qoder/skills/prd-review/SKILL.md +1 -1
- package/templates/qoder/skills/prototype-generator/SKILL.md +3 -3
- package/templates/qoder/skills/spec-coder/SKILL.md +1 -1
- package/templates/qoder/skills/spec-generator/SKILL.md +80 -23
- package/templates/qoder/skills/test-generator/SKILL.md +1 -1
- package/templates/qoder/skills/wl-code/SKILL.md +13 -1
- package/templates/qoder/skills/wl-commit/SKILL.md +1 -1
- package/templates/qoder/skills/wl-design/SKILL.md +6 -6
- package/templates/qoder/skills/wl-init/SKILL.md +2 -2
- package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
- package/templates/qoder/skills/wl-prd/SKILL.md +60 -0
- package/templates/qoder/skills/wl-report/SKILL.md +2 -2
- package/templates/qoder/skills/wl-search/SKILL.md +1 -1
- package/templates/qoder/skills/wl-spec/SKILL.md +2 -2
- package/templates/qoder/skills/wl-status/SKILL.md +2 -2
- package/templates/qoder/skills/wl-task/SKILL.md +3 -3
- package/templates/qoder/skills/wl-test/SKILL.md +2 -2
- package/templates/qoder/templates/spec-template.md +124 -0
- package/templates/root/AGENTS.md +41 -14
- package/templates/qoder/scripts/domain/task/zentao_panel.py +0 -451
- package/templates/qoder/skills/wl-prd-full/SKILL.md +0 -121
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +0 -50
- package/templates/qoder/skills/wl-prd-review/SKILL.md +0 -47
|
@@ -9,7 +9,7 @@ for _ in range(10):
|
|
|
9
9
|
break
|
|
10
10
|
if _cp not in _s.path: _s.path.insert(0, _cp)
|
|
11
11
|
from bootstrap import setup; setup()
|
|
12
|
-
from foundation.core.paths import get_repo_root
|
|
12
|
+
from foundation.core.paths import get_repo_root
|
|
13
13
|
|
|
14
14
|
"""
|
|
15
15
|
kg.py — QODER 知识图谱统一 CLI 入口(全系列通用)
|
|
@@ -46,8 +46,8 @@ try:
|
|
|
46
46
|
except Exception:
|
|
47
47
|
pass
|
|
48
48
|
|
|
49
|
-
SCRIPTS_DIR = get_repo_root()
|
|
50
|
-
BASE_DIR = get_repo_root()
|
|
49
|
+
SCRIPTS_DIR = get_repo_root()
|
|
50
|
+
BASE_DIR = get_repo_root()
|
|
51
51
|
sys.path.insert(0, SCRIPTS_DIR)
|
|
52
52
|
_KG_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
53
53
|
if _KG_DIR not in sys.path: sys.path.insert(0, _KG_DIR) # 同包import
|
|
@@ -69,8 +69,10 @@ def _kg_db_path():
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def _has_duckdb():
|
|
72
|
-
"""
|
|
73
|
-
|
|
72
|
+
"""知识图谱本地 DuckDB 是否已构建。
|
|
73
|
+
★ 全迁 PG 后本地不再有 DuckDB——这个函数返回 False,让所有旧路径走 MCP 远程。
|
|
74
|
+
"""
|
|
75
|
+
return False # 本地零 DuckDB,全部走 MCP → 工作台 PG
|
|
74
76
|
|
|
75
77
|
|
|
76
78
|
def _hint_rebuild(action_cn='知识图谱'):
|
|
@@ -821,6 +823,36 @@ def _try_daemon(cmd, rest):
|
|
|
821
823
|
return True
|
|
822
824
|
|
|
823
825
|
|
|
826
|
+
def _undo_msys_pathconv(arg):
|
|
827
|
+
"""Undo Git Bash (MSYS2) path conversion on a single argument.
|
|
828
|
+
|
|
829
|
+
On Windows Git Bash, a leading-slash token like '/insurance' is
|
|
830
|
+
auto-rewritten to '<git-install>/insurance' (e.g. D:/programming/tools/
|
|
831
|
+
Git/Git/insurance) before Python ever sees argv. kg.py endpoints start
|
|
832
|
+
with '/', so every '/xxx' query is corrupted in that shell.
|
|
833
|
+
|
|
834
|
+
Detection: arg looks like an absolute Windows path (X:/...) AND contains
|
|
835
|
+
'/Git/' — then the real intent was '/<tail>'. Safe no-op for genuine paths
|
|
836
|
+
(file commands pass real paths that rarely contain /Git/, and the worst
|
|
837
|
+
case is a real path stays unchanged).
|
|
838
|
+
"""
|
|
839
|
+
if not arg or len(arg) < 2 or arg[1] != ':':
|
|
840
|
+
return arg
|
|
841
|
+
# e.g. D:/programming/tools/Git/Git/insurance -> /insurance
|
|
842
|
+
# Use rfind: the install path itself may contain "Git" (e.g. .../Git/Git/),
|
|
843
|
+
# so the LAST /Git/ is the one MSYS inserts.
|
|
844
|
+
marker = '/Git/'
|
|
845
|
+
idx = arg.rfind(marker)
|
|
846
|
+
if idx < 0:
|
|
847
|
+
return arg
|
|
848
|
+
tail = arg[idx + len(marker):]
|
|
849
|
+
# Only restore when the tail looks like an endpoint fragment (no drive,
|
|
850
|
+
# no '.ext' that would indicate a real file path).
|
|
851
|
+
if ':' in tail or ('.' in tail and tail.rfind('.') > tail.rfind('/')):
|
|
852
|
+
return arg
|
|
853
|
+
return '/' + tail
|
|
854
|
+
|
|
855
|
+
|
|
824
856
|
def main(argv=None):
|
|
825
857
|
argv = argv if argv is not None else sys.argv[1:]
|
|
826
858
|
|
|
@@ -830,6 +862,11 @@ def main(argv=None):
|
|
|
830
862
|
use_daemon = False
|
|
831
863
|
argv = [a for a in argv if a != '--no-daemon']
|
|
832
864
|
|
|
865
|
+
# Undo MSYS path conversion on arguments (Git Bash rewrites '/xxx' →
|
|
866
|
+
# '<git-install>/xxx', which corrupts endpoint queries like /insurance).
|
|
867
|
+
if len(argv) > 1:
|
|
868
|
+
argv[1:] = [_undo_msys_pathconv(a) for a in argv[1:]]
|
|
869
|
+
|
|
833
870
|
if not argv or argv[0] in ('-h', '--help', 'help'):
|
|
834
871
|
print(_usage())
|
|
835
872
|
return 0
|
|
@@ -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():
|
|
@@ -29,6 +29,7 @@ Schema:
|
|
|
29
29
|
import json
|
|
30
30
|
import os
|
|
31
31
|
import sys
|
|
32
|
+
import time
|
|
32
33
|
|
|
33
34
|
import duckdb
|
|
34
35
|
|
|
@@ -102,6 +103,19 @@ def init_schema(con):
|
|
|
102
103
|
value TEXT
|
|
103
104
|
)
|
|
104
105
|
""")
|
|
106
|
+
# ── 文件签名表 (P0: fingerprint 三级增量) ──
|
|
107
|
+
# 记录每个被索引文件的 content_hash (全文) 和 structural_hash (去注释去空白)。
|
|
108
|
+
# 增量构建前比对: NONE=完全没变; COSMETIC=只改注释/空白(跳过重算);
|
|
109
|
+
# STRUCTURAL=真改结构(正常重算)。图谱只记拓扑不记实现 → COSMETIC 跳过 100% 安全。
|
|
110
|
+
con.execute("""
|
|
111
|
+
CREATE TABLE IF NOT EXISTS file_signatures (
|
|
112
|
+
file_path TEXT PRIMARY KEY,
|
|
113
|
+
content_hash TEXT,
|
|
114
|
+
structural_hash TEXT,
|
|
115
|
+
level TEXT,
|
|
116
|
+
updated_at TEXT
|
|
117
|
+
)
|
|
118
|
+
""")
|
|
105
119
|
# ── 搜索层表 ──
|
|
106
120
|
con.execute("CREATE TABLE IF NOT EXISTS keywords(keyword TEXT, file_path TEXT)")
|
|
107
121
|
con.execute("CREATE TABLE IF NOT EXISTS endpoints(endpoint TEXT PRIMARY KEY, controller_file TEXT)")
|
|
@@ -182,6 +196,8 @@ def init_schema(con):
|
|
|
182
196
|
con.execute("CREATE INDEX IF NOT EXISTS idx_alias ON aliases(alias)")
|
|
183
197
|
con.execute("CREATE INDEX IF NOT EXISTS idx_edge_from ON edges(from_id)")
|
|
184
198
|
con.execute("CREATE INDEX IF NOT EXISTS idx_edge_to ON edges(to_id)")
|
|
199
|
+
# P1: 复合索引加速按类型的边查询 (calls/imports 边量大, multi_hop 按类型过滤)
|
|
200
|
+
con.execute("CREATE INDEX IF NOT EXISTS idx_edge_from_type ON edges(from_id, edge_type)")
|
|
185
201
|
con.execute("CREATE INDEX IF NOT EXISTS idx_kw ON keywords(keyword)")
|
|
186
202
|
con.execute("CREATE INDEX IF NOT EXISTS idx_page_type ON pages(page_type)")
|
|
187
203
|
con.execute("CREATE INDEX IF NOT EXISTS idx_field ON fields(field)")
|
|
@@ -192,6 +208,7 @@ def init_schema(con):
|
|
|
192
208
|
con.execute("CREATE INDEX IF NOT EXISTS idx_learn_dev ON learning_events(dev)")
|
|
193
209
|
con.execute("CREATE INDEX IF NOT EXISTS idx_learn_pat_cat ON learning_patterns(category)")
|
|
194
210
|
con.execute("CREATE INDEX IF NOT EXISTS idx_wf_mod ON workflows(module)")
|
|
211
|
+
con.execute("CREATE INDEX IF NOT EXISTS idx_fsig ON file_signatures(file_path)")
|
|
195
212
|
|
|
196
213
|
|
|
197
214
|
def _sql_escape(val):
|
|
@@ -244,6 +261,32 @@ def delete_edges_from(con, from_id, edge_type=None):
|
|
|
244
261
|
con.execute("DELETE FROM edges WHERE from_id = ?", [from_id])
|
|
245
262
|
|
|
246
263
|
|
|
264
|
+
def upsert_file_signature(con, file_path, content_hash, structural_hash, level):
|
|
265
|
+
"""Insert or replace a file's signature (P0: fingerprint incremental).
|
|
266
|
+
|
|
267
|
+
Called after classifying a changed file; stored so the next incremental
|
|
268
|
+
run can compare and skip COSMETIC-only changes.
|
|
269
|
+
"""
|
|
270
|
+
con.execute(
|
|
271
|
+
"INSERT OR REPLACE INTO file_signatures VALUES (?, ?, ?, ?, ?)",
|
|
272
|
+
[file_path, content_hash, structural_hash, level, str(int(time.time()))]
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def get_file_signature(con, file_path):
|
|
277
|
+
"""Read a file's stored signature, or None if never seen."""
|
|
278
|
+
row = con.execute(
|
|
279
|
+
"SELECT content_hash, structural_hash FROM file_signatures WHERE file_path = ?",
|
|
280
|
+
[file_path]
|
|
281
|
+
).fetchone()
|
|
282
|
+
return {'content_hash': row[0], 'structural_hash': row[1]} if row else None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def clear_file_signatures(con):
|
|
286
|
+
"""Wipe all signatures (full rebuild resets the baseline)."""
|
|
287
|
+
con.execute("DELETE FROM file_signatures")
|
|
288
|
+
|
|
289
|
+
|
|
247
290
|
def resolve_entity(con, alias):
|
|
248
291
|
"""Resolve any alias to its canonical entity ID."""
|
|
249
292
|
row = con.execute(
|