fix(stats): restore dynamic badges and update community statistics

- Recover dynamic Shields.io badges in README by restoring missing Gist ID 'db3d95687075a880af6f1fba76d679c6'.
- Add 'tool' to DOWNLOADABLE_TYPES and implement TYPE_ALIASES for normalization (mapping 'tools' to 'tool').
- Update community statistics and Ranking list (Top 6) based on latest marketplace data.
- Refactor openwebui_stats.py with 100% English comments and enhanced user ID auto-resolution.
- Verify Smart Mind Map (#1) and other top plugins maintain correct sorting.
This commit is contained in:
fujie
2026-03-03 19:29:34 +08:00
parent 83e317a335
commit 369e8c900c
5 changed files with 775 additions and 278 deletions

View File

@@ -9,7 +9,6 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
<!-- STATS_START -->
## 📊 Community Stats
>
> ![updated](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_updated.json&style=flat)
| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |
@@ -20,19 +19,18 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
| :---: | :---: | :---: | :---: | :---: |
| ![posts](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_posts.json&style=flat) | ![downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_downloads.json&style=flat) | ![views](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_views.json&style=flat) | ![upvotes](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_upvotes.json&style=flat) | ![saves](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_saves.json&style=flat) |
### 🔥 Top 6 Popular Plugins
### 🔥 Top 6 Popular Plugins
| Rank | Plugin | Version | Downloads | Views | 📅 Updated |
| :---: | :--- | :---: | :---: | :---: | :---: |
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | ![p1_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_version.json&style=flat) | ![p1_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_dl.json&style=flat) | ![p1_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--27-gray?style=flat) |
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | ![p1_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_version.json&style=flat) | ![p1_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_dl.json&style=flat) | ![p1_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--28-gray?style=flat) |
| 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | ![p2_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_version.json&style=flat) | ![p2_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_dl.json&style=flat) | ![p2_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--13-gray?style=flat) |
| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | ![p3_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_version.json&style=flat) | ![p3_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_dl.json&style=flat) | ![p3_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--27-gray?style=flat) |
| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | ![p3_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_version.json&style=flat) | ![p3_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_dl.json&style=flat) | ![p3_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--28-gray?style=flat) |
| 4⃣ | [Export to Word Enhanced](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | ![p4_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_version.json&style=flat) | ![p4_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_dl.json&style=flat) | ![p4_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--13-gray?style=flat) |
| 5⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | ![p5_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_version.json&style=flat) | ![p5_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_dl.json&style=flat) | ![p5_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--21-gray?style=flat) |
| 5⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | ![p5_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_version.json&style=flat) | ![p5_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_dl.json&style=flat) | ![p5_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--28-gray?style=flat) |
| 6⃣ | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | ![p6_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_version.json&style=flat) | ![p6_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_dl.json&style=flat) | ![p6_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--13-gray?style=flat) |
### 📈 Total Downloads Trend
![Activity](https://gist.githubusercontent.com/Fu-Jie/db3d95687075a880af6f1fba76d679c6/raw/chart.svg)
*See full stats and charts in [Community Stats Report](./docs/community-stats.md)*

View File

@@ -6,7 +6,6 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
<!-- STATS_START -->
## 📊 社区统计
>
> ![updated_zh](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_updated_zh.json&style=flat)
| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |
@@ -17,19 +16,18 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
| :---: | :---: | :---: | :---: | :---: |
| ![posts](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_posts.json&style=flat) | ![downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_downloads.json&style=flat) | ![views](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_views.json&style=flat) | ![upvotes](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_upvotes.json&style=flat) | ![saves](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_saves.json&style=flat) |
### 🔥 热门插件 Top 6
### 🔥 热门插件 Top 6
| 排名 | 插件 | 版本 | 下载 | 浏览 | 📅 更新 |
| :---: | :--- | :---: | :---: | :---: | :---: |
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | ![p1_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_version.json&style=flat) | ![p1_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_dl.json&style=flat) | ![p1_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--27-gray?style=flat) |
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | ![p1_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_version.json&style=flat) | ![p1_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_dl.json&style=flat) | ![p1_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--28-gray?style=flat) |
| 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | ![p2_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_version.json&style=flat) | ![p2_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_dl.json&style=flat) | ![p2_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--13-gray?style=flat) |
| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | ![p3_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_version.json&style=flat) | ![p3_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_dl.json&style=flat) | ![p3_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--27-gray?style=flat) |
| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | ![p3_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_version.json&style=flat) | ![p3_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_dl.json&style=flat) | ![p3_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--28-gray?style=flat) |
| 4⃣ | [Export to Word Enhanced](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | ![p4_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_version.json&style=flat) | ![p4_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_dl.json&style=flat) | ![p4_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--13-gray?style=flat) |
| 5⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | ![p5_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_version.json&style=flat) | ![p5_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_dl.json&style=flat) | ![p5_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--21-gray?style=flat) |
| 5⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | ![p5_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_version.json&style=flat) | ![p5_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_dl.json&style=flat) | ![p5_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--28-gray?style=flat) |
| 6⃣ | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | ![p6_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_version.json&style=flat) | ![p6_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_dl.json&style=flat) | ![p6_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--13-gray?style=flat) |
### 📈 总下载量累计趋势
![Activity](https://gist.githubusercontent.com/Fu-Jie/db3d95687075a880af6f1fba76d679c6/raw/chart.svg)
*完整统计与趋势图请查看 [社区统计报告](./docs/community-stats.zh.md)*

View File

@@ -1,16 +1,17 @@
{
"total_posts": 25,
"total_downloads": 6379,
"total_views": 67827,
"total_upvotes": 254,
"total_downvotes": 3,
"total_saves": 337,
"total_downloads": 7058,
"total_views": 75199,
"total_upvotes": 273,
"total_downvotes": 4,
"total_saves": 372,
"total_comments": 58,
"by_type": {
"post": 6,
"tool": 1,
"post": 5,
"pipe": 1,
"action": 12,
"filter": 4,
"action": 12,
"prompt": 1,
"review": 1
},
@@ -22,13 +23,13 @@
"version": "1.0.0",
"author": "Fu-Jie",
"description": "Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.",
"downloads": 1328,
"views": 11410,
"upvotes": 23,
"saves": 59,
"downloads": 1426,
"views": 12082,
"upvotes": 26,
"saves": 63,
"comments": 15,
"created_at": "2025-12-30",
"updated_at": "2026-02-27",
"created_at": "2025-12-31",
"updated_at": "2026-02-28",
"url": "https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a"
},
{
@@ -38,10 +39,10 @@
"version": "1.5.0",
"author": "Fu-Jie",
"description": "AI-powered infographic generator based on AntV Infographic. Supports professional templates, auto-icon matching, and SVG/PNG downloads.",
"downloads": 1076,
"views": 10746,
"downloads": 1155,
"views": 11609,
"upvotes": 25,
"saves": 40,
"saves": 45,
"comments": 10,
"created_at": "2025-12-28",
"updated_at": "2026-02-13",
@@ -54,13 +55,13 @@
"version": "1.2.7",
"author": "Fu-Jie",
"description": "A content normalizer filter that fixes common Markdown formatting issues in LLM outputs, such as broken code blocks, LaTeX formulas, and list formatting. Including LaTeX command protection.",
"downloads": 609,
"views": 6795,
"upvotes": 18,
"saves": 37,
"downloads": 661,
"views": 7239,
"upvotes": 20,
"saves": 40,
"comments": 5,
"created_at": "2026-01-12",
"updated_at": "2026-02-27",
"updated_at": "2026-02-28",
"url": "https://openwebui.com/posts/markdown_normalizer_baaa8732"
},
{
@@ -70,10 +71,10 @@
"version": "0.4.4",
"author": "Fu-Jie",
"description": "Export current conversation from Markdown to Word (.docx) with Mermaid diagrams rendered client-side (Mermaid.js, SVG+PNG), LaTeX math, real hyperlinks, improved tables, syntax highlighting, and blockquote support.",
"downloads": 578,
"views": 4611,
"downloads": 628,
"views": 4995,
"upvotes": 16,
"saves": 30,
"saves": 35,
"comments": 5,
"created_at": "2026-01-03",
"updated_at": "2026-02-13",
@@ -86,13 +87,13 @@
"version": "1.3.0",
"author": "Fu-Jie",
"description": "Reduces token consumption in long conversations while maintaining coherence through intelligent summarization and message compression.",
"downloads": 559,
"views": 5452,
"upvotes": 15,
"saves": 41,
"downloads": 619,
"views": 5875,
"upvotes": 16,
"saves": 46,
"comments": 0,
"created_at": "2025-11-08",
"updated_at": "2026-02-21",
"updated_at": "2026-02-28",
"url": "https://openwebui.com/posts/async_context_compression_b1655bc8"
},
{
@@ -102,10 +103,10 @@
"version": "0.3.7",
"author": "Fu-Jie",
"description": "Extracts tables from chat messages and exports them to Excel (.xlsx) files with smart formatting.",
"downloads": 492,
"views": 2693,
"downloads": 523,
"views": 2898,
"upvotes": 10,
"saves": 8,
"saves": 9,
"comments": 0,
"created_at": "2025-05-30",
"updated_at": "2026-02-13",
@@ -118,8 +119,8 @@
"version": "",
"author": "",
"description": "",
"downloads": 473,
"views": 5498,
"downloads": 523,
"views": 6055,
"upvotes": 9,
"saves": 14,
"comments": 0,
@@ -127,22 +128,6 @@
"updated_at": "2026-01-28",
"url": "https://openwebui.com/posts/ai_task_instruction_generator_9bab8b37"
},
{
"title": "Flash Card",
"slug": "flash_card_65a2ea8f",
"type": "action",
"version": "0.2.4",
"author": "Fu-Jie",
"description": "Quickly generates beautiful flashcards from text, extracting key points and categories.",
"downloads": 285,
"views": 4128,
"upvotes": 13,
"saves": 18,
"comments": 2,
"created_at": "2025-12-30",
"updated_at": "2026-02-13",
"url": "https://openwebui.com/posts/flash_card_65a2ea8f"
},
{
"title": "GitHub Copilot Official SDK Pipe",
"slug": "github_copilot_official_sdk_pipe_ce96f7b4",
@@ -150,15 +135,31 @@
"version": "0.9.0",
"author": "Fu-Jie",
"description": "Integrate GitHub Copilot SDK. Supports dynamic models, multi-turn conversation, streaming, multimodal input, infinite sessions, bidirectional OpenWebUI Skills bridge, and manage_skills tool.",
"downloads": 263,
"views": 4106,
"upvotes": 14,
"downloads": 301,
"views": 4540,
"upvotes": 16,
"saves": 10,
"comments": 6,
"created_at": "2026-01-26",
"updated_at": "2026-02-27",
"updated_at": "2026-02-28",
"url": "https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4"
},
{
"title": "Flash Card",
"slug": "flash_card_65a2ea8f",
"type": "action",
"version": "0.2.4",
"author": "Fu-Jie",
"description": "Quickly generates beautiful flashcards from text, extracting key points and categories.",
"downloads": 295,
"views": 4297,
"upvotes": 13,
"saves": 20,
"comments": 2,
"created_at": "2025-12-30",
"updated_at": "2026-02-13",
"url": "https://openwebui.com/posts/flash_card_65a2ea8f"
},
{
"title": "Deep Dive",
"slug": "deep_dive_c0b846e4",
@@ -166,15 +167,31 @@
"version": "1.0.0",
"author": "Fu-Jie",
"description": "A comprehensive thinking lens that dives deep into any content - from context to logic, insights, and action paths.",
"downloads": 204,
"views": 1631,
"downloads": 211,
"views": 1699,
"upvotes": 6,
"saves": 13,
"saves": 14,
"comments": 0,
"created_at": "2026-01-08",
"updated_at": "2026-01-08",
"url": "https://openwebui.com/posts/deep_dive_c0b846e4"
},
{
"title": "OpenWebUI Skills Manager Tool",
"slug": "openwebui_skills_manager_tool_b4bce8e4",
"type": "tool",
"version": "",
"author": "",
"description": "",
"downloads": 169,
"views": 2629,
"upvotes": 6,
"saves": 7,
"comments": 0,
"created_at": "2026-02-28",
"updated_at": "2026-02-28",
"url": "https://openwebui.com/posts/openwebui_skills_manager_tool_b4bce8e4"
},
{
"title": "导出为Word增强版",
"slug": "导出为_word_支持公式流程图表格和代码块_8a6306c0",
@@ -182,8 +199,8 @@
"version": "0.4.4",
"author": "Fu-Jie",
"description": "将对话导出为 Word (.docx),支持 Mermaid 图表 (客户端渲染 SVG+PNG)、LaTeX 数学公式、真实超链接、增强表格格式、代码高亮和引用块。",
"downloads": 153,
"views": 2631,
"downloads": 157,
"views": 2732,
"upvotes": 14,
"saves": 7,
"comments": 4,
@@ -198,8 +215,8 @@
"version": "0.1.0",
"author": "Fu-Jie",
"description": "Automatically extracts project rules from conversations and injects them into the folder's system prompt.",
"downloads": 99,
"views": 1839,
"downloads": 106,
"views": 1911,
"upvotes": 7,
"saves": 11,
"comments": 0,
@@ -207,6 +224,22 @@
"updated_at": "2026-01-20",
"url": "https://openwebui.com/posts/folder_memory_auto_evolving_project_context_4a9875b2"
},
{
"title": "GitHub Copilot SDK Files Filter",
"slug": "github_copilot_sdk_files_filter_403a62ee",
"type": "filter",
"version": "0.1.3",
"author": "Fu-Jie",
"description": "A specialized filter to bypass OpenWebUI's default RAG for GitHub Copilot SDK models. It moves uploaded files to a safe location ('copilot_files') so the Copilot Pipe can process them natively without interference.",
"downloads": 69,
"views": 2231,
"upvotes": 4,
"saves": 1,
"comments": 0,
"created_at": "2026-02-09",
"updated_at": "2026-02-26",
"url": "https://openwebui.com/posts/github_copilot_sdk_files_filter_403a62ee"
},
{
"title": "智能信息图",
"slug": "智能信息图_e04a48ff",
@@ -215,7 +248,7 @@
"author": "Fu-Jie",
"description": "基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。",
"downloads": 65,
"views": 1304,
"views": 1370,
"upvotes": 10,
"saves": 1,
"comments": 0,
@@ -223,22 +256,6 @@
"updated_at": "2026-02-13",
"url": "https://openwebui.com/posts/智能信息图_e04a48ff"
},
{
"title": "GitHub Copilot SDK Files Filter",
"slug": "github_copilot_sdk_files_filter_403a62ee",
"type": "filter",
"version": "0.1.3",
"author": "Fu-Jie",
"description": "A specialized filter to bypass OpenWebUI's default RAG for GitHub Copilot SDK models. It moves uploaded files to a safe location ('copilot_files') so the Copilot Pipe can process them natively without interference.",
"downloads": 54,
"views": 2098,
"upvotes": 3,
"saves": 1,
"comments": 0,
"created_at": "2026-02-09",
"updated_at": "2026-02-25",
"url": "https://openwebui.com/posts/github_copilot_sdk_files_filter_403a62ee"
},
{
"title": "思维导图",
"slug": "智能生成交互式思维导图帮助用户可视化知识_8d4b097b",
@@ -246,8 +263,8 @@
"version": "0.9.2",
"author": "Fu-Jie",
"description": "智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。",
"downloads": 45,
"views": 691,
"downloads": 50,
"views": 734,
"upvotes": 6,
"saves": 2,
"comments": 0,
@@ -263,7 +280,7 @@
"author": "Fu-Jie",
"description": "通过智能摘要和消息压缩,降低长对话的 token 消耗,同时保持对话连贯性。",
"downloads": 38,
"views": 783,
"views": 814,
"upvotes": 7,
"saves": 5,
"comments": 0,
@@ -278,8 +295,8 @@
"version": "0.2.4",
"author": "Fu-Jie",
"description": "快速将文本提炼为精美的学习记忆卡片,支持核心要点提取与分类。",
"downloads": 32,
"views": 830,
"downloads": 33,
"views": 863,
"upvotes": 7,
"saves": 1,
"comments": 0,
@@ -294,8 +311,8 @@
"version": "1.0.0",
"author": "Fu-Jie",
"description": "全方位的思维透镜 —— 从背景全景到逻辑脉络,从深度洞察到行动路径。",
"downloads": 26,
"views": 581,
"downloads": 29,
"views": 626,
"upvotes": 5,
"saves": 1,
"comments": 0,
@@ -304,51 +321,35 @@
"url": "https://openwebui.com/posts/精读_99830b0f"
},
{
"title": "🚀 GitHub Copilot SDK Pipe v0.9.0: Copilot SDK Skills Core Capabilities & Extended Delivery",
"title": "🚀 GitHub Copilot SDK Pipe v0.9.0: Skills & RichUI",
"slug": "github_copilot_sdk_pipe_v090_copilot_sdk_skills_co_99a42452",
"type": "post",
"version": "",
"author": "",
"description": "",
"downloads": 0,
"views": 7,
"upvotes": 0,
"saves": 0,
"views": 1162,
"upvotes": 5,
"saves": 1,
"comments": 0,
"created_at": "2026-02-27",
"updated_at": "2026-02-27",
"created_at": "2026-02-28",
"updated_at": "2026-02-28",
"url": "https://openwebui.com/posts/github_copilot_sdk_pipe_v090_copilot_sdk_skills_co_99a42452"
},
{
"title": "🚀 GitHub Copilot SDK Pipe v0.8.0: Conditional Tool Filtering & Publish Reliability 🎛",
"slug": "github_copilot_sdk_pipe_v080_conditional_tool_filt_a5a3322d",
"type": "post",
"version": "",
"author": "",
"description": "",
"downloads": 0,
"views": 1059,
"upvotes": 2,
"saves": 2,
"comments": 0,
"created_at": "2026-02-25",
"updated_at": "2026-02-25",
"url": "https://openwebui.com/posts/github_copilot_sdk_pipe_v080_conditional_tool_filt_a5a3322d"
},
{
"title": "🚀 GitHub Copilot SDK Pipe v0.7.0: Native Tool UI & Zero-Config CLI 🛠️",
"title": "🚀 GitHub Copilot SDK Pipe v0.7.0: Skills & Rich UI 🛠",
"slug": "github_copilot_sdk_pipe_v070_native_tool_ui_zero_c_4af38131",
"type": "post",
"version": "",
"author": "",
"description": "",
"downloads": 0,
"views": 2162,
"upvotes": 7,
"views": 2504,
"upvotes": 8,
"saves": 2,
"comments": 1,
"created_at": "2026-02-22",
"updated_at": "2026-02-22",
"created_at": "2026-02-23",
"updated_at": "2026-02-28",
"url": "https://openwebui.com/posts/github_copilot_sdk_pipe_v070_native_tool_ui_zero_c_4af38131"
},
{
@@ -359,7 +360,7 @@
"author": "",
"description": "",
"downloads": 0,
"views": 2257,
"views": 2341,
"upvotes": 7,
"saves": 4,
"comments": 0,
@@ -375,12 +376,12 @@
"author": "",
"description": "",
"downloads": 0,
"views": 1839,
"views": 1887,
"upvotes": 12,
"saves": 19,
"saves": 21,
"comments": 8,
"created_at": "2026-01-25",
"updated_at": "2026-01-28",
"updated_at": "2026-01-29",
"url": "https://openwebui.com/posts/open_webui_prompt_plus_ai_powered_prompt_manager_s_15fa060e"
},
{
@@ -391,7 +392,7 @@
"author": "",
"description": "",
"downloads": 0,
"views": 234,
"views": 246,
"upvotes": 2,
"saves": 0,
"comments": 0,
@@ -407,9 +408,9 @@
"author": "",
"description": "",
"downloads": 0,
"views": 1502,
"views": 1531,
"upvotes": 16,
"saves": 11,
"saves": 12,
"comments": 2,
"created_at": "2026-01-10",
"updated_at": "2026-01-10",
@@ -421,10 +422,10 @@
"name": "Fu-Jie",
"profile_url": "https://openwebui.com/u/Fu-Jie",
"profile_image": "https://community.s3.openwebui.com/uploads/users/b15d1348-4347-42b4-b815-e053342d6cb0/profile_d9510745-4bd4-4f8f-a997-4a21847d9300.webp",
"followers": 295,
"followers": 307,
"following": 6,
"total_points": 299,
"post_points": 251,
"total_points": 319,
"post_points": 271,
"comment_points": 48,
"contributions": 54
}

View File

@@ -0,0 +1,447 @@
#!/usr/bin/env python3
"""
Run an autonomous programming task via Copilot SDK.
Usage:
python plugins/debug/copilot-sdk/auto_programming_task.py \
--task "Fix failing tests in tests/test_xxx.py" \
--cwd /Users/fujie/app/python/oui/openwebui-extensions
Notes:
- Default model is gpt-5-mini (low-cost for repeated runs).
- This script DOES NOT pin/upgrade SDK versions.
- Copilot CLI must be available (or set COPILOT_CLI_PATH).
"""
import argparse
import asyncio
import os
import sys
import textwrap
from pathlib import Path
from typing import Optional
DEFAULT_TASK = (
"Convert plugins/actions/smart-mind-map/smart_mind_map.py (Action plugin) "
"into a Tool plugin implementation under plugins/tools/. "
"Keep Copilot SDK version unchanged, follow patterns from "
"plugins/pipes/github-copilot-sdk/, and implement a runnable MVP with "
"i18n/status events/basic validation."
)
def _ensure_copilot_importable() -> None:
"""Try local SDK path fallback if `copilot` package is not installed."""
try:
import copilot # noqa: F401
return
except Exception:
pass
candidates = []
env_path = os.environ.get("COPILOT_SDK_PYTHON_PATH", "").strip()
if env_path:
candidates.append(Path(env_path))
# Default sibling repo path: ../copilot-sdk/python
# Current file: plugins/debug/copilot-sdk/auto_programming_task.py
repo_root = Path(__file__).resolve().parents[3]
candidates.append(repo_root.parent / "copilot-sdk" / "python")
for path in candidates:
if path.exists():
sys.path.insert(0, str(path))
try:
import copilot # noqa: F401
return
except Exception:
continue
raise RuntimeError(
"Cannot import `copilot` package. Install copilot-sdk python package "
"or set COPILOT_SDK_PYTHON_PATH to copilot-sdk/python directory."
)
def _build_agent_prompt(task: str, cwd: str, extra_context: Optional[str]) -> str:
extra = extra_context.strip() if extra_context else ""
return textwrap.dedent(
f"""
You are an autonomous coding agent running in workspace: {cwd}
Primary task:
{task}
Requirements:
1. Inspect relevant files and implement changes directly in the workspace.
2. Keep changes minimal and focused on the task.
3. If tests/build commands exist, run targeted validation for changed scope.
4. If blocked, explain the blocker and propose concrete next steps.
5. At the end, provide a concise summary of:
- files changed
- what was implemented
- validation results
{f'Additional context:\n{extra}' if extra else ''}
"""
).strip()
def _build_planning_prompt(task: str, cwd: str, extra_context: Optional[str]) -> str:
extra = extra_context.strip() if extra_context else ""
return textwrap.dedent(
f"""
You are a senior autonomous coding planner running in workspace: {cwd}
User requirement (may be underspecified):
{task}
Goal:
Expand the requirement into an actionable implementation plan that can be executed end-to-end without extra clarification whenever possible.
Output format (strict):
1) Expanded Objective (clear, concrete, scoped)
2) Assumptions (only necessary assumptions)
3) Step-by-step Plan (ordered, verifiable)
4) Validation Plan (how to verify changes)
5) Execution Brief (concise instruction for implementation agent)
Constraints:
- Keep scope minimal and aligned with the user requirement.
- Do not invent unrelated features.
- Prefer practical MVP completion.
{f'Additional context:\n{extra}' if extra else ''}
"""
).strip()
def _build_execution_prompt(
task: str, cwd: str, extra_context: Optional[str], plan_text: str
) -> str:
extra = extra_context.strip() if extra_context else ""
return textwrap.dedent(
f"""
You are an autonomous coding agent running in workspace: {cwd}
User requirement:
{task}
Planner output (must follow):
{plan_text}
Execution requirements:
1. Execute the plan directly; do not stop after analysis.
2. If the original requirement is underspecified, use the planner assumptions and continue.
3. Keep changes minimal, focused, and runnable.
4. Run targeted validation for changed scope where possible.
5. If blocked by missing prerequisites, report blocker and the smallest next action.
6. Finish with concise summary:
- files changed
- implemented behavior
- validation results
{f'Additional context:\n{extra}' if extra else ''}
"""
).strip()
async def _run_single_session(
client,
args: argparse.Namespace,
prompt: str,
stage_name: str,
stream_output: bool,
) -> tuple[int, str]:
from copilot.types import PermissionHandler
def _auto_user_input_handler(request, _invocation):
question = ""
if isinstance(request, dict):
question = str(request.get("question", "")).lower()
choices = request.get("choices") or []
if choices and isinstance(choices, list):
preferred = args.auto_user_answer.strip()
for choice in choices:
c = str(choice)
if preferred and preferred.lower() == c.lower():
return {"answer": c, "wasFreeform": False}
return {"answer": str(choices[0]), "wasFreeform": False}
preferred = args.auto_user_answer.strip() or "continue"
if "confirm" in question or "proceed" in question:
preferred = "yes"
return {"answer": preferred, "wasFreeform": True}
session_config = {
"model": args.model,
"reasoning_effort": args.reasoning_effort,
"streaming": True,
"infinite_sessions": {
"enabled": True,
},
"on_permission_request": PermissionHandler.approve_all,
"on_user_input_request": _auto_user_input_handler,
}
session = await client.create_session(session_config)
done = asyncio.Event()
full_messages = []
has_error = False
def on_event(event):
nonlocal has_error
etype = getattr(event, "type", "unknown")
if hasattr(etype, "value"):
etype = etype.value
if args.trace_events:
print(f"\n[{stage_name}][EVENT] {etype}", flush=True)
if etype == "assistant.message_delta" and stream_output:
delta = getattr(event.data, "delta_content", "") or ""
if delta:
print(delta, end="", flush=True)
elif etype == "assistant.message":
content = getattr(event.data, "content", "") or ""
if content:
full_messages.append(content)
elif etype == "session.error":
has_error = True
done.set()
elif etype == "session.idle":
done.set()
unsubscribe = session.on(on_event)
heartbeat_task = None
async def _heartbeat():
while not done.is_set():
await asyncio.sleep(max(3, int(args.heartbeat_seconds)))
if not done.is_set():
print(
f"[{stage_name}][heartbeat] waiting for assistant events...",
flush=True,
)
try:
heartbeat_task = asyncio.create_task(_heartbeat())
await session.send({"prompt": prompt, "mode": "immediate"})
await asyncio.wait_for(done.wait(), timeout=args.timeout)
if stream_output:
print("\n")
final_message = full_messages[-1] if full_messages else ""
if final_message:
print(f"\n===== {stage_name} FINAL MESSAGE =====\n")
print(final_message)
if has_error:
return 1, final_message
return 0, final_message
except asyncio.TimeoutError:
print(f"\n❌ [{stage_name}] Timeout after {args.timeout}s")
return 124, ""
except Exception as exc:
print(f"\n❌ [{stage_name}] Run failed: {exc}")
return 1, ""
finally:
if heartbeat_task:
heartbeat_task.cancel()
try:
unsubscribe()
except Exception:
pass
try:
await session.destroy()
except Exception:
pass
async def run_task(args: argparse.Namespace) -> int:
_ensure_copilot_importable()
from copilot import CopilotClient
task_text = (args.task or "").strip()
if args.task_file:
task_text = Path(args.task_file).read_text(encoding="utf-8").strip()
if not task_text:
task_text = DEFAULT_TASK
direct_prompt = _build_agent_prompt(task_text, args.cwd, args.extra_context)
client_options = {
"cwd": args.cwd,
"log_level": args.log_level,
}
if args.cli_path:
client_options["cli_path"] = args.cli_path
if args.github_token:
client_options["github_token"] = args.github_token
print(f"🚀 Starting Copilot SDK task runner")
print(f" cwd: {args.cwd}")
print(f" model: {args.model}")
print(f" reasoning_effort: {args.reasoning_effort}")
print(f" plan_first: {args.plan_first}")
client = CopilotClient(client_options)
await client.start()
try:
if args.plan_first:
planning_prompt = _build_planning_prompt(
task_text, args.cwd, args.extra_context
)
print("\n🧭 Stage 1/2: Planning and requirement expansion")
plan_code, plan_text = await _run_single_session(
client=client,
args=args,
prompt=planning_prompt,
stage_name="PLANNING",
stream_output=False,
)
if plan_code != 0:
return plan_code
execution_prompt = _build_execution_prompt(
task=task_text,
cwd=args.cwd,
extra_context=args.extra_context,
plan_text=plan_text or "(No planner output provided)",
)
print("\n⚙️ Stage 2/2: Execute plan autonomously")
exec_code, _ = await _run_single_session(
client=client,
args=args,
prompt=execution_prompt,
stage_name="EXECUTION",
stream_output=args.stream,
)
return exec_code
print("\n⚙️ Direct mode: Execute task without planning stage")
exec_code, _ = await _run_single_session(
client=client,
args=args,
prompt=direct_prompt,
stage_name="EXECUTION",
stream_output=args.stream,
)
return exec_code
finally:
try:
await client.stop()
except Exception:
pass
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Run one autonomous programming task with Copilot SDK"
)
parser.add_argument(
"--task",
default="",
help="Task description text (if empty, uses built-in default task)",
)
parser.add_argument("--task-file", default="", help="Path to a task text file")
parser.add_argument("--cwd", default=os.getcwd(), help="Workspace directory")
parser.add_argument(
"--model",
default="gpt-5-mini",
help="Model id for Copilot session (default: gpt-5-mini)",
)
parser.add_argument(
"--reasoning-effort",
default="medium",
choices=["low", "medium", "high", "xhigh"],
help="Reasoning effort",
)
parser.add_argument("--timeout", type=int, default=1800, help="Timeout seconds")
parser.add_argument(
"--log-level",
default="info",
choices=["trace", "debug", "info", "warn", "error"],
help="Copilot client log level",
)
parser.add_argument(
"--github-token",
default=os.environ.get("GH_TOKEN", ""),
help="Optional GitHub token; defaults to GH_TOKEN",
)
parser.add_argument(
"--cli-path",
default=os.environ.get("COPILOT_CLI_PATH", ""),
help="Optional Copilot CLI path",
)
parser.add_argument(
"--extra-context",
default="",
help="Optional extra context appended to the task prompt",
)
parser.add_argument(
"--stream",
action="store_true",
help="Print assistant delta stream in real-time",
)
parser.add_argument(
"--trace-events",
action="store_true",
help="Print each SDK event type for debugging",
)
parser.add_argument(
"--auto-user-answer",
default="continue",
help="Default answer for on_user_input_request",
)
parser.add_argument(
"--heartbeat-seconds",
type=int,
default=12,
help="Heartbeat interval while waiting for events",
)
parser.add_argument(
"--plan-first",
action="store_true",
help="Run planning stage before execution (default behavior)",
)
parser.add_argument(
"--no-plan-first",
action="store_true",
help="Disable planning stage and run direct execution",
)
return parser
def main() -> int:
parser = build_parser()
args = parser.parse_args()
if args.task_file and (args.task or "").strip():
parser.error("Use either --task or --task-file, not both")
args.plan_first = True
if args.no_plan_first:
args.plan_first = False
elif args.plan_first:
args.plan_first = True
return asyncio.run(run_task(args))
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,20 +1,21 @@
#!/usr/bin/env python3
"""
OpenWebUI 社区统计工具
OpenWebUI community stats utility.
获取并统计你在 openwebui.com 上发布的插件/帖子数据。
Collect and summarize your published posts/plugins on openwebui.com.
使用方法:
1. 设置环境变量:
- OPENWEBUI_API_KEY: 你的 API Key
- OPENWEBUI_USER_ID: 你的用户 ID
2. 运行: python scripts/openwebui_stats.py
Usage:
1. Set environment variables:
- OPENWEBUI_API_KEY: required
- OPENWEBUI_USER_ID: optional (auto-resolved from /api/v1/auths/ when missing)
2. Run: python scripts/openwebui_stats.py
获取 API Key
访问 https://openwebui.com/settings/api 创建 API Key (sk-开头)
How to get API key:
Visit https://openwebui.com/settings/api and create a key (starts with sk-).
获取 User ID
从个人主页的 API 请求中获取,格式如: b15d1348-4347-42b4-b815-e053342d6cb0
How to get user ID (optional):
Read the `id` field from /api/v1/auths/, format example:
b15d1348-4347-42b4-b815-e053342d6cb0
"""
import os
@@ -28,16 +29,16 @@ from datetime import datetime, timezone, timedelta
from typing import Optional
from pathlib import Path
# 北京时区 (UTC+8)
# Beijing timezone (UTC+8)
BEIJING_TZ = timezone(timedelta(hours=8))
def get_beijing_time() -> datetime:
"""获取当前北京时间"""
"""Get current time in Beijing timezone."""
return datetime.now(BEIJING_TZ)
# 尝试加载 .env 文件
# Try loading local .env file (if python-dotenv is installed)
try:
from dotenv import load_dotenv
@@ -47,7 +48,7 @@ except ImportError:
class OpenWebUIStats:
"""OpenWebUI 社区统计工具"""
"""OpenWebUI community stats utility."""
BASE_URL = "https://api.openwebui.com/api/v1"
@@ -59,18 +60,18 @@ class OpenWebUIStats:
gist_id: Optional[str] = None,
):
"""
初始化统计工具
Initialize the stats utility
Args:
api_key: OpenWebUI API Key (JWT Token)
user_id: 用户 ID如果为 None 则从 token 中解析
gist_token: GitHub Personal Access Token (用于读写 Gist)
user_id: User ID; if None, will be parsed from token
gist_token: GitHub Personal Access Token (for reading/writing Gist)
gist_id: GitHub Gist ID
"""
self.api_key = api_key
self.user_id = user_id or self._parse_user_id_from_token(api_key)
self.gist_token = gist_token
self.gist_id = gist_id
self.gist_id = gist_id or "db3d95687075a880af6f1fba76d679c6"
self.history_filename = "community-stats-history.json"
self.session = requests.Session()
@@ -83,23 +84,32 @@ class OpenWebUIStats:
)
self.history_file = Path("docs/stats-history.json")
# 定义下载类别的判定(这些类别会计入总浏览量/下载量统计)
# Types considered downloadable (included in total view/download stats)
DOWNLOADABLE_TYPES = [
"action",
"filter",
"pipe",
"toolkit",
"tool",
"function",
"prompt",
"model",
]
TYPE_ALIASES = {
"tools": "tool",
}
def _normalize_post_type(self, post_type: str) -> str:
"""Normalize post type to avoid synonym type splitting in statistics."""
normalized = str(post_type or "").strip().lower()
return self.TYPE_ALIASES.get(normalized, normalized)
def load_history(self) -> list:
"""加载历史记录 (合并 Gist + 本地文件, 取记录更多的)"""
"""Load history records (merge Gist + local file, keep the one with more records)"""
gist_history = []
local_history = []
# 1. 尝试从 Gist 加载
# 1. Try loading from Gist
if self.gist_token and self.gist_id:
try:
url = f"https://api.github.com/gists/{self.gist_id}"
@@ -111,30 +121,36 @@ class OpenWebUIStats:
if file_info:
content = file_info.get("content")
gist_history = json.loads(content)
print(f"✅ 已从 Gist 加载历史记录 ({len(gist_history)} 条)")
print(
f"✅ Loaded history from Gist ({len(gist_history)} records)"
)
except Exception as e:
print(f"⚠️ 无法从 Gist 加载历史: {e}")
print(f"⚠️ Failed to load history from Gist: {e}")
# 2. 同时从本地文件加载
# 2. Also load from local file
if self.history_file.exists():
try:
with open(self.history_file, "r", encoding="utf-8") as f:
local_history = json.load(f)
print(f"✅ 已从本地加载历史记录 ({len(local_history)} 条)")
print(
f"✅ Loaded history from local file ({len(local_history)} records)"
)
except Exception as e:
print(f"⚠️ 无法加载本地历史记录: {e}")
print(f"⚠️ Failed to load local history: {e}")
# 3. 合并两个来源 (以日期为 key, 有冲突时保留更新的)
# 3. Merge two sources (by date as key, keep newer when conflicts)
hist_dict = {}
for item in gist_history:
hist_dict[item["date"]] = item
for item in local_history:
hist_dict[item["date"]] = item # 本地数据覆盖 Gist (更可能是最新的)
hist_dict[item["date"]] = (
item # Local data overrides Gist (more likely to be latest)
)
history = sorted(hist_dict.values(), key=lambda x: x["date"])
print(f"📊 合并后历史记录: {len(history)}")
print(f"📊 Merged history records: {len(history)}")
# 4. 如果合并后仍然太少, 尝试从 Git 历史重建
# 4. If merged data is still too short, try rebuilding from Git
if len(history) < 5 and os.path.isdir(".git"):
print("📉 History too short, attempting Git rebuild...")
git_history = self.rebuild_history_from_git()
@@ -146,7 +162,7 @@ class OpenWebUIStats:
hist_dict[item["date"]] = item
history = sorted(hist_dict.values(), key=lambda x: x["date"])
# 5. 如果有新数据, 同步回 Gist
# 5. If there is new data, sync back to Gist
if len(history) > len(gist_history) and self.gist_token and self.gist_id:
try:
url = f"https://api.github.com/gists/{self.gist_id}"
@@ -160,7 +176,7 @@ class OpenWebUIStats:
}
resp = requests.patch(url, headers=headers, json=payload)
if resp.status_code == 200:
print(f"历史记录已同步至 Gist ({len(history)} )")
print(f"History synced to Gist ({len(history)} records)")
else:
print(f"⚠️ Gist sync failed: {resp.status_code}")
except Exception as e:
@@ -169,11 +185,11 @@ class OpenWebUIStats:
return history
def save_history(self, stats: dict):
"""保存当前快照到历史记录 (优先保存到 Gist, 其次本地)"""
"""Save current snapshot to history (prioritize Gist, fallback to local)"""
history = self.load_history()
today = get_beijing_time().strftime("%Y-%m-%d")
# 构造详细快照 (包含每个插件的下载量)
# Build detailed snapshot (including each plugin's download count)
snapshot = {
"date": today,
"total_posts": stats["total_posts"],
@@ -187,7 +203,7 @@ class OpenWebUIStats:
"posts": {p["slug"]: p["downloads"] for p in stats.get("posts", [])},
}
# 更新或追加数据点
# Update or append data point
updated = False
for i, item in enumerate(history):
if item.get("date") == today:
@@ -197,10 +213,10 @@ class OpenWebUIStats:
if not updated:
history.append(snapshot)
# 限制长度 (90天)
# Limit length (90 days)
history = history[-90:]
# 尝试保存到 Gist
# Try saving to Gist
if self.gist_token and self.gist_id:
try:
url = f"https://api.github.com/gists/{self.gist_id}"
@@ -214,19 +230,19 @@ class OpenWebUIStats:
}
resp = requests.patch(url, headers=headers, json=payload)
if resp.status_code == 200:
print(f"历史记录已同步至 Gist ({self.gist_id})")
# 如果同步成功,不再保存到本地,减少 commit 压力
print(f"History synced to Gist ({self.gist_id})")
# If sync succeeds, do not save to local to reduce commit pressure
return
except Exception as e:
print(f"⚠️ 同步至 Gist 失败: {e}")
print(f"⚠️ Failed to sync to Gist: {e}")
# 降级:保存到本地
# Fallback: save to local
with open(self.history_file, "w", encoding="utf-8") as f:
json.dump(history, f, ensure_ascii=False, indent=2)
print(f"历史记录已更新至本地 ({today})")
print(f"History updated to local ({today})")
def get_stat_delta(self, stats: dict) -> dict:
"""计算相对于上次记录的增长 (24h)"""
"""Calculate growth relative to last recorded snapshot (24h delta)"""
history = self.load_history()
if not history:
return {}
@@ -234,7 +250,7 @@ class OpenWebUIStats:
today = get_beijing_time().strftime("%Y-%m-%d")
prev = None
# 查找非今天的最后一笔数据作为基准
# Find last data point from a different day as baseline
for item in reversed(history):
if item.get("date") != today:
prev = item
@@ -262,14 +278,14 @@ class OpenWebUIStats:
}
def _resolve_post_type(self, post: dict) -> str:
"""解析帖子类别"""
"""Resolve the post category type"""
top_type = post.get("type")
function_data = post.get("data", {}) or {}
function_obj = function_data.get("function", {}) or {}
meta = function_obj.get("meta", {}) or {}
manifest = meta.get("manifest", {}) or {}
# 类别识别优先级:
# Category identification priority:
if top_type == "review":
return "review"
@@ -283,7 +299,9 @@ class OpenWebUIStats:
elif not meta and not function_obj:
post_type = "post"
# 统一和启发式识别逻辑
post_type = self._normalize_post_type(post_type)
# Unified and heuristic identification logic
if post_type == "unknown" and function_obj:
post_type = "action"
@@ -298,17 +316,17 @@ class OpenWebUIStats:
post_type = "filter"
elif "pipe" in all_metadata:
post_type = "pipe"
elif "toolkit" in all_metadata:
post_type = "toolkit"
elif "tool" in all_metadata:
post_type = "tool"
return post_type
return self._normalize_post_type(post_type)
def rebuild_history_from_git(self) -> list:
"""从 Git 历史提交中重建统计数据"""
"""Rebuild statistics from Git commit history"""
history = []
try:
# docs/community-stats.json 的 Git 历史重建 (该文件历史最丰富)
# 格式: hash date
# Rebuild from Git history of docs/community-stats.json (has richest history)
# Format: hash date
target = "docs/community-stats.json"
cmd = [
"git",
@@ -324,8 +342,8 @@ class OpenWebUIStats:
seen_dates = set()
# 从旧到新处理git log 默认是从新到旧,所以我们要反转或者用 reverse
# 其实顺序无所谓,只要最后 sort 一下就行
# Process from oldest to newest (git log defaults to newest first, so reverse)
# The order doesn't really matter as long as we sort at the end
for line in reversed(commits): # Process from oldest to newest
parts = line.split()
if len(parts) < 2:
@@ -338,7 +356,7 @@ class OpenWebUIStats:
continue
seen_dates.add(commit_date)
# 读取该 commit 时的文件内容
# Read file content at this commit
# Note: The file name in git show needs to be relative to the repo root
show_cmd = ["git", "show", f"{commit_hash}:{target}"]
show_res = subprocess.run(
@@ -405,13 +423,16 @@ class OpenWebUIStats:
return []
def _parse_user_id_from_token(self, token: str) -> str:
""" JWT Token 中解析用户 ID"""
"""Parse user ID from JWT Token"""
import base64
if not token or token.startswith("sk-"):
return ""
try:
# JWT 格式: header.payload.signature
# JWT format: header.payload.signature
payload = token.split(".")[1]
# 添加 padding
# Add padding
padding = 4 - len(payload) % 4
if padding != 4:
payload += "=" * padding
@@ -419,16 +440,36 @@ class OpenWebUIStats:
data = json.loads(decoded)
return data.get("id", "")
except Exception as e:
print(f"⚠️ 无法从 Token 解析用户 ID: {e}")
print(f"⚠️ Failed to parse user ID from token: {e}")
return ""
def resolve_user_id(self) -> str:
"""Auto-resolve current user ID via community API (for sk- type API keys)"""
if self.user_id:
return self.user_id
try:
resp = self.session.get(f"{self.BASE_URL}/auths/", timeout=20)
if resp.status_code == 200:
data = resp.json() if resp.text else {}
resolved = str(data.get("id", "")).strip()
if resolved:
self.user_id = resolved
return resolved
else:
print(f"⚠️ Failed to auto-resolve user ID: HTTP {resp.status_code}")
except Exception as e:
print(f"⚠️ Exception while auto-resolving user ID: {e}")
return ""
def generate_mermaid_chart(self, stats: dict = None, lang: str = "zh") -> str:
"""生成支持 Kroki 服务端渲染的动态 Mermaid 图表链接 (零 Commit)"""
"""Generate dynamic Mermaid chart links with Kroki server-side rendering (zero commit)"""
history = self.load_history()
if not history:
return ""
# 多语言标签
# Multi-language labels
labels = {
"zh": {
"trend_title": "增长与趋势 (Last 14 Days)",
@@ -495,14 +536,14 @@ class OpenWebUIStats:
def get_user_posts(self, sort: str = "new", page: int = 1) -> list:
"""
获取用户发布的帖子列表
Fetch list of posts published by the user
Args:
sort: 排序方式 (new/top/hot)
page: 页码
sort: Sort order (new/top/hot)
page: Page number
Returns:
帖子列表
List of posts
"""
url = f"{self.BASE_URL}/posts/users/{self.user_id}"
params = {"sort": sort, "page": page}
@@ -512,7 +553,7 @@ class OpenWebUIStats:
return response.json()
def get_all_posts(self, sort: str = "new") -> list:
"""获取所有帖子(自动分页)"""
"""Fetch all posts (automatic pagination)"""
all_posts = []
page = 1
@@ -526,7 +567,7 @@ class OpenWebUIStats:
return all_posts
def generate_stats(self, posts: list) -> dict:
"""生成统计数据"""
"""Generate statistics"""
stats = {
"total_posts": len(posts),
"total_downloads": 0,
@@ -537,10 +578,10 @@ class OpenWebUIStats:
"total_comments": 0,
"by_type": {},
"posts": [],
"user": {}, # 用户信息
"user": {}, # User info
}
# 从第一个帖子中提取用户信息
# Extract user info from first post
if posts and "user" in posts[0]:
user = posts[0]["user"]
stats["user"] = {
@@ -564,7 +605,7 @@ class OpenWebUIStats:
meta = function_obj.get("meta", {}) or {}
manifest = meta.get("manifest", {}) or {}
# 累计统计
# Accumulate statistics
post_downloads = post.get("downloads", 0)
post_views = post.get("views", 0)
@@ -574,7 +615,7 @@ class OpenWebUIStats:
stats["total_saves"] += post.get("saveCount", 0)
stats["total_comments"] += post.get("commentCount", 0)
# 关键:总浏览量不包括不可以下载的类型 (如 post, review)
# Key: total views do not include non-downloadable types (e.g., post, review)
if post_type in self.DOWNLOADABLE_TYPES or post_downloads > 0:
stats["total_views"] += post_views
@@ -582,7 +623,7 @@ class OpenWebUIStats:
stats["by_type"][post_type] = 0
stats["by_type"][post_type] += 1
# 单个帖子信息
# Individual post information
created_at = datetime.fromtimestamp(post.get("createdAt", 0))
updated_at = datetime.fromtimestamp(post.get("updatedAt", 0))
@@ -605,43 +646,45 @@ class OpenWebUIStats:
}
)
# 按下载量排序
# Sort by download count
stats["posts"].sort(key=lambda x: x["downloads"], reverse=True)
return stats
def print_stats(self, stats: dict):
"""打印统计报告到终端"""
"""Print statistics report to terminal"""
print("\n" + "=" * 60)
print("📊 OpenWebUI 社区统计报告")
print("📊 OpenWebUI Community Statistics Report")
print("=" * 60)
print(f"📅 生成时间 (北京): {get_beijing_time().strftime('%Y-%m-%d %H:%M')}")
print(
f"📅 Generated (Beijing time): {get_beijing_time().strftime('%Y-%m-%d %H:%M')}"
)
print()
# 总览
print("📈 总览")
# Overview
print("📈 Overview")
print("-" * 40)
print(f" 📝 发布数量: {stats['total_posts']}")
print(f" ⬇️ 总下载量: {stats['total_downloads']}")
print(f" 👁️ 总浏览量: {stats['total_views']}")
print(f" 👍 总点赞数: {stats['total_upvotes']}")
print(f" 💾 总收藏数: {stats['total_saves']}")
print(f" 💬 总评论数: {stats['total_comments']}")
print(f" 📝 Posts: {stats['total_posts']}")
print(f" ⬇️ Total Downloads: {stats['total_downloads']}")
print(f" 👁️ Total Views: {stats['total_views']}")
print(f" 👍 Total Upvotes: {stats['total_upvotes']}")
print(f" 💾 Total Saves: {stats['total_saves']}")
print(f" 💬 Total Comments: {stats['total_comments']}")
print()
# 按类型分类
print("📂 按类型分类")
# By type
print("📂 By Type")
print("-" * 40)
for post_type, count in stats["by_type"].items():
print(f"{post_type}: {count}")
print()
# 详细列表
print("📋 发布列表 (按下载量排序)")
# Detailed list
print("📋 Posts List (sorted by downloads)")
print("-" * 60)
# 表头
print(f"{'排名':<4} {'标题':<30} {'下载':<8} {'浏览':<8} {'点赞':<6}")
# Header
print(f"{'Rank':<4} {'Title':<30} {'Downloads':<8} {'Views':<8} {'Upvotes':<6}")
print("-" * 60)
for i, post in enumerate(stats["posts"], 1):
@@ -655,23 +698,23 @@ class OpenWebUIStats:
print("=" * 60)
def _safe_key(self, key: str) -> str:
"""生成安全的文件名 Key (MD5 hash) 以避免中文字符问题"""
"""Generate safe filename key (MD5 hash) to avoid Chinese character issues"""
import hashlib
return hashlib.md5(key.encode("utf-8")).hexdigest()
def generate_markdown(self, stats: dict, lang: str = "zh") -> str:
"""
生成 Markdown 格式报告 (全动态徽章与 Kroki 图表)
Generate Markdown format report (fully dynamic badges and Kroki charts)
Args:
stats: 统计数据
lang: 语言 ("zh" 中文, "en" 英文)
stats: Statistics data
lang: Language ("zh" Chinese, "en" English)
"""
# 获取增量数据
# Get delta data
delta = self.get_stat_delta(stats)
# 中英文文本
# Bilingual text
texts = {
"zh": {
"title": "# 📊 OpenWebUI 社区统计报告",
@@ -761,6 +804,7 @@ class OpenWebUIStats:
"filter": "brightgreen",
"action": "orange",
"pipe": "blueviolet",
"tool": "teal",
"pipeline": "purple",
"review": "yellow",
"prompt": "lightgrey",
@@ -815,30 +859,30 @@ class OpenWebUIStats:
return "\n".join(md)
def save_json(self, stats: dict, filepath: str):
"""保存 JSON 格式数据"""
"""Save data in JSON format"""
with open(filepath, "w", encoding="utf-8") as f:
json.dump(stats, f, ensure_ascii=False, indent=2)
print(f"✅ JSON 数据已保存到: {filepath}")
print(f"✅ JSON data saved to: {filepath}")
def generate_shields_endpoints(self, stats: dict, output_dir: str = "docs/badges"):
"""
生成 Shields.io endpoint JSON 文件
Generate Shields.io endpoint JSON files
Args:
stats: 统计数据
output_dir: 输出目录
stats: Statistics data
output_dir: Output directory
"""
Path(output_dir).mkdir(parents=True, exist_ok=True)
def format_number(n: int) -> str:
"""格式化数字为易读格式"""
"""Format number to readable format"""
if n >= 1000000:
return f"{n/1000000:.1f}M"
elif n >= 1000:
return f"{n/1000:.1f}k"
return str(n)
# 各种徽章数据
# Badge data
badges = {
"downloads": {
"schemaVersion": 1,
@@ -884,18 +928,18 @@ class OpenWebUIStats:
# 构造并上传 Shields.io 徽章数据
self.upload_gist_badges(stats)
except Exception as e:
print(f"⚠️ 徽章生成失败: {e}")
print(f"⚠️ Badge generation failed: {e}")
print(f"✅ Shields.io endpoints saved to: {output_dir}/")
def upload_gist_badges(self, stats: dict):
"""生成并上传 Gist 徽章数据 (用于 Shields.io Endpoint)"""
"""Generate and upload Gist badge data (for Shields.io Endpoint)"""
if not (self.gist_token and self.gist_id):
return
delta = self.get_stat_delta(stats)
# 定义徽章配置 {key: (label, value, color)}
# Define badge config {key: (label, value, color)}
badges_config = {
"downloads": ("Downloads", stats["total_downloads"], "brightgreen"),
"views": ("Views", stats["total_views"], "blue"),
@@ -923,7 +967,7 @@ class OpenWebUIStats:
for key, (label, val, color) in badges_config.items():
diff = delta.get(key, 0)
if isinstance(diff, dict):
diff = 0 # 避免 'posts' key 导致的 dict vs int 比较错误
diff = 0 # Avoid dict vs int comparison error with 'posts' key
message = f"{val}"
if diff > 0:
@@ -931,7 +975,7 @@ class OpenWebUIStats:
elif diff < 0:
message += f" ({diff})"
# 构造 Shields.io endpoint JSON
# Build Shields.io endpoint JSON
# 参考: https://shields.io/badges/endpoint-badge
badge_data = {
"schemaVersion": 1,
@@ -945,13 +989,13 @@ class OpenWebUIStats:
"content": json.dumps(badge_data, ensure_ascii=False)
}
# 生成 Top 6 插件徽章 (基于槽位 p1, p2...)
# Generate top 6 plugins badges (based on slots p1, p2...)
post_deltas = delta.get("posts", {})
for i, post in enumerate(stats.get("posts", [])[:6]):
idx = i + 1
diff = post_deltas.get(post["slug"], 0)
# 下载量徽章
# Downloads badge
dl_msg = f"{post['downloads']}"
if diff > 0:
dl_msg += f" (+{diff}🚀)"
@@ -1103,6 +1147,8 @@ class OpenWebUIStats:
def _fmt_delta(k: str) -> str:
val = delta.get(k, 0)
if isinstance(val, dict):
return ""
if val > 0:
return f" <br><sub>(+{val}🚀)</sub>"
return ""
@@ -1462,86 +1508,93 @@ class OpenWebUIStats:
def main():
"""主函数"""
# 获取配置
"""CLI entry point."""
# Load runtime config
api_key = os.getenv("OPENWEBUI_API_KEY")
user_id = os.getenv("OPENWEBUI_USER_ID")
if not api_key:
print("错误: 未设置 OPENWEBUI_API_KEY 环境变量")
print("请设置环境变量:")
print("Error: OPENWEBUI_API_KEY is not set")
print("Please set environment variable:")
print(" export OPENWEBUI_API_KEY='your_api_key_here'")
return 1
if not user_id:
print("❌ 错误: 未设置 OPENWEBUI_USER_ID 环境变量")
print("请设置环境变量:")
print(" export OPENWEBUI_USER_ID='your_user_id_here'")
print("\n提示: 用户 ID 可以从之前的 curl 请求中获取")
print(" 例如: b15d1348-4347-42b4-b815-e053342d6cb0")
return 1
print(" OPENWEBUI_USER_ID not set, attempting auto-resolve via API key...")
# 获取 Gist 配置 (用于存储历史记录)
# Gist config (optional, for badges/history sync)
gist_token = os.getenv("GIST_TOKEN")
gist_id = os.getenv("GIST_ID")
# 初始化
# Initialize client
stats_client = OpenWebUIStats(api_key, user_id, gist_token, gist_id)
print(f"🔍 用户 ID: {stats_client.user_id}")
if not stats_client.user_id:
stats_client.resolve_user_id()
if not stats_client.user_id:
print("❌ Error: failed to auto-resolve OPENWEBUI_USER_ID")
print("Please set environment variable:")
print(" export OPENWEBUI_USER_ID='your_user_id_here'")
print("\nTip: user id is the 'id' field returned by /api/v1/auths/")
print(" e.g. b15d1348-4347-42b4-b815-e053342d6cb0")
return 1
print(f"🔍 User ID: {stats_client.user_id}")
if gist_id:
print(f"📦 Gist 存储已启用: {gist_id}")
print(f"📦 Gist storage enabled: {gist_id}")
# 获取所有帖子
print("📥 正在获取帖子数据...")
# Fetch posts
print("📥 Fetching posts...")
posts = stats_client.get_all_posts()
print(f"获取到 {len(posts)} 个帖子")
print(f"Retrieved {len(posts)} posts")
# 生成统计
# Build stats
stats = stats_client.generate_stats(posts)
# 保存历史快照
# Save history snapshot
stats_client.save_history(stats)
# 打印到终端
# Print terminal report
stats_client.print_stats(stats)
# 保存 Markdown 报告 (中英文双版本)
# Save markdown reports (zh/en)
script_dir = Path(__file__).parent.parent
# 中文报告
# Chinese report
md_zh_path = script_dir / "docs" / "community-stats.zh.md"
md_zh_content = stats_client.generate_markdown(stats, lang="zh")
with open(md_zh_path, "w", encoding="utf-8") as f:
f.write(md_zh_content)
print(f"\n中文报告已保存到: {md_zh_path}")
print(f"\nChinese report saved to: {md_zh_path}")
# 英文报告
# English report
md_en_path = script_dir / "docs" / "community-stats.md"
md_en_content = stats_client.generate_markdown(stats, lang="en")
with open(md_en_path, "w", encoding="utf-8") as f:
f.write(md_en_content)
print(f"英文报告已保存到: {md_en_path}")
print(f"English report saved to: {md_en_path}")
# 保存 JSON 数据
# Save JSON snapshot
json_path = script_dir / "docs" / "community-stats.json"
stats_client.save_json(stats, str(json_path))
# 生成 Shields.io endpoint JSON (用于动态徽章)
# Generate Shields.io endpoint JSON (dynamic badges)
badges_dir = script_dir / "docs" / "badges"
# 生成徽章
# Generate badges
stats_client.generate_shields_endpoints(stats, str(badges_dir))
# 生成并上传 SVG 图表 (每日更新 Gist, README URL 保持不变)
# Generate and upload SVG chart (if Gist is configured)
stats_client.upload_chart_svg()
# 更新 README 文件
# Update README files
readme_path = script_dir / "README.md"
readme_cn_path = script_dir / "README_CN.md"
stats_client.update_readme(stats, str(readme_path), lang="en")
stats_client.update_readme(stats, str(readme_cn_path), lang="zh")
# 更新 docs 中的图表
# Update charts in docs pages
stats_client.update_docs_chart(str(md_en_path), lang="en")
stats_client.update_docs_chart(str(md_zh_path), lang="zh")