git-analytics-cli 0.1.0__py3-none-any.whl

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.
generate_report.py ADDED
@@ -0,0 +1,2967 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Git Analytics - 个人代码习惯体检报告生成器
4
+ 使用 Chart.js 生成可视化报告,支持交互式筛选
5
+ """
6
+
7
+ import json
8
+ import os
9
+
10
+ OUTPUT_FILE = "report.html"
11
+
12
+
13
+ def load_data(data_path="data.json"):
14
+ with open(data_path, 'r', encoding='utf-8') as f:
15
+ return json.load(f)
16
+
17
+
18
+ def get_score_color(score):
19
+ if score >= 80: return '#1a7f37'
20
+ elif score >= 60: return '#bf8700'
21
+ else: return '#cf222e'
22
+
23
+
24
+ def get_score_label(score):
25
+ if score >= 80: return '优秀'
26
+ elif score >= 60: return '良好'
27
+ elif score >= 40: return '一般'
28
+ else: return '需改进'
29
+
30
+
31
+ # ============================================================
32
+ # CSS 样式(独立字符串,无 f-string 转义问题)
33
+ # ============================================================
34
+ CSS = """
35
+ * { margin: 0; padding: 0; box-sizing: border-box; }
36
+ body {
37
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'PingFang SC', system-ui, sans-serif;
38
+ background: #f6f8fa; color: #1f2328; line-height: 1.6; padding: 40px 20px;
39
+ }
40
+ .container { max-width: 1100px; margin: 0 auto; }
41
+ .header { text-align: center; margin-bottom: 32px; }
42
+ .header h1 { font-size: 2.4em; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 8px; color: #1f2328; }
43
+ .header p { color: #656d76; font-size: 1.1em; }
44
+ .stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 40px; }
45
+ .stat-card { background: #fff; border-radius: 6px; padding: 24px; text-align: center; border: 1px solid #d0d7de; }
46
+ .stat-number { font-size: 2.4em; font-weight: 700; color: #0969da; }
47
+ .stat-label { color: #656d76; font-size: 0.85em; margin-top: 4px; }
48
+ .section { margin-bottom: 40px; }
49
+ .section-title {
50
+ font-size: 1.4em; font-weight: 600; margin-bottom: 16px; color: #1f2328;
51
+ display: flex; align-items: center; gap: 8px;
52
+ padding-bottom: 8px; border-bottom: 1px solid #d8dee4;
53
+ }
54
+ .card { background: #fff; border-radius: 6px; padding: 24px; border: 1px solid #d0d7de; margin-bottom: 16px; }
55
+ .card h3 { font-size: 1em; font-weight: 600; margin-bottom: 16px; color: #1f2328; }
56
+ .chart-container { position: relative; height: 300px; }
57
+ .insight-card {
58
+ background: #f6f8fa; border-left: 3px solid #0969da;
59
+ border-radius: 0 6px 6px 0; padding: 14px 16px; margin: 16px 0 0;
60
+ font-size: 0.9em; color: #656d76;
61
+ }
62
+ .insight-card strong { color: #0969da; }
63
+ .footer { text-align: center; margin-top: 48px; padding: 24px; color: #656d76; font-size: 0.8em; border-top: 1px solid #d8dee4; }
64
+ .two-cols { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
65
+ .filter-bar {
66
+ background: #fff; border: 1px solid #d0d7de; border-radius: 6px;
67
+ padding: 16px 20px; margin-bottom: 32px;
68
+ display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
69
+ }
70
+ .filter-bar label { font-size: 0.85em; color: #656d76; font-weight: 600; }
71
+ .filter-bar select, .filter-bar input {
72
+ padding: 6px 10px; border: 1px solid #d0d7de; border-radius: 6px;
73
+ font-size: 0.9em; background: #fff; color: #1f2328;
74
+ }
75
+ .filter-bar select:focus, .filter-bar input:focus { outline: none; border-color: #0969da; }
76
+ .filter-bar button {
77
+ padding: 6px 16px; background: #0969da; color: #fff; border: none;
78
+ border-radius: 6px; font-size: 0.9em; cursor: pointer; font-weight: 600;
79
+ }
80
+ .filter-bar button:hover { background: #0550ae; }
81
+ .filter-bar .reset-btn { background: #fff; color: #656d76; border: 1px solid #d0d7de; }
82
+ .filter-bar .reset-btn:hover { background: #f6f8fa; }
83
+
84
+ /* 维度光谱 */
85
+ .dim-item { background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 12px; }
86
+ .dim-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
87
+ .dim-name { font-size: 0.74em; color: #656d76; font-weight: 600; }
88
+ .dim-codepair { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.78em; color: #656d76; }
89
+ .dim-active { display: flex; align-items: baseline; gap: 8px; margin-bottom: 8px; }
90
+ .dim-active-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 1.65em; font-weight: 800; color: #0969da; line-height: 1; }
91
+ .dim-active-name { font-size: 0.95em; font-weight: 700; color: #1f2328; }
92
+ .dim-row { display: flex; align-items: center; gap: 8px; }
93
+ .dim-label { font-size: 0.72em; min-width: 58px; color: #656d76; }
94
+ .dim-label.active { color: #1f2328; font-weight: 700; }
95
+ .dim-bar { flex: 1; height: 6px; background: #d8dee4; border-radius: 3px; position: relative; }
96
+ .dim-bar-fill { position: absolute; left: 0; top: 0; height: 100%; background: #0969da; border-radius: 3px; }
97
+ .dim-pct { font-size: 0.72em; color: #656d76; margin-top: 5px; }
98
+ .dim-desc { font-size: 0.72em; color: #656d76; margin-top: 6px; line-height: 1.4; }
99
+
100
+ /* 分数维度条 */
101
+ .score-dim-row { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
102
+ .score-dim-name { width: 70px; font-size: 0.85em; color: #656d76; }
103
+ .score-dim-bar { flex: 1; height: 8px; background: #d8dee4; border-radius: 4px; overflow: hidden; }
104
+ .score-dim-bar-fill { height: 100%; border-radius: 4px; }
105
+ .score-dim-val { width: 80px; font-size: 0.85em; color: #1f2328; text-align: right; }
106
+ .score-dim-pct { color: #656d76; font-size: 0.8em; }
107
+
108
+ /* 标签 */
109
+ .tag-item { background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 14px; text-align: center; }
110
+ .tag-icon { font-size: 1.5em; margin-bottom: 4px; }
111
+ .tag-name { font-weight: 600; font-size: 0.85em; margin-bottom: 2px; }
112
+ .tag-desc { font-size: 0.7em; color: #656d76; }
113
+
114
+ /* 工程健康 */
115
+ .eng-health-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
116
+ .eng-item { text-align: center; padding: 20px 12px; background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; }
117
+ .eng-val { font-size: 2em; font-weight: 700; }
118
+ .eng-label { font-size: 0.85em; font-weight: 600; color: #1f2328; margin-top: 4px; }
119
+ .eng-desc { font-size: 0.72em; color: #656d76; margin-top: 2px; }
120
+
121
+ /* 提交类型条 */
122
+ .type-bar-row { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
123
+ .type-bar-name { width: 70px; font-size: 0.85em; color: #656d76; }
124
+ .type-bar-track { flex: 1; height: 8px; background: #d8dee4; border-radius: 4px; overflow: hidden; }
125
+ .type-bar-fill { height: 100%; border-radius: 4px; }
126
+ .type-bar-val { width: 90px; font-size: 0.85em; color: #1f2328; text-align: right; }
127
+
128
+ /* 项目排行榜 */
129
+ .rank-row { display: flex; align-items: center; gap: 14px; padding: 12px 0; }
130
+ .rank-row + .rank-row { border-top: 1px solid #d8dee4; }
131
+ .rank-num { width: 24px; height: 24px; background: #0969da; color: #fff; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.8em; }
132
+ .rank-info { flex: 1; }
133
+ .rank-name { font-weight: 600; font-size: 0.95em; }
134
+ .rank-meta { display: flex; gap: 10px; font-size: 0.75em; color: #656d76; margin-top: 2px; }
135
+ .rank-commits { font-size: 1em; font-weight: 700; color: #0969da; }
136
+
137
+ /* 建议 */
138
+ .sug-item { display: flex; gap: 12px; margin-bottom: 10px; padding: 12px 14px; background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; }
139
+ .sug-num { width: 20px; height: 20px; background: #1a7f37; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.7em; font-weight: 700; flex-shrink: 0; }
140
+ .sug-text { font-size: 0.9em; color: #1f2328; }
141
+
142
+ /* AI Impact */
143
+ .ai-visual-grid { display: grid; grid-template-columns: minmax(240px, 0.85fr) minmax(280px, 1.15fr); gap: 16px; margin-top: 16px; }
144
+ .ai-score-box, .ai-chart-box { background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; }
145
+ .ai-box-title { font-weight: 600; margin-bottom: 10px; color: #1f2328; }
146
+ .ai-score-track { height: 12px; display: flex; background: #d8dee4; border-radius: 999px; overflow: hidden; margin: 12px 0 10px; }
147
+ .ai-score-explicit { background: #0969da; }
148
+ .ai-score-tooling { background: #8250df; }
149
+ .ai-score-row { display: flex; justify-content: space-between; gap: 12px; font-size: 0.82em; color: #656d76; margin-top: 6px; }
150
+ .ai-score-row strong { color: #1f2328; }
151
+ .ai-trend-wrap { position: relative; height: 220px; }
152
+ .ai-trend-empty { display: none; align-items: center; justify-content: center; height: 100%; color: #656d76; font-size: 0.9em; }
153
+
154
+ @media (max-width: 768px) {
155
+ .stats-row { grid-template-columns: repeat(2, 1fr); }
156
+ .two-cols { grid-template-columns: 1fr; }
157
+ .header h1 { font-size: 1.8em; }
158
+ .filter-bar { flex-direction: column; align-items: stretch; }
159
+ .eng-health-grid { grid-template-columns: 1fr; }
160
+ .ai-visual-grid { grid-template-columns: 1fr; }
161
+ }
162
+ """
163
+
164
+
165
+ def generate_report(data):
166
+ summary = data['summary']
167
+ habit_score = data['habit_score']
168
+ persona = data.get('persona', {})
169
+ tags = data['developer_tags']
170
+ hourly = data['hourly']
171
+ weekly = data['weekly']
172
+ monthly = data['monthly']
173
+ projects = data['projects']
174
+ commit_types = data['commit_types']
175
+ health = data['engineering_health']
176
+ ai = data['ai_signals']
177
+ peak_hours = data['peak_hours']
178
+ peak_weekdays = data['peak_weekdays']
179
+ all_commits = data.get('all_commits', [])
180
+
181
+ project_names = [p['name'] for p in projects]
182
+ project_meta = {
183
+ p['name']: {
184
+ 'language': p.get('language', 'Unknown'),
185
+ 'active_days': p.get('active_days', 0)
186
+ }
187
+ for p in projects
188
+ }
189
+ for c in all_commits:
190
+ meta = project_meta.get(c.get('project'), {})
191
+ c.setdefault('language', meta.get('language', 'Unknown'))
192
+
193
+ # 月度数据
194
+ monthly_sorted = sorted(monthly.items())
195
+ month_labels = [m[0] for m in monthly_sorted]
196
+
197
+ # 月份显示标签(跨年时带年份)
198
+ years = set(m[:4] for m in month_labels)
199
+ multi_year = len(years) > 1
200
+ def fmt_month(m):
201
+ return f'{m[:4]}年{m[5:]}月' if multi_year else f'{m[5:]}月'
202
+ month_display = [fmt_month(m) for m in month_labels]
203
+
204
+ # 项目堆叠数据(Top 7)
205
+ top_projects = [p for p in projects if p['commits'] >= 10][:7]
206
+ colors = ['#0969da', '#8250df', '#bf3989', '#cf222e', '#1a7f37', '#bf8700', '#0550ae', '#6e7781']
207
+
208
+ stack_datasets = []
209
+ for i, proj in enumerate(top_projects):
210
+ proj_monthly = proj.get('monthly', {})
211
+ proj_data = [proj_monthly.get(m, 0) for m in month_labels]
212
+ stack_datasets.append({
213
+ 'label': proj['name'],
214
+ 'data': proj_data,
215
+ 'backgroundColor': colors[i % len(colors)]
216
+ })
217
+
218
+ other_projects = [p for p in projects if p['commits'] < 10]
219
+ if other_projects:
220
+ other_data = [0] * len(month_labels)
221
+ for p in other_projects:
222
+ for j, m in enumerate(month_labels):
223
+ other_data[j] += p.get('monthly', {}).get(m, 0)
224
+ if any(v > 0 for v in other_data):
225
+ stack_datasets.append({'label': '其他', 'data': other_data, 'backgroundColor': '#9ca3af'})
226
+
227
+ # 气泡图数据(月份标签作为 x,分类轴)
228
+ bubble_datasets = []
229
+ for i, proj in enumerate(projects):
230
+ if proj['commits'] < 5:
231
+ continue
232
+ points = []
233
+ for m, label in zip(month_labels, month_display):
234
+ count = proj.get('monthly', {}).get(m, 0)
235
+ if count > 0:
236
+ points.append({'x': label, 'y': count, 'r': min(max(count ** 0.5 * 2.5, 4), 30)})
237
+ if points:
238
+ bubble_datasets.append({
239
+ 'label': proj['name'],
240
+ 'data': points,
241
+ 'backgroundColor': f'{colors[i % len(colors)]}b3'
242
+ })
243
+
244
+ # Commit 类型
245
+ type_labels = ['feat', 'fix', 'docs', 'test', 'refactor', 'chore', 'other']
246
+ type_names = ['功能开发', 'Bug 修复', '文档', '测试', '重构', '构建/CI', '其他']
247
+ type_colors = ['#0969da', '#cf222e', '#1a7f37', '#bf8700', '#8250df', '#6e7781', '#afb8c1']
248
+ type_values = [commit_types.get(t, 0) for t in type_labels]
249
+
250
+ # 语言分布
251
+ lang_counter = {}
252
+ for p in projects:
253
+ lang = p.get('language', 'Unknown')
254
+ lang_counter[lang] = lang_counter.get(lang, 0) + 1
255
+ lang_labels = list(lang_counter.keys())
256
+ lang_values = list(lang_counter.values())
257
+
258
+ # Top3 聚焦度
259
+ top3_commits = sum(p['commits'] for p in projects[:3])
260
+ top3_ratio = top3_commits / max(summary['total_commits'], 1) * 100
261
+
262
+ # 维度
263
+ dims = persona.get('dimensions', {})
264
+ dim_keys = ['time', 'rhythm', 'focus', 'style', 'engineering', 'ai']
265
+ dim_names = {'time': '时间偏好', 'rhythm': '节奏风格', 'focus': '专注程度',
266
+ 'style': '开发风格', 'engineering': '工程取向', 'ai': 'AI 协作'}
267
+
268
+ # ============================================================
269
+ # 构建 HTML 片段(Python 变量,避免在 f-string 中拼接)
270
+ # ============================================================
271
+
272
+ # 统计卡片
273
+ stats_html = f'''
274
+ <div class="stats-row">
275
+ <div class="stat-card"><div class="stat-number" id="statScore">{habit_score['total']}</div><div class="stat-label">习惯健康分</div></div>
276
+ <div class="stat-card"><div class="stat-number" id="statProjects">{summary['total_projects']}</div><div class="stat-label">项目总数</div></div>
277
+ <div class="stat-card"><div class="stat-number" id="statCommits">{summary['total_commits']}</div><div class="stat-label">总提交数</div></div>
278
+ <div class="stat-card"><div class="stat-number" id="statDaily">{summary['avg_commits_per_day']}</div><div class="stat-label">日均提交</div></div>
279
+ </div>'''
280
+
281
+ # 开发者人格
282
+ persona_html = f'''
283
+ <div class="card">
284
+ <h3>你的开发者人格</h3>
285
+ <div style="display:flex;align-items:center;gap:40px;padding:10px 0;">
286
+ <div style="text-align:center;" id="personaCard">
287
+ <div style="font-size:3em;margin-bottom:8px;">{persona.get('icon', '❓')}</div>
288
+ <div style="font-size:1.6em;font-weight:700;color:#1f2328;">{persona.get('name', '未知')}</div>
289
+ <div style="font-size:1.1em;color:#0969da;font-weight:600;margin-top:4px;font-family:monospace;">{persona.get('code', '????')}</div>
290
+ <div style="color:#1f2328;font-size:0.95em;margin-top:8px;font-weight:500;">{persona.get('desc', '')}</div>
291
+ <div style="color:#656d76;font-size:0.85em;margin-top:4px;">{persona.get('detail', '')}</div>
292
+ </div>
293
+ <div style="flex:1;">
294
+ <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;" id="dimsDetail">{_build_dims_html(dims, dim_keys, dim_names)}</div>
295
+ </div>
296
+ </div>
297
+ </div>'''
298
+
299
+ # Habit Score
300
+ score_dims = [
301
+ ('提交粒度', habit_score['granularity'], 30),
302
+ ('测试意识', habit_score['test_awareness'], 20),
303
+ ('文档意识', habit_score['doc_awareness'], 15),
304
+ ('作息规律', habit_score['schedule'], 20),
305
+ ('项目聚焦', habit_score['focus'], 15),
306
+ ]
307
+ score_html = f'''
308
+ <div class="card">
309
+ <h3>Developer Habit Score</h3>
310
+ <div style="display:flex;align-items:center;gap:30px;">
311
+ <div style="text-align:center;" id="scoreCircle">
312
+ <div style="font-size:4em;font-weight:700;color:{get_score_color(habit_score['total'])};line-height:1;" id="scoreNumber">{habit_score['total']}</div>
313
+ <div style="color:#656d76;font-size:0.9em;margin-top:4px;" id="scoreLabel">/ 100 · {get_score_label(habit_score['total'])}</div>
314
+ </div>
315
+ <div style="flex:1;" id="scoreDims">{_build_score_dims_html(score_dims)}</div>
316
+ </div>
317
+ </div>'''
318
+
319
+ # 标签
320
+ tags_html = f'''
321
+ <div class="card">
322
+ <h3>特征标签</h3>
323
+ <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;" id="tagsGrid">{_build_tags_html(tags)}</div>
324
+ </div>'''
325
+
326
+ # 24小时
327
+ hour_chart_html = f'''
328
+ <div class="card">
329
+ <h3>24 小时提交分布</h3>
330
+ <div class="chart-container"><canvas id="hourChart"></canvas></div>
331
+ <div class="insight-card" id="insightHour">
332
+ <strong>洞察:</strong>最活跃时段 {', '.join([f'{h}:00' for h in peak_hours])}。
333
+ {'夜间提交占比 ' + str(health['night_ratio']) + '%,属于夜间爆发型。' if health['night_ratio'] > 25 else '作息相对规律。'}
334
+ </div>
335
+ </div>'''
336
+
337
+ # 星期
338
+ week_chart_html = f'''
339
+ <div class="card">
340
+ <h3>星期提交分布</h3>
341
+ <div class="chart-container" style="height:260px;"><canvas id="weekChart"></canvas></div>
342
+ <div class="insight-card" id="insightWeek">
343
+ <strong>洞察:</strong>最活跃 {', '.join(peak_weekdays)}。
344
+ {'周末提交占比 ' + str(health['weekend_ratio']) + '%。' if health['weekend_ratio'] > 20 else '以工作日为主。'}
345
+ </div>
346
+ </div>'''
347
+
348
+ # 每月项目投入
349
+ monthly_chart_html = f'''
350
+ <div class="card">
351
+ <h3>每月项目投入</h3>
352
+ <div class="chart-container" style="height:380px;"><canvas id="monthlyChart"></canvas></div>
353
+ <div class="insight-card" id="insightMonthly">
354
+ <strong>洞察:</strong>Top 3 项目占据 {top3_ratio:.0f}% 的提交。
355
+ {'专注度高。' if top3_ratio > 60 else '精力较分散。'}
356
+ </div>
357
+ </div>'''
358
+
359
+ # 气泡图
360
+ bubble_chart_html = '''
361
+ <div class="card">
362
+ <h3>项目时间线</h3>
363
+ <div class="chart-container" style="height:350px;"><canvas id="bubbleChart"></canvas></div>
364
+ </div>'''
365
+
366
+ # 项目排行榜
367
+ projects_html = _build_projects_html(projects)
368
+ projects_ranking_html = f'''
369
+ <div class="card">
370
+ <h3>项目排行榜</h3>
371
+ <div id="projectsRanking">{projects_html}</div>
372
+ </div>'''
373
+
374
+ # Commit 类型
375
+ type_chart_html = '''
376
+ <div class="card">
377
+ <h3>Commit 类型分布</h3>
378
+ <div class="chart-container"><canvas id="typeChart"></canvas></div>
379
+ </div>'''
380
+
381
+ lang_chart_html = '''
382
+ <div class="card">
383
+ <h3>语言分布</h3>
384
+ <div class="chart-container"><canvas id="langChart"></canvas></div>
385
+ </div>'''
386
+
387
+ # 提交类型详情
388
+ type_detail_html = f'''
389
+ <div class="card">
390
+ <h3>提交类型详情</h3>
391
+ <div id="typeBars"></div>
392
+ <div class="insight-card" id="insightTypes">
393
+ <strong>洞察:</strong>
394
+ 功能开发占比 {health['feat_ratio']}%,测试 {health['test_ratio']}%,文档 {health['doc_ratio']}%。
395
+ {'测试投入偏低。' if health['test_ratio'] < 5 else ''}
396
+ {'低信息量 commit 占比 ' + str(health['low_info_ratio']) + '%。' if health['low_info_ratio'] > 15 else ''}
397
+ </div>
398
+ </div>'''
399
+
400
+ # 工程健康
401
+ eng_health_html = _build_eng_health_html(health)
402
+ eng_insight_html = _build_eng_insight(health)
403
+
404
+ eng_section_html = f'''
405
+ <div class="card">
406
+ <div class="eng-health-grid" id="engHealthGrid">{eng_health_html}</div>
407
+ </div>
408
+ <div class="insight-card" id="insightEng"><strong>洞察:</strong>{eng_insight_html}</div>'''
409
+
410
+ # AI
411
+ if ai['detected']:
412
+ # 构建工具分布 HTML
413
+ tools_html = ''
414
+ if ai.get('tools'):
415
+ tool_items = []
416
+ for tool, count in ai['tools'].items():
417
+ tool_items.append(f'<span style="background:#e8f5e9;padding:4px 10px;border-radius:12px;font-size:0.85em;">{tool}: {count}</span>')
418
+ tools_html = f'''<div style="margin-top:12px;display:flex;gap:8px;justify-content:center;flex-wrap:wrap;">
419
+ {''.join(tool_items)}
420
+ </div>'''
421
+
422
+ # 生成 AI 洞察文字
423
+ ai_insight_parts = []
424
+ if ai['ai_commit_ratio'] >= 30:
425
+ ai_insight_parts.append(f'AI 使用率 {ai["ai_commit_ratio"]}%,你是 AI 协作开发的重度用户。')
426
+ elif ai['ai_commit_ratio'] >= 10:
427
+ ai_insight_parts.append(f'AI 使用率 {ai["ai_commit_ratio"]}%,AI 已成为你的开发助手。')
428
+ else:
429
+ ai_insight_parts.append(f'AI 使用率 {ai["ai_commit_ratio"]}%,你主要依靠手工编码。')
430
+
431
+ if ai.get('tools'):
432
+ top_tool = max(ai['tools'], key=ai['tools'].get)
433
+ ai_insight_parts.append(f'最常用工具: {top_tool}。')
434
+
435
+ ai_insight_html = ' '.join(ai_insight_parts)
436
+
437
+ ai_html = f'''
438
+ <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:16px;">
439
+ <div style="background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:20px;text-align:center;">
440
+ <div style="font-size:2em;margin-bottom:4px;">🤖</div>
441
+ <div style="font-size:1.8em;font-weight:700;color:#1f2328;">{ai['ai_commit_ratio']}%</div>
442
+ <div style="color:#656d76;font-size:0.85em;">AI 明确信号率</div>
443
+ </div>
444
+ <div style="background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:20px;text-align:center;">
445
+ <div style="font-size:2em;margin-bottom:4px;">📊</div>
446
+ <div style="font-size:1.8em;font-weight:700;color:#1f2328;">{ai['ai_commit_count']}</div>
447
+ <div style="color:#656d76;font-size:0.85em;">AI 明确信号提交</div>
448
+ </div>
449
+ <div style="background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:20px;text-align:center;">
450
+ <div style="font-size:2em;margin-bottom:4px;">🔧</div>
451
+ <div style="font-size:1.8em;font-weight:700;color:#1f2328;">{ai.get('ai_influence_score', 0)}</div>
452
+ <div style="color:#656d76;font-size:0.85em;">AI 影响分</div>
453
+ </div>
454
+ </div>
455
+ <div style="background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:16px;">
456
+ <div style="font-weight:600;margin-bottom:8px;">使用的 AI 工具</div>
457
+ {tools_html if tools_html else '<div style="color:#656d76;">无详细工具信息</div>'}
458
+ </div>
459
+ <div style="background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:16px;margin-top:12px;">
460
+ <div style="font-weight:600;margin-bottom:8px;">AI 使用趋势</div>
461
+ <canvas id="aiTrendChart" height="200"></canvas>
462
+ </div>
463
+ <div class="insight-card"><strong>洞察:</strong>{ai_insight_html}</div>'''
464
+ else:
465
+ ai_html = '''
466
+ <div style="background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:24px;text-align:center;">
467
+ <div style="color:#656d76;">未检测到明显的 AI 辅助开发痕迹</div>
468
+ </div>'''
469
+ ai_insight_html = '你目前主要依靠手工编码,没有检测到 AI 辅助开发的痕迹。'
470
+
471
+ # 建议
472
+ sug_html = _build_suggestions_html(habit_score, health, data)
473
+
474
+ # ============================================================
475
+ # 嵌入的 JSON 数据
476
+ # ============================================================
477
+ all_commits_json = json.dumps(all_commits, ensure_ascii=False)
478
+ project_names_json = json.dumps(project_names, ensure_ascii=False)
479
+ project_meta_json = json.dumps(project_meta, ensure_ascii=False)
480
+ month_labels_json = json.dumps(month_labels)
481
+ type_labels_json = json.dumps(type_labels)
482
+ type_names_json = json.dumps(type_names)
483
+ type_colors_json = json.dumps(type_colors)
484
+ colors_json = json.dumps(colors)
485
+ lang_labels_json = json.dumps(lang_labels)
486
+ lang_values_json = json.dumps(lang_values)
487
+
488
+ # Python 常量(JS 无法重算)
489
+ eng_spectrum = dims.get('engineering', {}).get('spectrum', 50)
490
+ ai_spectrum = dims.get('ai', {}).get('spectrum', 0)
491
+ test_ratio = health['test_ratio']
492
+ doc_ratio = health['doc_ratio']
493
+ ai_detected = 'true' if ai['detected'] else 'false'
494
+ ai_count = ai['count']
495
+ low_info_ratio = health['low_info_ratio']
496
+
497
+ # AI 月度趋势数据
498
+ monthly_ai = ai.get('monthly_ai', {})
499
+ monthly_ai_json = json.dumps(monthly_ai)
500
+
501
+ # ============================================================
502
+ # JS 脚本(独立字符串,无 f-string 转义问题)
503
+ # ============================================================
504
+ js = _build_js(
505
+ all_commits_json, project_names_json, project_meta_json,
506
+ month_labels_json, type_labels_json, type_names_json, type_colors_json,
507
+ colors_json, lang_labels_json, lang_values_json,
508
+ eng_spectrum, ai_spectrum, test_ratio, doc_ratio,
509
+ ai_detected, ai_count, low_info_ratio,
510
+ json.dumps(hourly),
511
+ json.dumps([weekly.get(str(i), 0) for i in range(7)]),
512
+ json.dumps(type_values),
513
+ json.dumps(stack_datasets),
514
+ json.dumps(bubble_datasets),
515
+ json.dumps(month_display),
516
+ 'true' if multi_year else 'false',
517
+ monthly_ai_json,
518
+ )
519
+
520
+ # ============================================================
521
+ # 组装完整 HTML
522
+ # ============================================================
523
+ html = f'''<!DOCTYPE html>
524
+ <html lang="zh-CN">
525
+ <head>
526
+ <meta charset="UTF-8">
527
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
528
+ <title>Git Analytics - 代码习惯体检报告</title>
529
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
530
+ <style>{CSS}</style>
531
+ </head>
532
+ <body>
533
+ <div class="container">
534
+ <div class="header">
535
+ <h1>代码习惯体检报告</h1>
536
+ <p id="headerSubtitle">{summary['total_projects']} 个项目 · {summary['total_commits']} 次提交 · {summary['total_active_days']} 天活跃</p>
537
+ </div>
538
+
539
+ <div class="filter-bar">
540
+ <label>项目</label>
541
+ <select id="filterProject"><option value="all">全部项目</option></select>
542
+ <label>时间</label>
543
+ <select id="filterRange" onchange="applyPresetRange()">
544
+ <option value="all">全部时间</option>
545
+ <option value="1m">近一个月</option>
546
+ <option value="6m">近半年</option>
547
+ <option value="1y">近一年</option>
548
+ <option value="custom">自定义</option>
549
+ </select>
550
+ <label>从</label>
551
+ <input type="date" id="filterSince" onchange="markCustomRange()">
552
+ <label>至</label>
553
+ <input type="date" id="filterUntil" onchange="markCustomRange()">
554
+ <button onclick="applyFilters()">筛选</button>
555
+ <button class="reset-btn" onclick="resetFilters()">重置</button>
556
+ </div>
557
+
558
+ {stats_html}
559
+
560
+ <!-- 总览 -->
561
+ <div class="section">
562
+ <div class="section-title">📊 总览</div>
563
+ {persona_html}
564
+ <div class="two-cols">{score_html}{tags_html}</div>
565
+ </div>
566
+
567
+ <!-- 时间习惯 -->
568
+ <div class="section">
569
+ <div class="section-title">⏰ 时间习惯</div>
570
+ {hour_chart_html}
571
+ {week_chart_html}
572
+ </div>
573
+
574
+ <!-- 项目投入 -->
575
+ <div class="section">
576
+ <div class="section-title">🎯 项目投入</div>
577
+ {monthly_chart_html}
578
+ {bubble_chart_html}
579
+ {projects_ranking_html}
580
+ </div>
581
+
582
+ <!-- 提交习惯 -->
583
+ <div class="section">
584
+ <div class="section-title">📝 提交习惯</div>
585
+ <div class="two-cols">{type_chart_html}{lang_chart_html}</div>
586
+ {type_detail_html}
587
+ </div>
588
+
589
+ <!-- 工程健康 -->
590
+ <div class="section">
591
+ <div class="section-title">🏥 工程健康</div>
592
+ {eng_section_html}
593
+ </div>
594
+
595
+ <!-- AI Impact -->
596
+ <div class="section">
597
+ <div class="section-title">🤖 AI Coding Impact</div>
598
+ <div class="card" id="aiImpact">{ai_html}</div>
599
+ </div>
600
+
601
+ <!-- 建议 -->
602
+ <div class="section">
603
+ <div class="section-title">💡 改进建议</div>
604
+ <div class="card" id="suggestions">{sug_html}</div>
605
+ </div>
606
+
607
+ <div class="footer">
608
+ <p>生成时间: {data['generated_at']}</p>
609
+ <p style="margin-top:6px;">Git Analytics - 本地优先的个人代码习惯分析工具</p>
610
+ </div>
611
+ </div>
612
+
613
+ <script>{js}</script>
614
+ </body>
615
+ </html>'''
616
+
617
+ return html
618
+
619
+
620
+ # ============================================================
621
+ # HTML 片段构建函数(纯 Python,无 f-string 嵌套问题)
622
+ # ============================================================
623
+
624
+ def _build_dims_html(dims, dim_keys, dim_names):
625
+ dim_meta = {
626
+ 'time': {'left_code': 'D', 'right_code': 'N', 'left_trait': '日间掌控者', 'right_trait': '夜间爆发者',
627
+ 'left_desc': '白天更容易进入状态,节奏清晰。', 'right_desc': '安静时段更容易输出,灵感来得晚。'},
628
+ 'rhythm': {'left_code': 'M', 'right_code': 'S', 'left_trait': '长线推进者', 'right_trait': '冲刺迭代者',
629
+ 'left_desc': '偏稳定推进,提交更像长跑。', 'right_desc': '偏高频推进,短时间爆发强。'},
630
+ 'focus': {'left_code': 'D', 'right_code': 'C', 'left_trait': '多线调度者', 'right_trait': '核心深挖者',
631
+ 'left_desc': '能在多个项目间切换上下文。', 'right_desc': '精力更集中在核心项目。'},
632
+ 'style': {'left_code': 'G', 'right_code': 'P', 'left_trait': '系统守护者', 'right_trait': '功能开拓者',
633
+ 'left_desc': '更常维护、修复和打磨系统。', 'right_desc': '更常推进新功能和新想法。'},
634
+ 'engineering': {'left_code': 'R', 'right_code': 'Q', 'left_trait': '快速试错派', 'right_trait': '质量洁癖派',
635
+ 'left_desc': '偏速度和反馈,先跑起来。', 'right_desc': '偏测试、文档和工程质量。'},
636
+ 'ai': {'left_code': 'H', 'right_code': 'A', 'left_trait': '手作掌控派', 'right_trait': 'AI 搭子派',
637
+ 'left_desc': '主要靠自己手工推进。', 'right_desc': '会把 AI 当成结对伙伴。'},
638
+ }
639
+ html = ""
640
+ for dim_key in dim_keys:
641
+ dim = dims.get(dim_key, {})
642
+ meta = dim_meta.get(dim_key, {})
643
+ spectrum = dim.get('spectrum', 50)
644
+ left_label = dim.get('left', '')
645
+ right_label = dim.get('right', '')
646
+ dim_name = dim_names[dim_key]
647
+ left_code = meta.get('left_code', '')
648
+ right_code = meta.get('right_code', '')
649
+ active_code = dim.get('code') or (right_code if spectrum > 50 else left_code)
650
+ is_right = active_code == right_code
651
+ pct = spectrum if is_right else (100 - spectrum)
652
+ active_trait = meta.get('right_trait' if is_right else 'left_trait', right_label if is_right else left_label)
653
+ active_desc = meta.get('right_desc' if is_right else 'left_desc', '')
654
+ align = 'right' if is_right else 'left'
655
+ left_active = ' active' if not is_right else ''
656
+ right_active = ' active' if is_right else ''
657
+ html += f'''
658
+ <div class="dim-item">
659
+ <div class="dim-head">
660
+ <div class="dim-name">{dim_name}</div>
661
+ <div class="dim-codepair">{left_code}/{right_code}</div>
662
+ </div>
663
+ <div class="dim-active">
664
+ <span class="dim-active-code">{active_code}</span>
665
+ <span class="dim-active-name">{active_trait}</span>
666
+ </div>
667
+ <div class="dim-row">
668
+ <span class="dim-label{left_active}" style="text-align:right;">{left_label}</span>
669
+ <div class="dim-bar"><div class="dim-bar-fill" style="width:{spectrum}%;"></div></div>
670
+ <span class="dim-label{right_active}">{right_label}</span>
671
+ </div>
672
+ <div class="dim-pct" style="text-align:{align};">倾向 {pct}%</div>
673
+ <div class="dim-desc">{active_desc}</div>
674
+ </div>'''
675
+ return html
676
+
677
+
678
+ def _build_score_dims_html(score_dims):
679
+ html = ""
680
+ for name, score, max_score in score_dims:
681
+ pct = score / max_score * 100
682
+ color = get_score_color(pct)
683
+ html += f'''
684
+ <div class="score-dim-row">
685
+ <div class="score-dim-name">{name}</div>
686
+ <div class="score-dim-bar"><div class="score-dim-bar-fill" style="width:{pct}%;background:{color};"></div></div>
687
+ <div class="score-dim-val">{score}/{max_score} <span class="score-dim-pct">({pct:.0f}%)</span></div>
688
+ </div>'''
689
+ return html
690
+
691
+
692
+ def _build_tags_html(tags):
693
+ html = ""
694
+ for tag in tags[1:]:
695
+ html += f'''
696
+ <div class="tag-item">
697
+ <div class="tag-icon">{tag['icon']}</div>
698
+ <div class="tag-name">{tag['name']}</div>
699
+ <div class="tag-desc">{tag['desc']}</div>
700
+ </div>'''
701
+ return html
702
+
703
+
704
+ def _build_projects_html(projects):
705
+ html = ""
706
+ for i, p in enumerate(projects[:8], 1):
707
+ html += f'''
708
+ <div class="rank-row">
709
+ <div class="rank-num">{i}</div>
710
+ <div class="rank-info">
711
+ <div class="rank-name">{p['name']}</div>
712
+ <div class="rank-meta"><span>{p['language']}</span><span>{p['active_days']} 天活跃</span></div>
713
+ </div>
714
+ <div class="rank-commits">{p['commits']}</div>
715
+ </div>'''
716
+ return html
717
+
718
+
719
+ def _build_eng_health_html(health):
720
+ items = [
721
+ ('测试覆盖', '改代码时有改测试吗', health['test_ratio'], '#1a7f37' if health['test_ratio'] >= 10 else '#cf222e'),
722
+ ('文档覆盖', '改代码时有更新文档吗', health['doc_ratio'], '#1a7f37' if health['doc_ratio'] >= 5 else '#bf8700'),
723
+ ('功能开发', '写新功能的时间占比', health['feat_ratio'], '#0969da'),
724
+ ('Bug 修复', '修 bug 的时间占比', health['fix_ratio'], '#cf222e'),
725
+ ('重构', '优化老代码的时间占比', health['refactor_ratio'], '#8250df'),
726
+ ('夜间提交', '深夜写的代码占比', health['night_ratio'], '#bf8700' if health['night_ratio'] > 25 else '#656d76'),
727
+ ('周末提交', '周末写的代码占比', health['weekend_ratio'], '#8250df' if health['weekend_ratio'] > 25 else '#656d76'),
728
+ ('低信息量', 'commit 信息够详细吗', health['low_info_ratio'], '#cf222e' if health['low_info_ratio'] > 20 else '#1a7f37'),
729
+ ]
730
+ html = ""
731
+ for label, desc, val, color in items:
732
+ html += f'''
733
+ <div class="eng-item">
734
+ <div class="eng-val" style="color:{color};">{val}%</div>
735
+ <div class="eng-label">{label}</div>
736
+ <div class="eng-desc">{desc}</div>
737
+ </div>'''
738
+ return html
739
+
740
+
741
+ def _build_eng_insight(health):
742
+ parts = []
743
+ if health['test_ratio'] < 5:
744
+ parts.append('很少写测试,建议为新功能补充测试用例。')
745
+ elif health['test_ratio'] >= 10:
746
+ parts.append('测试习惯不错。')
747
+ if health['doc_ratio'] < 3:
748
+ parts.append('文档更新较少,建议定期维护 README。')
749
+ if health['night_ratio'] > 30:
750
+ parts.append('深夜写代码比例较高,注意休息。')
751
+ if health['low_info_ratio'] > 15:
752
+ parts.append('有些 commit 描述太简略,不利于后期回溯。')
753
+ return ' '.join(parts) if parts else '各项指标正常。'
754
+
755
+
756
+ def _build_suggestions_html(habit_score, health, data):
757
+ suggestions = []
758
+ summary = data.get('summary', {})
759
+ ai = data.get('ai_signals', {})
760
+ avg_daily = summary.get('avg_commits_per_day', 0)
761
+ hourly = data.get('hourly', [])
762
+ weekly = data.get('weekly', {})
763
+ peak_hours = data.get('peak_hours', [])
764
+ commit_types = data.get('commit_types', {})
765
+ total_commits = summary.get('total_commits', 0)
766
+ total_active_days = summary.get('total_active_days', 0)
767
+ total_projects = summary.get('total_projects', 0)
768
+
769
+ # ============================================================
770
+ # 1. 习惯分数维度
771
+ # ============================================================
772
+ if habit_score['schedule'] < 10:
773
+ suggestions.append("改善作息规律:作息得分仅 {}/20,夜间和周末提交较多".format(habit_score['schedule']))
774
+ if habit_score['focus'] < 10:
775
+ suggestions.append("提升专注度:项目聚焦得分仅 {}/15,建议减少同时维护的项目数".format(habit_score['focus']))
776
+ if habit_score['granularity'] < 15:
777
+ suggestions.append("优化提交粒度:粒度得分仅 {}/30,建议保持稳定的提交频率".format(habit_score['granularity']))
778
+
779
+ # ============================================================
780
+ # 2. 测试相关
781
+ # ============================================================
782
+ if habit_score['test_awareness'] < 10:
783
+ suggestions.append("提高测试意识:尝试为每个新功能编写测试用例,目标覆盖率 10%+")
784
+ elif health['test_ratio'] < 8:
785
+ suggestions.append("增加测试投入:当前测试文件占比仅 {:.0f}%,建议补充单元测试".format(health['test_ratio']))
786
+ if health['test_ratio'] >= 30:
787
+ suggestions.append("测试做得好:测试文件占比 {:.0f}%,继续保持!".format(health['test_ratio']))
788
+
789
+ # ============================================================
790
+ # 3. 文档相关
791
+ # ============================================================
792
+ if habit_score['doc_awareness'] < 10:
793
+ suggestions.append("增加文档投入:定期更新 README 和 API 文档")
794
+ elif health['doc_ratio'] < 5:
795
+ suggestions.append("完善文档:文档变更占比 {:.0f}%,重要功能应有文档说明".format(health['doc_ratio']))
796
+ if health['doc_ratio'] >= 15:
797
+ suggestions.append("文档做得好:文档变更占比 {:.0f}%,继续保持!".format(health['doc_ratio']))
798
+
799
+ # ============================================================
800
+ # 4. 作息相关
801
+ # ============================================================
802
+ if health['night_ratio'] > 25:
803
+ suggestions.append("注意作息健康:夜间提交占比 {:.0f}%,建议调整为白天工作".format(health['night_ratio']))
804
+ if health['weekend_ratio'] > 25:
805
+ suggestions.append("平衡工作生活:周末提交占比 {:.0f}%,注意适当休息".format(health['weekend_ratio']))
806
+
807
+ # ============================================================
808
+ # 5. 深夜提交
809
+ # ============================================================
810
+ if hourly:
811
+ late_night = sum(hourly[0:6]) # 凌晨 0-5 点
812
+ total = sum(hourly)
813
+ if total > 0 and late_night / total > 0.15:
814
+ suggestions.append("减少深夜编码:凌晨 0-6 点提交占比 {:.0f}%,长期熬夜影响代码质量".format(late_night / total * 100))
815
+
816
+ # ============================================================
817
+ # 6. Commit 质量
818
+ # ============================================================
819
+ if health['low_info_ratio'] > 15:
820
+ suggestions.append("优化 Commit Message:{:.0f}% 的提交缺少描述,建议使用 Conventional Commits 规范".format(health['low_info_ratio']))
821
+ elif health['low_info_ratio'] > 5:
822
+ suggestions.append("完善提交描述:部分 commit 信息过于简略,建议写清楚改动原因")
823
+ if health['low_info_ratio'] < 3:
824
+ suggestions.append("Commit 质量高:{:.0f}% 的提交有详细描述,继续保持!".format(100 - health['low_info_ratio']))
825
+
826
+ # ============================================================
827
+ # 7. 项目聚焦
828
+ # ============================================================
829
+ focus_index = data.get('focus_index', 100)
830
+ if focus_index < 60:
831
+ suggestions.append("提高项目聚焦度:精力较分散,建议集中精力在 1-2 个核心项目上")
832
+
833
+ # ============================================================
834
+ # 8. 提交粒度
835
+ # ============================================================
836
+ if avg_daily < 1:
837
+ suggestions.append("提高提交频率:日均仅 {:.1f} 次提交,建议小步快跑、勤提交".format(avg_daily))
838
+ elif avg_daily > 8:
839
+ suggestions.append("优化提交粒度:日均 {:.1f} 次提交偏多,考虑合并相关改动".format(avg_daily))
840
+ elif avg_daily >= 2 and avg_daily <= 5:
841
+ suggestions.append("提交频率适中:日均 {:.1f} 次提交,节奏良好".format(avg_daily))
842
+
843
+ # ============================================================
844
+ # 9. 代码质量
845
+ # ============================================================
846
+ if health['fix_ratio'] > 20:
847
+ suggestions.append("提升代码质量:Bug 修复占比 {:.0f}%,建议加强测试和 Code Review".format(health['fix_ratio']))
848
+ elif health['fix_ratio'] < 5 and total_commits > 50:
849
+ suggestions.append("Bug 很少:Bug 修复仅占 {:.0f}%,代码质量不错".format(health['fix_ratio']))
850
+ if health['refactor_ratio'] < 10 and health['feat_ratio'] > 50:
851
+ suggestions.append("关注技术债:功能开发占比高但重构不足,建议定期安排重构时间")
852
+ if health['refactor_ratio'] >= 10:
853
+ suggestions.append("重构做得好:重构占比 {:.0f}%,代码质量持续改善".format(health['refactor_ratio']))
854
+
855
+ # ============================================================
856
+ # 10. AI 使用
857
+ # ============================================================
858
+ ai_ratio = ai.get('ai_commit_ratio', 0)
859
+ if ai_ratio > 40:
860
+ suggestions.append("善用 AI:AI 占比超过 40%,建议仔细审查 AI 生成的代码")
861
+ elif ai_ratio > 0 and ai_ratio < 15:
862
+ suggestions.append("探索 AI 工具:当前 AI 使用率 {:.0f}%,可尝试 Copilot/Cursor 提升效率".format(ai_ratio))
863
+ if ai_ratio >= 20 and ai_ratio <= 40:
864
+ suggestions.append("AI 使用适中:AI 占比 {:.0f}%,人机协作良好".format(ai_ratio))
865
+
866
+ # AI 工具多样性
867
+ ai_tools = ai.get('tools', {})
868
+ if len(ai_tools) >= 3:
869
+ suggestions.append("AI 工具多样化:使用了 {} 种 AI 工具,建议固定 1-2 种核心工具".format(len(ai_tools)))
870
+
871
+ # ============================================================
872
+ # 11. 提交时间规律
873
+ # ============================================================
874
+ if peak_hours:
875
+ if 22 in peak_hours or 23 in peak_hours:
876
+ suggestions.append("调整工作节奏:深夜是你最活跃的时段,建议将核心工作移到白天")
877
+ if 6 in peak_hours or 7 in peak_hours:
878
+ suggestions.append("利用晨间高效期:早上 6-7 点是你最活跃的时段,适合处理复杂任务")
879
+
880
+ # ============================================================
881
+ # 12. 最活跃星期
882
+ # ============================================================
883
+ peak_weekdays = data.get('peak_weekdays', [])
884
+ if peak_weekdays:
885
+ weekend_days = [d for d in peak_weekdays if d in ['周六', '周日']]
886
+ if len(weekend_days) >= 2:
887
+ suggestions.append("调整工作节奏:最活跃的 3 天中有 {} 天是周末,建议以工作日为主".format(len(weekend_days)))
888
+
889
+ # ============================================================
890
+ # 13. 工作强度
891
+ # ============================================================
892
+ if avg_daily > 5:
893
+ suggestions.append("注意工作强度:日均 {:.1f} 次提交,注意劳逸结合避免倦怠".format(avg_daily))
894
+
895
+ # ============================================================
896
+ # 14. 项目数量
897
+ # ============================================================
898
+ if total_projects > 15:
899
+ suggestions.append("精简项目数量:同时维护 {} 个项目,建议聚焦核心项目提升效率".format(total_projects))
900
+ elif total_projects <= 3 and total_commits > 100:
901
+ suggestions.append("项目聚焦度高:仅维护 {} 个项目,精力集中".format(total_projects))
902
+
903
+ # ============================================================
904
+ # 15. 活跃度
905
+ # ============================================================
906
+ if total_active_days > 0 and avg_daily > 0:
907
+ active_rate = min(total_active_days / 365 * 100, 100)
908
+ if active_rate < 30:
909
+ suggestions.append("保持持续性:活跃天数 {} 天,建议养成每天提交的习惯".format(total_active_days))
910
+ elif active_rate > 70:
911
+ suggestions.append("活跃度高:{} 天有提交, coding 习惯良好".format(total_active_days))
912
+
913
+ # ============================================================
914
+ # 16. Commit 类型分布
915
+ # ============================================================
916
+ if total_commits > 0:
917
+ feat_count = commit_types.get('feat', 0)
918
+ test_count = commit_types.get('test', 0)
919
+ docs_count = commit_types.get('docs', 0)
920
+ refactor_count = commit_types.get('refactor', 0)
921
+ fix_count = commit_types.get('fix', 0)
922
+ other_count = commit_types.get('other', 0)
923
+ chore_count = commit_types.get('chore', 0)
924
+
925
+ # 测试提交比例
926
+ if test_count / total_commits < 0.05 and total_commits > 50:
927
+ suggestions.append("增加测试提交:测试相关提交仅占 {:.0f}%,建议为新功能编写测试".format(test_count / total_commits * 100))
928
+ elif test_count / total_commits >= 0.15:
929
+ suggestions.append("测试提交充足:测试占比 {:.0f}%,质量意识强".format(test_count / total_commits * 100))
930
+
931
+ # 文档提交比例
932
+ if docs_count / total_commits < 0.05 and total_commits > 50:
933
+ suggestions.append("增加文档提交:文档相关提交仅占 {:.0f}%,建议及时更新文档".format(docs_count / total_commits * 100))
934
+ elif docs_count / total_commits >= 0.15:
935
+ suggestions.append("文档提交充足:文档占比 {:.0f}%,文档维护良好".format(docs_count / total_commits * 100))
936
+
937
+ # 重构提交比例
938
+ if refactor_count / total_commits < 0.03 and feat_count / total_commits > 0.3:
939
+ suggestions.append("定期重构:重构提交仅占 {:.0f}%,功能开发占比高,建议安排重构时间".format(refactor_count / total_commits * 100))
940
+
941
+ # other 类型提交比例
942
+ if other_count / total_commits > 0.25:
943
+ suggestions.append("规范 Commit 类型:{:.0f}% 的提交被归为 other,建议使用 feat/fix/refactor 等标准类型".format(other_count / total_commits * 100))
944
+
945
+ # chore 类型提交比例
946
+ if chore_count / total_commits > 0.15:
947
+ suggestions.append("自动化琐事:chore 类型提交占比 {:.0f}%,考虑用脚本或 CI 自动化".format(chore_count / total_commits * 100))
948
+
949
+ # 功能开发占比
950
+ if feat_count / total_commits > 0.4:
951
+ suggestions.append("功能开发为主:feat 占比 {:.0f}%,注意平衡新功能与维护".format(feat_count / total_commits * 100))
952
+
953
+ # Bug 修复占比
954
+ if fix_count / total_commits > 0.2:
955
+ suggestions.append("Bug 修复较多:fix 占比 {:.0f}%,建议加强测试预防".format(fix_count / total_commits * 100))
956
+
957
+ # ============================================================
958
+ # 17. 语言多样性
959
+ # ============================================================
960
+ projects = data.get('projects', [])
961
+ if projects:
962
+ lang_set = set()
963
+ for p in projects:
964
+ if isinstance(p, dict):
965
+ lang = p.get('language', '')
966
+ if lang and lang != 'Other':
967
+ lang_set.add(lang)
968
+ if len(lang_set) == 1:
969
+ suggestions.append("拓展技术栈:当前只使用 {} 语言,建议学习其他语言拓宽视野".format(list(lang_set)[0]))
970
+ elif len(lang_set) >= 5:
971
+ suggestions.append("聚焦核心技术:同时使用 {} 种语言,建议深耕 1-2 种核心语言".format(len(lang_set)))
972
+
973
+ # ============================================================
974
+ # 18. 提交频率波动
975
+ # ============================================================
976
+ if hourly:
977
+ max_hour = max(hourly)
978
+ min_hour = min(hourly)
979
+ if max_hour > 0 and min_hour / max_hour < 0.1:
980
+ suggestions.append("平衡提交时间:提交集中在特定时段,建议分散到全天各时段")
981
+
982
+ # ============================================================
983
+ # 19. 提交连续性
984
+ # ============================================================
985
+ if total_active_days > 0 and total_commits > 0:
986
+ commits_per_active_day = total_commits / total_active_days
987
+ if commits_per_active_day > 10:
988
+ suggestions.append("控制单日提交量:活跃日均提交 {:.0f} 次,建议拆分为更小的改动".format(commits_per_active_day))
989
+
990
+ # ============================================================
991
+ # 20. 提交间隔
992
+ # ============================================================
993
+ if total_active_days > 0 and total_commits > 0:
994
+ avg_interval = 24 * total_active_days / total_commits
995
+ if avg_interval < 1:
996
+ suggestions.append("降低提交频率:平均 {:.0f} 分钟一次提交,考虑合并相关改动".format(avg_interval * 60))
997
+
998
+ # ============================================================
999
+ # 21. 工作节奏一致性
1000
+ # ============================================================
1001
+ if hourly:
1002
+ total = sum(hourly)
1003
+ if total > 0:
1004
+ # 计算工作时间(9-18点)vs 非工作时间
1005
+ work_hours = sum(hourly[9:18])
1006
+ work_ratio = work_hours / total
1007
+ if work_ratio > 0.6:
1008
+ suggestions.append("工作节奏规律:{:.0f}% 的提交在工作时间,作息健康".format(work_ratio * 100))
1009
+ elif work_ratio < 0.3:
1010
+ suggestions.append("工作时间不规律:仅 {:.0f}% 的提交在工作时间,建议调整".format(work_ratio * 100))
1011
+
1012
+ # ============================================================
1013
+ # 22. 项目活跃度
1014
+ # ============================================================
1015
+ if projects and total_commits > 0:
1016
+ active_projects = sum(1 for p in projects if isinstance(p, dict) and p.get('commits', 0) > 10)
1017
+ if active_projects <= 2 and total_projects > 5:
1018
+ suggestions.append("项目活跃度不均:{} 个项目中仅 {} 个活跃,建议清理不活跃项目".format(total_projects, active_projects))
1019
+
1020
+ # ============================================================
1021
+ # 23. 提交消息长度
1022
+ # ============================================================
1023
+ if health['low_info_ratio'] < 5 and total_commits > 100:
1024
+ suggestions.append("提交消息质量高:{:.0f}% 的提交有详细描述,代码可维护性强".format(100 - health['low_info_ratio']))
1025
+
1026
+ # ============================================================
1027
+ # 24. 代码变更规模
1028
+ # ============================================================
1029
+ if total_commits > 0 and total_active_days > 0:
1030
+ avg_changes_per_commit = total_commits / total_active_days # 粗略估计
1031
+ if avg_changes_per_commit > 5:
1032
+ suggestions.append("单次提交较大:建议拆分为更小的提交,便于 Code Review")
1033
+
1034
+ # ============================================================
1035
+ # 25. 技术债务
1036
+ # ============================================================
1037
+ if health['refactor_ratio'] < 5 and health['feat_ratio'] > 40 and total_commits > 100:
1038
+ suggestions.append("技术债积累:重构仅占 {:.0f}%,建议定期安排重构时间".format(health['refactor_ratio']))
1039
+
1040
+ # ============================================================
1041
+ # 26. 测试覆盖趋势
1042
+ # ============================================================
1043
+ if health['test_ratio'] >= 20:
1044
+ suggestions.append("测试覆盖良好:测试文件占比 {:.0f}%,代码质量有保障".format(health['test_ratio']))
1045
+
1046
+ # ============================================================
1047
+ # 27. 文档覆盖趋势
1048
+ # ============================================================
1049
+ if health['doc_ratio'] >= 10:
1050
+ suggestions.append("文档覆盖良好:文档变更占比 {:.0f}%,项目可维护性强".format(health['doc_ratio']))
1051
+
1052
+ # ============================================================
1053
+ # 28. AI 协作深度
1054
+ # ============================================================
1055
+ ai_influence = ai.get('ai_influence_score', 0)
1056
+ if ai_influence > 70:
1057
+ suggestions.append("AI 深度协作:AI 影响分 {},建议定期审查 AI 生成的代码".format(ai_influence))
1058
+ elif ai_influence > 0 and ai_influence < 30:
1059
+ suggestions.append("AI 使用较少:AI 影响分 {},可尝试更多 AI 工具提升效率".format(ai_influence))
1060
+
1061
+ # ============================================================
1062
+ # 29. 工作生活平衡
1063
+ # ============================================================
1064
+ if health['night_ratio'] < 15 and health['weekend_ratio'] < 15:
1065
+ suggestions.append("工作生活平衡:夜间和周末提交都很少,作息健康")
1066
+
1067
+ # ============================================================
1068
+ # 30. 代码稳定性
1069
+ # ============================================================
1070
+ if health['fix_ratio'] < 5 and health['refactor_ratio'] < 5 and total_commits > 100:
1071
+ suggestions.append("代码稳定:Bug 修复和重构都很少,代码质量稳定")
1072
+
1073
+ # ============================================================
1074
+ # 31. 提交频率波动(小时级别)
1075
+ # ============================================================
1076
+ if hourly and len(hourly) == 24:
1077
+ # 计算变异系数
1078
+ import statistics
1079
+ mean_val = statistics.mean(hourly)
1080
+ if mean_val > 0:
1081
+ cv = statistics.stdev(hourly) / mean_val
1082
+ if cv > 1.5:
1083
+ suggestions.append("提交时间波动大:建议保持更稳定的工作节奏")
1084
+
1085
+ # ============================================================
1086
+ # 32. 项目集中度
1087
+ # ============================================================
1088
+ if projects and total_commits > 0:
1089
+ # 计算 HHI 指数(赫芬达尔指数)
1090
+ shares = [p.get('commits', 0) / total_commits for p in projects if isinstance(p, dict)]
1091
+ hhi = sum(s ** 2 for s in shares)
1092
+ if hhi < 0.1:
1093
+ suggestions.append("项目分散:提交分布均匀,建议聚焦核心项目")
1094
+ elif hhi > 0.5:
1095
+ suggestions.append("项目集中:大部分提交集中在少数项目,精力分配合理")
1096
+
1097
+ # ============================================================
1098
+ # 33. 周末提交模式
1099
+ # ============================================================
1100
+ if weekly:
1101
+ sat = weekly.get('周六', 0) if isinstance(weekly, dict) else (weekly[5] if len(weekly) > 5 else 0)
1102
+ sun = weekly.get('周日', 0) if isinstance(weekly, dict) else (weekly[6] if len(weekly) > 6 else 0)
1103
+ total_weekly = sum(weekly.values()) if isinstance(weekly, dict) else sum(weekly)
1104
+ if total_weekly > 0:
1105
+ sat_ratio = sat / total_weekly * 100
1106
+ sun_ratio = sun / total_weekly * 100
1107
+ if sat_ratio > 20 and sun_ratio < 5:
1108
+ suggestions.append("周六活跃:周六提交占比 {:.0f}%,周日较少,节奏不错".format(sat_ratio))
1109
+ elif sun_ratio > 20 and sat_ratio < 5:
1110
+ suggestions.append("周日活跃:周日提交占比 {:.0f}%,建议利用周六提前完成".format(sun_ratio))
1111
+
1112
+ # ============================================================
1113
+ # 34. 提交分布集中度
1114
+ # ============================================================
1115
+ if hourly and len(hourly) == 24:
1116
+ total = sum(hourly)
1117
+ if total > 0:
1118
+ top3_hours = sorted(range(24), key=lambda i: hourly[i], reverse=True)[:3]
1119
+ top3_ratio = sum(hourly[h] for h in top3_hours) / total * 100
1120
+ if top3_ratio > 50:
1121
+ suggestions.append("提交高度集中:最活跃的 3 小时占 {:.0f}% 提交,建议更均匀分布".format(top3_ratio))
1122
+ elif top3_ratio < 25:
1123
+ suggestions.append("提交分布均匀:各时段提交较分散,时间管理良好")
1124
+
1125
+ # ============================================================
1126
+ # 35. 测试与功能平衡
1127
+ # ============================================================
1128
+ if total_commits > 0:
1129
+ feat_count = commit_types.get('feat', 0)
1130
+ test_count = commit_types.get('test', 0)
1131
+ if feat_count > 0:
1132
+ test_feat_ratio = test_count / feat_count
1133
+ if test_feat_ratio < 0.3 and feat_count > 20:
1134
+ suggestions.append("测试跟不上功能:每 {} 个功能提交才对应 1 个测试提交,建议提高测试比例".format(int(1 / test_feat_ratio) if test_feat_ratio > 0 else "∞"))
1135
+ elif test_feat_ratio >= 0.8:
1136
+ suggestions.append("测试与功能同步:测试/功能比 {:.1f},质量意识强".format(test_feat_ratio))
1137
+
1138
+ # ============================================================
1139
+ # 36. 提交历史跨度
1140
+ # ============================================================
1141
+ if total_active_days > 0:
1142
+ if total_active_days > 300:
1143
+ suggestions.append("开发持续性强:活跃 {} 天,长期坚持 coding".format(total_active_days))
1144
+ elif total_active_days < 30 and total_commits > 50:
1145
+ suggestions.append("开发集中在短期:仅 {} 天活跃但有 {} 次提交,建议保持持续性".format(total_active_days, total_commits))
1146
+
1147
+ # ============================================================
1148
+ # 37. 工作日偏好
1149
+ # ============================================================
1150
+ if weekly:
1151
+ if isinstance(weekly, dict):
1152
+ max_day = max(weekly, key=weekly.get)
1153
+ min_day = min(weekly, key=weekly.get)
1154
+ max_val = weekly[max_day]
1155
+ min_val = weekly[min_day]
1156
+ total_weekly = sum(weekly.values())
1157
+ if total_weekly > 0 and max_val > 0:
1158
+ day_ratio = max_val / total_weekly * 100
1159
+ if day_ratio > 25:
1160
+ suggestions.append("工作日偏好明显:{} 占 {:.0f}% 提交,是最活跃的一天".format(max_day, day_ratio))
1161
+
1162
+ # ============================================================
1163
+ # 38. 提交时间分布
1164
+ # ============================================================
1165
+ if hourly and len(hourly) == 24:
1166
+ # 计算高峰时段
1167
+ max_val = max(hourly)
1168
+ peak_hours_list = [i for i, v in enumerate(hourly) if v > max_val * 0.8]
1169
+ if len(peak_hours_list) <= 3:
1170
+ suggestions.append("提交时间集中:高峰时段集中在 {} 点,建议分散到其他时段".format(', '.join([str(h) for h in peak_hours_list])))
1171
+
1172
+ # ============================================================
1173
+ # 39. 项目提交分布
1174
+ # ============================================================
1175
+ if projects and total_commits > 0:
1176
+ # 计算项目提交分布的熵
1177
+ import math
1178
+ shares = [p.get('commits', 0) / total_commits for p in projects if isinstance(p, dict) and p.get('commits', 0) > 0]
1179
+ entropy = -sum(s * math.log2(s) for s in shares if s > 0)
1180
+ max_entropy = math.log2(len(shares)) if len(shares) > 1 else 1
1181
+ if max_entropy > 0:
1182
+ normalized_entropy = entropy / max_entropy
1183
+ if normalized_entropy > 0.8:
1184
+ suggestions.append("项目分布均匀:提交分散在多个项目,建议聚焦核心项目")
1185
+ elif normalized_entropy < 0.3:
1186
+ suggestions.append("项目分布集中:大部分提交集中在少数项目,精力分配合理")
1187
+
1188
+ # ============================================================
1189
+ # 40. 开发习惯总结
1190
+ # ============================================================
1191
+ if habit_score['total'] >= 80:
1192
+ suggestions.append("开发习惯优秀:总分 {} 分,继续保持!".format(habit_score['total']))
1193
+ elif habit_score['total'] >= 60:
1194
+ suggestions.append("开发习惯良好:总分 {} 分,还有提升空间".format(habit_score['total']))
1195
+
1196
+ # ============================================================
1197
+ # 41. 项目类型多样性
1198
+ # ============================================================
1199
+ if projects and len(projects) > 0:
1200
+ # 计算项目提交分布的基尼系数
1201
+ commits_list = sorted([p.get('commits', 0) for p in projects if isinstance(p, dict)])
1202
+ n = len(commits_list)
1203
+ if n > 0 and sum(commits_list) > 0:
1204
+ cumulative = 0
1205
+ gini_sum = 0
1206
+ for i, c in enumerate(commits_list):
1207
+ cumulative += c
1208
+ gini_sum += (2 * (i + 1) - n - 1) * c
1209
+ gini = gini_sum / (n * sum(commits_list))
1210
+ if gini > 0.6:
1211
+ suggestions.append("项目分布不均:基尼系数 {:.2f},建议更均匀地分配精力".format(gini))
1212
+ elif gini < 0.2:
1213
+ suggestions.append("项目分布均匀:基尼系数 {:.2f},精力分配合理".format(gini))
1214
+
1215
+ # ============================================================
1216
+ # 42. 提交频率稳定性
1217
+ # ============================================================
1218
+ if hourly and len(hourly) == 24:
1219
+ # 计算提交时间的峰度
1220
+ mean_val = statistics.mean(hourly)
1221
+ if mean_val > 0 and len(hourly) > 3:
1222
+ variance = statistics.variance(hourly)
1223
+ if variance > 0:
1224
+ # 简化的峰度计算
1225
+ skewness = sum((x - mean_val) ** 3 for x in hourly) / (len(hourly) * (variance ** 1.5))
1226
+ if abs(skewness) > 1:
1227
+ suggestions.append("提交时间分布偏斜:建议保持更规律的工作节奏")
1228
+
1229
+ # ============================================================
1230
+ # 43. 项目活跃周期
1231
+ # ============================================================
1232
+ if projects and total_commits > 0:
1233
+ # 计算项目活跃周期(最近一次提交的时间)
1234
+ recent_projects = 0
1235
+ for p in projects:
1236
+ if isinstance(p, dict):
1237
+ last_commit = p.get('last_commit', '')
1238
+ if last_commit and '2026' in last_commit:
1239
+ recent_projects += 1
1240
+ if recent_projects <= 3 and total_projects > 10:
1241
+ suggestions.append("活跃项目少:{} 个项目中仅 {} 个最近有提交,建议清理不活跃项目".format(total_projects, recent_projects))
1242
+
1243
+ # ============================================================
1244
+ # 44. 提交频率建议
1245
+ # ============================================================
1246
+ if avg_daily > 0:
1247
+ if avg_daily < 0.5:
1248
+ suggestions.append("提交频率低:日均 {:.1f} 次提交,建议更频繁地提交代码".format(avg_daily))
1249
+ elif avg_daily > 5:
1250
+ suggestions.append("提交频率高:日均 {:.1f} 次提交,注意保持代码质量".format(avg_daily))
1251
+
1252
+ # ============================================================
1253
+ # 45. 开发效率
1254
+ # ============================================================
1255
+ if total_commits > 0 and total_active_days > 0:
1256
+ efficiency = total_commits / total_active_days
1257
+ if efficiency > 5:
1258
+ suggestions.append("开发效率高:每次活跃日平均 {:.0f} 次提交,产出稳定".format(efficiency))
1259
+ elif efficiency < 2:
1260
+ suggestions.append("开发效率待提升:每次活跃日平均 {:.0f} 次提交,建议提高效率".format(efficiency))
1261
+
1262
+ # ============================================================
1263
+ # 46. 功能 vs 维护平衡
1264
+ # ============================================================
1265
+ if total_commits > 0:
1266
+ feat_c = commit_types.get('feat', 0)
1267
+ fix_c = commit_types.get('fix', 0)
1268
+ refactor_c = commit_types.get('refactor', 0)
1269
+ maintenance = fix_c + refactor_c
1270
+ if feat_c > 0 and maintenance > 0:
1271
+ ratio = feat_c / maintenance
1272
+ if ratio > 5:
1273
+ suggestions.append("功能远超维护:功能/维护比 {:.1f}:1,注意技术债积累".format(ratio))
1274
+ elif ratio < 1:
1275
+ suggestions.append("维护为主:维护提交多于功能,代码在持续改善")
1276
+
1277
+ # ============================================================
1278
+ # 47. 项目状态分布
1279
+ # ============================================================
1280
+ if projects and total_projects > 3:
1281
+ active = sum(1 for p in projects if isinstance(p, dict) and p.get('commits', 0) > 10)
1282
+ inactive = total_projects - active
1283
+ if inactive > total_projects * 0.6:
1284
+ suggestions.append("不活跃项目多:{} 个项目中 {} 个提交不足 10 次,建议归档".format(total_projects, inactive))
1285
+
1286
+ # ============================================================
1287
+ # 48. 提交密度变化
1288
+ # ============================================================
1289
+ if hourly and len(hourly) == 24:
1290
+ total = sum(hourly)
1291
+ if total > 0:
1292
+ # 计算高密度时段(>平均值2倍)
1293
+ avg_per_hour = total / 24
1294
+ high_density_hours = [h for h in range(24) if hourly[h] > avg_per_hour * 2]
1295
+ if len(high_density_hours) <= 2 and len(high_density_hours) > 0:
1296
+ suggestions.append("编码时段集中:高峰仅在 {} 点,建议适当分散".format(', '.join(str(h) for h in high_density_hours)))
1297
+
1298
+ # ============================================================
1299
+ # 49. 周内提交均匀度
1300
+ # ============================================================
1301
+ if weekly and isinstance(weekly, dict) and len(weekly) == 7:
1302
+ values = list(weekly.values())
1303
+ total = sum(values)
1304
+ if total > 0:
1305
+ avg_per_day = total / 7
1306
+ variance = sum((v - avg_per_day) ** 2 for v in values) / 7
1307
+ if avg_per_day > 0:
1308
+ cv = (variance ** 0.5) / avg_per_day
1309
+ if cv > 0.8:
1310
+ suggestions.append("周内提交不均:各天差异大,建议保持更稳定的周节奏")
1311
+
1312
+ # ============================================================
1313
+ # 50. 功能开发占比
1314
+ # ============================================================
1315
+ if total_commits > 0:
1316
+ feat_c = commit_types.get('feat', 0)
1317
+ feat_pct = feat_c / total_commits * 100
1318
+ if feat_pct > 60:
1319
+ suggestions.append("功能开发为主:feat 占 {:.0f}%,建议平衡新功能与质量保障".format(feat_pct))
1320
+ elif feat_pct < 10 and total_commits > 50:
1321
+ suggestions.append("功能开发少:feat 仅占 {:.0f}%,开发以维护为主".format(feat_pct))
1322
+
1323
+ # ============================================================
1324
+ # 51. Chore 类型分析
1325
+ # ============================================================
1326
+ if total_commits > 0:
1327
+ chore_c = commit_types.get('chore', 0)
1328
+ chore_pct = chore_c / total_commits * 100
1329
+ if chore_pct > 20:
1330
+ suggestions.append("琐事较多:chore 占 {:.0f}%,建议用自动化脚本减少重复工作".format(chore_pct))
1331
+
1332
+ # ============================================================
1333
+ # 52. 提交历史长度
1334
+ # ============================================================
1335
+ if total_active_days > 0:
1336
+ if total_active_days > 200:
1337
+ suggestions.append("长期开发者:活跃超过 {} 天,积累了丰富的开发经验".format(total_active_days))
1338
+
1339
+ # ============================================================
1340
+ # 53. 最热门项目贡献
1341
+ # ============================================================
1342
+ if projects and total_commits > 0 and len(projects) > 1:
1343
+ max_commits = max(p.get('commits', 0) for p in projects if isinstance(p, dict))
1344
+ top_ratio = max_commits / total_commits * 100
1345
+ if top_ratio > 60:
1346
+ suggestions.append("单项目主导:最热门项目占 {:.0f}% 提交,精力高度集中".format(top_ratio))
1347
+ elif top_ratio < 20:
1348
+ suggestions.append("精力分散:最热门项目仅占 {:.0f}% 提交,建议聚焦核心项目".format(top_ratio))
1349
+
1350
+ # ============================================================
1351
+ # 54. 文档与代码比例
1352
+ # ============================================================
1353
+ if total_commits > 0:
1354
+ docs_c = commit_types.get('docs', 0)
1355
+ code_c = total_commits - docs_c
1356
+ if code_c > 0:
1357
+ doc_code_ratio = docs_c / code_c
1358
+ if doc_code_ratio > 0.3:
1359
+ suggestions.append("文档投入高:文档/代码比 {:.1f},文档维护良好".format(doc_code_ratio))
1360
+ elif doc_code_ratio < 0.05 and total_commits > 50:
1361
+ suggestions.append("文档投入不足:文档/代码比 {:.2f},建议增加文档更新".format(doc_code_ratio))
1362
+
1363
+ # ============================================================
1364
+ # 55. 早起 vs 夜猫子
1365
+ # ============================================================
1366
+ if hourly and len(hourly) == 24:
1367
+ total = sum(hourly)
1368
+ if total > 0:
1369
+ morning = sum(hourly[5:9]) # 5-8点
1370
+ night = sum(hourly[22:24]) + sum(hourly[0:2]) # 22-1点
1371
+ if morning > night * 2 and morning > 0:
1372
+ suggestions.append("早起型开发者:早晨提交多于深夜,作息健康")
1373
+ elif night > morning * 2 and night > 0:
1374
+ suggestions.append("夜猫子型开发者:深夜提交多于早晨,建议调整作息")
1375
+
1376
+ # ============================================================
1377
+ # 56. 提交稳定性
1378
+ # ============================================================
1379
+ if total_active_days > 10 and total_commits > 0:
1380
+ commits_per_day = total_commits / total_active_days
1381
+ if commits_per_day >= 2 and commits_per_day <= 6:
1382
+ suggestions.append("提交节奏稳定:活跃日均 {:.1f} 次提交,节奏适中".format(commits_per_day))
1383
+
1384
+ # ============================================================
1385
+ # 57. 多语言项目管理
1386
+ # ============================================================
1387
+ if projects:
1388
+ lang_project_count = {}
1389
+ for p in projects:
1390
+ if isinstance(p, dict):
1391
+ lang = p.get('language', '')
1392
+ if lang and lang != 'Other':
1393
+ lang_project_count[lang] = lang_project_count.get(lang, 0) + 1
1394
+ if len(lang_project_count) >= 3:
1395
+ top_lang = max(lang_project_count, key=lang_project_count.get)
1396
+ suggestions.append("多语言开发者:使用 {} 种语言,{} 项目最多".format(len(lang_project_count), top_lang))
1397
+
1398
+ # ============================================================
1399
+ # 58. 其他类型提交分析
1400
+ # ============================================================
1401
+ if total_commits > 0:
1402
+ other_c = commit_types.get('other', 0)
1403
+ other_pct = other_c / total_commits * 100
1404
+ if other_pct > 30:
1405
+ suggestions.append("提交分类不清:{:.0f}% 归为 other,建议使用标准 commit 类型".format(other_pct))
1406
+ elif other_pct < 10 and total_commits > 50:
1407
+ suggestions.append("提交分类规范:other 仅占 {:.0f}%,类型使用清晰".format(other_pct))
1408
+
1409
+ # ============================================================
1410
+ # 59. 功能开发深度
1411
+ # ============================================================
1412
+ if total_commits > 0:
1413
+ feat_c = commit_types.get('feat', 0)
1414
+ fix_c = commit_types.get('fix', 0)
1415
+ if feat_c > 0 and fix_c > 0:
1416
+ feat_fix_ratio = feat_c / fix_c
1417
+ if feat_fix_ratio > 4:
1418
+ suggestions.append("功能驱动:功能/修复比 {:.1f}:1,开发节奏快".format(feat_fix_ratio))
1419
+ elif feat_fix_ratio < 1:
1420
+ suggestions.append("修复驱动:修复多于新功能,建议分析根本原因")
1421
+
1422
+ # ============================================================
1423
+ # 60. 项目维护活跃度
1424
+ # ============================================================
1425
+ if projects and total_commits > 0:
1426
+ well_maintained = sum(1 for p in projects if isinstance(p, dict) and p.get('commits', 0) > 30)
1427
+ if well_maintained >= 3:
1428
+ suggestions.append("多项目深耕:{} 个项目提交超过 30 次,项目管理能力强".format(well_maintained))
1429
+
1430
+ # === 默认 ===
1431
+ if not suggestions:
1432
+ suggestions.append("继续保持良好的开发习惯!各项指标都很健康")
1433
+
1434
+ html = ""
1435
+ for i, s in enumerate(suggestions, 1):
1436
+ html += f'''
1437
+ <div class="sug-item">
1438
+ <div class="sug-num">{i}</div>
1439
+ <div class="sug-text">{s}</div>
1440
+ </div>'''
1441
+ return html
1442
+
1443
+
1444
+ # ============================================================
1445
+ # JS 脚本构建(普通字符串,不需要 f-string 转义)
1446
+ # ============================================================
1447
+
1448
+ def _build_js(all_commits_json, project_names_json, project_meta_json,
1449
+ month_labels_json, type_labels_json, type_names_json, type_colors_json,
1450
+ colors_json, lang_labels_json, lang_values_json,
1451
+ eng_spectrum, ai_spectrum, test_ratio, doc_ratio,
1452
+ ai_detected, ai_count, low_info_ratio,
1453
+ init_hourly_json, init_weekly_json, init_type_values_json,
1454
+ init_stack_json, init_bubble_json, init_month_labels_json,
1455
+ multi_year, monthly_ai_json):
1456
+
1457
+ return f"""
1458
+ // 原始数据
1459
+ const allCommits = {all_commits_json};
1460
+ const projectNames = {project_names_json};
1461
+ const projectMeta = {project_meta_json};
1462
+ const typeLabels = {type_labels_json};
1463
+ const typeNames = {type_names_json};
1464
+ const MULTI_YEAR = {multi_year};
1465
+ const typeColors = {type_colors_json};
1466
+ const COLORS = {colors_json};
1467
+ const langLabels = {lang_labels_json};
1468
+ const langValues = {lang_values_json};
1469
+
1470
+ // 月份格式化(跨年时带年份)
1471
+ function fmtMonth(m) {{
1472
+ return MULTI_YEAR ? m.slice(0, 4) + '年' + m.slice(5, 7) + '月' : m.slice(5, 7) + '月';
1473
+ }}
1474
+
1475
+ // 旧数据兜底默认值
1476
+ const ORIG_ENG_SPECTRUM = {eng_spectrum};
1477
+ const ORIG_AI_SPECTRUM = {ai_spectrum};
1478
+ const ORIG_TEST_RATIO = {test_ratio};
1479
+ const ORIG_DOC_RATIO = {doc_ratio};
1480
+ const ORIG_AI_DETECTED = {ai_detected};
1481
+ const ORIG_AI_COUNT = {ai_count};
1482
+ const ORIG_LOW_INFO_RATIO = {low_info_ratio};
1483
+
1484
+ // AI 月度趋势数据
1485
+ const monthlyAI = {monthly_ai_json};
1486
+
1487
+ allCommits.forEach(c => {{
1488
+ const meta = projectMeta[c.project] || {{}};
1489
+ if (!c.language) c.language = meta.language || 'Unknown';
1490
+ c.fileChangeCount = Number(c.file_change_count ?? c.fileChangeCount ?? 0);
1491
+ c.testFiles = Number(c.test_files ?? c.testFiles ?? 0);
1492
+ c.docFiles = Number(c.doc_files ?? c.docFiles ?? 0);
1493
+ c.lowInfo = Boolean(c.low_info ?? c.lowInfo ?? false);
1494
+ c.aiSignal = Boolean(c.ai_signal ?? c.aiSignal ?? false);
1495
+ c.repoAiSignal = Boolean(c.repo_ai_signal ?? c.repoAiSignal ?? false);
1496
+ c.classificationConfidence = c.classification_confidence || c.classificationConfidence || 'low';
1497
+ }});
1498
+
1499
+ function dateStartTs(dateText) {{
1500
+ return new Date(dateText + 'T00:00:00').getTime() / 1000;
1501
+ }}
1502
+
1503
+ function dateEndTs(dateText) {{
1504
+ return new Date(dateText + 'T23:59:59').getTime() / 1000;
1505
+ }}
1506
+
1507
+ function dateTextFromTs(ts) {{
1508
+ return formatDateInput(new Date(ts * 1000));
1509
+ }}
1510
+
1511
+ function getProjectCommits(project) {{
1512
+ return project === 'all' ? allCommits : allCommits.filter(c => c.project === project);
1513
+ }}
1514
+
1515
+ function syncDateInputsToCommits(commits) {{
1516
+ const sinceInput = document.getElementById('filterSince');
1517
+ const untilInput = document.getElementById('filterUntil');
1518
+ if (!commits.length) {{
1519
+ sinceInput.value = '';
1520
+ untilInput.value = '';
1521
+ return;
1522
+ }}
1523
+ const timestamps = commits.map(c => c.ts);
1524
+ sinceInput.value = dateTextFromTs(Math.min(...timestamps));
1525
+ untilInput.value = dateTextFromTs(Math.max(...timestamps));
1526
+ }}
1527
+
1528
+ function formatDateInput(date) {{
1529
+ const y = date.getFullYear();
1530
+ const m = String(date.getMonth() + 1).padStart(2, '0');
1531
+ const d = String(date.getDate()).padStart(2, '0');
1532
+ return `${{y}}-${{m}}-${{d}}`;
1533
+ }}
1534
+
1535
+ function shiftMonths(date, months) {{
1536
+ const shifted = new Date(date);
1537
+ const originalDay = shifted.getDate();
1538
+ shifted.setMonth(shifted.getMonth() + months);
1539
+ if (shifted.getDate() !== originalDay) shifted.setDate(0);
1540
+ return shifted;
1541
+ }}
1542
+
1543
+ function markCustomRange() {{
1544
+ document.getElementById('filterRange').value = 'custom';
1545
+ }}
1546
+
1547
+ function applyPresetRange() {{
1548
+ const range = document.getElementById('filterRange').value;
1549
+ const sinceInput = document.getElementById('filterSince');
1550
+ const untilInput = document.getElementById('filterUntil');
1551
+
1552
+ if (range === 'all') {{
1553
+ const project = document.getElementById('filterProject').value;
1554
+ syncDateInputsToCommits(getProjectCommits(project));
1555
+ }} else if (range !== 'custom') {{
1556
+ const until = new Date();
1557
+ const since = range === '1m'
1558
+ ? shiftMonths(until, -1)
1559
+ : range === '6m'
1560
+ ? shiftMonths(until, -6)
1561
+ : shiftMonths(until, -12);
1562
+ sinceInput.value = formatDateInput(since);
1563
+ untilInput.value = formatDateInput(until);
1564
+ }}
1565
+
1566
+ applyFilters();
1567
+ }}
1568
+
1569
+ // 填充项目下拉
1570
+ const select = document.getElementById('filterProject');
1571
+ projectNames.forEach(name => {{
1572
+ const opt = document.createElement('option');
1573
+ opt.value = name;
1574
+ opt.textContent = name;
1575
+ select.appendChild(opt);
1576
+ }});
1577
+
1578
+ // Chart.js 配置
1579
+ const chartDefaults = {{
1580
+ responsive: true,
1581
+ maintainAspectRatio: false,
1582
+ plugins: {{ legend: {{ labels: {{ padding: 14, usePointStyle: true, font: {{ size: 11 }} }} }} }}
1583
+ }};
1584
+ const charts = {{}};
1585
+
1586
+ // 工具函数
1587
+ function getScoreColor(score) {{
1588
+ if (score >= 80) return '#1a7f37';
1589
+ if (score >= 60) return '#bf8700';
1590
+ return '#cf222e';
1591
+ }}
1592
+ function getScoreLabel(score) {{
1593
+ if (score >= 80) return '优秀';
1594
+ if (score >= 60) return '良好';
1595
+ if (score >= 40) return '一般';
1596
+ return '需改进';
1597
+ }}
1598
+
1599
+ // 人格名称映射(MBTI 风格)
1600
+ const FRONTEND_LANGUAGES = new Set(['JavaScript', 'TypeScript', 'HTML', 'CSS']);
1601
+
1602
+ function pickPersona(agg, personaCode, timeSpectrum, rhythmSpectrum, focusSpectrum, styleSpectrum) {{
1603
+ const projectCount = Object.keys(agg.projectCounts).length;
1604
+ const frontendCommits = Object.entries(agg.languageCounts)
1605
+ .filter(([lang]) => FRONTEND_LANGUAGES.has(lang))
1606
+ .reduce((sum, [, count]) => sum + count, 0);
1607
+ const frontendRatio = agg.total > 0 ? frontendCommits / agg.total : 0;
1608
+ const choreRatio = agg.total > 0 ? (agg.types.chore || 0) / agg.total : 0;
1609
+
1610
+ const topHackerScore = (agg.total >= 200) + (agg.avgPerDay >= 2) + (projectCount >= 5) + (agg.engSpectrum >= 50);
1611
+ const personas = [
1612
+ {{name: '顶级黑客', icon: '⚡', desc: '跨项目、高频率、能交付也能打磨,像是把工程现场当主场。', score: topHackerScore >= 3 ? topHackerScore : 0}},
1613
+ {{name: '前端狂魔', icon: '🎨', desc: 'UI、交互和产品触感占据主舞台,对用户看到的那一面格外敏感。', score: frontendRatio >= 0.35 ? 1 : 0}},
1614
+ {{name: '基建达人', icon: '🏗️', desc: '热衷把脚手架、依赖、CI 和工程底座收拾顺手,别人开工会更舒服。', score: choreRatio >= 0.18 ? 1 : 0}},
1615
+ {{name: '测试守门人', icon: '✅', desc: '不太信任“应该没问题”,更喜欢让测试替自己站岗。', score: agg.testRatio >= 0.20 ? 1 : 0}},
1616
+ {{name: '文档布道者', icon: '📚', desc: '知道未来的自己也会忘,所以愿意把上下文写清楚。', score: agg.docRatio >= 0.18 ? 1 : 0}},
1617
+ {{name: 'Bug 猎手', icon: '🎯', desc: '对异常、回归和边界条件很敏感,擅长把系统拉回正轨。', score: agg.fixRatio >= 0.25 ? 1 : 0}},
1618
+ {{name: '重构外科医生', icon: '🔧', desc: '看不得结构别扭,喜欢在不惊动用户的地方让代码重新呼吸。', score: agg.refactorRatio >= 0.10 ? 1 : 0}},
1619
+ {{name: '产品推进器', icon: '🚀', desc: '目标感强,喜欢把想法迅速推到可运行、可体验的状态。', score: agg.featRatio >= 0.35 ? 1 : 0}},
1620
+ {{name: '多仓调度大师', icon: '🧭', desc: '能在多个项目之间切换上下文,脑内自带任务看板。', score: projectCount >= 10 && agg.focusIndex < 0.70 ? 1 : 0}},
1621
+ {{name: '深夜黑客', icon: '🌙', desc: '夜深后状态更容易上线,安静时间是你的高产窗口。', score: agg.nightRatio >= 0.35 ? 1 : 0}},
1622
+ {{name: 'AI 搭子型开发者', icon: '🤖', desc: '会把 AI 当成结对伙伴,用它放大探索、实现和整理能力。', score: agg.aiDetected ? 1 : 0}},
1623
+ {{name: '长期主义维护者', icon: '🛠️', desc: '节奏稳、耐心足,愿意把项目长期照看在可维护状态。', score: 1}},
1624
+ ];
1625
+ const selected = personas.reduce((best, item) => item.score > best.score ? item : best, personas[0]);
1626
+
1627
+ const sideTags = [];
1628
+ if (selected.name !== '前端狂魔' && frontendRatio >= 0.25) sideTags.push('前端狂魔');
1629
+ if (selected.name !== '基建达人' && choreRatio >= 0.12) sideTags.push('基建达人');
1630
+ if (selected.name !== '多仓调度大师' && projectCount >= 10) sideTags.push('多仓调度大师');
1631
+ if (selected.name !== 'AI 搭子型开发者' && agg.aiDetected) sideTags.push('AI 搭子');
1632
+ if (selected.name !== '测试守门人' && agg.testRatio >= 0.15) sideTags.push('测试守门人');
1633
+ if (selected.name !== '文档布道者' && agg.docRatio >= 0.10) sideTags.push('文档布道者');
1634
+ if (selected.name !== '深夜黑客' && agg.nightRatio >= 0.30) sideTags.push('深夜黑客');
1635
+
1636
+ const descParts = [];
1637
+ descParts.push(personaCode[0] === 'N' ? `夜间 ${{timeSpectrum}}%` : `白天 ${{100 - timeSpectrum}}%`);
1638
+ descParts.push(personaCode[1] === 'S' ? `高频提交 ${{rhythmSpectrum}}%` : `稳定推进 ${{100 - rhythmSpectrum}}%`);
1639
+ descParts.push(personaCode[2] === 'C' ? `核心项目 ${{focusSpectrum}}%` : `多项目并行 ${{100 - focusSpectrum}}%`);
1640
+ descParts.push(personaCode[3] === 'P' ? `新功能 ${{styleSpectrum}}%` : `维护系统 ${{100 - styleSpectrum}}%`);
1641
+ let detail = descParts.join(',');
1642
+ if (sideTags.length) detail += ';副人格:' + sideTags.slice(0, 3).join(' / ');
1643
+ detail += `;证据:${{projectCount}} 个项目、${{agg.total}} 次提交、日均 ${{agg.avgPerDay.toFixed(1)}} 次`;
1644
+
1645
+ return {{...selected, detail}};
1646
+ }};
1647
+
1648
+ // ============================================================
1649
+ // 图表初始化
1650
+ // ============================================================
1651
+ function initCharts(hourly, weekly, typeValues, stackData, bubbleData, monthLabels) {{
1652
+ // 24小时
1653
+ charts.hour = new Chart(document.getElementById('hourChart'), {{
1654
+ type: 'bar',
1655
+ data: {{
1656
+ labels: Array.from({{length: 24}}, (_, i) => i.toString().padStart(2, '0') + ':00'),
1657
+ datasets: [{{
1658
+ data: hourly,
1659
+ backgroundColor: hourly.map(v => {{
1660
+ const max = Math.max(...hourly, 1);
1661
+ return `rgba(9,105,218,${{0.2 + v/max*0.8}})`;
1662
+ }}),
1663
+ borderRadius: 6
1664
+ }}]
1665
+ }},
1666
+ options: {{
1667
+ ...chartDefaults,
1668
+ plugins: {{ legend: {{ display: false }} }},
1669
+ scales: {{
1670
+ y: {{ beginAtZero: true, grid: {{ color: 'rgba(0,0,0,0.04)' }} }},
1671
+ x: {{ grid: {{ display: false }}, ticks: {{ font: {{ size: 10 }} }} }}
1672
+ }}
1673
+ }}
1674
+ }});
1675
+
1676
+ // 星期
1677
+ charts.week = new Chart(document.getElementById('weekChart'), {{
1678
+ type: 'bar',
1679
+ data: {{
1680
+ labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
1681
+ datasets: [{{
1682
+ data: weekly,
1683
+ backgroundColor: ['#0969da','#0969da','#0969da','#0969da','#0969da','#8250df','#8250df'],
1684
+ borderRadius: 6
1685
+ }}]
1686
+ }},
1687
+ options: {{
1688
+ ...chartDefaults,
1689
+ plugins: {{ legend: {{ display: false }} }},
1690
+ scales: {{
1691
+ y: {{ beginAtZero: true, grid: {{ color: 'rgba(0,0,0,0.04)' }} }},
1692
+ x: {{ grid: {{ display: false }} }}
1693
+ }}
1694
+ }}
1695
+ }});
1696
+
1697
+ // 每月堆叠
1698
+ charts.monthly = new Chart(document.getElementById('monthlyChart'), {{
1699
+ type: 'bar',
1700
+ data: {{ labels: monthLabels, datasets: stackData }},
1701
+ options: {{
1702
+ ...chartDefaults,
1703
+ plugins: {{
1704
+ legend: {{ position: 'bottom', labels: {{ padding: 12, usePointStyle: true, font: {{ size: 11 }} }} }},
1705
+ tooltip: {{ mode: 'index', intersect: false }}
1706
+ }},
1707
+ scales: {{
1708
+ x: {{ stacked: true, grid: {{ display: false }} }},
1709
+ y: {{ stacked: true, beginAtZero: true, grid: {{ color: 'rgba(0,0,0,0.04)' }} }}
1710
+ }}
1711
+ }}
1712
+ }});
1713
+
1714
+ // 气泡图
1715
+ charts.bubble = new Chart(document.getElementById('bubbleChart'), {{
1716
+ type: 'bubble',
1717
+ data: {{ labels: monthLabels, datasets: bubbleData }},
1718
+ options: {{
1719
+ ...chartDefaults,
1720
+ plugins: {{
1721
+ legend: {{ position: 'bottom', labels: {{ padding: 10, usePointStyle: true, font: {{ size: 10 }} }} }},
1722
+ tooltip: {{
1723
+ callbacks: {{
1724
+ label: function(ctx) {{
1725
+ return ctx.dataset.label + ': ' + (ctx.parsed.y) + ' 次';
1726
+ }}
1727
+ }}
1728
+ }}
1729
+ }},
1730
+ scales: {{
1731
+ x: {{ type: 'category', offset: true, grid: {{ display: false }} }},
1732
+ y: {{ beginAtZero: true, grid: {{ color: 'rgba(0,0,0,0.04)' }} }}
1733
+ }}
1734
+ }}
1735
+ }});
1736
+
1737
+ // Commit 类型
1738
+ charts.type = new Chart(document.getElementById('typeChart'), {{
1739
+ type: 'doughnut',
1740
+ data: {{
1741
+ labels: typeNames,
1742
+ datasets: [{{ data: typeValues, backgroundColor: typeColors, borderWidth: 0 }}]
1743
+ }},
1744
+ options: {{
1745
+ ...chartDefaults,
1746
+ plugins: {{ legend: {{ position: 'bottom', labels: {{ padding: 12, usePointStyle: true }} }} }},
1747
+ cutout: '60%'
1748
+ }}
1749
+ }});
1750
+
1751
+ // 语言分布
1752
+ charts.lang = new Chart(document.getElementById('langChart'), {{
1753
+ type: 'doughnut',
1754
+ data: {{
1755
+ labels: langLabels,
1756
+ datasets: [{{ data: langValues, backgroundColor: COLORS, borderWidth: 0 }}]
1757
+ }},
1758
+ options: {{
1759
+ ...chartDefaults,
1760
+ plugins: {{ legend: {{ position: 'bottom', labels: {{ padding: 12, usePointStyle: true }} }} }},
1761
+ cutout: '60%'
1762
+ }}
1763
+ }});
1764
+
1765
+ // AI 趋势图
1766
+ const aiTrendCanvas = document.getElementById('aiTrendChart');
1767
+ if (aiTrendCanvas && Object.keys(monthlyAI).length > 0) {{
1768
+ const sortedMonths = Object.keys(monthlyAI).sort();
1769
+ const aiData = sortedMonths.map(m => monthlyAI[m]);
1770
+ const monthLabels2 = sortedMonths.map(fmtMonth);
1771
+
1772
+ charts.aiTrend = new Chart(aiTrendCanvas, {{
1773
+ type: 'line',
1774
+ data: {{
1775
+ labels: monthLabels2,
1776
+ datasets: [{{
1777
+ label: 'AI 辅助提交',
1778
+ data: aiData,
1779
+ borderColor: '#10b981',
1780
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
1781
+ fill: true,
1782
+ tension: 0.3,
1783
+ pointRadius: 4,
1784
+ pointHoverRadius: 6
1785
+ }}]
1786
+ }},
1787
+ options: {{
1788
+ ...chartDefaults,
1789
+ plugins: {{
1790
+ legend: {{ display: false }},
1791
+ tooltip: {{
1792
+ callbacks: {{
1793
+ label: function(ctx) {{
1794
+ return ctx.parsed.y + ' 次 AI 辅助提交';
1795
+ }}
1796
+ }}
1797
+ }}
1798
+ }},
1799
+ scales: {{
1800
+ x: {{ grid: {{ display: false }} }},
1801
+ y: {{ beginAtZero: true, grid: {{ color: 'rgba(0,0,0,0.04)' }} }}
1802
+ }}
1803
+ }}
1804
+ }});
1805
+ }}
1806
+ }}
1807
+
1808
+ // ============================================================
1809
+ // 筛选逻辑
1810
+ // ============================================================
1811
+ function applyFilters() {{
1812
+ const project = document.getElementById('filterProject').value;
1813
+ const range = document.getElementById('filterRange').value;
1814
+ const since = document.getElementById('filterSince').value;
1815
+ const until = document.getElementById('filterUntil').value;
1816
+
1817
+ let filtered = getProjectCommits(project);
1818
+ if (range === 'all') {{
1819
+ syncDateInputsToCommits(filtered);
1820
+ }} else {{
1821
+ if (since) {{
1822
+ const ts = dateStartTs(since);
1823
+ filtered = filtered.filter(c => c.ts >= ts);
1824
+ }}
1825
+ if (until) {{
1826
+ const ts = dateEndTs(until);
1827
+ filtered = filtered.filter(c => c.ts <= ts);
1828
+ }}
1829
+ }}
1830
+ if (filtered.length === 0) {{ alert('没有符合条件的数据'); return; }}
1831
+ updateAll(filtered, project);
1832
+ }}
1833
+
1834
+ function resetFilters() {{
1835
+ document.getElementById('filterProject').value = 'all';
1836
+ document.getElementById('filterRange').value = 'all';
1837
+ syncDateInputsToCommits(allCommits);
1838
+ updateAll(allCommits, 'all');
1839
+ }}
1840
+
1841
+ // ============================================================
1842
+ // 聚合计算
1843
+ // ============================================================
1844
+ function aggregate(commits) {{
1845
+ const total = commits.length;
1846
+ const hourly = new Array(24).fill(0);
1847
+ const weekly = new Array(7).fill(0);
1848
+ const monthly = {{}};
1849
+ const types = {{}};
1850
+ const projectCounts = {{}};
1851
+ const languageCounts = {{}};
1852
+ const projectActiveDays = {{}};
1853
+ const activeDays = new Set();
1854
+ let fileChanges = 0;
1855
+ let testFiles = 0;
1856
+ let docFiles = 0;
1857
+ let lowInfoCommits = 0;
1858
+ let aiSignalCount = 0;
1859
+ let aiToolingSignalCount = 0;
1860
+ const confidenceCounts = {{high: 0, medium: 0, low: 0}};
1861
+
1862
+ commits.forEach(c => {{
1863
+ const language = c.language || (projectMeta[c.project] || {{}}).language || 'Unknown';
1864
+ hourly[c.hour]++;
1865
+ weekly[c.weekday]++;
1866
+ monthly[c.month] = (monthly[c.month] || 0) + 1;
1867
+ types[c.type] = (types[c.type] || 0) + 1;
1868
+ projectCounts[c.project] = (projectCounts[c.project] || 0) + 1;
1869
+ languageCounts[language] = (languageCounts[language] || 0) + 1;
1870
+ const dayKey = Math.floor(c.ts / 86400);
1871
+ activeDays.add(dayKey);
1872
+ if (!projectActiveDays[c.project]) projectActiveDays[c.project] = new Set();
1873
+ projectActiveDays[c.project].add(dayKey);
1874
+ fileChanges += c.fileChangeCount || 0;
1875
+ testFiles += c.testFiles || 0;
1876
+ docFiles += c.docFiles || 0;
1877
+ if (c.lowInfo) lowInfoCommits++;
1878
+ if (c.aiSignal) aiSignalCount++;
1879
+ if (c.repoAiSignal) aiToolingSignalCount++;
1880
+ confidenceCounts[c.classificationConfidence] = (confidenceCounts[c.classificationConfidence] || 0) + 1;
1881
+ }});
1882
+
1883
+ const days = activeDays.size || 1;
1884
+ const avgPerDay = total / days;
1885
+ const nightCommits = hourly.slice(22).reduce((a, b) => a + b, 0) + hourly.slice(0, 5).reduce((a, b) => a + b, 0);
1886
+ const nightRatio = total > 0 ? nightCommits / total : 0;
1887
+ const dayCommits = hourly.slice(8, 20).reduce((a, b) => a + b, 0);
1888
+ const lateCommits = hourly.slice(20, 24).reduce((a, b) => a + b, 0) + hourly.slice(0, 6).reduce((a, b) => a + b, 0);
1889
+ const weekendCommits = weekly[5] + weekly[6];
1890
+ const weekendRatio = total > 0 ? weekendCommits / total : 0;
1891
+ const featRatio = total > 0 ? (types.feat || 0) / total : 0;
1892
+ const fixRatio = total > 0 ? (types.fix || 0) / total : 0;
1893
+ const refactorRatio = total > 0 ? (types.refactor || 0) / total : 0;
1894
+ const maintenanceRatio = fixRatio + refactorRatio + (total > 0 ? (types.chore || 0) / total : 0);
1895
+ const testRatio = fileChanges > 0 ? testFiles / fileChanges : ORIG_TEST_RATIO / 100;
1896
+ const docRatio = fileChanges > 0 ? docFiles / fileChanges : ORIG_DOC_RATIO / 100;
1897
+ const lowInfoRatio = total > 0 ? lowInfoCommits / total : ORIG_LOW_INFO_RATIO / 100;
1898
+ const lowConfidenceRatio = total > 0 ? confidenceCounts.low / total : 0;
1899
+ const engSpectrum = Math.round(Math.min((testRatio + docRatio) / 0.25 * 100, 100));
1900
+ const aiDetected = aiSignalCount >= 3 || aiToolingSignalCount > 0;
1901
+ const aiCommitRatio = total > 0 ? aiSignalCount / total : 0;
1902
+ const aiToolingRatio = total > 0 ? aiToolingSignalCount / total : 0;
1903
+ const aiSpectrum = Math.round(Math.min(aiCommitRatio * 300 + Math.min(aiToolingRatio * 15, 15), 100));
1904
+
1905
+ const projSorted = Object.entries(projectCounts).sort((a, b) => b[1] - a[1]);
1906
+ const top3Commits = projSorted.slice(0, 3).reduce((a, b) => a + b[1], 0);
1907
+ const focusIndex = total > 0 ? top3Commits / total : 0;
1908
+
1909
+ return {{
1910
+ total, hourly, weekly, monthly, types, projectCounts, languageCounts, projectActiveDays, activeDays, days,
1911
+ avgPerDay, nightRatio, dayCommits, lateCommits, weekendRatio,
1912
+ featRatio, fixRatio, refactorRatio, maintenanceRatio,
1913
+ testRatio, docRatio, lowInfoRatio, lowConfidenceRatio, confidenceCounts, engSpectrum, aiDetected, aiSignalCount, aiToolingSignalCount, aiCommitRatio, aiToolingRatio, aiSpectrum,
1914
+ projSorted, top3Commits, focusIndex
1915
+ }};
1916
+ }}
1917
+
1918
+ // ============================================================
1919
+ // 更新统计卡片
1920
+ // ============================================================
1921
+ function updateStats(agg, project) {{
1922
+ document.getElementById('statCommits').textContent = agg.total;
1923
+ document.getElementById('statProjects').textContent = project === 'all' ? Object.keys(agg.projectCounts).length : 1;
1924
+ document.getElementById('statDaily').textContent = agg.avgPerDay.toFixed(1);
1925
+ const projText = project === 'all' ? Object.keys(agg.projectCounts).length + ' 个项目' : project;
1926
+ document.getElementById('headerSubtitle').textContent = `${{projText}} · ${{agg.total}} 次提交 · ${{agg.days}} 天活跃`;
1927
+ }}
1928
+
1929
+ // ============================================================
1930
+ // 更新 DevPersona
1931
+ // ============================================================
1932
+ function updatePersona(agg) {{
1933
+ const timeSpectrum = Math.round(agg.lateCommits / Math.max(agg.dayCommits + agg.lateCommits, 1) * 100);
1934
+ const rhythmSpectrum = Math.round(Math.min(agg.avgPerDay / 5 * 100, 100));
1935
+ const focusSpectrum = Math.round(agg.focusIndex * 100);
1936
+ const styleSpectrum = Math.round(agg.featRatio / Math.max(agg.featRatio + agg.maintenanceRatio, 0.01) * 100);
1937
+
1938
+ const t = timeSpectrum > 50 ? 'N' : 'D';
1939
+ const r = rhythmSpectrum > 50 ? 'S' : 'M';
1940
+ const f = focusSpectrum > 50 ? 'C' : 'D';
1941
+ const s = styleSpectrum > 50 ? 'P' : 'G';
1942
+ const e = agg.engSpectrum > 50 ? 'Q' : 'R';
1943
+ const a = agg.aiSpectrum > 50 ? 'A' : 'H';
1944
+ const personaCode = t + r + f + s + e + a;
1945
+ const personaInfo = pickPersona(agg, personaCode, timeSpectrum, rhythmSpectrum, focusSpectrum, styleSpectrum);
1946
+
1947
+ document.getElementById('personaCard').innerHTML = `
1948
+ <div style="font-size:3em;margin-bottom:8px;">${{personaInfo.icon}}</div>
1949
+ <div style="font-size:1.6em;font-weight:700;color:#1f2328;">${{personaInfo.name}}</div>
1950
+ <div style="font-size:1.1em;color:#0969da;font-weight:600;margin-top:4px;font-family:monospace;">${{personaCode}}</div>
1951
+ <div style="color:#1f2328;font-size:0.95em;margin-top:8px;font-weight:500;">${{personaInfo.desc}}</div>
1952
+ <div style="color:#656d76;font-size:0.85em;margin-top:4px;">${{personaInfo.detail}}</div>
1953
+ `;
1954
+
1955
+ // 维度光谱
1956
+ const dims = [
1957
+ {{key: 'time', name: '时间偏好', code: t, spectrum: timeSpectrum, left: '白天型', right: '夜猫型', leftCode: 'D', rightCode: 'N', leftTrait: '日间掌控者', rightTrait: '夜间爆发者', leftDesc: '白天更容易进入状态,节奏清晰。', rightDesc: '安静时段更容易输出,灵感来得晚。'}},
1958
+ {{key: 'rhythm', name: '节奏风格', code: r, spectrum: rhythmSpectrum, left: '马拉松型', right: '冲刺型', leftCode: 'M', rightCode: 'S', leftTrait: '长线推进者', rightTrait: '冲刺迭代者', leftDesc: '偏稳定推进,提交更像长跑。', rightDesc: '偏高频推进,短时间爆发强。'}},
1959
+ {{key: 'focus', name: '专注程度', code: f, spectrum: focusSpectrum, left: '分散型', right: '专注型', leftCode: 'D', rightCode: 'C', leftTrait: '多线调度者', rightTrait: '核心深挖者', leftDesc: '能在多个项目间切换上下文。', rightDesc: '精力更集中在核心项目。'}},
1960
+ {{key: 'style', name: '开发风格', code: s, spectrum: styleSpectrum, left: '守护型', right: '先锋型', leftCode: 'G', rightCode: 'P', leftTrait: '系统守护者', rightTrait: '功能开拓者', leftDesc: '更常维护、修复和打磨系统。', rightDesc: '更常推进新功能和新想法。'}},
1961
+ {{key: 'engineering', name: '工程取向', code: e, spectrum: agg.engSpectrum, left: '快速迭代', right: '质量导向', leftCode: 'R', rightCode: 'Q', leftTrait: '快速试错派', rightTrait: '质量洁癖派', leftDesc: '偏速度和反馈,先跑起来。', rightDesc: '偏测试、文档和工程质量。'}},
1962
+ {{key: 'ai', name: 'AI 协作', code: a, spectrum: agg.aiSpectrum, left: '手工型', right: 'AI 协作型', leftCode: 'H', rightCode: 'A', leftTrait: '手作掌控派', rightTrait: 'AI 搭子派', leftDesc: '主要靠自己手工推进。', rightDesc: '会把 AI 当成结对伙伴。'}},
1963
+ ];
1964
+ document.getElementById('dimsDetail').innerHTML = dims.map(d => {{
1965
+ const isRight = d.code === d.rightCode;
1966
+ const pct = isRight ? d.spectrum : (100 - d.spectrum);
1967
+ const align = isRight ? 'right' : 'left';
1968
+ const activeTrait = isRight ? d.rightTrait : d.leftTrait;
1969
+ const activeDesc = isRight ? d.rightDesc : d.leftDesc;
1970
+ const leftActive = isRight ? '' : ' active';
1971
+ const rightActive = isRight ? ' active' : '';
1972
+ return `<div class="dim-item">
1973
+ <div class="dim-head">
1974
+ <div class="dim-name">${{d.name}}</div>
1975
+ <div class="dim-codepair">${{d.leftCode}}/${{d.rightCode}}</div>
1976
+ </div>
1977
+ <div class="dim-active">
1978
+ <span class="dim-active-code">${{d.code}}</span>
1979
+ <span class="dim-active-name">${{activeTrait}}</span>
1980
+ </div>
1981
+ <div class="dim-row">
1982
+ <span class="dim-label${{leftActive}}" style="text-align:right;">${{d.left}}</span>
1983
+ <div class="dim-bar"><div class="dim-bar-fill" style="width:${{d.spectrum}}%;"></div></div>
1984
+ <span class="dim-label${{rightActive}}">${{d.right}}</span>
1985
+ </div>
1986
+ <div class="dim-pct" style="text-align:${{align}};">倾向 ${{pct}}%</div>
1987
+ <div class="dim-desc">${{activeDesc}}</div>
1988
+ </div>`;
1989
+ }}).join('');
1990
+ }}
1991
+
1992
+ // ============================================================
1993
+ // 更新 Habit Score
1994
+ // ============================================================
1995
+ function updateHabitScore(agg) {{
1996
+ const granularity = Math.round(Math.min(30, agg.avgPerDay / 4.5 * 30));
1997
+ const schedule = Math.round(Math.min(20, Math.max(0, (1 - agg.nightRatio / 0.4)) * 20));
1998
+ const focusScore = Math.round(Math.min(15, agg.focusIndex / 0.7 * 15));
1999
+ const testAwareness = Math.round(Math.min(20, agg.testRatio / 0.15 * 20));
2000
+ const docAwareness = Math.round(Math.min(15, agg.docRatio / 0.10 * 15));
2001
+ const totalScore = granularity + testAwareness + docAwareness + schedule + focusScore;
2002
+ agg.habitScore = {{totalScore, granularity, testAwareness, docAwareness, schedule, focusScore}};
2003
+
2004
+ document.getElementById('statScore').textContent = totalScore;
2005
+ document.getElementById('scoreNumber').textContent = totalScore;
2006
+ document.getElementById('scoreNumber').style.color = getScoreColor(totalScore);
2007
+ document.getElementById('scoreLabel').textContent = `/ 100 · ${{getScoreLabel(totalScore)}}`;
2008
+
2009
+ const scoreDims = [
2010
+ ['提交粒度', granularity, 30],
2011
+ ['测试意识', testAwareness, 20],
2012
+ ['文档意识', docAwareness, 15],
2013
+ ['作息规律', schedule, 20],
2014
+ ['项目聚焦', focusScore, 15],
2015
+ ];
2016
+ document.getElementById('scoreDims').innerHTML = scoreDims.map(([name, score, max]) => {{
2017
+ const pct = (score / max * 100).toFixed(0);
2018
+ const color = getScoreColor(score / max * 100);
2019
+ return `<div class="score-dim-row">
2020
+ <div class="score-dim-name">${{name}}</div>
2021
+ <div class="score-dim-bar"><div class="score-dim-bar-fill" style="width:${{pct}}%;background:${{color}};"></div></div>
2022
+ <div class="score-dim-val">${{score}}/${{max}} <span class="score-dim-pct">(${{pct}}%)</span></div>
2023
+ </div>`;
2024
+ }}).join('');
2025
+ }}
2026
+
2027
+ // ============================================================
2028
+ // 更新标签
2029
+ // ============================================================
2030
+ function updateTags(agg) {{
2031
+ const tags = [];
2032
+ const testPct = agg.testRatio * 100;
2033
+ const docPct = agg.docRatio * 100;
2034
+ if (agg.weekendRatio >= 0.3) tags.push({{icon: '📅', name: '周末战士', desc: `周末提交占比 ${{(agg.weekendRatio*100).toFixed(0)}}%`}});
2035
+ if (agg.aiDetected) tags.push({{icon: '🤖', name: 'AI 协作者', desc: '使用 AI 工具辅助开发'}});
2036
+ if (testPct >= 15) tags.push({{icon: '✅', name: '测试达人', desc: `测试覆盖 ${{testPct.toFixed(0)}}%`}});
2037
+ else if (testPct < 5) tags.push({{icon: '⚠️', name: '测试待加强', desc: `测试覆盖仅 ${{testPct.toFixed(0)}}%`}});
2038
+ if (docPct >= 10) tags.push({{icon: '📚', name: '文档之星', desc: '文档维护优秀'}});
2039
+ else if (docPct < 3) tags.push({{icon: '📝', name: '文档债务', desc: '文档投入不足'}});
2040
+ if (Object.keys(agg.projectCounts).length >= 10) tags.push({{icon: '🎪', name: '多面手', desc: `同时维护 ${{Object.keys(agg.projectCounts).length}} 个项目`}});
2041
+ if (agg.lateCommits > agg.dayCommits) tags.push({{icon: '🌙', name: '夜猫子', desc: '夜间比白天更活跃'}});
2042
+ if (agg.avgPerDay >= 5) tags.push({{icon: '🏃', name: '暴风提交', desc: `日均提交 ${{agg.avgPerDay.toFixed(1)}} 次`}});
2043
+ else if (agg.avgPerDay < 0.5) tags.push({{icon: '🦥', name: '佛系开发', desc: `日均提交仅 ${{agg.avgPerDay.toFixed(1)}} 次`}});
2044
+ if (agg.refactorRatio >= 0.1) tags.push({{icon: '🔧', name: '重构狂魔', desc: `重构占比 ${{(agg.refactorRatio*100).toFixed(0)}}%`}});
2045
+
2046
+ document.getElementById('tagsGrid').innerHTML = tags.slice(0, 5).map(tag => `
2047
+ <div class="tag-item">
2048
+ <div class="tag-icon">${{tag.icon}}</div>
2049
+ <div class="tag-name">${{tag.name}}</div>
2050
+ <div class="tag-desc">${{tag.desc}}</div>
2051
+ </div>
2052
+ `).join('');
2053
+ }}
2054
+
2055
+ // ============================================================
2056
+ // 更新图表
2057
+ // ============================================================
2058
+ function updateCharts(agg, project) {{
2059
+ // 24小时
2060
+ charts.hour.data.datasets[0].data = agg.hourly;
2061
+ const maxH = Math.max(...agg.hourly, 1);
2062
+ charts.hour.data.datasets[0].backgroundColor = agg.hourly.map(v => `rgba(9,105,218,${{0.2 + v/maxH*0.8}})`);
2063
+ charts.hour.update();
2064
+
2065
+ // 星期
2066
+ charts.week.data.datasets[0].data = agg.weekly;
2067
+ charts.week.update();
2068
+
2069
+ // 每月堆叠
2070
+ const sortedMonths = Object.keys(agg.monthly).sort();
2071
+ const projList = project === 'all'
2072
+ ? agg.projSorted.slice(0, 7)
2073
+ : [[project, agg.total]];
2074
+ const stackData = projList.map(([name, _], i) => {{
2075
+ const pm = {{}};
2076
+ agg.commits.filter(c => c.project === name).forEach(c => {{ pm[c.month] = (pm[c.month] || 0) + 1; }});
2077
+ return {{ label: name, data: sortedMonths.map(m => pm[m] || 0), backgroundColor: COLORS[i % COLORS.length] }};
2078
+ }});
2079
+ if (project === 'all' && agg.projSorted.length > 7) {{
2080
+ const topNames = projList.map(p => p[0]);
2081
+ const om = {{}};
2082
+ agg.commits.filter(c => !topNames.includes(c.project)).forEach(c => {{ om[c.month] = (om[c.month] || 0) + 1; }});
2083
+ if (Object.keys(om).length > 0) stackData.push({{ label: '其他', data: sortedMonths.map(m => om[m] || 0), backgroundColor: '#9ca3af' }});
2084
+ }}
2085
+ charts.monthly.data.labels = sortedMonths.map(fmtMonth);
2086
+ charts.monthly.data.datasets = stackData;
2087
+ charts.monthly.update();
2088
+
2089
+ // 气泡图
2090
+ const bubbleProjList = project === 'all'
2091
+ ? agg.projSorted.filter(([_, c]) => c >= 5).slice(0, 10)
2092
+ : [[project, agg.total]];
2093
+ const sortedMonthsForBubble = Object.keys(agg.monthly).sort();
2094
+ charts.bubble.data.labels = sortedMonthsForBubble.map(fmtMonth);
2095
+ const bubbleData = bubbleProjList.map(([name, _], i) => {{
2096
+ const pm = {{}};
2097
+ agg.commits.filter(c => c.project === name).forEach(c => {{ pm[c.month] = (pm[c.month] || 0) + 1; }});
2098
+ const points = sortedMonthsForBubble.map(m => pm[m] ? {{ x: fmtMonth(m), y: pm[m], r: Math.min(Math.max(pm[m] ** 0.5 * 2.5, 4), 30) }} : null).filter(Boolean);
2099
+ return {{ label: name, data: points, backgroundColor: COLORS[i % COLORS.length] + 'b3' }};
2100
+ }});
2101
+ charts.bubble.data.datasets = bubbleData;
2102
+ charts.bubble.update();
2103
+
2104
+ // 类型环形图
2105
+ const typeValues = typeLabels.map(t => agg.types[t] || 0);
2106
+ charts.type.data.datasets[0].data = typeValues;
2107
+ charts.type.update();
2108
+
2109
+ // 语言分布
2110
+ const langEntries = Object.entries(agg.languageCounts).sort((a, b) => b[1] - a[1]);
2111
+ charts.lang.data.labels = langEntries.map(([name]) => name);
2112
+ charts.lang.data.datasets[0].data = langEntries.map(([, count]) => count);
2113
+ charts.lang.data.datasets[0].backgroundColor = langEntries.map((_, i) => COLORS[i % COLORS.length]);
2114
+ charts.lang.update();
2115
+ }}
2116
+
2117
+ // ============================================================
2118
+ // 更新洞察文字
2119
+ // ============================================================
2120
+ function updateInsights(agg) {{
2121
+ // 24小时洞察
2122
+ const peakHours = agg.hourly.map((v, i) => ({{v, i}})).sort((a, b) => b.v - a.v).slice(0, 3).map(x => x.i);
2123
+ document.getElementById('insightHour').innerHTML = `<strong>洞察:</strong>最活跃时段 ${{peakHours.map(h => h + ':00').join(', ')}}。${{agg.nightRatio > 0.25 ? '夜间提交占比 ' + (agg.nightRatio*100).toFixed(1) + '%。' : '作息相对规律。'}}`;
2124
+
2125
+ // 星期洞察
2126
+ const weekdayNames = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
2127
+ const peakWeekdays = agg.weekly.map((v, i) => ({{v, i}})).sort((a, b) => b.v - a.v).slice(0, 3).map(x => weekdayNames[x.i]);
2128
+ document.getElementById('insightWeek').innerHTML = `<strong>洞察:</strong>最活跃 ${{peakWeekdays.join(', ')}}。${{agg.weekendRatio > 0.2 ? '周末提交占比 ' + (agg.weekendRatio*100).toFixed(1) + '%。' : '以工作日为主。'}}`;
2129
+
2130
+ // 每月洞察
2131
+ const top3Ratio = (agg.top3Commits / agg.total * 100).toFixed(0);
2132
+ document.getElementById('insightMonthly').innerHTML = `<strong>洞察:</strong>Top 3 项目占据 ${{top3Ratio}}% 的提交。${{top3Ratio > 60 ? '专注度高。' : '精力较分散。'}}`;
2133
+
2134
+ // 类型洞察
2135
+ document.getElementById('insightTypes').innerHTML = `<strong>洞察:</strong>功能开发占比 ${{(agg.featRatio*100).toFixed(1)}}%,测试 ${{(agg.testRatio*100).toFixed(1)}}%,文档 ${{(agg.docRatio*100).toFixed(1)}}%。${{agg.lowInfoRatio > 0.15 ? ' 低信息量 commit 占比 ' + (agg.lowInfoRatio*100).toFixed(1) + '%。' : ''}}${{agg.lowConfidenceRatio > 0.3 ? ' Commit 类型分类置信度偏低,已优先使用文件路径兜底。' : ''}}`;
2136
+
2137
+ // 工程健康洞察
2138
+ const engInsight = [];
2139
+ if (agg.testRatio < 0.05) engInsight.push('很少写测试,建议为新功能补充测试用例。');
2140
+ else if (agg.testRatio >= 0.10) engInsight.push('测试习惯不错。');
2141
+ if (agg.docRatio < 0.03) engInsight.push('文档更新较少,建议定期维护 README。');
2142
+ if (agg.nightRatio > 0.3) engInsight.push('深夜写代码比例较高,注意休息。');
2143
+ if (agg.lowInfoRatio > 0.15) engInsight.push('有些 commit 描述太简略,不利于后期回溯。');
2144
+ document.getElementById('insightEng').innerHTML = `<strong>洞察:</strong>${{engInsight.length ? engInsight.join(' ') : '各项指标正常。'}}`;
2145
+ }}
2146
+
2147
+ // ============================================================
2148
+ // 更新工程健康
2149
+ // ============================================================
2150
+ function updateEngHealth(agg) {{
2151
+ const testPct = agg.testRatio * 100;
2152
+ const docPct = agg.docRatio * 100;
2153
+ const lowInfoPct = agg.lowInfoRatio * 100;
2154
+ const engItems = [
2155
+ {{label: '测试覆盖', desc: '改代码时有改测试吗', val: testPct.toFixed(1), color: testPct >= 10 ? '#1a7f37' : '#cf222e'}},
2156
+ {{label: '文档覆盖', desc: '改代码时有更新文档吗', val: docPct.toFixed(1), color: docPct >= 5 ? '#1a7f37' : '#bf8700'}},
2157
+ {{label: '功能开发', desc: '写新功能的时间占比', val: (agg.featRatio*100).toFixed(1), color: '#0969da'}},
2158
+ {{label: 'Bug 修复', desc: '修 bug 的时间占比', val: (agg.fixRatio*100).toFixed(1), color: '#cf222e'}},
2159
+ {{label: '重构', desc: '优化老代码的时间占比', val: (agg.refactorRatio*100).toFixed(1), color: '#8250df'}},
2160
+ {{label: '夜间提交', desc: '深夜写的代码占比', val: (agg.nightRatio*100).toFixed(1), color: agg.nightRatio > 0.25 ? '#bf8700' : '#656d76'}},
2161
+ {{label: '周末提交', desc: '周末写的代码占比', val: (agg.weekendRatio*100).toFixed(1), color: agg.weekendRatio > 0.25 ? '#8250df' : '#656d76'}},
2162
+ {{label: '低信息量', desc: 'commit 信息够详细吗', val: lowInfoPct.toFixed(1), color: lowInfoPct > 20 ? '#cf222e' : '#1a7f37'}},
2163
+ ];
2164
+ document.getElementById('engHealthGrid').innerHTML = engItems.map(item =>
2165
+ `<div class="eng-item">
2166
+ <div class="eng-val" style="color:${{item.color}};">${{item.val}}%</div>
2167
+ <div class="eng-label">${{item.label}}</div>
2168
+ <div class="eng-desc">${{item.desc}}</div>
2169
+ </div>`
2170
+ ).join('');
2171
+ }}
2172
+
2173
+ // ============================================================
2174
+ // 更新排行榜
2175
+ // ============================================================
2176
+ function updateRanking(agg) {{
2177
+ document.getElementById('projectsRanking').innerHTML = agg.projSorted.slice(0, 8).map(([name, count], i) => {{
2178
+ const meta = projectMeta[name] || {{}};
2179
+ const activeDays = agg.projectActiveDays[name] ? agg.projectActiveDays[name].size : 0;
2180
+ return `<div class="rank-row">
2181
+ <div class="rank-num">${{i + 1}}</div>
2182
+ <div class="rank-info">
2183
+ <div class="rank-name">${{name}}</div>
2184
+ <div class="rank-meta"><span>${{meta.language || 'Unknown'}}</span><span>${{activeDays}} 天活跃</span></div>
2185
+ </div>
2186
+ <div class="rank-commits">${{count}}</div>
2187
+ </div>`;
2188
+ }}).join('');
2189
+ }}
2190
+
2191
+ // ============================================================
2192
+ // 更新提交类型条形图
2193
+ // ============================================================
2194
+ function updateTypeBars(agg) {{
2195
+ const typeValues = typeLabels.map(t => agg.types[t] || 0);
2196
+ document.getElementById('typeBars').innerHTML = typeValues.map((val, i) => {{
2197
+ const pct = (val / agg.total * 100).toFixed(1);
2198
+ return `<div class="type-bar-row">
2199
+ <div class="type-bar-name">${{typeNames[i]}}</div>
2200
+ <div class="type-bar-track"><div class="type-bar-fill" style="width:${{pct}}%;background:${{typeColors[i]}};"></div></div>
2201
+ <div class="type-bar-val">${{val}} (${{pct}}%)</div>
2202
+ </div>`;
2203
+ }}).join('');
2204
+ }}
2205
+
2206
+ function buildAiMonthly(commits) {{
2207
+ const monthly = {{}};
2208
+ commits.forEach(c => {{
2209
+ if (c.aiSignal) monthly[c.month] = (monthly[c.month] || 0) + 1;
2210
+ }});
2211
+ return monthly;
2212
+ }}
2213
+
2214
+ function renderAiTrend(agg) {{
2215
+ const canvas = document.getElementById('aiTrendChart');
2216
+ const empty = document.getElementById('aiTrendEmpty');
2217
+ if (!canvas) return;
2218
+ if (charts.aiTrend) {{
2219
+ charts.aiTrend.destroy();
2220
+ delete charts.aiTrend;
2221
+ }}
2222
+
2223
+ const aiMonthly = buildAiMonthly(agg.commits);
2224
+ const sortedMonths = Object.keys(aiMonthly).sort();
2225
+ if (!sortedMonths.length) {{
2226
+ canvas.style.display = 'none';
2227
+ if (empty) empty.style.display = 'flex';
2228
+ return;
2229
+ }}
2230
+
2231
+ canvas.style.display = 'block';
2232
+ if (empty) empty.style.display = 'none';
2233
+ charts.aiTrend = new Chart(canvas, {{
2234
+ type: 'line',
2235
+ data: {{
2236
+ labels: sortedMonths.map(fmtMonth),
2237
+ datasets: [{{
2238
+ label: 'AI 明确信号提交',
2239
+ data: sortedMonths.map(m => aiMonthly[m]),
2240
+ borderColor: '#0969da',
2241
+ backgroundColor: 'rgba(9, 105, 218, 0.10)',
2242
+ fill: true,
2243
+ tension: 0.3,
2244
+ pointRadius: 4,
2245
+ pointHoverRadius: 6
2246
+ }}]
2247
+ }},
2248
+ options: {{
2249
+ ...chartDefaults,
2250
+ plugins: {{
2251
+ legend: {{ display: false }},
2252
+ tooltip: {{
2253
+ callbacks: {{
2254
+ label: function(ctx) {{
2255
+ return ctx.parsed.y + ' 次 AI 明确信号提交';
2256
+ }}
2257
+ }}
2258
+ }}
2259
+ }},
2260
+ scales: {{
2261
+ x: {{ grid: {{ display: false }} }},
2262
+ y: {{ beginAtZero: true, grid: {{ color: 'rgba(0,0,0,0.04)' }} }}
2263
+ }}
2264
+ }}
2265
+ }});
2266
+ }}
2267
+
2268
+ // ============================================================
2269
+ // 更新 AI Impact
2270
+ // ============================================================
2271
+ function updateAiImpact(agg) {{
2272
+ if (charts.aiTrend) {{
2273
+ charts.aiTrend.destroy();
2274
+ delete charts.aiTrend;
2275
+ }}
2276
+ if (agg.aiDetected) {{
2277
+ const ratio = (agg.aiCommitRatio * 100).toFixed(1);
2278
+ const toolingRatio = (agg.aiToolingRatio * 100).toFixed(1);
2279
+ const explicitScore = Math.min(agg.aiCommitRatio * 300, 100);
2280
+ const toolingScore = Math.min(agg.aiToolingRatio * 15, 15);
2281
+ let insight = '';
2282
+ if (agg.aiSpectrum > 50) {{
2283
+ insight = `AI 影响分 ${{agg.aiSpectrum}},明确信号率 ${{ratio}}%,AI tooling 覆盖 ${{toolingRatio}}%。`;
2284
+ }} else {{
2285
+ insight = `AI 明确信号率 ${{ratio}}%,AI tooling 覆盖 ${{toolingRatio}}%。tooling 只按弱信号计分,当前证据还不足以判成 AI 协作型。`;
2286
+ }}
2287
+
2288
+ document.getElementById('aiImpact').innerHTML = `
2289
+ <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:16px;">
2290
+ <div style="background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:20px;text-align:center;">
2291
+ <div style="font-size:2em;margin-bottom:4px;">🤖</div>
2292
+ <div style="font-size:1.8em;font-weight:700;color:#1f2328;">${{ratio}}%</div>
2293
+ <div style="color:#656d76;font-size:0.85em;">AI 明确信号率</div>
2294
+ </div>
2295
+ <div style="background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:20px;text-align:center;">
2296
+ <div style="font-size:2em;margin-bottom:4px;">📊</div>
2297
+ <div style="font-size:1.8em;font-weight:700;color:#1f2328;">${{agg.aiSignalCount}}</div>
2298
+ <div style="color:#656d76;font-size:0.85em;">AI 明确信号提交</div>
2299
+ </div>
2300
+ <div style="background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:20px;text-align:center;">
2301
+ <div style="font-size:2em;margin-bottom:4px;">🔧</div>
2302
+ <div style="font-size:1.8em;font-weight:700;color:#1f2328;">${{agg.aiSpectrum}}</div>
2303
+ <div style="color:#656d76;font-size:0.85em;">AI 影响分</div>
2304
+ </div>
2305
+ </div>
2306
+ <div class="ai-visual-grid">
2307
+ <div class="ai-score-box">
2308
+ <div class="ai-box-title">AI 影响分拆解</div>
2309
+ <div class="ai-score-track">
2310
+ <div class="ai-score-explicit" style="width:${{explicitScore}}%;"></div>
2311
+ <div class="ai-score-tooling" style="width:${{toolingScore}}%;"></div>
2312
+ </div>
2313
+ <div class="ai-score-row"><span>明确信号贡献</span><strong>${{explicitScore.toFixed(0)}} / 100</strong></div>
2314
+ <div class="ai-score-row"><span>tooling 弱信号贡献</span><strong>${{toolingScore.toFixed(0)}} / 15</strong></div>
2315
+ </div>
2316
+ <div class="ai-chart-box">
2317
+ <div class="ai-box-title">AI 明确信号趋势</div>
2318
+ <div class="ai-trend-wrap">
2319
+ <canvas id="aiTrendChart"></canvas>
2320
+ <div class="ai-trend-empty" id="aiTrendEmpty">当前筛选范围只有 AI tooling 弱信号,暂无明确 AI commit 趋势</div>
2321
+ </div>
2322
+ </div>
2323
+ </div>
2324
+ <div class="insight-card"><strong>洞察:</strong>${{insight}}</div>`;
2325
+ renderAiTrend(agg);
2326
+ }} else {{
2327
+ document.getElementById('aiImpact').innerHTML = `
2328
+ <div style="background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:24px;text-align:center;">
2329
+ <div style="color:#656d76;">未检测到明显的 AI 辅助开发痕迹</div>
2330
+ </div>
2331
+ <div class="insight-card"><strong>洞察:</strong>你目前主要依靠手工编码,没有检测到 AI 辅助开发的痕迹。</div>`;
2332
+ }}
2333
+ }}
2334
+
2335
+ // ============================================================
2336
+ // 更新建议
2337
+ // ============================================================
2338
+ function updateSuggestions(agg) {{
2339
+ const suggestions = [];
2340
+ const avgDaily = agg.avgPerDay || 0;
2341
+ const aiRatio = agg.aiCommitRatio || 0;
2342
+ const hourly = agg.hourly || [];
2343
+ const totalProjects = Object.keys(agg.projectCounts).length;
2344
+ const weekdayNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
2345
+
2346
+ // ============================================================
2347
+ // 1. 习惯分数维度
2348
+ // ============================================================
2349
+ if (agg.habitScore.schedule < 10) suggestions.push(`改善作息规律:作息得分仅 ${{agg.habitScore.schedule}}/20,夜间和周末提交较多`);
2350
+ if (agg.habitScore.focus < 10) suggestions.push(`提升专注度:项目聚焦得分仅 ${{agg.habitScore.focus}}/15,建议减少同时维护的项目数`);
2351
+ if (agg.habitScore.granularity < 15) suggestions.push(`优化提交粒度:粒度得分仅 ${{agg.habitScore.granularity}}/30,建议保持稳定的提交频率`);
2352
+
2353
+ // ============================================================
2354
+ // 2. 测试相关
2355
+ // ============================================================
2356
+ if (agg.habitScore.testAwareness < 10) suggestions.push('提高测试意识:尝试为每个新功能编写测试用例,目标覆盖率 10%+');
2357
+ else if (agg.testRatio < 0.08) suggestions.push(`增加测试投入:当前测试文件占比仅 ${{(agg.testRatio*100).toFixed(0)}}%,建议补充单元测试`);
2358
+ if (agg.testRatio >= 0.30) suggestions.push(`测试做得好:测试文件占比 ${{(agg.testRatio*100).toFixed(0)}}%,继续保持!`);
2359
+
2360
+ // ============================================================
2361
+ // 3. 文档相关
2362
+ // ============================================================
2363
+ if (agg.habitScore.docAwareness < 10) suggestions.push('增加文档投入:定期更新 README 和 API 文档');
2364
+ else if (agg.docRatio < 0.05) suggestions.push(`完善文档:文档变更占比 ${{(agg.docRatio*100).toFixed(0)}}%,重要功能应有文档说明`);
2365
+ if (agg.docRatio >= 0.15) suggestions.push(`文档做得好:文档变更占比 ${{(agg.docRatio*100).toFixed(0)}}%,继续保持!`);
2366
+
2367
+ // ============================================================
2368
+ // 4. 作息相关
2369
+ // ============================================================
2370
+ if (agg.nightRatio > 0.25) suggestions.push(`注意作息健康:夜间提交占比 ${{(agg.nightRatio*100).toFixed(0)}}%,建议调整为白天工作`);
2371
+ if (agg.weekendRatio > 0.25) suggestions.push(`平衡工作生活:周末提交占比 ${{(agg.weekendRatio*100).toFixed(0)}}%,注意适当休息`);
2372
+
2373
+ // ============================================================
2374
+ // 5. 深夜提交
2375
+ // ============================================================
2376
+ if (hourly.length === 24) {{
2377
+ const lateNight = hourly.slice(0, 6).reduce((a, b) => a + b, 0);
2378
+ const total = hourly.reduce((a, b) => a + b, 0);
2379
+ if (total > 0 && lateNight / total > 0.15) {{
2380
+ suggestions.push(`减少深夜编码:凌晨 0-6 点提交占比 ${{(lateNight/total*100).toFixed(0)}}%,长期熬夜影响代码质量`);
2381
+ }}
2382
+ }}
2383
+
2384
+ // ============================================================
2385
+ // 6. Commit 质量
2386
+ // ============================================================
2387
+ if (agg.lowInfoRatio > 0.15) suggestions.push(`优化 Commit Message:${{(agg.lowInfoRatio*100).toFixed(0)}}% 的提交缺少描述,建议使用 Conventional Commits 规范`);
2388
+ else if (agg.lowInfoRatio > 0.05) suggestions.push('完善提交描述:部分 commit 信息过于简略,建议写清楚改动原因');
2389
+ if (agg.lowInfoRatio < 0.03) suggestions.push(`Commit 质量高:${{((1-agg.lowInfoRatio)*100).toFixed(0)}}% 的提交有详细描述,继续保持!`);
2390
+
2391
+ // ============================================================
2392
+ // 7. 项目聚焦
2393
+ // ============================================================
2394
+ if (agg.focusIndex < 0.60) suggestions.push('提高项目聚焦度:精力较分散,建议集中精力在 1-2 个核心项目上');
2395
+
2396
+ // ============================================================
2397
+ // 8. 提交粒度
2398
+ // ============================================================
2399
+ if (avgDaily < 1) suggestions.push(`提高提交频率:日均仅 ${{avgDaily.toFixed(1)}} 次提交,建议小步快跑、勤提交`);
2400
+ else if (avgDaily > 8) suggestions.push(`优化提交粒度:日均 ${{avgDaily.toFixed(1)}} 次提交偏多,考虑合并相关改动`);
2401
+ else if (avgDaily >= 2 && avgDaily <= 5) suggestions.push(`提交频率适中:日均 ${{avgDaily.toFixed(1)}} 次提交,节奏良好`);
2402
+
2403
+ // ============================================================
2404
+ // 9. 代码质量
2405
+ // ============================================================
2406
+ if (agg.fixRatio > 0.20) suggestions.push(`提升代码质量:Bug 修复占比 ${{(agg.fixRatio*100).toFixed(0)}}%,建议加强测试和 Code Review`);
2407
+ else if (agg.fixRatio < 0.05 && agg.total > 50) suggestions.push(`Bug 很少:Bug 修复仅占 ${{(agg.fixRatio*100).toFixed(0)}}%,代码质量不错`);
2408
+ if (agg.refactorRatio < 0.10 && agg.featRatio > 0.50) suggestions.push('关注技术债:功能开发占比高但重构不足,建议定期安排重构时间');
2409
+ if (agg.refactorRatio >= 0.10) suggestions.push(`重构做得好:重构占比 ${{(agg.refactorRatio*100).toFixed(0)}}%,代码质量持续改善`);
2410
+
2411
+ // ============================================================
2412
+ // 10. AI 使用
2413
+ // ============================================================
2414
+ if (aiRatio > 0.40) suggestions.push('善用 AI:AI 占比超过 40%,建议仔细审查 AI 生成的代码');
2415
+ else if (aiRatio > 0 && aiRatio < 0.15) suggestions.push(`探索 AI 工具:当前 AI 使用率 ${{(aiRatio*100).toFixed(0)}}%,可尝试 Copilot/Cursor 提升效率`);
2416
+ if (aiRatio >= 0.20 && aiRatio <= 0.40) suggestions.push(`AI 使用适中:AI 占比 ${{(aiRatio*100).toFixed(0)}}%,人机协作良好`);
2417
+
2418
+ // ============================================================
2419
+ // 11. 最活跃星期
2420
+ // ============================================================
2421
+ const peakWeekdays = agg.weekly.map((v, i) => ({{v, i}})).sort((a, b) => b.v - a.v).slice(0, 3).map(x => weekdayNames[x.i]);
2422
+ const weekendDays = peakWeekdays.filter(d => d === '周六' || d === '周日');
2423
+ if (weekendDays.length >= 2) suggestions.push(`调整工作节奏:最活跃的 3 天中有 ${{weekendDays.length}} 天是周末,建议以工作日为主`);
2424
+
2425
+ // ============================================================
2426
+ // 12. 工作强度
2427
+ // ============================================================
2428
+ if (avgDaily > 5) suggestions.push(`注意工作强度:日均 ${{avgDaily.toFixed(1)}} 次提交,注意劳逸结合避免倦怠`);
2429
+
2430
+ // ============================================================
2431
+ // 13. 项目数量
2432
+ // ============================================================
2433
+ if (totalProjects > 15) suggestions.push(`精简项目数量:同时维护 ${{totalProjects}} 个项目,建议聚焦核心项目提升效率`);
2434
+ else if (totalProjects <= 3 && agg.total > 100) suggestions.push(`项目聚焦度高:仅维护 ${{totalProjects}} 个项目,精力集中`);
2435
+
2436
+ // ============================================================
2437
+ // 14. 活跃度
2438
+ // ============================================================
2439
+ if (agg.activeDays > 0 && avgDaily > 0) {{
2440
+ const activeRate = Math.min(agg.activeDays / 365 * 100, 100);
2441
+ if (activeRate < 30) suggestions.push(`保持持续性:活跃天数 ${{agg.activeDays}} 天,建议养成每天提交的习惯`);
2442
+ else if (activeRate > 70) suggestions.push(`活跃度高:${{agg.activeDays}} 天有提交, coding 习惯良好`);
2443
+ }}
2444
+
2445
+ // ============================================================
2446
+ // 15. Commit 类型分布
2447
+ // ============================================================
2448
+ if (agg.total > 50) {{
2449
+ const testRatio = (agg.types.test || 0) / agg.total;
2450
+ const docsRatio = (agg.types.docs || 0) / agg.total;
2451
+ const refactorRatio = (agg.types.refactor || 0) / agg.total;
2452
+ const otherRatio = (agg.types.other || 0) / agg.total;
2453
+ const choreRatio = (agg.types.chore || 0) / agg.total;
2454
+ const featRatio = (agg.types.feat || 0) / agg.total;
2455
+ const fixRatio = (agg.types.fix || 0) / agg.total;
2456
+
2457
+ if (testRatio < 0.05) suggestions.push(`增加测试提交:测试相关提交仅占 ${{(testRatio*100).toFixed(0)}}%,建议为新功能编写测试`);
2458
+ else if (testRatio >= 0.15) suggestions.push(`测试提交充足:测试占比 ${{(testRatio*100).toFixed(0)}}%,质量意识强`);
2459
+ if (docsRatio < 0.05) suggestions.push(`增加文档提交:文档相关提交仅占 ${{(docsRatio*100).toFixed(0)}}%,建议及时更新文档`);
2460
+ else if (docsRatio >= 0.15) suggestions.push(`文档提交充足:文档占比 ${{(docsRatio*100).toFixed(0)}}%,文档维护良好`);
2461
+ if (refactorRatio < 0.03 && agg.featRatio > 0.30) suggestions.push(`定期重构:重构提交仅占 ${{(refactorRatio*100).toFixed(0)}}%,功能开发占比高,建议安排重构时间`);
2462
+ if (otherRatio > 0.25) suggestions.push(`规范 Commit 类型:${{(otherRatio*100).toFixed(0)}}% 的提交被归为 other,建议使用 feat/fix/refactor 等标准类型`);
2463
+ if (choreRatio > 0.15) suggestions.push(`自动化琐事:chore 类型提交占比 ${{(choreRatio*100).toFixed(0)}}%,考虑用脚本或 CI 自动化`);
2464
+ if (featRatio > 0.40) suggestions.push(`功能开发为主:feat 占比 ${{(featRatio*100).toFixed(0)}}%,注意平衡新功能与维护`);
2465
+ if (fixRatio > 0.20) suggestions.push(`Bug 修复较多:fix 占比 ${{(fixRatio*100).toFixed(0)}}%,建议加强测试预防`);
2466
+ }}
2467
+
2468
+ // ============================================================
2469
+ // 16. 语言多样性
2470
+ // ============================================================
2471
+ const langSet = new Set(Object.values(agg.languageCounts).map((_, i) => Object.keys(agg.languageCounts)[i]).filter(l => l && l !== 'Other'));
2472
+ if (langSet.size === 1) suggestions.push(`拓展技术栈:当前只使用 ${{[...langSet][0]}} 语言,建议学习其他语言拓宽视野`);
2473
+ else if (langSet.size >= 5) suggestions.push(`聚焦核心技术:同时使用 ${{langSet.size}} 种语言,建议深耕 1-2 种核心语言`);
2474
+
2475
+ // ============================================================
2476
+ // 17. 提交频率波动
2477
+ // ============================================================
2478
+ if (hourly.length === 24) {{
2479
+ const maxHour = Math.max(...hourly);
2480
+ const minHour = Math.min(...hourly);
2481
+ if (maxHour > 0 && minHour / maxHour < 0.1) suggestions.push('平衡提交时间:提交集中在特定时段,建议分散到全天各时段');
2482
+ }}
2483
+
2484
+ // ============================================================
2485
+ // 18. 提交连续性
2486
+ // ============================================================
2487
+ if (agg.activeDays > 0 && agg.total > 0) {{
2488
+ const commitsPerActiveDay = agg.total / agg.activeDays;
2489
+ if (commitsPerActiveDay > 10) suggestions.push(`控制单日提交量:活跃日均提交 ${{commitsPerActiveDay.toFixed(0)}} 次,建议拆分为更小的改动`);
2490
+ }}
2491
+
2492
+ // ============================================================
2493
+ // 19. 提交间隔
2494
+ // ============================================================
2495
+ if (agg.activeDays > 0 && agg.total > 0) {{
2496
+ const avgInterval = 24 * agg.activeDays / agg.total;
2497
+ if (avgInterval < 1) suggestions.push(`降低提交频率:平均 ${{(avgInterval * 60).toFixed(0)}} 分钟一次提交,考虑合并相关改动`);
2498
+ }}
2499
+
2500
+ // ============================================================
2501
+ // 20. 工作节奏一致性
2502
+ // ============================================================
2503
+ if (hourly.length === 24) {{
2504
+ const total = hourly.reduce((a, b) => a + b, 0);
2505
+ if (total > 0) {{
2506
+ const workHours = hourly.slice(9, 18).reduce((a, b) => a + b, 0);
2507
+ const workRatio = workHours / total;
2508
+ if (workRatio > 0.6) suggestions.push(`工作节奏规律:${{(workRatio*100).toFixed(0)}}% 的提交在工作时间,作息健康`);
2509
+ else if (workRatio < 0.3) suggestions.push(`工作时间不规律:仅 ${{(workRatio*100).toFixed(0)}}% 的提交在工作时间,建议调整`);
2510
+ }}
2511
+ }}
2512
+
2513
+ // ============================================================
2514
+ // 21. 项目活跃度
2515
+ // ============================================================
2516
+ if (totalProjects > 5 && agg.total > 0) {{
2517
+ const activeProjects = Object.values(agg.projectCounts).filter(c => c > 10).length;
2518
+ if (activeProjects <= 2) suggestions.push(`项目活跃度不均:${{totalProjects}} 个项目中仅 ${{activeProjects}} 个活跃,建议清理不活跃项目`);
2519
+ }}
2520
+
2521
+ // ============================================================
2522
+ // 22. 测试覆盖趋势
2523
+ // ============================================================
2524
+ if (agg.testRatio >= 0.20) suggestions.push(`测试覆盖良好:测试文件占比 ${{(agg.testRatio*100).toFixed(0)}}%,代码质量有保障`);
2525
+
2526
+ // ============================================================
2527
+ // 23. 文档覆盖趋势
2528
+ // ============================================================
2529
+ if (agg.docRatio >= 0.10) suggestions.push(`文档覆盖良好:文档变更占比 ${{(agg.docRatio*100).toFixed(0)}}%,项目可维护性强`);
2530
+
2531
+ // ============================================================
2532
+ // 24. 工作生活平衡
2533
+ // ============================================================
2534
+ if (agg.nightRatio < 0.15 && agg.weekendRatio < 0.15) suggestions.push('工作生活平衡:夜间和周末提交都很少,作息健康');
2535
+
2536
+ // ============================================================
2537
+ // 25. 代码稳定性
2538
+ // ============================================================
2539
+ if (agg.fixRatio < 0.05 && agg.refactorRatio < 0.05 && agg.total > 100) suggestions.push('代码稳定:Bug 修复和重构都很少,代码质量稳定');
2540
+
2541
+ // ============================================================
2542
+ // 26. AI 工具多样性
2543
+ // ============================================================
2544
+ // 注:JS 端暂无 ai.tools 数据,跳过
2545
+
2546
+ // ============================================================
2547
+ // 27. AI 协作深度
2548
+ // ============================================================
2549
+ // 注:JS 端暂无 ai.ai_influence_score 数据,跳过
2550
+
2551
+ // ============================================================
2552
+ // 28. 提交消息长度
2553
+ // ============================================================
2554
+ if (agg.lowInfoRatio < 0.05 && agg.total > 100) suggestions.push(`提交消息质量高:${{((1-agg.lowInfoRatio)*100).toFixed(0)}}% 的提交有详细描述,代码可维护性强`);
2555
+
2556
+ // ============================================================
2557
+ // 29. 代码变更规模
2558
+ // ============================================================
2559
+ if (agg.total > 0 && agg.activeDays > 0) {{
2560
+ const avgChangesPerCommit = agg.total / agg.activeDays;
2561
+ if (avgChangesPerCommit > 5) suggestions.push('单次提交较大:建议拆分为更小的提交,便于 Code Review');
2562
+ }}
2563
+
2564
+ // ============================================================
2565
+ // 30. 技术债务
2566
+ // ============================================================
2567
+ if (agg.refactorRatio < 0.05 && agg.featRatio > 0.40 && agg.total > 100) suggestions.push(`技术债积累:重构仅占 ${{(agg.refactorRatio*100).toFixed(0)}}%,建议定期安排重构时间`);
2568
+
2569
+ // ============================================================
2570
+ // 31. 提交频率波动(小时级别)
2571
+ // ============================================================
2572
+ if (hourly.length === 24) {{
2573
+ const mean = hourly.reduce((a, b) => a + b, 0) / 24;
2574
+ if (mean > 0) {{
2575
+ const variance = hourly.reduce((a, b) => a + (b - mean) ** 2, 0) / 24;
2576
+ const cv = Math.sqrt(variance) / mean;
2577
+ if (cv > 1.5) suggestions.push('提交时间波动大:建议保持更稳定的工作节奏');
2578
+ }}
2579
+ }}
2580
+
2581
+ // ============================================================
2582
+ // 32. 项目集中度
2583
+ // ============================================================
2584
+ if (totalProjects > 0 && agg.total > 0) {{
2585
+ const shares = Object.values(agg.projectCounts).map(c => c / agg.total);
2586
+ const hhi = shares.reduce((a, b) => a + b * b, 0);
2587
+ if (hhi < 0.1) suggestions.push('项目分散:提交分布均匀,建议聚焦核心项目');
2588
+ else if (hhi > 0.5) suggestions.push('项目集中:大部分提交集中在少数项目,精力分配合理');
2589
+ }}
2590
+
2591
+ // ============================================================
2592
+ // 33. 周末提交模式
2593
+ // ============================================================
2594
+ if (agg.weekly.length === 7) {{
2595
+ const sat = agg.weekly[5] || 0;
2596
+ const sun = agg.weekly[6] || 0;
2597
+ const totalWeekly = agg.weekly.reduce((a, b) => a + b, 0);
2598
+ if (totalWeekly > 0) {{
2599
+ const satRatio = sat / totalWeekly * 100;
2600
+ const sunRatio = sun / totalWeekly * 100;
2601
+ if (satRatio > 20 && sunRatio < 5) suggestions.push(`周六活跃:周六提交占比 ${{satRatio.toFixed(0)}}%,周日较少,节奏不错`);
2602
+ else if (sunRatio > 20 && satRatio < 5) suggestions.push(`周日活跃:周日提交占比 ${{sunRatio.toFixed(0)}}%,建议利用周六提前完成`);
2603
+ }}
2604
+ }}
2605
+
2606
+ // ============================================================
2607
+ // 34. 提交分布集中度
2608
+ // ============================================================
2609
+ if (hourly.length === 24) {{
2610
+ const total = hourly.reduce((a, b) => a + b, 0);
2611
+ if (total > 0) {{
2612
+ const sorted = hourly.map((v, i) => ({{v, i}})).sort((a, b) => b.v - a.v).slice(0, 3);
2613
+ const top3Ratio = sorted.reduce((a, x) => a + x.v, 0) / total * 100;
2614
+ if (top3Ratio > 50) suggestions.push(`提交高度集中:最活跃的 3 小时占 ${{top3Ratio.toFixed(0)}}% 提交,建议更均匀分布`);
2615
+ else if (top3Ratio < 25) suggestions.push('提交分布均匀:各时段提交较分散,时间管理良好');
2616
+ }}
2617
+ }}
2618
+
2619
+ // ============================================================
2620
+ // 35. 测试与功能平衡
2621
+ // ============================================================
2622
+ if (agg.total > 0) {{
2623
+ const featCount = agg.types.feat || 0;
2624
+ const testCount = agg.types.test || 0;
2625
+ if (featCount > 0) {{
2626
+ const testFeatRatio = testCount / featCount;
2627
+ if (testFeatRatio < 0.3 && featCount > 20) suggestions.push(`测试跟不上功能:每 ${{Math.round(1/testFeatRatio)}} 个功能提交才对应 1 个测试提交,建议提高测试比例`);
2628
+ else if (testFeatRatio >= 0.8) suggestions.push(`测试与功能同步:测试/功能比 ${{testFeatRatio.toFixed(1)}},质量意识强`);
2629
+ }}
2630
+ }}
2631
+
2632
+ // ============================================================
2633
+ // 36. 提交历史跨度
2634
+ // ============================================================
2635
+ if (agg.activeDays > 0) {{
2636
+ if (agg.activeDays > 300) suggestions.push(`开发持续性强:活跃 ${{agg.activeDays}} 天,长期坚持 coding`);
2637
+ else if (agg.activeDays < 30 && agg.total > 50) suggestions.push(`开发集中在短期:仅 ${{agg.activeDays}} 天活跃但有 ${{agg.total}} 次提交,建议保持持续性`);
2638
+ }}
2639
+
2640
+ // ============================================================
2641
+ // 37. 工作日偏好
2642
+ // ============================================================
2643
+ if (agg.weekly.length === 7) {{
2644
+ const totalWeekly = agg.weekly.reduce((a, b) => a + b, 0);
2645
+ if (totalWeekly > 0) {{
2646
+ const maxIdx = agg.weekly.indexOf(Math.max(...agg.weekly));
2647
+ const dayRatio = agg.weekly[maxIdx] / totalWeekly * 100;
2648
+ if (dayRatio > 25) suggestions.push(`工作日偏好明显:${{weekdayNames[maxIdx]}} 占 ${{dayRatio.toFixed(0)}}% 提交,是最活跃的一天`);
2649
+ }}
2650
+ }}
2651
+
2652
+ // ============================================================
2653
+ // 38. 提交时间分布
2654
+ // ============================================================
2655
+ if (hourly.length === 24) {{
2656
+ const maxVal = Math.max(...hourly);
2657
+ const peakHoursList = hourly.map((v, i) => ({{v, i}})).filter(x => x.v > maxVal * 0.8).map(x => x.i);
2658
+ if (peakHoursList.length <= 3) suggestions.push(`提交时间集中:高峰时段集中在 ${{peakHoursList.join(', ')}} 点,建议分散到其他时段`);
2659
+ }}
2660
+
2661
+ // ============================================================
2662
+ // 39. 项目提交分布
2663
+ // ============================================================
2664
+ if (totalProjects > 0 && agg.total > 0) {{
2665
+ const shares = Object.values(agg.projectCounts).map(c => c / agg.total).filter(s => s > 0);
2666
+ const entropy = -shares.reduce((a, b) => a + b * Math.log2(b), 0);
2667
+ const maxEntropy = Math.log2(shares.length);
2668
+ if (maxEntropy > 0) {{
2669
+ const normalizedEntropy = entropy / maxEntropy;
2670
+ if (normalizedEntropy > 0.8) suggestions.push('项目分布均匀:提交分散在多个项目,建议聚焦核心项目');
2671
+ else if (normalizedEntropy < 0.3) suggestions.push('项目分布集中:大部分提交集中在少数项目,精力分配合理');
2672
+ }}
2673
+ }}
2674
+
2675
+ // ============================================================
2676
+ // 40. 开发习惯总结
2677
+ // ============================================================
2678
+ if (agg.habitScore.total >= 80) suggestions.push(`开发习惯优秀:总分 ${{agg.habitScore.total}} 分,继续保持!`);
2679
+ else if (agg.habitScore.total >= 60) suggestions.push(`开发习惯良好:总分 ${{agg.habitScore.total}} 分,还有提升空间`);
2680
+
2681
+ // ============================================================
2682
+ // 41. 项目类型多样性
2683
+ // ============================================================
2684
+ if (totalProjects > 0) {{
2685
+ const commitsList = Object.values(agg.projectCounts).sort((a, b) => a - b);
2686
+ const n = commitsList.length;
2687
+ if (n > 0 && agg.total > 0) {{
2688
+ let cumulative = 0;
2689
+ let giniSum = 0;
2690
+ for (let i = 0; i < n; i++) {{
2691
+ cumulative += commitsList[i];
2692
+ giniSum += (2 * (i + 1) - n - 1) * commitsList[i];
2693
+ }}
2694
+ const gini = giniSum / (n * agg.total);
2695
+ if (gini > 0.6) suggestions.push(`项目分布不均:基尼系数 ${{gini.toFixed(2)}},建议更均匀地分配精力`);
2696
+ else if (gini < 0.2) suggestions.push(`项目分布均匀:基尼系数 ${{gini.toFixed(2)}},精力分配合理`);
2697
+ }}
2698
+ }}
2699
+
2700
+ // ============================================================
2701
+ // 42. 提交频率稳定性
2702
+ // ============================================================
2703
+ if (hourly.length === 24) {{
2704
+ const mean = hourly.reduce((a, b) => a + b, 0) / 24;
2705
+ if (mean > 0) {{
2706
+ const variance = hourly.reduce((a, b) => a + (b - mean) ** 2, 0) / 24;
2707
+ if (variance > 0) {{
2708
+ const skewness = hourly.reduce((a, b) => a + (b - mean) ** 3, 0) / (24 * (variance ** 1.5));
2709
+ if (Math.abs(skewness) > 1) suggestions.push('提交时间分布偏斜:建议保持更规律的工作节奏');
2710
+ }}
2711
+ }}
2712
+ }}
2713
+
2714
+ // ============================================================
2715
+ // 43. 项目活跃周期
2716
+ // ============================================================
2717
+ // 注:JS 端暂无 projects 数据,跳过
2718
+
2719
+ // ============================================================
2720
+ // 44. 提交频率建议
2721
+ // ============================================================
2722
+ if (avgDaily > 0) {{
2723
+ if (avgDaily < 0.5) suggestions.push(`提交频率低:日均 ${{avgDaily.toFixed(1)}} 次提交,建议更频繁地提交代码`);
2724
+ else if (avgDaily > 5) suggestions.push(`提交频率高:日均 ${{avgDaily.toFixed(1)}} 次提交,注意保持代码质量`);
2725
+ }}
2726
+
2727
+ // ============================================================
2728
+ // 45. 开发效率
2729
+ // ============================================================
2730
+ if (agg.total > 0 && agg.activeDays > 0) {{
2731
+ const efficiency = agg.total / agg.activeDays;
2732
+ if (efficiency > 5) suggestions.push(`开发效率高:每次活跃日平均 ${{efficiency.toFixed(0)}} 次提交,产出稳定`);
2733
+ else if (efficiency < 2) suggestions.push(`开发效率待提升:每次活跃日平均 ${{efficiency.toFixed(0)}} 次提交,建议提高效率`);
2734
+ }}
2735
+
2736
+ // ============================================================
2737
+ // 46. 功能 vs 维护平衡
2738
+ // ============================================================
2739
+ if (agg.total > 0) {{
2740
+ const featC = agg.types.feat || 0;
2741
+ const fixC = agg.types.fix || 0;
2742
+ const refactorC = agg.types.refactor || 0;
2743
+ const maintenance = fixC + refactorC;
2744
+ if (featC > 0 && maintenance > 0) {{
2745
+ const ratio = featC / maintenance;
2746
+ if (ratio > 5) suggestions.push(`功能远超维护:功能/维护比 ${{ratio.toFixed(1)}}:1,注意技术债积累`);
2747
+ else if (ratio < 1) suggestions.push('维护为主:维护提交多于功能,代码在持续改善');
2748
+ }}
2749
+ }}
2750
+
2751
+ // ============================================================
2752
+ // 47. 项目状态分布
2753
+ // ============================================================
2754
+ if (totalProjects > 3 && agg.total > 0) {{
2755
+ const active = Object.values(agg.projectCounts).filter(c => c > 10).length;
2756
+ const inactive = totalProjects - active;
2757
+ if (inactive > totalProjects * 0.6) suggestions.push(`不活跃项目多:${{totalProjects}} 个项目中 ${{inactive}} 个提交不足 10 次,建议归档`);
2758
+ }}
2759
+
2760
+ // ============================================================
2761
+ // 48. 提交密度变化
2762
+ // ============================================================
2763
+ if (hourly.length === 24) {{
2764
+ const total = hourly.reduce((a, b) => a + b, 0);
2765
+ if (total > 0) {{
2766
+ const avgPerHour = total / 24;
2767
+ const highDensityHours = hourly.map((v, i) => ({{v, i}})).filter(x => x.v > avgPerHour * 2).map(x => x.i);
2768
+ if (highDensityHours.length <= 2 && highDensityHours.length > 0) suggestions.push(`编码时段集中:高峰仅在 ${{highDensityHours.join(', ')}} 点,建议适当分散`);
2769
+ }}
2770
+ }}
2771
+
2772
+ // ============================================================
2773
+ // 49. 周内提交均匀度
2774
+ // ============================================================
2775
+ if (agg.weekly.length === 7) {{
2776
+ const totalWeekly = agg.weekly.reduce((a, b) => a + b, 0);
2777
+ if (totalWeekly > 0) {{
2778
+ const avgPerDay = totalWeekly / 7;
2779
+ const variance = agg.weekly.reduce((a, v) => a + (v - avgPerDay) ** 2, 0) / 7;
2780
+ if (avgPerDay > 0) {{
2781
+ const cv = Math.sqrt(variance) / avgPerDay;
2782
+ if (cv > 0.8) suggestions.push('周内提交不均:各天差异大,建议保持更稳定的周节奏');
2783
+ }}
2784
+ }}
2785
+ }}
2786
+
2787
+ // ============================================================
2788
+ // 50. 功能开发占比
2789
+ // ============================================================
2790
+ if (agg.total > 0) {{
2791
+ const featC = agg.types.feat || 0;
2792
+ const featPct = featC / agg.total * 100;
2793
+ if (featPct > 60) suggestions.push(`功能开发为主:feat 占 ${{featPct.toFixed(0)}}%,建议平衡新功能与质量保障`);
2794
+ else if (featPct < 10 && agg.total > 50) suggestions.push(`功能开发少:feat 仅占 ${{featPct.toFixed(0)}}%,开发以维护为主`);
2795
+ }}
2796
+
2797
+ // ============================================================
2798
+ // 51. Chore 类型分析
2799
+ // ============================================================
2800
+ if (agg.total > 0) {{
2801
+ const choreC = agg.types.chore || 0;
2802
+ const chorePct = choreC / agg.total * 100;
2803
+ if (chorePct > 20) suggestions.push(`琐事较多:chore 占 ${{chorePct.toFixed(0)}}%,建议用自动化脚本减少重复工作`);
2804
+ }}
2805
+
2806
+ // ============================================================
2807
+ // 52. 提交历史长度
2808
+ // ============================================================
2809
+ if (agg.activeDays > 200) suggestions.push(`长期开发者:活跃超过 ${{agg.activeDays}} 天,积累了丰富的开发经验`);
2810
+
2811
+ // ============================================================
2812
+ // 53. 最热门项目贡献
2813
+ // ============================================================
2814
+ if (totalProjects > 1 && agg.total > 0) {{
2815
+ const maxCommits = Math.max(...Object.values(agg.projectCounts));
2816
+ const topRatio = maxCommits / agg.total * 100;
2817
+ if (topRatio > 60) suggestions.push(`单项目主导:最热门项目占 ${{topRatio.toFixed(0)}}% 提交,精力高度集中`);
2818
+ else if (topRatio < 20) suggestions.push(`精力分散:最热门项目仅占 ${{topRatio.toFixed(0)}}% 提交,建议聚焦核心项目`);
2819
+ }}
2820
+
2821
+ // ============================================================
2822
+ // 54. 文档与代码比例
2823
+ // ============================================================
2824
+ if (agg.total > 0) {{
2825
+ const docsC = agg.types.docs || 0;
2826
+ const codeC = agg.total - docsC;
2827
+ if (codeC > 0) {{
2828
+ const docCodeRatio = docsC / codeC;
2829
+ if (docCodeRatio > 0.3) suggestions.push(`文档投入高:文档/代码比 ${{docCodeRatio.toFixed(1)}},文档维护良好`);
2830
+ else if (docCodeRatio < 0.05 && agg.total > 50) suggestions.push(`文档投入不足:文档/代码比 ${{docCodeRatio.toFixed(2)}},建议增加文档更新`);
2831
+ }}
2832
+ }}
2833
+
2834
+ // ============================================================
2835
+ // 55. 早起 vs 夜猫子
2836
+ // ============================================================
2837
+ if (hourly.length === 24) {{
2838
+ const total = hourly.reduce((a, b) => a + b, 0);
2839
+ if (total > 0) {{
2840
+ const morning = hourly.slice(5, 9).reduce((a, b) => a + b, 0);
2841
+ const night = hourly.slice(22, 24).reduce((a, b) => a + b, 0) + hourly.slice(0, 2).reduce((a, b) => a + b, 0);
2842
+ if (morning > night * 2 && morning > 0) suggestions.push('早起型开发者:早晨提交多于深夜,作息健康');
2843
+ else if (night > morning * 2 && night > 0) suggestions.push('夜猫子型开发者:深夜提交多于早晨,建议调整作息');
2844
+ }}
2845
+ }}
2846
+
2847
+ // ============================================================
2848
+ // 56. 提交稳定性
2849
+ // ============================================================
2850
+ if (agg.activeDays > 10 && agg.total > 0) {{
2851
+ const commitsPerDay = agg.total / agg.activeDays;
2852
+ if (commitsPerDay >= 2 && commitsPerDay <= 6) suggestions.push(`提交节奏稳定:活跃日均 ${{commitsPerDay.toFixed(1)}} 次提交,节奏适中`);
2853
+ }}
2854
+
2855
+ // ============================================================
2856
+ // 57. 多语言项目管理
2857
+ // ============================================================
2858
+ if (langSet.size >= 3) {{
2859
+ const langCounts = agg.languageCounts;
2860
+ const topLang = Object.keys(langCounts).reduce((a, b) => (langCounts[a] || 0) > (langCounts[b] || 0) ? a : b, Object.keys(langCounts)[0]);
2861
+ suggestions.push(`多语言开发者:使用 ${{langSet.size}} 种语言,${{topLang}} 项目最多`);
2862
+ }}
2863
+
2864
+ // ============================================================
2865
+ // 58. 其他类型提交分析
2866
+ // ============================================================
2867
+ if (agg.total > 0) {{
2868
+ const otherC = agg.types.other || 0;
2869
+ const otherPct = otherC / agg.total * 100;
2870
+ if (otherPct > 30) suggestions.push(`提交分类不清:${{otherPct.toFixed(0)}}% 归为 other,建议使用标准 commit 类型`);
2871
+ else if (otherPct < 10 && agg.total > 50) suggestions.push(`提交分类规范:other 仅占 ${{otherPct.toFixed(0)}}%,类型使用清晰`);
2872
+ }}
2873
+
2874
+ // ============================================================
2875
+ // 59. 功能开发深度
2876
+ // ============================================================
2877
+ if (agg.total > 0) {{
2878
+ const featC = agg.types.feat || 0;
2879
+ const fixC = agg.types.fix || 0;
2880
+ if (featC > 0 && fixC > 0) {{
2881
+ const featFixRatio = featC / fixC;
2882
+ if (featFixRatio > 4) suggestions.push(`功能驱动:功能/修复比 ${{featFixRatio.toFixed(1)}}:1,开发节奏快`);
2883
+ else if (featFixRatio < 1) suggestions.push('修复驱动:修复多于新功能,建议分析根本原因');
2884
+ }}
2885
+ }}
2886
+
2887
+ // ============================================================
2888
+ // 60. 项目维护活跃度
2889
+ // ============================================================
2890
+ if (agg.total > 0) {{
2891
+ const wellMaintained = Object.values(agg.projectCounts).filter(c => c > 30).length;
2892
+ if (wellMaintained >= 3) suggestions.push(`多项目深耕:${{wellMaintained}} 个项目提交超过 30 次,项目管理能力强`);
2893
+ }}
2894
+
2895
+ // 默认
2896
+ if (!suggestions.length) suggestions.push('继续保持良好的开发习惯!各项指标都很健康');
2897
+
2898
+ document.getElementById('suggestions').innerHTML = suggestions.map((s, i) => `
2899
+ <div class="sug-item">
2900
+ <div class="sug-num">${{i + 1}}</div>
2901
+ <div class="sug-text">${{s}}</div>
2902
+ </div>`).join('');
2903
+ }}
2904
+
2905
+ // ============================================================
2906
+ // 主更新函数
2907
+ // ============================================================
2908
+ function updateAll(commits, project) {{
2909
+ const agg = aggregate(commits);
2910
+ agg.commits = commits; // 保留原始数据用于图表过滤
2911
+
2912
+ updateStats(agg, project);
2913
+ updatePersona(agg);
2914
+ updateHabitScore(agg);
2915
+ updateTags(agg);
2916
+ updateCharts(agg, project);
2917
+ updateInsights(agg);
2918
+ updateEngHealth(agg);
2919
+ updateRanking(agg);
2920
+ updateTypeBars(agg);
2921
+ updateAiImpact(agg);
2922
+ updateSuggestions(agg);
2923
+ }}
2924
+
2925
+ // ============================================================
2926
+ // 初始化
2927
+ // ============================================================
2928
+ const initHourly = {init_hourly_json};
2929
+ const initWeekly = {init_weekly_json};
2930
+ document.getElementById('filterProject').value = 'all';
2931
+ document.getElementById('filterRange').value = 'all';
2932
+ syncDateInputsToCommits(allCommits);
2933
+ initCharts(initHourly, initWeekly, {init_type_values_json}, {init_stack_json}, {init_bubble_json}, {init_month_labels_json});
2934
+ updateAll(allCommits, 'all');
2935
+ """
2936
+
2937
+
2938
+ def main(data_path=None, output_path=None):
2939
+ if data_path is None:
2940
+ data_path = 'data.json'
2941
+ if output_path is None:
2942
+ output_path = OUTPUT_FILE
2943
+
2944
+ data_path = os.path.abspath(os.path.expanduser(data_path))
2945
+ output_path = os.path.abspath(os.path.expanduser(output_path))
2946
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
2947
+
2948
+ print("📊 加载分析数据...")
2949
+ data = load_data(data_path)
2950
+
2951
+ print("📝 生成报告...")
2952
+ html = generate_report(data)
2953
+
2954
+ with open(output_path, 'w', encoding='utf-8') as f:
2955
+ f.write(html)
2956
+
2957
+ print(f"✅ 报告已生成: {output_path}")
2958
+ return output_path
2959
+
2960
+
2961
+ if __name__ == '__main__':
2962
+ import argparse
2963
+ parser = argparse.ArgumentParser()
2964
+ parser.add_argument('data_path', nargs='?', default=None, help='数据文件路径')
2965
+ parser.add_argument('--output', default=None, help='报告输出路径')
2966
+ args = parser.parse_args()
2967
+ main(data_path=args.data_path, output_path=args.output)