@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,169 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
|
|
3
|
+
import os as _o, sys as _s
|
|
4
|
+
_f = _o.path.abspath(__file__)
|
|
5
|
+
for _ in range(10):
|
|
6
|
+
_f = _o.path.dirname(_f)
|
|
7
|
+
_cp = _o.path.join(_f, 'foundation')
|
|
8
|
+
if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
|
|
9
|
+
break
|
|
10
|
+
if _cp not in _s.path: _s.path.insert(0, _cp)
|
|
11
|
+
from bootstrap import setup; setup()
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
File-signature based incremental change detection (P0: fingerprint 三级增量).
|
|
15
|
+
|
|
16
|
+
Goal: skip re-extraction for files whose changes don't affect the graph.
|
|
17
|
+
The knowledge graph only records *topology* (who clicks/calls/handles whom),
|
|
18
|
+
not implementations — so a file that only changed comments/whitespace has
|
|
19
|
+
zero impact on the graph and can be safely skipped.
|
|
20
|
+
|
|
21
|
+
Three levels (mirrors Understand-Anything's fingerprint approach, adapted to
|
|
22
|
+
our regex/topology-only graph):
|
|
23
|
+
- NONE : content_hash unchanged → file is byte-identical, skip entirely.
|
|
24
|
+
- COSMETIC : structural_hash unchanged but content_hash changed
|
|
25
|
+
→ only comments/whitespace differ, skip re-extraction.
|
|
26
|
+
- STRUCTURAL : structural_hash changed → real code changed, re-extract.
|
|
27
|
+
|
|
28
|
+
The signature is intentionally coarse (strip comments + all whitespace, hash
|
|
29
|
+
the rest) — language-agnostic and robust. Worst case (e.g. pure variable
|
|
30
|
+
rename) degrades to STRUCTURAL = re-extract = current behaviour, never wrong.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import hashlib
|
|
34
|
+
import os
|
|
35
|
+
import re
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ── Comment stripping (language-aware, single regex each) ────────────────────
|
|
39
|
+
# Block comments /* ... */ and HTML comments <!-- ... --> span both JS-family
|
|
40
|
+
# (.ts/.vue/.tsx/.jsx) and SQL (-- / /* */). Line comments vary by language.
|
|
41
|
+
|
|
42
|
+
# /* block */ and <!-- block --> — shared by JS-family + SQL + Vue template
|
|
43
|
+
_RE_BLOCK = re.compile(r'/\*.*?\*/', re.DOTALL)
|
|
44
|
+
_RE_HTML_BLOCK = re.compile(r'<!--.*?-->', re.DOTALL)
|
|
45
|
+
|
|
46
|
+
# // line comment — TS/JS/Vue script + Java. Robust against URL-ish tokens
|
|
47
|
+
# (only strips when // is at start of a token, not inside a string — see below).
|
|
48
|
+
_RE_LINE_SLASH = re.compile(r'(^|[\s;{}()\[\],])//[^\n]*')
|
|
49
|
+
|
|
50
|
+
# # line comment — Python / shell / YAML / .properties
|
|
51
|
+
_RE_LINE_HASH = re.compile(r'(^|[\s;{}()\[\],])#[^\n]*')
|
|
52
|
+
|
|
53
|
+
# -- line comment — SQL / XML DTD
|
|
54
|
+
_RE_LINE_DASH = re.compile(r'(^|[\s;{}()\[\],])--[^\n]*')
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def strip_comments(content, suffix):
|
|
58
|
+
"""Remove comments from source text by file suffix.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
content: source text (str)
|
|
62
|
+
suffix: file suffix lowercased with dot, e.g. '.vue', '.ts', '.java'
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Text with comments removed. Block comments always stripped for
|
|
66
|
+
JS-family/SQL; line-comment style picked by language.
|
|
67
|
+
"""
|
|
68
|
+
s = content
|
|
69
|
+
# Block comments: JS-family + SQL share /* */; Vue/HTML share <!-- -->.
|
|
70
|
+
if suffix in ('.ts', '.tsx', '.js', '.jsx', '.vue', '.java', '.sql'):
|
|
71
|
+
s = _RE_BLOCK.sub(' ', s)
|
|
72
|
+
if suffix in ('.vue', '.html', '.xml'):
|
|
73
|
+
s = _RE_HTML_BLOCK.sub(' ', s)
|
|
74
|
+
# Line comments by language family.
|
|
75
|
+
if suffix in ('.ts', '.tsx', '.js', '.jsx', '.vue', '.java'):
|
|
76
|
+
s = _RE_LINE_SLASH.sub(r'\1', s)
|
|
77
|
+
elif suffix in ('.py', '.sh', '.yml', '.yaml', '.properties', '.cfg', '.ini'):
|
|
78
|
+
s = _RE_LINE_HASH.sub(r'\1', s)
|
|
79
|
+
elif suffix == '.sql':
|
|
80
|
+
s = _RE_LINE_DASH.sub(r'\1', s)
|
|
81
|
+
return s
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _strip_all_whitespace(s):
|
|
85
|
+
"""Remove every whitespace char so formatting/indentation never affects hash."""
|
|
86
|
+
return re.sub(r'\s+', '', s)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def compute_hashes(filepath):
|
|
90
|
+
"""Compute (content_hash, structural_hash) for a file.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
filepath: absolute or relative path to the source file.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
(content_hash, structural_hash) — both hex sha256, or (None, None) if
|
|
97
|
+
the file can't be read.
|
|
98
|
+
"""
|
|
99
|
+
try:
|
|
100
|
+
with open(filepath, 'rb') as f:
|
|
101
|
+
raw = f.read()
|
|
102
|
+
except Exception:
|
|
103
|
+
return (None, None)
|
|
104
|
+
content_hash = hashlib.sha256(raw).hexdigest()
|
|
105
|
+
|
|
106
|
+
suffix = os.path.splitext(filepath)[1].lower()
|
|
107
|
+
try:
|
|
108
|
+
text = raw.decode('utf-8', errors='replace')
|
|
109
|
+
except Exception:
|
|
110
|
+
text = ''
|
|
111
|
+
stripped = strip_comments(text, suffix)
|
|
112
|
+
structural = _strip_all_whitespace(stripped)
|
|
113
|
+
structural_hash = hashlib.sha256(structural.encode('utf-8')).hexdigest()
|
|
114
|
+
return (content_hash, structural_hash)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def classify(old_sig, new_sig):
|
|
118
|
+
"""Compare old vs new (content_hash, structural_hash) → change level.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
old_sig: dict {content_hash, structural_hash}, or tuple of the two,
|
|
122
|
+
or None (never seen → STRUCTURAL).
|
|
123
|
+
new_sig: (content_hash, structural_hash) tuple from compute_hashes.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
'NONE' | 'COSMETIC' | 'STRUCTURAL'.
|
|
127
|
+
Never-seen files (old_sig is None) → 'STRUCTURAL' (safe default: extract).
|
|
128
|
+
"""
|
|
129
|
+
if old_sig is None:
|
|
130
|
+
return 'STRUCTURAL'
|
|
131
|
+
# Accept both dict (from get_file_signature) and tuple (from compute_hashes)
|
|
132
|
+
# so callers don't have to convert.
|
|
133
|
+
if isinstance(old_sig, dict):
|
|
134
|
+
old_content = old_sig.get('content_hash')
|
|
135
|
+
old_struct = old_sig.get('structural_hash')
|
|
136
|
+
else:
|
|
137
|
+
old_content, old_struct = old_sig[0], old_sig[1]
|
|
138
|
+
new_content, new_struct = new_sig
|
|
139
|
+
if old_content == new_content:
|
|
140
|
+
return 'NONE'
|
|
141
|
+
if old_struct == new_struct:
|
|
142
|
+
return 'COSMETIC'
|
|
143
|
+
return 'STRUCTURAL'
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ── Suffixes the incremental builder cares about ─────────────────────────────
|
|
147
|
+
# Keep in sync with kg_incremental.py's filter: ('.vue', '.ts', '.java').
|
|
148
|
+
TRACKED_SUFFIXES = ('.vue', '.ts', '.java')
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == '__main__':
|
|
152
|
+
# Quick self-test: same code + extra comment → COSMETIC; changed code → STRUCTURAL.
|
|
153
|
+
import tempfile
|
|
154
|
+
tmp = tempfile.NamedTemporaryFile(suffix='.ts', delete=False, mode='w', encoding='utf-8')
|
|
155
|
+
tmp.write('export function add(a, b) {\n return a + b\n}\n')
|
|
156
|
+
tmp.close()
|
|
157
|
+
h1 = compute_hashes(tmp.name)
|
|
158
|
+
print('original:', h1)
|
|
159
|
+
|
|
160
|
+
with open(tmp.name, 'w', encoding='utf-8') as f:
|
|
161
|
+
f.write('// leading comment\nexport function add(a, b) {\n return a + b\n}\n')
|
|
162
|
+
h2 = compute_hashes(tmp.name)
|
|
163
|
+
print('comment-only change:', h2, '→', classify(h1, h2))
|
|
164
|
+
|
|
165
|
+
with open(tmp.name, 'w', encoding='utf-8') as f:
|
|
166
|
+
f.write('export function add(a, b, c) {\n return a + b + c\n}\n')
|
|
167
|
+
h3 = compute_hashes(tmp.name)
|
|
168
|
+
print('structural change:', h3, '→', classify(h1, h3))
|
|
169
|
+
os.unlink(tmp.name)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""资产中心 25 表吸收 (GOAL 4)。
|
|
3
|
+
|
|
4
|
+
子模块:
|
|
5
|
+
prd_importer prds + prd_versions → 业务知识 (调 LLM)
|
|
6
|
+
prototype_importer prototypes + annotations → 意图知识
|
|
7
|
+
discussion_importer discussions → 决策知识 (LLM)
|
|
8
|
+
asset_tree assets → 模块结构
|
|
9
|
+
returns_importer asset_returns → 实践知识
|
|
10
|
+
"""
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""资产树 → 模块结构知识。
|
|
3
|
+
|
|
4
|
+
assets 表 (飞书式文件树, 330节点) → MODULE 实体。
|
|
5
|
+
"01_PRD/02_设计/03_交付" 这种结构本身就是项目知识。
|
|
6
|
+
parent_id 递归 → 模块层级。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import os, sys
|
|
10
|
+
|
|
11
|
+
_THIS = os.path.dirname(os.path.abspath(__file__))
|
|
12
|
+
for _i in range(8):
|
|
13
|
+
_p = os.path.dirname(_THIS)
|
|
14
|
+
if os.path.isfile(os.path.join(_p, 'foundation', 'bootstrap.py')):
|
|
15
|
+
sys.path.insert(0, _p); break
|
|
16
|
+
_THIS = _p
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def import_asset_tree(project_id: str = None, logger=print):
|
|
20
|
+
"""assets 表 → MODULE 实体 + 层级边。"""
|
|
21
|
+
try:
|
|
22
|
+
from dotenv import load_dotenv
|
|
23
|
+
for p in ['wlinkj-workspace/backend/.env', '.env']:
|
|
24
|
+
if os.path.isfile(p): load_dotenv(p); break
|
|
25
|
+
except: pass
|
|
26
|
+
sys.path.insert(0, os.path.join(os.getcwd(), 'wlinkj-workspace', 'backend'))
|
|
27
|
+
from app.db import SessionLocal
|
|
28
|
+
from app.models import Asset
|
|
29
|
+
from domain.kg.extract.java import pg_upsert
|
|
30
|
+
|
|
31
|
+
db = SessionLocal()
|
|
32
|
+
q = db.query(Asset)
|
|
33
|
+
if project_id: q = q.filter(Asset.project_id == project_id)
|
|
34
|
+
assets = q.all()
|
|
35
|
+
logger(' [asset-tree] %d 个资产节点' % len(assets))
|
|
36
|
+
|
|
37
|
+
entities, edges = [], []
|
|
38
|
+
for a in assets:
|
|
39
|
+
repo_id = 'asset-%s' % (a.project_id or 'global')[:8]
|
|
40
|
+
entities.append({
|
|
41
|
+
'id': 'asset:%s' % a.id, 'repo_id': repo_id,
|
|
42
|
+
'type': 'MODULE' if a.is_folder else 'ASSET',
|
|
43
|
+
'canonical': a.name, 'cn': a.type or '',
|
|
44
|
+
'props': {'project_id': a.project_id, 'kind': 'folder' if a.is_folder else 'file',
|
|
45
|
+
'mime': a.mime_type, 'source': 'asset_center'},
|
|
46
|
+
})
|
|
47
|
+
# parent_id → 层级边
|
|
48
|
+
if a.parent_id:
|
|
49
|
+
edges.append({
|
|
50
|
+
'from_id': 'asset:%s' % a.id, 'to_id': 'asset:%s' % a.parent_id,
|
|
51
|
+
'edge_type': 'child_of', 'from_repo': repo_id, 'to_repo': repo_id,
|
|
52
|
+
'confidence': 1.0, 'source': 'explicit',
|
|
53
|
+
})
|
|
54
|
+
db.close()
|
|
55
|
+
|
|
56
|
+
upsert_r = pg_upsert.upsert_batch(entities, edges) if (entities or edges) else {'ok': False}
|
|
57
|
+
return {'assets': len(assets), 'entities': len(entities), 'edges': len(edges), 'upsert': upsert_r}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""讨论 → 决策知识。
|
|
3
|
+
|
|
4
|
+
discussion_messages → DECISION 实体 (LLM 抽取决策点)。
|
|
5
|
+
当前 0 数据, 管线先建好, 等团队用起来立刻生效。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import os, 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 import_discussions(project_id: str = None, logger=print):
|
|
19
|
+
"""讨论 → KG。"""
|
|
20
|
+
try:
|
|
21
|
+
from dotenv import load_dotenv
|
|
22
|
+
for p in ['wlinkj-workspace/backend/.env', '.env']:
|
|
23
|
+
if os.path.isfile(p): load_dotenv(p); break
|
|
24
|
+
except: pass
|
|
25
|
+
sys.path.insert(0, os.path.join(os.getcwd(), 'wlinkj-workspace', 'backend'))
|
|
26
|
+
from app.db import SessionLocal
|
|
27
|
+
from app.models import Discussion, DiscussionMessage
|
|
28
|
+
from domain.kg.extract.java import pg_upsert
|
|
29
|
+
|
|
30
|
+
db = SessionLocal()
|
|
31
|
+
q = db.query(Discussion)
|
|
32
|
+
if project_id: q = q.filter(Discussion.project_id == project_id)
|
|
33
|
+
discussions = q.all()
|
|
34
|
+
logger(' [asset-disc] %d 个讨论' % len(discussions))
|
|
35
|
+
|
|
36
|
+
entities, edges = [], []
|
|
37
|
+
for d in discussions:
|
|
38
|
+
repo_id = 'asset-%s' % (d.project_id or 'global')[:8]
|
|
39
|
+
# 讨论 → DECISION 实体
|
|
40
|
+
entities.append({
|
|
41
|
+
'id': 'disc:%s' % d.id, 'repo_id': repo_id, 'type': 'DECISION',
|
|
42
|
+
'canonical': d.title[:80], 'cn': '',
|
|
43
|
+
'props': {'kind': 'discussion', 'project_id': d.project_id, 'source': 'asset_center'},
|
|
44
|
+
})
|
|
45
|
+
# 消息 → 关联到讨论
|
|
46
|
+
msgs = db.query(DiscussionMessage).filter(DiscussionMessage.discussion_id == d.id).all()
|
|
47
|
+
for m in msgs:
|
|
48
|
+
if not m.content: continue
|
|
49
|
+
entities.append({
|
|
50
|
+
'id': 'discmsg:%s' % m.id, 'repo_id': repo_id, 'type': 'DECISION',
|
|
51
|
+
'canonical': m.content[:60], 'cn': m.author_name or '',
|
|
52
|
+
'props': {'discussion_id': d.id, 'author': m.author_name, 'source': 'discussion_msg'},
|
|
53
|
+
})
|
|
54
|
+
edges.append({
|
|
55
|
+
'from_id': 'discmsg:%s' % m.id, 'to_id': 'disc:%s' % d.id,
|
|
56
|
+
'edge_type': 'decided_in', 'from_repo': repo_id, 'to_repo': repo_id,
|
|
57
|
+
'confidence': 1.0, 'source': 'explicit',
|
|
58
|
+
})
|
|
59
|
+
db.close()
|
|
60
|
+
|
|
61
|
+
upsert_r = pg_upsert.upsert_batch(entities, edges) if (entities or edges) else {'ok': False}
|
|
62
|
+
return {'discussions': len(discussions), 'entities': len(entities), 'edges': len(edges), 'upsert': upsert_r}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""资产中心 prds + prd_versions → KG 业务知识吸收。
|
|
3
|
+
|
|
4
|
+
把工作台 SQLite/PG 的 prds 表灌进 KG (当前0吸收)。
|
|
5
|
+
对每个有正文的 PRD:
|
|
6
|
+
1. 调 prd_llm_extract 抽功能点/字段/规则/端点/权限
|
|
7
|
+
2. 调 prd_chunk_embed 做向量 (LightRAG vector 路)
|
|
8
|
+
3. 调 req_anchor 锚到 REQ-ID
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
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 import_all_prds(project_id: str = None, do_embed: bool = False, llm_limit: int = 10, logger=print):
|
|
23
|
+
"""扫资产中心所有 PRD → LLM 抽取 + 锚定。
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
project_id: 限定某项目 (None=全部)
|
|
27
|
+
do_embed: 是否做 embedding (慢, 默认False)
|
|
28
|
+
llm_limit: LLM 抽取上限 (避免154个PRD全调qwen超时; 0=不调LLM只锚定)
|
|
29
|
+
Returns: {stats}
|
|
30
|
+
"""
|
|
31
|
+
from domain.kg.extract.prd import prd_llm_extract, prd_chunk_embed, req_anchor
|
|
32
|
+
|
|
33
|
+
# 连工作台 DB 读 PRD
|
|
34
|
+
try:
|
|
35
|
+
from dotenv import load_dotenv
|
|
36
|
+
for p in ['wlinkj-workspace/backend/.env', '.env']:
|
|
37
|
+
if os.path.isfile(p):
|
|
38
|
+
load_dotenv(p); break
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
sys.path.insert(0, os.path.join(os.getcwd(), 'wlinkj-workspace', 'backend'))
|
|
42
|
+
from app.db import SessionLocal
|
|
43
|
+
from app.models import PRD, PRDVersion
|
|
44
|
+
from sqlalchemy import text
|
|
45
|
+
|
|
46
|
+
db = SessionLocal()
|
|
47
|
+
q = db.query(PRD)
|
|
48
|
+
if project_id:
|
|
49
|
+
q = q.filter(PRD.project_id == project_id)
|
|
50
|
+
prds = q.all()
|
|
51
|
+
logger(' [asset-prd] %d 个 PRD 待吸收' % len(prds))
|
|
52
|
+
|
|
53
|
+
all_entities, all_edges = [], []
|
|
54
|
+
embedded = 0
|
|
55
|
+
llm_done = 0
|
|
56
|
+
for prd in prds:
|
|
57
|
+
# 取最新版本正文
|
|
58
|
+
ver = db.query(PRDVersion).filter(PRDVersion.prd_id == prd.id).order_by(PRDVersion.version.desc()).first()
|
|
59
|
+
content_md = ver.content_md if ver else ''
|
|
60
|
+
repo_id = 'asset-%s' % (prd.project_id or 'global')[:8]
|
|
61
|
+
|
|
62
|
+
# ① PRD 实体本身 (写 entities, group=prd)
|
|
63
|
+
all_entities.append({
|
|
64
|
+
'id': 'prd:%s' % prd.id, 'repo_id': repo_id, 'type': 'PRD',
|
|
65
|
+
'canonical': prd.title[:80], 'cn': prd.category or '',
|
|
66
|
+
'props': {'project_id': prd.project_id, 'status': prd.status,
|
|
67
|
+
'version': prd.current_version, 'source': 'asset_center'},
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
# ② REQ-ID 锚定 (PRD 标题 → anchors)
|
|
71
|
+
a_ent, a_edg = req_anchor.anchor_prd(prd.id, prd.title, prd.project_id, repo_id)
|
|
72
|
+
all_entities += a_ent
|
|
73
|
+
all_edges += a_edg
|
|
74
|
+
|
|
75
|
+
# ③ 有正文且未超 LLM 上限 → LLM 抽取
|
|
76
|
+
if content_md and len(content_md) > 100 and llm_done < llm_limit:
|
|
77
|
+
extracted = prd_llm_extract.extract_prd_knowledge(content_md)
|
|
78
|
+
e_ent, e_edg = prd_llm_extract.to_entities_edges(
|
|
79
|
+
extracted, prd.id, prd.project_id, repo_id)
|
|
80
|
+
all_entities += e_ent
|
|
81
|
+
all_edges += e_edg
|
|
82
|
+
llm_done += 1
|
|
83
|
+
|
|
84
|
+
# ④ embedding (可选, 慢)
|
|
85
|
+
if do_embed and len(content_md) > 500:
|
|
86
|
+
try:
|
|
87
|
+
r = prd_chunk_embed.embed_prd(prd.id, content_md, repo_id)
|
|
88
|
+
embedded += r.get('embedded', 0)
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
db.close()
|
|
93
|
+
|
|
94
|
+
# 写 PG
|
|
95
|
+
from domain.kg.extract.java import pg_upsert
|
|
96
|
+
upsert_r = pg_upsert.upsert_batch(all_entities, all_edges) if (all_entities or all_edges) else {'ok': False}
|
|
97
|
+
|
|
98
|
+
# ★ 同时把锚点写进 anchors 专用表 (GOAL 1 建的)
|
|
99
|
+
anchor_records = []
|
|
100
|
+
seen_anchor = set()
|
|
101
|
+
for e in all_entities:
|
|
102
|
+
if e.get('type') == 'ANCHOR':
|
|
103
|
+
aid = e['id']
|
|
104
|
+
if aid in seen_anchor:
|
|
105
|
+
continue
|
|
106
|
+
seen_anchor.add(aid)
|
|
107
|
+
anchor_records.append({
|
|
108
|
+
'id': aid,
|
|
109
|
+
'project_id': e['props'].get('project_id'),
|
|
110
|
+
'title': e.get('cn', ''),
|
|
111
|
+
'kind': e['props'].get('kind', 'feature'),
|
|
112
|
+
'prd_id': e['props'].get('prd_id'),
|
|
113
|
+
})
|
|
114
|
+
anchor_ok = _upsert_anchors(anchor_records) if anchor_records else 0
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
'prds_total': len(prds),
|
|
118
|
+
'entities': len(all_entities),
|
|
119
|
+
'edges': len(all_edges),
|
|
120
|
+
'embedded': embedded,
|
|
121
|
+
'anchors': anchor_ok,
|
|
122
|
+
'upsert': upsert_r,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _upsert_anchors(records):
|
|
127
|
+
"""写 anchors 专用表。"""
|
|
128
|
+
from domain.kg.extract.java import pg_upsert as _pg
|
|
129
|
+
engine, _ = _pg._get_pg_engine()
|
|
130
|
+
if engine is None or not records:
|
|
131
|
+
return 0
|
|
132
|
+
from sqlalchemy import text
|
|
133
|
+
ok = 0
|
|
134
|
+
with engine.begin() as conn:
|
|
135
|
+
for r in records:
|
|
136
|
+
try:
|
|
137
|
+
conn.execute(text("""
|
|
138
|
+
INSERT INTO anchors (id, project_id, title, kind, prd_id)
|
|
139
|
+
VALUES (:id, :pid, :title, :kind, :prd)
|
|
140
|
+
ON CONFLICT (id) DO UPDATE SET title=EXCLUDED.title, updated_at=now()
|
|
141
|
+
"""), {'id': r['id'], 'pid': r.get('project_id'), 'title': r.get('title', ''),
|
|
142
|
+
'kind': r.get('kind', 'feature'), 'prd': r.get('prd_id')})
|
|
143
|
+
ok += 1
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
return ok
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""原型 + 标注 → 意图知识。
|
|
3
|
+
|
|
4
|
+
把 prototypes 表灌进 KG (当前10个原型, 0标注 - 管线先建好)。
|
|
5
|
+
prototype → PAGE 实体; annotation → CONSTRAINT 实体。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import os, 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 import_prototypes(project_id: str = None, logger=print):
|
|
19
|
+
"""原型 + 标注 → KG。"""
|
|
20
|
+
try:
|
|
21
|
+
from dotenv import load_dotenv
|
|
22
|
+
for p in ['wlinkj-workspace/backend/.env', '.env']:
|
|
23
|
+
if os.path.isfile(p): load_dotenv(p); break
|
|
24
|
+
except: pass
|
|
25
|
+
sys.path.insert(0, os.path.join(os.getcwd(), 'wlinkj-workspace', 'backend'))
|
|
26
|
+
from app.db import SessionLocal
|
|
27
|
+
from app.models import Prototype, PrototypeAnnotation
|
|
28
|
+
from domain.kg.extract.java import pg_upsert
|
|
29
|
+
|
|
30
|
+
db = SessionLocal()
|
|
31
|
+
q = db.query(Prototype)
|
|
32
|
+
if project_id: q = q.filter(Prototype.project_id == project_id)
|
|
33
|
+
protos = q.all()
|
|
34
|
+
logger(' [asset-proto] %d 个原型' % len(protos))
|
|
35
|
+
|
|
36
|
+
entities, edges = [], []
|
|
37
|
+
for p in protos:
|
|
38
|
+
repo_id = 'asset-%s' % (p.project_id or 'global')[:8]
|
|
39
|
+
# 原型 → PAGE 实体
|
|
40
|
+
entities.append({
|
|
41
|
+
'id': 'proto:%s' % p.id, 'repo_id': repo_id, 'type': 'PAGE',
|
|
42
|
+
'canonical': p.feature or p.id[:8], 'cn': p.platform or '',
|
|
43
|
+
'props': {'kind': 'prototype', 'project_id': p.project_id,
|
|
44
|
+
'status': p.status, 'source': 'asset_center'},
|
|
45
|
+
})
|
|
46
|
+
# 该原型的标注 → CONSTRAINT
|
|
47
|
+
anns = db.query(PrototypeAnnotation).filter(PrototypeAnnotation.prototype_id == p.id).all()
|
|
48
|
+
for a in anns:
|
|
49
|
+
if not a.comment: continue
|
|
50
|
+
entities.append({
|
|
51
|
+
'id': 'ANNO:%s' % a.id, 'repo_id': repo_id, 'type': 'CONSTRAINT',
|
|
52
|
+
'canonical': a.comment[:60], 'cn': a.type or '',
|
|
53
|
+
'props': {'prototype_id': p.id, 'status': a.status,
|
|
54
|
+
'coords': {'x': a.x, 'y': a.y}, 'source': 'annotation'},
|
|
55
|
+
})
|
|
56
|
+
edges.append({
|
|
57
|
+
'from_id': 'ANNO:%s' % a.id, 'to_id': 'proto:%s' % p.id,
|
|
58
|
+
'edge_type': 'annotates', 'from_repo': repo_id, 'to_repo': repo_id,
|
|
59
|
+
'confidence': 1.0, 'source': 'explicit',
|
|
60
|
+
})
|
|
61
|
+
db.close()
|
|
62
|
+
|
|
63
|
+
upsert_r = pg_upsert.upsert_batch(entities, edges) if (entities or edges) else {'ok': False}
|
|
64
|
+
return {'prototypes': len(protos), 'entities': len(entities), 'edges': len(edges), 'upsert': upsert_r}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""回流历史 → 实践知识。
|
|
3
|
+
|
|
4
|
+
asset_returns (引擎→平台产出) → embedding (能搜"之前怎么做的")。
|
|
5
|
+
当前3条, 是引擎回流的PRD/原型/资产。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import os, 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 import_returns(project_id: str = None, logger=print):
|
|
19
|
+
"""asset_returns → KG 实践知识。"""
|
|
20
|
+
try:
|
|
21
|
+
from dotenv import load_dotenv
|
|
22
|
+
for p in ['wlinkj-workspace/backend/.env', '.env']:
|
|
23
|
+
if os.path.isfile(p): load_dotenv(p); break
|
|
24
|
+
except: pass
|
|
25
|
+
sys.path.insert(0, os.path.join(os.getcwd(), 'wlinkj-workspace', 'backend'))
|
|
26
|
+
from app.db import SessionLocal
|
|
27
|
+
from app.models import AssetReturn
|
|
28
|
+
from domain.kg.extract.java import pg_upsert
|
|
29
|
+
|
|
30
|
+
db = SessionLocal()
|
|
31
|
+
q = db.query(AssetReturn)
|
|
32
|
+
if project_id: q = q.filter(AssetReturn.project_id == project_id)
|
|
33
|
+
returns = q.all()
|
|
34
|
+
logger(' [asset-ret] %d 条回流' % len(returns))
|
|
35
|
+
|
|
36
|
+
entities, edges = [], []
|
|
37
|
+
for r in returns:
|
|
38
|
+
repo_id = 'asset-%s' % (r.project_id or 'global')[:8]
|
|
39
|
+
# 回流产出 → 实体 (按 return_type 分类)
|
|
40
|
+
etype = {'prd': 'PRD', 'prototype': 'PAGE', 'asset': 'ASSET'}.get(r.return_type, 'ASSET')
|
|
41
|
+
entities.append({
|
|
42
|
+
'id': 'return:%s' % r.id, 'repo_id': repo_id, 'type': etype,
|
|
43
|
+
'canonical': r.title[:80], 'cn': r.return_type or '',
|
|
44
|
+
'props': {'project_id': r.project_id, 'return_type': r.return_type,
|
|
45
|
+
'source': r.source, 'status': r.status,
|
|
46
|
+
'preview': (r.content_preview or '')[:200],
|
|
47
|
+
'source': 'asset_return'},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
db.close()
|
|
51
|
+
upsert_r = pg_upsert.upsert_batch(entities, edges) if (entities or edges) else {'ok': False}
|
|
52
|
+
return {'returns': len(returns), 'entities': len(entities), 'edges': len(edges), 'upsert': upsert_r}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""GOAL 3 统一构建入口: MyBatis XML + DB Schema + 写 PG。
|
|
3
|
+
|
|
4
|
+
被 kg_build.py 调用, 或独立 CLI:
|
|
5
|
+
python -m domain.kg.extract.build_goal3 --repo-dir data/code/fywl-ics --repo-id fywl-ics
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
_THIS = os.path.dirname(os.path.abspath(__file__))
|
|
12
|
+
for _i in range(8):
|
|
13
|
+
_p = os.path.dirname(_THIS)
|
|
14
|
+
if os.path.isfile(os.path.join(_p, 'foundation', 'bootstrap.py')):
|
|
15
|
+
sys.path.insert(0, _p); break
|
|
16
|
+
_THIS = _p
|
|
17
|
+
|
|
18
|
+
from domain.kg.extract.mybatis import all as mybatis_all
|
|
19
|
+
from domain.kg.extract.db import schema_extractor, fk_extractor
|
|
20
|
+
from domain.kg.extract.java import pg_upsert
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build(repo_dir: str, repo_id: str, write_pg: bool = True, logger=print):
|
|
24
|
+
"""GOAL 3 主入口: mybatis 提取 + db schema + 写 PG。"""
|
|
25
|
+
logger(' [goal3] === MyBatis XML 提取 ===')
|
|
26
|
+
mb_entities, mb_edges = mybatis_all.extract_repo(repo_dir, repo_id, logger=logger)
|
|
27
|
+
|
|
28
|
+
logger(' [goal3] === DB Schema 真源 ===')
|
|
29
|
+
db_tables, db_columns = schema_extractor.extract_all_schema(repo_id, logger=logger)
|
|
30
|
+
fk_edges = fk_extractor.extract_foreign_keys(repo_id, logger=logger)
|
|
31
|
+
|
|
32
|
+
# db_tables/db_columns 写专用表 (不是 entities)
|
|
33
|
+
db_pg_result = None
|
|
34
|
+
if write_pg and (db_tables or db_columns):
|
|
35
|
+
db_pg_result = _upsert_db_schema(db_tables, db_columns)
|
|
36
|
+
|
|
37
|
+
# mybatis 实体/边 写 entities/edges (复用 GOAL 2 的 pg_upsert)
|
|
38
|
+
mb_pg_result = None
|
|
39
|
+
if write_pg and (mb_entities or mb_edges):
|
|
40
|
+
# 加上 FK 边
|
|
41
|
+
all_edges = mb_edges + fk_edges
|
|
42
|
+
mb_pg_result = pg_upsert.upsert_batch(mb_entities, all_edges)
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
'mybatis_entities': len(mb_entities), 'mybatis_edges': len(mb_edges),
|
|
46
|
+
'db_tables': len(db_tables), 'db_columns': len(db_columns),
|
|
47
|
+
'fk_edges': len(fk_edges),
|
|
48
|
+
'mb_pg': mb_pg_result, 'db_pg': db_pg_result,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _upsert_db_schema(tables: list, columns: list):
|
|
53
|
+
"""把 db_tables / db_columns 写进 PG 专用表 (GOAL 1 建的)。"""
|
|
54
|
+
engine, _ = pg_upsert._get_pg_engine()
|
|
55
|
+
if engine is None:
|
|
56
|
+
return {'ok': False, 'error': 'PG 不可用'}
|
|
57
|
+
from sqlalchemy import text
|
|
58
|
+
t_ok = c_ok = 0
|
|
59
|
+
with engine.begin() as conn:
|
|
60
|
+
for t in tables:
|
|
61
|
+
try:
|
|
62
|
+
conn.execute(text("""
|
|
63
|
+
INSERT INTO db_tables (repo_id, schema_name, table_name, table_comment, rows_count)
|
|
64
|
+
VALUES (:r, :s, :t, :c, :n)
|
|
65
|
+
ON CONFLICT (repo_id, schema_name, table_name) DO UPDATE SET
|
|
66
|
+
table_comment=EXCLUDED.table_comment, rows_count=EXCLUDED.rows_count
|
|
67
|
+
"""), {'r': t['repo_id'], 's': t['schema_name'], 't': t['table_name'],
|
|
68
|
+
'c': t['table_comment'], 'n': t.get('rows_count')})
|
|
69
|
+
t_ok += 1
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
for c in columns:
|
|
73
|
+
try:
|
|
74
|
+
conn.execute(text("""
|
|
75
|
+
INSERT INTO db_columns (repo_id, schema_name, table_name, column_name,
|
|
76
|
+
data_type, is_nullable, column_comment, ordinal_position)
|
|
77
|
+
VALUES (:r, :s, :t, :cn, :dt, :nn, :cc, :op)
|
|
78
|
+
ON CONFLICT (repo_id, schema_name, table_name, column_name) DO UPDATE SET
|
|
79
|
+
data_type=EXCLUDED.data_type, is_nullable=EXCLUDED.is_nullable,
|
|
80
|
+
column_comment=EXCLUDED.column_comment
|
|
81
|
+
"""), {'r': c['repo_id'], 's': c['schema_name'], 't': c['table_name'],
|
|
82
|
+
'cn': c['column_name'], 'dt': c['data_type'], 'nn': c['is_nullable'],
|
|
83
|
+
'cc': c['column_comment'], 'op': c.get('ordinal_position')})
|
|
84
|
+
c_ok += 1
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
return {'ok': True, 'tables_ok': t_ok, 'columns_ok': c_ok}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == '__main__':
|
|
91
|
+
import argparse, json
|
|
92
|
+
ap = argparse.ArgumentParser()
|
|
93
|
+
ap.add_argument('--repo-dir', required=True)
|
|
94
|
+
ap.add_argument('--repo-id', default='')
|
|
95
|
+
ap.add_argument('--dry-run', action='store_true')
|
|
96
|
+
args = ap.parse_args()
|
|
97
|
+
try:
|
|
98
|
+
from dotenv import load_dotenv
|
|
99
|
+
for p in ['wlinkj-workspace/backend/.env', '.env']:
|
|
100
|
+
if os.path.isfile(p): load_dotenv(p); break
|
|
101
|
+
except: pass
|
|
102
|
+
rid = args.repo_id or os.path.basename(args.repo_dir.rstrip('/\\'))
|
|
103
|
+
st = build(args.repo_dir, rid, write_pg=not args.dry_run)
|
|
104
|
+
print(json.dumps(st, ensure_ascii=False, indent=2, default=str))
|