feat(stats): enhance plugin contribution tracking and update statistics display

This commit is contained in:
fujie
2026-03-16 01:54:05 +08:00
parent e92ad60e3f
commit 1df328a304

View File

@@ -89,12 +89,15 @@ class OpenWebUIStats:
"action", "action",
"filter", "filter",
"pipe", "pipe",
"pipeline",
"tool", "tool",
"function", "function",
"prompt", "prompt",
"model", "model",
] ]
NON_PLUGIN_TYPES = {"post", "review", "comment"}
TYPE_ALIASES = { TYPE_ALIASES = {
"tools": "tool", "tools": "tool",
} }
@@ -104,6 +107,11 @@ class OpenWebUIStats:
normalized = str(post_type or "").strip().lower() normalized = str(post_type or "").strip().lower()
return self.TYPE_ALIASES.get(normalized, normalized) return self.TYPE_ALIASES.get(normalized, normalized)
def _is_published_plugin(self, post_type: str, downloads: int) -> bool:
"""Treat marketplace items with downloads > 0 as published plugins."""
normalized = self._normalize_post_type(post_type)
return downloads > 0 and normalized not in self.NON_PLUGIN_TYPES
def load_history(self) -> list: def load_history(self) -> list:
"""Load history records (merge Gist + local file, keep the one with more records)""" """Load history records (merge Gist + local file, keep the one with more records)"""
gist_history = [] gist_history = []
@@ -199,7 +207,10 @@ class OpenWebUIStats:
"total_saves": stats["total_saves"], "total_saves": stats["total_saves"],
"followers": stats.get("user", {}).get("followers", 0), "followers": stats.get("user", {}).get("followers", 0),
"points": stats.get("user", {}).get("total_points", 0), "points": stats.get("user", {}).get("total_points", 0),
"contributions": stats.get("user", {}).get("contributions", 0), "contributions": stats.get(
"plugin_contributions",
stats.get("user", {}).get("contributions", 0),
),
"posts": {p["slug"]: p["downloads"] for p in stats.get("posts", [])}, "posts": {p["slug"]: p["downloads"] for p in stats.get("posts", [])},
} }
@@ -268,8 +279,11 @@ class OpenWebUIStats:
- prev.get("followers", 0), - prev.get("followers", 0),
"points": stats.get("user", {}).get("total_points", 0) "points": stats.get("user", {}).get("total_points", 0)
- prev.get("points", 0), - prev.get("points", 0),
"contributions": stats.get("user", {}).get("contributions", 0) "contributions": stats.get(
- prev.get("contributions", 0), "plugin_contributions",
stats.get("user", {}).get("contributions", 0),
)
- prev.get("contributions", prev.get("plugin_contributions", 0)),
"posts": { "posts": {
p["slug"]: p["downloads"] p["slug"]: p["downloads"]
- prev.get("posts", {}).get(p["slug"], p["downloads"]) - prev.get("posts", {}).get(p["slug"], p["downloads"])
@@ -601,6 +615,7 @@ class OpenWebUIStats:
"total_downvotes": 0, "total_downvotes": 0,
"total_saves": 0, "total_saves": 0,
"total_comments": 0, "total_comments": 0,
"plugin_contributions": 0,
"by_type": {}, "by_type": {},
"posts": [], "posts": [],
"user": {}, # User info "user": {}, # User info
@@ -629,19 +644,21 @@ class OpenWebUIStats:
meta = plugin_obj.get("meta", {}) or {} meta = plugin_obj.get("meta", {}) or {}
manifest = meta.get("manifest", {}) or {} manifest = meta.get("manifest", {}) or {}
# Accumulate statistics
post_downloads = post.get("downloads", 0) post_downloads = post.get("downloads", 0)
post_views = post.get("views", 0) post_views = post.get("views", 0)
post_saves = post.get("saveCount", 0)
is_published_plugin = self._is_published_plugin(post_type, post_downloads)
stats["total_downloads"] += post_downloads
stats["total_upvotes"] += post.get("upvotes", 0) stats["total_upvotes"] += post.get("upvotes", 0)
stats["total_downvotes"] += post.get("downvotes", 0) stats["total_downvotes"] += post.get("downvotes", 0)
stats["total_saves"] += post.get("saveCount", 0)
stats["total_comments"] += post.get("commentCount", 0) stats["total_comments"] += post.get("commentCount", 0)
# Key: only count views for posts with actual downloads (exclude post/review types) # Plugin-only marketplace totals: published items with downloads > 0.
if post_type not in ("post", "review") and post_downloads > 0: if is_published_plugin:
stats["total_downloads"] += post_downloads
stats["total_views"] += post_views stats["total_views"] += post_views
stats["total_saves"] += post_saves
stats["plugin_contributions"] += 1
if post_type not in stats["by_type"]: if post_type not in stats["by_type"]:
stats["by_type"][post_type] = 0 stats["by_type"][post_type] = 0
stats["by_type"][post_type] += 1 stats["by_type"][post_type] += 1
@@ -661,8 +678,9 @@ class OpenWebUIStats:
"downloads": post.get("downloads", 0), "downloads": post.get("downloads", 0),
"views": post.get("views", 0), "views": post.get("views", 0),
"upvotes": post.get("upvotes", 0), "upvotes": post.get("upvotes", 0),
"saves": post.get("saveCount", 0), "saves": post_saves,
"comments": post.get("commentCount", 0), "comments": post.get("commentCount", 0),
"is_published_plugin": is_published_plugin,
"created_at": created_at.strftime("%Y-%m-%d"), "created_at": created_at.strftime("%Y-%m-%d"),
"updated_at": updated_at.strftime("%Y-%m-%d"), "updated_at": updated_at.strftime("%Y-%m-%d"),
"url": f"https://openwebui.com/posts/{post.get('slug', '')}", "url": f"https://openwebui.com/posts/{post.get('slug', '')}",
@@ -688,10 +706,11 @@ class OpenWebUIStats:
print("📈 Overview") print("📈 Overview")
print("-" * 40) print("-" * 40)
print(f" 📝 Posts: {stats['total_posts']}") print(f" 📝 Posts: {stats['total_posts']}")
print(f" ⬇️ Total Downloads: {stats['total_downloads']}") print(f" 🧩 Published Plugins: {stats['plugin_contributions']}")
print(f" 👁 Total Views: {stats['total_views']}") print(f" Plugin Downloads: {stats['total_downloads']}")
print(f" 👁️ Plugin Views: {stats['total_views']}")
print(f" 👍 Total Upvotes: {stats['total_upvotes']}") print(f" 👍 Total Upvotes: {stats['total_upvotes']}")
print(f" 💾 Total Saves: {stats['total_saves']}") print(f" 💾 Plugin Saves: {stats['total_saves']}")
print(f" 💬 Total Comments: {stats['total_comments']}") print(f" 💬 Total Comments: {stats['total_comments']}")
print() print()
@@ -745,13 +764,14 @@ class OpenWebUIStats:
"overview_title": "## 📈 总览", "overview_title": "## 📈 总览",
"overview_header": "| 指标 | 数值 |", "overview_header": "| 指标 | 数值 |",
"posts": "📝 发布数量", "posts": "📝 发布数量",
"downloads": "⬇️ 总下载量", "downloads": "⬇️ 插件总下载量",
"views": "👁️ 总浏览量", "views": "👁️ 插件总浏览量",
"upvotes": "👍 总点赞数", "upvotes": "👍 总点赞数",
"saves": "💾 总收藏数", "saves": "💾 插件总收藏数",
"comments": "💬 总评论数", "comments": "💬 总评论数",
"author_points": "⭐ 作者总积分", "author_points": "⭐ 作者总积分",
"author_followers": "👥 粉丝数量", "author_followers": "👥 粉丝数量",
"author_contributions": "🧩 已发布插件数",
"type_title": "## 📂 按类型分类", "type_title": "## 📂 按类型分类",
"list_title": "## 📋 发布列表", "list_title": "## 📋 发布列表",
"list_header": "| 排名 | 标题 | 类型 | 版本 | 下载 | 浏览 | 点赞 | 收藏 | 更新日期 |", "list_header": "| 排名 | 标题 | 类型 | 版本 | 下载 | 浏览 | 点赞 | 收藏 | 更新日期 |",
@@ -762,13 +782,14 @@ class OpenWebUIStats:
"overview_title": "## 📈 Overview", "overview_title": "## 📈 Overview",
"overview_header": "| Metric | Value |", "overview_header": "| Metric | Value |",
"posts": "📝 Total Posts", "posts": "📝 Total Posts",
"downloads": "⬇️ Total Downloads", "downloads": "⬇️ Total Plugin Downloads",
"views": "👁️ Total Views", "views": "👁️ Total Plugin Views",
"upvotes": "👍 Total Upvotes", "upvotes": "👍 Total Upvotes",
"saves": "💾 Total Saves", "saves": "💾 Total Plugin Saves",
"comments": "💬 Total Comments", "comments": "💬 Total Comments",
"author_points": "⭐ Author Points", "author_points": "⭐ Author Points",
"author_followers": "👥 Followers", "author_followers": "👥 Followers",
"author_contributions": "🧩 Published Plugins",
"type_title": "## 📂 By Type", "type_title": "## 📂 By Type",
"list_title": "## 📋 Posts List", "list_title": "## 📋 Posts List",
"list_header": "| Rank | Title | Type | Version | Downloads | Views | Upvotes | Saves | Updated |", "list_header": "| Rank | Title | Type | Version | Downloads | Views | Upvotes | Saves | Updated |",
@@ -815,6 +836,9 @@ class OpenWebUIStats:
md.append( md.append(
f"| {t['author_followers']} | {self.get_badge('followers', stats, user, delta)} |" f"| {t['author_followers']} | {self.get_badge('followers', stats, user, delta)} |"
) )
md.append(
f"| {t['author_contributions']} | {self.get_badge('contributions', stats, user, delta)} |"
)
md.append("") md.append("")
@@ -914,6 +938,12 @@ class OpenWebUIStats:
"color": "blue", "color": "blue",
"namedLogo": "openwebui", "namedLogo": "openwebui",
}, },
"views": {
"schemaVersion": 1,
"label": "views",
"message": format_number(stats["total_views"]),
"color": "blueviolet",
},
"plugins": { "plugins": {
"schemaVersion": 1, "schemaVersion": 1,
"label": "plugins", "label": "plugins",
@@ -932,6 +962,18 @@ class OpenWebUIStats:
"message": format_number(stats.get("user", {}).get("total_points", 0)), "message": format_number(stats.get("user", {}).get("total_points", 0)),
"color": "orange", "color": "orange",
}, },
"saves": {
"schemaVersion": 1,
"label": "saves",
"message": format_number(stats["total_saves"]),
"color": "lightgrey",
},
"contributions": {
"schemaVersion": 1,
"label": "contributions",
"message": str(stats.get("plugin_contributions", 0)),
"color": "green",
},
"upvotes": { "upvotes": {
"schemaVersion": 1, "schemaVersion": 1,
"label": "upvotes", "label": "upvotes",
@@ -960,8 +1002,6 @@ class OpenWebUIStats:
if not (self.gist_token and self.gist_id): if not (self.gist_token and self.gist_id):
return return
delta = self.get_stat_delta(stats)
# Define badge config {key: (label, value, color)} # Define badge config {key: (label, value, color)}
badges_config = { badges_config = {
"downloads": ("Downloads", stats["total_downloads"], "brightgreen"), "downloads": ("Downloads", stats["total_downloads"], "brightgreen"),
@@ -980,7 +1020,7 @@ class OpenWebUIStats:
), ),
"contributions": ( "contributions": (
"Contributions", "Contributions",
stats.get("user", {}).get("contributions", 0), stats.get("plugin_contributions", 0),
"green", "green",
), ),
"posts": ("Posts", stats["total_posts"], "informational"), "posts": ("Posts", stats["total_posts"], "informational"),
@@ -988,22 +1028,12 @@ class OpenWebUIStats:
files_payload = {} files_payload = {}
for key, (label, val, color) in badges_config.items(): for key, (label, val, color) in badges_config.items():
diff = delta.get(key, 0)
if isinstance(diff, dict):
diff = 0 # Avoid dict vs int comparison error with 'posts' key
message = f"{val}"
if diff > 0:
message += f" (+{diff}🚀)"
elif diff < 0:
message += f" ({diff})"
# Build Shields.io endpoint JSON # Build Shields.io endpoint JSON
# 参考: https://shields.io/badges/endpoint-badge # 参考: https://shields.io/badges/endpoint-badge
badge_data = { badge_data = {
"schemaVersion": 1, "schemaVersion": 1,
"label": label, "label": label,
"message": message, "message": f"{val}",
"color": color, "color": color,
} }
@@ -1013,22 +1043,18 @@ class OpenWebUIStats:
} }
# Generate top 6 plugins badges (based on slots p1, p2...) # Generate top 6 plugins badges (based on slots p1, p2...)
post_deltas = delta.get("posts", {}) top_plugin_posts = [
for i, post in enumerate(stats.get("posts", [])[:6]): post for post in stats.get("posts", []) if post.get("is_published_plugin")
][:6]
for i, post in enumerate(top_plugin_posts):
idx = i + 1 idx = i + 1
diff = post_deltas.get(post["slug"], 0)
# Downloads badge
dl_msg = f"{post['downloads']}"
if diff > 0:
dl_msg += f" (+{diff}🚀)"
files_payload[f"badge_p{idx}_dl.json"] = { files_payload[f"badge_p{idx}_dl.json"] = {
"content": json.dumps( "content": json.dumps(
{ {
"schemaVersion": 1, "schemaVersion": 1,
"label": "Downloads", "label": "Downloads",
"message": dl_msg, "message": f"{post['downloads']}",
"color": "brightgreen", "color": "brightgreen",
} }
) )
@@ -1062,19 +1088,13 @@ class OpenWebUIStats:
# 生成所有帖子的个体徽章 (用于详细报表) # 生成所有帖子的个体徽章 (用于详细报表)
for post in stats.get("posts", []): for post in stats.get("posts", []):
slug_hash = self._safe_key(post["slug"]) slug_hash = self._safe_key(post["slug"])
diff = post_deltas.get(post["slug"], 0)
# 1. Downloads
dl_msg = f"{post['downloads']}"
if diff > 0:
dl_msg += f" (+{diff}🚀)"
files_payload[f"badge_post_{slug_hash}_dl.json"] = { files_payload[f"badge_post_{slug_hash}_dl.json"] = {
"content": json.dumps( "content": json.dumps(
{ {
"schemaVersion": 1, "schemaVersion": 1,
"label": "Downloads", "label": "Downloads",
"message": dl_msg, "message": f"{post['downloads']}",
"color": "brightgreen", "color": "brightgreen",
} }
) )
@@ -1163,19 +1183,11 @@ class OpenWebUIStats:
is_post: bool = False, is_post: bool = False,
style: str = "flat", style: str = "flat",
) -> str: ) -> str:
"""获取 Shields.io 徽章 URL (包含增量显示)""" """获取 Shields.io 徽章 URL"""
import urllib.parse import urllib.parse
gist_user = "Fu-Jie" gist_user = "Fu-Jie"
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 ""
if not self.gist_id: if not self.gist_id:
if is_post: if is_post:
return "**-**" return "**-**"
@@ -1185,14 +1197,14 @@ class OpenWebUIStats:
if key == "points": if key == "points":
val = user.get("total_points", 0) val = user.get("total_points", 0)
if key == "contributions": if key == "contributions":
val = user.get("contributions", 0) val = stats.get("plugin_contributions", user.get("contributions", 0))
if key == "posts": if key == "posts":
val = stats.get("total_posts", 0) val = stats.get("total_posts", 0)
if key == "saves": if key == "saves":
val = stats.get("total_saves", 0) val = stats.get("total_saves", 0)
if key.startswith("updated"): if key.startswith("updated"):
return f"🕐 {get_beijing_time().strftime('%Y-%m-%d %H:%M')}" return f"🕐 {get_beijing_time().strftime('%Y-%m-%d %H:%M')}"
return f"**{val}**{_fmt_delta(key)}" return f"**{val}**"
raw_url = f"https://gist.githubusercontent.com/{gist_user}/{self.gist_id}/raw/badge_{key}.json" raw_url = f"https://gist.githubusercontent.com/{gist_user}/{self.gist_id}/raw/badge_{key}.json"
encoded_url = urllib.parse.quote(raw_url, safe="") encoded_url = urllib.parse.quote(raw_url, safe="")
@@ -1208,30 +1220,26 @@ class OpenWebUIStats:
stats: 统计数据 stats: 统计数据
lang: 语言 ("zh" 中文, "en" 英文) lang: 语言 ("zh" 中文, "en" 英文)
""" """
# 获取 Top 6 插件 # 获取 Top 6 已发布插件
top_plugins = stats["posts"][:6] top_plugins = [
post for post in stats["posts"] if post.get("is_published_plugin")
][:6]
delta = self.get_stat_delta(stats) delta = self.get_stat_delta(stats)
def fmt_delta(key: str) -> str:
val = delta.get(key, 0)
if val > 0:
return f" <br><sub>(+{val}🚀)</sub>"
return ""
# 中英文文本 # 中英文文本
texts = { texts = {
"zh": { "zh": {
"title": "## 📊 社区统计", "title": "## 📊 社区统计",
"author_header": "| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |", "author_header": "| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🧩 插件贡献 |",
"header": "| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |", "header": "| 📝 发布 | ⬇️ 插件下载 | 👁️ 插件浏览 | 👍 点赞 | 💾 插件收藏 |",
"top6_title": "### 🔥 热门插件 Top 6", "top6_title": "### 🔥 热门插件 Top 6",
"top6_header": "| 排名 | 插件 | 版本 | 下载 | 浏览 | 📅 更新 |", "top6_header": "| 排名 | 插件 | 版本 | 下载 | 浏览 | 📅 更新 |",
"full_stats": "*完整统计与趋势图请查看 [社区统计报告](./docs/community-stats.zh.md)*", "full_stats": "*完整统计与趋势图请查看 [社区统计报告](./docs/community-stats.zh.md)*",
}, },
"en": { "en": {
"title": "## 📊 Community Stats", "title": "## 📊 Community Stats",
"author_header": "| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |", "author_header": "| 👤 Author | 👥 Followers | ⭐ Points | 🧩 Plugin Contributions |",
"header": "| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |", "header": "| 📝 Posts | ⬇️ Plugin Downloads | 👁️ Plugin Views | 👍 Upvotes | 💾 Plugin Saves |",
"top6_title": "### 🔥 Top 6 Popular Plugins", "top6_title": "### 🔥 Top 6 Popular Plugins",
"top6_header": "| Rank | Plugin | Version | Downloads | Views | 📅 Updated |", "top6_header": "| Rank | Plugin | Version | Downloads | Views | 📅 Updated |",
"full_stats": "*See full stats and charts in [Community Stats Report](./docs/community-stats.md)*", "full_stats": "*See full stats and charts in [Community Stats Report](./docs/community-stats.md)*",