@hupan56/wlkj 3.3.14 → 3.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/bin/cli.js +16 -1
  2. package/package.json +1 -1
  3. package/templates/qoder/commands/optional/wl-spec.md +4 -2
  4. package/templates/qoder/commands/wl-commit.md +3 -1
  5. package/templates/qoder/commands/wl-init.md +2 -2
  6. package/templates/qoder/commands/wl-prd.md +24 -0
  7. package/templates/qoder/commands/wl-search.md +18 -11
  8. package/templates/qoder/commands/wl-task.md +3 -1
  9. package/templates/qoder/config.yaml +0 -6
  10. package/templates/qoder/contracts/insight.md +55 -0
  11. package/templates/qoder/hooks/pre-tool-use-commit.py +124 -0
  12. package/templates/qoder/hooks/session-start.py +20 -1
  13. package/templates/qoder/hooks/stop-eval.py +129 -0
  14. package/templates/qoder/scripts/capability/adapters/mcp.py +9 -1
  15. package/templates/qoder/scripts/capability/registry.py +0 -3
  16. package/templates/qoder/scripts/capability/registry_mcp.py +10 -4
  17. package/templates/qoder/scripts/deployment/setup/init_doctor.py +6 -2
  18. package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +7 -7
  19. package/templates/qoder/scripts/deployment/setup/setup.py +5 -2
  20. package/templates/qoder/scripts/domain/integration/return_to_platform.py +4 -2
  21. package/templates/qoder/scripts/domain/integration/spec_upload.py +4 -4
  22. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +12 -12
  23. package/templates/qoder/scripts/domain/kg/build/build_relations.py +12 -12
  24. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +41 -5
  25. package/templates/qoder/scripts/domain/kg/kg.py +14 -0
  26. package/templates/qoder/scripts/domain/kg/switch_project.py +2 -2
  27. package/templates/qoder/scripts/domain/task/wlkj_panel.py +169 -14
  28. package/templates/qoder/scripts/foundation/io/context_cache.py +94 -0
  29. package/templates/qoder/scripts/orchestration/wlkj.py +106 -0
  30. package/templates/qoder/scripts/tool_guide.md +70 -0
  31. package/templates/qoder/scripts/validation/eval/alignment_matrix.py +176 -0
  32. package/templates/qoder/scripts/validation/eval/bf2_content_fidelity.py +110 -0
  33. package/templates/qoder/scripts/validation/eval/bf2_llmjudge.py +104 -0
  34. package/templates/qoder/scripts/validation/eval/bf_score.py +218 -0
  35. package/templates/qoder/scripts/validation/eval/code_flywheel.py +150 -0
  36. package/templates/qoder/scripts/validation/eval/dispatcher_ab.py +156 -0
  37. package/templates/qoder/scripts/validation/eval/dispatcher_ab_2026-07-21.json +23 -0
  38. package/templates/qoder/scripts/validation/eval/feature_fidelity_flywheel.py +143 -0
  39. package/templates/qoder/scripts/validation/eval/gradient_matrix.py +261 -0
  40. package/templates/qoder/scripts/validation/eval/gradient_matrix_baseline_2026-07-21.json +33 -0
  41. package/templates/qoder/scripts/validation/eval/metrics_dashboard.py +105 -0
  42. package/templates/qoder/scripts/validation/eval/multi_turn_flywheel.py +118 -0
  43. package/templates/qoder/scripts/validation/eval/prd_fidelity_flywheel.py +128 -0
  44. package/templates/qoder/scripts/validation/eval/prd_flywheel.py +148 -0
  45. package/templates/qoder/scripts/validation/eval/prototype_fidelity_flywheel.py +166 -0
  46. package/templates/qoder/scripts/validation/eval/recall_flywheel.py +148 -0
  47. package/templates/qoder/scripts/validation/eval/robustness_flywheel.py +139 -0
  48. package/templates/qoder/scripts/validation/eval/speed_accuracy_flywheel.py +188 -0
  49. package/templates/qoder/scripts/validation/eval/task_flywheel.py +124 -0
  50. package/templates/qoder/scripts/validation/eval/token_flywheel.py +88 -0
  51. package/templates/qoder/scripts/validation/metrics/eval_code_ac.py +177 -0
  52. package/templates/qoder/scripts/validation/metrics/lint_cases.py +170 -0
  53. package/templates/qoder/scripts/validation/test/test_context_cache.py +78 -0
  54. package/templates/qoder/scripts/validation/test/test_lint_cases.py +60 -0
  55. package/templates/qoder/scripts/validation/test/test_pre_tool_use_commit.py +70 -0
  56. package/templates/qoder/settings.json +8 -0
  57. package/templates/qoder/skills/wl-spec/SKILL.md +1 -1
  58. package/templates/qoder/agents/design-agent.md +0 -20
  59. package/templates/qoder/agents/spec-generator.md +0 -21
  60. package/templates/qoder/scripts/capability/smoke_test_report.json.new +0 -94
  61. package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +0 -10
  62. package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +0 -57
  63. package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +0 -62
  64. package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +0 -146
  65. package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +0 -64
  66. package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +0 -52
  67. package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +0 -104
  68. package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +0 -55
  69. package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +0 -95
  70. package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +0 -8
  71. package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +0 -22
  72. package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +0 -55
  73. package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +0 -90
  74. package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +0 -9
  75. package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +0 -206
  76. package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +0 -132
  77. package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +0 -80
  78. package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +0 -59
  79. package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +0 -93
  80. package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +0 -421
  81. package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +0 -9
  82. package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +0 -79
  83. package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +0 -99
  84. package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +0 -69
  85. package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +0 -78
  86. package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +0 -8
  87. package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +0 -105
  88. package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +0 -153
  89. package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +0 -120
  90. package/templates/qoder/scripts/domain/kg/graph/kg_link_db.py +0 -235
  91. package/templates/qoder/scripts/domain/kg/search/enrich_prompt.py +0 -238
  92. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +0 -197
  93. package/templates/qoder/scripts/domain/kg/sync_repowiki.py +0 -109
  94. package/templates/qoder/scripts/engine/poller.py +0 -219
@@ -1,80 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """命名同源匹配: 不依赖 embedding, 用命名规则建弱关联。
3
-
4
- AssetController ↔ asset 表 ↔ 资产管理 PRD → 词干相同 → 弱边。
5
- 这是 embedding 不可用时的主路径, 也是 embedding 的补充 (硬规则置信度高)。
6
- """
7
- from __future__ import annotations
8
- import re
9
-
10
-
11
- def extract_stem(name: str) -> str:
12
- """提取名称的词干 (去掉类型后缀 + 小写 + 驼峰拆词)。
13
-
14
- AssetController → 'asset'
15
- EmployeeOnline → 'employee online' → 'employee'
16
- IQrCodeLoginService → 'qr code login'
17
- test_demo → 'test demo'
18
- 资产管理 → '资产管理'
19
- """
20
- if not name:
21
- return ''
22
- s = name.strip()
23
- # 去掉常见类型后缀
24
- for suffix in ['Controller', 'ServiceImpl', 'Service', 'Mapper',
25
- 'Repository', 'Entity', 'Vo', 'BO', 'Bo', 'Dto', 'DTO',
26
- 'Form', 'Body', 'Request', 'Response', 'Result']:
27
- if s.endswith(suffix) and len(s) > len(suffix):
28
- s = s[:-len(suffix)]
29
- break
30
- # 去掉接口前缀 I
31
- if s.startswith('I') and len(s) > 1 and s[1].isupper():
32
- s = s[1:]
33
- # 驼峰拆词
34
- words = re.findall(r'[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)|[\u4e00-\u9fff]+', s)
35
- if not words:
36
- return s.lower()
37
- # 中文直接返回, 英文取第一个词 (主词)
38
- if any('\u4e00' <= c <= '\u9fff' for c in s):
39
- return ''.join(w for w in words if '\u4e00' <= w[0] <= '\u9fff')[:6]
40
- return words[0].lower() if words else s.lower()
41
-
42
-
43
- def naming_similarity(name_a: str, name_b: str) -> float:
44
- """两个名称的命名同源相似度 → [0, 1]。
45
-
46
- 词干相同 → 0.8
47
- 词干包含关系 → 0.6
48
- 无关 → 0
49
- """
50
- stem_a = extract_stem(name_a)
51
- stem_b = extract_stem(name_b)
52
- if not stem_a or not stem_b:
53
- return 0.0
54
- if stem_a == stem_b:
55
- return 0.8
56
- if stem_a in stem_b or stem_b in stem_a:
57
- return 0.6
58
- # 词重叠 (多词情况)
59
- words_a = set(stem_a.split())
60
- words_b = set(stem_b.split())
61
- if words_a and words_b:
62
- overlap = len(words_a & words_b)
63
- if overlap > 0:
64
- return 0.5 * overlap / max(len(words_a), len(words_b))
65
- return 0.0
66
-
67
-
68
- def match_to_anchors(entity_canonical: str, anchors: list) -> list:
69
- """把实体和 anchors 做命名匹配 → [(anchor_id, score)]。
70
-
71
- anchors: [{'id':..., 'title':..., 'kind':...}]
72
- Returns: 按相似度降序, score >= 0.6 的
73
- """
74
- out = []
75
- for a in anchors:
76
- score = naming_similarity(entity_canonical, a.get('title', '') or a.get('id', ''))
77
- if score >= 0.6:
78
- out.append((a['id'], score))
79
- out.sort(key=lambda x: x[1], reverse=True)
80
- return out
@@ -1,59 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """弱边升格: 出现强关联时, 对应弱边自动删除。
3
-
4
- 避免同一对 (entity, anchor) 既有强边又有弱边, 强边优先。
5
- """
6
- from __future__ import annotations
7
- import os
8
- import sys
9
-
10
- _THIS = os.path.dirname(os.path.abspath(__file__))
11
- for _i in range(8):
12
- _p = os.path.dirname(_THIS)
13
- if os.path.isfile(os.path.join(_p, 'foundation', 'bootstrap.py')):
14
- sys.path.insert(0, _p); break
15
- _THIS = _p
16
-
17
-
18
- def _get_engine():
19
- from domain.kg.extract.java import pg_upsert
20
- return pg_upsert._get_pg_engine()
21
-
22
-
23
- def promote_strong_over_weak(logger=print):
24
- """删除有强关联的实体的弱边。
25
-
26
- 逻辑: entity 如果有 source='explicit'/'commit' 的 implements_req 边,
27
- 则该 entity 的所有 source='inferred' 的 relates_to 边删除 (被强边替代)。
28
-
29
- Returns: {deleted}
30
- """
31
- engine, _ = _get_engine()
32
- if engine is None:
33
- return {'deleted': 0}
34
- from sqlalchemy import text
35
-
36
- deleted = 0
37
- with engine.begin() as conn:
38
- # 找有强关联的实体
39
- strong_entities = conn.execute(text("""
40
- SELECT DISTINCT from_id FROM edges
41
- WHERE source IN ('explicit', 'commit')
42
- AND edge_type IN ('implements_req', 'anchors')
43
- """)).all()
44
- strong_ids = [r[0] for r in strong_entities]
45
- # 也包括 prd: 锚点本身 (它们的实体不该有弱边)
46
- if strong_ids:
47
- # 删除这些实体的弱边
48
- result = conn.execute(text("""
49
- DELETE FROM edges
50
- WHERE source='inferred' AND from_id = ANY(:ids)
51
- """), {'ids': strong_ids})
52
- deleted = result.rowcount or 0
53
-
54
- logger(' [promote] 删除 %d 条冗余弱边 (升格到强关联)' % deleted)
55
- return {'deleted': deleted}
56
-
57
-
58
- if __name__ == '__main__':
59
- print(promote_strong_over_weak())
@@ -1,93 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """C 方案指纹重算: 过期弱边对比指纹 → 续期/重推理/删除。
3
-
4
- 每天 celery 定时跑:
5
- 1. 找 expires_at < now 的弱边
6
- 2. 对比 fingerprint: 没变 → 续期; 变了 → 删除重推理
7
- 3. 升格: 弱边的 from 出现强关联 → 弱边删除 (在 promote.py)
8
- """
9
- from __future__ import annotations
10
- import os
11
- import sys
12
- from datetime import datetime
13
-
14
- _THIS = os.path.dirname(os.path.abspath(__file__))
15
- for _i in range(8):
16
- _p = os.path.dirname(_THIS)
17
- if os.path.isfile(os.path.join(_p, 'foundation', 'bootstrap.py')):
18
- sys.path.insert(0, _p); break
19
- _THIS = _p
20
-
21
-
22
- def _get_engine():
23
- from domain.kg.extract.java import pg_upsert
24
- return pg_upsert._get_pg_engine()
25
-
26
-
27
- def recompute_expired(logger=print):
28
- """重算所有过期弱边 (C 方案主入口)。
29
-
30
- Returns: {expired, renewed, deleted, errors}
31
- """
32
- engine, _ = _get_engine()
33
- if engine is None:
34
- return {'error': 'PG 不可用'}
35
- from sqlalchemy import text
36
-
37
- now = datetime.utcnow().isoformat()
38
- stats = {'expired': 0, 'renewed': 0, 'deleted': 0, 'errors': 0}
39
-
40
- with engine.begin() as conn:
41
- # 找过期弱边
42
- rows = conn.execute(text("""
43
- SELECT from_id, to_id, fingerprint FROM edges
44
- WHERE source='inferred' AND expires_at IS NOT NULL
45
- AND expires_at < CAST(:now AS TIMESTAMPTZ)
46
- LIMIT 200
47
- """), {'now': now}).all()
48
- stats['expired'] = len(rows)
49
- logger(' [recompute] %d 条过期弱边' % len(rows))
50
-
51
- for from_id, to_id, fingerprint in rows:
52
- try:
53
- # 对比当前指纹: 用和建边时相同的 _get_file_fingerprint 函数算
54
- # (保证一致性: file_signatures 真实 hash 优先, 无 file 时 canonical hash 兜底)
55
- ent_row = conn.execute(text("""
56
- SELECT id, repo_id, canonical, cn, props FROM entities WHERE id=:id
57
- """), {'id': from_id}).first()
58
- if not ent_row:
59
- # 实体已删 → 删弱边
60
- conn.execute(text("DELETE FROM edges WHERE from_id=:f AND to_id=:t AND source='inferred'"),
61
- {'f': from_id, 't': to_id})
62
- stats['deleted'] += 1
63
- continue
64
- ent = {'id': ent_row[0], 'repo_id': ent_row[1], 'canonical': ent_row[2],
65
- 'cn': ent_row[3], 'props': ent_row[4] or {}}
66
- # 用 weak_link 的统一 fingerprint 函数 (和建边时一致)
67
- from domain.kg.extract.inference.weak_link import _get_file_fingerprint
68
- cur_fp = _get_file_fingerprint(ent)
69
-
70
- if cur_fp and fingerprint and str(cur_fp)[:16] == str(fingerprint)[:16]:
71
- # ★ 指纹没变 → 续期 30 天
72
- conn.execute(text("""
73
- UPDATE edges SET expires_at = now() + interval '30 days'
74
- WHERE from_id=:f AND to_id=:t AND source='inferred'
75
- """), {'f': from_id, 't': to_id})
76
- stats['renewed'] += 1
77
- else:
78
- # ★ 指纹变了 → 删除旧弱边 (下次全量推理重建)
79
- conn.execute(text("""
80
- DELETE FROM edges WHERE from_id=:f AND to_id=:t AND source='inferred'
81
- """), {'f': from_id, 't': to_id})
82
- stats['deleted'] += 1
83
- except Exception:
84
- stats['errors'] += 1
85
-
86
- logger(' [recompute] 续期%d 删除%d 错%d' % (
87
- stats['renewed'], stats['deleted'], stats['errors']))
88
- return stats
89
-
90
-
91
- if __name__ == '__main__':
92
- r = recompute_expired()
93
- print(r)
@@ -1,421 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """embedding 弱关联推理 (L3 核心)。
3
-
4
- 对没有 REQ-ID 标注的老代码:
5
- 1. 算实体 embedding (DashScope text-embedding-v3)
6
- 2. 向量召回最近 anchor (pgvector HNSW; 不可用降级命名匹配)
7
- 3. 按相似度建弱边:
8
- > 0.85 → confidence=0.9, expires_at=now+30天
9
- 0.6-0.85 → confidence=0.7, expires_at=now+14天
10
- 4. 绑 fingerprint (C方案重算依赖)
11
-
12
- embedding 成本高 (37515 实体), 用 llm_limit 限量 + 增量。
13
- """
14
- from __future__ import annotations
15
- import os
16
- import sys
17
- import time
18
- import json
19
- import urllib.request
20
- from datetime import datetime, timedelta
21
-
22
- _THIS = os.path.dirname(os.path.abspath(__file__))
23
- for _i in range(8):
24
- _p = os.path.dirname(_THIS)
25
- if os.path.isfile(os.path.join(_p, 'foundation', 'bootstrap.py')):
26
- sys.path.insert(0, _p); break
27
- _THIS = _p
28
-
29
- from domain.kg.extract.inference import naming_matcher
30
-
31
-
32
- def embed_one(text: str) -> list:
33
- """调 DashScope text-embedding-v3 算单个向量 (1024维)。失败返回 []。"""
34
- key = os.environ.get('DASHSCOPE_API_KEY', '')
35
- if not key or not text:
36
- return []
37
- payload = json.dumps({
38
- 'model': 'text-embedding-v3',
39
- 'input': [text[:500]],
40
- 'dimensions': 1024,
41
- }).encode('utf-8')
42
- req = urllib.request.Request(
43
- 'https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings',
44
- data=payload,
45
- headers={'Authorization': 'Bearer ' + key, 'Content-Type': 'application/json'},
46
- )
47
- try:
48
- with urllib.request.urlopen(req, timeout=15) as resp:
49
- r = json.loads(resp.read())
50
- return r['data'][0]['embedding']
51
- except Exception:
52
- return []
53
-
54
-
55
- def cosine_sim(a: list, b: list) -> float:
56
- """余弦相似度 (纯 Python, 不依赖 numpy)。"""
57
- if not a or not b or len(a) != len(b):
58
- return 0.0
59
- dot = sum(x * y for x, y in zip(a, b))
60
- na = sum(x * x for x in a) ** 0.5
61
- nb = sum(y * y for y in b) ** 0.5
62
- if na == 0 or nb == 0:
63
- return 0.0
64
- return dot / (na * nb)
65
-
66
-
67
- def infer_one(entity: dict, anchors: list, anchor_vecs: dict = None,
68
- vec_matrix=None, vec_ids=None, preset_vec=None):
69
- """对单个实体推理弱关联 → edges 列表。
70
-
71
- Args:
72
- entity: {id, canonical, cn, type, props}
73
- anchors: [{id, title, kind}] 全部锚点
74
- anchor_vecs: {anchor_id: vec} 锚点向量缓存 (可选, 没有则用命名匹配)
75
- vec_matrix: numpy 矩阵 (N, 1024) 持久化向量 (GOAL A 矩阵召回)
76
- vec_ids: 矩阵对应的 entity_id 列表 (长度 N)
77
- preset_vec: 预计算的实体向量 (GOAL A 批量 embed, 避免逐个调 API)
78
- Returns: [{from_id, to_id, edge_type, confidence, source, expires_at, fingerprint, props}]
79
- """
80
- eid = entity['id']
81
- desc = (entity.get('canonical', '') + ' ' + entity.get('cn', '')).strip() or entity['id']
82
-
83
- # 路1: numpy 矩阵召回 (GOAL A, 最快) — 一次矩阵乘算 top-K
84
- if vec_matrix is not None and vec_ids is not None:
85
- # ★ 优先用预计算向量 (GOAL A 批量 embed), 没有才逐个 embed
86
- ent_vec = preset_vec or embed_one(desc)
87
- if ent_vec:
88
- scored = _numpy_recall(ent_vec, vec_matrix, vec_ids, top_k=5, min_score=0.6)
89
- if scored:
90
- return _build_edges(eid, scored, entity, anchors, via='embedding')
91
-
92
- # 路2: 逐个 cosine_sim 召回 (有 anchor_vecs 时, 旧逻辑)
93
- if anchor_vecs:
94
- ent_vec = preset_vec or embed_one(desc)
95
- if ent_vec:
96
- scored = []
97
- for aid, avec in anchor_vecs.items():
98
- s = cosine_sim(ent_vec, avec)
99
- if s >= 0.6:
100
- scored.append((aid, s))
101
- scored.sort(key=lambda x: x[1], reverse=True)
102
- if scored:
103
- return _build_edges(eid, scored, entity, anchors, via='embedding')
104
- # 路3: 命名匹配降级 (embedding 不可用或没 anchor_vecs 或 embedding 0命中)
105
- matches = naming_matcher.match_to_anchors(entity.get('canonical', ''), anchors)
106
- return _build_edges(eid, matches, entity, anchors, via='naming')
107
-
108
-
109
- def _numpy_recall(query_vec: list, matrix, ids: list, top_k: int = 5,
110
- min_score: float = 0.6) -> list:
111
- """numpy 矩阵召回 top-K (GOAL A 核心, 替代逐个 cosine_sim)。
112
-
113
- query_vec × matrix 一次矩阵乘 → 全量相似度 → top-K。
114
- 比逐个 Python cosine_sim 快 100x。
115
- Returns: [(id, score)] 按 score 降序
116
- """
117
- try:
118
- import numpy as np
119
- q = np.array(query_vec, dtype=np.float32)
120
- # 归一化 (余弦相似度 = 点积/模长)
121
- q_norm = q / (np.linalg.norm(q) + 1e-8)
122
- m = matrix # 已归一化的矩阵 (N, dim)
123
- # 一次矩阵乘: (1, dim) × (dim, N) → (1, N)
124
- scores = m @ q_norm # m 已归一化, q 已归一化 → 点积=余弦
125
- # top-K
126
- top_idx = np.argsort(scores)[::-1][:top_k]
127
- out = []
128
- for idx in top_idx:
129
- s = float(scores[idx])
130
- if s >= min_score:
131
- out.append((ids[idx], round(s, 4)))
132
- return out
133
- except Exception:
134
- return []
135
-
136
-
137
- def _load_vec_matrix(engine, kinds=None) -> tuple:
138
- """从持久化向量加载到 numpy 矩阵 (GOAL A)。
139
-
140
- 优先读 embeddings_ali (rag.py 的 19093 条, 已有), 兜底读 embeddings (GOAL A 新建)。
141
- Returns: (matrix_normalized, id_list) matrix 是 (N, 1024) 已归一化矩阵
142
- """
143
- import numpy as np
144
- from sqlalchemy import text
145
- import json
146
- # 路1: embeddings_ali (rag.py 的向量源, 数据最多)
147
- try:
148
- with engine.connect() as conn:
149
- rows = conn.execute(text("""
150
- SELECT entity_id, vec FROM embeddings_ali
151
- WHERE vec IS NOT NULL AND array_length(vec, 1) > 0
152
- LIMIT 5000
153
- """)).all()
154
- if rows:
155
- ids, vecs = [], []
156
- for eid, v in rows:
157
- if isinstance(v, list) and len(v) > 0:
158
- ids.append(eid)
159
- vecs.append(v)
160
- if vecs:
161
- matrix = np.array(vecs, dtype=np.float32)
162
- norms = np.linalg.norm(matrix, axis=1, keepdims=True) + 1e-8
163
- return matrix / norms, ids
164
- except Exception:
165
- pass
166
- # 路2: embeddings (GOAL A 新建, vec_json JSONB)
167
- try:
168
- with engine.connect() as conn:
169
- rows = conn.execute(text("""
170
- SELECT entity_id, vec_json FROM embeddings
171
- WHERE vec_json IS NOT NULL
172
- """)).all()
173
- except Exception:
174
- return None, None
175
- if not rows:
176
- return None, None
177
- ids, vecs = [], []
178
- for eid, vj in rows:
179
- try:
180
- v = vj if isinstance(vj, list) else json.loads(vj)
181
- if isinstance(v, list) and len(v) > 0:
182
- ids.append(eid)
183
- vecs.append(v)
184
- except Exception:
185
- continue
186
- if not vecs:
187
- return None, None
188
- matrix = np.array(vecs, dtype=np.float32)
189
- norms = np.linalg.norm(matrix, axis=1, keepdims=True) + 1e-8
190
- matrix_norm = matrix / norms
191
- return matrix_norm, ids
192
-
193
-
194
- def _build_edges(entity_id: str, scored: list, entity: dict, anchors: list, via: str = 'naming'):
195
- """按相似度分档建弱边。
196
-
197
- via: 'embedding' 或 'naming' (影响 props 标注)
198
- fingerprint: 从 file_signatures 表读 content_hash (C方案重算依赖真实文件指纹)
199
- """
200
- edges = []
201
- repo_id = entity.get('repo_id', '')
202
- # fingerprint: 优先从 file_signatures 表读真实 content_hash
203
- fp = _get_file_fingerprint(entity)
204
- now = datetime.utcnow()
205
-
206
- for anchor_id, score in scored[:3]: # 最多建3条弱边
207
- if score > 0.85:
208
- conf, days = 0.9, 30
209
- elif score >= 0.6:
210
- conf, days = 0.7, 14
211
- else:
212
- continue
213
- edges.append({
214
- 'from_id': entity_id, 'to_id': anchor_id,
215
- 'edge_type': 'relates_to', 'from_repo': repo_id, 'to_repo': repo_id,
216
- 'confidence': conf, 'source': 'inferred',
217
- 'expires_at': now + timedelta(days=days),
218
- 'fingerprint': fp,
219
- 'props': {'score': round(score, 3), 'via': via},
220
- })
221
- return edges
222
-
223
-
224
- def _get_file_fingerprint(entity: dict) -> str:
225
- """从 file_signatures 表读实体文件的 content_hash。
226
-
227
- entity.props.file 是相对路径 (data/code/fywl-ics\...),
228
- file_signatures.file_path 是绝对路径, 用路径段末尾匹配。
229
- 读不到时降级用 canonical hash (best-effort)。
230
- """
231
- import hashlib
232
- props = entity.get('props') or {}
233
- if isinstance(props, str):
234
- try:
235
- import json
236
- props = json.loads(props)
237
- except Exception:
238
- props = {}
239
- file_path = props.get('file', '') if isinstance(props, dict) else ''
240
- if not file_path:
241
- # 无文件信息 → 用 canonical hash 兜底
242
- return hashlib.md5((entity.get('canonical', '') + entity.get('cn', '')).encode()).hexdigest()[:16]
243
- # 从 file_signatures 查 content_hash (路径末尾段匹配)
244
- try:
245
- from domain.kg.extract.java import pg_upsert
246
- engine, _ = pg_upsert._get_pg_engine()
247
- if engine is None:
248
- return hashlib.md5(file_path.encode()).hexdigest()[:16]
249
- from sqlalchemy import text
250
- # 标准化路径: 取最后两段做 LIKE 匹配 (跨绝对/相对路径)
251
- norm = file_path.replace('\\', '/').split('/')
252
- tail = '/'.join(norm[-3:]) if len(norm) >= 3 else file_path.replace('\\', '/')
253
- with engine.connect() as conn:
254
- row = conn.execute(text("""
255
- SELECT content_hash FROM file_signatures
256
- WHERE file_path ILIKE :tail LIMIT 1
257
- """), {'tail': '%' + tail}).first()
258
- if row and row[0]:
259
- return row[0][:16]
260
- except Exception:
261
- pass
262
- return hashlib.md5(file_path.encode()).hexdigest()[:16]
263
-
264
-
265
- def build_anchor_vectors(anchors: list, logger=print) -> dict:
266
- """批量 embed anchors 算向量缓存 → {anchor_id: vec}。
267
-
268
- 让 weak_link 的向量召回主路径真正跑通 (不依赖 pgvector, 用 Python cosine_sim)。
269
- anchors 多时只 embed 前 N 个 (限量, 避免全量 154 个 anchors 调 154 次 API)。
270
- """
271
- out = {}
272
- if not anchors:
273
- return out
274
- # 只 embed 前 30 个 anchors (够召回, 控成本)
275
- for a in anchors[:30]:
276
- desc = (a.get('title') or a.get('id', '')).strip()
277
- if not desc:
278
- continue
279
- vec = embed_one(desc)
280
- if vec:
281
- out[a['id']] = vec
282
- logger(' [infer] anchor 向量缓存: %d/%d' % (len(out), len(anchors)))
283
- return out
284
-
285
-
286
- def infer_batch(entities: list, anchors: list, use_embedding: bool = True, logger=print):
287
- """批量推理弱关联 (GOAL A 升级: numpy 矩阵召回)。
288
-
289
- Args:
290
- entities: 待推理实体 (无强关联的)
291
- anchors: 锚点列表
292
- use_embedding: True 走向量主路径; False 只命名匹配
293
- Returns: (edges, stats)
294
-
295
- GOAL A 升级: 优先从 embeddings 表加载持久化向量到 numpy 矩阵,
296
- 每个实体只 embed 一次自己, 用矩阵乘召回 top-K (不再每个 anchor 都 embed)。
297
- """
298
- t0 = time.time()
299
- all_edges = []
300
- matched = 0
301
-
302
- # ★ GOAL A: 优先用持久化向量矩阵 (最快)
303
- vec_matrix, vec_ids = None, None
304
- if use_embedding:
305
- try:
306
- from domain.kg.extract.java import pg_upsert
307
- engine, _ = pg_upsert._get_pg_engine()
308
- if engine:
309
- vec_matrix, vec_ids = _load_vec_matrix(engine)
310
- if vec_matrix is not None:
311
- logger(' [infer] 加载持久化向量矩阵: %d × %d' % vec_matrix.shape)
312
- except Exception as e:
313
- logger(' [infer] 矩阵加载失败, 降级: %s' % str(e)[:50])
314
-
315
- # 矩阵不可用 → 降级到 build_anchor_vectors (旧路径)
316
- anchor_vecs = None
317
- if vec_matrix is None and use_embedding:
318
- anchor_vecs = build_anchor_vectors(anchors, logger=logger)
319
-
320
- # ★ GOAL A: 优先复用持久化向量, 缺的才批量 embed
321
- entity_vecs = {}
322
- if vec_matrix is not None and use_embedding:
323
- from domain.kg.extract.inference.embed_builder import embed_batch as _embed_batch
324
- from domain.kg.extract.java import pg_upsert as _pg
325
- _engine, _ = _pg._get_pg_engine()
326
- # ① 先从 embeddings_ali 批量读已有向量 (免 API 调用, 分chunk避免参数过多)
327
- all_ids = [e['id'] for e in entities]
328
- if _engine and all_ids:
329
- try:
330
- # 分 chunk 查 (每 chunk 100 个 id, 避免 PG 参数上限)
331
- import json as _json
332
- for ci in range(0, len(all_ids), 100):
333
- chunk_ids = all_ids[ci:ci+100]
334
- # 用 ANY(ARRAY[...]) 避免构造大量 :params
335
- arr = "ARRAY[" + ','.join("'" + eid.replace("'", "''") + "'" for eid in chunk_ids) + "]"
336
- from sqlalchemy import text as _text_fn
337
- with _engine.connect() as conn:
338
- rows = conn.execute(_text_fn(
339
- f"SELECT entity_id, vec FROM embeddings_ali WHERE entity_id = ANY({arr}) AND vec IS NOT NULL"
340
- )).all()
341
- for eid, v in rows:
342
- if isinstance(v, list) and len(v) > 0:
343
- entity_vecs[eid] = v
344
- logger(' [infer] 复用 embeddings_ali 已有向量: %d/%d' % (len(entity_vecs), len(all_ids)))
345
- except Exception as _e:
346
- logger(' [infer] 复用查询异常: %s' % str(_e)[:50])
347
- # ② 缺的才并发批量 embed (4线程并行, 破10秒关键)
348
- missing = [e for e in entities if e['id'] not in entity_vecs]
349
- if missing:
350
- from domain.kg.extract.inference.embed_builder import embed_batch as _embed2
351
- from concurrent.futures import ThreadPoolExecutor
352
- descs = [((e.get('canonical', '') + ' ' + e.get('cn', '')).strip() or e['id'])[:200] for e in missing]
353
- logger(' [infer] 并发 embed %d 缺失实体 (4线程)...' % len(missing))
354
- try:
355
- # 分 4 块并发, 每块调 embed_batch (内含 10条/批)
356
- chunk = max(50, len(descs) // 4)
357
- chunks = [descs[i:i+chunk] for i in range(0, len(descs), chunk)]
358
- with ThreadPoolExecutor(max_workers=4) as ex:
359
- results = list(ex.map(_embed2, chunks))
360
- vecs = [v for cv in results for v in cv]
361
- for ent, v in zip(missing, vecs):
362
- if v:
363
- entity_vecs[ent['id']] = v
364
- logger(' [infer] 实体向量就绪: %d/%d' % (len(entity_vecs), len(entities)))
365
- except Exception as e:
366
- logger(' [infer] 并发 embed 失败: %s' % str(e)[:50])
367
-
368
- for i, ent in enumerate(entities):
369
- try:
370
- eds = infer_one(ent, anchors, anchor_vecs=anchor_vecs,
371
- vec_matrix=vec_matrix, vec_ids=vec_ids,
372
- preset_vec=entity_vecs.get(ent['id']))
373
- all_edges += eds
374
- if eds:
375
- matched += 1
376
- except Exception:
377
- pass
378
- if (i + 1) % 100 == 0:
379
- logger(' [infer] %d/%d (匹配%d, 边%d)' % (i + 1, len(entities), matched, len(all_edges)))
380
- dt = time.time() - t0
381
- logger(' [infer] 完成: %d 实体 → %d 匹配 → %d 弱边 / %.1fs' % (
382
- len(entities), matched, len(all_edges), dt))
383
-
384
- # ★ GOAL A: 把新 embed 的实体向量持久化 (下次跑免 API 调用)
385
- if entity_vecs and vec_matrix is not None:
386
- try:
387
- _persist_entity_vecs(entity_vecs, entities)
388
- except Exception:
389
- pass
390
-
391
- return all_edges, {'entities': len(entities), 'matched': matched,
392
- 'edges': len(all_edges), 'seconds': round(dt, 1)}
393
-
394
-
395
- def _persist_entity_vecs(vecs: dict, entities: list, logger=print):
396
- """把推理时计算的实体向量存回 embeddings_ali (下次复用, 免 API)。"""
397
- from domain.kg.extract.java import pg_upsert
398
- from sqlalchemy import text
399
- import json
400
- engine, _ = pg_upsert._get_pg_engine()
401
- if engine is None:
402
- return
403
- ent_map = {e['id']: e for e in entities}
404
- ok = 0
405
- with engine.begin() as conn:
406
- for eid, vec in vecs.items():
407
- ent = ent_map.get(eid, {})
408
- try:
409
- # embeddings_ali 表 vec 是 ARRAY 类型
410
- arr_str = '{' + ','.join(str(float(v)) for v in vec[:1024]) + '}'
411
- conn.execute(text("""
412
- INSERT INTO embeddings_ali (entity_id, kind, text, vec, model, embedded_at)
413
- VALUES (:id, :kind, :txt, CAST(:vec AS FLOAT[]), 'text-embedding-v3', now())
414
- ON CONFLICT (entity_id) DO NOTHING
415
- """), {'id': eid, 'kind': ent.get('type', 'entity'),
416
- 'txt': (ent.get('canonical', '') or eid)[:200], 'vec': arr_str})
417
- ok += 1
418
- except Exception:
419
- pass
420
- if ok:
421
- logger(' [infer] 持久化 %d 条实体向量 (下次复用)' % ok)
@@ -1,9 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """MyBatis mapper XML 提取器 (GOAL 3)。
3
-
4
- 子模块:
5
- mapper_parser XML namespace + resultMap + 方法→SQL
6
- sql_extractor SQL 文本 → 表名 + 列引用
7
- relation_builder Mapper ↔ Entity ↔ Table 三向关联
8
- all 协调器: 扫所有 mapper XML → 提取 → 返回 (entities, edges)
9
- """