@hupan56/wlkj 3.3.13 → 3.3.14
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 +1 -23
- package/package.json +29 -29
- package/templates/qoder/agents/design-agent.md +20 -0
- package/templates/qoder/agents/spec-generator.md +21 -0
- package/templates/qoder/commands/optional/wl-spec.md +2 -12
- package/templates/qoder/commands/optional/wl-status.md +0 -8
- package/templates/qoder/commands/wl-code.md +0 -8
- package/templates/qoder/commands/wl-commit.md +1 -11
- package/templates/qoder/commands/wl-init.md +129 -129
- package/templates/qoder/commands/wl-prd.md +0 -24
- package/templates/qoder/commands/wl-search.md +11 -26
- package/templates/qoder/commands/wl-task.md +1 -11
- package/templates/qoder/commands/wl-test.md +0 -8
- package/templates/qoder/config.yaml +8 -3
- package/templates/qoder/hooks/session-start.py +365 -384
- package/templates/qoder/hooks/stop-eval.py +254 -383
- package/templates/qoder/scripts/capability/adapters/mcp.py +314 -322
- package/templates/qoder/scripts/capability/registry.py +248 -245
- package/templates/qoder/scripts/capability/registry_mcp.py +4 -10
- package/templates/qoder/scripts/capability/smoke_test_report.json.new +94 -0
- package/templates/qoder/scripts/deployment/setup/init_doctor.py +2 -6
- package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +14 -0
- package/templates/qoder/scripts/deployment/setup/setup.py +2 -16
- package/templates/qoder/scripts/domain/integration/return_to_platform.py +2 -4
- package/templates/qoder/scripts/domain/integration/spec_upload.py +4 -4
- package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +12 -12
- package/templates/qoder/scripts/domain/kg/build/build_relations.py +12 -12
- 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/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/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_link_db.py +235 -0
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +5 -41
- package/templates/qoder/scripts/domain/kg/kg.py +0 -14
- package/templates/qoder/scripts/domain/kg/search/enrich_prompt.py +238 -0
- package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
- package/templates/qoder/scripts/domain/kg/switch_project.py +2 -2
- package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
- package/templates/qoder/scripts/domain/task/wlkj_panel.py +1348 -1503
- package/templates/qoder/scripts/engine/poller.py +219 -0
- package/templates/qoder/scripts/foundation/bootstrap.py +145 -156
- package/templates/qoder/scripts/orchestration/wlkj.py +189 -306
- package/templates/qoder/settings.json +0 -8
- package/templates/qoder/skills/wl-spec/SKILL.md +1 -1
- package/templates/qoder/commands/wl-data.md +0 -46
- package/templates/qoder/commands/wl-fix.md +0 -47
- package/templates/qoder/commands/wl-knowledge.md +0 -41
- package/templates/qoder/commands/wl-review.md +0 -49
- package/templates/qoder/contracts/insight.md +0 -55
- package/templates/qoder/hooks/pre-tool-use-commit.py +0 -124
- package/templates/qoder/scripts/foundation/io/context_cache.py +0 -94
- package/templates/qoder/scripts/tool_guide.md +0 -70
- package/templates/qoder/scripts/validation/eval/alignment_matrix.py +0 -176
- package/templates/qoder/scripts/validation/eval/bf2_content_fidelity.py +0 -110
- package/templates/qoder/scripts/validation/eval/bf2_llmjudge.py +0 -104
- package/templates/qoder/scripts/validation/eval/bf_score.py +0 -218
- package/templates/qoder/scripts/validation/eval/code_flywheel.py +0 -150
- package/templates/qoder/scripts/validation/eval/dispatcher_ab.py +0 -156
- package/templates/qoder/scripts/validation/eval/dispatcher_ab_2026-07-21.json +0 -23
- package/templates/qoder/scripts/validation/eval/feature_fidelity_flywheel.py +0 -143
- package/templates/qoder/scripts/validation/eval/gradient_matrix.py +0 -261
- package/templates/qoder/scripts/validation/eval/gradient_matrix_baseline_2026-07-21.json +0 -33
- package/templates/qoder/scripts/validation/eval/metrics_dashboard.py +0 -105
- package/templates/qoder/scripts/validation/eval/multi_turn_flywheel.py +0 -118
- package/templates/qoder/scripts/validation/eval/prd_fidelity_flywheel.py +0 -128
- package/templates/qoder/scripts/validation/eval/prd_flywheel.py +0 -148
- package/templates/qoder/scripts/validation/eval/prototype_fidelity_flywheel.py +0 -166
- package/templates/qoder/scripts/validation/eval/recall_flywheel.py +0 -148
- package/templates/qoder/scripts/validation/eval/robustness_flywheel.py +0 -139
- package/templates/qoder/scripts/validation/eval/speed_accuracy_flywheel.py +0 -188
- package/templates/qoder/scripts/validation/eval/task_flywheel.py +0 -124
- package/templates/qoder/scripts/validation/eval/token_flywheel.py +0 -88
- package/templates/qoder/scripts/validation/metrics/eval_code_ac.py +0 -177
- package/templates/qoder/scripts/validation/metrics/lint_cases.py +0 -170
- package/templates/qoder/scripts/validation/test/test_context_cache.py +0 -78
- package/templates/qoder/scripts/validation/test/test_lint_cases.py +0 -60
- package/templates/qoder/scripts/validation/test/test_pre_tool_use_commit.py +0 -70
- package/templates/workspace/specs/REQ-TEST-CTX/.context-cache.json +0 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
|
|
4
|
+
import os as _o, sys as _s
|
|
5
|
+
_f = _o.path.abspath(__file__)
|
|
6
|
+
for _ in range(10):
|
|
7
|
+
_f = _o.path.dirname(_f)
|
|
8
|
+
_cp = _o.path.join(_f, 'foundation')
|
|
9
|
+
if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
|
|
10
|
+
break
|
|
11
|
+
if _cp not in _s.path: _s.path.insert(0, _cp)
|
|
12
|
+
from bootstrap import setup; setup()
|
|
13
|
+
|
|
14
|
+
"""kg_link_db.py - 扫代码 @TableName 注解, 建 Entity 类 ↔ 数据库表 的语义桥。
|
|
15
|
+
|
|
16
|
+
解决"数据层孤岛"问题: kg_db.duckdb 有表结构, 但不知道对应代码里的哪个 Entity。
|
|
17
|
+
本脚本扫 Java 源码里的 @TableName("xxx") 注解, 提取:
|
|
18
|
+
Entity 类名 → 数据库表名 → (关联到) db_columns 的真实列
|
|
19
|
+
|
|
20
|
+
调研依据 (FalkorDB "结构遍历 > 语义匹配"):
|
|
21
|
+
- 不靠表名子串猜 endpoint (验证过 0 命中, 不准)
|
|
22
|
+
- 靠代码显式声明的关系 (@TableName 注解) 建边, 这才准
|
|
23
|
+
|
|
24
|
+
产出表 (写进 kg_db.duckdb, 不动 kg.duckdb 写锁):
|
|
25
|
+
entity_table_map:
|
|
26
|
+
entity_class TEXT -- QualityCaseBo (代码层类名)
|
|
27
|
+
table_name TEXT -- quality_case (数据库表名, 关联 db_tables)
|
|
28
|
+
source_file TEXT -- 哪个 .java 读到的
|
|
29
|
+
annotation TEXT -- @TableName("quality_case") 原文
|
|
30
|
+
|
|
31
|
+
建好后可遍历:
|
|
32
|
+
数据库表 → entity_table_map → Entity 类 → (kg.duckdb) Controller/接口
|
|
33
|
+
|
|
34
|
+
用法:
|
|
35
|
+
python kg_link_db.py # 扫 data/code/ 全量
|
|
36
|
+
python kg_link_db.py --rebuild # 清旧重建
|
|
37
|
+
python kg_link_db.py --stats # 看统计
|
|
38
|
+
"""
|
|
39
|
+
import os
|
|
40
|
+
import re
|
|
41
|
+
import sys
|
|
42
|
+
import time
|
|
43
|
+
|
|
44
|
+
SCRIPTS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 子包→scripts/根
|
|
45
|
+
sys.path.insert(0, SCRIPTS)
|
|
46
|
+
|
|
47
|
+
import duckdb
|
|
48
|
+
from foundation.core.paths import DATA_INDEX_DIR, PROJECT_ROOT
|
|
49
|
+
|
|
50
|
+
CODE_DIR = str(PROJECT_ROOT / 'data' / 'code')
|
|
51
|
+
DB_PATH = str(DATA_INDEX_DIR / 'kg_db.duckdb')
|
|
52
|
+
|
|
53
|
+
# @TableName 的两种格式:
|
|
54
|
+
# @TableName("quality_case")
|
|
55
|
+
# @TableName(value = "quality_case")
|
|
56
|
+
RE_TABLENAME = re.compile(
|
|
57
|
+
r'@TableName\s*\(\s*(?:value\s*=\s*)?"([^"]+)"',
|
|
58
|
+
re.IGNORECASE
|
|
59
|
+
)
|
|
60
|
+
# 紧跟注解的 class 声明: class QualityCaseBo
|
|
61
|
+
RE_CLASS = re.compile(r'class\s+(\w+)')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _normalize_table_name(raw):
|
|
65
|
+
"""归一化表名, 提升和数据库真实表的匹配率。
|
|
66
|
+
处理注解写法不规范的情况:
|
|
67
|
+
fywl_ics_cloud.sys_dept → sys_dept (去库名前缀)
|
|
68
|
+
empContractImport → emp_contract_import (camelCase→snake_case)
|
|
69
|
+
quality_case → quality_case (已是规范, 不变)
|
|
70
|
+
"""
|
|
71
|
+
t = raw.strip().lower()
|
|
72
|
+
# 去库名前缀 (db.table → table)
|
|
73
|
+
if '.' in t:
|
|
74
|
+
t = t.rsplit('.', 1)[-1]
|
|
75
|
+
# 去常见前缀 t_/tb_/t (数据库表常加这些, 注解有时不加)
|
|
76
|
+
# 注意: 不在这里去前缀, 因为 db_tables 里有的带 t_ 有的不带, 留原样让 JOIN 双向兜
|
|
77
|
+
# camelCase → snake_case (Java 注解里有时写驼峰表名)
|
|
78
|
+
if re.search(r'[a-z][A-Z]', t):
|
|
79
|
+
t = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', t).lower()
|
|
80
|
+
return t
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _scan_java_files():
|
|
84
|
+
"""扫 data/code/ 下所有 .java, 返回 [(entity_class, table_name, normalized_table, source_file, annotation)]。"""
|
|
85
|
+
mappings = []
|
|
86
|
+
scanned = 0
|
|
87
|
+
for root, dirs, files in os.walk(CODE_DIR):
|
|
88
|
+
# 跳过非业务目录
|
|
89
|
+
dirs[:] = [d for d in dirs if d not in (
|
|
90
|
+
'.git', 'node_modules', '__pycache__', 'target', 'test')]
|
|
91
|
+
for fn in files:
|
|
92
|
+
if not fn.endswith('.java'):
|
|
93
|
+
continue
|
|
94
|
+
scanned += 1
|
|
95
|
+
fpath = os.path.join(root, fn)
|
|
96
|
+
try:
|
|
97
|
+
for enc in ('utf-8', 'gbk'):
|
|
98
|
+
try:
|
|
99
|
+
with open(fpath, encoding=enc) as f:
|
|
100
|
+
content = f.read()
|
|
101
|
+
break
|
|
102
|
+
except (UnicodeDecodeError, OSError):
|
|
103
|
+
content = ''
|
|
104
|
+
continue
|
|
105
|
+
except OSError:
|
|
106
|
+
continue
|
|
107
|
+
if '@TableName' not in content:
|
|
108
|
+
continue
|
|
109
|
+
# 找所有 @TableName 注解
|
|
110
|
+
for m in RE_TABLENAME.finditer(content):
|
|
111
|
+
table_name = m.group(1)
|
|
112
|
+
normalized = _normalize_table_name(table_name)
|
|
113
|
+
annotation = content[m.start():m.end() + 1]
|
|
114
|
+
# 找注解之后最近的 class 声明 (Entity 类名)
|
|
115
|
+
after = content[m.end():m.end() + 500]
|
|
116
|
+
cm = RE_CLASS.search(after)
|
|
117
|
+
entity_class = cm.group(1) if cm else os.path.splitext(fn)[0]
|
|
118
|
+
rel = os.path.relpath(fpath, PROJECT_ROOT).replace('\\', '/')
|
|
119
|
+
mappings.append((entity_class, table_name, normalized, rel, annotation))
|
|
120
|
+
return mappings, scanned
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def build(rebuild=False):
|
|
124
|
+
"""扫代码 @TableName → 写 entity_table_map。"""
|
|
125
|
+
t0 = time.time()
|
|
126
|
+
print('扫描 data/code/ 下的 @TableName 注解...')
|
|
127
|
+
mappings, scanned = _scan_java_files()
|
|
128
|
+
print(' 扫描 %d 个 .java 文件, 找到 %d 个 @TableName 映射 (%.1fs)'
|
|
129
|
+
% (scanned, len(mappings), time.time() - t0))
|
|
130
|
+
|
|
131
|
+
if not mappings:
|
|
132
|
+
print('未找到任何 @TableName 注解。检查 data/code/ 是否有 Java 源码。')
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
con = duckdb.connect(DB_PATH)
|
|
136
|
+
try:
|
|
137
|
+
con.execute("""CREATE TABLE IF NOT EXISTS entity_table_map (
|
|
138
|
+
entity_class TEXT,
|
|
139
|
+
table_name TEXT,
|
|
140
|
+
normalized_table TEXT,
|
|
141
|
+
source_file TEXT,
|
|
142
|
+
annotation TEXT
|
|
143
|
+
)""")
|
|
144
|
+
con.execute("CREATE INDEX IF NOT EXISTS idx_etm_table ON entity_table_map(table_name)")
|
|
145
|
+
con.execute("CREATE INDEX IF NOT EXISTS idx_etm_norm ON entity_table_map(normalized_table)")
|
|
146
|
+
con.execute("CREATE INDEX IF NOT EXISTS idx_etm_class ON entity_table_map(entity_class)")
|
|
147
|
+
|
|
148
|
+
if rebuild:
|
|
149
|
+
con.execute("DELETE FROM entity_table_map")
|
|
150
|
+
|
|
151
|
+
# 用 JSONL COPY 批量插入 (DuckDB 1.4 executemany 慢)
|
|
152
|
+
import json as _json
|
|
153
|
+
import tempfile
|
|
154
|
+
tmp = tempfile.NamedTemporaryFile(suffix='.jsonl', delete=False,
|
|
155
|
+
mode='w', encoding='utf-8')
|
|
156
|
+
cols = ['entity_class', 'table_name', 'normalized_table', 'source_file', 'annotation']
|
|
157
|
+
for row in mappings:
|
|
158
|
+
obj = {cols[i]: (row[i] if row[i] is not None else '')
|
|
159
|
+
for i in range(len(cols))}
|
|
160
|
+
tmp.write(_json.dumps(obj, ensure_ascii=False) + '\n')
|
|
161
|
+
tmp.close()
|
|
162
|
+
try:
|
|
163
|
+
con.execute("COPY entity_table_map FROM '%s' (FORMAT JSON)"
|
|
164
|
+
% tmp.name.replace('\\', '/'))
|
|
165
|
+
finally:
|
|
166
|
+
os.unlink(tmp.name)
|
|
167
|
+
|
|
168
|
+
con.execute("INSERT OR REPLACE INTO build_meta(key, value) VALUES (?,?)",
|
|
169
|
+
['entity_table_map_built', str(time.time())])
|
|
170
|
+
print('写入 %d 条 Entity→表 映射到 entity_table_map' % len(mappings))
|
|
171
|
+
return len(mappings)
|
|
172
|
+
finally:
|
|
173
|
+
con.close()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def query_impact(table_name):
|
|
177
|
+
"""查某张表关联的 Entity 类 (给"改表影响分析"用)。"""
|
|
178
|
+
con = duckdb.connect(DB_PATH, read_only=True)
|
|
179
|
+
try:
|
|
180
|
+
rows = con.execute(
|
|
181
|
+
"SELECT DISTINCT entity_class, source_file FROM entity_table_map "
|
|
182
|
+
"WHERE table_name = ?", [table_name]
|
|
183
|
+
).fetchall()
|
|
184
|
+
return rows
|
|
185
|
+
finally:
|
|
186
|
+
con.close()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def stats():
|
|
190
|
+
"""映射统计 + 和 db_tables/db_columns 的关联率。"""
|
|
191
|
+
con = duckdb.connect(DB_PATH, read_only=True)
|
|
192
|
+
try:
|
|
193
|
+
total = con.execute("SELECT COUNT(*) FROM entity_table_map").fetchone()[0]
|
|
194
|
+
classes = con.execute("SELECT COUNT(DISTINCT entity_class) FROM entity_table_map").fetchone()[0]
|
|
195
|
+
tables_mapped = con.execute("SELECT COUNT(DISTINCT table_name) FROM entity_table_map").fetchone()[0]
|
|
196
|
+
# 关联率: 映射的表名 vs 数据库真实表名 (原始 + 归一化)
|
|
197
|
+
try:
|
|
198
|
+
raw_matched = con.execute("""
|
|
199
|
+
SELECT COUNT(DISTINCT m.table_name) FROM entity_table_map m
|
|
200
|
+
JOIN db_tables t ON m.table_name = t.table_name
|
|
201
|
+
""").fetchone()[0]
|
|
202
|
+
norm_matched = con.execute("""
|
|
203
|
+
SELECT COUNT(DISTINCT m.normalized_table) FROM entity_table_map m
|
|
204
|
+
JOIN db_tables t ON m.normalized_table = t.table_name
|
|
205
|
+
""").fetchone()[0]
|
|
206
|
+
db_total = con.execute("SELECT COUNT(DISTINCT table_name) FROM db_tables").fetchone()[0]
|
|
207
|
+
except Exception:
|
|
208
|
+
raw_matched, norm_matched, db_total = 0, 0, 0
|
|
209
|
+
print('entity_table_map 统计:')
|
|
210
|
+
print(' 总映射: %d' % total)
|
|
211
|
+
print(' Entity 类数: %d' % classes)
|
|
212
|
+
print(' 映射的表名数: %d' % tables_mapped)
|
|
213
|
+
if db_total:
|
|
214
|
+
print(' 原始表名匹配: %d/%d (%.0f%%)'
|
|
215
|
+
% (raw_matched, db_total, raw_matched / db_total * 100))
|
|
216
|
+
print(' 归一化后匹配: %d/%d (%.0f%%)'
|
|
217
|
+
% (norm_matched, db_total, norm_matched / db_total * 100))
|
|
218
|
+
# 抽样
|
|
219
|
+
sample = con.execute(
|
|
220
|
+
"SELECT entity_class, table_name FROM entity_table_map LIMIT 8"
|
|
221
|
+
).fetchall()
|
|
222
|
+
print(' 抽样:')
|
|
223
|
+
for ec, tn in sample:
|
|
224
|
+
print(' %s → %s' % (ec, tn))
|
|
225
|
+
finally:
|
|
226
|
+
con.close()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == '__main__':
|
|
230
|
+
args = sys.argv[1:]
|
|
231
|
+
if '--stats' in args:
|
|
232
|
+
stats()
|
|
233
|
+
else:
|
|
234
|
+
build(rebuild='--rebuild' in args)
|
|
235
|
+
stats()
|
|
@@ -278,31 +278,6 @@ def build_embeddings(batch_size=64):
|
|
|
278
278
|
con.close()
|
|
279
279
|
|
|
280
280
|
|
|
281
|
-
def _cloud_rag_search(query, top_k=10):
|
|
282
|
-
"""云优先语义搜索:调工作台 MCP rag_search(本地无 KG/embedding,全迁云)。
|
|
283
|
-
|
|
284
|
-
Returns:
|
|
285
|
-
[(entity_id, text, score), ...] 或 None(云不可用时返回 None,让调用方退本地)。
|
|
286
|
-
"""
|
|
287
|
-
try:
|
|
288
|
-
from domain.kg.search._remote import call_remote
|
|
289
|
-
data = call_remote("rag_search", {"query": query, "top_k": top_k})
|
|
290
|
-
except Exception:
|
|
291
|
-
return None
|
|
292
|
-
items = data.get("items") or []
|
|
293
|
-
out = []
|
|
294
|
-
for it in items[:top_k]:
|
|
295
|
-
eid = it.get("entity_id") or it.get("id") or ""
|
|
296
|
-
text = it.get("text") or it.get("name") or ""
|
|
297
|
-
score = it.get("score")
|
|
298
|
-
try:
|
|
299
|
-
score = float(score) if score is not None else 0.0
|
|
300
|
-
except Exception:
|
|
301
|
-
score = 0.0
|
|
302
|
-
out.append((eid, text, score))
|
|
303
|
-
return out
|
|
304
|
-
|
|
305
|
-
|
|
306
281
|
def semantic_search(query, top_k=10):
|
|
307
282
|
"""语义搜索: query→向量→余弦相似度→top_k 实体。
|
|
308
283
|
|
|
@@ -681,24 +656,13 @@ if __name__ == '__main__':
|
|
|
681
656
|
n = build_embeddings()
|
|
682
657
|
print('Built %d embeddings' % n)
|
|
683
658
|
|
|
684
|
-
elif
|
|
685
|
-
|
|
686
|
-
# 兼容两种姿势: `search <词>` 和裸词 `<词>`(wlkj.py semantic 保险异常 透传过来)。
|
|
687
|
-
# ★ 云优先:本地无 KG/embedding(全迁云),先调云 rag_search;云不可用才退本地 semantic_search。
|
|
688
|
-
query = sys.argv[2] if sys.argv[1] == 'search' else sys.argv[1]
|
|
689
|
-
results = _cloud_rag_search(query)
|
|
690
|
-
if results is None:
|
|
691
|
-
# 云不可用 → 退本地(开发期有本地 embedding 时仍可用)
|
|
692
|
-
results = semantic_search(query)
|
|
659
|
+
elif len(sys.argv) > 2 and sys.argv[1] == 'search':
|
|
660
|
+
results = semantic_search(sys.argv[2])
|
|
693
661
|
if results:
|
|
694
|
-
for
|
|
695
|
-
|
|
696
|
-
eid, text, score = row
|
|
697
|
-
print(' %.3f %s' % (score, text[:70]))
|
|
698
|
-
else:
|
|
699
|
-
print(' %s' % row)
|
|
662
|
+
for eid, text, score in results:
|
|
663
|
+
print(' %.3f %s' % (score, text[:70]))
|
|
700
664
|
else:
|
|
701
|
-
print('(
|
|
665
|
+
print('(无结果或 embedding 未构建)')
|
|
702
666
|
|
|
703
667
|
elif len(sys.argv) > 1 and sys.argv[1] == '--status':
|
|
704
668
|
st = embedding_status()
|
|
@@ -872,20 +872,6 @@ def main(argv=None):
|
|
|
872
872
|
return 0
|
|
873
873
|
cmd = argv[0]
|
|
874
874
|
rest = argv[1:]
|
|
875
|
-
# ★ MCP 名兼容:AI 常把 MCP 工具名(search_code)当 kg.py 子命令调 → "未知子命令"。
|
|
876
|
-
# 在此统一归一化,AI 猜哪个名(kg.py 子命令 / MCP 工具名)都能命中(治工作流混名)。
|
|
877
|
-
_MCP_ALIASES = {
|
|
878
|
-
'search_code': 'search', 'search_api': 'api', 'search_prds': 'prd',
|
|
879
|
-
'search_field': 'search', 'search_style': 'search', 'search_wiki': 'wiki',
|
|
880
|
-
'rag_search': 'semantic', 'ask_corpus': 'semantic',
|
|
881
|
-
'context_pack': 'context', 'context_360': 'context360',
|
|
882
|
-
'get_impact': 'impact', 'multi_hop': 'hop',
|
|
883
|
-
'coverage_matrix': 'coverage', 'feature_overview': 'feature',
|
|
884
|
-
'get_workflow': 'workflow', 'get_design_system': 'design-system',
|
|
885
|
-
'fill_prototype': 'fill-prototype',
|
|
886
|
-
}
|
|
887
|
-
if cmd in _MCP_ALIASES:
|
|
888
|
-
cmd = _MCP_ALIASES[cmd]
|
|
889
875
|
# 查找子命令
|
|
890
876
|
handler = None
|
|
891
877
|
for name, fn, _ in COMMANDS:
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
|
|
4
|
+
import os as _o, sys as _s
|
|
5
|
+
_f = _o.path.abspath(__file__)
|
|
6
|
+
for _ in range(10):
|
|
7
|
+
_f = _o.path.dirname(_f)
|
|
8
|
+
_cp = _o.path.join(_f, 'foundation')
|
|
9
|
+
if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
|
|
10
|
+
break
|
|
11
|
+
if _cp not in _s.path: _s.path.insert(0, _cp)
|
|
12
|
+
from bootstrap import setup; setup()
|
|
13
|
+
from foundation.core.paths import get_repo_root
|
|
14
|
+
|
|
15
|
+
"""enrich_prompt.py - 极速提示词润色 (用户提问后第 0 步)
|
|
16
|
+
|
|
17
|
+
目标: 在用户提问后, <10ms 内判断这句话命中哪些业务关键词,
|
|
18
|
+
输出一个「润色后的提示词」给用户确认, 再走原命令。
|
|
19
|
+
|
|
20
|
+
设计:
|
|
21
|
+
- 纯内存查表 (不跑重检索 context_pack, 那个留给确认后)
|
|
22
|
+
- 中文分词复用 common/terms.py 的 expand_chinese_query
|
|
23
|
+
- 命中 keyword-index / api-index / prd-index 的键
|
|
24
|
+
- 命中 0 个 = 非业务消息 (谢谢/继续), 直接放行不润色
|
|
25
|
+
|
|
26
|
+
用法:
|
|
27
|
+
python .qoder/scripts/enrich_prompt.py "保险批改那个异常统计页加个审批按钮"
|
|
28
|
+
python .qoder/scripts/enrich_prompt.py "保险批改..." --json # 机器可读
|
|
29
|
+
|
|
30
|
+
输出 (默认人类可读):
|
|
31
|
+
── 极速润色 ──
|
|
32
|
+
业务词: 保险→insurance, 批改→amend, 异常→abnormal...
|
|
33
|
+
相关代码符号: 108 处 (abnormalovertime, approval-cell...)
|
|
34
|
+
相关API: 5 个
|
|
35
|
+
历史 PRD: 2 篇
|
|
36
|
+
──────────────
|
|
37
|
+
将以上下文继续。回复"改:xxx"调整, 任意其它内容=接受。
|
|
38
|
+
"""
|
|
39
|
+
import os, sys, json, time
|
|
40
|
+
|
|
41
|
+
if sys.platform == 'win32':
|
|
42
|
+
try:
|
|
43
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
BASE = get_repo_root()
|
|
48
|
+
sys.path.insert(0, os.path.join(BASE, '.qoder', 'scripts'))
|
|
49
|
+
|
|
50
|
+
IDX_DIR = os.path.join(BASE, 'data', 'index')
|
|
51
|
+
# 缓存: 同一进程内只加载一次索引
|
|
52
|
+
_CACHE = {}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _load(name):
|
|
56
|
+
"""加载索引 (优先 DuckDB, 回退 JSON)。复用 search_index.load_index 的 fallback 逻辑。"""
|
|
57
|
+
if name in _CACHE:
|
|
58
|
+
return _CACHE[name]
|
|
59
|
+
try:
|
|
60
|
+
from domain.kg.search.search_index import load_index
|
|
61
|
+
d = load_index(name) or {}
|
|
62
|
+
except ImportError:
|
|
63
|
+
# search_index 不可用时, 直接读 JSON (老逻辑)
|
|
64
|
+
d = {}
|
|
65
|
+
path = os.path.join(IDX_DIR, name)
|
|
66
|
+
if os.path.isfile(path):
|
|
67
|
+
try:
|
|
68
|
+
for enc in ('utf-8', 'gbk'):
|
|
69
|
+
try:
|
|
70
|
+
with open(path, encoding=enc) as f:
|
|
71
|
+
d = json.load(f)
|
|
72
|
+
break
|
|
73
|
+
except (OSError, UnicodeDecodeError):
|
|
74
|
+
continue
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
_CACHE[name] = d
|
|
78
|
+
return d
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# 短而无意义的英文 token 黑名单 (避免子串误命中 'cs'/'fl'/'in' 这类噪声)
|
|
82
|
+
# 提到模块级: _match_keys 和 code_total 共用一份
|
|
83
|
+
NOISE_KEYS = {
|
|
84
|
+
'app', 'api', 'all', 'add', 'age', 'and', 'are', 'ars', 'ase', 'ask',
|
|
85
|
+
'ate', 'aud', 'bar', 'big', 'btn', 'can', 'car', 'cas', 'cat', 'code',
|
|
86
|
+
'con', 'cor', 'cs', 'cur', 'dae', 'dat', 'day', 'del', 'dep', 'des',
|
|
87
|
+
'det', 'dim', 'div', 'doc', 'dos', 'ear', 'ecs', 'ema', 'end', 'ent',
|
|
88
|
+
'err', 'eva', 'exe', 'exp', 'ext', 'fil', 'fix', 'fl', 'for', 'ge',
|
|
89
|
+
'gen', 'get', 'has', 'her', 'his', 'ics', 'idx', 'imp', 'int', 'inv',
|
|
90
|
+
'io', 'is', 'iso', 'ite', 'its', 'key', 'lab', 'lan', 'lat', 'let',
|
|
91
|
+
'lin', 'lis', 'log', 'ma', 'map', 'max', 'min', 'mod', 'msg', 'new',
|
|
92
|
+
'nil', 'not', 'now', 'num', 'obj', 'off', 'one', 'opt', 'ord', 'out',
|
|
93
|
+
'ove', 'own', 'pag', 'par', 'pas', 'pay', 'pen', 'per', 'pos', 'pre',
|
|
94
|
+
'pro', 'put', 'que', 'rec', 'red', 'ref', 'reg', 'rep', 'res', 'ret',
|
|
95
|
+
'rev', 'row', 'run', 'sa', 'sel', 'set', 'sev', 'sie', 'sit', 'sor',
|
|
96
|
+
'src', 'sta', 'sub', 'sum', 'sys', 'tab', 'tar', 'tes', 'tex', 'tim',
|
|
97
|
+
'tmp', 'tok', 'tom', 'top', 'tot', 'tru', 'try', 'typ', 'udp', 'uni',
|
|
98
|
+
'upd', 'url', 'use', 'usr', 'val', 'ver', 'vid', 'vio', 'vis', 'voi',
|
|
99
|
+
'was', 'way', 'who', 'why', 'win', 'wri', 'xml', 'you', 'your', 'zone',
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _match_keys(query_lower, keys, max_hits=15, min_len=3):
|
|
104
|
+
"""在 keys 里找被 query 包含的 (子串匹配), 返回命中列表 + 总数。
|
|
105
|
+
min_len: 短于这个长度的键忽略 (避免 'cs'/'fl'/'in' 这类噪声子串)。"""
|
|
106
|
+
hits = []
|
|
107
|
+
for k in keys:
|
|
108
|
+
kl = k.lower()
|
|
109
|
+
if len(kl) < min_len:
|
|
110
|
+
continue
|
|
111
|
+
if kl in NOISE_KEYS:
|
|
112
|
+
continue
|
|
113
|
+
if kl in query_lower:
|
|
114
|
+
hits.append(k)
|
|
115
|
+
if len(hits) >= max_hits:
|
|
116
|
+
break
|
|
117
|
+
return hits
|
|
118
|
+
for k in keys:
|
|
119
|
+
kl = k.lower()
|
|
120
|
+
if len(kl) < min_len:
|
|
121
|
+
continue
|
|
122
|
+
if kl in NOISE:
|
|
123
|
+
continue
|
|
124
|
+
if kl in query_lower:
|
|
125
|
+
hits.append(k)
|
|
126
|
+
if len(hits) >= max_hits:
|
|
127
|
+
break
|
|
128
|
+
return hits
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def enrich(query):
|
|
132
|
+
"""润色主函数。返回 dict:
|
|
133
|
+
{
|
|
134
|
+
'is_business': bool, # 是否业务消息 (有命中才算)
|
|
135
|
+
'cn_words': [...], # 中文分词出的英文搜索词
|
|
136
|
+
'code_hits': [...], # 命中的代码符号 (样例)
|
|
137
|
+
'code_total': int, # 代码命中总数
|
|
138
|
+
'api_hits': [...], # 命中的 API
|
|
139
|
+
'prd_hits': [...], # 命中的历史 PRD
|
|
140
|
+
'elapsed_ms': float, # 耗时
|
|
141
|
+
'polished': str, # 润色后的提示词 (人类可读)
|
|
142
|
+
}
|
|
143
|
+
"""
|
|
144
|
+
t0 = time.perf_counter()
|
|
145
|
+
from foundation.integrations.terms import expand_chinese_query
|
|
146
|
+
|
|
147
|
+
# 1. 中文分词 → 英文搜索词
|
|
148
|
+
cn_words = list(dict.fromkeys(expand_chinese_query(query))) # 去重保序
|
|
149
|
+
|
|
150
|
+
# 2. 构造匹配串: 原句 + 扩展出的英文词, 一起拿去命中索引
|
|
151
|
+
match_str = query.lower()
|
|
152
|
+
for w in cn_words:
|
|
153
|
+
match_str += ' ' + w.lower()
|
|
154
|
+
|
|
155
|
+
# 3. 命中三类索引
|
|
156
|
+
code_idx = _load('code-keyword.json')
|
|
157
|
+
code_keys = list(code_idx.keys()) if isinstance(code_idx, dict) else []
|
|
158
|
+
code_hits = _match_keys(match_str, code_keys)
|
|
159
|
+
|
|
160
|
+
api_idx = _load('code-api.json')
|
|
161
|
+
api_keys = []
|
|
162
|
+
if isinstance(api_idx, dict):
|
|
163
|
+
api_keys = list(api_idx.keys())
|
|
164
|
+
api_hits = _match_keys(match_str, api_keys)
|
|
165
|
+
|
|
166
|
+
prd_idx = _load('prd-index.json')
|
|
167
|
+
prd_keys = []
|
|
168
|
+
if isinstance(prd_idx, dict):
|
|
169
|
+
prd_keys = list(prd_idx.keys())
|
|
170
|
+
prd_hits = _match_keys(match_str, prd_keys, max_hits=5)
|
|
171
|
+
|
|
172
|
+
# 4. 代码命中总数 (不限 max, 用于展示规模; 用与 _match_keys 一致的过滤)
|
|
173
|
+
code_total = 0
|
|
174
|
+
if code_keys:
|
|
175
|
+
code_total = sum(1 for k in code_keys
|
|
176
|
+
if len(k) >= 3 and k.lower() not in NOISE_KEYS
|
|
177
|
+
and k.lower() in match_str)
|
|
178
|
+
|
|
179
|
+
elapsed_ms = (time.perf_counter() - t0) * 1000
|
|
180
|
+
is_business = bool(code_hits or api_hits or prd_hits or cn_words)
|
|
181
|
+
|
|
182
|
+
# 5. 组装润色后提示词 (人类可读)
|
|
183
|
+
lines = []
|
|
184
|
+
if not is_business:
|
|
185
|
+
lines.append('(非业务消息,无需润色,直接放行)')
|
|
186
|
+
else:
|
|
187
|
+
if cn_words:
|
|
188
|
+
shown = ', '.join(cn_words[:8])
|
|
189
|
+
lines.append('业务词: ' + shown)
|
|
190
|
+
if code_total:
|
|
191
|
+
sample = ', '.join(code_hits[:8])
|
|
192
|
+
more = '' if code_total <= len(code_hits) else ' ...'
|
|
193
|
+
lines.append('相关代码符号: %d 处 (%s%s)' % (code_total, sample, more))
|
|
194
|
+
if api_hits:
|
|
195
|
+
lines.append('相关 API: %d 个 (%s)' % (len(api_hits), ', '.join(api_hits[:5])))
|
|
196
|
+
if prd_hits:
|
|
197
|
+
lines.append('历史 PRD: %s' % ', '.join(prd_hits[:5]))
|
|
198
|
+
|
|
199
|
+
polished = '\n'.join(lines)
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
'is_business': is_business,
|
|
203
|
+
'cn_words': cn_words,
|
|
204
|
+
'code_hits': code_hits,
|
|
205
|
+
'code_total': code_total,
|
|
206
|
+
'api_hits': api_hits,
|
|
207
|
+
'prd_hits': prd_hits,
|
|
208
|
+
'elapsed_ms': round(elapsed_ms, 2),
|
|
209
|
+
'polished': polished,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def main():
|
|
214
|
+
args = sys.argv[1:]
|
|
215
|
+
if not args:
|
|
216
|
+
print(__doc__)
|
|
217
|
+
print('错误: 缺少查询参数。用法: enrich_prompt.py "你的问题"')
|
|
218
|
+
sys.exit(2)
|
|
219
|
+
query = ' '.join(args[:1]) if args[0] in ('--json',) else ' '.join(args)
|
|
220
|
+
# 处理 --json 标志
|
|
221
|
+
as_json = '--json' in args
|
|
222
|
+
query = query.replace('--json', '').strip()
|
|
223
|
+
|
|
224
|
+
result = enrich(query)
|
|
225
|
+
if as_json:
|
|
226
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
# 人类可读输出
|
|
230
|
+
print('── 极速润色 (%.1fms) ──' % result['elapsed_ms'])
|
|
231
|
+
print(result['polished'])
|
|
232
|
+
if result['is_business']:
|
|
233
|
+
print('──────────────')
|
|
234
|
+
print('将以上下文继续。回复"改:xxx"调整,任意其它内容=接受。')
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
if __name__ == '__main__':
|
|
238
|
+
main()
|