Skip to content

Commit 9dce28d

Browse files
committed
✨ Add copy & download actions for reports (MD/HTML)
1 parent 6bea943 commit 9dce28d

8 files changed

Lines changed: 918 additions & 74 deletions

File tree

docusaurus.config.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,43 @@ const config = {
2323
locales: ['zh-Hans'],
2424
},
2525

26+
// 插件配置 - 复制 markdown 源文件到静态目录
27+
plugins: [
28+
async function copyMarkdownPlugin(context, options) {
29+
return {
30+
name: 'copy-markdown-plugin',
31+
async postBuild({ outDir }) {
32+
const fs = require('fs');
33+
const path = require('path');
34+
35+
// 复制 reports 目录到 build 目录
36+
const copyDir = (src, dest) => {
37+
if (!fs.existsSync(dest)) {
38+
fs.mkdirSync(dest, { recursive: true });
39+
}
40+
const files = fs.readdirSync(src);
41+
for (const file of files) {
42+
const srcPath = path.join(src, file);
43+
const destPath = path.join(dest, file);
44+
const stat = fs.statSync(srcPath);
45+
if (stat.isDirectory()) {
46+
copyDir(srcPath, destPath);
47+
} else if (file.endsWith('.md')) {
48+
fs.copyFileSync(srcPath, destPath);
49+
}
50+
}
51+
};
52+
53+
const reportsDir = path.join(context.siteDir, 'reports');
54+
const outReportsDir = path.join(outDir, 'reports');
55+
if (fs.existsSync(reportsDir)) {
56+
copyDir(reportsDir, outReportsDir);
57+
}
58+
},
59+
};
60+
},
61+
],
62+
2663
presets: [
2764
[
2865
'classic',

llm_analyzer.py

Lines changed: 137 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,59 @@ def _build_repos_summary(self, repos: List[Dict]) -> str:
2727
"""Build a text summary of repositories for LLM input"""
2828
summary_parts = []
2929
for i, repo in enumerate(repos, 1):
30+
topics = ", ".join(repo.get("topics", [])[:5]) if repo.get("topics") else "无"
3031
summary_parts.append(
3132
f"{i}. **{repo.get('full_name', 'Unknown')}** ({repo.get('language', 'Unknown')})\n"
3233
f" - ⭐ Stars: {repo.get('stars', 0):,} (+{repo.get('stars_today', 0):,} today)\n"
3334
f" - 🍴 Forks: {repo.get('forks', 0):,}\n"
3435
f" - 📝 Description: {repo.get('description', 'No description')}\n"
36+
f" - 🏷️ Topics: {topics}\n"
3537
f" - 🔗 URL: {repo.get('url', '')}"
3638
)
3739
return "\n\n".join(summary_parts)
3840

39-
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
40-
def analyze_trends(self, repos: List[Dict], analysis_type: str = "comprehensive") -> str:
41-
"""
42-
Analyze trending repositories using LLM
41+
def _build_detailed_repo_info(self, repo: Dict) -> str:
42+
"""Build detailed repository info including README excerpt"""
43+
info_parts = [
44+
f"## 项目: {repo.get('full_name', 'Unknown')}",
45+
f"- **编程语言**: {repo.get('language', 'Unknown')}",
46+
f"- **Star 数**: {repo.get('stars', 0):,} (+{repo.get('stars_today', 0):,} today)",
47+
f"- **Fork 数**: {repo.get('forks', 0):,}",
48+
f"- **Open Issues**: {repo.get('open_issues', 0):,}",
49+
f"- **License**: {repo.get('license', 'Unknown')}",
50+
f"- **项目描述**: {repo.get('description', 'No description')}",
51+
]
4352

44-
Args:
45-
repos: List of repository dictionaries
46-
analysis_type: Type of analysis - 'comprehensive', 'brief', 'technical'
53+
# Topics
54+
if repo.get("topics"):
55+
info_parts.append(f"- **Topics**: {', '.join(repo['topics'][:10])}")
4756

48-
Returns:
49-
Analysis text from LLM
50-
"""
57+
# Languages breakdown
58+
if repo.get("languages"):
59+
total = sum(repo["languages"].values())
60+
lang_breakdown = ", ".join([
61+
f"{lang}: {bytes/total*100:.1f}%"
62+
for lang, bytes in list(repo["languages"].items())[:5]
63+
])
64+
info_parts.append(f"- **语言分布**: {lang_breakdown}")
65+
66+
# Recent commits
67+
if repo.get("recent_commits"):
68+
info_parts.append("\n**最近提交**:")
69+
for commit in repo["recent_commits"][:3]:
70+
info_parts.append(f" - [{commit['sha']}] {commit['message']}")
71+
72+
# README excerpt
73+
if repo.get("readme_excerpt"):
74+
# 截取 README 的前 800 字符
75+
readme = repo["readme_excerpt"][:800]
76+
info_parts.append(f"\n**README 摘要**:\n```\n{readme}\n```")
77+
78+
return "\n".join(info_parts)
79+
80+
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
81+
def analyze_trends(self, repos: List[Dict], analysis_type: str = "comprehensive") -> str:
82+
"""Analyze trending repositories using LLM"""
5183
repos_summary = self._build_repos_summary(repos)
5284

5385
prompts = {
@@ -135,65 +167,84 @@ def analyze_trends(self, repos: List[Dict], analysis_type: str = "comprehensive"
135167
return response.choices[0].message.content
136168

137169
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
138-
def analyze_single_repo(self, repo: Dict) -> str:
170+
def analyze_single_repo_detailed(self, repo: Dict) -> str:
139171
"""
140-
Analyze a single repository in detail
172+
Analyze a single repository in detail with README context
141173
142174
Args:
143-
repo: Repository dictionary
175+
repo: Repository dictionary with enriched data
144176
145177
Returns:
146178
Detailed analysis text
147179
"""
148-
prompt = f"""请对以下 GitHub 项目进行详细分析:
149-
150-
项目名称: {repo.get('full_name', 'Unknown')}
151-
编程语言: {repo.get('language', 'Unknown')}
152-
Star 数: {repo.get('stars', 0):,}
153-
今日新增 Star: {repo.get('stars_today', 0):,}
154-
Fork 数: {repo.get('forks', 0):,}
155-
项目描述: {repo.get('description', 'No description')}
156-
项目链接: {repo.get('url', '')}
157-
158-
请用中文提供以下分析:
159-
1. 项目定位和主要功能
160-
2. 技术特点和创新之处
161-
3. 适用场景和目标用户
162-
4. 项目优势和潜在不足
163-
5. 学习和使用建议"""
180+
repo_info = self._build_detailed_repo_info(repo)
181+
182+
prompt = f"""请对以下 GitHub 项目进行深度分析和解读:
183+
184+
{repo_info}
185+
186+
请用中文提供详细的项目解读,包括以下内容:
187+
188+
### 🎯 项目定位
189+
- 这个项目是什么?解决什么问题?
190+
- 核心功能和特性有哪些?
191+
192+
### 💡 技术亮点
193+
- 项目采用了哪些技术?有什么创新之处?
194+
- 架构设计有什么特点?
195+
196+
### 👥 目标用户
197+
- 这个项目适合谁使用?
198+
- 有哪些典型的使用场景?
199+
200+
### 📈 发展潜力
201+
- 基于当前数据,项目的发展趋势如何?
202+
- 社区活跃度如何?
203+
204+
### 🔧 快速上手
205+
- 如何快速开始使用这个项目?
206+
- 有哪些学习资源推荐?
207+
208+
### ⚠️ 注意事项
209+
- 使用时需要注意什么?
210+
- 有哪些已知的限制或问题?
211+
212+
请确保分析基于提供的信息,内容专业且对开发者有实际帮助。"""
164213

165214
response = self.client.chat.completions.create(
166215
model=self.model,
167216
messages=[
168217
{
169218
"role": "system",
170-
"content": "你是一位专业的开源项目分析师,擅长深入分析项目的技术特点和应用价值。"
219+
"content": "你是一位资深的开源项目分析师和技术专家。你擅长深入分析项目的技术细节、应用价值和发展潜力。请基于提供的项目信息(包括README内容)进行准确、专业的分析。"
171220
},
172221
{
173222
"role": "user",
174223
"content": prompt
175224
}
176225
],
177226
temperature=0.7,
178-
max_tokens=2000
227+
max_tokens=2500
179228
)
180229

181230
return response.choices[0].message.content
182231

183-
def generate_daily_report(self, repos: List[Dict], date_str: str) -> str:
232+
def generate_daily_report(self, repos: List[Dict], date_str: str, detailed_analysis: bool = True) -> str:
184233
"""
185-
Generate a complete daily report
234+
Generate a complete daily report with optional detailed project analysis
186235
187236
Args:
188237
repos: List of repository dictionaries
189-
date_str: Date string for the report (e.g., '2026-01-12')
238+
date_str: Date string for the report
239+
detailed_analysis: Whether to include detailed analysis for top projects
190240
191241
Returns:
192242
Complete markdown report with Docusaurus frontmatter
193243
"""
194-
analysis = self.analyze_trends(repos, "comprehensive")
244+
print("🤖 Generating overall trend analysis...")
245+
overall_analysis = self.analyze_trends(repos, "comprehensive")
195246

196-
# Build the complete report with Docusaurus frontmatter
247+
# Build the report
197248
report_parts = [
198249
"---",
199250
f"sidebar_position: 1",
@@ -202,48 +253,67 @@ def generate_daily_report(self, repos: List[Dict], date_str: str) -> str:
202253
"---\n",
203254
f"# 📈 GitHub Trending 日报 - {date_str}\n",
204255
f"> 本报告由 AI 自动生成,分析了 GitHub 当日 {len(repos)} 个热门项目\n",
205-
"---\n",
256+
]
257+
258+
# Table of Contents
259+
report_parts.extend([
260+
"## 📑 目录\n",
261+
"- [今日热门项目列表](#-今日热门项目列表)",
262+
"- [AI 趋势分析](#-ai-趋势分析)",
263+
])
264+
265+
if detailed_analysis:
266+
report_parts.append("- [重点项目深度解读](#-重点项目深度解读)")
267+
268+
report_parts.append("\n---\n")
269+
270+
# Project List
271+
report_parts.extend([
206272
"## 📋 今日热门项目列表\n",
207273
self._build_repos_summary(repos),
208274
"\n---\n",
209-
"## 🤖 AI 分析报告\n",
210-
analysis,
275+
])
276+
277+
# Overall Analysis
278+
report_parts.extend([
279+
"## 🤖 AI 趋势分析\n",
280+
overall_analysis,
211281
"\n---\n",
212-
f"*Generated by GitHub Trending Reporter | Data collected at {date_str}*"
213-
]
282+
])
283+
284+
# Detailed Analysis for Top Projects
285+
if detailed_analysis:
286+
report_parts.append("## 🔬 重点项目深度解读\n")
287+
report_parts.append("> 以下是对今日 Top 5 热门项目的详细解读\n\n")
288+
289+
# Analyze top 5 projects
290+
top_repos = repos[:5]
291+
for i, repo in enumerate(top_repos, 1):
292+
print(f"🔍 Analyzing project {i}/{len(top_repos)}: {repo.get('full_name')}...")
293+
294+
report_parts.append(f"### {i}. {repo.get('full_name', 'Unknown')}\n")
295+
report_parts.append(f"![{repo.get('name')}](https://opengraph.githubassets.com/1/{repo.get('full_name')})\n")
296+
297+
try:
298+
detailed = self.analyze_single_repo_detailed(repo)
299+
report_parts.append(detailed)
300+
except Exception as e:
301+
print(f" ⚠️ Error analyzing {repo.get('full_name')}: {e}")
302+
report_parts.append(f"*分析生成失败: {str(e)}*")
303+
304+
report_parts.append("\n---\n")
305+
306+
# Footer
307+
report_parts.append(f"\n*Generated by GitHub Trending Reporter | Data collected at {date_str}*")
214308

215309
return "\n".join(report_parts)
216310

217311

218312
def analyze_trending(repos: List[Dict], analysis_type: str = "comprehensive") -> str:
219-
"""
220-
Convenience function to analyze trending repositories
221-
222-
Args:
223-
repos: List of repository dictionaries
224-
analysis_type: Type of analysis
225-
226-
Returns:
227-
Analysis text
228-
"""
313+
"""Convenience function to analyze trending repositories"""
229314
analyzer = LLMAnalyzer()
230315
return analyzer.analyze_trends(repos, analysis_type)
231316

232317

233318
if __name__ == "__main__":
234-
# Test with sample data
235-
sample_repos = [
236-
{
237-
"full_name": "test/repo",
238-
"language": "Python",
239-
"stars": 1000,
240-
"stars_today": 100,
241-
"forks": 50,
242-
"description": "A test repository",
243-
"url": "https://github.com/test/repo"
244-
}
245-
]
246-
247-
analyzer = LLMAnalyzer()
248-
print("Testing LLM Analyzer...")
249-
# Note: Will fail without valid API key
319+
print("LLM Analyzer module - requires valid API key to test")

main.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ def run_report(
2222
analysis_type: str = "comprehensive",
2323
push_to_repo: bool = True,
2424
output_local: bool = False,
25-
date_str: Optional[str] = None
25+
date_str: Optional[str] = None,
26+
detailed_analysis: bool = True
2627
) -> bool:
2728
"""
2829
Run the complete trending report workflow
@@ -34,6 +35,7 @@ def run_report(
3435
push_to_repo: Whether to push results to GitHub repository
3536
output_local: Whether to save results locally
3637
date_str: Override date string (for testing)
38+
detailed_analysis: Whether to include detailed analysis for top projects
3739
3840
Returns:
3941
True if successful
@@ -45,6 +47,7 @@ def run_report(
4547
print(f" Language filter: {language or 'All'}")
4648
print(f" Time range: {since}")
4749
print(f" Analysis type: {analysis_type}")
50+
print(f" Detailed analysis: {'Yes (Top 5 projects)' if detailed_analysis else 'No'}")
4851
print()
4952

5053
# Step 1: Fetch trending repositories
@@ -64,7 +67,7 @@ def run_report(
6467
print("🤖 Analyzing with LLM...")
6568
try:
6669
analyzer = LLMAnalyzer()
67-
report = analyzer.generate_daily_report(repos, date_str)
70+
report = analyzer.generate_daily_report(repos, date_str, detailed_analysis=detailed_analysis)
6871
print(" Analysis complete")
6972
except Exception as e:
7073
print(f"❌ Error during LLM analysis: {e}")
@@ -220,6 +223,12 @@ def main():
220223
help="Override date string (format: YYYY-MM-DD)"
221224
)
222225

226+
parser.add_argument(
227+
"--no-detailed",
228+
action="store_true",
229+
help="Skip detailed analysis for individual projects"
230+
)
231+
223232
args = parser.parse_args()
224233

225234
success = run_report(
@@ -228,7 +237,8 @@ def main():
228237
analysis_type=args.analysis,
229238
push_to_repo=not args.no_push,
230239
output_local=args.local,
231-
date_str=args.date
240+
date_str=args.date,
241+
detailed_analysis=not args.no_detailed
232242
)
233243

234244
sys.exit(0 if success else 1)

0 commit comments

Comments
 (0)