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:
10
README.md
10
README.md
@@ -9,7 +9,6 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
|
||||
|
||||
<!-- STATS_START -->
|
||||
## 📊 Community Stats
|
||||
>
|
||||
> 
|
||||
|
||||
| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |
|
||||
@@ -20,19 +19,18 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
|
||||
| :---: | :---: | :---: | :---: | :---: |
|
||||
|  |  |  |  |  |
|
||||
|
||||
### 🔥 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) |  |  |  |  |
|
||||
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) |  |  |  |  |
|
||||
| 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) |  |  |  |  |
|
||||
| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) |  |  |  |  |
|
||||
| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) |  |  |  |  |
|
||||
| 4️⃣ | [Export to Word Enhanced](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) |  |  |  |  |
|
||||
| 5️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) |  |  |  |  |
|
||||
| 5️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) |  |  |  |  |
|
||||
| 6️⃣ | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) |  |  |  |  |
|
||||
|
||||
### 📈 Total Downloads Trend
|
||||
|
||||

|
||||
|
||||
*See full stats and charts in [Community Stats Report](./docs/community-stats.md)*
|
||||
|
||||
10
README_CN.md
10
README_CN.md
@@ -6,7 +6,6 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
||||
|
||||
<!-- STATS_START -->
|
||||
## 📊 社区统计
|
||||
>
|
||||
> 
|
||||
|
||||
| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |
|
||||
@@ -17,19 +16,18 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
||||
| :---: | :---: | :---: | :---: | :---: |
|
||||
|  |  |  |  |  |
|
||||
|
||||
### 🔥 热门插件 Top 6
|
||||
|
||||
### 🔥 热门插件 Top 6
|
||||
| 排名 | 插件 | 版本 | 下载 | 浏览 | 📅 更新 |
|
||||
| :---: | :--- | :---: | :---: | :---: | :---: |
|
||||
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) |  |  |  |  |
|
||||
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) |  |  |  |  |
|
||||
| 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) |  |  |  |  |
|
||||
| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) |  |  |  |  |
|
||||
| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) |  |  |  |  |
|
||||
| 4️⃣ | [Export to Word Enhanced](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) |  |  |  |  |
|
||||
| 5️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) |  |  |  |  |
|
||||
| 5️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) |  |  |  |  |
|
||||
| 6️⃣ | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) |  |  |  |  |
|
||||
|
||||
### 📈 总下载量累计趋势
|
||||
|
||||

|
||||
|
||||
*完整统计与趋势图请查看 [社区统计报告](./docs/community-stats.zh.md)*
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
447
plugins/debug/copilot-sdk/auto_programming_task.py
Normal file
447
plugins/debug/copilot-sdk/auto_programming_task.py
Normal 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())
|
||||
@@ -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"\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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user