@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.
- package/bin/cli.js +261 -11
- package/package.json +1 -1
- package/templates/qoder/agents/insight-planning.md +67 -0
- package/templates/qoder/agents/insight-research.md +61 -0
- package/templates/qoder/agents/prd-reference.md +8 -2
- package/templates/qoder/commands/optional/wl-insight.md +275 -0
- package/templates/qoder/commands/{wl-report.md → optional/wl-report.md} +13 -5
- package/templates/qoder/commands/{wl-spec.md → optional/wl-spec.md} +1 -1
- package/templates/qoder/commands/{wl-status.md → optional/wl-status.md} +28 -2
- package/templates/qoder/commands/wl-code.md +10 -2
- package/templates/qoder/commands/wl-commit.md +1 -1
- package/templates/qoder/commands/wl-design-draw.md +78 -0
- package/templates/qoder/commands/wl-design-scan.md +108 -0
- package/templates/qoder/commands/wl-design-spec.md +154 -0
- package/templates/qoder/commands/wl-design.md +32 -0
- package/templates/qoder/commands/wl-init.md +24 -3
- package/templates/qoder/commands/wl-prd-full.md +226 -0
- package/templates/qoder/commands/wl-prd-quick.md +134 -0
- package/templates/qoder/commands/wl-prd-review.md +104 -0
- package/templates/qoder/commands/wl-prd.md +17 -311
- package/templates/qoder/commands/wl-search.md +66 -30
- package/templates/qoder/commands/wl-task.md +290 -59
- package/templates/qoder/commands/wl-test.md +92 -24
- package/templates/qoder/config.yaml +51 -15
- package/templates/qoder/hooks/inject-workflow-state.py +2 -2
- package/templates/qoder/hooks/session-start.py +82 -51
- package/templates/qoder/rules/wl-pipeline.md +216 -105
- package/templates/qoder/scripts/__pycache__/search_index.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/archive_prd.py +377 -0
- package/templates/qoder/scripts/autotest.py +1715 -0
- package/templates/qoder/scripts/autotest_batch.py +224 -0
- package/templates/qoder/scripts/autotest_run.py +297 -0
- package/templates/qoder/scripts/benchmark.py +210 -209
- package/templates/qoder/scripts/build_style_index.py +444 -4
- package/templates/qoder/scripts/check_carriers.py +238 -0
- package/templates/qoder/scripts/check_mcp.py +298 -0
- package/templates/qoder/scripts/check_qoderwork_consistency.py +166 -0
- package/templates/qoder/scripts/common/events.py +46 -0
- package/templates/qoder/scripts/common/extract.py +419 -0
- package/templates/qoder/scripts/common/graph_traverse.py +533 -0
- package/templates/qoder/scripts/common/paths.py +89 -0
- package/templates/qoder/scripts/common/pip_install.py +144 -0
- package/templates/qoder/scripts/common/platform_guard.py +61 -0
- package/templates/qoder/scripts/common/search_engine.py +205 -205
- package/templates/qoder/scripts/common/terms.py +57 -0
- package/templates/qoder/scripts/common/ts_extract.py +536 -0
- package/templates/qoder/scripts/context_pack.py +73 -13
- package/templates/qoder/scripts/enrich_prompt.py +226 -0
- package/templates/qoder/scripts/eval_prd.py +318 -231
- package/templates/qoder/scripts/export.py +487 -487
- package/templates/qoder/scripts/extract_api_params.py +246 -0
- package/templates/qoder/scripts/extract_routes.py +54 -0
- package/templates/qoder/scripts/extract_routes_tree.py +78 -0
- package/templates/qoder/scripts/fill_prototype.py +707 -0
- package/templates/qoder/scripts/gen_design_doc.py +394 -0
- package/templates/qoder/scripts/git_sync.py +27 -15
- package/templates/qoder/scripts/init_doctor.py +292 -40
- package/templates/qoder/scripts/install_qoderwork.py +366 -9
- package/templates/qoder/scripts/kg.py +708 -0
- package/templates/qoder/scripts/kg_auto_login.py +196 -0
- package/templates/qoder/scripts/kg_build.py +612 -0
- package/templates/qoder/scripts/kg_build_db.py +327 -0
- package/templates/qoder/scripts/kg_duckdb.py +549 -0
- package/templates/qoder/scripts/kg_incremental.py +393 -0
- package/templates/qoder/scripts/kg_link_db.py +224 -0
- package/templates/qoder/scripts/kg_mcp_server.py +801 -0
- package/templates/qoder/scripts/kg_semantic.py +150 -0
- package/templates/qoder/scripts/kg_test_runner.py +241 -0
- package/templates/qoder/scripts/lanhu_stdio_wrapper.py +119 -0
- package/templates/qoder/scripts/learn.py +118 -39
- package/templates/qoder/scripts/learn_aggregate.py +201 -0
- package/templates/qoder/scripts/mcp_launcher.py +359 -0
- package/templates/qoder/scripts/mysql_mcp_server.py +396 -0
- package/templates/qoder/scripts/repo_root.py +106 -0
- package/templates/qoder/scripts/role.py +12 -0
- package/templates/qoder/scripts/run_weekly_update.bat +5 -0
- package/templates/qoder/scripts/run_weekly_update.sh +5 -0
- package/templates/qoder/scripts/search_index.py +307 -60
- package/templates/qoder/scripts/secure-ls.js +5640 -0
- package/templates/qoder/scripts/setup.py +706 -641
- package/templates/qoder/scripts/setup_lanhu.py +963 -0
- package/templates/qoder/scripts/status.py +250 -11
- package/templates/qoder/scripts/sync_carriers.py +259 -0
- package/templates/qoder/scripts/syncgate.py +2 -2
- package/templates/qoder/scripts/task.py +75 -0
- package/templates/qoder/scripts/team_sync.py +60 -4
- package/templates/qoder/scripts/workspace_init.py +1 -1
- package/templates/qoder/skills/design-import/SKILL.md +226 -0
- package/templates/qoder/skills/design-import/figma-workflow.md +81 -0
- package/templates/qoder/skills/design-review/SKILL.md +82 -25
- package/templates/qoder/skills/prd-generator/SKILL.md +184 -60
- package/templates/qoder/skills/prd-review/SKILL.md +18 -1
- package/templates/qoder/skills/prompt-enrich/SKILL.md +90 -0
- package/templates/qoder/skills/prototype-generator/SKILL.md +256 -141
- package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
- package/templates/qoder/skills/spec-coder/SKILL.md +18 -1
- package/templates/qoder/skills/spec-generator/SKILL.md +18 -1
- package/templates/qoder/skills/test-generator/SKILL.md +15 -2
- package/templates/qoder/skills/wl-code/SKILL.md +55 -36
- package/templates/qoder/skills/wl-commit/SKILL.md +89 -76
- package/templates/qoder/skills/wl-design/SKILL.md +55 -0
- package/templates/qoder/skills/wl-init/SKILL.md +76 -67
- package/templates/qoder/skills/wl-insight/SKILL.md +201 -81
- package/templates/qoder/skills/wl-prd-full/SKILL.md +69 -0
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +49 -0
- package/templates/qoder/skills/wl-prd-review/SKILL.md +34 -0
- package/templates/qoder/skills/wl-report/SKILL.md +131 -107
- package/templates/qoder/skills/wl-search/SKILL.md +141 -75
- package/templates/qoder/skills/wl-spec/SKILL.md +49 -39
- package/templates/qoder/skills/wl-status/SKILL.md +83 -61
- package/templates/qoder/skills/wl-task/SKILL.md +132 -58
- package/templates/qoder/skills/wl-test/SKILL.md +406 -40
- package/templates/qoder/templates/prototype-app.html +13 -8
- package/templates/qoder/templates/prototype-web.html +376 -93
- package/templates/root/AGENTS.md +89 -34
- package/templates/root/requirements.txt +21 -0
- package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +259 -259
- package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -186
- package/templates/qoder/agents/prd-planning.md +0 -57
- package/templates/qoder/agents/prd-research.md +0 -34
- package/templates/qoder/commands/wl-insight.md +0 -51
- package/templates/qoder/scripts/__pycache__/init_doctor.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/setup.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/common/__pycache__/developer.cpython-39.pyc +0 -0
- package/templates/qoder/skills/wl-prd/SKILL.md +0 -92
|
@@ -1,67 +1,146 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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
|
-
|
|
17
|
-
|
|
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: '
|
|
66
|
+
print('Recorded: %s → %s' % (event, fb_path.name))
|
|
67
|
+
|
|
20
68
|
|
|
21
69
|
def _update_stats(event):
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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(
|
|
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):
|
|
64
|
-
|
|
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))
|