@hupan56/wlkj 3.3.13 → 3.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 -156
  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
@@ -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))
@@ -0,0 +1,55 @@
1
+ # -*- coding: utf-8 -*-
2
+ """GOAL 4 统一入口: PRD LLM + 资产中心 5 表 + REQ-ID 锚定。
3
+
4
+ 被 kg_build.py 调用, 或独立 CLI:
5
+ python -m domain.kg.extract.build_goal4 [--project-id PID] [--embed]
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 build(project_id: str = None, do_embed: bool = False, logger=print):
19
+ """GOAL 4 主入口: 资产中心 5 表吸收。"""
20
+ from domain.kg.extract.asset import (prd_importer, prototype_importer,
21
+ discussion_importer, asset_tree, returns_importer)
22
+
23
+ logger(' [goal4] === PRD LLM 抽取 + REQ-ID 锚定 ===')
24
+ prd_r = prd_importer.import_all_prds(project_id, do_embed=do_embed, logger=logger)
25
+ logger(' [goal4] PRD: %s' % {k: v for k, v in prd_r.items() if k != 'upsert'})
26
+
27
+ logger(' [goal4] === 原型 + 标注 ===')
28
+ proto_r = prototype_importer.import_prototypes(project_id, logger=logger)
29
+
30
+ logger(' [goal4] === 讨论 ===')
31
+ disc_r = discussion_importer.import_discussions(project_id, logger=logger)
32
+
33
+ logger(' [goal4] === 资产树 ===')
34
+ tree_r = asset_tree.import_asset_tree(project_id, logger=logger)
35
+
36
+ logger(' [goal4] === 回流 ===')
37
+ ret_r = returns_importer.import_returns(project_id, logger=logger)
38
+
39
+ return {'prd': prd_r, 'prototype': proto_r, 'discussion': disc_r,
40
+ 'asset_tree': tree_r, 'returns': ret_r}
41
+
42
+
43
+ if __name__ == '__main__':
44
+ import argparse, json
45
+ ap = argparse.ArgumentParser()
46
+ ap.add_argument('--project-id', default='')
47
+ ap.add_argument('--embed', action='store_true')
48
+ args = ap.parse_args()
49
+ try:
50
+ from dotenv import load_dotenv
51
+ for p in ['wlinkj-workspace/backend/.env', '.env']:
52
+ if os.path.isfile(p): load_dotenv(p); break
53
+ except: pass
54
+ st = build(project_id=args.project_id or None, do_embed=args.embed)
55
+ print(json.dumps(st, ensure_ascii=False, indent=2, default=str))
@@ -0,0 +1,95 @@
1
+ # -*- coding: utf-8 -*-
2
+ """GOAL 5 统一入口: L3 弱关联推理 + C 方案重算。
3
+
4
+ CLI:
5
+ # 限量推理弱关联 (前500个无强关联实体)
6
+ python -m domain.kg.extract.build_goal5 --infer --limit 500
7
+ # C方案重算 (过期弱边续期/删除)
8
+ python -m domain.kg.extract.build_goal5 --recompute
9
+ # 升格 (删冗余弱边)
10
+ python -m domain.kg.extract.build_goal5 --promote
11
+ """
12
+ from __future__ import annotations
13
+ import os, sys
14
+
15
+ _THIS = os.path.dirname(os.path.abspath(__file__))
16
+ for _i in range(8):
17
+ _p = os.path.dirname(_THIS)
18
+ if os.path.isfile(os.path.join(_p, 'foundation', 'bootstrap.py')):
19
+ sys.path.insert(0, _p); break
20
+ _THIS = _p
21
+
22
+
23
+ def run_infer(limit: int = 500, logger=print):
24
+ """推理弱关联 (限量)。"""
25
+ from domain.kg.extract.inference import weak_link
26
+ from domain.kg.extract.java import pg_upsert
27
+ from sqlalchemy import text
28
+
29
+ engine, _ = pg_upsert._get_pg_engine()
30
+ if engine is None:
31
+ return {'error': 'PG 不可用'}
32
+
33
+ with engine.connect() as conn:
34
+ # 取无强关联的代码实体
35
+ entities = conn.execute(text(f"""
36
+ SELECT id, repo_id, type, canonical, cn, props FROM entities
37
+ WHERE type IN ('FUNCTION','BUTTON','ENDPOINT','CONTROLLER','SERVICE',
38
+ 'MAPPER','ENTITY','TABLE','COLUMN','FEATURE')
39
+ AND id NOT IN (
40
+ SELECT from_id FROM edges
41
+ WHERE source IN ('explicit','commit')
42
+ AND edge_type IN ('implements_req','anchors')
43
+ )
44
+ LIMIT {int(limit)}
45
+ """)).all()
46
+ # 取全部 anchors
47
+ anchors = conn.execute(text("SELECT id, title, kind FROM anchors")).all()
48
+
49
+ ent_list = [{'id': r[0], 'repo_id': r[1], 'type': r[2],
50
+ 'canonical': r[3], 'cn': r[4], 'props': r[5] or {}} for r in entities]
51
+ anchor_list = [{'id': r[0], 'title': r[1], 'kind': r[2]} for r in anchors]
52
+ logger(' [goal5] %d 实体待推理, %d 锚点' % (len(ent_list), len(anchor_list)))
53
+
54
+ edges, st = weak_link.infer_batch(ent_list, anchor_list, logger=logger)
55
+
56
+ # 写 PG (用 pg_upsert, 它会处理 expires_at/fingerprint 字段)
57
+ upsert_r = pg_upsert.upsert_batch([], edges) if edges else {'ok': False, 'edges_ok': 0}
58
+ st['upsert'] = upsert_r
59
+ return st
60
+
61
+
62
+ def run_recompute(logger=print):
63
+ """C 方案重算。"""
64
+ from domain.kg.extract.inference import recompute
65
+ return recompute.recompute_expired(logger=logger)
66
+
67
+
68
+ def run_promote(logger=print):
69
+ """弱边升格。"""
70
+ from domain.kg.extract.inference import promote
71
+ return promote.promote_strong_over_weak(logger=logger)
72
+
73
+
74
+ if __name__ == '__main__':
75
+ import argparse, json
76
+ ap = argparse.ArgumentParser()
77
+ ap.add_argument('--infer', action='store_true')
78
+ ap.add_argument('--recompute', action='store_true')
79
+ ap.add_argument('--promote', action='store_true')
80
+ ap.add_argument('--limit', type=int, default=500)
81
+ args = ap.parse_args()
82
+ try:
83
+ from dotenv import load_dotenv
84
+ for p in ['wlinkj-workspace/backend/.env', '.env']:
85
+ if os.path.isfile(p): load_dotenv(p); break
86
+ except: pass
87
+
88
+ if args.infer:
89
+ print(json.dumps(run_infer(args.limit), ensure_ascii=False, indent=2, default=str))
90
+ elif args.recompute:
91
+ print(json.dumps(run_recompute(), ensure_ascii=False, indent=2, default=str))
92
+ elif args.promote:
93
+ print(json.dumps(run_promote(), ensure_ascii=False, indent=2, default=str))
94
+ else:
95
+ ap.print_help()
@@ -0,0 +1,8 @@
1
+ # -*- coding: utf-8 -*-
2
+ """DB schema 提取器 (GOAL 3)。
3
+
4
+ 子模块:
5
+ schema_extractor 连 information_schema → 灌 db_tables/db_columns
6
+ fk_extractor 外键关系 → references 边
7
+ data_profile 数据特征 (null率/枚举分布, 选做)
8
+ """
@@ -0,0 +1,22 @@
1
+ # -*- coding: utf-8 -*-
2
+ """数据特征 profile (选做): 表行数、列 null 率、枚举分布。
3
+
4
+ 价值: 让 KG 知道"这张表有 50 万行""这个字段 null 率 80%"——数据质量知识。
5
+ 本轮 best-effort, 连不上 DB 跳过。
6
+ """
7
+ from __future__ import annotations
8
+ from . import schema_extractor as _se
9
+
10
+
11
+ def profile_tables(repo_id: str = 'fywl-ics', sample_size: int = 100, logger=print):
12
+ """采样表的数据特征。Returns: list[dict] 连不上返回 []。"""
13
+ if not _se._can_connect():
14
+ return []
15
+ try:
16
+ import pymysql
17
+ except ImportError:
18
+ return []
19
+ # 本轮简化: 只返回行数 (table_rows 已在 schema_extractor 取过)
20
+ # 完整版可加: SELECT COUNT(*), 列 null 率, DISTINCT 值分布
21
+ logger(' [db-profile] 本轮跳过详细 profile (行数已在 schema_extractor 取)')
22
+ return []
@@ -0,0 +1,55 @@
1
+ # -*- coding: utf-8 -*-
2
+ """外键关系提取器: KEY_COLUMN_USAGE → references 边。
3
+
4
+ 让 table_impact 真正能做"改表影响谁"的跨表追溯。
5
+ """
6
+ from __future__ import annotations
7
+ from . import schema_extractor as _se
8
+
9
+
10
+ def extract_foreign_keys(repo_id: str = 'fywl-ics', logger=print):
11
+ """连 MySQL → 提取外键关系 → references 边。
12
+
13
+ Returns: [{from_id, to_id, edge_type:'references', ...}] 连不上返回 []
14
+ """
15
+ if not _se._can_connect():
16
+ return []
17
+ try:
18
+ import pymysql
19
+ except ImportError:
20
+ return []
21
+
22
+ conn = pymysql.connect(host=_se.MYSQL_HOST, port=_se.MYSQL_PORT, user=_se.MYSQL_USER,
23
+ password=_se.MYSQL_PASSWORD, charset='utf8mb4',
24
+ cursorclass=pymysql.cursors.DictCursor,
25
+ connect_timeout=5, read_timeout=30)
26
+ edges = []
27
+ try:
28
+ cur = conn.cursor()
29
+ cur.execute("SELECT schema_name FROM information_schema.schemata "
30
+ "WHERE schema_name LIKE %s", (_se.DB_NAME_PREFIX + '%',))
31
+ dbs = [r['schema_name'] for r in cur.fetchall()]
32
+ for db in dbs:
33
+ # 外键关系
34
+ cur.execute("""
35
+ SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
36
+ FROM information_schema.KEY_COLUMN_USAGE
37
+ WHERE TABLE_SCHEMA=%s AND REFERENCED_TABLE_NAME IS NOT NULL
38
+ """, (db,))
39
+ for r in cur.fetchall():
40
+ from_tbl = r['TABLE_NAME'].lower()
41
+ to_tbl = r['REFERENCED_TABLE_NAME'].lower()
42
+ edges.append({
43
+ 'from_id': 'TBL:%s:%s' % (repo_id, from_tbl),
44
+ 'to_id': 'TBL:%s:%s' % (repo_id, to_tbl),
45
+ 'edge_type': 'references',
46
+ 'from_repo': repo_id, 'to_repo': repo_id,
47
+ 'confidence': 1.0, 'source': 'db_fk',
48
+ 'props': {'column': r['COLUMN_NAME'],
49
+ 'ref_column': r['REFERENCED_COLUMN_NAME'],
50
+ 'schema': db},
51
+ })
52
+ logger(' [db-fk] %d 条外键关系' % len(edges))
53
+ finally:
54
+ conn.close()
55
+ return edges
@@ -0,0 +1,90 @@
1
+ # -*- coding: utf-8 -*-
2
+ """连 MySQL information_schema → 灌 db_tables / db_columns。
3
+
4
+ 复用 kg_build_db.py 的 MySQL 连接配置 (内网 10.54.6.9, fywl_ics_* 库)。
5
+ 连不上内网时 best-effort 返回空, 不阻塞 (GOAL 3 主链路是 mapper XML)。
6
+ """
7
+ from __future__ import annotations
8
+ import os
9
+ import sys
10
+ import socket
11
+
12
+ _THIS = os.path.dirname(os.path.abspath(__file__))
13
+ for _i in range(8):
14
+ _p = os.path.dirname(_THIS)
15
+ if os.path.isfile(os.path.join(_p, 'foundation', 'bootstrap.py')):
16
+ sys.path.insert(0, _p); break
17
+ _THIS = _p
18
+
19
+ # 复用 kg_build_db 的配置
20
+ MYSQL_HOST = os.environ.get('MYSQL_HOST', '10.54.6.9')
21
+ MYSQL_PORT = int(os.environ.get('MYSQL_PORT', '3306'))
22
+ MYSQL_USER = os.environ.get('MYSQL_USER', 'fywl_ics_test')
23
+ MYSQL_PASSWORD = os.environ.get('MYSQL_PASSWORD', 'Fywl_ics_test56476')
24
+ DB_NAME_PREFIX = os.environ.get('DB_NAME_PREFIX', 'fywl_ics_')
25
+
26
+
27
+ def _can_connect(timeout=3):
28
+ """探测内网 MySQL 是否可达。"""
29
+ try:
30
+ with socket.create_connection((MYSQL_HOST, MYSQL_PORT), timeout=timeout):
31
+ return True
32
+ except Exception:
33
+ return False
34
+
35
+
36
+ def extract_all_schema(repo_id: str = 'fywl-ics', logger=print):
37
+ """连 MySQL → 灌 db_tables + db_columns。
38
+
39
+ Returns: (tables: list[dict], columns: list[dict]) 连不上返回 ([], [])
40
+ """
41
+ if not _can_connect():
42
+ logger(' [db] ⚠️ 内网 MySQL 不可达 (%s:%s), 跳过 schema 提取' % (MYSQL_HOST, MYSQL_PORT))
43
+ return [], []
44
+ try:
45
+ import pymysql
46
+ except ImportError:
47
+ logger(' [db] pymysql 未装, 跳过')
48
+ return [], []
49
+
50
+ logger(' [db] 连接 MySQL %s:%s ...' % (MYSQL_HOST, MYSQL_PORT))
51
+ conn = pymysql.connect(host=MYSQL_HOST, port=MYSQL_PORT, user=MYSQL_USER,
52
+ password=MYSQL_PASSWORD, charset='utf8mb4',
53
+ cursorclass=pymysql.cursors.DictCursor,
54
+ connect_timeout=5, read_timeout=30)
55
+ tables, columns = [], []
56
+ try:
57
+ cur = conn.cursor()
58
+ cur.execute("SELECT schema_name FROM information_schema.schemata "
59
+ "WHERE schema_name LIKE %s ORDER BY schema_name",
60
+ (DB_NAME_PREFIX + '%'))
61
+ dbs = [r['schema_name'] for r in cur.fetchall()]
62
+ logger(' [db] 发现 %d 个业务库: %s' % (len(dbs), dbs[:3]))
63
+
64
+ for db in dbs:
65
+ # 表
66
+ cur.execute("SELECT table_name, table_comment, table_rows "
67
+ "FROM information_schema.tables WHERE table_schema=%s", (db,))
68
+ for r in cur.fetchall():
69
+ tables.append({
70
+ 'repo_id': repo_id, 'schema_name': db,
71
+ 'table_name': r['table_name'], 'table_comment': r.get('table_comment') or '',
72
+ 'rows_count': r.get('table_rows'),
73
+ })
74
+ # 列
75
+ cur.execute("SELECT table_name, column_name, data_type, is_nullable, "
76
+ "column_comment, ordinal_position "
77
+ "FROM information_schema.columns WHERE table_schema=%s "
78
+ "ORDER BY table_name, ordinal_position", (db,))
79
+ for r in cur.fetchall():
80
+ columns.append({
81
+ 'repo_id': repo_id, 'schema_name': db,
82
+ 'table_name': r['table_name'], 'column_name': r['column_name'],
83
+ 'data_type': r['data_type'], 'is_nullable': r['is_nullable'],
84
+ 'column_comment': r.get('column_comment') or '',
85
+ 'ordinal_position': r.get('ordinal_position'),
86
+ })
87
+ logger(' [db] 完成: %d 表 / %d 列' % (len(tables), len(columns)))
88
+ finally:
89
+ conn.close()
90
+ return tables, columns
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8 -*-
2
+ """L3 推断层 (GOAL 5): 跨源锚点关联。
3
+
4
+ 子模块:
5
+ weak_link embedding 弱关联推理 (向量召回, 需 pgvector; 不可用时降级命名匹配)
6
+ naming_matcher 命名同源匹配 (AssetController ↔ asset ↔ 资产管理)
7
+ recompute C 方案指纹重算 (30天过期弱边对比指纹)
8
+ promote 弱边升格 (出现强关联时弱边删除)
9
+ """
@@ -0,0 +1,206 @@
1
+ # -*- coding: utf-8 -*-
2
+ """D2: 补社区摘要 + 摘要向量。
3
+
4
+ 对 graph_communities 表里 summary 为空的社区:
5
+ ① 读 member_ids → 取成员的 canonical+cn → LLM 生成摘要
6
+ ② 对摘要算 embedding → summary_vec
7
+
8
+ 独立于现有 summarize_communities (不改动它), 直接补 PG。
9
+ """
10
+ from __future__ import annotations
11
+ import os
12
+ import sys
13
+ import json
14
+ import time
15
+ import urllib.request
16
+ from concurrent.futures import ThreadPoolExecutor
17
+
18
+ _THIS = os.path.dirname(os.path.abspath(__file__))
19
+ for _i in range(8):
20
+ _p = os.path.dirname(_THIS)
21
+ if os.path.isfile(os.path.join(_p, 'foundation', 'bootstrap.py')):
22
+ sys.path.insert(0, _p); break
23
+ _THIS = _p
24
+
25
+ _SUMMARY_PROMPT = """你是软件架构师。下面是一个代码社区(聚类在一起的相关功能)的成员列表。
26
+ 请用1-2句话概括这个社区负责什么业务功能。
27
+
28
+ 成员列表(类名/端点/按钮, 括号内是中文含义):
29
+ %s
30
+
31
+ 只返回摘要文本, 不要解释。"""
32
+
33
+
34
+ def _call_qwen(prompt: str, timeout: int = 20) -> str:
35
+ key = os.environ.get('DASHSCOPE_API_KEY', '')
36
+ base = os.environ.get('ALI_CHAT_BASE', '').rstrip('/')
37
+ model = os.environ.get('ALI_CHAT_MODEL', 'qwen-plus')
38
+ if not key or not base:
39
+ return ''
40
+ req = urllib.request.Request(
41
+ base + '/chat/completions',
42
+ data=json.dumps({'model': model, 'messages': [{'role': 'user', 'content': prompt}],
43
+ 'max_tokens': 200, 'temperature': 0.2}).encode('utf-8'),
44
+ headers={'Authorization': 'Bearer ' + key, 'Content-Type': 'application/json'},
45
+ )
46
+ try:
47
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
48
+ return json.loads(resp.read())['choices'][0]['message']['content'].strip()
49
+ except Exception:
50
+ return ''
51
+
52
+
53
+ def _embed(text: str) -> list:
54
+ """调 DashScope embedding, 返回 1024 维向量。"""
55
+ key = os.environ.get('DASHSCOPE_API_KEY', '')
56
+ if not key or not text:
57
+ return []
58
+ req = urllib.request.Request(
59
+ 'https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings',
60
+ data=json.dumps({'model': 'text-embedding-v3', 'input': [text[:500]], 'dimensions': 1024}).encode('utf-8'),
61
+ headers={'Authorization': 'Bearer ' + key, 'Content-Type': 'application/json'},
62
+ )
63
+ try:
64
+ with urllib.request.urlopen(req, timeout=15) as resp:
65
+ return json.loads(resp.read())['data'][0]['embedding']
66
+ except Exception:
67
+ return []
68
+
69
+
70
+ def summarize_missing(limit: int = 2500, do_embed: bool = True, logger=print):
71
+ """补全缺摘要的社区。
72
+
73
+ Args:
74
+ limit: 上限 (默认 2500, 覆盖全部 2206 个缺的)
75
+ do_embed: True 同时补 summary_vec
76
+ """
77
+ from sqlalchemy import text
78
+ from domain.kg.extract.java import pg_upsert
79
+
80
+ engine, _ = pg_upsert._get_pg_engine()
81
+ if engine is None:
82
+ return {'error': 'PG 不可用'}
83
+
84
+ # 取缺摘要的社区
85
+ with engine.connect() as conn:
86
+ rows = conn.execute(text("""
87
+ SELECT id, level, size, member_ids FROM graph_communities
88
+ WHERE summary IS NULL OR summary = ''
89
+ LIMIT :lim
90
+ """), {'lim': limit}).all()
91
+ logger(' [d2] %d 个社区待补摘要' % len(rows))
92
+ if not rows:
93
+ return {'summarized': 0}
94
+
95
+ # 批量取成员的 canonical+cn (一次查)
96
+ all_member_ids = set()
97
+ for r in rows:
98
+ try:
99
+ mids = json.loads(r[3]) if isinstance(r[3], str) else (r[3] or [])
100
+ all_member_ids.update(mids[:20]) # 每社区最多取20个成员
101
+ except Exception:
102
+ pass
103
+
104
+ member_info = {} # {entity_id: "canonical(cn)"}
105
+ if all_member_ids:
106
+ member_list = list(all_member_ids)[:5000] # 限 5000 避免查询太大
107
+ # 分 chunk 查
108
+ for ci in range(0, len(member_list), 200):
109
+ chunk = member_list[ci:ci+200]
110
+ arr = "ARRAY[" + ','.join("'" + eid.replace("'", "''") + "'" for eid in chunk) + "]"
111
+ with engine.connect() as conn:
112
+ mrows = conn.execute(text(
113
+ f"SELECT id, canonical, cn FROM entities WHERE id = ANY({arr})"
114
+ )).all()
115
+ for eid, canonical, cn in mrows:
116
+ desc = canonical or eid
117
+ if cn:
118
+ desc += '(' + cn[:20] + ')'
119
+ member_info[eid] = desc[:50]
120
+
121
+ logger(' [d2] 成员信息: %d 条' % len(member_info))
122
+
123
+ # 批量生成摘要 (4线程并发)
124
+ def make_summary(row):
125
+ cid, level, size, member_ids_raw = row
126
+ try:
127
+ mids = json.loads(member_ids_raw) if isinstance(member_ids_raw, str) else (member_ids_raw or [])
128
+ except Exception:
129
+ mids = []
130
+ members_desc = [member_info.get(mid, mid) for mid in mids[:15]]
131
+ if not members_desc:
132
+ members_desc = [mid for mid in mids[:15]]
133
+ if not members_desc:
134
+ return (cid, '空社区')
135
+ prompt = _SUMMARY_PROMPT % '\n'.join(members_desc)
136
+ summary = _call_qwen(prompt)
137
+ if not summary:
138
+ # 兜底: 实体名拼接
139
+ summary = '、'.join(members_desc[:5]) + '等相关功能'
140
+ return (cid, summary[:200])
141
+
142
+ # 并发跑
143
+ t0 = time.time()
144
+ batches = [rows[i:i+10] for i in range(0, len(rows), 10)]
145
+ all_summaries = []
146
+ for bi in range(0, len(batches), 5):
147
+ chunk = batches[bi:bi+5]
148
+ with ThreadPoolExecutor(max_workers=4) as ex:
149
+ results = list(ex.map(make_summary, [r for b in chunk for r in b]))
150
+ all_summaries += results
151
+ if (bi//5+1) % 10 == 0:
152
+ logger(' [d2] %d/%d 社区摘要生成 (%d)' % (len(all_summaries), len(rows), len(all_summaries)))
153
+
154
+ logger(' [d2] 摘要生成完成: %d / %.1fs' % (len(all_summaries), time.time()-t0))
155
+
156
+ # 写 PG (分块提交)
157
+ ok = 0
158
+ vec_ok = 0
159
+ for i in range(0, len(all_summaries), 100):
160
+ chunk = all_summaries[i:i+100]
161
+ with engine.begin() as conn:
162
+ for cid, summary in chunk:
163
+ try:
164
+ # 算摘要向量 (可选)
165
+ vec_str = ''
166
+ if do_embed:
167
+ vec = _embed(summary)
168
+ if vec:
169
+ # summary_vec 存成 JSONB (兼容无 pgvector)
170
+ vec_str = json.dumps(vec)
171
+ if vec_str:
172
+ # summary_vec 是 ARRAY 类型, 用 FLOAT[] 写入
173
+ arr_str = '{' + ','.join(str(float(v)) for v in vec[:1024]) + '}'
174
+ conn.execute(text("""
175
+ UPDATE graph_communities SET summary=:s, summary_vec=CAST(:arr AS FLOAT[])
176
+ WHERE id=:id
177
+ """), {'s': summary, 'arr': arr_str, 'id': cid})
178
+ vec_ok += 1
179
+ else:
180
+ conn.execute(text("""
181
+ UPDATE graph_communities SET summary=:s WHERE id=:id
182
+ """), {'s': summary, 'id': cid})
183
+ ok += 1
184
+ except Exception:
185
+ pass
186
+ if (i//100+1) % 5 == 0:
187
+ logger(' [d2] 写入 %d/%d (vec=%d)' % (ok, len(all_summaries), vec_ok))
188
+
189
+ dt = time.time() - t0
190
+ logger(' [d2] 完成: %d 摘要 / %d 向量 / %.1fs' % (ok, vec_ok, dt))
191
+ return {'summarized': ok, 'vectorized': vec_ok, 'seconds': round(dt, 1)}
192
+
193
+
194
+ if __name__ == '__main__':
195
+ import argparse
196
+ ap = argparse.ArgumentParser()
197
+ ap.add_argument('--limit', type=int, default=2500)
198
+ ap.add_argument('--no-embed', action='store_true')
199
+ args = ap.parse_args()
200
+ try:
201
+ from dotenv import load_dotenv
202
+ for p in ['wlinkj-workspace/backend/.env', '.env']:
203
+ if os.path.isfile(p): load_dotenv(p); break
204
+ except: pass
205
+ r = summarize_missing(limit=args.limit, do_embed=not args.no_embed)
206
+ print(json.dumps(r, ensure_ascii=False, indent=2))