@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
|
@@ -0,0 +1,421 @@
|
|
|
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)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Java 后端全栈提取器 (GOAL 2)。
|
|
3
|
+
|
|
4
|
+
子模块:
|
|
5
|
+
_parser tree-sitter Java 引擎 + AST 通用遍历
|
|
6
|
+
spring_extractor Spring 注解 → SERVICE/CONTROLLER/CONFIG + injects
|
|
7
|
+
dubbo_extractor Dubbo 注解 → RPC injects/exposes 边
|
|
8
|
+
satoken_extractor Sa-Token → PERMISSION 实体 + guards 边
|
|
9
|
+
mybatisplus_extractor @TableName/@TableField → ENTITY/TABLE/COLUMN + maps_to
|
|
10
|
+
validation_extractor @NotNull/@Size → VALIDATION 实体 + validates 边
|
|
11
|
+
call_chain 方法体内调用 → calls 边 (Service→Mapper 关键链)
|
|
12
|
+
endpoint_extractor @GetMapping → ENDPOINT + handles 边
|
|
13
|
+
domain_extractor Entity/BO/VO 字段定义 → DTO 实体 + 字段语义
|
|
14
|
+
all 协调器: 单文件一次解析 → 多 extractor → 小批量 PG upsert
|
|
15
|
+
"""
|