@hupan56/wlkj 2.7.11 → 3.0.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.
Files changed (152) hide show
  1. package/bin/cli.js +375 -78
  2. package/package.json +29 -29
  3. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
  4. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
  5. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
  6. package/templates/.qoder/learning/eval-history.jsonl +28 -0
  7. package/templates/data/index/wiki-index.json +8 -0
  8. package/templates/qoder/agents/insight-planning.md +1 -1
  9. package/templates/qoder/agents/insight-research.md +28 -15
  10. package/templates/qoder/commands/optional/wl-insight.md +159 -161
  11. package/templates/qoder/commands/optional/wl-report.md +4 -4
  12. package/templates/qoder/commands/optional/wl-status.md +2 -2
  13. package/templates/qoder/commands/wl-code.md +2 -2
  14. package/templates/qoder/commands/wl-design.md +2 -2
  15. package/templates/qoder/commands/wl-init.md +3 -3
  16. package/templates/qoder/commands/wl-prd.md +2 -2
  17. package/templates/qoder/commands/wl-req.md +43 -0
  18. package/templates/qoder/commands/wl-search.md +8 -8
  19. package/templates/qoder/commands/wl-task.md +17 -17
  20. package/templates/qoder/commands/wl-test.md +41 -15
  21. package/templates/qoder/config.yaml +17 -1
  22. package/templates/qoder/hooks/session-start.py +12 -22
  23. package/templates/qoder/nul +4 -0
  24. package/templates/qoder/rules/wl-pipeline.md +22 -48
  25. package/templates/qoder/scripts/README.md +139 -0
  26. package/templates/qoder/scripts/common/autotest_auth.py +109 -0
  27. package/templates/qoder/scripts/common/bootstrap.py +145 -0
  28. package/templates/qoder/scripts/common/check_publish.py +98 -0
  29. package/templates/qoder/scripts/common/cmd_registry.py +112 -0
  30. package/templates/qoder/scripts/common/config.py +187 -0
  31. package/templates/qoder/scripts/common/contract.py +317 -0
  32. package/templates/qoder/scripts/common/developer.py +2 -1
  33. package/templates/qoder/scripts/common/feishu.py +10 -9
  34. package/templates/qoder/scripts/common/guard.py +159 -0
  35. package/templates/qoder/scripts/common/identity.py +121 -2
  36. package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
  37. package/templates/qoder/scripts/common/mcp_base.py +268 -0
  38. package/templates/qoder/scripts/common/paths.py +187 -1
  39. package/templates/qoder/scripts/common/result.py +223 -0
  40. package/templates/qoder/scripts/common/roles.py +60 -0
  41. package/templates/qoder/scripts/common/task_utils.py +21 -9
  42. package/templates/qoder/scripts/common/test_extract.py +115 -0
  43. package/templates/qoder/scripts/kg/__init__.py +11 -0
  44. package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
  45. package/templates/qoder/scripts/kg/build_relations.py +127 -0
  46. package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
  47. package/templates/qoder/scripts/kg/build_workflows.py +144 -0
  48. package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
  49. package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
  50. package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
  51. package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
  52. package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
  53. package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
  54. package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
  55. package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
  56. package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
  57. package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
  58. package/templates/qoder/scripts/kg/prefetch.py +359 -0
  59. package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
  60. package/templates/qoder/scripts/mcp/__init__.py +11 -0
  61. package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
  62. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
  63. package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
  64. package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
  65. package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
  66. package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
  67. package/templates/qoder/scripts/report/__init__.py +11 -0
  68. package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
  69. package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
  70. package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
  71. package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
  72. package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
  73. package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
  74. package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
  75. package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
  76. package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
  77. package/templates/qoder/scripts/report/req.py +222 -0
  78. package/templates/qoder/scripts/report/role.py +33 -0
  79. package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
  80. package/templates/qoder/scripts/setup/__init__.py +11 -0
  81. package/templates/qoder/scripts/setup/carriers.py +662 -0
  82. package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
  83. package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
  84. package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
  85. package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
  86. package/templates/qoder/scripts/{setup.py → setup/setup.py} +147 -0
  87. package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
  88. package/templates/qoder/scripts/task/__init__.py +11 -0
  89. package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
  90. package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
  91. package/templates/qoder/scripts/task/task.py +221 -0
  92. package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
  93. package/templates/qoder/scripts/task/task_query.py +161 -0
  94. package/templates/qoder/scripts/task/task_relations.py +424 -0
  95. package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
  96. package/templates/qoder/scripts/test/__init__.py +11 -0
  97. package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
  98. package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
  99. package/templates/qoder/scripts/test/autotest_data.py +675 -0
  100. package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
  101. package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
  102. package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
  103. package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
  104. package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
  105. package/templates/qoder/scripts/wlkj.py +116 -0
  106. package/templates/qoder/settings.json +1 -10
  107. package/templates/qoder/skills/design-import/SKILL.md +226 -226
  108. package/templates/qoder/skills/design-review/SKILL.md +82 -82
  109. package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
  110. package/templates/qoder/skills/prd-review/SKILL.md +5 -5
  111. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
  112. package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
  113. package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
  114. package/templates/qoder/skills/test-generator/SKILL.md +5 -5
  115. package/templates/qoder/skills/wl-code/SKILL.md +4 -4
  116. package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
  117. package/templates/qoder/skills/wl-design/SKILL.md +3 -3
  118. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  119. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  120. package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
  121. package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
  122. package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
  123. package/templates/qoder/skills/wl-report/SKILL.md +7 -7
  124. package/templates/qoder/skills/wl-search/SKILL.md +13 -13
  125. package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
  126. package/templates/qoder/skills/wl-status/SKILL.md +5 -5
  127. package/templates/qoder/skills/wl-task/SKILL.md +6 -6
  128. package/templates/qoder/skills/wl-test/SKILL.md +102 -39
  129. package/templates/root/AGENTS.md +39 -40
  130. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  131. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  132. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  133. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  135. package/templates/qoder/scripts/check_carriers.py +0 -238
  136. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  137. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  138. package/templates/qoder/scripts/collect_prds.py +0 -31
  139. package/templates/qoder/scripts/common/mentions.py +0 -134
  140. package/templates/qoder/scripts/common/utf8.py +0 -38
  141. package/templates/qoder/scripts/extract_routes.py +0 -54
  142. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  143. package/templates/qoder/scripts/handoff.py +0 -22
  144. package/templates/qoder/scripts/init_developer.py +0 -76
  145. package/templates/qoder/scripts/parse_prds.py +0 -33
  146. package/templates/qoder/scripts/role.py +0 -51
  147. package/templates/qoder/scripts/sync_carriers.py +0 -259
  148. package/templates/qoder/scripts/task.py +0 -1261
  149. package/templates/qoder/scripts/workspace_init.py +0 -102
  150. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  151. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  152. /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
@@ -1,201 +1,207 @@
1
- # -*- coding: utf-8 -*-
2
- """learn_aggregate.py - 团队学习聚合 (同构 kg_build.py)
3
-
4
- 扫所有人的 workspace/members/{dev}/journal/feedback.jsonl
5
- 增量写入 kg.duckdb learning_events (团队单一真源)
6
-
7
- kg_build.py 对称:
8
- kg_build.py: 扫 data/code/* kg.duckdb (代码图谱)
9
- learn_aggregate.py: 扫 workspace/members/*/journal → 写 kg.duckdb (学习图谱)
10
-
11
- 增量策略 (复用 build_meta):
12
- 记录上次聚合时间, 只处理 mtime > last 的文件。全量重建用 --rebuild。
13
-
14
- 用法:
15
- python learn_aggregate.py # 增量
16
- python learn_aggregate.py --rebuild # 全量重建
17
- """
18
- import os
19
- import sys
20
- import json
21
- import hashlib
22
- import time
23
- from datetime import datetime
24
-
25
- SCRIPTS = os.path.dirname(os.path.abspath(__file__))
26
- sys.path.insert(0, SCRIPTS)
27
-
28
- from common.paths import MEMBERS_DIR
29
- from kg_duckdb import get_db, init_schema
30
-
31
- BUILD_META_KEY = 'learning_last_aggregate'
32
-
33
-
34
- def _collect_journal_files():
35
- """扫 workspace/members/*/journal/feedback.jsonl, 返回 [(dev, path)]。"""
36
- results = []
37
- if not MEMBERS_DIR.is_dir():
38
- return results
39
- for dev_dir in sorted(MEMBERS_DIR.iterdir()):
40
- if not dev_dir.is_dir():
41
- continue
42
- fb = dev_dir / 'journal' / 'feedback.jsonl'
43
- if fb.is_file():
44
- results.append((dev_dir.name, fb))
45
- return results
46
-
47
-
48
- def _event_id(entry):
49
- """生成事件唯一 ID (去重用): hash(ts+dev+event+data)。"""
50
- raw = entry.get('ts', '') + '|' + entry.get('dev', '') + '|' + \
51
- entry.get('event', '') + '|' + json.dumps(entry.get('data', {}), sort_keys=True)
52
- return hashlib.md5(raw.encode('utf-8')).hexdigest()[:16]
53
-
54
-
55
- def _read_events(dev, path):
56
- """读一个开发者的 feedback.jsonl, 返回事件列表。容错: 跳过坏行。"""
57
- events = []
58
- try:
59
- with open(path, encoding='utf-8') as f:
60
- for line in f:
61
- line = line.strip()
62
- if not line:
63
- continue
64
- try:
65
- e = json.loads(line)
66
- e.setdefault('dev', dev)
67
- events.append(e)
68
- except json.JSONDecodeError:
69
- continue # 跳过坏行, 不中断
70
- except (OSError, IOError):
71
- pass
72
- return events
73
-
74
-
75
- def aggregate(rebuild=False):
76
- """聚合所有开发者的 feedback → kg.duckdb。
77
- 返回写入的事件数。
78
- 增量: 只处理 mtime > last_aggregate 的文件。
79
- """
80
- con = get_db()
81
- init_schema(con)
82
-
83
- # 读上次聚合时间 (增量依据)
84
- last_ts = None
85
- if not rebuild:
86
- row = con.execute(
87
- "SELECT value FROM build_meta WHERE key = ?", [BUILD_META_KEY]
88
- ).fetchone()
89
- if row:
90
- try:
91
- last_ts = float(row[0])
92
- except (ValueError, TypeError):
93
- last_ts = None
94
-
95
- files = _collect_journal_files()
96
- if rebuild:
97
- con.execute("DELETE FROM learning_events")
98
- to_process = files
99
- else:
100
- to_process = [(d, p) for d, p in files
101
- if last_ts is None or p.stat().st_mtime > last_ts]
102
-
103
- if not to_process:
104
- print('无新增事件 (上次聚合后无变化)。')
105
- _refresh_stats(con)
106
- _update_meta(con)
107
- return 0
108
-
109
- # 收集所有事件 + 去重 (同 event_id 只写一次)
110
- seen_ids = set()
111
- if not rebuild:
112
- existing = con.execute("SELECT id FROM learning_events").fetchall()
113
- seen_ids = {r[0] for r in existing}
114
-
115
- rows = []
116
- total_events = 0
117
- for dev, path in to_process:
118
- for e in _read_events(dev, path):
119
- eid = _event_id(e)
120
- if eid in seen_ids:
121
- continue
122
- seen_ids.add(eid)
123
- rows.append((
124
- eid,
125
- e.get('ts', ''),
126
- e.get('dev', dev),
127
- e.get('event', ''),
128
- json.dumps(e.get('data', {}), ensure_ascii=False),
129
- ))
130
- total_events += 1
131
-
132
- # 批量写入 (DuckDB executemany)
133
- if rows:
134
- con.executemany(
135
- "INSERT OR IGNORE INTO learning_events(id, ts, dev, event, data) VALUES (?,?,?,?,?)",
136
- rows,
137
- )
138
-
139
- _refresh_stats(con)
140
- _update_meta(con)
141
- print('聚合完成: %d 条事件, 来自 %d 个开发者 (%d 个文件)。'
142
- % (total_events, len({d for d, _ in to_process}), len(to_process)))
143
- return total_events
144
-
145
-
146
- def _refresh_stats(con):
147
- """刷新 learning_stats ( learning_events 重新统计每人计数)。
148
-
149
- 事件→列映射来自 common/events.py (SSOT)。新增埋点只要在 events.py 注册,
150
- 这里自动生效, 不用改 SQL (修旧版硬编码漂移 bug)。
151
- """
152
- from common.events import event_to_sql_filter
153
- con.execute("DELETE FROM learning_stats")
154
- # 列名 = events.py 的 stat 值; 动态拼 FILTER 子句
155
- cols = event_to_sql_filter() # {stat_column: event}
156
- col_names = list(cols.keys())
157
- select_cols = ', '.join(
158
- "COUNT(*) FILTER (WHERE event = '%s') AS %s" % (cols[c], c)
159
- for c in col_names
160
- )
161
- col_list = ', '.join(col_names)
162
- con.execute("""
163
- INSERT INTO learning_stats (dev, %s, updated_at)
164
- SELECT dev, %s, ?
165
- FROM learning_events
166
- GROUP BY dev
167
- """ % (col_list, select_cols), [datetime.now().isoformat(timespec='seconds')])
168
-
169
-
170
- def _update_meta(con):
171
- """记录本次聚合时间。"""
172
- con.execute(
173
- "INSERT OR REPLACE INTO build_meta(key, value) VALUES (?, ?)",
174
- [BUILD_META_KEY, str(time.time())],
175
- )
176
-
177
-
178
- def query_summary():
179
- """查询团队学习摘要 ( /wl-status )"""
180
- con = get_db()
181
- init_schema(con)
182
- total = con.execute("SELECT COUNT(*) FROM learning_events").fetchone()[0]
183
- by_event = con.execute("""
184
- SELECT event, COUNT(*) FROM learning_events GROUP BY event ORDER BY COUNT(*) DESC
185
- """).fetchall()
186
- by_dev = con.execute("""
187
- SELECT dev, COUNT(*) FROM learning_events GROUP BY dev ORDER BY COUNT(*) DESC
188
- """).fetchall()
189
- return {'total': total, 'by_event': by_event, 'by_dev': by_dev}
190
-
191
-
192
- if __name__ == '__main__':
193
- rebuild = '--rebuild' in sys.argv
194
- n = aggregate(rebuild=rebuild)
195
- s = query_summary()
196
- print('\n团队学习摘要:')
197
- print(' 总事件: %d' % s['total'])
198
- for ev, cnt in s['by_event']:
199
- print(' %s: %d' % (ev, cnt))
200
- for dev, cnt in s['by_dev']:
201
- print(' %s: %d 条' % (dev, cnt))
1
+ # -*- coding: utf-8 -*-
2
+ """learn_aggregate.py - 团队学习聚合 (同构 kg_build.py)
3
+
4
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
5
+ import os as _o, sys as _s
6
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
7
+ if _cp not in _s.path: _s.path.insert(0, _cp)
8
+ from bootstrap import setup; setup()
9
+
10
+ 扫所有人的 workspace/members/{dev}/journal/feedback.jsonl
11
+ 增量写入 kg.duckdb 的 learning_events 表 (团队单一真源)
12
+
13
+ 和 kg_build.py 对称:
14
+ kg_build.py: 扫 data/code/* → 写 kg.duckdb (代码图谱)
15
+ learn_aggregate.py: 扫 workspace/members/*/journal → 写 kg.duckdb (学习图谱)
16
+
17
+ 增量策略 (复用 build_meta):
18
+ 记录上次聚合时间, 只处理 mtime > last 的文件。全量重建用 --rebuild。
19
+
20
+ 用法:
21
+ python learn_aggregate.py # 增量
22
+ python learn_aggregate.py --rebuild # 全量重建
23
+ """
24
+ import os
25
+ import sys
26
+ import json
27
+ import hashlib
28
+ import time
29
+ from datetime import datetime
30
+
31
+ SCRIPTS = os.path.dirname(os.path.abspath(__file__))
32
+ sys.path.insert(0, SCRIPTS)
33
+
34
+ from common.paths import MEMBERS_DIR
35
+ from kg.kg_duckdb import get_db, init_schema # v3.0: kg_duckdb 已迁入 kg/ 子包
36
+
37
+ BUILD_META_KEY = 'learning_last_aggregate'
38
+
39
+
40
+ def _collect_journal_files():
41
+ """扫 workspace/members/*/journal/feedback.jsonl, 返回 [(dev, path)]。"""
42
+ results = []
43
+ if not MEMBERS_DIR.is_dir():
44
+ return results
45
+ for dev_dir in sorted(MEMBERS_DIR.iterdir()):
46
+ if not dev_dir.is_dir():
47
+ continue
48
+ fb = dev_dir / 'journal' / 'feedback.jsonl'
49
+ if fb.is_file():
50
+ results.append((dev_dir.name, fb))
51
+ return results
52
+
53
+
54
+ def _event_id(entry):
55
+ """生成事件唯一 ID (去重用): hash(ts+dev+event+data)。"""
56
+ raw = entry.get('ts', '') + '|' + entry.get('dev', '') + '|' + \
57
+ entry.get('event', '') + '|' + json.dumps(entry.get('data', {}), sort_keys=True)
58
+ return hashlib.md5(raw.encode('utf-8')).hexdigest()[:16]
59
+
60
+
61
+ def _read_events(dev, path):
62
+ """读一个开发者的 feedback.jsonl, 返回事件列表。容错: 跳过坏行。"""
63
+ events = []
64
+ try:
65
+ with open(path, encoding='utf-8') as f:
66
+ for line in f:
67
+ line = line.strip()
68
+ if not line:
69
+ continue
70
+ try:
71
+ e = json.loads(line)
72
+ e.setdefault('dev', dev)
73
+ events.append(e)
74
+ except json.JSONDecodeError:
75
+ continue # 跳过坏行, 不中断
76
+ except (OSError, IOError):
77
+ pass
78
+ return events
79
+
80
+
81
+ def aggregate(rebuild=False):
82
+ """聚合所有开发者的 feedback → kg.duckdb。
83
+ 返回写入的事件数。
84
+ 增量: 只处理 mtime > last_aggregate 的文件。
85
+ """
86
+ con = get_db()
87
+ init_schema(con)
88
+
89
+ # 读上次聚合时间 (增量依据)
90
+ last_ts = None
91
+ if not rebuild:
92
+ row = con.execute(
93
+ "SELECT value FROM build_meta WHERE key = ?", [BUILD_META_KEY]
94
+ ).fetchone()
95
+ if row:
96
+ try:
97
+ last_ts = float(row[0])
98
+ except (ValueError, TypeError):
99
+ last_ts = None
100
+
101
+ files = _collect_journal_files()
102
+ if rebuild:
103
+ con.execute("DELETE FROM learning_events")
104
+ to_process = files
105
+ else:
106
+ to_process = [(d, p) for d, p in files
107
+ if last_ts is None or p.stat().st_mtime > last_ts]
108
+
109
+ if not to_process:
110
+ print('无新增事件 (上次聚合后无变化)。')
111
+ _refresh_stats(con)
112
+ _update_meta(con)
113
+ return 0
114
+
115
+ # 收集所有事件 + 去重 (同 event_id 只写一次)
116
+ seen_ids = set()
117
+ if not rebuild:
118
+ existing = con.execute("SELECT id FROM learning_events").fetchall()
119
+ seen_ids = {r[0] for r in existing}
120
+
121
+ rows = []
122
+ total_events = 0
123
+ for dev, path in to_process:
124
+ for e in _read_events(dev, path):
125
+ eid = _event_id(e)
126
+ if eid in seen_ids:
127
+ continue
128
+ seen_ids.add(eid)
129
+ rows.append((
130
+ eid,
131
+ e.get('ts', ''),
132
+ e.get('dev', dev),
133
+ e.get('event', ''),
134
+ json.dumps(e.get('data', {}), ensure_ascii=False),
135
+ ))
136
+ total_events += 1
137
+
138
+ # 批量写入 (DuckDB executemany)
139
+ if rows:
140
+ con.executemany(
141
+ "INSERT OR IGNORE INTO learning_events(id, ts, dev, event, data) VALUES (?,?,?,?,?)",
142
+ rows,
143
+ )
144
+
145
+ _refresh_stats(con)
146
+ _update_meta(con)
147
+ print('聚合完成: %d 条事件, 来自 %d 个开发者 (%d 个文件)。'
148
+ % (total_events, len({d for d, _ in to_process}), len(to_process)))
149
+ return total_events
150
+
151
+
152
+ def _refresh_stats(con):
153
+ """刷新 learning_stats 表 (从 learning_events 重新统计每人计数)
154
+
155
+ 事件→列映射来自 common/events.py (SSOT)。新增埋点只要在 events.py 注册,
156
+ 这里自动生效, 不用改 SQL (修旧版硬编码漂移 bug)
157
+ """
158
+ from common.events import event_to_sql_filter
159
+ con.execute("DELETE FROM learning_stats")
160
+ # 列名 = events.py 的 stat 值; 动态拼 FILTER 子句
161
+ cols = event_to_sql_filter() # {stat_column: event}
162
+ col_names = list(cols.keys())
163
+ select_cols = ', '.join(
164
+ "COUNT(*) FILTER (WHERE event = '%s') AS %s" % (cols[c], c)
165
+ for c in col_names
166
+ )
167
+ col_list = ', '.join(col_names)
168
+ con.execute("""
169
+ INSERT INTO learning_stats (dev, %s, updated_at)
170
+ SELECT dev, %s, ?
171
+ FROM learning_events
172
+ GROUP BY dev
173
+ """ % (col_list, select_cols), [datetime.now().isoformat(timespec='seconds')])
174
+
175
+
176
+ def _update_meta(con):
177
+ """记录本次聚合时间。"""
178
+ con.execute(
179
+ "INSERT OR REPLACE INTO build_meta(key, value) VALUES (?, ?)",
180
+ [BUILD_META_KEY, str(time.time())],
181
+ )
182
+
183
+
184
+ def query_summary():
185
+ """查询团队学习摘要 (给 /wl-status 用)。"""
186
+ con = get_db()
187
+ init_schema(con)
188
+ total = con.execute("SELECT COUNT(*) FROM learning_events").fetchone()[0]
189
+ by_event = con.execute("""
190
+ SELECT event, COUNT(*) FROM learning_events GROUP BY event ORDER BY COUNT(*) DESC
191
+ """).fetchall()
192
+ by_dev = con.execute("""
193
+ SELECT dev, COUNT(*) FROM learning_events GROUP BY dev ORDER BY COUNT(*) DESC
194
+ """).fetchall()
195
+ return {'total': total, 'by_event': by_event, 'by_dev': by_dev}
196
+
197
+
198
+ if __name__ == '__main__':
199
+ rebuild = '--rebuild' in sys.argv
200
+ n = aggregate(rebuild=rebuild)
201
+ s = query_summary()
202
+ print('\n团队学习摘要:')
203
+ print(' 总事件: %d' % s['total'])
204
+ for ev, cnt in s['by_event']:
205
+ print(' %s: %d' % (ev, cnt))
206
+ for dev, cnt in s['by_dev']:
207
+ print(' %s: %d 条' % (dev, cnt))