时间显示改为北京时间并精确到分钟

- 所有时间戳使用北京时区 (UTC+8)
- 格式从 YYYY-MM-DD 改为 YYYY-MM-DD HH:MM
- 添加 '(北京时间)' 标注
This commit is contained in:
fujie
2026-01-06 19:26:43 +08:00
parent 4d44b72dab
commit 8471680efe
6 changed files with 783 additions and 32 deletions

View File

@@ -7,7 +7,7 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
<!-- STATS_START --> <!-- STATS_START -->
## 📊 Community Stats ## 📊 Community Stats
> 🕐 Auto-updated on 2026-01-06 > 🕐 Auto-updated: 2026-01-06 19:26 (Beijing Time)
| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions | | 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |
|:---:|:---:|:---:|:---:| |:---:|:---:|:---:|:---:|
@@ -16,6 +16,7 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves | | 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |
|:---:|:---:|:---:|:---:|:---:| |:---:|:---:|:---:|:---:|:---:|
| **11** | **785** | **8394** | **54** | **46** | | **11** | **785** | **8394** | **54** | **46** |
| **11** | **785** | **8411** | **54** | **47** |
### 🔥 Top 5 Popular Plugins ### 🔥 Top 5 Popular Plugins
@@ -26,6 +27,11 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
| 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 112 | 1234 | | 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 112 | 1234 |
| 4⃣ | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | 75 | 1413 | | 4⃣ | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | 75 | 1413 |
| 5⃣ | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 65 | 900 | | 5⃣ | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 65 | 900 |
| 🥇 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 235 | 2103 |
| 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 170 | 456 |
| 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 112 | 1235 |
| 4⃣ | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | 75 | 1414 |
| 5⃣ | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 65 | 904 |
*See full stats in [Community Stats Report](./docs/community-stats.md)* *See full stats in [Community Stats Report](./docs/community-stats.md)*
<!-- STATS_END --> <!-- STATS_END -->

View File

@@ -7,7 +7,7 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
<!-- STATS_START --> <!-- STATS_START -->
## 📊 社区统计 ## 📊 社区统计
> 🕐 自动更新于 2026-01-06 > 🕐 自动更新于 2026-01-06 19:26 (北京时间)
| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 | | 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |
|:---:|:---:|:---:|:---:| |:---:|:---:|:---:|:---:|

View File

@@ -85,7 +85,7 @@
"downloads": 65, "downloads": 65,
"views": 900, "views": 900,
"upvotes": 6, "upvotes": 6,
"saves": 7, "saves": 8,
"comments": 2, "comments": 2,
"created_at": "2025-12-28", "created_at": "2025-12-28",
"updated_at": "2026-01-03", "updated_at": "2026-01-03",

View File

@@ -3,7 +3,7 @@ title: Smart Mind Map
author: Fu-Jie author: Fu-Jie
author_url: https://github.com/Fu-Jie author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.8.2 version: 0.9.0
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4= icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
description: Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge. description: Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.
""" """
@@ -13,7 +13,7 @@ import os
import re import re
import time import time
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Dict, Optional from typing import Any, Callable, Awaitable, Dict, Optional
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from fastapi import Request from fastapi import Request
@@ -786,6 +786,18 @@ class Action:
default=1, default=1,
description="Number of recent messages to use for generation. Set to 1 for just the last message, or higher for more context.", description="Number of recent messages to use for generation. Set to 1 for just the last message, or higher for more context.",
) )
OUTPUT_MODE: str = Field(
default="html",
description="Output mode: 'html' for interactive HTML (default), or 'image' to embed as Markdown image.",
)
SVG_WIDTH: int = Field(
default=1200,
description="Width of the SVG canvas in pixels (for image mode).",
)
SVG_HEIGHT: int = Field(
default=800,
description="Height of the SVG canvas in pixels (for image mode).",
)
def __init__(self): def __init__(self):
self.valves = self.Valves() self.valves = self.Valves()
@@ -814,6 +826,46 @@ class Action:
"user_language": user_data.get("language", "en-US"), "user_language": user_data.get("language", "en-US"),
} }
def _extract_chat_id(self, body: dict, metadata: Optional[dict]) -> str:
"""Extract chat_id from body or metadata"""
if isinstance(body, dict):
chat_id = body.get("chat_id")
if isinstance(chat_id, str) and chat_id.strip():
return chat_id.strip()
body_metadata = body.get("metadata", {})
if isinstance(body_metadata, dict):
chat_id = body_metadata.get("chat_id")
if isinstance(chat_id, str) and chat_id.strip():
return chat_id.strip()
if isinstance(metadata, dict):
chat_id = metadata.get("chat_id")
if isinstance(chat_id, str) and chat_id.strip():
return chat_id.strip()
return ""
def _extract_message_id(self, body: dict, metadata: Optional[dict]) -> str:
"""Extract message_id from body or metadata"""
if isinstance(body, dict):
message_id = body.get("id")
if isinstance(message_id, str) and message_id.strip():
return message_id.strip()
body_metadata = body.get("metadata", {})
if isinstance(body_metadata, dict):
message_id = body_metadata.get("message_id")
if isinstance(message_id, str) and message_id.strip():
return message_id.strip()
if isinstance(metadata, dict):
message_id = metadata.get("message_id")
if isinstance(message_id, str) and message_id.strip():
return message_id.strip()
return ""
def _extract_markdown_syntax(self, llm_output: str) -> str: def _extract_markdown_syntax(self, llm_output: str) -> str:
match = re.search(r"```markdown\s*(.*?)\s*```", llm_output, re.DOTALL) match = re.search(r"```markdown\s*(.*?)\s*```", llm_output, re.DOTALL)
if match: if match:
@@ -901,11 +953,286 @@ class Action:
return base_html.strip() return base_html.strip()
def _generate_image_js_code(
self,
unique_id: str,
chat_id: str,
message_id: str,
markdown_syntax: str,
svg_width: int,
svg_height: int,
) -> str:
"""Generate JavaScript code for frontend SVG rendering and image embedding"""
# Escape the syntax for JS embedding
syntax_escaped = (
markdown_syntax
.replace("\\", "\\\\")
.replace("`", "\\`")
.replace("${", "\\${")
.replace("</script>", "<\\/script>")
)
return f"""
(async function() {{
const uniqueId = "{unique_id}";
const chatId = "{chat_id}";
const messageId = "{message_id}";
const defaultWidth = {svg_width};
const defaultHeight = {svg_height};
// Auto-detect chat container width for responsive sizing
let svgWidth = defaultWidth;
let svgHeight = defaultHeight;
const chatContainer = document.getElementById('chat-container');
if (chatContainer) {{
const containerWidth = chatContainer.clientWidth;
if (containerWidth > 100) {{
// Use container width with some padding (90% of container)
svgWidth = Math.floor(containerWidth * 0.9);
// Maintain aspect ratio based on default dimensions
svgHeight = Math.floor(svgWidth * (defaultHeight / defaultWidth));
console.log("[MindMap Image] Auto-detected container width:", containerWidth, "-> SVG:", svgWidth, "x", svgHeight);
}}
}}
console.log("[MindMap Image] Starting render...");
console.log("[MindMap Image] chatId:", chatId, "messageId:", messageId);
try {{
// Load D3 if not loaded
if (typeof d3 === 'undefined') {{
console.log("[MindMap Image] Loading D3...");
await new Promise((resolve, reject) => {{
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/d3@7';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
}});
}}
// Load markmap-lib if not loaded
if (!window.markmap || !window.markmap.Transformer) {{
console.log("[MindMap Image] Loading markmap-lib...");
await new Promise((resolve, reject) => {{
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/markmap-lib@0.17';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
}});
}}
// Load markmap-view if not loaded
if (!window.markmap || !window.markmap.Markmap) {{
console.log("[MindMap Image] Loading markmap-view...");
await new Promise((resolve, reject) => {{
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/markmap-view@0.17';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
}});
}}
const {{ Transformer, Markmap }} = window.markmap;
// Get markdown syntax
let syntaxContent = `{syntax_escaped}`;
console.log("[MindMap Image] Syntax length:", syntaxContent.length);
// Create offscreen container
const container = document.createElement('div');
container.id = 'mindmap-offscreen-' + uniqueId;
container.style.cssText = 'position:absolute;left:-9999px;top:-9999px;width:' + svgWidth + 'px;height:' + svgHeight + 'px;';
document.body.appendChild(container);
// Create SVG element
const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgEl.setAttribute('width', svgWidth);
svgEl.setAttribute('height', svgHeight);
svgEl.style.width = svgWidth + 'px';
svgEl.style.height = svgHeight + 'px';
svgEl.style.backgroundColor = '#ffffff';
container.appendChild(svgEl);
// Transform markdown to tree
const transformer = new Transformer();
const {{ root }} = transformer.transform(syntaxContent);
// Create markmap instance
const options = {{
autoFit: true,
initialExpandLevel: Infinity,
zoom: false,
pan: false
}};
console.log("[MindMap Image] Rendering markmap...");
const markmapInstance = Markmap.create(svgEl, options, root);
// Wait for render to complete
await new Promise(resolve => setTimeout(resolve, 1500));
markmapInstance.fit();
await new Promise(resolve => setTimeout(resolve, 500));
// Clone and prepare SVG for export
const clonedSvg = svgEl.cloneNode(true);
clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
clonedSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
// Add background rect
const bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
bgRect.setAttribute('width', '100%');
bgRect.setAttribute('height', '100%');
bgRect.setAttribute('fill', '#ffffff');
clonedSvg.insertBefore(bgRect, clonedSvg.firstChild);
// Add inline styles
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
style.textContent = `
text {{ font-family: sans-serif; font-size: 14px; fill: #000000; }}
foreignObject, .markmap-foreign, .markmap-foreign div {{ color: #000000; font-family: sans-serif; font-size: 14px; }}
h1 {{ font-size: 22px; font-weight: 700; margin: 0; }}
h2 {{ font-size: 18px; font-weight: 600; margin: 0; }}
strong {{ font-weight: 700; }}
.markmap-link {{ stroke: #546e7a; fill: none; }}
.markmap-node circle, .markmap-node rect {{ stroke: #94a3b8; }}
`;
clonedSvg.insertBefore(style, bgRect.nextSibling);
// Convert foreignObject to text for better compatibility
const foreignObjects = clonedSvg.querySelectorAll('foreignObject');
foreignObjects.forEach(fo => {{
const text = fo.textContent || '';
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
textEl.setAttribute('x', fo.getAttribute('x') || '0');
textEl.setAttribute('y', (parseFloat(fo.getAttribute('y') || '0') + 14).toString());
textEl.setAttribute('fill', '#000000');
textEl.setAttribute('font-family', 'sans-serif');
textEl.setAttribute('font-size', '14');
textEl.textContent = text.trim();
g.appendChild(textEl);
fo.parentNode.replaceChild(g, fo);
}});
// Serialize SVG to string
const svgData = new XMLSerializer().serializeToString(clonedSvg);
const svgBase64 = btoa(unescape(encodeURIComponent(svgData)));
const dataUrl = 'data:image/svg+xml;base64,' + svgBase64;
console.log("[MindMap Image] Data URL generated, length:", dataUrl.length);
// Cleanup
document.body.removeChild(container);
// Generate markdown image
const markdownImage = `![🧠 Mind Map](${{dataUrl}})`;
// Update message via API
if (chatId && messageId) {{
const token = localStorage.getItem("token");
// Get current chat data
const getResponse = await fetch(`/api/v1/chats/${{chatId}}`, {{
method: "GET",
headers: {{ "Authorization": `Bearer ${{token}}` }}
}});
if (!getResponse.ok) {{
throw new Error("Failed to get chat data: " + getResponse.status);
}}
const chatData = await getResponse.json();
let originalContent = "";
let updatedMessages = [];
if (chatData.chat && chatData.chat.messages) {{
updatedMessages = chatData.chat.messages.map(m => {{
if (m.id === messageId) {{
originalContent = m.content || "";
// Remove existing mindmap images
const mindmapPattern = /\\n*!\\[🧠[^\\]]*\\]\\(data:image\\/[^)]+\\)/g;
let cleanedContent = originalContent.replace(mindmapPattern, "");
cleanedContent = cleanedContent.replace(/\\n{{3,}}/g, "\\n\\n").trim();
// Append new image
const newContent = cleanedContent + "\\n\\n" + markdownImage;
// Critical: Update content in both messages array AND history object
// The history object is often the source of truth for the database
if (chatData.chat.history && chatData.chat.history.messages && chatData.chat.history.messages[messageId]) {{
chatData.chat.history.messages[messageId].content = newContent;
}}
return {{ ...m, content: newContent }};
}}
return m;
}});
}}
// First: Update frontend display via event API (for immediate visual feedback)
await fetch(`/api/v1/chats/${{chatId}}/messages/${{messageId}}/event`, {{
method: "POST",
headers: {{
"Content-Type": "application/json",
"Authorization": `Bearer ${{token}}`
}},
body: JSON.stringify({{
type: "chat:message",
data: {{ content: updatedMessages.find(m => m.id === messageId)?.content || "" }}
}})
}});
// Second: Persist to database by updating the entire chat
const updatePayload = {{
chat: {{
...chatData.chat,
messages: updatedMessages
}}
}};
const persistResponse = await fetch(`/api/v1/chats/${{chatId}}`, {{
method: "POST",
headers: {{
"Content-Type": "application/json",
"Authorization": `Bearer ${{token}}`
}},
body: JSON.stringify(updatePayload)
}});
if (persistResponse.ok) {{
console.log("[MindMap Image] ✅ Message persisted successfully!");
}} else {{
console.error("[MindMap Image] Persist API error:", persistResponse.status);
// Try alternative update method
const altResponse = await fetch(`/api/v1/chats/${{chatId}}/share`, {{
method: "POST",
headers: {{
"Content-Type": "application/json",
"Authorization": `Bearer ${{token}}`
}}
}});
console.log("[MindMap Image] Alt persist attempted:", altResponse.status);
}}
}} else {{
console.warn("[MindMap Image] ⚠️ Missing chatId or messageId");
}}
}} catch (error) {{
console.error("[MindMap Image] Error:", error);
}}
}})();
"""
async def action( async def action(
self, self,
body: dict, body: dict,
__user__: Optional[Dict[str, Any]] = None, __user__: Optional[Dict[str, Any]] = None,
__event_emitter__: Optional[Any] = None, __event_emitter__: Optional[Any] = None,
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
__metadata__: Optional[dict] = None,
__request__: Optional[Request] = None, __request__: Optional[Request] = None,
) -> Optional[dict]: ) -> Optional[dict]:
logger.info("Action: Smart Mind Map (v0.8.0) started") logger.info("Action: Smart Mind Map (v0.8.0) started")
@@ -1090,6 +1417,47 @@ class Action:
user_language, user_language,
) )
# Check output mode
if self.valves.OUTPUT_MODE == "image":
# Image mode: use JavaScript to render and embed as Markdown image
chat_id = self._extract_chat_id(body, __metadata__)
message_id = self._extract_message_id(body, __metadata__)
await self._emit_status(
__event_emitter__,
"Smart Mind Map: Rendering image...",
False,
)
if __event_call__:
js_code = self._generate_image_js_code(
unique_id=unique_id,
chat_id=chat_id,
message_id=message_id,
markdown_syntax=markdown_syntax,
svg_width=self.valves.SVG_WIDTH,
svg_height=self.valves.SVG_HEIGHT,
)
await __event_call__(
{
"type": "execute",
"data": {"code": js_code},
}
)
await self._emit_status(
__event_emitter__, "Smart Mind Map: Image generated!", True
)
await self._emit_notification(
__event_emitter__,
f"Mind map image has been generated, {user_name}!",
"success",
)
logger.info("Action: Smart Mind Map (v0.9.0) completed in image mode")
return body
# HTML mode (default): embed as HTML block
html_embed_tag = f"```html\n{final_html}\n```" html_embed_tag = f"```html\n{final_html}\n```"
body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}" body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}"
@@ -1101,7 +1469,7 @@ class Action:
f"Mind map has been generated, {user_name}!", f"Mind map has been generated, {user_name}!",
"success", "success",
) )
logger.info("Action: Smart Mind Map (v0.8.0) completed successfully") logger.info("Action: Smart Mind Map (v0.9.0) completed in HTML mode")
except Exception as e: except Exception as e:
error_message = f"Smart Mind Map processing failed: {str(e)}" error_message = f"Smart Mind Map processing failed: {str(e)}"

View File

@@ -3,7 +3,7 @@ title: 思维导图
author: Fu-Jie author: Fu-Jie
author_url: https://github.com/Fu-Jie author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.8.2 version: 0.9.0
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4= icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。 description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。
""" """
@@ -13,7 +13,7 @@ import os
import re import re
import time import time
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Dict, Optional from typing import Any, Callable, Awaitable, Dict, Optional
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from fastapi import Request from fastapi import Request
@@ -443,7 +443,7 @@ SCRIPT_TEMPLATE_MINDMAP = """
const markdownContent = sourceEl.textContent.trim(); const markdownContent = sourceEl.textContent.trim();
if (!markdownContent) { if (!markdownContent) {
containerEl.innerHTML = '<div class="error-message">⚠️ 无法加载思维导图缺少有效内容。</div>'; containerEl.innerHTML = '<div class="error-message">⚠️ 无法加载思维导图:缺少有效内容。</div>';
return; return;
} }
@@ -485,7 +485,7 @@ SCRIPT_TEMPLATE_MINDMAP = """
}).catch((error) => { }).catch((error) => {
console.error('Markmap loading error:', error); console.error('Markmap loading error:', error);
containerEl.innerHTML = '<div class="error-message">⚠️ 资源加载失败请稍后重试。</div>'; containerEl.innerHTML = '<div class="error-message">⚠️ 资源加载失败,请稍后重试。</div>';
}); });
}; };
@@ -771,19 +771,31 @@ class Action:
) )
MODEL_ID: str = Field( MODEL_ID: str = Field(
default="", default="",
description="用于文本分析的内置LLM模型ID。如果为空则使用当前对话的模型。", description="用于文本分析的内置LLM模型ID。如果为空,则使用当前对话的模型。",
) )
MIN_TEXT_LENGTH: int = Field( MIN_TEXT_LENGTH: int = Field(
default=100, default=100,
description="进行思维导图分析所需的最小文本长度字符数", description="进行思维导图分析所需的最小文本长度(字符数)",
) )
CLEAR_PREVIOUS_HTML: bool = Field( CLEAR_PREVIOUS_HTML: bool = Field(
default=False, default=False,
description="是否强制清除旧的插件结果如果为 True则不合并直接覆盖", description="是否强制清除旧的插件结果(如果为 True,则不合并,直接覆盖)",
) )
MESSAGE_COUNT: int = Field( MESSAGE_COUNT: int = Field(
default=1, default=1,
description="用于生成的最近消息数量。设置为1仅使用最后一条消息更大值可包含更多上下文。", description="用于生成的最近消息数量。设置为1仅使用最后一条消息,更大值可包含更多上下文。",
)
OUTPUT_MODE: str = Field(
default="html",
description="输出模式: 'html' 为交互式HTML(默认),'image' 为嵌入Markdown图片。",
)
SVG_WIDTH: int = Field(
default=1200,
description="SVG画布宽度(像素,用于图片模式)。",
)
SVG_HEIGHT: int = Field(
default=800,
description="SVG画布高度(像素,用于图片模式)。",
) )
def __init__(self): def __init__(self):
@@ -813,13 +825,53 @@ class Action:
"user_language": user_data.get("language", "zh-CN"), "user_language": user_data.get("language", "zh-CN"),
} }
def _extract_chat_id(self, body: dict, metadata: Optional[dict]) -> str:
"""从 body 或 metadata 中提取 chat_id"""
if isinstance(body, dict):
chat_id = body.get("chat_id")
if isinstance(chat_id, str) and chat_id.strip():
return chat_id.strip()
body_metadata = body.get("metadata", {})
if isinstance(body_metadata, dict):
chat_id = body_metadata.get("chat_id")
if isinstance(chat_id, str) and chat_id.strip():
return chat_id.strip()
if isinstance(metadata, dict):
chat_id = metadata.get("chat_id")
if isinstance(chat_id, str) and chat_id.strip():
return chat_id.strip()
return ""
def _extract_message_id(self, body: dict, metadata: Optional[dict]) -> str:
"""从 body 或 metadata 中提取 message_id"""
if isinstance(body, dict):
message_id = body.get("id")
if isinstance(message_id, str) and message_id.strip():
return message_id.strip()
body_metadata = body.get("metadata", {})
if isinstance(body_metadata, dict):
message_id = body_metadata.get("message_id")
if isinstance(message_id, str) and message_id.strip():
return message_id.strip()
if isinstance(metadata, dict):
message_id = metadata.get("message_id")
if isinstance(message_id, str) and message_id.strip():
return message_id.strip()
return ""
def _extract_markdown_syntax(self, llm_output: str) -> str: def _extract_markdown_syntax(self, llm_output: str) -> str:
match = re.search(r"```markdown\s*(.*?)\s*```", llm_output, re.DOTALL) match = re.search(r"```markdown\s*(.*?)\s*```", llm_output, re.DOTALL)
if match: if match:
extracted_content = match.group(1).strip() extracted_content = match.group(1).strip()
else: else:
logger.warning( logger.warning(
"LLM输出未严格遵循预期Markdown格式将整个输出作为摘要处理。" "LLM输出未严格遵循预期Markdown格式,将整个输出作为摘要处理。"
) )
extracted_content = llm_output.strip() extracted_content = llm_output.strip()
return extracted_content.replace("</script>", "<\\/script>") return extracted_content.replace("</script>", "<\\/script>")
@@ -844,7 +896,7 @@ class Action:
return re.sub(pattern, "", content).strip() return re.sub(pattern, "", content).strip()
def _extract_text_content(self, content) -> str: def _extract_text_content(self, content) -> str:
"""从消息内容中提取文本支持多模态消息格式""" """从消息内容中提取文本,支持多模态消息格式"""
if isinstance(content, str): if isinstance(content, str):
return content return content
elif isinstance(content, list): elif isinstance(content, list):
@@ -867,7 +919,7 @@ class Action:
user_language: str = "zh-CN", user_language: str = "zh-CN",
) -> str: ) -> str:
""" """
将新内容合并到现有的 HTML 容器中或者创建一个新的容器。 将新内容合并到现有的 HTML 容器中,或者创建一个新的容器。
""" """
if ( if (
"<!-- OPENWEBUI_PLUGIN_OUTPUT -->" in existing_html_code "<!-- OPENWEBUI_PLUGIN_OUTPUT -->" in existing_html_code
@@ -900,11 +952,286 @@ class Action:
return base_html.strip() return base_html.strip()
def _generate_image_js_code(
self,
unique_id: str,
chat_id: str,
message_id: str,
markdown_syntax: str,
svg_width: int,
svg_height: int,
) -> str:
"""生成用于前端 SVG 渲染和图片嵌入的 JavaScript 代码"""
# 转义语法以便嵌入 JS
syntax_escaped = (
markdown_syntax
.replace("\\", "\\\\")
.replace("`", "\\`")
.replace("${", "\\${")
.replace("</script>", "<\\/script>")
)
return f"""
(async function() {{
const uniqueId = "{unique_id}";
const chatId = "{chat_id}";
const messageId = "{message_id}";
const defaultWidth = {svg_width};
const defaultHeight = {svg_height};
// 自动检测聊天容器宽度以实现自适应
let svgWidth = defaultWidth;
let svgHeight = defaultHeight;
const chatContainer = document.getElementById('chat-container');
if (chatContainer) {{
const containerWidth = chatContainer.clientWidth;
if (containerWidth > 100) {{
// 使用容器宽度的90%(留出边距)
svgWidth = Math.floor(containerWidth * 0.9);
// 根据默认尺寸保持宽高比
svgHeight = Math.floor(svgWidth * (defaultHeight / defaultWidth));
console.log("[思维导图图片] 自动检测容器宽度:", containerWidth, "-> SVG:", svgWidth, "x", svgHeight);
}}
}}
console.log("[思维导图图片] 开始渲染...");
console.log("[思维导图图片] chatId:", chatId, "messageId:", messageId);
try {{
// 加载 D3
if (typeof d3 === 'undefined') {{
console.log("[思维导图图片] 正在加载 D3...");
await new Promise((resolve, reject) => {{
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/d3@7';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
}});
}}
// 加载 markmap-lib
if (!window.markmap || !window.markmap.Transformer) {{
console.log("[思维导图图片] 正在加载 markmap-lib...");
await new Promise((resolve, reject) => {{
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/markmap-lib@0.17';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
}});
}}
// 加载 markmap-view
if (!window.markmap || !window.markmap.Markmap) {{
console.log("[思维导图图片] 正在加载 markmap-view...");
await new Promise((resolve, reject) => {{
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/markmap-view@0.17';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
}});
}}
const {{ Transformer, Markmap }} = window.markmap;
// 获取 markdown 语法
let syntaxContent = `{syntax_escaped}`;
console.log("[思维导图图片] 语法长度:", syntaxContent.length);
// 创建离屏容器
const container = document.createElement('div');
container.id = 'mindmap-offscreen-' + uniqueId;
container.style.cssText = 'position:absolute;left:-9999px;top:-9999px;width:' + svgWidth + 'px;height:' + svgHeight + 'px;';
document.body.appendChild(container);
// 创建 SVG 元素
const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgEl.setAttribute('width', svgWidth);
svgEl.setAttribute('height', svgHeight);
svgEl.style.width = svgWidth + 'px';
svgEl.style.height = svgHeight + 'px';
svgEl.style.backgroundColor = '#ffffff';
container.appendChild(svgEl);
// 将 markdown 转换为树结构
const transformer = new Transformer();
const {{ root }} = transformer.transform(syntaxContent);
// 创建 markmap 实例
const options = {{
autoFit: true,
initialExpandLevel: Infinity,
zoom: false,
pan: false
}};
console.log("[思维导图图片] 正在渲染 markmap...");
const markmapInstance = Markmap.create(svgEl, options, root);
// 等待渲染完成
await new Promise(resolve => setTimeout(resolve, 1500));
markmapInstance.fit();
await new Promise(resolve => setTimeout(resolve, 500));
// 克隆并准备 SVG 导出
const clonedSvg = svgEl.cloneNode(true);
clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
clonedSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
// 添加背景矩形
const bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
bgRect.setAttribute('width', '100%');
bgRect.setAttribute('height', '100%');
bgRect.setAttribute('fill', '#ffffff');
clonedSvg.insertBefore(bgRect, clonedSvg.firstChild);
// 添加内联样式
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
style.textContent = `
text {{ font-family: sans-serif; font-size: 14px; fill: #000000; }}
foreignObject, .markmap-foreign, .markmap-foreign div {{ color: #000000; font-family: sans-serif; font-size: 14px; }}
h1 {{ font-size: 22px; font-weight: 700; margin: 0; }}
h2 {{ font-size: 18px; font-weight: 600; margin: 0; }}
strong {{ font-weight: 700; }}
.markmap-link {{ stroke: #546e7a; fill: none; }}
.markmap-node circle, .markmap-node rect {{ stroke: #94a3b8; }}
`;
clonedSvg.insertBefore(style, bgRect.nextSibling);
// 将 foreignObject 转换为 text 以提高兼容性
const foreignObjects = clonedSvg.querySelectorAll('foreignObject');
foreignObjects.forEach(fo => {{
const text = fo.textContent || '';
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
textEl.setAttribute('x', fo.getAttribute('x') || '0');
textEl.setAttribute('y', (parseFloat(fo.getAttribute('y') || '0') + 14).toString());
textEl.setAttribute('fill', '#000000');
textEl.setAttribute('font-family', 'sans-serif');
textEl.setAttribute('font-size', '14');
textEl.textContent = text.trim();
g.appendChild(textEl);
fo.parentNode.replaceChild(g, fo);
}});
// 序列化 SVG 为字符串
const svgData = new XMLSerializer().serializeToString(clonedSvg);
const svgBase64 = btoa(unescape(encodeURIComponent(svgData)));
const dataUrl = 'data:image/svg+xml;base64,' + svgBase64;
console.log("[思维导图图片] Data URL 已生成,长度:", dataUrl.length);
// 清理
document.body.removeChild(container);
// 生成 markdown 图片
const markdownImage = `![🧠 思维导图](${{dataUrl}})`;
// 通过 API 更新消息
if (chatId && messageId) {{
const token = localStorage.getItem("token");
// 获取当前聊天数据
const getResponse = await fetch(`/api/v1/chats/${{chatId}}`, {{
method: "GET",
headers: {{ "Authorization": `Bearer ${{token}}` }}
}});
if (!getResponse.ok) {{
throw new Error("获取聊天数据失败: " + getResponse.status);
}}
const chatData = await getResponse.json();
let originalContent = "";
let updatedMessages = [];
if (chatData.chat && chatData.chat.messages) {{
updatedMessages = chatData.chat.messages.map(m => {{
if (m.id === messageId) {{
originalContent = m.content || "";
// 移除已有的思维导图图片
const mindmapPattern = /\\n*!\\[🧠[^\\]]*\\]\\(data:image\\/[^)]+\\)/g;
let cleanedContent = originalContent.replace(mindmapPattern, "");
cleanedContent = cleanedContent.replace(/\\n{{3,}}/g, "\\n\\n").trim();
// 追加新图片
const newContent = cleanedContent + "\\n\\n" + markdownImage;
// 关键: 同时更新 messages 数组和 history 对象中的内容
// history 对象通常是数据库的单一真值来源
if (chatData.chat.history && chatData.chat.history.messages && chatData.chat.history.messages[messageId]) {{
chatData.chat.history.messages[messageId].content = newContent;
}}
return {{ ...m, content: newContent }};
}}
return m;
}});
}}
// 第一步: 通过事件 API 更新前端显示(立即视觉反馈)
await fetch(`/api/v1/chats/${{chatId}}/messages/${{messageId}}/event`, {{
method: "POST",
headers: {{
"Content-Type": "application/json",
"Authorization": `Bearer ${{token}}`
}},
body: JSON.stringify({{
type: "chat:message",
data: {{ content: updatedMessages.find(m => m.id === messageId)?.content || "" }}
}})
}});
// 第二步: 通过更新整个聊天来持久化到数据库
const updatePayload = {{
chat: {{
...chatData.chat,
messages: updatedMessages
}}
}};
const persistResponse = await fetch(`/api/v1/chats/${{chatId}}`, {{
method: "POST",
headers: {{
"Content-Type": "application/json",
"Authorization": `Bearer ${{token}}`
}},
body: JSON.stringify(updatePayload)
}});
if (persistResponse.ok) {{
console.log("[思维导图图片] ✅ 消息已持久化保存!");
}} else {{
console.error("[思维导图图片] 持久化 API 错误:", persistResponse.status);
// 尝试备用更新方法
const altResponse = await fetch(`/api/v1/chats/${{chatId}}/share`, {{
method: "POST",
headers: {{
"Content-Type": "application/json",
"Authorization": `Bearer ${{token}}`
}}
}});
console.log("[思维导图图片] 备用持久化尝试:", altResponse.status);
}}
}} else {{
console.warn("[思维导图图片] ⚠️ 缺少 chatId 或 messageId");
}}
}} catch (error) {{
console.error("[思维导图图片] 错误:", error);
}}
}})();
"""
async def action( async def action(
self, self,
body: dict, body: dict,
__user__: Optional[Dict[str, Any]] = None, __user__: Optional[Dict[str, Any]] = None,
__event_emitter__: Optional[Any] = None, __event_emitter__: Optional[Any] = None,
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
__metadata__: Optional[dict] = None,
__request__: Optional[Request] = None, __request__: Optional[Request] = None,
) -> Optional[dict]: ) -> Optional[dict]:
logger.info("Action: 思维导图 (v12 - Final Feedback Fix) started") logger.info("Action: 思维导图 (v12 - Final Feedback Fix) started")
@@ -923,7 +1250,7 @@ class Action:
current_year = now_dt.strftime("%Y") current_year = now_dt.strftime("%Y")
current_timezone_str = tz_env or "UTC" current_timezone_str = tz_env or "UTC"
except Exception as e: except Exception as e:
logger.warning(f"获取时区信息失败: {e}使用默认值。") logger.warning(f"获取时区信息失败: {e},使用默认值。")
now = datetime.now() now = datetime.now()
current_date_time_str = now.strftime("%Y年%m月%d%H:%M:%S") current_date_time_str = now.strftime("%Y年%m月%d%H:%M:%S")
current_weekday_zh = "未知星期" current_weekday_zh = "未知星期"
@@ -931,7 +1258,7 @@ class Action:
current_timezone_str = "未知时区" current_timezone_str = "未知时区"
await self._emit_notification( await self._emit_notification(
__event_emitter__, "思维导图已启动正在为您生成思维导图...", "info" __event_emitter__, "思维导图已启动,正在为您生成思维导图...", "info"
) )
messages = body.get("messages") messages = body.get("messages")
@@ -980,7 +1307,7 @@ class Action:
long_text_content = original_content.strip() long_text_content = original_content.strip()
if len(long_text_content) < self.valves.MIN_TEXT_LENGTH: if len(long_text_content) < self.valves.MIN_TEXT_LENGTH:
short_text_message = f"文本内容过短({len(long_text_content)}字符)无法进行有效分析。请提供至少{self.valves.MIN_TEXT_LENGTH}字符的文本。" short_text_message = f"文本内容过短({len(long_text_content)}字符),无法进行有效分析。请提供至少{self.valves.MIN_TEXT_LENGTH}字符的文本。"
await self._emit_notification( await self._emit_notification(
__event_emitter__, short_text_message, "warning" __event_emitter__, short_text_message, "warning"
) )
@@ -1021,7 +1348,7 @@ class Action:
} }
user_obj = Users.get_user_by_id(user_id) user_obj = Users.get_user_by_id(user_id)
if not user_obj: if not user_obj:
raise ValueError(f"无法获取用户对象用户ID: {user_id}") raise ValueError(f"无法获取用户对象,用户ID: {user_id}")
llm_response = await generate_chat_completion( llm_response = await generate_chat_completion(
__request__, llm_payload, user_obj __request__, llm_payload, user_obj
@@ -1084,26 +1411,67 @@ class Action:
user_language, user_language,
) )
# 检查输出模式
if self.valves.OUTPUT_MODE == "image":
# 图片模式: 使用 JavaScript 渲染并嵌入为 Markdown 图片
chat_id = self._extract_chat_id(body, __metadata__)
message_id = self._extract_message_id(body, __metadata__)
await self._emit_status(
__event_emitter__,
"思维导图: 正在渲染图片...",
False,
)
if __event_call__:
js_code = self._generate_image_js_code(
unique_id=unique_id,
chat_id=chat_id,
message_id=message_id,
markdown_syntax=markdown_syntax,
svg_width=self.valves.SVG_WIDTH,
svg_height=self.valves.SVG_HEIGHT,
)
await __event_call__(
{
"type": "execute",
"data": {"code": js_code},
}
)
await self._emit_status(
__event_emitter__, "思维导图: 图片已生成!", True
)
await self._emit_notification(
__event_emitter__,
f"思维导图图片已生成,{user_name}!",
"success",
)
logger.info("Action: 思维导图 (v0.9.0) 图片模式完成")
return body
# HTML 模式(默认): 嵌入为 HTML 块
html_embed_tag = f"```html\n{final_html}\n```" html_embed_tag = f"```html\n{final_html}\n```"
body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}" body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}"
await self._emit_status(__event_emitter__, "思维导图: 绘制完成", True) await self._emit_status(__event_emitter__, "思维导图: 绘制完成!", True)
await self._emit_notification( await self._emit_notification(
__event_emitter__, f"思维导图已生成{user_name}", "success" __event_emitter__, f"思维导图已生成,{user_name}!", "success"
) )
logger.info("Action: 思维导图 (v12) completed successfully") logger.info("Action: 思维导图 (v0.9.0) HTML 模式完成")
except Exception as e: except Exception as e:
error_message = f"思维导图处理失败: {str(e)}" error_message = f"思维导图处理失败: {str(e)}"
logger.error(f"思维导图错误: {error_message}", exc_info=True) logger.error(f"思维导图错误: {error_message}", exc_info=True)
user_facing_error = f"抱歉思维导图在处理时遇到错误: {str(e)}\n请检查Open WebUI后端日志获取更多详情。" user_facing_error = f"抱歉,思维导图在处理时遇到错误: {str(e)}\n请检查Open WebUI后端日志获取更多详情。"
body["messages"][-1][ body["messages"][-1][
"content" "content"
] = f"{long_text_content}\n\n❌ **错误:** {user_facing_error}" ] = f"{long_text_content}\n\n❌ **错误:** {user_facing_error}"
await self._emit_status(__event_emitter__, "思维导图: 处理失败。", True) await self._emit_status(__event_emitter__, "思维导图: 处理失败。", True)
await self._emit_notification( await self._emit_notification(
__event_emitter__, f"思维导图生成失败, {user_name}", "error" __event_emitter__, f"思维导图生成失败, {user_name}!", "error"
) )
return body return body

View File

@@ -20,10 +20,19 @@ OpenWebUI 社区统计工具
import os import os
import json import json
import requests import requests
from datetime import datetime from datetime import datetime, timezone, timedelta
from typing import Optional from typing import Optional
from pathlib import Path from pathlib import Path
# 北京时区 (UTC+8)
BEIJING_TZ = timezone(timedelta(hours=8))
def get_beijing_time() -> datetime:
"""获取当前北京时间"""
return datetime.now(BEIJING_TZ)
# 尝试加载 .env 文件 # 尝试加载 .env 文件
try: try:
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -190,7 +199,7 @@ class OpenWebUIStats:
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("📊 OpenWebUI 社区统计报告") print("📊 OpenWebUI 社区统计报告")
print("=" * 60) print("=" * 60)
print(f"📅 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") print(f"📅 生成时间 (北京): {get_beijing_time().strftime('%Y-%m-%d %H:%M')}")
print() print()
# 总览 # 总览
@@ -241,7 +250,7 @@ class OpenWebUIStats:
texts = { texts = {
"zh": { "zh": {
"title": "# 📊 OpenWebUI 社区统计报告", "title": "# 📊 OpenWebUI 社区统计报告",
"updated": f"> 📅 更新时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", "updated": f"> 📅 更新时间 (北京): {get_beijing_time().strftime('%Y-%m-%d %H:%M')}",
"overview_title": "## 📈 总览", "overview_title": "## 📈 总览",
"overview_header": "| 指标 | 数值 |", "overview_header": "| 指标 | 数值 |",
"posts": "📝 发布数量", "posts": "📝 发布数量",
@@ -256,7 +265,7 @@ class OpenWebUIStats:
}, },
"en": { "en": {
"title": "# 📊 OpenWebUI Community Stats Report", "title": "# 📊 OpenWebUI Community Stats Report",
"updated": f"> 📅 Updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", "updated": f"> 📅 Updated (Beijing Time): {get_beijing_time().strftime('%Y-%m-%d %H:%M')}",
"overview_title": "## 📈 Overview", "overview_title": "## 📈 Overview",
"overview_header": "| Metric | Value |", "overview_header": "| Metric | Value |",
"posts": "📝 Total Posts", "posts": "📝 Total Posts",
@@ -337,7 +346,7 @@ class OpenWebUIStats:
texts = { texts = {
"zh": { "zh": {
"title": "## 📊 社区统计", "title": "## 📊 社区统计",
"updated": f"> 🕐 自动更新于 {datetime.now().strftime('%Y-%m-%d')}", "updated": f"> 🕐 自动更新于 {get_beijing_time().strftime('%Y-%m-%d %H:%M')} (北京时间)",
"author_header": "| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |", "author_header": "| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |",
"header": "| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |", "header": "| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |",
"top5_title": "### 🔥 热门插件 Top 5", "top5_title": "### 🔥 热门插件 Top 5",
@@ -346,7 +355,7 @@ class OpenWebUIStats:
}, },
"en": { "en": {
"title": "## 📊 Community Stats", "title": "## 📊 Community Stats",
"updated": f"> 🕐 Auto-updated on {datetime.now().strftime('%Y-%m-%d')}", "updated": f"> 🕐 Auto-updated: {get_beijing_time().strftime('%Y-%m-%d %H:%M')} (Beijing Time)",
"author_header": "| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |", "author_header": "| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |",
"header": "| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |", "header": "| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |",
"top5_title": "### 🔥 Top 5 Popular Plugins", "top5_title": "### 🔥 Top 5 Popular Plugins",