@hupan56/wlkj 3.3.12 → 3.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/bin/cli.js +1 -23
  2. package/package.json +29 -29
  3. package/templates/qoder/agents/design-agent.md +20 -0
  4. package/templates/qoder/agents/spec-generator.md +21 -0
  5. package/templates/qoder/commands/optional/wl-spec.md +2 -12
  6. package/templates/qoder/commands/optional/wl-status.md +0 -8
  7. package/templates/qoder/commands/wl-code.md +0 -8
  8. package/templates/qoder/commands/wl-commit.md +1 -11
  9. package/templates/qoder/commands/wl-init.md +129 -129
  10. package/templates/qoder/commands/wl-prd.md +0 -24
  11. package/templates/qoder/commands/wl-search.md +11 -26
  12. package/templates/qoder/commands/wl-task.md +1 -11
  13. package/templates/qoder/commands/wl-test.md +0 -8
  14. package/templates/qoder/config.yaml +8 -3
  15. package/templates/qoder/hooks/session-start.py +365 -384
  16. package/templates/qoder/hooks/stop-eval.py +254 -383
  17. package/templates/qoder/scripts/capability/adapters/mcp.py +314 -322
  18. package/templates/qoder/scripts/capability/registry.py +248 -245
  19. package/templates/qoder/scripts/capability/registry_mcp.py +4 -10
  20. package/templates/qoder/scripts/capability/smoke_test_report.json.new +94 -0
  21. package/templates/qoder/scripts/deployment/setup/init_doctor.py +2 -6
  22. package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +14 -0
  23. package/templates/qoder/scripts/deployment/setup/setup.py +2 -16
  24. package/templates/qoder/scripts/domain/integration/return_to_platform.py +2 -4
  25. package/templates/qoder/scripts/domain/integration/spec_upload.py +4 -4
  26. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +12 -12
  27. package/templates/qoder/scripts/domain/kg/build/build_relations.py +12 -12
  28. package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
  29. package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
  30. package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
  31. package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
  32. package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
  33. package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
  34. package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
  35. package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
  36. package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
  37. package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
  38. package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
  39. package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
  40. package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
  41. package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
  42. package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
  43. package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
  44. package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
  45. package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
  46. package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
  47. package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
  48. package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
  49. package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
  50. package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
  51. package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
  52. package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
  53. package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
  54. package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
  55. package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
  56. package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
  57. package/templates/qoder/scripts/domain/kg/graph/kg_link_db.py +235 -0
  58. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +5 -41
  59. package/templates/qoder/scripts/domain/kg/kg.py +0 -14
  60. package/templates/qoder/scripts/domain/kg/search/enrich_prompt.py +238 -0
  61. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  62. package/templates/qoder/scripts/domain/kg/switch_project.py +2 -2
  63. package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
  64. package/templates/qoder/scripts/domain/task/wlkj_panel.py +1348 -1503
  65. package/templates/qoder/scripts/engine/poller.py +219 -0
  66. package/templates/qoder/scripts/foundation/bootstrap.py +145 -145
  67. package/templates/qoder/scripts/orchestration/wlkj.py +189 -306
  68. package/templates/qoder/settings.json +0 -8
  69. package/templates/qoder/skills/wl-spec/SKILL.md +1 -1
  70. package/templates/qoder/commands/wl-data.md +0 -46
  71. package/templates/qoder/commands/wl-fix.md +0 -47
  72. package/templates/qoder/commands/wl-knowledge.md +0 -41
  73. package/templates/qoder/commands/wl-review.md +0 -49
  74. package/templates/qoder/contracts/insight.md +0 -55
  75. package/templates/qoder/hooks/pre-tool-use-commit.py +0 -124
  76. package/templates/qoder/scripts/foundation/io/context_cache.py +0 -94
  77. package/templates/qoder/scripts/tool_guide.md +0 -70
  78. package/templates/qoder/scripts/validation/eval/alignment_matrix.py +0 -176
  79. package/templates/qoder/scripts/validation/eval/bf2_content_fidelity.py +0 -110
  80. package/templates/qoder/scripts/validation/eval/bf2_llmjudge.py +0 -104
  81. package/templates/qoder/scripts/validation/eval/bf_score.py +0 -218
  82. package/templates/qoder/scripts/validation/eval/code_flywheel.py +0 -150
  83. package/templates/qoder/scripts/validation/eval/dispatcher_ab.py +0 -156
  84. package/templates/qoder/scripts/validation/eval/dispatcher_ab_2026-07-21.json +0 -23
  85. package/templates/qoder/scripts/validation/eval/feature_fidelity_flywheel.py +0 -143
  86. package/templates/qoder/scripts/validation/eval/gradient_matrix.py +0 -261
  87. package/templates/qoder/scripts/validation/eval/gradient_matrix_baseline_2026-07-21.json +0 -33
  88. package/templates/qoder/scripts/validation/eval/metrics_dashboard.py +0 -105
  89. package/templates/qoder/scripts/validation/eval/multi_turn_flywheel.py +0 -118
  90. package/templates/qoder/scripts/validation/eval/prd_fidelity_flywheel.py +0 -128
  91. package/templates/qoder/scripts/validation/eval/prd_flywheel.py +0 -148
  92. package/templates/qoder/scripts/validation/eval/prototype_fidelity_flywheel.py +0 -166
  93. package/templates/qoder/scripts/validation/eval/recall_flywheel.py +0 -148
  94. package/templates/qoder/scripts/validation/eval/robustness_flywheel.py +0 -139
  95. package/templates/qoder/scripts/validation/eval/speed_accuracy_flywheel.py +0 -188
  96. package/templates/qoder/scripts/validation/eval/task_flywheel.py +0 -124
  97. package/templates/qoder/scripts/validation/eval/token_flywheel.py +0 -88
  98. package/templates/qoder/scripts/validation/metrics/eval_code_ac.py +0 -177
  99. package/templates/qoder/scripts/validation/metrics/lint_cases.py +0 -170
  100. package/templates/qoder/scripts/validation/test/test_context_cache.py +0 -78
  101. package/templates/qoder/scripts/validation/test/test_lint_cases.py +0 -60
  102. package/templates/qoder/scripts/validation/test/test_pre_tool_use_commit.py +0 -70
  103. package/templates/workspace/specs/REQ-TEST-CTX/.context-cache.json +0 -1
@@ -834,7 +834,6 @@ def check_lanhu():
834
834
  def main():
835
835
  args = [a for a in sys.argv[1:]]
836
836
  fix = '--fix' in args
837
- skip_code = '--skip-code' in args or '--cloud' in args
838
837
  pos = [a for a in args if not a.startswith('--')]
839
838
  name = pos[0] if pos else None
840
839
  role = pos[1] if len(pos) > 1 else None
@@ -849,11 +848,8 @@ def main():
849
848
  check_dependencies(fix)
850
849
  check_developer(fix, name, role)
851
850
  check_team_sync(fix)
852
- if skip_code:
853
- print('\n--- 4. 源码/索引: 云模式跳过(KG 在云平台,引擎调云 MCP)---')
854
- else:
855
- check_source_repos(fix, config)
856
- check_index(fix)
851
+ check_source_repos(fix, config)
852
+ check_index(fix)
857
853
  check_style(fix)
858
854
  check_prd_templates()
859
855
  check_weekly(fix)
@@ -438,6 +438,20 @@ def rewrite_mcp_json_for_launcher():
438
438
  }
439
439
  changed = False
440
440
  for srv, sub in name_map.items():
441
+ # 共识:kg 走云平台(读 config platform_mcp.url → SSE url),不连本地 kg.duckdb(空)
442
+ if srv == "qoder-knowledge-graph":
443
+ try:
444
+ import yaml as _y
445
+ _cfg_p = get_repo_root() / ".qoder" / "config.yaml"
446
+ if _cfg_p.is_file():
447
+ _cfg = _y.safe_load(_cfg_p.read_text(encoding="utf-8")) or {}
448
+ _pm = (_cfg.get("platform_mcp") or {}).get("url", "")
449
+ if _pm:
450
+ servers[srv] = {"url": _pm, "type": "sse", "enabled": True}
451
+ changed = True
452
+ continue
453
+ except Exception:
454
+ pass # config 无 platform_mcp → 回退 launcher
441
455
  entry = servers.get(srv)
442
456
  # 全新机器: kg/mysql/lanhu 条目可能不存在 → 创建它 (launcher 模式)
443
457
  if not entry:
@@ -1,16 +1,5 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
- # Windows 控制台 UTF-8 修复(ctypes 直调 Windows API,非子shell chcp)
4
- import sys as _s
5
- if _s.platform == 'win32':
6
- import ctypes as _ct, io as _io
7
- try:
8
- _ct.windll.kernel32.SetConsoleOutputCP(65001)
9
- _ct.windll.kernel32.SetConsoleCP(65001)
10
- _s.stdout = _io.TextIOWrapper(_s.stdout.buffer, encoding='utf-8', errors='replace')
11
- _s.stderr = _io.TextIOWrapper(_s.stderr.buffer, encoding='utf-8', errors='replace')
12
- except Exception:
13
- pass
14
3
  # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
15
4
  import os as _o, sys as _s
16
5
  _f = _o.path.abspath(__file__)
@@ -253,11 +242,9 @@ def detect_name(explicit=None):
253
242
  # Step 2: 调 init_doctor --fix (主体)
254
243
  # ============================================================
255
244
 
256
- def run_doctor(name, role, skip_code=False):
245
+ def run_doctor(name, role):
257
246
  print('\n--- 初始化 (init_doctor) ---')
258
247
  cmd = [sys.executable, str(THIS_DIR / 'init_doctor.py'), '--fix', name, role]
259
- if skip_code:
260
- cmd.append('--skip-code')
261
248
  # 透传 stdio (用户能看到实时输出) + 强制 UTF-8 环境 (解决 cmd GBK 乱码)
262
249
  env = dict(os.environ)
263
250
  env['PYTHONIOENCODING'] = 'utf-8'
@@ -924,7 +911,6 @@ def main():
924
911
  parser.add_argument('--skip-cron', action='store_true', help='跳过 cron 注册')
925
912
  parser.add_argument('--skip-qoderwork', action='store_true', help='跳过 QoderWork 安装')
926
913
  parser.add_argument('--skip-lanhu', action='store_true', help='跳过蓝湖 MCP 引导')
927
- parser.add_argument('--skip-code', '--cloud', action='store_true', help='云模式:跳过本地源码 clone/索引(KG 在云平台)')
928
914
  args = parser.parse_args()
929
915
 
930
916
  print('=' * 56)
@@ -945,7 +931,7 @@ def main():
945
931
  configure_git_identity(name)
946
932
 
947
933
  # Step 2
948
- if not run_doctor(name, args.role, args.skip_code):
934
+ if not run_doctor(name, args.role):
949
935
  warn('init_doctor 未完全成功, 继续 setup 后续步骤...')
950
936
 
951
937
  # Step 3
@@ -20,7 +20,6 @@ integration.return_to_platform —— 引擎产出回流平台的统一通道。
20
20
  """
21
21
  from __future__ import annotations
22
22
 
23
- import os
24
23
  from typing import Optional
25
24
 
26
25
 
@@ -35,7 +34,6 @@ def _load_mcp_config() -> dict:
35
34
  except Exception:
36
35
  repo = Path(__file__).resolve().parent.parent.parent.parent.parent
37
36
  for cand in [
38
- repo / ".qoder" / "mcp_config.json", # ★ switch_project 写这里,优先查(否则读不到 token → 回流误报"无 X-Engine-Token")
39
37
  repo / "mcp_config.json",
40
38
  ]:
41
39
  try:
@@ -123,7 +121,7 @@ def _via_http(payload: dict) -> tuple[bool, str]:
123
121
 
124
122
 
125
123
  def _platform_base() -> str:
126
- """平台 base url(默认 http://10.89.7.120 云),从 mcp_config server url 推导。"""
124
+ """平台 base urlhttp://127.0.0.1:10010),从 mcp_config server url 推导。"""
127
125
  from urllib.parse import urlparse
128
126
  cfg = _load_mcp_config()
129
127
  for srv in (cfg.get("mcpServers") or {}).values():
@@ -135,7 +133,7 @@ def _platform_base() -> str:
135
133
  if ep:
136
134
  p = urlparse(ep)
137
135
  return "%s://%s" % (p.scheme, p.netloc)
138
- return os.environ.get("WLKJ_BASE_URL", "http://10.89.7.120")
136
+ return "http://127.0.0.1:10010"
139
137
 
140
138
 
141
139
  def _engine_token() -> Optional[str]:
@@ -5,7 +5,7 @@
5
5
  契约: wlinkj-workspace/docs/SPEC-MCP-CONTRACT.md v1(create/confirm/update/get_spec)
6
6
 
7
7
  流程: 解析契约头 → 有platform_spec_id走update/无走create → confirm → 回写幂等锚
8
- 传输: POST /mcp direct JSON-RPC + X-MCP-Token
8
+ 传输: POST /mcp direct JSON-RPC + X-MCP-Token(同 engine/poller.py:_mcp_call)
9
9
  """
10
10
  from __future__ import annotations
11
11
  import json, os, re, sys, urllib.request, urllib.error
@@ -24,8 +24,8 @@ def _load_mcp_config() -> dict:
24
24
  repo = get_repo_root()
25
25
  except Exception:
26
26
  repo = Path(__file__).resolve().parent.parent.parent.parent.parent
27
- for cand in [repo / ".qoder" / "mcp_config.json", # ★ switch_project 写这里,优先(同 return_to_platform,否则可能读到 repo/ 旧文件)
28
- repo / "mcp_config.json"]:
27
+ for cand in [repo / "mcp_config.json",
28
+ repo / ".qoder" / "mcp_config.json"]:
29
29
  try:
30
30
  if cand.is_file():
31
31
  with open(cand, encoding="utf-8") as f:
@@ -48,7 +48,7 @@ def _resolve_endpoint() -> Tuple[str, str, str]:
48
48
  u = srv.get("url", "")
49
49
  if "/mcp" in u:
50
50
  base = u.split("/mcp")[0]; break
51
- base = (base or os.environ.get("WLKJ_BASE_URL", "http://10.89.7.120")).rstrip("/")
51
+ base = (base or "http://127.0.0.1:10010").rstrip("/")
52
52
  token = os.environ.get("WLKJ_MCP_TOKEN", "")
53
53
  if not token:
54
54
  for srv in (cfg.get("mcpServers") or {}).values():
@@ -1,6 +1,17 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """build_entity_registry.py — 从 ui-modules + code-api 构建 entity-registry.json。
3
3
 
4
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
5
+ import os as _o, sys as _s
6
+ _f = _o.path.abspath(__file__)
7
+ for _ in range(10):
8
+ _f = _o.path.dirname(_f)
9
+ _cp = _o.path.join(_f, 'foundation')
10
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
11
+ break
12
+ if _cp not in _s.path: _s.path.insert(0, _cp)
13
+ from bootstrap import setup; setup()
14
+
4
15
  产出 data/index/_build_cache/entity-registry.json, 供 kg_duckdb 导入 entities/aliases 表,
5
16
  让 kg.py feature <功能名> 能返回功能画像。
6
17
 
@@ -23,18 +34,6 @@
23
34
  业务流程) 由 build_relations.py/build_features.py/build_workflows.py 补充,
24
35
  那些脚本未实现时不影响本脚本的 entity 基础层。
25
36
  """
26
- # v3.0 路径自举: 引导到 foundation/bootstrap, 统一 sys.path 逻辑
27
- # ⚠ 必须在 module docstring 之外! 旧版把这块包进 docstring → bootstrap 永不执行 → 跑死。
28
- import os as _o, sys as _s
29
- _f = _o.path.abspath(__file__)
30
- for _ in range(10):
31
- _f = _o.path.dirname(_f)
32
- _cp = _o.path.join(_f, 'foundation')
33
- if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
34
- break
35
- if _cp not in _s.path: _s.path.insert(0, _cp)
36
- from bootstrap import setup; setup()
37
-
38
37
  import json
39
38
  import os
40
39
  import sys
@@ -42,6 +41,7 @@ from datetime import datetime
42
41
 
43
42
  # 路径自适应 (和兄弟脚本一致)
44
43
  THIS_DIR = os.path.dirname(os.path.abspath(__file__))
44
+ sys.path.insert(0, os.path.join(THIS_DIR, '..'))
45
45
  from foundation.core.paths import DATA_INDEX_DIR, BUILD_CACHE_DIR
46
46
 
47
47
 
@@ -1,6 +1,17 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """build_relations.py — 从 trace-chain + code-api 推导关系边, 生成 relations.json。
3
3
 
4
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
5
+ import os as _o, sys as _s
6
+ _f = _o.path.abspath(__file__)
7
+ for _ in range(10):
8
+ _f = _o.path.dirname(_f)
9
+ _cp = _o.path.join(_f, 'foundation')
10
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
11
+ break
12
+ if _cp not in _s.path: _s.path.insert(0, _cp)
13
+ from bootstrap import setup; setup()
14
+
4
15
  产出 data/index/_build_cache/relations.json, 供 kg_duckdb 导入 edges 表,
5
16
  让 kg.py feature 的端点/按钮数据非零, kg.py hop 多跳遍历有边可走。
6
17
 
@@ -21,24 +32,13 @@
21
32
  - code-api.json: {endpoint: controller_file} (api_to_controller 直接用)
22
33
  - test-assertions.json: tests (controller_to_tests 推导)
23
34
  """
24
- # v3.0 路径自举: 引导到 foundation/bootstrap, 统一 sys.path 逻辑
25
- # ⚠ 必须在 module docstring 之外! 旧版把这块包进 docstring → bootstrap 永不执行 → 跑死。
26
- import os as _o, sys as _s
27
- _f = _o.path.abspath(__file__)
28
- for _ in range(10):
29
- _f = _o.path.dirname(_f)
30
- _cp = _o.path.join(_f, 'foundation')
31
- if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
32
- break
33
- if _cp not in _s.path: _s.path.insert(0, _cp)
34
- from bootstrap import setup; setup()
35
-
36
35
  import json
37
36
  import os
38
37
  import sys
39
38
  from datetime import datetime
40
39
 
41
40
  THIS_DIR = os.path.dirname(os.path.abspath(__file__))
41
+ sys.path.insert(0, os.path.join(THIS_DIR, '..'))
42
42
  from foundation.core.paths import DATA_INDEX_DIR, BUILD_CACHE_DIR
43
43
 
44
44
 
@@ -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}