@hupan56/wlkj 2.6.0 → 2.7.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 (125) hide show
  1. package/bin/cli.js +261 -11
  2. package/package.json +1 -1
  3. package/templates/qoder/agents/insight-planning.md +67 -0
  4. package/templates/qoder/agents/insight-research.md +61 -0
  5. package/templates/qoder/agents/prd-reference.md +8 -2
  6. package/templates/qoder/commands/optional/wl-insight.md +275 -0
  7. package/templates/qoder/commands/{wl-report.md → optional/wl-report.md} +13 -5
  8. package/templates/qoder/commands/{wl-spec.md → optional/wl-spec.md} +1 -1
  9. package/templates/qoder/commands/{wl-status.md → optional/wl-status.md} +28 -2
  10. package/templates/qoder/commands/wl-code.md +10 -2
  11. package/templates/qoder/commands/wl-commit.md +1 -1
  12. package/templates/qoder/commands/wl-design-draw.md +78 -0
  13. package/templates/qoder/commands/wl-design-scan.md +108 -0
  14. package/templates/qoder/commands/wl-design-spec.md +154 -0
  15. package/templates/qoder/commands/wl-design.md +32 -0
  16. package/templates/qoder/commands/wl-init.md +24 -3
  17. package/templates/qoder/commands/wl-prd-full.md +226 -0
  18. package/templates/qoder/commands/wl-prd-quick.md +134 -0
  19. package/templates/qoder/commands/wl-prd-review.md +104 -0
  20. package/templates/qoder/commands/wl-prd.md +17 -311
  21. package/templates/qoder/commands/wl-search.md +66 -30
  22. package/templates/qoder/commands/wl-task.md +290 -59
  23. package/templates/qoder/commands/wl-test.md +92 -24
  24. package/templates/qoder/config.yaml +51 -15
  25. package/templates/qoder/hooks/inject-workflow-state.py +2 -2
  26. package/templates/qoder/hooks/session-start.py +82 -51
  27. package/templates/qoder/rules/wl-pipeline.md +216 -105
  28. package/templates/qoder/scripts/__pycache__/search_index.cpython-39.pyc +0 -0
  29. package/templates/qoder/scripts/archive_prd.py +377 -0
  30. package/templates/qoder/scripts/autotest.py +1715 -0
  31. package/templates/qoder/scripts/autotest_batch.py +224 -0
  32. package/templates/qoder/scripts/autotest_run.py +297 -0
  33. package/templates/qoder/scripts/benchmark.py +210 -209
  34. package/templates/qoder/scripts/build_style_index.py +444 -4
  35. package/templates/qoder/scripts/check_carriers.py +238 -0
  36. package/templates/qoder/scripts/check_mcp.py +298 -0
  37. package/templates/qoder/scripts/check_qoderwork_consistency.py +166 -0
  38. package/templates/qoder/scripts/common/events.py +46 -0
  39. package/templates/qoder/scripts/common/extract.py +419 -0
  40. package/templates/qoder/scripts/common/graph_traverse.py +533 -0
  41. package/templates/qoder/scripts/common/paths.py +89 -0
  42. package/templates/qoder/scripts/common/pip_install.py +144 -0
  43. package/templates/qoder/scripts/common/platform_guard.py +61 -0
  44. package/templates/qoder/scripts/common/search_engine.py +205 -205
  45. package/templates/qoder/scripts/common/terms.py +57 -0
  46. package/templates/qoder/scripts/common/ts_extract.py +536 -0
  47. package/templates/qoder/scripts/context_pack.py +73 -13
  48. package/templates/qoder/scripts/enrich_prompt.py +226 -0
  49. package/templates/qoder/scripts/eval_prd.py +318 -231
  50. package/templates/qoder/scripts/export.py +487 -487
  51. package/templates/qoder/scripts/extract_api_params.py +246 -0
  52. package/templates/qoder/scripts/extract_routes.py +54 -0
  53. package/templates/qoder/scripts/extract_routes_tree.py +78 -0
  54. package/templates/qoder/scripts/fill_prototype.py +707 -0
  55. package/templates/qoder/scripts/gen_design_doc.py +394 -0
  56. package/templates/qoder/scripts/git_sync.py +27 -15
  57. package/templates/qoder/scripts/init_doctor.py +292 -40
  58. package/templates/qoder/scripts/install_qoderwork.py +366 -9
  59. package/templates/qoder/scripts/kg.py +708 -0
  60. package/templates/qoder/scripts/kg_auto_login.py +196 -0
  61. package/templates/qoder/scripts/kg_build.py +612 -0
  62. package/templates/qoder/scripts/kg_build_db.py +327 -0
  63. package/templates/qoder/scripts/kg_duckdb.py +549 -0
  64. package/templates/qoder/scripts/kg_incremental.py +393 -0
  65. package/templates/qoder/scripts/kg_link_db.py +224 -0
  66. package/templates/qoder/scripts/kg_mcp_server.py +801 -0
  67. package/templates/qoder/scripts/kg_semantic.py +150 -0
  68. package/templates/qoder/scripts/kg_test_runner.py +241 -0
  69. package/templates/qoder/scripts/lanhu_stdio_wrapper.py +119 -0
  70. package/templates/qoder/scripts/learn.py +118 -39
  71. package/templates/qoder/scripts/learn_aggregate.py +201 -0
  72. package/templates/qoder/scripts/mcp_launcher.py +359 -0
  73. package/templates/qoder/scripts/mysql_mcp_server.py +396 -0
  74. package/templates/qoder/scripts/repo_root.py +106 -0
  75. package/templates/qoder/scripts/role.py +12 -0
  76. package/templates/qoder/scripts/run_weekly_update.bat +5 -0
  77. package/templates/qoder/scripts/run_weekly_update.sh +5 -0
  78. package/templates/qoder/scripts/search_index.py +307 -60
  79. package/templates/qoder/scripts/secure-ls.js +5640 -0
  80. package/templates/qoder/scripts/setup.py +706 -641
  81. package/templates/qoder/scripts/setup_lanhu.py +963 -0
  82. package/templates/qoder/scripts/status.py +250 -11
  83. package/templates/qoder/scripts/sync_carriers.py +259 -0
  84. package/templates/qoder/scripts/syncgate.py +2 -2
  85. package/templates/qoder/scripts/task.py +75 -0
  86. package/templates/qoder/scripts/team_sync.py +60 -4
  87. package/templates/qoder/scripts/workspace_init.py +1 -1
  88. package/templates/qoder/skills/design-import/SKILL.md +226 -0
  89. package/templates/qoder/skills/design-import/figma-workflow.md +81 -0
  90. package/templates/qoder/skills/design-review/SKILL.md +82 -25
  91. package/templates/qoder/skills/prd-generator/SKILL.md +184 -60
  92. package/templates/qoder/skills/prd-review/SKILL.md +18 -1
  93. package/templates/qoder/skills/prompt-enrich/SKILL.md +90 -0
  94. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -141
  95. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  96. package/templates/qoder/skills/spec-coder/SKILL.md +18 -1
  97. package/templates/qoder/skills/spec-generator/SKILL.md +18 -1
  98. package/templates/qoder/skills/test-generator/SKILL.md +15 -2
  99. package/templates/qoder/skills/wl-code/SKILL.md +55 -36
  100. package/templates/qoder/skills/wl-commit/SKILL.md +89 -76
  101. package/templates/qoder/skills/wl-design/SKILL.md +55 -0
  102. package/templates/qoder/skills/wl-init/SKILL.md +76 -67
  103. package/templates/qoder/skills/wl-insight/SKILL.md +201 -81
  104. package/templates/qoder/skills/wl-prd-full/SKILL.md +69 -0
  105. package/templates/qoder/skills/wl-prd-quick/SKILL.md +49 -0
  106. package/templates/qoder/skills/wl-prd-review/SKILL.md +34 -0
  107. package/templates/qoder/skills/wl-report/SKILL.md +131 -107
  108. package/templates/qoder/skills/wl-search/SKILL.md +141 -75
  109. package/templates/qoder/skills/wl-spec/SKILL.md +49 -39
  110. package/templates/qoder/skills/wl-status/SKILL.md +83 -61
  111. package/templates/qoder/skills/wl-task/SKILL.md +132 -58
  112. package/templates/qoder/skills/wl-test/SKILL.md +406 -40
  113. package/templates/qoder/templates/prototype-app.html +13 -8
  114. package/templates/qoder/templates/prototype-web.html +376 -93
  115. package/templates/root/AGENTS.md +89 -34
  116. package/templates/root/requirements.txt +21 -0
  117. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +259 -259
  118. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -186
  119. package/templates/qoder/agents/prd-planning.md +0 -57
  120. package/templates/qoder/agents/prd-research.md +0 -34
  121. package/templates/qoder/commands/wl-insight.md +0 -51
  122. package/templates/qoder/scripts/__pycache__/init_doctor.cpython-39.pyc +0 -0
  123. package/templates/qoder/scripts/__pycache__/setup.cpython-39.pyc +0 -0
  124. package/templates/qoder/scripts/common/__pycache__/developer.cpython-39.pyc +0 -0
  125. package/templates/qoder/skills/wl-prd/SKILL.md +0 -92
@@ -1,67 +1,146 @@
1
- # learn.py - Learning engine for QODER evolution
2
- import os, json
1
+ # -*- coding: utf-8 -*-
2
+ """learn.py - Learning engine: 个人埋点 (workspace/members/{dev}/journal/)
3
+
4
+ 数据归属 (和 kg.duckdb 对称的设计):
5
+ - 原始事件流 → workspace/members/{dev}/journal/feedback.jsonl (个人, 进git, 按人分不冲突)
6
+ - 统计计数 → workspace/members/{dev}/journal/learning.yaml (个人, 进git)
7
+ - 团队聚合 → data/index/kg.duckdb 的 learning_events 表 (管理员跑 learn_aggregate.py 聚合)
8
+
9
+ 为什么按人分文件:
10
+ team_sync 用单分支 rebase 协作, 所有人 append 同一个 feedback.jsonl 会冲突。
11
+ 每人写自己的文件 → rebase 时改的是不同文件 → git 自动合并, 零冲突。
12
+ (旧版把 feedback 放 .qoder/learning/ 并 gitignore, 导致数据不随团队同步 = 丢失)
13
+
14
+ 用法:
15
+ python learn.py record prd_accepted '{"req_id":"REQ-2026-009","score":87}'
16
+ python learn.py status
17
+ python learn.py aggregate # 管理员: 扫所有人 → kg.duckdb
18
+ """
19
+ import os
20
+ import sys
21
+ import json
3
22
  from datetime import datetime
4
23
 
5
- BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
6
- LEARNING_DIR = os.path.join(BASE, '.qoder', 'learning')
7
- FEEDBACK_FILE = os.path.join(LEARNING_DIR, 'feedback.jsonl')
8
- PREFS_FILE = os.path.join(LEARNING_DIR, 'preferences.yaml')
24
+ SCRIPTS = os.path.dirname(os.path.abspath(__file__))
25
+ sys.path.insert(0, SCRIPTS)
26
+
27
+ from common.paths import get_developer, get_developer_journal_dir
28
+
9
29
 
10
- def record_feedback(event, data):
30
+ def _feedback_path():
31
+ """当前开发者的 feedback 文件 (workspace/members/{dev}/journal/feedback.jsonl)。"""
32
+ dev = get_developer()
33
+ if not dev:
34
+ raise RuntimeError('未初始化开发者身份, 请先运行 /wl-init')
35
+ return get_developer_journal_dir(dev) / 'feedback.jsonl'
36
+
37
+
38
+ def _prefs_path():
39
+ """当前开发者的统计文件 (workspace/members/{dev}/journal/learning.yaml)。"""
40
+ dev = get_developer()
41
+ if not dev:
42
+ raise RuntimeError('未初始化开发者身份, 请先运行 /wl-init')
43
+ return get_developer_journal_dir(dev) / 'learning.yaml'
44
+
45
+
46
+ def record_feedback(event, data=None):
47
+ """记录一条事件到当前开发者的 feedback.jsonl。
48
+ 每条带 dev 字段 (聚合时区分来源)。
49
+ """
50
+ dev = get_developer()
51
+ if not dev:
52
+ raise RuntimeError('未初始化开发者身份, 请先运行 /wl-init')
53
+ if data is None:
54
+ data = {}
11
55
  entry = {
12
- 'ts': datetime.now().isoformat(),
56
+ 'ts': datetime.now().isoformat(timespec='seconds'),
57
+ 'dev': dev,
13
58
  'event': event,
14
- 'data': data
59
+ 'data': data,
15
60
  }
16
- with open(FEEDBACK_FILE, 'a', encoding='utf-8') as f:
17
- f.write(json.dumps(entry, ensure_ascii=False) + chr(10))
61
+ fb_path = _feedback_path()
62
+ fb_path.parent.mkdir(parents=True, exist_ok=True)
63
+ with open(fb_path, 'a', encoding='utf-8') as f:
64
+ f.write(json.dumps(entry, ensure_ascii=False) + '\n')
18
65
  _update_stats(event)
19
- print('Recorded: ' + event)
66
+ print('Recorded: %s → %s' % (event, fb_path.name))
67
+
20
68
 
21
69
  def _update_stats(event):
22
- import yaml
70
+ """更新当前开发者的统计计数 (learning.yaml)。
71
+ 事件→列名映射来自 common/events.py (SSOT), 不再硬编码。
72
+ """
73
+ try:
74
+ import yaml
75
+ except ImportError:
76
+ return # 无 yaml 库则跳过统计, 不阻塞埋点
77
+ from common.events import stats_event_map
78
+ key = stats_event_map().get(event)
79
+ if not key:
80
+ return
23
81
  prefs = _load_prefs()
24
- stats = {
25
- 'prd_accepted': 'prds_generated',
26
- 'prd_rejected': 'rejections',
27
- 'review_done': 'reviews_done',
28
- 'task_completed': 'tasks_completed',
29
- 'handoff_done': 'handoffs_completed',
30
- }
31
- key = stats.get(event)
32
- if key:
33
- prefs[key] = prefs.get(key, 0) + 1
34
- _save_prefs(prefs)
82
+ prefs[key] = prefs.get(key, 0) + 1
83
+ _save_prefs(prefs)
84
+
35
85
 
36
86
  def _load_prefs():
37
- import yaml
38
- if not os.path.isfile(PREFS_FILE): return {}
39
- try: return yaml.safe_load(open(PREFS_FILE, encoding='utf-8')) or {}
40
- except: return {}
87
+ try:
88
+ import yaml
89
+ except ImportError:
90
+ return {}
91
+ p = _prefs_path()
92
+ if not p.is_file():
93
+ return {}
94
+ try:
95
+ return yaml.safe_load(p.read_text(encoding='utf-8')) or {}
96
+ except Exception:
97
+ return {}
98
+
41
99
 
42
100
  def _save_prefs(prefs):
43
- import yaml
44
- with open(PREFS_FILE, 'w', encoding='utf-8') as f:
101
+ try:
102
+ import yaml
103
+ except ImportError:
104
+ return
105
+ p = _prefs_path()
106
+ p.parent.mkdir(parents=True, exist_ok=True)
107
+ with open(p, 'w', encoding='utf-8') as f:
45
108
  yaml.dump(prefs, f, allow_unicode=True, default_flow_style=False)
46
109
 
110
+
47
111
  def get_prefs():
48
112
  return _load_prefs()
49
113
 
114
+
50
115
  def should_evolve():
51
- prefs = _load_prefs()
52
- tc = prefs.get('tasks_completed', 0)
53
- return tc > 0 and tc % 10 == 0
116
+ """是否到了该提炼模式的阈值 (每完成 10 个任务)。"""
117
+ return _load_prefs().get('tasks_completed', 0) % 10 == 0
118
+
119
+
120
+ def aggregate_all():
121
+ """管理员聚合: 扫所有人的 feedback.jsonl → 写入 kg.duckdb。
122
+ 委托给 learn_aggregate.py (同构 kg_build.py)。
123
+ """
124
+ import learn_aggregate
125
+ return learn_aggregate.aggregate()
126
+
54
127
 
55
128
  if __name__ == '__main__':
56
- import sys
57
129
  if len(sys.argv) < 2:
58
- print('Usage: learn.py record <event> [json] | learn.py status')
130
+ print(__doc__)
131
+ print('用法: learn.py record <event> [json] | learn.py status | learn.py aggregate')
59
132
  elif sys.argv[1] == 'status':
60
133
  prefs = get_prefs()
61
- print('Learning Stats:')
134
+ print('Learning Stats (%s):' % (get_developer() or 'unknown'))
62
135
  for k, v in prefs.items():
63
- if isinstance(v, int): print(' ' + k + ': ' + str(v))
64
- print(' Evolve ready: ' + str(should_evolve()))
136
+ if isinstance(v, int):
137
+ print(' %s: %d' % (k, v))
138
+ print(' Evolve ready: %s' % should_evolve())
139
+ elif sys.argv[1] == 'aggregate':
140
+ n = aggregate_all()
141
+ print('聚合完成: %d 条事件 → kg.duckdb' % n)
65
142
  elif sys.argv[1] == 'record' and len(sys.argv) >= 3:
66
143
  data = json.loads(sys.argv[3]) if len(sys.argv) > 3 else {}
67
- record_feedback(sys.argv[2], data)
144
+ record_feedback(sys.argv[2], data)
145
+ else:
146
+ print('未知子命令: %s' % sys.argv[1])
@@ -0,0 +1,201 @@
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))