From 369e8c900ccc2255ec79cce7f586f4d515f0b529 Mon Sep 17 00:00:00 2001 From: fujie Date: Tue, 3 Mar 2026 19:29:34 +0800 Subject: [PATCH] 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. --- README.md | 10 +- README_CN.md | 10 +- docs/community-stats.json | 245 +++++----- .../copilot-sdk/auto_programming_task.py | 447 ++++++++++++++++++ scripts/openwebui_stats.py | 341 +++++++------ 5 files changed, 775 insertions(+), 278 deletions(-) create mode 100644 plugins/debug/copilot-sdk/auto_programming_task.py diff --git a/README.md b/README.md index 5bdf284..d4f2fed 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu ## 📊 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)* diff --git a/README_CN.md b/README_CN.md index 4395c81..ff5348b 100644 --- a/README_CN.md +++ b/README_CN.md @@ -6,7 +6,6 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词 ## 📊 社区统计 -> > ![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)* diff --git a/docs/community-stats.json b/docs/community-stats.json index d3bcff7..182e39e 100644 --- a/docs/community-stats.json +++ b/docs/community-stats.json @@ -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 } diff --git a/plugins/debug/copilot-sdk/auto_programming_task.py b/plugins/debug/copilot-sdk/auto_programming_task.py new file mode 100644 index 0000000..329e9e5 --- /dev/null +++ b/plugins/debug/copilot-sdk/auto_programming_task.py @@ -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()) diff --git a/scripts/openwebui_stats.py b/scripts/openwebui_stats.py index deceb51..0e00016 100644 --- a/scripts/openwebui_stats.py +++ b/scripts/openwebui_stats.py @@ -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"
(+{val}🚀)" 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"\n✅ Chinese 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")